-
Notifications
You must be signed in to change notification settings - Fork 467
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
BleedingEdge - OverridingConstantRule
- Loading branch information
1 parent
c0e78e4
commit 89acb0d
Showing
10 changed files
with
339 additions
and
9 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,140 @@ | ||
<?php declare(strict_types = 1); | ||
|
||
namespace PHPStan\Rules\Constants; | ||
|
||
use PhpParser\Node; | ||
use PHPStan\Analyser\Scope; | ||
use PHPStan\Php\PhpVersion; | ||
use PHPStan\Reflection\ClassConstantReflection; | ||
use PHPStan\Reflection\ClassReflection; | ||
use PHPStan\Reflection\ConstantReflection; | ||
use PHPStan\Rules\Rule; | ||
use PHPStan\Rules\RuleError; | ||
use PHPStan\Rules\RuleErrorBuilder; | ||
use PHPStan\Type\VerbosityLevel; | ||
|
||
/** | ||
* @implements Rule<Node\Stmt\ClassConst> | ||
*/ | ||
class OverridingConstantRule implements Rule | ||
{ | ||
|
||
private PhpVersion $phpVersion; | ||
|
||
private bool $checkPhpDocMethodSignatures; | ||
|
||
public function __construct( | ||
PhpVersion $phpVersion, | ||
bool $checkPhpDocMethodSignatures | ||
) | ||
{ | ||
$this->phpVersion = $phpVersion; | ||
$this->checkPhpDocMethodSignatures = $checkPhpDocMethodSignatures; | ||
} | ||
|
||
public function getNodeType(): string | ||
{ | ||
return Node\Stmt\ClassConst::class; | ||
} | ||
|
||
public function processNode(Node $node, Scope $scope): array | ||
{ | ||
if (!$scope->isInClass()) { | ||
throw new \PHPStan\ShouldNotHappenException(); | ||
} | ||
|
||
$errors = []; | ||
foreach ($node->consts as $const) { | ||
$constantName = $const->name->toString(); | ||
$errors = array_merge($errors, $this->processSingleConstant($scope->getClassReflection(), $constantName)); | ||
} | ||
|
||
return $errors; | ||
} | ||
|
||
/** | ||
* @param string $constantName | ||
* @return RuleError[] | ||
*/ | ||
private function processSingleConstant(ClassReflection $classReflection, string $constantName): array | ||
{ | ||
$prototype = $this->findPrototype($classReflection, $constantName); | ||
if (!$prototype instanceof ClassConstantReflection) { | ||
return []; | ||
} | ||
|
||
$constantReflection = $classReflection->getConstant($constantName); | ||
if (!$constantReflection instanceof ClassConstantReflection) { | ||
return []; | ||
} | ||
|
||
$errors = []; | ||
if ( | ||
$prototype->isFinal() | ||
|| ( | ||
$this->phpVersion->isInterfaceConstantImplicitlyFinal() | ||
&& $prototype->getDeclaringClass()->isInterface() | ||
) | ||
) { | ||
$errors[] = RuleErrorBuilder::message(sprintf( | ||
'Constant %s::%s overrides final constant %s::%s.', | ||
$classReflection->getDisplayName(), | ||
$constantReflection->getName(), | ||
$prototype->getDeclaringClass()->getDisplayName(), | ||
$prototype->getName() | ||
))->nonIgnorable()->build(); | ||
} | ||
|
||
if (!$this->checkPhpDocMethodSignatures) { | ||
return $errors; | ||
} | ||
|
||
if (!$prototype->hasPhpDocType()) { | ||
return $errors; | ||
} | ||
|
||
if (!$constantReflection->hasPhpDocType()) { | ||
return $errors; | ||
} | ||
|
||
if (!$prototype->getValueType()->isSuperTypeOf($constantReflection->getValueType())->yes()) { | ||
$errors[] = RuleErrorBuilder::message(sprintf( | ||
'Type %s of constant %s::%s is not covariant with type %s of constant %s::%s.', | ||
$constantReflection->getValueType()->describe(VerbosityLevel::value()), | ||
$constantReflection->getDeclaringClass()->getDisplayName(), | ||
$constantReflection->getName(), | ||
$prototype->getValueType()->describe(VerbosityLevel::value()), | ||
$prototype->getDeclaringClass()->getDisplayName(), | ||
$prototype->getName() | ||
))->build(); | ||
} | ||
|
||
return $errors; | ||
} | ||
|
||
private function findPrototype(ClassReflection $classReflection, string $constantName): ?ConstantReflection | ||
{ | ||
foreach ($classReflection->getImmediateInterfaces() as $immediateInterface) { | ||
if ($immediateInterface->hasConstant($constantName)) { | ||
return $immediateInterface->getConstant($constantName); | ||
} | ||
} | ||
|
||
$parentClass = $classReflection->getParentClass(); | ||
if ($parentClass === false) { | ||
return null; | ||
} | ||
|
||
if (!$parentClass->hasConstant($constantName)) { | ||
return null; | ||
} | ||
|
||
$constant = $parentClass->getConstant($constantName); | ||
if ($constant->isPrivate()) { | ||
return null; | ||
} | ||
|
||
return $constant; | ||
} | ||
|
||
} |
75 changes: 75 additions & 0 deletions
75
tests/PHPStan/Rules/Constants/OverridingConstantRuleTest.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
<?php declare(strict_types = 1); | ||
|
||
namespace PHPStan\Rules\Constants; | ||
|
||
use PHPStan\Php\PhpVersion; | ||
use PHPStan\Testing\RuleTestCase; | ||
|
||
/** @extends RuleTestCase<OverridingConstantRule> */ | ||
class OverridingConstantRuleTest extends RuleTestCase | ||
{ | ||
|
||
protected function getRule(): \PHPStan\Rules\Rule | ||
{ | ||
return new OverridingConstantRule(new PhpVersion(PHP_VERSION_ID), true); | ||
} | ||
|
||
public function testRule(): void | ||
{ | ||
$this->analyse([__DIR__ . '/data/overriding-constant.php'], [ | ||
[ | ||
'Type string of constant OverridingConstant\Bar::BAR is not covariant with type int of constant OverridingConstant\Foo::BAR.', | ||
30, | ||
], | ||
[ | ||
'Type int|string of constant OverridingConstant\Bar::IPSUM is not covariant with type int of constant OverridingConstant\Foo::IPSUM.', | ||
39, | ||
], | ||
]); | ||
} | ||
|
||
public function testFinal(): void | ||
{ | ||
if (!self::$useStaticReflectionProvider) { | ||
$this->markTestSkipped('Test requires static reflection.'); | ||
} | ||
|
||
$errors = [ | ||
[ | ||
'Constant OverridingFinalConstant\Bar::FOO overrides final constant OverridingFinalConstant\Foo::FOO.', | ||
18, | ||
], | ||
[ | ||
'Constant OverridingFinalConstant\Bar::BAR overrides final constant OverridingFinalConstant\Foo::BAR.', | ||
19, | ||
], | ||
]; | ||
|
||
if (PHP_VERSION_ID < 80100) { | ||
$errors[] = [ | ||
'Constant OverridingFinalConstant\Baz::FOO overrides final constant OverridingFinalConstant\FooInterface::FOO.', | ||
34, | ||
]; | ||
} | ||
|
||
$errors[] = [ | ||
'Constant OverridingFinalConstant\Baz::BAR overrides final constant OverridingFinalConstant\FooInterface::BAR.', | ||
35, | ||
]; | ||
|
||
if (PHP_VERSION_ID < 80100) { | ||
$errors[] = [ | ||
'Constant OverridingFinalConstant\Lorem::FOO overrides final constant OverridingFinalConstant\BarInterface::FOO.', | ||
51, | ||
]; | ||
} | ||
|
||
$errors[] = [ | ||
'Type string of constant OverridingFinalConstant\Lorem::FOO is not covariant with type int of constant OverridingFinalConstant\BarInterface::FOO.', | ||
51, | ||
]; | ||
|
||
$this->analyse([__DIR__ . '/data/overriding-final-constant.php'], $errors); | ||
} | ||
|
||
} |
41 changes: 41 additions & 0 deletions
41
tests/PHPStan/Rules/Constants/data/overriding-constant.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
<?php | ||
|
||
namespace OverridingConstant; | ||
|
||
class Foo | ||
{ | ||
|
||
const FOO = 1; | ||
|
||
/** @var int */ | ||
const BAR = 1; | ||
|
||
/** @var int */ | ||
private const BAZ = 1; | ||
|
||
/** @var string|int */ | ||
const LOREM = 1; | ||
|
||
/** @var int */ | ||
const IPSUM = 1; | ||
|
||
} | ||
|
||
class Bar extends Foo | ||
{ | ||
|
||
const FOO = 'foo'; | ||
|
||
/** @var string */ | ||
const BAR = 'bar'; | ||
|
||
/** @var string */ | ||
const BAZ = 'foo'; | ||
|
||
/** @var string */ | ||
const LOREM = 'foo'; | ||
|
||
/** @var int|string */ | ||
const IPSUM = 'foo'; | ||
|
||
} |
Oops, something went wrong.