Skip to content

Commit b4315e8

Browse files
committed
Fix phpstan/phpstan#14250: Report trait.duplicateMethod for duplicate methods in traits
- Created DuplicateTraitDeclarationRule that listens for InTraitNode to detect duplicate methods, properties, and constants within trait declarations - The existing DuplicateDeclarationRule only handles InClassNode, which is never emitted for traits (NodeScopeResolver skips trait declarations at line 967-968) - InTraitNode is emitted when a trait is used by a class, allowing the new rule to check trait bodies for duplicates - Added regression test in tests/PHPStan/Rules/Classes/data/bug-14250.php
1 parent c1c4816 commit b4315e8

3 files changed

Lines changed: 168 additions & 0 deletions

File tree

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Classes;
4+
5+
use PhpParser\Node;
6+
use PhpParser\Node\Stmt\ClassConst;
7+
use PHPStan\Analyser\Scope;
8+
use PHPStan\DependencyInjection\RegisteredRule;
9+
use PHPStan\Node\InTraitNode;
10+
use PHPStan\Rules\Rule;
11+
use PHPStan\Rules\RuleErrorBuilder;
12+
use PHPStan\ShouldNotHappenException;
13+
use function array_key_exists;
14+
use function is_string;
15+
use function sprintf;
16+
use function strtolower;
17+
18+
/**
19+
* @implements Rule<InTraitNode>
20+
*/
21+
#[RegisteredRule(level: 0)]
22+
final class DuplicateTraitDeclarationRule implements Rule
23+
{
24+
25+
public function getNodeType(): string
26+
{
27+
return InTraitNode::class;
28+
}
29+
30+
public function processNode(Node $node, Scope $scope): array
31+
{
32+
$traitReflection = $node->getTraitReflection();
33+
34+
$errors = [];
35+
36+
$declaredClassConstants = [];
37+
foreach ($node->getOriginalNode()->stmts as $stmtNode) {
38+
if (!($stmtNode instanceof ClassConst)) {
39+
continue;
40+
}
41+
foreach ($stmtNode->consts as $classConstNode) {
42+
if (array_key_exists($classConstNode->name->name, $declaredClassConstants)) {
43+
$errors[] = RuleErrorBuilder::message(sprintf(
44+
'Cannot redeclare constant %s::%s.',
45+
$traitReflection->getDisplayName(),
46+
$classConstNode->name->name,
47+
))->identifier('trait.duplicateConstant')
48+
->line($classConstNode->getStartLine())
49+
->nonIgnorable()
50+
->build();
51+
} else {
52+
$declaredClassConstants[$classConstNode->name->name] = true;
53+
}
54+
}
55+
}
56+
57+
$declaredProperties = [];
58+
foreach ($node->getOriginalNode()->getProperties() as $propertyDecl) {
59+
foreach ($propertyDecl->props as $property) {
60+
if (array_key_exists($property->name->name, $declaredProperties)) {
61+
$errors[] = RuleErrorBuilder::message(sprintf(
62+
'Cannot redeclare property %s::$%s.',
63+
$traitReflection->getDisplayName(),
64+
$property->name->name,
65+
))->identifier('trait.duplicateProperty')
66+
->line($property->getStartLine())
67+
->nonIgnorable()
68+
->build();
69+
} else {
70+
$declaredProperties[$property->name->name] = true;
71+
}
72+
}
73+
}
74+
75+
$declaredFunctions = [];
76+
foreach ($node->getOriginalNode()->getMethods() as $method) {
77+
if ($method->name->toLowerString() === '__construct') {
78+
foreach ($method->params as $param) {
79+
if ($param->flags === 0) {
80+
continue;
81+
}
82+
83+
if (!$param->var instanceof Node\Expr\Variable || !is_string($param->var->name)) {
84+
throw new ShouldNotHappenException();
85+
}
86+
87+
$propertyName = $param->var->name;
88+
89+
if (array_key_exists($propertyName, $declaredProperties)) {
90+
$errors[] = RuleErrorBuilder::message(sprintf(
91+
'Cannot redeclare property %s::$%s.',
92+
$traitReflection->getDisplayName(),
93+
$propertyName,
94+
))->identifier('trait.duplicateProperty')
95+
->line($param->getStartLine())
96+
->nonIgnorable()
97+
->build();
98+
} else {
99+
$declaredProperties[$propertyName] = true;
100+
}
101+
}
102+
}
103+
if (array_key_exists(strtolower($method->name->name), $declaredFunctions)) {
104+
$errors[] = RuleErrorBuilder::message(sprintf(
105+
'Cannot redeclare method %s::%s().',
106+
$traitReflection->getDisplayName(),
107+
$method->name->name,
108+
))->identifier('trait.duplicateMethod')
109+
->line($method->getStartLine())
110+
->nonIgnorable()
111+
->build();
112+
} else {
113+
$declaredFunctions[strtolower($method->name->name)] = true;
114+
}
115+
}
116+
117+
return $errors;
118+
}
119+
120+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Classes;
4+
5+
use PHPStan\Rules\Rule;
6+
use PHPStan\Testing\RuleTestCase;
7+
8+
/**
9+
* @extends RuleTestCase<DuplicateTraitDeclarationRule>
10+
*/
11+
class DuplicateTraitDeclarationRuleTest extends RuleTestCase
12+
{
13+
14+
protected function getRule(): Rule
15+
{
16+
return new DuplicateTraitDeclarationRule();
17+
}
18+
19+
public function testBug14250(): void
20+
{
21+
$this->analyse([__DIR__ . '/data/bug-14250.php'], [
22+
[
23+
'Cannot redeclare method Bug14250\MyTrait::doSomething().',
24+
11,
25+
],
26+
]);
27+
}
28+
29+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug14250;
4+
5+
trait MyTrait
6+
{
7+
public function doSomething(): void
8+
{
9+
}
10+
11+
public function doSomething(): void
12+
{
13+
}
14+
}
15+
16+
class Foo
17+
{
18+
use MyTrait;
19+
}

0 commit comments

Comments
 (0)