Skip to content

Commit 4fe4ec4

Browse files
phpstan-botclaude
andcommitted
Add tests for duplicate/indirect trait use and fix false positive
When a class uses the same trait multiple times (directly or indirectly through another trait), PHP does not consider it a collision. Track the original declaring trait to avoid false positives in cross-trait method collision detection. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 3d030f9 commit 4fe4ec4

File tree

2 files changed

+51
-5
lines changed

2 files changed

+51
-5
lines changed

src/Rules/Classes/DuplicateDeclarationHelper.php

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
use PHPStan\Rules\RuleErrorBuilder;
1414
use PHPStan\ShouldNotHappenException;
1515
use function array_key_exists;
16+
use function array_values;
1617
use function count;
1718
use function is_string;
1819
use 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
}

tests/PHPStan/Rules/Classes/data/trait-method-collisions.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,3 +116,27 @@ class AliasedTalker
116116
TraitF::bigTalk as talk;
117117
}
118118
}
119+
120+
trait TraitG
121+
{
122+
public function doFoo(): void {}
123+
}
124+
125+
trait TraitH
126+
{
127+
use TraitG;
128+
}
129+
130+
// Should not report - same trait used twice via separate use statements
131+
class DuplicateTraitUse
132+
{
133+
use TraitG;
134+
use TraitG;
135+
}
136+
137+
// Should not report - trait used directly and indirectly via another trait
138+
class IndirectTraitUse
139+
{
140+
use TraitG;
141+
use TraitH;
142+
}

0 commit comments

Comments
 (0)