Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

namespace Rector\Doctrine\Tests\TypedCollections\Rector\Property\NarrowPropertyUnionToCollectionRector\Fixture;

use Doctrine\Common\Collections\Collection;
use Rector\Doctrine\Tests\TypedCollections\Rector\Property\NarrowPropertyUnionToCollectionRector\Source\SomeIterableObject;

final class NarrowIntersection
{
/**
* @var (Collection & iterable<SomeIterableObject>)
*/
public $items;
}

?>
-----
<?php

namespace Rector\Doctrine\Tests\TypedCollections\Rector\Property\NarrowPropertyUnionToCollectionRector\Fixture;

use Doctrine\Common\Collections\Collection;
use Rector\Doctrine\Tests\TypedCollections\Rector\Property\NarrowPropertyUnionToCollectionRector\Source\SomeIterableObject;

final class NarrowIntersection
{
/**
* @var \Doctrine\Common\Collections\Collection<SomeIterableObject>
*/
public $items;
}

?>
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php

namespace Rector\Doctrine\Tests\TypedCollections\Rector\Property\NarrowPropertyUnionToCollectionRector\Source;

class SomeIterableObject
{

}
Original file line number Diff line number Diff line change
Expand Up @@ -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<key, value>
if ($unionedTypeNode instanceof GenericTypeNode && $unionedTypeNode->type->name === 'array') {
$hasArrayType = true;
Expand All @@ -88,36 +97,46 @@ 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;
}
}

if (($hasArrayType === false || $hasCollectionType === false) && $hasChanged === false) {
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'),
Expand All @@ -130,6 +149,7 @@ public function narrow(
}

if ($hasNativeCollectionType && $type->name === 'null') {
// remove null type
unset($tagValueNode->type->types[$key]);
continue;
}
Expand All @@ -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']
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,6 @@ private function refactorPropertyDocBlock(Property $property): bool
}

$varTagValueNode = $propertyPhpDocInfo->getVarTagValueNode();

if (! $varTagValueNode instanceof VarTagValueNode) {
return false;
}
Expand Down