From 763e473b4984c753f75d82b6bf96e77a8364e49f Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Sat, 3 Jan 2026 00:37:38 +0100 Subject: [PATCH 1/2] [tdd] Add AddVarArrayDocblockFromDimFetchAssignRector --- ...ayDocblockFromDimFetchAssignRectorTest.php | 28 ++++ .../Fixture/multiple_assigns.php.inc | 44 +++++ .../Fixture/skp_already_known.php.inc | 16 ++ .../Fixture/some_class.php.inc | 34 ++++ .../config/configured_rule.php | 10 ++ .../NodeFinder/ArrayDimFetchFinder.php | 41 +++++ ...rArrayDocblockFromDimFetchAssignRector.php | 154 ++++++++++++++++++ .../Level/TypeDeclarationDocblocksLevel.php | 2 + src/PhpParser/Node/BetterNodeFinder.php | 2 +- 9 files changed, 330 insertions(+), 1 deletion(-) create mode 100644 rules-tests/TypeDeclarationDocblocks/Rector/Class_/AddVarArrayDocblockFromDimFetchAssignRector/AddVarArrayDocblockFromDimFetchAssignRectorTest.php create mode 100644 rules-tests/TypeDeclarationDocblocks/Rector/Class_/AddVarArrayDocblockFromDimFetchAssignRector/Fixture/multiple_assigns.php.inc create mode 100644 rules-tests/TypeDeclarationDocblocks/Rector/Class_/AddVarArrayDocblockFromDimFetchAssignRector/Fixture/skp_already_known.php.inc create mode 100644 rules-tests/TypeDeclarationDocblocks/Rector/Class_/AddVarArrayDocblockFromDimFetchAssignRector/Fixture/some_class.php.inc create mode 100644 rules-tests/TypeDeclarationDocblocks/Rector/Class_/AddVarArrayDocblockFromDimFetchAssignRector/config/configured_rule.php create mode 100644 rules/TypeDeclarationDocblocks/Rector/Class_/AddVarArrayDocblockFromDimFetchAssignRector.php diff --git a/rules-tests/TypeDeclarationDocblocks/Rector/Class_/AddVarArrayDocblockFromDimFetchAssignRector/AddVarArrayDocblockFromDimFetchAssignRectorTest.php b/rules-tests/TypeDeclarationDocblocks/Rector/Class_/AddVarArrayDocblockFromDimFetchAssignRector/AddVarArrayDocblockFromDimFetchAssignRectorTest.php new file mode 100644 index 00000000000..43915ced242 --- /dev/null +++ b/rules-tests/TypeDeclarationDocblocks/Rector/Class_/AddVarArrayDocblockFromDimFetchAssignRector/AddVarArrayDocblockFromDimFetchAssignRectorTest.php @@ -0,0 +1,28 @@ +doTestFile($filePath); + } + + public static function provideData(): Iterator + { + return self::yieldFilesFromDirectory(__DIR__ . '/Fixture'); + } + + public function provideConfigFilePath(): string + { + return __DIR__ . '/config/configured_rule.php'; + } +} diff --git a/rules-tests/TypeDeclarationDocblocks/Rector/Class_/AddVarArrayDocblockFromDimFetchAssignRector/Fixture/multiple_assigns.php.inc b/rules-tests/TypeDeclarationDocblocks/Rector/Class_/AddVarArrayDocblockFromDimFetchAssignRector/Fixture/multiple_assigns.php.inc new file mode 100644 index 00000000000..5f7efb2e448 --- /dev/null +++ b/rules-tests/TypeDeclarationDocblocks/Rector/Class_/AddVarArrayDocblockFromDimFetchAssignRector/Fixture/multiple_assigns.php.inc @@ -0,0 +1,44 @@ +items[] = 'item1'; + } + + public function run() + { + $this->items[] = 'item2'; + } +} + +?> +----- +items[] = 'item1'; + } + + public function run() + { + $this->items[] = 'item2'; + } +} + +?> diff --git a/rules-tests/TypeDeclarationDocblocks/Rector/Class_/AddVarArrayDocblockFromDimFetchAssignRector/Fixture/skp_already_known.php.inc b/rules-tests/TypeDeclarationDocblocks/Rector/Class_/AddVarArrayDocblockFromDimFetchAssignRector/Fixture/skp_already_known.php.inc new file mode 100644 index 00000000000..a776be789f3 --- /dev/null +++ b/rules-tests/TypeDeclarationDocblocks/Rector/Class_/AddVarArrayDocblockFromDimFetchAssignRector/Fixture/skp_already_known.php.inc @@ -0,0 +1,16 @@ + + */ + private array $items = []; + + public function go() + { + $this->items[] = 'item1'; + } +} diff --git a/rules-tests/TypeDeclarationDocblocks/Rector/Class_/AddVarArrayDocblockFromDimFetchAssignRector/Fixture/some_class.php.inc b/rules-tests/TypeDeclarationDocblocks/Rector/Class_/AddVarArrayDocblockFromDimFetchAssignRector/Fixture/some_class.php.inc new file mode 100644 index 00000000000..858aa98f3ff --- /dev/null +++ b/rules-tests/TypeDeclarationDocblocks/Rector/Class_/AddVarArrayDocblockFromDimFetchAssignRector/Fixture/some_class.php.inc @@ -0,0 +1,34 @@ +items[] = 'item1'; + } +} + +?> +----- +items[] = 'item1'; + } +} + +?> diff --git a/rules-tests/TypeDeclarationDocblocks/Rector/Class_/AddVarArrayDocblockFromDimFetchAssignRector/config/configured_rule.php b/rules-tests/TypeDeclarationDocblocks/Rector/Class_/AddVarArrayDocblockFromDimFetchAssignRector/config/configured_rule.php new file mode 100644 index 00000000000..28ef793994b --- /dev/null +++ b/rules-tests/TypeDeclarationDocblocks/Rector/Class_/AddVarArrayDocblockFromDimFetchAssignRector/config/configured_rule.php @@ -0,0 +1,10 @@ +rule(AddVarArrayDocblockFromDimFetchAssignRector::class); +}; diff --git a/rules/TypeDeclarationDocblocks/NodeFinder/ArrayDimFetchFinder.php b/rules/TypeDeclarationDocblocks/NodeFinder/ArrayDimFetchFinder.php index a0481e794ce..a4047bed05b 100644 --- a/rules/TypeDeclarationDocblocks/NodeFinder/ArrayDimFetchFinder.php +++ b/rules/TypeDeclarationDocblocks/NodeFinder/ArrayDimFetchFinder.php @@ -8,7 +8,9 @@ use PhpParser\Node\Expr; use PhpParser\Node\Expr\ArrayDimFetch; use PhpParser\Node\Expr\Assign; +use PhpParser\Node\Expr\PropertyFetch; use PhpParser\Node\Expr\Variable; +use PhpParser\Node\Stmt\Class_; use PhpParser\Node\Stmt\ClassMethod; use Rector\NodeNameResolver\NodeNameResolver; use Rector\PhpParser\Node\BetterNodeFinder; @@ -49,6 +51,45 @@ public function findDimFetchAssignToVariableName(ClassMethod $classMethod, strin return $exprs; } + /** + * Look for bare assigns, $this->someProperty[] = ... + * @return Expr[] + */ + public function findDimFetchAssignToPropertyName(Class_ $class, string $variableName): array + { + $assigns = $this->betterNodeFinder->findInstancesOfScoped($class->getMethods(), Assign::class); + + $exprs = []; + foreach ($assigns as $assign) { + if (! $assign->var instanceof ArrayDimFetch) { + continue; + } + + $arrayDimFetch = $assign->var; + if ($arrayDimFetch->dim instanceof Expr) { + continue; + } + + if (! $arrayDimFetch->var instanceof PropertyFetch) { + continue; + } + + $propertyFetch = $arrayDimFetch->var; + + if (! $this->nodeNameResolver->isName($propertyFetch->var, 'this')) { + continue; + } + + if (! $this->nodeNameResolver->isName($propertyFetch->name, $variableName)) { + continue; + } + + $exprs[] = $assign->expr; + } + + return $exprs; + } + /** * @return ArrayDimFetch[] */ diff --git a/rules/TypeDeclarationDocblocks/Rector/Class_/AddVarArrayDocblockFromDimFetchAssignRector.php b/rules/TypeDeclarationDocblocks/Rector/Class_/AddVarArrayDocblockFromDimFetchAssignRector.php new file mode 100644 index 00000000000..4b55ffa52f4 --- /dev/null +++ b/rules/TypeDeclarationDocblocks/Rector/Class_/AddVarArrayDocblockFromDimFetchAssignRector.php @@ -0,0 +1,154 @@ +items[] = [ + 'name' => 'John', + ]; + } +} +CODE_SAMPLE + , + <<<'CODE_SAMPLE' +final class SomeClass +{ + /** + * @var array> + */ + private array $items = []; + + public function run() + { + $this->items[] = [ + 'name' => 'John', + ]; + } +} +CODE_SAMPLE + ), + ]); + } + + /** + * @return array> + */ + public function getNodeTypes(): array + { + return [Class_::class]; + } + + /** + * @param Class_ $node + */ + public function refactor(Node $node): ?Node + { + $hasChanged = false; + + foreach ($node->getProperties() as $property) { + if (! $this->isPropertyTypeArray($property)) { + continue; + } + + $propertyPhpDocInfo = $this->phpDocInfoFactory->createFromNodeOrEmpty($property); + + if ($this->usefulArrayTagNodeAnalyzer->isUsefulArrayTag($propertyPhpDocInfo->getVarTagValueNode())) { + continue; + } + + $propertyName = $this->getName($property); + + $assignedExprs = $this->arrayDimFetchFinder->findDimFetchAssignToPropertyName($node, $propertyName); + + $assignedExprTypes = []; + foreach ($assignedExprs as $assignedExpr) { + $assignedExprTypes[] = $this->getType($assignedExpr); + } + + // nothing to add + if ($assignedExprTypes === []) { + continue; + } + + $uniqueGeneralizedUnionTypes = $this->typeFactory->uniquateTypes($assignedExprTypes); + + if (count($uniqueGeneralizedUnionTypes) > 1) { + $generalizedUnionedTypes = new UnionType($uniqueGeneralizedUnionTypes); + } else { + $generalizedUnionedTypes = $uniqueGeneralizedUnionTypes[0]; + } + + $arrayReturnType = new ArrayType(new MixedType(), $generalizedUnionedTypes); + + $hasPropertyChanged = $this->nodeDocblockTypeDecorator->decorateGenericIterableVarType( + $arrayReturnType, + $propertyPhpDocInfo, + $property + ); + + if ($hasPropertyChanged === false) { + continue; + } + + $hasChanged = true; + } + + if (! $hasChanged) { + return null; + } + + return $node; + } + + private function isPropertyTypeArray(Property $property): bool + { + if (! $property->type instanceof Identifier) { + return false; + } + + return $this->isName($property->type, 'array'); + } +} diff --git a/src/Config/Level/TypeDeclarationDocblocksLevel.php b/src/Config/Level/TypeDeclarationDocblocksLevel.php index 62b7581ecf0..d95a2f3b0ff 100644 --- a/src/Config/Level/TypeDeclarationDocblocksLevel.php +++ b/src/Config/Level/TypeDeclarationDocblocksLevel.php @@ -9,6 +9,7 @@ use Rector\TypeDeclaration\Rector\ClassMethod\AddReturnArrayDocblockBasedOnArrayMapRector; use Rector\TypeDeclaration\Rector\ClassMethod\AddReturnDocblockForScalarArrayFromAssignsRector; use Rector\TypeDeclarationDocblocks\Rector\Class_\AddReturnDocblockDataProviderRector; +use Rector\TypeDeclarationDocblocks\Rector\Class_\AddVarArrayDocblockFromDimFetchAssignRector; use Rector\TypeDeclarationDocblocks\Rector\Class_\ClassMethodArrayDocblockParamFromLocalCallsRector; use Rector\TypeDeclarationDocblocks\Rector\Class_\DocblockVarArrayFromGetterReturnRector; use Rector\TypeDeclarationDocblocks\Rector\Class_\DocblockVarArrayFromPropertyDefaultsRector; @@ -57,6 +58,7 @@ final class TypeDeclarationDocblocksLevel // property var DocblockVarFromParamDocblockInConstructorRector::class, DocblockVarArrayFromGetterReturnRector::class, + AddVarArrayDocblockFromDimFetchAssignRector::class, // return DocblockGetterReturnArrayFromPropertyDocblockVarRector::class, diff --git a/src/PhpParser/Node/BetterNodeFinder.php b/src/PhpParser/Node/BetterNodeFinder.php index 86ec1521b04..79093f843b6 100644 --- a/src/PhpParser/Node/BetterNodeFinder.php +++ b/src/PhpParser/Node/BetterNodeFinder.php @@ -239,7 +239,7 @@ public function findInstancesOfScoped(array $nodes, string|array $types): array $this->simpleCallableNodeTraverser->traverseNodesWithCallable( $nodes, static function (Node $subNode) use ($types, &$foundNodes): ?int { - if ($subNode instanceof Class_ || $subNode instanceof FunctionLike) { + if ($subNode instanceof Class_ || ($subNode instanceof FunctionLike && ! $subNode instanceof Stmt\ClassMethod)) { return NodeVisitor::DONT_TRAVERSE_CURRENT_AND_CHILDREN; } From 382f5729e8813488d309712f71e69ceade4439e8 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Mon, 5 Jan 2026 08:37:11 +0000 Subject: [PATCH 2/2] [ci-review] Rector Rectify --- src/PhpParser/Node/BetterNodeFinder.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/PhpParser/Node/BetterNodeFinder.php b/src/PhpParser/Node/BetterNodeFinder.php index 79093f843b6..631c06763bf 100644 --- a/src/PhpParser/Node/BetterNodeFinder.php +++ b/src/PhpParser/Node/BetterNodeFinder.php @@ -4,6 +4,7 @@ namespace Rector\PhpParser\Node; +use PhpParser\Node\Stmt\ClassMethod; use PhpParser\Node; use PhpParser\Node\Expr\Variable; use PhpParser\Node\Expr\Yield_; @@ -239,7 +240,7 @@ public function findInstancesOfScoped(array $nodes, string|array $types): array $this->simpleCallableNodeTraverser->traverseNodesWithCallable( $nodes, static function (Node $subNode) use ($types, &$foundNodes): ?int { - if ($subNode instanceof Class_ || ($subNode instanceof FunctionLike && ! $subNode instanceof Stmt\ClassMethod)) { + if ($subNode instanceof Class_ || ($subNode instanceof FunctionLike && ! $subNode instanceof ClassMethod)) { return NodeVisitor::DONT_TRAVERSE_CURRENT_AND_CHILDREN; }