Skip to content

Commit f94a3c3

Browse files
committed
ConflictingTraitConstantsRule
1 parent 64ffdd6 commit f94a3c3

File tree

6 files changed

+437
-0
lines changed

6 files changed

+437
-0
lines changed

Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ lint:
5858
--exclude tests/PHPStan/Rules/Methods/data/bug-9014.php \
5959
--exclude tests/PHPStan/Rules/Methods/data/bug-10101.php \
6060
--exclude tests/PHPStan/Rules/Methods/data/final-method-by-phpdoc.php \
61+
--exclude tests/PHPStan/Rules/Traits/data/conflicting-trait-constants-types.php \
6162
src tests
6263

6364
cs:

conf/config.level0.neon

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ rules:
9191
- PHPStan\Rules\Properties\PropertiesInInterfaceRule
9292
- PHPStan\Rules\Properties\PropertyAttributesRule
9393
- PHPStan\Rules\Properties\ReadOnlyPropertyRule
94+
- PHPStan\Rules\Traits\ConflictingTraitConstantsRule
9495
- PHPStan\Rules\Traits\ConstantsInTraitsRule
9596
- PHPStan\Rules\Variables\UnsetRule
9697
- PHPStan\Rules\Whitespace\FileWhitespaceRule
Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Traits;
4+
5+
use PhpParser\Node;
6+
use PHPStan\Analyser\Scope;
7+
use PHPStan\BetterReflection\Reflection\Adapter\ReflectionClassConstant;
8+
use PHPStan\Reflection\ClassReflection;
9+
use PHPStan\Reflection\InitializerExprContext;
10+
use PHPStan\Reflection\InitializerExprTypeResolver;
11+
use PHPStan\Rules\Rule;
12+
use PHPStan\Rules\RuleError;
13+
use PHPStan\Rules\RuleErrorBuilder;
14+
use PHPStan\Type\ParserNodeTypeToPHPStanType;
15+
use PHPStan\Type\TypehintHelper;
16+
use PHPStan\Type\VerbosityLevel;
17+
use function sprintf;
18+
19+
/**
20+
* @implements Rule<Node\Stmt\ClassConst>
21+
*/
22+
class ConflictingTraitConstantsRule implements Rule
23+
{
24+
25+
public function __construct(private InitializerExprTypeResolver $initializerExprTypeResolver)
26+
{
27+
}
28+
29+
public function getNodeType(): string
30+
{
31+
return Node\Stmt\ClassConst::class;
32+
}
33+
34+
public function processNode(Node $node, Scope $scope): array
35+
{
36+
if (!$scope->isInClass()) {
37+
return [];
38+
}
39+
40+
$classReflection = $scope->getClassReflection();
41+
$traitConstants = [];
42+
foreach ($classReflection->getTraits(true) as $trait) {
43+
foreach ($trait->getNativeReflection()->getReflectionConstants() as $constant) {
44+
$traitConstants[] = $constant;
45+
}
46+
}
47+
48+
$errors = [];
49+
foreach ($node->consts as $const) {
50+
foreach ($traitConstants as $traitConstant) {
51+
if ($traitConstant->getName() !== $const->name->toString()) {
52+
continue;
53+
}
54+
55+
foreach ($this->processSingleConstant($classReflection, $traitConstant, $node, $const->value) as $error) {
56+
$errors[] = $error;
57+
}
58+
}
59+
}
60+
61+
return $errors;
62+
}
63+
64+
/**
65+
* @return list<RuleError>
66+
*/
67+
private function processSingleConstant(ClassReflection $classReflection, ReflectionClassConstant $traitConstant, Node\Stmt\ClassConst $classConst, Node\Expr $valueExpr): array
68+
{
69+
$errors = [];
70+
if ($traitConstant->isPublic()) {
71+
if ($classConst->isProtected()) {
72+
$errors[] = RuleErrorBuilder::message(sprintf(
73+
'Protected constant %s::%s overriding public constant %s::%s should also be public.',
74+
$classReflection->getDisplayName(),
75+
$traitConstant->getName(),
76+
$traitConstant->getDeclaringClass()->getName(),
77+
$traitConstant->getName(),
78+
))->nonIgnorable()->build();
79+
} elseif ($classConst->isPrivate()) {
80+
$errors[] = RuleErrorBuilder::message(sprintf(
81+
'Private constant %s::%s overriding public constant %s::%s should also be public.',
82+
$classReflection->getDisplayName(),
83+
$traitConstant->getName(),
84+
$traitConstant->getDeclaringClass()->getName(),
85+
$traitConstant->getName(),
86+
))->nonIgnorable()->build();
87+
}
88+
} elseif ($traitConstant->isProtected()) {
89+
if ($classConst->isPublic()) {
90+
$errors[] = RuleErrorBuilder::message(sprintf(
91+
'Public constant %s::%s overriding protected constant %s::%s should also be protected.',
92+
$classReflection->getDisplayName(),
93+
$traitConstant->getName(),
94+
$traitConstant->getDeclaringClass()->getName(),
95+
$traitConstant->getName(),
96+
))->nonIgnorable()->build();
97+
} elseif ($classConst->isPrivate()) {
98+
$errors[] = RuleErrorBuilder::message(sprintf(
99+
'Private constant %s::%s overriding protected constant %s::%s should also be protected.',
100+
$classReflection->getDisplayName(),
101+
$traitConstant->getName(),
102+
$traitConstant->getDeclaringClass()->getName(),
103+
$traitConstant->getName(),
104+
))->nonIgnorable()->build();
105+
}
106+
} elseif ($traitConstant->isPrivate()) {
107+
if ($classConst->isPublic()) {
108+
$errors[] = RuleErrorBuilder::message(sprintf(
109+
'Public constant %s::%s overriding private constant %s::%s should also be private.',
110+
$classReflection->getDisplayName(),
111+
$traitConstant->getName(),
112+
$traitConstant->getDeclaringClass()->getName(),
113+
$traitConstant->getName(),
114+
))->nonIgnorable()->build();
115+
} elseif ($classConst->isProtected()) {
116+
$errors[] = RuleErrorBuilder::message(sprintf(
117+
'Protected constant %s::%s overriding private constant %s::%s should also be private.',
118+
$classReflection->getDisplayName(),
119+
$traitConstant->getName(),
120+
$traitConstant->getDeclaringClass()->getName(),
121+
$traitConstant->getName(),
122+
))->nonIgnorable()->build();
123+
}
124+
}
125+
126+
if ($traitConstant->isFinal()) {
127+
if (!$classConst->isFinal()) {
128+
$errors[] = RuleErrorBuilder::message(sprintf(
129+
'Non-final constant %s::%s overriding final constant %s::%s should also be final.',
130+
$classReflection->getDisplayName(),
131+
$traitConstant->getName(),
132+
$traitConstant->getDeclaringClass()->getName(),
133+
$traitConstant->getName(),
134+
))->nonIgnorable()->build();
135+
}
136+
} elseif ($classConst->isFinal()) {
137+
$errors[] = RuleErrorBuilder::message(sprintf(
138+
'Final constant %s::%s overriding non-final constant %s::%s should also be non-final.',
139+
$classReflection->getDisplayName(),
140+
$traitConstant->getName(),
141+
$traitConstant->getDeclaringClass()->getName(),
142+
$traitConstant->getName(),
143+
))->nonIgnorable()->build();
144+
}
145+
146+
$traitNativeType = $traitConstant->getType();
147+
$constantNativeType = $classConst->type;
148+
$traitDeclaringClass = $traitConstant->getDeclaringClass();
149+
if ($traitNativeType === null) {
150+
if ($constantNativeType !== null) {
151+
$constantNativeTypeType = ParserNodeTypeToPHPStanType::resolve($constantNativeType, $classReflection);
152+
$errors[] = RuleErrorBuilder::message(sprintf(
153+
'Constant %s::%s (%s) overriding constant %s::%s should not have a native type.',
154+
$classReflection->getDisplayName(),
155+
$traitConstant->getName(),
156+
$constantNativeTypeType->describe(VerbosityLevel::typeOnly()),
157+
$traitConstant->getDeclaringClass()->getName(),
158+
$traitConstant->getName(),
159+
))->nonIgnorable()->build();
160+
}
161+
} elseif ($constantNativeType === null) {
162+
$traitNativeTypeType = TypehintHelper::decideTypeFromReflection($traitNativeType, null, $traitDeclaringClass->getName());
163+
$errors[] = RuleErrorBuilder::message(sprintf(
164+
'Constant %s::%s overriding constant %s::%s (%s) should also have native type %s.',
165+
$classReflection->getDisplayName(),
166+
$traitConstant->getName(),
167+
$traitConstant->getDeclaringClass()->getName(),
168+
$traitConstant->getName(),
169+
$traitNativeTypeType->describe(VerbosityLevel::typeOnly()),
170+
$traitNativeTypeType->describe(VerbosityLevel::typeOnly()),
171+
))->nonIgnorable()->build();
172+
} else {
173+
$traitNativeTypeType = TypehintHelper::decideTypeFromReflection($traitNativeType, null, $traitDeclaringClass->getName());
174+
$constantNativeTypeType = ParserNodeTypeToPHPStanType::resolve($constantNativeType, $classReflection);
175+
if (!$traitNativeTypeType->equals($constantNativeTypeType)) {
176+
$errors[] = RuleErrorBuilder::message(sprintf(
177+
'Constant %s::%s (%s) overriding constant %s::%s (%s) should have the same native type %s.',
178+
$classReflection->getDisplayName(),
179+
$traitConstant->getName(),
180+
$constantNativeTypeType->describe(VerbosityLevel::typeOnly()),
181+
$traitConstant->getDeclaringClass()->getName(),
182+
$traitConstant->getName(),
183+
$traitNativeTypeType->describe(VerbosityLevel::typeOnly()),
184+
$traitNativeTypeType->describe(VerbosityLevel::typeOnly()),
185+
))->nonIgnorable()->build();
186+
}
187+
}
188+
189+
$classConstantValueType = $this->initializerExprTypeResolver->getType($valueExpr, InitializerExprContext::fromClassReflection($classReflection));
190+
$traitConstantValueType = $this->initializerExprTypeResolver->getType(
191+
$traitConstant->getValueExpression(),
192+
InitializerExprContext::fromClass(
193+
$traitDeclaringClass->getName(),
194+
$traitDeclaringClass->getFileName() !== false ? $traitDeclaringClass->getFileName() : null,
195+
),
196+
);
197+
if (!$classConstantValueType->equals($traitConstantValueType)) {
198+
$errors[] = RuleErrorBuilder::message(sprintf(
199+
'Constant %s::%s with value %s overriding constant %s::%s with different value %s should have the same value.',
200+
$classReflection->getDisplayName(),
201+
$traitConstant->getName(),
202+
$classConstantValueType->describe(VerbosityLevel::value()),
203+
$traitConstant->getDeclaringClass()->getName(),
204+
$traitConstant->getName(),
205+
$traitConstantValueType->describe(VerbosityLevel::value()),
206+
))->nonIgnorable()->build();
207+
}
208+
209+
return $errors;
210+
}
211+
212+
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Traits;
4+
5+
use PHPStan\Reflection\InitializerExprTypeResolver;
6+
use PHPStan\Rules\Rule as TRule;
7+
use PHPStan\Testing\RuleTestCase;
8+
9+
/**
10+
* @extends RuleTestCase<ConflictingTraitConstantsRule>
11+
*/
12+
class ConflictingTraitConstantsRuleTest extends RuleTestCase
13+
{
14+
15+
protected function getRule(): TRule
16+
{
17+
return new ConflictingTraitConstantsRule(self::getContainer()->getByType(InitializerExprTypeResolver::class));
18+
}
19+
20+
public function testRule(): void
21+
{
22+
$this->analyse([__DIR__ . '/data/conflicting-trait-constants.php'], [
23+
[
24+
'Protected constant ConflictingTraitConstants\Bar::PUBLIC_CONSTANT overriding public constant ConflictingTraitConstants\Foo::PUBLIC_CONSTANT should also be public.',
25+
23,
26+
],
27+
[
28+
'Private constant ConflictingTraitConstants\Bar2::PUBLIC_CONSTANT overriding public constant ConflictingTraitConstants\Foo::PUBLIC_CONSTANT should also be public.',
29+
32,
30+
],
31+
[
32+
'Public constant ConflictingTraitConstants\Bar3::PROTECTED_CONSTANT overriding protected constant ConflictingTraitConstants\Foo::PROTECTED_CONSTANT should also be protected.',
33+
41,
34+
],
35+
[
36+
'Private constant ConflictingTraitConstants\Bar4::PROTECTED_CONSTANT overriding protected constant ConflictingTraitConstants\Foo::PROTECTED_CONSTANT should also be protected.',
37+
50,
38+
],
39+
[
40+
'Protected constant ConflictingTraitConstants\Bar5::PRIVATE_CONSTANT overriding private constant ConflictingTraitConstants\Foo::PRIVATE_CONSTANT should also be private.',
41+
59,
42+
],
43+
[
44+
'Public constant ConflictingTraitConstants\Bar5::PRIVATE_CONSTANT overriding private constant ConflictingTraitConstants\Foo::PRIVATE_CONSTANT should also be private.',
45+
68,
46+
],
47+
[
48+
'Non-final constant ConflictingTraitConstants\Bar6::PUBLIC_FINAL_CONSTANT overriding final constant ConflictingTraitConstants\Foo::PUBLIC_FINAL_CONSTANT should also be final.',
49+
77,
50+
],
51+
[
52+
'Final constant ConflictingTraitConstants\Bar7::PUBLIC_CONSTANT overriding non-final constant ConflictingTraitConstants\Foo::PUBLIC_CONSTANT should also be non-final.',
53+
86,
54+
],
55+
[
56+
'Constant ConflictingTraitConstants\Bar8::PUBLIC_CONSTANT with value 2 overriding constant ConflictingTraitConstants\Foo::PUBLIC_CONSTANT with different value 1 should have the same value.',
57+
96,
58+
],
59+
]);
60+
}
61+
62+
public function testNativeTypes(): void
63+
{
64+
if (PHP_VERSION_ID < 80300) {
65+
$this->markTestSkipped('Test requires PHP 8.3.');
66+
}
67+
68+
$this->analyse([__DIR__ . '/data/conflicting-trait-constants-types.php'], [
69+
[
70+
'Constant ConflictingTraitConstantsTypes\Baz::FOO_CONST (int) overriding constant ConflictingTraitConstantsTypes\Foo::FOO_CONST (int|string) should have the same native type int|string.',
71+
28,
72+
],
73+
[
74+
'Constant ConflictingTraitConstantsTypes\Baz::BAR_CONST (int) overriding constant ConflictingTraitConstantsTypes\Foo::BAR_CONST should not have a native type.',
75+
30,
76+
],
77+
[
78+
'Constant ConflictingTraitConstantsTypes\Lorem::FOO_CONST overriding constant ConflictingTraitConstantsTypes\Foo::FOO_CONST (int|string) should also have native type int|string.',
79+
39,
80+
],
81+
]);
82+
}
83+
84+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?php
2+
3+
namespace ConflictingTraitConstantsTypes;
4+
5+
trait Foo
6+
{
7+
8+
public const int|string FOO_CONST = 1;
9+
10+
public const BAR_CONST = 1;
11+
12+
}
13+
14+
class Bar
15+
{
16+
17+
use Foo;
18+
19+
public const int|string FOO_CONST = 1;
20+
21+
}
22+
23+
class Baz
24+
{
25+
26+
use Foo;
27+
28+
public const int FOO_CONST = 1;
29+
30+
public const int BAR_CONST = 1;
31+
32+
}
33+
34+
class Lorem
35+
{
36+
37+
use Foo;
38+
39+
public const FOO_CONST = 1;
40+
41+
}

0 commit comments

Comments
 (0)