Skip to content

Commit 44e475b

Browse files
committed
kick off split set
1 parent 0fa7412 commit 44e475b

9 files changed

Lines changed: 222 additions & 13 deletions

File tree

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use Rector\Doctrine\TypedCollections\Rector\ClassMethod\CollectionDocblockGenericTypeRector;
6+
use Rector\Config\RectorConfig;
7+
8+
return static function (RectorConfig $rectorConfig): void {
9+
$rectorConfig->rules([
10+
// safe rules that handle only docblocks
11+
CollectionDocblockGenericTypeRector::class,
12+
]);
13+
};

config/sets/typed-collections.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@
4141
use Rector\Doctrine\TypedCollections\Rector\Property\TypedPropertyFromToManyRelationTypeRector;
4242

4343
return static function (RectorConfig $rectorConfig): void {
44+
// rule that handle docblocks only, safer to apply
45+
$rectorConfig->import(__DIR__ . '/typed-collections-docblocks.php');
46+
4447
$rectorConfig->rules([
4548
// init
4649
InitializeCollectionInConstructorRector::class,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Rector\Doctrine\Tests\TypedCollections\Rector\ClassMethod\CollectionDocblockGenericTypeRector\Fixture;
6+
7+
use Doctrine\Common\Collections\Collection;
8+
use Doctrine\Common\Collections\ArrayCollection;
9+
use Rector\Doctrine\Tests\TypedCollections\Rector\ClassMethod\CollectionDocblockGenericTypeRector\Source\RandomHouse;
10+
11+
final class SkipDifferentVariableReturned
12+
{
13+
public function getItems(): Collection
14+
{
15+
$collection = new ArrayCollection();
16+
$collection->add(new RandomHouse());
17+
18+
$collection2 = [];
19+
20+
return $collection2;
21+
}
22+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Rector\Doctrine\Tests\TypedCollections\Rector\ClassMethod\CollectionDocblockGenericTypeRector\Fixture;
6+
7+
use Doctrine\Common\Collections\Collection;
8+
use Doctrine\Common\Collections\ArrayCollection;
9+
10+
final class SkipNoTypeAdded
11+
{
12+
public function getItems(): Collection
13+
{
14+
$collection = new ArrayCollection();
15+
16+
return $collection;
17+
}
18+
}

rules-tests/TypedCollections/Rector/ClassMethod/CollectionDocblockGenericTypeRector/Fixture/some_method_with_return_collection.php.inc

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ final class SomeMethodWithReturnCollection
2121

2222
?>
2323
-----
24-
<?php
24+
<?php
2525

2626
declare(strict_types=1);
2727

@@ -34,7 +34,7 @@ use Rector\Doctrine\Tests\TypedCollections\Rector\ClassMethod\CollectionDocblock
3434
final class SomeMethodWithReturnCollection
3535
{
3636
/**
37-
* @return Collection<int, RandomHouse>
37+
* @return \Doctrine\Common\Collections\Collection<int, \Rector\Doctrine\Tests\TypedCollections\Rector\ClassMethod\CollectionDocblockGenericTypeRector\Source\RandomHouse>
3838
*/
3939
public function getItems(): Collection
4040
{
@@ -46,4 +46,3 @@ final class SomeMethodWithReturnCollection
4646
}
4747

4848
?>
49-

rules-tests/TypedCollections/Rector/ClassMethod/CollectionDocblockGenericTypeRector/config/configured_rule.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
declare(strict_types=1);
44

55
use Rector\Config\RectorConfig;
6-
use Rector\Doctrine\TypedCollections\Rector\ClassMethod\CollectionGetterNativeTypeRector;
6+
use Rector\Doctrine\TypedCollections\Rector\ClassMethod\CollectionDocblockGenericTypeRector;
77

88
return static function (RectorConfig $rectorConfig): void {
9-
$rectorConfig->rule(\Rector\Doctrine\TypedCollections\Rector\ClassMethod\CollectionDocblockGenericTypeRector::class);
9+
$rectorConfig->rule(CollectionDocblockGenericTypeRector::class);
1010
};
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Rector\Doctrine\TypedCollections\NodeAnalyzer;
6+
7+
use PhpParser\Node\Expr\Assign;
8+
use PhpParser\Node\Expr\New_;
9+
use PhpParser\Node\Expr\Variable;
10+
use PhpParser\Node\Stmt\ClassMethod;
11+
use Rector\Doctrine\Enum\DoctrineClass;
12+
use Rector\NodeNameResolver\NodeNameResolver;
13+
use Rector\PhpParser\Node\BetterNodeFinder;
14+
15+
final readonly class FreshArrayCollectionAnalyzer
16+
{
17+
public function __construct(
18+
private BetterNodeFinder $betterNodeFinder,
19+
private NodeNameResolver $nodeNameResolver
20+
) {
21+
22+
}
23+
24+
public function doesReturnNewArrayCollectionVariable(ClassMethod $classMethod): bool
25+
{
26+
$newArrayCollectionVariableName = $this->resolveNewArrayCollectionVariableName($classMethod);
27+
if ($newArrayCollectionVariableName === null) {
28+
return false;
29+
}
30+
31+
$returns = $this->betterNodeFinder->findReturnsScoped($classMethod);
32+
if (count($returns) !== 1) {
33+
return false;
34+
}
35+
36+
// the exact variable name must be returned
37+
$soleReturn = $returns[0];
38+
if (! $soleReturn->expr instanceof Variable) {
39+
return false;
40+
}
41+
42+
return $this->nodeNameResolver->isName($soleReturn->expr, $newArrayCollectionVariableName);
43+
}
44+
45+
private function resolveNewArrayCollectionVariableName(ClassMethod $classMethod): ?string
46+
{
47+
$assigns = $this->betterNodeFinder->findInstancesOfScoped([$classMethod], Assign::class);
48+
49+
foreach ($assigns as $assign) {
50+
if (! $assign->expr instanceof New_) {
51+
continue;
52+
}
53+
54+
$new = $assign->expr;
55+
if (! $this->nodeNameResolver->isName($new->class, DoctrineClass::ARRAY_COLLECTION)) {
56+
continue;
57+
}
58+
59+
// detect variable name
60+
if (! $assign->var instanceof Variable) {
61+
continue;
62+
}
63+
64+
$variable = $assign->var;
65+
66+
return $this->nodeNameResolver->getName($variable);
67+
}
68+
69+
return null;
70+
}
71+
}

rules/TypedCollections/Rector/ClassMethod/CollectionDocblockGenericTypeRector.php

Lines changed: 86 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,18 @@
55
namespace Rector\Doctrine\TypedCollections\Rector\ClassMethod;
66

77
use PhpParser\Node;
8-
use PhpParser\Node\Expr\PropertyFetch;
9-
use PhpParser\Node\Name\FullyQualified;
8+
use PhpParser\Node\Expr\MethodCall;
109
use PhpParser\Node\Stmt\ClassMethod;
11-
use PhpParser\Node\Stmt\Return_;
10+
use PHPStan\PhpDocParser\Ast\PhpDoc\ReturnTagValueNode;
11+
use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode;
12+
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
13+
use PHPStan\Type\ObjectType;
14+
use Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfoFactory;
15+
use Rector\BetterPhpDocParser\ValueObject\Type\FullyQualifiedIdentifierTypeNode;
16+
use Rector\Comments\NodeDocBlock\DocBlockUpdater;
1217
use Rector\Doctrine\Enum\DoctrineClass;
13-
use Rector\Doctrine\TypedCollections\TypeAnalyzer\CollectionTypeDetector;
18+
use Rector\Doctrine\TypedCollections\NodeAnalyzer\FreshArrayCollectionAnalyzer;
19+
use Rector\PhpParser\Node\BetterNodeFinder;
1420
use Rector\Rector\AbstractRector;
1521
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
1622
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
@@ -20,6 +26,14 @@
2026
*/
2127
final class CollectionDocblockGenericTypeRector extends AbstractRector
2228
{
29+
public function __construct(
30+
private readonly BetterNodeFinder $betterNodeFinder,
31+
private readonly FreshArrayCollectionAnalyzer $freshArrayCollectionAnalyzer,
32+
private readonly PhpDocInfoFactory $phpDocInfoFactory,
33+
private readonly DocBlockUpdater $docBlockUpdater
34+
) {
35+
}
36+
2337
public function getRuleDefinition(): RuleDefinition
2438
{
2539
return new RuleDefinition(
@@ -78,22 +92,86 @@ public function refactor(Node $node): ?ClassMethod
7892
return null;
7993
}
8094

81-
// already different type
82-
if ($node->returnType instanceof \PhpParser\Node) {
95+
if ($node->returnType === null) {
8396
return null;
8497
}
8598

86-
if ($node->returnType === null) {
99+
if (! $this->isName($node->returnType, DoctrineClass::COLLECTION)) {
87100
return null;
88101
}
89102

90-
if (! $this->isName($node->returnType, DoctrineClass::COLLECTION)) {
103+
if (! $this->freshArrayCollectionAnalyzer->doesReturnNewArrayCollectionVariable($node)) {
91104
return null;
92105
}
93106

107+
// we have a match here!
108+
109+
// lets resolve docblock of this method
110+
$phpDocInfo = $this->phpDocInfoFactory->createFromNodeOrEmpty($node);
111+
112+
$returnTagValueNode = $phpDocInfo->getReturnTagValue();
113+
114+
// is known and generic? lets skip it
115+
if ($returnTagValueNode instanceof ReturnTagValueNode && $returnTagValueNode->type instanceof GenericTypeNode) {
116+
return null;
117+
}
118+
119+
// resolve type added to collection
120+
121+
$methodCalls = $this->betterNodeFinder->findInstancesOfScoped([$node], MethodCall::class);
122+
123+
$collectionAddMethodCalls = array_filter($methodCalls, function (MethodCall $methodCall): bool {
124+
if (! $this->isName($methodCall->name, 'add')) {
125+
return false;
126+
}
127+
128+
$callerType = $this->getType($methodCall->var);
129+
if (! $callerType instanceof ObjectType) {
130+
return false;
131+
}
132+
133+
return $callerType->isInstanceOf(DoctrineClass::ARRAY_COLLECTION)->yes();
134+
});
135+
136+
if ($collectionAddMethodCalls === []) {
137+
return null;
138+
}
139+
140+
$setTypeClasses = [];
141+
142+
foreach ($collectionAddMethodCalls as $collectionAddMethodCall) {
143+
$setArg = $collectionAddMethodCall->getArgs()[0];
144+
$setType = $this->getType($setArg->value);
145+
146+
if (! isset($setType->getObjectClassNames()[0])) {
147+
continue;
148+
}
149+
150+
$setTypeClasses[] = $setType->getObjectClassNames()[0];
151+
}
152+
153+
if (count($setTypeClasses) !== 1) {
154+
return null;
155+
}
156+
157+
$setTypeClass = $setTypeClasses[0];
158+
159+
// add a new one with generic type
94160
// find return
95161
// find new ArrayCollection()
96162
// improve return type
163+
$genericTypeNode = new GenericTypeNode(
164+
new FullyQualifiedIdentifierTypeNode(DoctrineClass::COLLECTION),
165+
[
166+
new IdentifierTypeNode('int'),
167+
new FullyQualifiedIdentifierTypeNode($setTypeClass),
168+
] // @todo resolve the class name
169+
);
170+
$returnTagValueNode = new ReturnTagValueNode($genericTypeNode, '');
171+
172+
$phpDocInfo->addTagValueNode($returnTagValueNode);
173+
174+
$this->docBlockUpdater->updateRefactoredNodeWithPhpDocInfo($node);
97175

98176
return null;
99177
}

src/Set/DoctrineSetList.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@ final class DoctrineSetList
1414
*/
1515
public const TYPED_COLLECTIONS = __DIR__ . '/../../config/sets/typed-collections.php';
1616

17+
/**
18+
* @var string
19+
*/
20+
public const TYPED_COLLECTIONS_DOCBLOCKS = __DIR__ . '/../../config/sets/typed-collections-docblocks.php';
21+
1722
/**
1823
* @var string
1924
*/

0 commit comments

Comments
 (0)