diff --git a/conf/bleedingEdge.neon b/conf/bleedingEdge.neon index ebc00780c4..73d103c6ce 100644 --- a/conf/bleedingEdge.neon +++ b/conf/bleedingEdge.neon @@ -13,3 +13,4 @@ parameters: checkLogicalOrConstantCondition: true checkMissingTemplateTypeInParameter: true wrongVarUsage: true + arrayDeconstruction: true diff --git a/conf/config.level3.neon b/conf/config.level3.neon index afe67c44f9..884bc1f276 100644 --- a/conf/config.level3.neon +++ b/conf/config.level3.neon @@ -17,6 +17,10 @@ rules: - PHPStan\Rules\Variables\ThrowTypeRule - PHPStan\Rules\Variables\VariableCloningRule +conditionalTags: + PHPStan\Rules\Arrays\ArrayDeconstructionRule: + phpstan.rules.rule: %featureToggles.arrayDeconstruction% + parameters: checkPhpDocMethodSignatures: true @@ -28,6 +32,9 @@ services: tags: - phpstan.rules.rule + - + class: PHPStan\Rules\Arrays\ArrayDeconstructionRule + - class: PHPStan\Rules\Arrays\InvalidKeyInArrayDimFetchRule arguments: diff --git a/conf/config.neon b/conf/config.neon index c80730a5a4..e62476e802 100644 --- a/conf/config.neon +++ b/conf/config.neon @@ -26,6 +26,7 @@ parameters: checkLogicalOrConstantCondition: false checkMissingTemplateTypeInParameter: false wrongVarUsage: false + arrayDeconstruction: false fileExtensions: - php checkAlwaysTrueCheckTypeFunctionCall: false @@ -178,7 +179,8 @@ parametersSchema: checkLogicalAndConstantCondition: bool(), checkLogicalOrConstantCondition: bool(), checkMissingTemplateTypeInParameter: bool(), - wrongVarUsage: bool() + wrongVarUsage: bool(), + arrayDeconstruction: bool() ]) fileExtensions: listOf(string()) checkAlwaysTrueCheckTypeFunctionCall: bool() diff --git a/src/Rules/Arrays/ArrayDeconstructionRule.php b/src/Rules/Arrays/ArrayDeconstructionRule.php new file mode 100644 index 0000000000..c777e883ae --- /dev/null +++ b/src/Rules/Arrays/ArrayDeconstructionRule.php @@ -0,0 +1,130 @@ + + */ +class ArrayDeconstructionRule implements Rule +{ + + private RuleLevelHelper $ruleLevelHelper; + + private NonexistentOffsetInArrayDimFetchCheck $nonexistentOffsetInArrayDimFetchCheck; + + public function __construct( + RuleLevelHelper $ruleLevelHelper, + NonexistentOffsetInArrayDimFetchCheck $nonexistentOffsetInArrayDimFetchCheck + ) + { + $this->ruleLevelHelper = $ruleLevelHelper; + $this->nonexistentOffsetInArrayDimFetchCheck = $nonexistentOffsetInArrayDimFetchCheck; + } + + public function getNodeType(): string + { + return Assign::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!$node->var instanceof Node\Expr\List_ && !$node->var instanceof Node\Expr\Array_) { + return []; + } + + return $this->getErrors( + $scope, + $node->var, + $node->expr + ); + } + + /** + * @param Node\Expr\List_|Node\Expr\Array_ $var + * @return RuleError[] + */ + private function getErrors(Scope $scope, Expr $var, Expr $expr): array + { + $exprTypeResult = $this->ruleLevelHelper->findTypeToCheck( + $scope, + $expr, + '', + static function (Type $varType): bool { + return $varType->isArray()->yes(); + } + ); + $exprType = $exprTypeResult->getType(); + if ($exprType instanceof ErrorType) { + return []; + } + if (!$exprType->isArray()->yes()) { + return [ + RuleErrorBuilder::message(sprintf('Cannot use array destructuring on %s.', $exprType->describe(VerbosityLevel::typeOnly())))->build(), + ]; + } + + $errors = []; + $i = 0; + foreach ($var->items as $item) { + if ($item === null) { + $i++; + continue; + } + + $keyExpr = null; + if ($item->key === null) { + $keyType = new ConstantIntegerType($i); + $keyExpr = new Node\Scalar\LNumber($i); + } else { + $keyType = $scope->getType($item->key); + if ($keyType instanceof ConstantIntegerType) { + $keyExpr = new LNumber($keyType->getValue()); + } elseif ($keyType instanceof ConstantStringType) { + $keyExpr = new Node\Scalar\String_($keyType->getValue()); + } + } + + $itemErrors = $this->nonexistentOffsetInArrayDimFetchCheck->check( + $scope, + $expr, + '', + $keyType + ); + $errors = array_merge($errors, $itemErrors); + + if ($keyExpr === null) { + $i++; + continue; + } + + if (!$item->value instanceof Node\Expr\List_ && !$item->value instanceof Node\Expr\Array_) { + $i++; + continue; + } + + $errors = array_merge($errors, $this->getErrors( + $scope, + $item->value, + new Expr\ArrayDimFetch($expr, $keyExpr) + )); + } + + return $errors; + } + +} diff --git a/tests/PHPStan/Levels/LevelsIntegrationTest.php b/tests/PHPStan/Levels/LevelsIntegrationTest.php index 141c11e0b9..f70127daea 100644 --- a/tests/PHPStan/Levels/LevelsIntegrationTest.php +++ b/tests/PHPStan/Levels/LevelsIntegrationTest.php @@ -36,6 +36,7 @@ public function dataTopics(): array ['arrayAccess'], ['typehints'], ['coalesce'], + ['arrayDestructuring'], ]; } diff --git a/tests/PHPStan/Levels/data/arrayDestructuring-3.json b/tests/PHPStan/Levels/data/arrayDestructuring-3.json new file mode 100644 index 0000000000..710133680d --- /dev/null +++ b/tests/PHPStan/Levels/data/arrayDestructuring-3.json @@ -0,0 +1,12 @@ +[ + { + "message": "Cannot use array destructuring on iterable.", + "line": 23, + "ignorable": true + }, + { + "message": "Offset 3 does not exist on array('a', 'b', 'c').", + "line": 30, + "ignorable": true + } +] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/arrayDestructuring-8.json b/tests/PHPStan/Levels/data/arrayDestructuring-8.json new file mode 100644 index 0000000000..7842bce806 --- /dev/null +++ b/tests/PHPStan/Levels/data/arrayDestructuring-8.json @@ -0,0 +1,7 @@ +[ + { + "message": "Cannot use array destructuring on array|null.", + "line": 15, + "ignorable": true + } +] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/arrayDestructuring.php b/tests/PHPStan/Levels/data/arrayDestructuring.php new file mode 100644 index 0000000000..5d7e61ea93 --- /dev/null +++ b/tests/PHPStan/Levels/data/arrayDestructuring.php @@ -0,0 +1,33 @@ + $it + */ + public function doBar(iterable $it): void + { + [$a] = $it; + } + + public function doBaz(): void + { + $array = ['a', 'b', 'c']; + [$a] = $array; + [$a, , , $d] = $array; + } + +} diff --git a/tests/PHPStan/Rules/Arrays/ArrayDeconstructionRuleTest.php b/tests/PHPStan/Rules/Arrays/ArrayDeconstructionRuleTest.php new file mode 100644 index 0000000000..83459fc4eb --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/ArrayDeconstructionRuleTest.php @@ -0,0 +1,47 @@ + + */ +class ArrayDeconstructionRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + $ruleLevelHelper = new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false); + + return new ArrayDeconstructionRule( + $ruleLevelHelper, + new NonexistentOffsetInArrayDimFetchCheck($ruleLevelHelper, true) + ); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/array-destructuring.php'], [ + [ + 'Cannot use array destructuring on array|null.', + 11, + ], + [ + 'Offset 0 does not exist on array().', + 12, + ], + [ + 'Cannot use array destructuring on stdClass.', + 13, + ], + [ + 'Offset 2 does not exist on array(1, 2).', + 15, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Arrays/data/array-destructuring.php b/tests/PHPStan/Rules/Arrays/data/array-destructuring.php new file mode 100644 index 0000000000..ba9076c684 --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/array-destructuring.php @@ -0,0 +1,18 @@ +