12
12
use PHPStan \Analyser \Scope ;
13
13
use PHPStan \PhpDocParser \Ast \PhpDoc \ReturnTagValueNode ;
14
14
use PHPStan \PhpDocParser \Ast \Type \GenericTypeNode ;
15
+ use PHPStan \PhpDocParser \Ast \Type \IdentifierTypeNode ;
15
16
use PHPStan \Reflection \ClassReflection ;
16
17
use PHPStan \Type \Constant \ConstantStringType ;
17
18
use PHPStan \Type \Generic \GenericClassStringType ;
18
19
use PHPStan \Type \Generic \GenericObjectType ;
19
20
use PHPStan \Type \ObjectType ;
21
+ use PHPStan \Type \ThisType ;
20
22
use Rector \BetterPhpDocParser \PhpDocInfo \PhpDocInfoFactory ;
21
23
use Rector \BetterPhpDocParser \ValueObject \Type \FullyQualifiedIdentifierTypeNode ;
22
24
use Rector \Comments \NodeDocBlock \DocBlockUpdater ;
25
+ use Rector \Contract \Rector \ConfigurableRectorInterface ;
23
26
use Rector \NodeTypeResolver \TypeComparator \TypeComparator ;
24
27
use Rector \PhpParser \Node \BetterNodeFinder ;
25
28
use Rector \Rector \AbstractScopeAwareRector ;
26
29
use Rector \StaticTypeMapper \StaticTypeMapper ;
27
- use Symplify \RuleDocGenerator \ValueObject \CodeSample \CodeSample ;
30
+ use Symplify \RuleDocGenerator \ValueObject \CodeSample \ConfiguredCodeSample ;
28
31
use Symplify \RuleDocGenerator \ValueObject \RuleDefinition ;
32
+ use Webmozart \Assert \Assert ;
29
33
30
- /** @see \RectorLaravel\Tests\Rector\ClassMethod\AddGenericReturnTypeToRelationsRector\AddGenericReturnTypeToRelationsRectorTest */
31
- class AddGenericReturnTypeToRelationsRector extends AbstractScopeAwareRector
34
+ /**
35
+ * @see \RectorLaravel\Tests\Rector\ClassMethod\AddGenericReturnTypeToRelationsRector\AddGenericReturnTypeToRelationsRectorNewGenericsTest
36
+ * @see \RectorLaravel\Tests\Rector\ClassMethod\AddGenericReturnTypeToRelationsRector\AddGenericReturnTypeToRelationsRectorOldGenericsTest
37
+ */
38
+ class AddGenericReturnTypeToRelationsRector extends AbstractScopeAwareRector implements ConfigurableRectorInterface
32
39
{
33
40
// Relation methods which are supported by this Rector.
34
41
private const RELATION_METHODS = [
@@ -41,6 +48,11 @@ class AddGenericReturnTypeToRelationsRector extends AbstractScopeAwareRector
41
48
// Relation methods which need the class as TChildModel.
42
49
private const RELATION_WITH_CHILD_METHODS = ['belongsTo ' , 'morphTo ' ];
43
50
51
+ // Relation methods which need the class as TIntermediateModel.
52
+ private const RELATION_WITH_INTERMEDIATE_METHODS = ['hasManyThrough ' , 'hasOneThrough ' ];
53
+
54
+ private bool $ shouldUseNewGenerics = false ;
55
+
44
56
public function __construct (
45
57
private readonly TypeComparator $ typeComparator ,
46
58
private readonly DocBlockUpdater $ docBlockUpdater ,
@@ -55,7 +67,7 @@ public function getRuleDefinition(): RuleDefinition
55
67
return new RuleDefinition (
56
68
'Add generic return type to relations in child of Illuminate\Database\Eloquent\Model ' ,
57
69
[
58
- new CodeSample (
70
+ new ConfiguredCodeSample (
59
71
<<<'CODE_SAMPLE'
60
72
use App\Account;
61
73
use Illuminate\Database\Eloquent\Model;
@@ -84,8 +96,39 @@ public function accounts(): HasMany
84
96
return $this->hasMany(Account::class);
85
97
}
86
98
}
99
+ CODE_SAMPLE,
100
+ ['shouldUseNewGenerics ' => false ]),
101
+ new ConfiguredCodeSample (
102
+ <<<'CODE_SAMPLE'
103
+ use App\Account;
104
+ use Illuminate\Database\Eloquent\Model;
105
+ use Illuminate\Database\Eloquent\Relations\HasMany;
106
+
107
+ class User extends Model
108
+ {
109
+ public function accounts(): HasMany
110
+ {
111
+ return $this->hasMany(Account::class);
112
+ }
113
+ }
87
114
CODE_SAMPLE
88
- ),
115
+
116
+ ,
117
+ <<<'CODE_SAMPLE'
118
+ use App\Account;
119
+ use Illuminate\Database\Eloquent\Model;
120
+ use Illuminate\Database\Eloquent\Relations\HasMany;
121
+
122
+ class User extends Model
123
+ {
124
+ /** @return HasMany<Account, $this> */
125
+ public function accounts(): HasMany
126
+ {
127
+ return $this->hasMany(Account::class);
128
+ }
129
+ }
130
+ CODE_SAMPLE,
131
+ ['shouldUseNewGenerics ' => true ]),
89
132
]
90
133
);
91
134
}
@@ -154,6 +197,7 @@ public function refactorWithScope(Node $node, Scope $scope): ?Node
154
197
}
155
198
156
199
$ classForChildGeneric = $ this ->getClassForChildGeneric ($ scope , $ relationMethodCall );
200
+ $ classForIntermediateGeneric = $ this ->getClassForIntermediateGeneric ($ relationMethodCall );
157
201
158
202
// Don't update the docblock if return type already contains the correct generics. This avoids overwriting
159
203
// non-FQCN with our fully qualified ones.
@@ -163,15 +207,16 @@ public function refactorWithScope(Node $node, Scope $scope): ?Node
163
207
$ node ,
164
208
$ phpDocInfo ->getReturnTagValue (),
165
209
$ relatedClass ,
166
- $ classForChildGeneric
210
+ $ classForChildGeneric ,
211
+ $ classForIntermediateGeneric
167
212
)
168
213
) {
169
214
return null ;
170
215
}
171
216
172
217
$ genericTypeNode = new GenericTypeNode (
173
218
new FullyQualifiedIdentifierTypeNode ($ methodReturnTypeName ),
174
- $ this ->getGenericTypes ($ relatedClass , $ classForChildGeneric ),
219
+ $ this ->getGenericTypes ($ relatedClass , $ classForChildGeneric, $ classForIntermediateGeneric ),
175
220
);
176
221
177
222
// Update or add return tag
@@ -187,6 +232,24 @@ public function refactorWithScope(Node $node, Scope $scope): ?Node
187
232
return $ node ;
188
233
}
189
234
235
+ /**
236
+ * {@inheritDoc}
237
+ */
238
+ public function configure (array $ configuration ): void
239
+ {
240
+ if ($ configuration === []) {
241
+ $ this ->shouldUseNewGenerics = false ;
242
+
243
+ return ;
244
+ }
245
+
246
+ Assert::count ($ configuration , 1 );
247
+ Assert::keyExists ($ configuration , 'shouldUseNewGenerics ' );
248
+ Assert::boolean ($ configuration ['shouldUseNewGenerics ' ]);
249
+
250
+ $ this ->shouldUseNewGenerics = $ configuration ['shouldUseNewGenerics ' ];
251
+ }
252
+
190
253
private function getRelatedModelClassFromMethodCall (MethodCall $ methodCall ): ?string
191
254
{
192
255
$ argType = $ this ->getType ($ methodCall ->getArgs ()[0 ]->value );
@@ -243,6 +306,10 @@ private function getRelationMethodCall(ClassMethod $classMethod): ?MethodCall
243
306
*/
244
307
private function getClassForChildGeneric (Scope $ scope , MethodCall $ methodCall ): ?string
245
308
{
309
+ if ($ this ->shouldUseNewGenerics ) {
310
+ return null ;
311
+ }
312
+
246
313
if (! $ this ->doesMethodHasName ($ methodCall , self ::RELATION_WITH_CHILD_METHODS )) {
247
314
return null ;
248
315
}
@@ -252,6 +319,45 @@ private function getClassForChildGeneric(Scope $scope, MethodCall $methodCall):
252
319
return $ classReflection ?->getName();
253
320
}
254
321
322
+ /**
323
+ * We need the intermediate class for generics which need a TIntermediateModel.
324
+ * This is the case for *through relations
325
+ */
326
+ private function getClassForIntermediateGeneric (MethodCall $ methodCall ): ?string
327
+ {
328
+ if (! $ this ->shouldUseNewGenerics ) {
329
+ return null ;
330
+ }
331
+
332
+ if (! $ this ->doesMethodHasName ($ methodCall , self ::RELATION_WITH_INTERMEDIATE_METHODS )) {
333
+ return null ;
334
+ }
335
+
336
+ $ args = $ methodCall ->getArgs ();
337
+
338
+ if (count ($ args ) < 2 ) {
339
+ return null ;
340
+ }
341
+
342
+ $ argType = $ this ->getType ($ args [1 ]->value );
343
+
344
+ if ($ argType instanceof ConstantStringType && $ argType ->isClassStringType ()->yes ()) {
345
+ return $ argType ->getValue ();
346
+ }
347
+
348
+ if (! $ argType instanceof GenericClassStringType) {
349
+ return null ;
350
+ }
351
+
352
+ $ modelType = $ argType ->getGenericType ();
353
+
354
+ if (! $ modelType instanceof ObjectType) {
355
+ return null ;
356
+ }
357
+
358
+ return $ modelType ->getClassName ();
359
+ }
360
+
255
361
private function areNativeTypeAndPhpDocReturnTypeEqual (
256
362
ClassMethod $ classMethod ,
257
363
Node $ node ,
@@ -279,7 +385,8 @@ private function areGenericTypesEqual(
279
385
Node $ node ,
280
386
ReturnTagValueNode $ returnTagValueNode ,
281
387
string $ relatedClass ,
282
- ?string $ classForChildGeneric
388
+ ?string $ classForChildGeneric ,
389
+ ?string $ classForIntermediateGeneric
283
390
): bool {
284
391
$ phpDocPHPStanType = $ this ->staticTypeMapper ->mapPHPStanPhpDocTypeNodeToPHPStanType (
285
392
$ returnTagValueNode ->type ,
@@ -299,16 +406,37 @@ private function areGenericTypesEqual(
299
406
return false ;
300
407
}
301
408
302
- $ phpDocHasChildGeneric = count ($ phpDocTypes ) === 2 ;
303
- if ($ classForChildGeneric === null && ! $ phpDocHasChildGeneric ) {
304
- return true ;
409
+ if (! $ this ->shouldUseNewGenerics ) {
410
+ $ phpDocHasChildGeneric = count ($ phpDocTypes ) === 2 ;
411
+
412
+ if ($ classForChildGeneric === null && ! $ phpDocHasChildGeneric ) {
413
+ return true ;
414
+ }
415
+
416
+ if ($ classForChildGeneric === null || ! $ phpDocHasChildGeneric ) {
417
+ return false ;
418
+ }
419
+
420
+ return $ this ->typeComparator ->areTypesEqual ($ phpDocTypes [1 ], new ObjectType ($ classForChildGeneric ));
421
+ }
422
+
423
+ $ phpDocHasIntermediateGeneric = count ($ phpDocTypes ) === 3 ;
424
+
425
+ if ($ classForIntermediateGeneric === null && ! $ phpDocHasIntermediateGeneric ) {
426
+ // If there is only one generic, it means method is using the old format. We should update it.
427
+ if (count ($ phpDocTypes ) === 1 ) {
428
+ return false ;
429
+ }
430
+
431
+ // We want to convert the existing relationship definition to use `$this` as the second generic
432
+ return $ phpDocTypes [1 ] instanceof ThisType;
305
433
}
306
434
307
- if ($ classForChildGeneric === null || ! $ phpDocHasChildGeneric ) {
435
+ if ($ classForIntermediateGeneric === null || ! $ phpDocHasIntermediateGeneric ) {
308
436
return false ;
309
437
}
310
438
311
- return $ this ->typeComparator ->areTypesEqual ($ phpDocTypes [1 ], new ObjectType ($ classForChildGeneric ));
439
+ return $ this ->typeComparator ->areTypesEqual ($ phpDocTypes [1 ], new ObjectType ($ classForIntermediateGeneric ));
312
440
}
313
441
314
442
private function shouldSkipNode (ClassMethod $ classMethod , Scope $ scope ): bool
@@ -341,16 +469,24 @@ private function doesMethodHasName(MethodCall $methodCall, array $methodNames):
341
469
}
342
470
343
471
/**
344
- * @return FullyQualifiedIdentifierTypeNode []
472
+ * @return IdentifierTypeNode []
345
473
*/
346
- private function getGenericTypes (string $ relatedClass , ?string $ childClass ): array
474
+ private function getGenericTypes (string $ relatedClass , ?string $ childClass, ? string $ intermediateClass ): array
347
475
{
348
476
$ generics = [new FullyQualifiedIdentifierTypeNode ($ relatedClass )];
349
477
350
- if ($ childClass !== null ) {
478
+ if (! $ this -> shouldUseNewGenerics && $ childClass !== null ) {
351
479
$ generics [] = new FullyQualifiedIdentifierTypeNode ($ childClass );
352
480
}
353
481
482
+ if ($ this ->shouldUseNewGenerics ) {
483
+ if ($ intermediateClass !== null ) {
484
+ $ generics [] = new FullyQualifiedIdentifierTypeNode ($ intermediateClass );
485
+ }
486
+
487
+ $ generics [] = new IdentifierTypeNode ('$this ' );
488
+ }
489
+
354
490
return $ generics ;
355
491
}
356
492
}
0 commit comments