Skip to content

Commit 89acb0d

Browse files
committed
BleedingEdge - OverridingConstantRule
1 parent c0e78e4 commit 89acb0d

File tree

10 files changed

+339
-9
lines changed

10 files changed

+339
-9
lines changed

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
"nette/utils": "^3.1.3",
2424
"nikic/php-parser": "4.12.0",
2525
"ondram/ci-detector": "^3.4.0",
26-
"ondrejmirtes/better-reflection": "4.3.62",
26+
"ondrejmirtes/better-reflection": "4.3.63",
2727
"phpstan/php-8-stubs": "^0.1.22",
2828
"phpstan/phpdoc-parser": "^0.5.5",
2929
"react/child-process": "^0.6.1",

composer.lock

Lines changed: 7 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

conf/config.level0.neon

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ conditionalTags:
1919
phpstan.rules.rule: %featureToggles.apiRules%
2020
PHPStan\Rules\Api\PhpStanNamespaceIn3rdPartyPackageRule:
2121
phpstan.rules.rule: %featureToggles.apiRules%
22+
PHPStan\Rules\Constants\OverridingConstantRule:
23+
phpstan.rules.rule: %featureToggles.classConstants%
2224
PHPStan\Rules\Functions\ClosureUsesThisRule:
2325
phpstan.rules.rule: %featureToggles.closureUsesThis%
2426
PHPStan\Rules\Missing\MissingClosureNativeReturnTypehintRule:
@@ -144,6 +146,11 @@ services:
144146
checkFunctionNameCase: %checkFunctionNameCase%
145147
reportMagicMethods: %reportMagicMethods%
146148

149+
-
150+
class: PHPStan\Rules\Constants\OverridingConstantRule
151+
arguments:
152+
checkPhpDocMethodSignatures: %checkPhpDocMethodSignatures%
153+
147154
-
148155
class: PHPStan\Rules\Methods\OverridingMethodRule
149156
arguments:

src/Php/PhpVersion.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,4 +127,9 @@ public function isNullValidArgInMbSubstituteCharacter(): bool
127127
return $this->versionId >= 80000;
128128
}
129129

130+
public function isInterfaceConstantImplicitlyFinal(): bool
131+
{
132+
return $this->versionId < 80100;
133+
}
134+
130135
}

src/Reflection/ClassConstantReflection.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,15 @@ public function isPublic(): bool
101101
return $this->reflection->isPublic();
102102
}
103103

104+
public function isFinal(): bool
105+
{
106+
if (method_exists($this->reflection, 'isFinal')) {
107+
return $this->reflection->isFinal();
108+
}
109+
110+
return false;
111+
}
112+
104113
public function isDeprecated(): TrinaryLogic
105114
{
106115
return TrinaryLogic::createFromBoolean($this->isDeprecated);

src/Reflection/ClassReflection.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -650,7 +650,7 @@ private function collectInterfaces(ClassReflection $interface): array
650650
/**
651651
* @return \PHPStan\Reflection\ClassReflection[]
652652
*/
653-
private function getImmediateInterfaces(): array
653+
public function getImmediateInterfaces(): array
654654
{
655655
$indirectInterfaceNames = [];
656656
$parent = $this->getParentClass();
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Constants;
4+
5+
use PhpParser\Node;
6+
use PHPStan\Analyser\Scope;
7+
use PHPStan\Php\PhpVersion;
8+
use PHPStan\Reflection\ClassConstantReflection;
9+
use PHPStan\Reflection\ClassReflection;
10+
use PHPStan\Reflection\ConstantReflection;
11+
use PHPStan\Rules\Rule;
12+
use PHPStan\Rules\RuleError;
13+
use PHPStan\Rules\RuleErrorBuilder;
14+
use PHPStan\Type\VerbosityLevel;
15+
16+
/**
17+
* @implements Rule<Node\Stmt\ClassConst>
18+
*/
19+
class OverridingConstantRule implements Rule
20+
{
21+
22+
private PhpVersion $phpVersion;
23+
24+
private bool $checkPhpDocMethodSignatures;
25+
26+
public function __construct(
27+
PhpVersion $phpVersion,
28+
bool $checkPhpDocMethodSignatures
29+
)
30+
{
31+
$this->phpVersion = $phpVersion;
32+
$this->checkPhpDocMethodSignatures = $checkPhpDocMethodSignatures;
33+
}
34+
35+
public function getNodeType(): string
36+
{
37+
return Node\Stmt\ClassConst::class;
38+
}
39+
40+
public function processNode(Node $node, Scope $scope): array
41+
{
42+
if (!$scope->isInClass()) {
43+
throw new \PHPStan\ShouldNotHappenException();
44+
}
45+
46+
$errors = [];
47+
foreach ($node->consts as $const) {
48+
$constantName = $const->name->toString();
49+
$errors = array_merge($errors, $this->processSingleConstant($scope->getClassReflection(), $constantName));
50+
}
51+
52+
return $errors;
53+
}
54+
55+
/**
56+
* @param string $constantName
57+
* @return RuleError[]
58+
*/
59+
private function processSingleConstant(ClassReflection $classReflection, string $constantName): array
60+
{
61+
$prototype = $this->findPrototype($classReflection, $constantName);
62+
if (!$prototype instanceof ClassConstantReflection) {
63+
return [];
64+
}
65+
66+
$constantReflection = $classReflection->getConstant($constantName);
67+
if (!$constantReflection instanceof ClassConstantReflection) {
68+
return [];
69+
}
70+
71+
$errors = [];
72+
if (
73+
$prototype->isFinal()
74+
|| (
75+
$this->phpVersion->isInterfaceConstantImplicitlyFinal()
76+
&& $prototype->getDeclaringClass()->isInterface()
77+
)
78+
) {
79+
$errors[] = RuleErrorBuilder::message(sprintf(
80+
'Constant %s::%s overrides final constant %s::%s.',
81+
$classReflection->getDisplayName(),
82+
$constantReflection->getName(),
83+
$prototype->getDeclaringClass()->getDisplayName(),
84+
$prototype->getName()
85+
))->nonIgnorable()->build();
86+
}
87+
88+
if (!$this->checkPhpDocMethodSignatures) {
89+
return $errors;
90+
}
91+
92+
if (!$prototype->hasPhpDocType()) {
93+
return $errors;
94+
}
95+
96+
if (!$constantReflection->hasPhpDocType()) {
97+
return $errors;
98+
}
99+
100+
if (!$prototype->getValueType()->isSuperTypeOf($constantReflection->getValueType())->yes()) {
101+
$errors[] = RuleErrorBuilder::message(sprintf(
102+
'Type %s of constant %s::%s is not covariant with type %s of constant %s::%s.',
103+
$constantReflection->getValueType()->describe(VerbosityLevel::value()),
104+
$constantReflection->getDeclaringClass()->getDisplayName(),
105+
$constantReflection->getName(),
106+
$prototype->getValueType()->describe(VerbosityLevel::value()),
107+
$prototype->getDeclaringClass()->getDisplayName(),
108+
$prototype->getName()
109+
))->build();
110+
}
111+
112+
return $errors;
113+
}
114+
115+
private function findPrototype(ClassReflection $classReflection, string $constantName): ?ConstantReflection
116+
{
117+
foreach ($classReflection->getImmediateInterfaces() as $immediateInterface) {
118+
if ($immediateInterface->hasConstant($constantName)) {
119+
return $immediateInterface->getConstant($constantName);
120+
}
121+
}
122+
123+
$parentClass = $classReflection->getParentClass();
124+
if ($parentClass === false) {
125+
return null;
126+
}
127+
128+
if (!$parentClass->hasConstant($constantName)) {
129+
return null;
130+
}
131+
132+
$constant = $parentClass->getConstant($constantName);
133+
if ($constant->isPrivate()) {
134+
return null;
135+
}
136+
137+
return $constant;
138+
}
139+
140+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Constants;
4+
5+
use PHPStan\Php\PhpVersion;
6+
use PHPStan\Testing\RuleTestCase;
7+
8+
/** @extends RuleTestCase<OverridingConstantRule> */
9+
class OverridingConstantRuleTest extends RuleTestCase
10+
{
11+
12+
protected function getRule(): \PHPStan\Rules\Rule
13+
{
14+
return new OverridingConstantRule(new PhpVersion(PHP_VERSION_ID), true);
15+
}
16+
17+
public function testRule(): void
18+
{
19+
$this->analyse([__DIR__ . '/data/overriding-constant.php'], [
20+
[
21+
'Type string of constant OverridingConstant\Bar::BAR is not covariant with type int of constant OverridingConstant\Foo::BAR.',
22+
30,
23+
],
24+
[
25+
'Type int|string of constant OverridingConstant\Bar::IPSUM is not covariant with type int of constant OverridingConstant\Foo::IPSUM.',
26+
39,
27+
],
28+
]);
29+
}
30+
31+
public function testFinal(): void
32+
{
33+
if (!self::$useStaticReflectionProvider) {
34+
$this->markTestSkipped('Test requires static reflection.');
35+
}
36+
37+
$errors = [
38+
[
39+
'Constant OverridingFinalConstant\Bar::FOO overrides final constant OverridingFinalConstant\Foo::FOO.',
40+
18,
41+
],
42+
[
43+
'Constant OverridingFinalConstant\Bar::BAR overrides final constant OverridingFinalConstant\Foo::BAR.',
44+
19,
45+
],
46+
];
47+
48+
if (PHP_VERSION_ID < 80100) {
49+
$errors[] = [
50+
'Constant OverridingFinalConstant\Baz::FOO overrides final constant OverridingFinalConstant\FooInterface::FOO.',
51+
34,
52+
];
53+
}
54+
55+
$errors[] = [
56+
'Constant OverridingFinalConstant\Baz::BAR overrides final constant OverridingFinalConstant\FooInterface::BAR.',
57+
35,
58+
];
59+
60+
if (PHP_VERSION_ID < 80100) {
61+
$errors[] = [
62+
'Constant OverridingFinalConstant\Lorem::FOO overrides final constant OverridingFinalConstant\BarInterface::FOO.',
63+
51,
64+
];
65+
}
66+
67+
$errors[] = [
68+
'Type string of constant OverridingFinalConstant\Lorem::FOO is not covariant with type int of constant OverridingFinalConstant\BarInterface::FOO.',
69+
51,
70+
];
71+
72+
$this->analyse([__DIR__ . '/data/overriding-final-constant.php'], $errors);
73+
}
74+
75+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?php
2+
3+
namespace OverridingConstant;
4+
5+
class Foo
6+
{
7+
8+
const FOO = 1;
9+
10+
/** @var int */
11+
const BAR = 1;
12+
13+
/** @var int */
14+
private const BAZ = 1;
15+
16+
/** @var string|int */
17+
const LOREM = 1;
18+
19+
/** @var int */
20+
const IPSUM = 1;
21+
22+
}
23+
24+
class Bar extends Foo
25+
{
26+
27+
const FOO = 'foo';
28+
29+
/** @var string */
30+
const BAR = 'bar';
31+
32+
/** @var string */
33+
const BAZ = 'foo';
34+
35+
/** @var string */
36+
const LOREM = 'foo';
37+
38+
/** @var int|string */
39+
const IPSUM = 'foo';
40+
41+
}

0 commit comments

Comments
 (0)