From c0a57e0c72155c5736f640d647715f979e861975 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Wed, 1 Jul 2026 23:54:04 +0200 Subject: [PATCH] [TypeDeclarationDocblocks] Add NarrowArrayCollectionUnionReturnDocblockRector to turn Type[]|ArrayCollection @return union into generic ArrayCollection --- .../Fixture/array_collection_union.php.inc | 41 +++++ .../Fixture/reversed_order.php.inc | 41 +++++ .../Fixture/skip_already_generic.php.inc | 18 ++ .../Fixture/skip_non_collection_union.php.inc | 16 ++ ...ollectionUnionReturnDocblockRectorTest.php | 28 +++ .../config/configured_rule.php | 9 + ...rayCollectionUnionReturnDocblockRector.php | 163 ++++++++++++++++++ .../Level/TypeDeclarationDocblocksLevel.php | 2 + 8 files changed, 318 insertions(+) create mode 100644 rules-tests/TypeDeclarationDocblocks/Rector/ClassMethod/NarrowArrayCollectionUnionReturnDocblockRector/Fixture/array_collection_union.php.inc create mode 100644 rules-tests/TypeDeclarationDocblocks/Rector/ClassMethod/NarrowArrayCollectionUnionReturnDocblockRector/Fixture/reversed_order.php.inc create mode 100644 rules-tests/TypeDeclarationDocblocks/Rector/ClassMethod/NarrowArrayCollectionUnionReturnDocblockRector/Fixture/skip_already_generic.php.inc create mode 100644 rules-tests/TypeDeclarationDocblocks/Rector/ClassMethod/NarrowArrayCollectionUnionReturnDocblockRector/Fixture/skip_non_collection_union.php.inc create mode 100644 rules-tests/TypeDeclarationDocblocks/Rector/ClassMethod/NarrowArrayCollectionUnionReturnDocblockRector/NarrowArrayCollectionUnionReturnDocblockRectorTest.php create mode 100644 rules-tests/TypeDeclarationDocblocks/Rector/ClassMethod/NarrowArrayCollectionUnionReturnDocblockRector/config/configured_rule.php create mode 100644 rules/TypeDeclarationDocblocks/Rector/ClassMethod/NarrowArrayCollectionUnionReturnDocblockRector.php diff --git a/rules-tests/TypeDeclarationDocblocks/Rector/ClassMethod/NarrowArrayCollectionUnionReturnDocblockRector/Fixture/array_collection_union.php.inc b/rules-tests/TypeDeclarationDocblocks/Rector/ClassMethod/NarrowArrayCollectionUnionReturnDocblockRector/Fixture/array_collection_union.php.inc new file mode 100644 index 00000000000..ea8cb54cf64 --- /dev/null +++ b/rules-tests/TypeDeclarationDocblocks/Rector/ClassMethod/NarrowArrayCollectionUnionReturnDocblockRector/Fixture/array_collection_union.php.inc @@ -0,0 +1,41 @@ +successful; + } +} + +?> +----- + + */ + public function getSuccessful(): ArrayCollection + { + return $this->successful; + } +} + +?> diff --git a/rules-tests/TypeDeclarationDocblocks/Rector/ClassMethod/NarrowArrayCollectionUnionReturnDocblockRector/Fixture/reversed_order.php.inc b/rules-tests/TypeDeclarationDocblocks/Rector/ClassMethod/NarrowArrayCollectionUnionReturnDocblockRector/Fixture/reversed_order.php.inc new file mode 100644 index 00000000000..63f576bdd3e --- /dev/null +++ b/rules-tests/TypeDeclarationDocblocks/Rector/ClassMethod/NarrowArrayCollectionUnionReturnDocblockRector/Fixture/reversed_order.php.inc @@ -0,0 +1,41 @@ +items; + } +} + +?> +----- + + */ + public function getItems(): Collection + { + return $this->items; + } +} + +?> diff --git a/rules-tests/TypeDeclarationDocblocks/Rector/ClassMethod/NarrowArrayCollectionUnionReturnDocblockRector/Fixture/skip_already_generic.php.inc b/rules-tests/TypeDeclarationDocblocks/Rector/ClassMethod/NarrowArrayCollectionUnionReturnDocblockRector/Fixture/skip_already_generic.php.inc new file mode 100644 index 00000000000..95579766689 --- /dev/null +++ b/rules-tests/TypeDeclarationDocblocks/Rector/ClassMethod/NarrowArrayCollectionUnionReturnDocblockRector/Fixture/skip_already_generic.php.inc @@ -0,0 +1,18 @@ + + */ + public function getSuccessful(): ArrayCollection + { + return $this->successful; + } +} diff --git a/rules-tests/TypeDeclarationDocblocks/Rector/ClassMethod/NarrowArrayCollectionUnionReturnDocblockRector/Fixture/skip_non_collection_union.php.inc b/rules-tests/TypeDeclarationDocblocks/Rector/ClassMethod/NarrowArrayCollectionUnionReturnDocblockRector/Fixture/skip_non_collection_union.php.inc new file mode 100644 index 00000000000..e1cc0afec88 --- /dev/null +++ b/rules-tests/TypeDeclarationDocblocks/Rector/ClassMethod/NarrowArrayCollectionUnionReturnDocblockRector/Fixture/skip_non_collection_union.php.inc @@ -0,0 +1,16 @@ +values; + } +} diff --git a/rules-tests/TypeDeclarationDocblocks/Rector/ClassMethod/NarrowArrayCollectionUnionReturnDocblockRector/NarrowArrayCollectionUnionReturnDocblockRectorTest.php b/rules-tests/TypeDeclarationDocblocks/Rector/ClassMethod/NarrowArrayCollectionUnionReturnDocblockRector/NarrowArrayCollectionUnionReturnDocblockRectorTest.php new file mode 100644 index 00000000000..cf0ed81b1db --- /dev/null +++ b/rules-tests/TypeDeclarationDocblocks/Rector/ClassMethod/NarrowArrayCollectionUnionReturnDocblockRector/NarrowArrayCollectionUnionReturnDocblockRectorTest.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/ClassMethod/NarrowArrayCollectionUnionReturnDocblockRector/config/configured_rule.php b/rules-tests/TypeDeclarationDocblocks/Rector/ClassMethod/NarrowArrayCollectionUnionReturnDocblockRector/config/configured_rule.php new file mode 100644 index 00000000000..5dde9806eca --- /dev/null +++ b/rules-tests/TypeDeclarationDocblocks/Rector/ClassMethod/NarrowArrayCollectionUnionReturnDocblockRector/config/configured_rule.php @@ -0,0 +1,9 @@ +withRules([NarrowArrayCollectionUnionReturnDocblockRector::class]); diff --git a/rules/TypeDeclarationDocblocks/Rector/ClassMethod/NarrowArrayCollectionUnionReturnDocblockRector.php b/rules/TypeDeclarationDocblocks/Rector/ClassMethod/NarrowArrayCollectionUnionReturnDocblockRector.php new file mode 100644 index 00000000000..4d0c8d8ec3d --- /dev/null +++ b/rules/TypeDeclarationDocblocks/Rector/ClassMethod/NarrowArrayCollectionUnionReturnDocblockRector.php @@ -0,0 +1,163 @@ +"', + [ + new CodeSample( + <<<'CODE_SAMPLE' +use Doctrine\Common\Collections\ArrayCollection; + +class SomeClass +{ + /** + * @return LeadEventLog[]|ArrayCollection + */ + public function getSuccessful(): ArrayCollection + { + return $this->successful; + } +} +CODE_SAMPLE + , + <<<'CODE_SAMPLE' +use Doctrine\Common\Collections\ArrayCollection; + +class SomeClass +{ + /** + * @return ArrayCollection + */ + public function getSuccessful(): ArrayCollection + { + return $this->successful; + } +} +CODE_SAMPLE + ), + ], + ); + } + + public function getNodeTypes(): array + { + return [ClassMethod::class, Function_::class]; + } + + /** + * @param ClassMethod|Function_ $node + */ + public function refactor(Node $node): null|ClassMethod|Function_ + { + $phpDocInfo = $this->phpDocInfoFactory->createFromNode($node); + if (! $phpDocInfo instanceof PhpDocInfo) { + return null; + } + + $returnTagValueNode = $phpDocInfo->getReturnTagValue(); + if (! $returnTagValueNode instanceof ReturnTagValueNode) { + return null; + } + + if (! $returnTagValueNode->type instanceof UnionTypeNode) { + return null; + } + + $genericTypeNode = $this->matchGenericCollectionTypeNode($returnTagValueNode->type); + if (! $genericTypeNode instanceof GenericTypeNode) { + return null; + } + + $returnTagValueNode->type = $genericTypeNode; + + $this->docBlockUpdater->updateRefactoredNodeWithPhpDocInfo($node); + + return $node; + } + + private function matchGenericCollectionTypeNode(UnionTypeNode $unionTypeNode): ?GenericTypeNode + { + if (count($unionTypeNode->types) !== 2) { + return null; + } + + $arrayItemTypeNode = null; + $collectionIdentifierTypeNode = null; + + foreach ($unionTypeNode->types as $typeNode) { + if ($typeNode instanceof ArrayTypeNode && $typeNode->type instanceof IdentifierTypeNode) { + $arrayItemTypeNode = $typeNode->type; + continue; + } + + if ($typeNode instanceof IdentifierTypeNode && $this->isCollectionIdentifier($typeNode)) { + $collectionIdentifierTypeNode = $typeNode; + } + } + + if (! $arrayItemTypeNode instanceof IdentifierTypeNode) { + return null; + } + + if (! $collectionIdentifierTypeNode instanceof IdentifierTypeNode) { + return null; + } + + $genericTypeNodes = [new IdentifierTypeNode('int'), $arrayItemTypeNode]; + return new GenericTypeNode($collectionIdentifierTypeNode, $genericTypeNodes); + } + + private function isCollectionIdentifier(IdentifierTypeNode $identifierTypeNode): bool + { + $shortName = $this->resolveShortName($identifierTypeNode->name); + return in_array($shortName, self::COLLECTION_SHORT_NAMES, true); + } + + private function resolveShortName(string $name): string + { + $name = ltrim($name, '\\'); + + $lastBackslashPosition = strrpos($name, '\\'); + if ($lastBackslashPosition === false) { + return $name; + } + + return substr($name, $lastBackslashPosition + 1); + } +} diff --git a/src/Config/Level/TypeDeclarationDocblocksLevel.php b/src/Config/Level/TypeDeclarationDocblocksLevel.php index 42155b30aaf..06f732c52cf 100644 --- a/src/Config/Level/TypeDeclarationDocblocksLevel.php +++ b/src/Config/Level/TypeDeclarationDocblocksLevel.php @@ -24,6 +24,7 @@ use Rector\TypeDeclarationDocblocks\Rector\ClassMethod\AddReturnDocblockForJsonArrayRector; use Rector\TypeDeclarationDocblocks\Rector\ClassMethod\DocblockGetterReturnArrayFromPropertyDocblockVarRector; use Rector\TypeDeclarationDocblocks\Rector\ClassMethod\DocblockReturnArrayFromDirectArrayInstanceRector; +use Rector\TypeDeclarationDocblocks\Rector\ClassMethod\NarrowArrayCollectionUnionReturnDocblockRector; final class TypeDeclarationDocblocksLevel { @@ -62,6 +63,7 @@ final class TypeDeclarationDocblocksLevel // return DocblockGetterReturnArrayFromPropertyDocblockVarRector::class, + NarrowArrayCollectionUnionReturnDocblockRector::class, // run latter after other rules, as more generic AddReturnDocblockForDimFetchArrayFromAssignsRector::class,