1111use PhpParser \Node \Stmt \ClassLike ;
1212use PhpParser \Node \Stmt \ClassMethod ;
1313use PhpParser \Node \Stmt \Return_ ;
14+ use PHPStan \Analyser \Scope ;
1415use PHPStan \PhpDocParser \Ast \PhpDoc \ReturnTagValueNode ;
1516use PHPStan \PhpDocParser \Ast \Type \GenericTypeNode ;
1617use PHPStan \Type \Constant \ConstantStringType ;
1718use PHPStan \Type \Generic \GenericClassStringType ;
19+ use PHPStan \Type \Generic \GenericObjectType ;
1820use PHPStan \Type \ObjectType ;
1921use Rector \BetterPhpDocParser \ValueObject \Type \FullyQualifiedIdentifierTypeNode ;
20- use Rector \Core \Rector \AbstractRector ;
22+ use Rector \Core \Rector \AbstractScopeAwareRector ;
2123use Rector \NodeTypeResolver \TypeComparator \TypeComparator ;
2224use Symplify \RuleDocGenerator \ValueObject \CodeSample \CodeSample ;
2325use Symplify \RuleDocGenerator \ValueObject \RuleDefinition ;
2426
2527/** @see \RectorLaravel\Tests\Rector\ClassMethod\AddGenericReturnTypeToRelationsRector\AddGenericReturnTypeToRelationsRectorTest */
26- class AddGenericReturnTypeToRelationsRector extends AbstractRector
28+ class AddGenericReturnTypeToRelationsRector extends AbstractScopeAwareRector
2729{
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+
2841 public function __construct (
2942 private readonly TypeComparator $ typeComparator
3043 ) {
@@ -78,11 +91,12 @@ public function getNodeTypes(): array
7891 return [ClassMethod::class];
7992 }
8093
81- /**
82- * @param ClassMethod $node
83- */
84- public function refactor (Node $ node ): ?Node
94+ public function refactorWithScope (Node $ node , Scope $ scope ): ?Node
8595 {
96+ if (! $ node instanceof ClassMethod) {
97+ return null ;
98+ }
99+
86100 if ($ this ->shouldSkipNode ($ node )) {
87101 return null ;
88102 }
@@ -111,41 +125,46 @@ public function refactor(Node $node): ?Node
111125 // Don't update an existing return type if it differs from the native return type (thus the one without generics).
112126 // E.g. we only add generics to an existing return type, but don't change the type itself.
113127 if (
114- $ phpDocInfo ->getReturnTagValue () !== null &&
115- ! $ this ->typeComparator ->arePhpParserAndPhpStanPhpDocTypesEqual (
128+ $ phpDocInfo ->getReturnTagValue () !== null
129+ && ! $ this ->areNativeTypeAndPhpDocReturnTypeEqual (
130+ $ node ,
116131 $ methodReturnType ,
117132 $ phpDocInfo ->getReturnTagValue ()
118- ->type ,
119- $ node
120133 )
121134 ) {
122135 return null ;
123136 }
124137
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) {
131140 return null ;
132141 }
133142
134- $ relationMethodCall = $ this ->betterNodeFinder -> findFirstInstanceOf ( $ returnStatement , MethodCall::class );
143+ $ relatedClass = $ this ->getRelatedModelClassFromMethodCall ( $ relationMethodCall );
135144
136- if (! $ relationMethodCall instanceof MethodCall ) {
145+ if ($ relatedClass === null ) {
137146 return null ;
138147 }
139148
140- $ relatedClass = $ this ->getRelatedModelClassFromMethodCall ( $ relationMethodCall );
149+ $ classForChildGeneric = $ this ->getClassForChildGeneric ( $ scope , $ relationMethodCall );
141150
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+ ) {
143162 return null ;
144163 }
145164
146165 $ genericTypeNode = new GenericTypeNode (
147166 new FullyQualifiedIdentifierTypeNode ($ methodReturnTypeName ),
148- [ new FullyQualifiedIdentifierTypeNode ($ relatedClass)] ,
167+ $ this -> getGenericTypes ($ relatedClass, $ classForChildGeneric ) ,
149168 );
150169
151170 // Update or add return tag
@@ -161,43 +180,125 @@ public function refactor(Node $node): ?Node
161180
162181 private function getRelatedModelClassFromMethodCall (MethodCall $ methodCall ): ?string
163182 {
164- $ methodName = $ methodCall ->name ;
183+ $ argType = $ this -> getType ( $ methodCall ->getArgs ()[ 0 ]-> value ) ;
165184
166- if (! $ methodName instanceof Identifier) {
185+ if ($ argType instanceof ConstantStringType) {
186+ return $ argType ->getValue ();
187+ }
188+
189+ if (! $ argType instanceof GenericClassStringType) {
167190 return null ;
168191 }
169192
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) {
177196 return null ;
178197 }
179198
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_) {
181210 return null ;
182211 }
183212
184- $ argType = $ this ->getType ( $ methodCall -> getArgs ()[ 0 ]-> value );
213+ $ methodCall = $ this ->betterNodeFinder -> findFirstInstanceOf ( $ node , MethodCall::class );
185214
186- if ($ argType instanceof ConstantStringType ) {
187- return $ argType -> getValue () ;
215+ if (! $ methodCall instanceof MethodCall ) {
216+ return null ;
188217 }
189218
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 )) {
191221 return null ;
192222 }
193223
194- $ modelType = $ argType ->getGenericType ();
224+ if (count ($ methodCall ->getArgs ()) < 1 ) {
225+ return null ;
226+ }
195227
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 )) {
197238 return null ;
198239 }
199240
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 ));
201302 }
202303
203304 private function shouldSkipNode (ClassMethod $ classMethod ): bool
@@ -218,4 +319,31 @@ private function shouldSkipNode(ClassMethod $classMethod): bool
218319
219320 return false ;
220321 }
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+ }
221349}
0 commit comments