From b065c501f36ad612cb6b081d83ece5898dc694c4 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Fri, 13 Jun 2025 15:16:19 +0200 Subject: [PATCH 1/2] add fixture --- .../Fixture/narrow_intersection.php.inc | 32 +++++++++++++++++++ .../Source/SomeIterableObject.php | 8 +++++ 2 files changed, 40 insertions(+) create mode 100644 rules-tests/TypedCollections/Rector/Property/NarrowPropertyUnionToCollectionRector/Fixture/narrow_intersection.php.inc create mode 100644 rules-tests/TypedCollections/Rector/Property/NarrowPropertyUnionToCollectionRector/Source/SomeIterableObject.php diff --git a/rules-tests/TypedCollections/Rector/Property/NarrowPropertyUnionToCollectionRector/Fixture/narrow_intersection.php.inc b/rules-tests/TypedCollections/Rector/Property/NarrowPropertyUnionToCollectionRector/Fixture/narrow_intersection.php.inc new file mode 100644 index 00000000..2a4f9a0a --- /dev/null +++ b/rules-tests/TypedCollections/Rector/Property/NarrowPropertyUnionToCollectionRector/Fixture/narrow_intersection.php.inc @@ -0,0 +1,32 @@ +) + */ + public $items; +} + +?> +----- + + */ + public $items; +} + +?> diff --git a/rules-tests/TypedCollections/Rector/Property/NarrowPropertyUnionToCollectionRector/Source/SomeIterableObject.php b/rules-tests/TypedCollections/Rector/Property/NarrowPropertyUnionToCollectionRector/Source/SomeIterableObject.php new file mode 100644 index 00000000..a530fc15 --- /dev/null +++ b/rules-tests/TypedCollections/Rector/Property/NarrowPropertyUnionToCollectionRector/Source/SomeIterableObject.php @@ -0,0 +1,8 @@ + Date: Fri, 13 Jun 2025 15:25:28 +0200 Subject: [PATCH 2/2] add intersection support --- .../Fixture/narrow_intersection.php.inc | 5 +- .../UnionCollectionTagValueNodeNarrower.php | 132 +++++++++++++----- .../NarrowPropertyUnionToCollectionRector.php | 1 - 3 files changed, 100 insertions(+), 38 deletions(-) diff --git a/rules-tests/TypedCollections/Rector/Property/NarrowPropertyUnionToCollectionRector/Fixture/narrow_intersection.php.inc b/rules-tests/TypedCollections/Rector/Property/NarrowPropertyUnionToCollectionRector/Fixture/narrow_intersection.php.inc index 2a4f9a0a..e94abfbb 100644 --- a/rules-tests/TypedCollections/Rector/Property/NarrowPropertyUnionToCollectionRector/Fixture/narrow_intersection.php.inc +++ b/rules-tests/TypedCollections/Rector/Property/NarrowPropertyUnionToCollectionRector/Fixture/narrow_intersection.php.inc @@ -19,12 +19,13 @@ final class NarrowIntersection namespace Rector\Doctrine\Tests\TypedCollections\Rector\Property\NarrowPropertyUnionToCollectionRector\Fixture; -use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; +use Rector\Doctrine\Tests\TypedCollections\Rector\Property\NarrowPropertyUnionToCollectionRector\Source\SomeIterableObject; final class NarrowIntersection { /** - * @var Collection + * @var \Doctrine\Common\Collections\Collection */ public $items; } diff --git a/rules/TypedCollections/DocBlockProcessor/UnionCollectionTagValueNodeNarrower.php b/rules/TypedCollections/DocBlockProcessor/UnionCollectionTagValueNodeNarrower.php index 15f81d56..f1329157 100644 --- a/rules/TypedCollections/DocBlockProcessor/UnionCollectionTagValueNodeNarrower.php +++ b/rules/TypedCollections/DocBlockProcessor/UnionCollectionTagValueNodeNarrower.php @@ -35,37 +35,46 @@ public function narrow( } if ($tagValueNode->type instanceof NullableTypeNode) { - if ($hasNativeCollectionType) { - $tagValueNode->type = $tagValueNode->type->type; - $tagValueNode->setAttribute(PhpDocAttributeKey::ORIG_NODE, null); - - $collectionType = $tagValueNode->type; - $this->addIntKeyIfMissing($collectionType); - - if ($collectionType->type instanceof IdentifierTypeNode && ! str_ends_with( - $collectionType->type->name, - 'Collection' - )) { - $collectionType->type = new FullyQualifiedIdentifierTypeNode(DoctrineClass::COLLECTION); - } + return $this->processNullableTypeNode($hasNativeCollectionType, $tagValueNode); + } - return true; - } + return $this->processIterableAndUnionTypeNode($tagValueNode, $hasNativeCollectionType); + } - return false; + private function addIntKeyIfMissing(TypeNode|IdentifierTypeNode $collectionType): void + { + if (! $collectionType instanceof GenericTypeNode) { + return; + } + + if (count($collectionType->genericTypes) !== 1) { + return; } + // add default key type + $collectionType->genericTypes = array_merge([new IdentifierTypeNode('int')], $collectionType->genericTypes); + } + + private function processIterableAndUnionTypeNode( + ParamTagValueNode|VarTagValueNode|ReturnTagValueNode $tagValueNode, + bool $hasNativeCollectionType + ): bool { if (! $tagValueNode->type instanceof UnionTypeNode && ! $tagValueNode->type instanceof IntersectionTypeNode) { return false; } $hasChanged = false; - $hasCollectionType = false; $hasArrayType = false; $arrayTypeNode = null; $arrayKeyTypeNode = null; - foreach ($tagValueNode->type->types as $key => $unionedTypeNode) { + // has collection docblock type? + $hasCollectionType = $this->hasCollectionDocblockType($tagValueNode->type); + $hasGenericIterableType = false; + + $complexTypeNode = $tagValueNode->type; + + foreach ($complexTypeNode->types as $key => $unionedTypeNode) { // possibly array if ($unionedTypeNode instanceof GenericTypeNode && $unionedTypeNode->type->name === 'array') { $hasArrayType = true; @@ -88,29 +97,29 @@ public function narrow( } // remove |null, if property type is present as Collection - if ($unionedTypeNode instanceof IdentifierTypeNode && $unionedTypeNode->name === 'null' && $hasNativeCollectionType) { - - $hasChanged = true; - unset($tagValueNode->type->types[$key]); - continue; - } + if ($unionedTypeNode instanceof IdentifierTypeNode) { + if ($unionedTypeNode->name === 'null' && $hasNativeCollectionType) { + $hasChanged = true; + unset($tagValueNode->type->types[$key]); + continue; + } - if ($unionedTypeNode instanceof IdentifierTypeNode && in_array( - $unionedTypeNode->name, - ['Collection', 'ArrayCollection'] - )) { if ($unionedTypeNode->name === 'ArrayCollection') { $tagValueNode->type->types[$key] = new IdentifierTypeNode('\\' . DoctrineClass::COLLECTION); $hasChanged = true; } - - $hasCollectionType = true; } // narrow array collection to more generic collection - if ($unionedTypeNode instanceof GenericTypeNode && $unionedTypeNode->type->name === 'ArrayCollection') { + if ($unionedTypeNode instanceof GenericTypeNode && in_array( + $unionedTypeNode->type->name, + ['ArrayCollection', 'iterable'], + true + )) { $unionedTypeNode->type = new IdentifierTypeNode('\\' . DoctrineClass::COLLECTION); $hasChanged = true; + + $hasGenericIterableType = true; } } @@ -118,6 +127,16 @@ public function narrow( return false; } + // remove duplicated Collection and Collection generics type + if ($hasCollectionType && $hasGenericIterableType) { + foreach ($complexTypeNode->types as $key => $singleType) { + if ($this->isCollectionIdentifierTypeNode($singleType)) { + // remove as has generic iterable type already + unset($complexTypeNode->types[$key]); + } + } + } + if ($arrayTypeNode instanceof TypeNode) { $tagValueNode->type = new GenericTypeNode(new IdentifierTypeNode('\\' . DoctrineClass::COLLECTION), [ $arrayKeyTypeNode ?? new IdentifierTypeNode('int'), @@ -130,6 +149,7 @@ public function narrow( } if ($hasNativeCollectionType && $type->name === 'null') { + // remove null type unset($tagValueNode->type->types[$key]); continue; } @@ -154,11 +174,53 @@ public function narrow( return true; } - private function addIntKeyIfMissing(TypeNode|IdentifierTypeNode $collectionType): void + private function hasCollectionDocblockType(UnionTypeNode|IntersectionTypeNode $complexTypeNode): bool { - if ($collectionType instanceof GenericTypeNode && count($collectionType->genericTypes) === 1) { - // add default key type - $collectionType->genericTypes = array_merge([new IdentifierTypeNode('int')], $collectionType->genericTypes); + foreach ($complexTypeNode->types as $singleType) { + if ($this->isCollectionIdentifierTypeNode($singleType)) { + return true; + } + } + + return false; + } + + private function processNullableTypeNode( + bool $hasNativeCollectionType, + ParamTagValueNode|VarTagValueNode|ReturnTagValueNode $tagValueNode + ): bool { + if ($hasNativeCollectionType === false) { + return false; } + + // unwrap nullable type + $tagValueNode->type = $tagValueNode->type->type; + + // invoke reprint + $tagValueNode->setAttribute(PhpDocAttributeKey::ORIG_NODE, null); + + $collectionType = $tagValueNode->type; + $this->addIntKeyIfMissing($collectionType); + + if ($collectionType->type instanceof IdentifierTypeNode && ! str_ends_with( + $collectionType->type->name, + 'Collection' + )) { + $collectionType->type = new FullyQualifiedIdentifierTypeNode(DoctrineClass::COLLECTION); + } + + return true; + } + + private function isCollectionIdentifierTypeNode(TypeNode $typeNode): bool + { + if (! $typeNode instanceof IdentifierTypeNode) { + return false; + } + + return in_array( + $typeNode->name, + [DoctrineClass::COLLECTION, DoctrineClass::ARRAY_COLLECTION, 'Collection', 'ArrayCollection'] + ); } } diff --git a/rules/TypedCollections/Rector/Property/NarrowPropertyUnionToCollectionRector.php b/rules/TypedCollections/Rector/Property/NarrowPropertyUnionToCollectionRector.php index 7ae3fe55..4260d711 100644 --- a/rules/TypedCollections/Rector/Property/NarrowPropertyUnionToCollectionRector.php +++ b/rules/TypedCollections/Rector/Property/NarrowPropertyUnionToCollectionRector.php @@ -151,7 +151,6 @@ private function refactorPropertyDocBlock(Property $property): bool } $varTagValueNode = $propertyPhpDocInfo->getVarTagValueNode(); - if (! $varTagValueNode instanceof VarTagValueNode) { return false; }