Skip to content

Commit

Permalink
BleedingEdge - OverridingConstantRule
Browse files Browse the repository at this point in the history
  • Loading branch information
ondrejmirtes committed Aug 17, 2021
1 parent c0e78e4 commit 89acb0d
Show file tree
Hide file tree
Showing 10 changed files with 339 additions and 9 deletions.
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
"nette/utils": "^3.1.3",
"nikic/php-parser": "4.12.0",
"ondram/ci-detector": "^3.4.0",
"ondrejmirtes/better-reflection": "4.3.62",
"ondrejmirtes/better-reflection": "4.3.63",
"phpstan/php-8-stubs": "^0.1.22",
"phpstan/phpdoc-parser": "^0.5.5",
"react/child-process": "^0.6.1",
Expand Down
14 changes: 7 additions & 7 deletions composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions conf/config.level0.neon
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ conditionalTags:
phpstan.rules.rule: %featureToggles.apiRules%
PHPStan\Rules\Api\PhpStanNamespaceIn3rdPartyPackageRule:
phpstan.rules.rule: %featureToggles.apiRules%
PHPStan\Rules\Constants\OverridingConstantRule:
phpstan.rules.rule: %featureToggles.classConstants%
PHPStan\Rules\Functions\ClosureUsesThisRule:
phpstan.rules.rule: %featureToggles.closureUsesThis%
PHPStan\Rules\Missing\MissingClosureNativeReturnTypehintRule:
Expand Down Expand Up @@ -144,6 +146,11 @@ services:
checkFunctionNameCase: %checkFunctionNameCase%
reportMagicMethods: %reportMagicMethods%

-
class: PHPStan\Rules\Constants\OverridingConstantRule
arguments:
checkPhpDocMethodSignatures: %checkPhpDocMethodSignatures%

-
class: PHPStan\Rules\Methods\OverridingMethodRule
arguments:
Expand Down
5 changes: 5 additions & 0 deletions src/Php/PhpVersion.php
Original file line number Diff line number Diff line change
Expand Up @@ -127,4 +127,9 @@ public function isNullValidArgInMbSubstituteCharacter(): bool
return $this->versionId >= 80000;
}

public function isInterfaceConstantImplicitlyFinal(): bool
{
return $this->versionId < 80100;
}

}
9 changes: 9 additions & 0 deletions src/Reflection/ClassConstantReflection.php
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,15 @@ public function isPublic(): bool
return $this->reflection->isPublic();
}

public function isFinal(): bool
{
if (method_exists($this->reflection, 'isFinal')) {
return $this->reflection->isFinal();
}

return false;
}

public function isDeprecated(): TrinaryLogic
{
return TrinaryLogic::createFromBoolean($this->isDeprecated);
Expand Down
2 changes: 1 addition & 1 deletion src/Reflection/ClassReflection.php
Original file line number Diff line number Diff line change
Expand Up @@ -650,7 +650,7 @@ private function collectInterfaces(ClassReflection $interface): array
/**
* @return \PHPStan\Reflection\ClassReflection[]
*/
private function getImmediateInterfaces(): array
public function getImmediateInterfaces(): array
{
$indirectInterfaceNames = [];
$parent = $this->getParentClass();
Expand Down
140 changes: 140 additions & 0 deletions src/Rules/Constants/OverridingConstantRule.php
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 tests/PHPStan/Rules/Constants/OverridingConstantRuleTest.php
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 tests/PHPStan/Rules/Constants/data/overriding-constant.php
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';

}
Loading

0 comments on commit 89acb0d

Please sign in to comment.