1313use PHPStan \Rules \RuleErrorBuilder ;
1414use PHPStan \ShouldNotHappenException ;
1515use function array_key_exists ;
16+ use function array_values ;
1617use function count ;
1718use function is_string ;
1819use function sprintf ;
@@ -155,7 +156,7 @@ public static function checkTraitMethodCollisions(ClassLike $classLike, ClassRef
155156 }
156157
157158 // Collect methods from each trait
158- /** @var array<string, list< array{string, string}>> $methodTraits methodNameLower => [[traitDisplayName, originalMethodName], ... ] */
159+ /** @var array<string, array<string, array{string, string}>> $methodTraits methodNameLower => [originTraitName => [traitDisplayName, originalMethodName]] */
159160 $ methodTraits = [];
160161 $ traits = $ classReflection ->getTraits ();
161162 foreach ($ traits as $ trait ) {
@@ -173,7 +174,16 @@ public static function checkTraitMethodCollisions(ClassLike $classLike, ClassRef
173174 continue ;
174175 }
175176
176- $ methodTraits [$ methodNameLower ][] = [$ trait ->getDisplayName (), $ method ->getName ()];
177+ // Find the original declaring trait to avoid false positives
178+ // when the same method reaches a class through multiple trait paths
179+ $ originTrait = self ::findOriginalDeclaringTrait ($ trait , $ methodNameLower );
180+ $ originKey = strtolower ($ originTrait ->getName ());
181+
182+ // Only record one entry per origin trait - if the same method
183+ // reaches the class from the same origin via different paths, it's not a collision
184+ if (!array_key_exists ($ methodNameLower , $ methodTraits ) || !array_key_exists ($ originKey , $ methodTraits [$ methodNameLower ])) {
185+ $ methodTraits [$ methodNameLower ][$ originKey ] = [$ trait ->getDisplayName (), $ method ->getName ()];
186+ }
177187 }
178188 }
179189
@@ -187,14 +197,15 @@ public static function checkTraitMethodCollisions(ClassLike $classLike, ClassRef
187197 }
188198
189199 // Report conflicts
190- foreach ($ methodTraits as $ traitInfos ) {
191- if (count ($ traitInfos ) <= 1 ) {
200+ foreach ($ methodTraits as $ originInfos ) {
201+ $ infos = array_values ($ originInfos );
202+ if (count ($ infos ) <= 1 ) {
192203 continue ;
193204 }
194205
195206 $ errors [] = RuleErrorBuilder::message (sprintf (
196207 'Trait method %s has not been applied, because there are collisions with other trait methods on %s. ' ,
197- $ traitInfos [1 ][0 ] . ':: ' . $ traitInfos [1 ][1 ] . '() ' ,
208+ $ infos [1 ][0 ] . ':: ' . $ infos [1 ][1 ] . '() ' ,
198209 $ classReflection ->getDisplayName (),
199210 ))->identifier ('class.traitMethodCollision ' )
200211 ->line ($ useLine ?? $ classLike ->getStartLine ())
@@ -205,4 +216,15 @@ public static function checkTraitMethodCollisions(ClassLike $classLike, ClassRef
205216 return $ errors ;
206217 }
207218
219+ private static function findOriginalDeclaringTrait (ClassReflection $ traitReflection , string $ methodNameLower ): ClassReflection
220+ {
221+ foreach ($ traitReflection ->getTraits () as $ subTrait ) {
222+ if ($ subTrait ->hasMethod ($ methodNameLower )) {
223+ return self ::findOriginalDeclaringTrait ($ subTrait , $ methodNameLower );
224+ }
225+ }
226+
227+ return $ traitReflection ;
228+ }
229+
208230}
0 commit comments