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..e94abfbb --- /dev/null +++ b/rules-tests/TypedCollections/Rector/Property/NarrowPropertyUnionToCollectionRector/Fixture/narrow_intersection.php.inc @@ -0,0 +1,33 @@ +) + */ + 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 @@ +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; }