11
11
use PhpParser \Node \Stmt \ClassLike ;
12
12
use PhpParser \Node \Stmt \ClassMethod ;
13
13
use PhpParser \Node \Stmt \Return_ ;
14
+ use PHPStan \Analyser \Scope ;
14
15
use PHPStan \PhpDocParser \Ast \PhpDoc \ReturnTagValueNode ;
15
16
use PHPStan \PhpDocParser \Ast \Type \GenericTypeNode ;
16
17
use PHPStan \Type \Constant \ConstantStringType ;
17
18
use PHPStan \Type \Generic \GenericClassStringType ;
19
+ use PHPStan \Type \Generic \GenericObjectType ;
18
20
use PHPStan \Type \ObjectType ;
19
21
use Rector \BetterPhpDocParser \ValueObject \Type \FullyQualifiedIdentifierTypeNode ;
20
- use Rector \Core \Rector \AbstractRector ;
22
+ use Rector \Core \Rector \AbstractScopeAwareRector ;
21
23
use Rector \NodeTypeResolver \TypeComparator \TypeComparator ;
22
24
use Symplify \RuleDocGenerator \ValueObject \CodeSample \CodeSample ;
23
25
use Symplify \RuleDocGenerator \ValueObject \RuleDefinition ;
24
26
25
27
/** @see \RectorLaravel\Tests\Rector\ClassMethod\AddGenericReturnTypeToRelationsRector\AddGenericReturnTypeToRelationsRectorTest */
26
- class AddGenericReturnTypeToRelationsRector extends AbstractRector
28
+ class AddGenericReturnTypeToRelationsRector extends AbstractScopeAwareRector
27
29
{
30
+ // Relation methods which are supported by this Rector.
31
+ private const RELATION_METHODS = [
32
+ 'hasOne ' , 'hasOneThrough ' , 'morphOne ' ,
33
+ 'belongsTo ' , 'morphTo ' ,
34
+ 'hasMany ' , 'hasManyThrough ' , 'morphMany ' ,
35
+ 'belongsToMany ' , 'morphToMany ' , 'morphedByMany ' ,
36
+ ];
37
+
38
+ // Relation methods which need the class as TChildModel.
39
+ private const RELATION_WITH_CHILD_METHODS = ['belongsTo ' , 'morphTo ' ];
40
+
28
41
public function __construct (
29
42
private readonly TypeComparator $ typeComparator
30
43
) {
@@ -78,11 +91,12 @@ public function getNodeTypes(): array
78
91
return [ClassMethod::class];
79
92
}
80
93
81
- /**
82
- * @param ClassMethod $node
83
- */
84
- public function refactor (Node $ node ): ?Node
94
+ public function refactorWithScope (Node $ node , Scope $ scope ): ?Node
85
95
{
96
+ if (! $ node instanceof ClassMethod) {
97
+ return null ;
98
+ }
99
+
86
100
if ($ this ->shouldSkipNode ($ node )) {
87
101
return null ;
88
102
}
@@ -111,41 +125,46 @@ public function refactor(Node $node): ?Node
111
125
// Don't update an existing return type if it differs from the native return type (thus the one without generics).
112
126
// E.g. we only add generics to an existing return type, but don't change the type itself.
113
127
if (
114
- $ phpDocInfo ->getReturnTagValue () !== null &&
115
- ! $ this ->typeComparator ->arePhpParserAndPhpStanPhpDocTypesEqual (
128
+ $ phpDocInfo ->getReturnTagValue () !== null
129
+ && ! $ this ->areNativeTypeAndPhpDocReturnTypeEqual (
130
+ $ node ,
116
131
$ methodReturnType ,
117
132
$ phpDocInfo ->getReturnTagValue ()
118
- ->type ,
119
- $ node
120
133
)
121
134
) {
122
135
return null ;
123
136
}
124
137
125
- $ returnStatement = $ this ->betterNodeFinder ->findFirstInFunctionLikeScoped (
126
- $ node ,
127
- fn (Node $ subNode ): bool => $ subNode instanceof Return_
128
- );
129
-
130
- if (! $ returnStatement instanceof Return_) {
138
+ $ relationMethodCall = $ this ->getRelationMethodCall ($ node );
139
+ if (! $ relationMethodCall instanceof MethodCall) {
131
140
return null ;
132
141
}
133
142
134
- $ relationMethodCall = $ this ->betterNodeFinder -> findFirstInstanceOf ( $ returnStatement , MethodCall::class );
143
+ $ relatedClass = $ this ->getRelatedModelClassFromMethodCall ( $ relationMethodCall );
135
144
136
- if (! $ relationMethodCall instanceof MethodCall ) {
145
+ if ($ relatedClass === null ) {
137
146
return null ;
138
147
}
139
148
140
- $ relatedClass = $ this ->getRelatedModelClassFromMethodCall ( $ relationMethodCall );
149
+ $ classForChildGeneric = $ this ->getClassForChildGeneric ( $ scope , $ relationMethodCall );
141
150
142
- if ($ relatedClass === null ) {
151
+ // Don't update the docblock if return type already contains the correct generics. This avoids overwriting
152
+ // non-FQCN with our fully qualified ones.
153
+ if (
154
+ $ phpDocInfo ->getReturnTagValue () !== null
155
+ && $ this ->areGenericTypesEqual (
156
+ $ node ,
157
+ $ phpDocInfo ->getReturnTagValue (),
158
+ $ relatedClass ,
159
+ $ classForChildGeneric
160
+ )
161
+ ) {
143
162
return null ;
144
163
}
145
164
146
165
$ genericTypeNode = new GenericTypeNode (
147
166
new FullyQualifiedIdentifierTypeNode ($ methodReturnTypeName ),
148
- [ new FullyQualifiedIdentifierTypeNode ($ relatedClass)] ,
167
+ $ this -> getGenericTypes ($ relatedClass, $ classForChildGeneric ) ,
149
168
);
150
169
151
170
// Update or add return tag
@@ -161,43 +180,125 @@ public function refactor(Node $node): ?Node
161
180
162
181
private function getRelatedModelClassFromMethodCall (MethodCall $ methodCall ): ?string
163
182
{
164
- $ methodName = $ methodCall ->name ;
183
+ $ argType = $ this -> getType ( $ methodCall ->getArgs ()[ 0 ]-> value ) ;
165
184
166
- if (! $ methodName instanceof Identifier) {
185
+ if ($ argType instanceof ConstantStringType) {
186
+ return $ argType ->getValue ();
187
+ }
188
+
189
+ if (! $ argType instanceof GenericClassStringType) {
167
190
return null ;
168
191
}
169
192
170
- // Called method should be one of the Laravel's relation methods
171
- if (! in_array ($ methodName ->name , [
172
- 'hasOne ' , 'hasOneThrough ' , 'morphOne ' ,
173
- 'belongsTo ' , 'morphTo ' ,
174
- 'hasMany ' , 'hasManyThrough ' , 'morphMany ' ,
175
- 'belongsToMany ' , 'morphToMany ' , 'morphedByMany ' ,
176
- ], true )) {
193
+ $ modelType = $ argType ->getGenericType ();
194
+
195
+ if (! $ modelType instanceof ObjectType) {
177
196
return null ;
178
197
}
179
198
180
- if (count ($ methodCall ->getArgs ()) < 1 ) {
199
+ return $ modelType ->getClassName ();
200
+ }
201
+
202
+ private function getRelationMethodCall (ClassMethod $ classMethod ): ?MethodCall
203
+ {
204
+ $ node = $ this ->betterNodeFinder ->findFirstInFunctionLikeScoped (
205
+ $ classMethod ,
206
+ fn (Node $ subNode ): bool => $ subNode instanceof Return_
207
+ );
208
+
209
+ if (! $ node instanceof Return_) {
181
210
return null ;
182
211
}
183
212
184
- $ argType = $ this ->getType ( $ methodCall -> getArgs ()[ 0 ]-> value );
213
+ $ methodCall = $ this ->betterNodeFinder -> findFirstInstanceOf ( $ node , MethodCall::class );
185
214
186
- if ($ argType instanceof ConstantStringType ) {
187
- return $ argType -> getValue () ;
215
+ if (! $ methodCall instanceof MethodCall ) {
216
+ return null ;
188
217
}
189
218
190
- if (! $ argType instanceof GenericClassStringType) {
219
+ // Called method should be one of the Laravel's relation methods
220
+ if (! $ this ->doesMethodHasName ($ methodCall , self ::RELATION_METHODS )) {
191
221
return null ;
192
222
}
193
223
194
- $ modelType = $ argType ->getGenericType ();
224
+ if (count ($ methodCall ->getArgs ()) < 1 ) {
225
+ return null ;
226
+ }
195
227
196
- if (! $ modelType instanceof ObjectType) {
228
+ return $ methodCall ;
229
+ }
230
+
231
+ /**
232
+ * We need the current class for generics which need a TChildModel. This is the case by for example the BelongsTo
233
+ * relation.
234
+ */
235
+ private function getClassForChildGeneric (Scope $ scope , MethodCall $ methodCall ): ?string
236
+ {
237
+ if (! $ this ->doesMethodHasName ($ methodCall , self ::RELATION_WITH_CHILD_METHODS )) {
197
238
return null ;
198
239
}
199
240
200
- return $ modelType ->getClassName ();
241
+ $ classReflection = $ scope ->getClassReflection ();
242
+
243
+ return $ classReflection ?->getName();
244
+ }
245
+
246
+ private function areNativeTypeAndPhpDocReturnTypeEqual (
247
+ ClassMethod $ classMethod ,
248
+ Node $ node ,
249
+ ReturnTagValueNode $ returnTagValueNode
250
+ ): bool {
251
+ $ phpDocPHPStanType = $ this ->staticTypeMapper ->mapPHPStanPhpDocTypeNodeToPHPStanType (
252
+ $ returnTagValueNode ->type ,
253
+ $ classMethod
254
+ );
255
+
256
+ $ phpDocPHPStanTypeWithoutGenerics = $ phpDocPHPStanType ;
257
+ if ($ phpDocPHPStanType instanceof GenericObjectType) {
258
+ $ phpDocPHPStanTypeWithoutGenerics = new ObjectType ($ phpDocPHPStanType ->getClassName ());
259
+ }
260
+
261
+ $ methodReturnTypePHPStanType = $ this ->staticTypeMapper ->mapPhpParserNodePHPStanType ($ node );
262
+
263
+ return $ this ->typeComparator ->areTypesEqual (
264
+ $ methodReturnTypePHPStanType ,
265
+ $ phpDocPHPStanTypeWithoutGenerics ,
266
+ );
267
+ }
268
+
269
+ private function areGenericTypesEqual (
270
+ Node $ node ,
271
+ ReturnTagValueNode $ returnTagValueNode ,
272
+ string $ relatedClass ,
273
+ ?string $ classForChildGeneric
274
+ ): bool {
275
+ $ phpDocPHPStanType = $ this ->staticTypeMapper ->mapPHPStanPhpDocTypeNodeToPHPStanType (
276
+ $ returnTagValueNode ->type ,
277
+ $ node
278
+ );
279
+
280
+ if (! $ phpDocPHPStanType instanceof GenericObjectType) {
281
+ return false ;
282
+ }
283
+
284
+ $ phpDocTypes = $ phpDocPHPStanType ->getTypes ();
285
+ if ($ phpDocTypes === []) {
286
+ return false ;
287
+ }
288
+
289
+ if (! $ this ->typeComparator ->areTypesEqual ($ phpDocTypes [0 ], new ObjectType ($ relatedClass ))) {
290
+ return false ;
291
+ }
292
+
293
+ $ phpDocHasChildGeneric = count ($ phpDocTypes ) === 2 ;
294
+ if ($ classForChildGeneric === null && ! $ phpDocHasChildGeneric ) {
295
+ return true ;
296
+ }
297
+
298
+ if ($ classForChildGeneric === null || ! $ phpDocHasChildGeneric ) {
299
+ return false ;
300
+ }
301
+ return $ this ->typeComparator ->areTypesEqual ($ phpDocTypes [1 ], new ObjectType ($ classForChildGeneric ));
201
302
}
202
303
203
304
private function shouldSkipNode (ClassMethod $ classMethod ): bool
@@ -218,4 +319,31 @@ private function shouldSkipNode(ClassMethod $classMethod): bool
218
319
219
320
return false ;
220
321
}
322
+
323
+ /**
324
+ * @param array<string> $methodNames
325
+ */
326
+ private function doesMethodHasName (MethodCall $ methodCall , array $ methodNames ): bool
327
+ {
328
+ $ methodName = $ methodCall ->name ;
329
+
330
+ if (! $ methodName instanceof Identifier) {
331
+ return false ;
332
+ }
333
+ return in_array ($ methodName ->name , $ methodNames , true );
334
+ }
335
+
336
+ /**
337
+ * @return FullyQualifiedIdentifierTypeNode[]
338
+ */
339
+ private function getGenericTypes (string $ relatedClass , ?string $ childClass ): array
340
+ {
341
+ $ generics = [new FullyQualifiedIdentifierTypeNode ($ relatedClass )];
342
+
343
+ if ($ childClass !== null ) {
344
+ $ generics [] = new FullyQualifiedIdentifierTypeNode ($ childClass );
345
+ }
346
+
347
+ return $ generics ;
348
+ }
221
349
}
0 commit comments