Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions conf/config.neon
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ parameters:
reportPossiblyNonexistentConstantArrayOffset: false
checkMissingOverrideMethodAttribute: false
checkMissingOverridePropertyAttribute: null
checkMissingOverrideConstantAttribute: null
mixinExcludeClasses: []
scanFiles: []
scanDirectories: []
Expand Down
1 change: 1 addition & 0 deletions conf/parametersSchema.neon
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ parametersSchema:
reportPossiblyNonexistentConstantArrayOffset: bool()
checkMissingOverrideMethodAttribute: bool()
checkMissingOverridePropertyAttribute: schema(bool(), nullable())
checkMissingOverrideConstantAttribute: schema(bool(), nullable())
parallel: structure([
jobSize: int(),
processTimeout: float(),
Expand Down
5 changes: 5 additions & 0 deletions src/Php/PhpVersions.php
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,11 @@ public function supportsOverrideAttributeOnProperty(): TrinaryLogic
return IntegerRangeType::fromInterval(80500, null)->isSuperTypeOf($this->phpVersions)->result;
}

public function supportsOverrideAttributeOnClassConstant(): TrinaryLogic
{
return IntegerRangeType::fromInterval(80600, null)->isSuperTypeOf($this->phpVersions)->result;
}

public function supportsAttributesOnGlobalConstants(): TrinaryLogic
{
return IntegerRangeType::fromInterval(80500, null)->isSuperTypeOf($this->phpVersions)->result;
Expand Down
155 changes: 155 additions & 0 deletions src/Rules/Constants/OverrideAttributeOnConstantCheck.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\Constants;

use PhpParser\Node;
use PhpParser\Node\Attribute;
use PHPStan\Analyser\Scope;
use PHPStan\DependencyInjection\AutowiredParameter;
use PHPStan\DependencyInjection\AutowiredService;
use PHPStan\Reflection\ClassConstantReflection;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Rules\IdentifierRuleError;
use PHPStan\Rules\RuleErrorBuilder;
use function count;
use function sprintf;

/**
* Checks the #[\Override] attribute on class constants and enum cases (PHP 8.6+).
*/
#[AutowiredService]
final class OverrideAttributeOnConstantCheck
{

public function __construct(
#[AutowiredParameter]
private ?bool $checkMissingOverrideConstantAttribute,
#[AutowiredParameter]
private bool $checkMissingOverrideMethodAttribute,
)
{
}

/**
* @param Node\AttributeGroup[] $attrGroups
* @param Node\Stmt\ClassConst|Node\Stmt\EnumCase|null $fixableNode
* @return list<IdentifierRuleError>
*/
public function check(
Scope $scope,
ClassReflection $classReflection,
string $constantName,
?ClassConstantReflection $prototype,
array $attrGroups,
?Node $fixableNode,
bool $isEnumCase,
): array
{
$description = $isEnumCase ? 'Enum case' : 'Constant';

if ($prototype === null) {
if (!$this->hasOverrideAttribute($attrGroups)) {
return [];
}

$errorBuilder = RuleErrorBuilder::message(sprintf(
'%s %s::%s has #[\Override] attribute but does not override any constant.',
$description,
$classReflection->getDisplayName(false),
$constantName,
))
->nonIgnorable()
->identifier($isEnumCase ? 'enum.caseOverride' : 'classConstant.override');

if ($fixableNode !== null) {
$errorBuilder->fixNode($fixableNode, function (Node\Stmt\ClassConst|Node\Stmt\EnumCase $node) {
$node->attrGroups = $this->filterOverrideAttribute($node->attrGroups);
return $node;
});
}

return [$errorBuilder->build()];
}

if (
$scope->isInTrait()
|| $this->hasOverrideAttribute($attrGroups)
|| !$this->isMissingOverrideChecked($scope)
) {
return [];
}

$errorBuilder = RuleErrorBuilder::message(sprintf(
'%s %s::%s overrides constant %s::%s but is missing the #[\Override] attribute.',
$description,
$classReflection->getDisplayName(false),
$constantName,
$prototype->getDeclaringClass()->getDisplayName(false),
$prototype->getName(),
))
->identifier($isEnumCase ? 'enum.caseMissingOverride' : 'classConstant.missingOverride');

if ($fixableNode !== null) {
$errorBuilder->fixNode($fixableNode, static function (Node\Stmt\ClassConst|Node\Stmt\EnumCase $node) {
$node->attrGroups[] = new Node\AttributeGroup([
new Attribute(new Node\Name\FullyQualified('Override')),
]);

return $node;
});
}

return [$errorBuilder->build()];
}

private function isMissingOverrideChecked(Scope $scope): bool
{
if ($this->checkMissingOverrideConstantAttribute !== null) {
return $this->checkMissingOverrideConstantAttribute;
}

return $this->checkMissingOverrideMethodAttribute
&& $scope->getPhpVersion()->supportsOverrideAttributeOnClassConstant()->yes();
}

/**
* @param Node\AttributeGroup[] $attrGroups
* @return Node\AttributeGroup[]
*/
private function filterOverrideAttribute(array $attrGroups): array
{
foreach ($attrGroups as $i => $attrGroup) {
foreach ($attrGroup->attrs as $j => $attr) {
if ($attr->name->toLowerString() !== 'override') {
continue;
}

unset($attrGroup->attrs[$j]);
if (count($attrGroup->attrs) !== 0) {
continue;
}

unset($attrGroups[$i]);
}
}

return $attrGroups;
}

/**
* @param Node\AttributeGroup[] $attrGroups
*/
private function hasOverrideAttribute(array $attrGroups): bool
{
foreach ($attrGroups as $attrGroup) {
foreach ($attrGroup->attrs as $attr) {
if ($attr->name->toLowerString() === 'override') {
return true;
}
}
}

return false;
}

}
18 changes: 17 additions & 1 deletion src/Rules/Constants/OverridingConstantRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
use PHPStan\ShouldNotHappenException;
use PHPStan\Type\VerbosityLevel;
use function array_merge;
use function count;
use function sprintf;

/**
Expand All @@ -26,6 +27,7 @@ final class OverridingConstantRule implements Rule
public function __construct(
#[AutowiredParameter]
private bool $checkPhpDocMethodSignatures,
private OverrideAttributeOnConstantCheck $overrideAttributeCheck,
)
{
}
Expand All @@ -41,10 +43,24 @@ public function processNode(Node $node, Scope $scope): array
throw new ShouldNotHappenException();
}

$classReflection = $scope->getClassReflection();

// the attributes of `const A = 1, B = 2;` apply to every constant, so only fix single declarations
$fixableNode = count($node->consts) === 1 ? $node : null;

$errors = [];
foreach ($node->consts as $const) {
$constantName = $const->name->toString();
$errors = array_merge($errors, $this->processSingleConstant($scope->getClassReflection(), $constantName));
$errors = array_merge($errors, $this->overrideAttributeCheck->check(
$scope,
$classReflection,
$constantName,
$this->findPrototype($classReflection, $constantName),
$node->attrGroups,
$fixableNode,
false,
));
$errors = array_merge($errors, $this->processSingleConstant($classReflection, $constantName));
}

return $errors;
Expand Down
67 changes: 67 additions & 0 deletions src/Rules/EnumCases/OverridingEnumCaseRule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\EnumCases;

use PhpParser\Node;
use PHPStan\Analyser\Scope;
use PHPStan\DependencyInjection\RegisteredRule;
use PHPStan\Reflection\ClassConstantReflection;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Rules\Constants\OverrideAttributeOnConstantCheck;
use PHPStan\Rules\Rule;

/**
* @implements Rule<Node\Stmt\EnumCase>
*/
#[RegisteredRule(level: 0)]
final class OverridingEnumCaseRule implements Rule
{

public function __construct(private OverrideAttributeOnConstantCheck $overrideAttributeCheck)
{
}

public function getNodeType(): string
{
return Node\Stmt\EnumCase::class;
}

public function processNode(Node $node, Scope $scope): array
{
if (!$scope->isInClass()) {
return [];
}

$classReflection = $scope->getClassReflection();
if (!$classReflection->isEnum()) {
return [];
}

$caseName = $node->name->toString();

return $this->overrideAttributeCheck->check(
$scope,
$classReflection,
$caseName,
$this->findPrototype($classReflection, $caseName),
$node->attrGroups,
$node,
true,
);
}

/**
* An enum case can only override a constant of an implemented interface.
*/
private function findPrototype(ClassReflection $classReflection, string $caseName): ?ClassConstantReflection
{
foreach ($classReflection->getImmediateInterfaces() as $immediateInterface) {
if ($immediateInterface->hasConstant($caseName)) {
return $immediateInterface->getConstant($caseName);
}
}

return null;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\Constants;

use PHPStan\Rules\Rule;
use PHPStan\Testing\RuleTestCase;
use PHPUnit\Framework\Attributes\RequiresPhp;

/** @extends RuleTestCase<OverridingConstantRule> */
class OverridingConstantRuleConfigPhpTest extends RuleTestCase
{

protected function getRule(): Rule
{
return new OverridingConstantRule(
true,
new OverrideAttributeOnConstantCheck(
checkMissingOverrideConstantAttribute: null,
checkMissingOverrideMethodAttribute: true,
),
);
}

#[RequiresPhp('>= 8.2.0')]
public function testMissingOverrideAttributeNotCheckedOnPhpVersionRangeSpanning86(): void
{
$this->analyse([__DIR__ . '/data/constant-override-attr.php'], [
[
'Constant ConstantOverrideAttr\Bar::PRIVATE_FROM_PARENT has #[\Override] attribute but does not override any constant.',
28,
],
[
'Constant ConstantOverrideAttr\Bar::NOT_OVERRIDING has #[\Override] attribute but does not override any constant.',
31,
],
[
'Constant ConstantOverrideAttr\Baz::ALSO_NOT_OVERRIDING has #[\Override] attribute but does not override any constant.',
41,
],
[
'Constant ConstantOverrideAttr\BarInterface::NOT_OVERRIDING has #[\Override] attribute but does not override any constant.',
50,
],
[
'Constant ConstantOverrideAttr\UsesTraitWithoutParent::FROM_PARENT has #[\Override] attribute but does not override any constant.',
56,
],
]);
}

public static function getAdditionalConfigFiles(): array
{
return [
__DIR__ . '/data/constant-override-attr-php-version.neon',
];
}

}
Loading
Loading