Skip to content

Commit db0e43a

Browse files
authored
Add intersection support to NarrowPropertyUnionToCollectionRector (#442)
* add fixture * add intersection support
1 parent c05cfb6 commit db0e43a

4 files changed

Lines changed: 138 additions & 36 deletions

File tree

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php
2+
3+
namespace Rector\Doctrine\Tests\TypedCollections\Rector\Property\NarrowPropertyUnionToCollectionRector\Fixture;
4+
5+
use Doctrine\Common\Collections\Collection;
6+
use Rector\Doctrine\Tests\TypedCollections\Rector\Property\NarrowPropertyUnionToCollectionRector\Source\SomeIterableObject;
7+
8+
final class NarrowIntersection
9+
{
10+
/**
11+
* @var (Collection & iterable<SomeIterableObject>)
12+
*/
13+
public $items;
14+
}
15+
16+
?>
17+
-----
18+
<?php
19+
20+
namespace Rector\Doctrine\Tests\TypedCollections\Rector\Property\NarrowPropertyUnionToCollectionRector\Fixture;
21+
22+
use Doctrine\Common\Collections\Collection;
23+
use Rector\Doctrine\Tests\TypedCollections\Rector\Property\NarrowPropertyUnionToCollectionRector\Source\SomeIterableObject;
24+
25+
final class NarrowIntersection
26+
{
27+
/**
28+
* @var \Doctrine\Common\Collections\Collection<SomeIterableObject>
29+
*/
30+
public $items;
31+
}
32+
33+
?>
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?php
2+
3+
namespace Rector\Doctrine\Tests\TypedCollections\Rector\Property\NarrowPropertyUnionToCollectionRector\Source;
4+
5+
class SomeIterableObject
6+
{
7+
8+
}

rules/TypedCollections/DocBlockProcessor/UnionCollectionTagValueNodeNarrower.php

Lines changed: 97 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -35,37 +35,46 @@ public function narrow(
3535
}
3636

3737
if ($tagValueNode->type instanceof NullableTypeNode) {
38-
if ($hasNativeCollectionType) {
39-
$tagValueNode->type = $tagValueNode->type->type;
40-
$tagValueNode->setAttribute(PhpDocAttributeKey::ORIG_NODE, null);
41-
42-
$collectionType = $tagValueNode->type;
43-
$this->addIntKeyIfMissing($collectionType);
44-
45-
if ($collectionType->type instanceof IdentifierTypeNode && ! str_ends_with(
46-
$collectionType->type->name,
47-
'Collection'
48-
)) {
49-
$collectionType->type = new FullyQualifiedIdentifierTypeNode(DoctrineClass::COLLECTION);
50-
}
38+
return $this->processNullableTypeNode($hasNativeCollectionType, $tagValueNode);
39+
}
5140

52-
return true;
53-
}
41+
return $this->processIterableAndUnionTypeNode($tagValueNode, $hasNativeCollectionType);
42+
}
5443

55-
return false;
44+
private function addIntKeyIfMissing(TypeNode|IdentifierTypeNode $collectionType): void
45+
{
46+
if (! $collectionType instanceof GenericTypeNode) {
47+
return;
48+
}
49+
50+
if (count($collectionType->genericTypes) !== 1) {
51+
return;
5652
}
5753

54+
// add default key type
55+
$collectionType->genericTypes = array_merge([new IdentifierTypeNode('int')], $collectionType->genericTypes);
56+
}
57+
58+
private function processIterableAndUnionTypeNode(
59+
ParamTagValueNode|VarTagValueNode|ReturnTagValueNode $tagValueNode,
60+
bool $hasNativeCollectionType
61+
): bool {
5862
if (! $tagValueNode->type instanceof UnionTypeNode && ! $tagValueNode->type instanceof IntersectionTypeNode) {
5963
return false;
6064
}
6165

6266
$hasChanged = false;
63-
$hasCollectionType = false;
6467
$hasArrayType = false;
6568
$arrayTypeNode = null;
6669
$arrayKeyTypeNode = null;
6770

68-
foreach ($tagValueNode->type->types as $key => $unionedTypeNode) {
71+
// has collection docblock type?
72+
$hasCollectionType = $this->hasCollectionDocblockType($tagValueNode->type);
73+
$hasGenericIterableType = false;
74+
75+
$complexTypeNode = $tagValueNode->type;
76+
77+
foreach ($complexTypeNode->types as $key => $unionedTypeNode) {
6978
// possibly array<key, value>
7079
if ($unionedTypeNode instanceof GenericTypeNode && $unionedTypeNode->type->name === 'array') {
7180
$hasArrayType = true;
@@ -88,36 +97,46 @@ public function narrow(
8897
}
8998

9099
// remove |null, if property type is present as Collection
91-
if ($unionedTypeNode instanceof IdentifierTypeNode && $unionedTypeNode->name === 'null' && $hasNativeCollectionType) {
92-
93-
$hasChanged = true;
94-
unset($tagValueNode->type->types[$key]);
95-
continue;
96-
}
100+
if ($unionedTypeNode instanceof IdentifierTypeNode) {
101+
if ($unionedTypeNode->name === 'null' && $hasNativeCollectionType) {
102+
$hasChanged = true;
103+
unset($tagValueNode->type->types[$key]);
104+
continue;
105+
}
97106

98-
if ($unionedTypeNode instanceof IdentifierTypeNode && in_array(
99-
$unionedTypeNode->name,
100-
['Collection', 'ArrayCollection']
101-
)) {
102107
if ($unionedTypeNode->name === 'ArrayCollection') {
103108
$tagValueNode->type->types[$key] = new IdentifierTypeNode('\\' . DoctrineClass::COLLECTION);
104109
$hasChanged = true;
105110
}
106-
107-
$hasCollectionType = true;
108111
}
109112

110113
// narrow array collection to more generic collection
111-
if ($unionedTypeNode instanceof GenericTypeNode && $unionedTypeNode->type->name === 'ArrayCollection') {
114+
if ($unionedTypeNode instanceof GenericTypeNode && in_array(
115+
$unionedTypeNode->type->name,
116+
['ArrayCollection', 'iterable'],
117+
true
118+
)) {
112119
$unionedTypeNode->type = new IdentifierTypeNode('\\' . DoctrineClass::COLLECTION);
113120
$hasChanged = true;
121+
122+
$hasGenericIterableType = true;
114123
}
115124
}
116125

117126
if (($hasArrayType === false || $hasCollectionType === false) && $hasChanged === false) {
118127
return false;
119128
}
120129

130+
// remove duplicated Collection and Collection generics type
131+
if ($hasCollectionType && $hasGenericIterableType) {
132+
foreach ($complexTypeNode->types as $key => $singleType) {
133+
if ($this->isCollectionIdentifierTypeNode($singleType)) {
134+
// remove as has generic iterable type already
135+
unset($complexTypeNode->types[$key]);
136+
}
137+
}
138+
}
139+
121140
if ($arrayTypeNode instanceof TypeNode) {
122141
$tagValueNode->type = new GenericTypeNode(new IdentifierTypeNode('\\' . DoctrineClass::COLLECTION), [
123142
$arrayKeyTypeNode ?? new IdentifierTypeNode('int'),
@@ -130,6 +149,7 @@ public function narrow(
130149
}
131150

132151
if ($hasNativeCollectionType && $type->name === 'null') {
152+
// remove null type
133153
unset($tagValueNode->type->types[$key]);
134154
continue;
135155
}
@@ -154,11 +174,53 @@ public function narrow(
154174
return true;
155175
}
156176

157-
private function addIntKeyIfMissing(TypeNode|IdentifierTypeNode $collectionType): void
177+
private function hasCollectionDocblockType(UnionTypeNode|IntersectionTypeNode $complexTypeNode): bool
158178
{
159-
if ($collectionType instanceof GenericTypeNode && count($collectionType->genericTypes) === 1) {
160-
// add default key type
161-
$collectionType->genericTypes = array_merge([new IdentifierTypeNode('int')], $collectionType->genericTypes);
179+
foreach ($complexTypeNode->types as $singleType) {
180+
if ($this->isCollectionIdentifierTypeNode($singleType)) {
181+
return true;
182+
}
183+
}
184+
185+
return false;
186+
}
187+
188+
private function processNullableTypeNode(
189+
bool $hasNativeCollectionType,
190+
ParamTagValueNode|VarTagValueNode|ReturnTagValueNode $tagValueNode
191+
): bool {
192+
if ($hasNativeCollectionType === false) {
193+
return false;
162194
}
195+
196+
// unwrap nullable type
197+
$tagValueNode->type = $tagValueNode->type->type;
198+
199+
// invoke reprint
200+
$tagValueNode->setAttribute(PhpDocAttributeKey::ORIG_NODE, null);
201+
202+
$collectionType = $tagValueNode->type;
203+
$this->addIntKeyIfMissing($collectionType);
204+
205+
if ($collectionType->type instanceof IdentifierTypeNode && ! str_ends_with(
206+
$collectionType->type->name,
207+
'Collection'
208+
)) {
209+
$collectionType->type = new FullyQualifiedIdentifierTypeNode(DoctrineClass::COLLECTION);
210+
}
211+
212+
return true;
213+
}
214+
215+
private function isCollectionIdentifierTypeNode(TypeNode $typeNode): bool
216+
{
217+
if (! $typeNode instanceof IdentifierTypeNode) {
218+
return false;
219+
}
220+
221+
return in_array(
222+
$typeNode->name,
223+
[DoctrineClass::COLLECTION, DoctrineClass::ARRAY_COLLECTION, 'Collection', 'ArrayCollection']
224+
);
163225
}
164226
}

rules/TypedCollections/Rector/Property/NarrowPropertyUnionToCollectionRector.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,6 @@ private function refactorPropertyDocBlock(Property $property): bool
151151
}
152152

153153
$varTagValueNode = $propertyPhpDocInfo->getVarTagValueNode();
154-
155154
if (! $varTagValueNode instanceof VarTagValueNode) {
156155
return false;
157156
}

0 commit comments

Comments
 (0)