Skip to content

Commit 8b85e62

Browse files
committed
[PHP 8.5] Support for #[Override] on properties
1 parent 216c8b1 commit 8b85e62

13 files changed

+363
-4
lines changed

conf/config.neon

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ parameters:
8282
reportPossiblyNonexistentGeneralArrayOffset: false
8383
reportPossiblyNonexistentConstantArrayOffset: false
8484
checkMissingOverrideMethodAttribute: false
85+
checkMissingOverridePropertyAttribute: %checkMissingOverrideMethodAttribute%
8586
mixinExcludeClasses: []
8687
scanFiles: []
8788
scanDirectories: []

conf/parametersSchema.neon

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ parametersSchema:
9191
reportPossiblyNonexistentGeneralArrayOffset: bool()
9292
reportPossiblyNonexistentConstantArrayOffset: bool()
9393
checkMissingOverrideMethodAttribute: bool()
94+
checkMissingOverridePropertyAttribute: bool()
9495
parallel: structure([
9596
jobSize: int(),
9697
processTimeout: float(),

src/Analyser/NodeScopeResolver.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1038,6 +1038,7 @@ private function processStmtNode(
10381038

10391039
$propStmt = clone $stmt;
10401040
$propStmt->setAttributes($prop->getAttributes());
1041+
$propStmt->setAttribute('originalPropertyStmt', $stmt);
10411042
$nodeCallback(
10421043
new ClassPropertyNode(
10431044
$propertyName,

src/Node/ClassPropertyNode.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,11 @@ public function isPromotedFromTrait(): bool
6565
return $this->isPromotedFromTrait;
6666
}
6767

68+
public function getOriginalNode(): Node\Stmt\Property|Node\Param
69+
{
70+
return $this->originalNode;
71+
}
72+
6873
public function getPhpDoc(): ?string
6974
{
7075
return $this->phpDoc;
@@ -187,4 +192,12 @@ public function isReadable(): bool
187192
return $this->classReflection->getNativeProperty($this->name)->isReadable();
188193
}
189194

195+
/**
196+
* @return Node\AttributeGroup[]
197+
*/
198+
public function getAttrGroups(): array
199+
{
200+
return $this->originalNode->attrGroups;
201+
}
202+
190203
}

src/Php/PhpVersion.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -459,4 +459,9 @@ public function supportsDeprecatedTraits(): bool
459459
return $this->versionId >= 80500;
460460
}
461461

462+
public function supportsOverrideAttributeOnProperty(): bool
463+
{
464+
return $this->versionId >= 80500;
465+
}
466+
462467
}

src/Rules/Properties/OverridingPropertyRule.php

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace PHPStan\Rules\Properties;
44

55
use PhpParser\Node;
6+
use PhpParser\Node\Attribute;
67
use PHPStan\Analyser\Scope;
78
use PHPStan\DependencyInjection\AutowiredParameter;
89
use PHPStan\DependencyInjection\RegisteredRule;
@@ -30,6 +31,8 @@ public function __construct(
3031
private bool $checkPhpDocMethodSignatures,
3132
#[AutowiredParameter(ref: '%reportMaybesInPropertyPhpDocTypes%')]
3233
private bool $reportMaybes,
34+
#[AutowiredParameter]
35+
private bool $checkMissingOverridePropertyAttribute,
3336
)
3437
{
3538
}
@@ -44,10 +47,63 @@ public function processNode(Node $node, Scope $scope): array
4447
$classReflection = $node->getClassReflection();
4548
$prototype = $this->findPrototype($classReflection, $node->getName());
4649
if ($prototype === null) {
50+
if (
51+
$this->phpVersion->supportsOverrideAttribute()
52+
&& $this->hasOverrideAttribute($node->getAttrGroups())
53+
) {
54+
$originalNode = $node->getOriginalNode();
55+
if ($originalNode instanceof Node\Stmt\Property) {
56+
/** @var Node\Stmt\Property $originalNode */
57+
$originalNode = $originalNode->getAttribute('originalPropertyStmt');
58+
}
59+
60+
return [
61+
RuleErrorBuilder::message(sprintf(
62+
'Property %s::$%s has #[\Override] attribute but does not override any property.',
63+
$node->getClassReflection()->getDisplayName(),
64+
$node->getName(),
65+
))
66+
->nonIgnorable()
67+
->identifier('property.override')
68+
->fixNode($originalNode, function ($property) {
69+
$property->attrGroups = $this->filterOverrideAttribute($property->attrGroups);
70+
return $property;
71+
})
72+
->build(),
73+
];
74+
}
4775
return [];
4876
}
4977

5078
$errors = [];
79+
if (
80+
$this->phpVersion->supportsOverrideAttributeOnProperty()
81+
&& $this->checkMissingOverridePropertyAttribute
82+
&& !$scope->isInTrait()
83+
&& !$this->hasOverrideAttribute($node->getAttrGroups())
84+
) {
85+
$originalNode = $node->getOriginalNode();
86+
if ($originalNode instanceof Node\Stmt\Property) {
87+
/** @var Node\Stmt\Property $originalNode */
88+
$originalNode = $originalNode->getAttribute('originalPropertyStmt');
89+
}
90+
$errors[] = RuleErrorBuilder::message(sprintf(
91+
'Property %s::$%s overrides property %s::$%s but is missing the #[\Override] attribute.',
92+
$node->getClassReflection()->getDisplayName(),
93+
$node->getName(),
94+
$prototype->getDeclaringClass()->getDisplayName(),
95+
$prototype->getName(),
96+
))
97+
->identifier('property.missingOverride')
98+
->fixNode($originalNode, static function ($property) {
99+
$property->attrGroups[] = new Node\AttributeGroup([
100+
new Attribute(new Node\Name\FullyQualified('Override')),
101+
]);
102+
103+
return $property;
104+
})
105+
->build();
106+
}
51107
if ($prototype->isStatic()) {
52108
if (!$node->isStatic()) {
53109
$errors[] = RuleErrorBuilder::message(sprintf(
@@ -356,4 +412,44 @@ private function findPrototypeInInterfaces(ClassReflection $classReflection, str
356412
return null;
357413
}
358414

415+
/**
416+
* @param Node\AttributeGroup[] $attrGroups
417+
* @return Node\AttributeGroup[]
418+
*/
419+
private function filterOverrideAttribute(array $attrGroups): array
420+
{
421+
foreach ($attrGroups as $i => $attrGroup) {
422+
foreach ($attrGroup->attrs as $j => $attr) {
423+
if ($attr->name->toLowerString() !== 'override') {
424+
continue;
425+
}
426+
427+
unset($attrGroup->attrs[$j]);
428+
if (count($attrGroup->attrs) !== 0) {
429+
continue;
430+
}
431+
432+
unset($attrGroups[$i]);
433+
}
434+
}
435+
436+
return $attrGroups;
437+
}
438+
439+
/**
440+
* @param Node\AttributeGroup[] $attrGroups
441+
*/
442+
private function hasOverrideAttribute(array $attrGroups): bool
443+
{
444+
foreach ($attrGroups as $attrGroup) {
445+
foreach ($attrGroup->attrs as $attr) {
446+
if ($attr->name->toLowerString() === 'override') {
447+
return true;
448+
}
449+
}
450+
}
451+
452+
return false;
453+
}
454+
359455
}

src/Rules/Properties/PropertyAttributesRule.php

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,30 +6,53 @@
66
use PhpParser\Node;
77
use PHPStan\Analyser\Scope;
88
use PHPStan\DependencyInjection\RegisteredRule;
9+
use PHPStan\Node\ClassPropertyNode;
10+
use PHPStan\Php\PhpVersion;
911
use PHPStan\Rules\AttributesCheck;
1012
use PHPStan\Rules\Rule;
13+
use PHPStan\Rules\RuleErrorBuilder;
14+
use function count;
1115

1216
/**
13-
* @implements Rule<Node\Stmt\Property>
17+
* @implements Rule<ClassPropertyNode>
1418
*/
1519
#[RegisteredRule(level: 0)]
1620
final class PropertyAttributesRule implements Rule
1721
{
1822

19-
public function __construct(private AttributesCheck $attributesCheck)
23+
public function __construct(
24+
private AttributesCheck $attributesCheck,
25+
private PhpVersion $phpVersion,
26+
)
2027
{
2128
}
2229

2330
public function getNodeType(): string
2431
{
25-
return Node\Stmt\Property::class;
32+
return ClassPropertyNode::class;
2633
}
2734

2835
public function processNode(Node $node, Scope $scope): array
2936
{
37+
if (!$this->phpVersion->supportsOverrideAttributeOnProperty()) {
38+
$propertyReflection = $node->getClassReflection()->getNativeProperty($node->getName());
39+
if (count($propertyReflection->getNativeReflection()->getAttributes('Override')) > 0) {
40+
return [
41+
RuleErrorBuilder::message('Attribute class Override can be used with properties only on PHP 8.5 and later.')
42+
->identifier('property.overrideAttribute')
43+
->nonIgnorable()
44+
->build(),
45+
];
46+
}
47+
}
48+
49+
if ($node->isPromoted()) {
50+
return [];
51+
}
52+
3053
return $this->attributesCheck->check(
3154
$scope,
32-
$node->attrGroups,
55+
$node->getAttrGroups(),
3356
Attribute::TARGET_PROPERTY,
3457
'property',
3558
);

tests/PHPStan/Rules/Properties/OverridingPropertyRuleTest.php

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,15 @@ class OverridingPropertyRuleTest extends RuleTestCase
1717

1818
private bool $reportMaybes;
1919

20+
private bool $checkMissingOverridePropertyAttribute = false;
21+
2022
protected function getRule(): Rule
2123
{
2224
return new OverridingPropertyRule(
2325
self::getContainer()->getByType(PhpVersion::class),
2426
true,
2527
$this->reportMaybes,
28+
$this->checkMissingOverridePropertyAttribute,
2629
);
2730
}
2831

@@ -283,4 +286,33 @@ public function testBug12586(): void
283286
]);
284287
}
285288

289+
#[RequiresPhp('>= 8.5')]
290+
public function testOverrideAttribute(): void
291+
{
292+
$this->checkMissingOverridePropertyAttribute = true;
293+
$this->reportMaybes = true;
294+
$this->analyse([__DIR__ . '/data/property-override-attr.php'], [
295+
[
296+
'Property PropertyOverrideAttr\Bar::$bar has #[\Override] attribute but does not override any property.',
297+
19,
298+
],
299+
[
300+
'Property PropertyOverrideAttr\Baz::$bar has #[\Override] attribute but does not override any property.',
301+
30,
302+
],
303+
[
304+
'Property PropertyOverrideAttr\Lorem::$foo overrides property PropertyOverrideAttr\Foo::$foo but is missing the #[\Override] attribute.',
305+
44,
306+
],
307+
]);
308+
}
309+
310+
#[RequiresPhp('>= 8.5')]
311+
public function testFixMissingOverrideAttribute(): void
312+
{
313+
$this->checkMissingOverridePropertyAttribute = true;
314+
$this->reportMaybes = true;
315+
$this->fix(__DIR__ . '/data/property-override-attr-missing.php', __DIR__ . '/data/property-override-attr-missing.php.fixed');
316+
}
317+
286318
}

tests/PHPStan/Rules/Properties/PropertyAttributesRuleTest.php

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace PHPStan\Rules\Properties;
44

5+
use PHPStan\Php\PhpVersion;
56
use PHPStan\Rules\AttributesCheck;
67
use PHPStan\Rules\ClassCaseSensitivityCheck;
78
use PHPStan\Rules\ClassForbiddenNameCheck;
@@ -12,6 +13,8 @@
1213
use PHPStan\Rules\Rule;
1314
use PHPStan\Rules\RuleLevelHelper;
1415
use PHPStan\Testing\RuleTestCase;
16+
use PHPUnit\Framework\Attributes\RequiresPhp;
17+
use const PHP_VERSION_ID;
1518

1619
/**
1720
* @extends RuleTestCase<PropertyAttributesRule>
@@ -43,6 +46,7 @@ protected function getRule(): Rule
4346
),
4447
true,
4548
),
49+
new PhpVersion(PHP_VERSION_ID),
4650
);
4751
}
4852

@@ -70,4 +74,25 @@ public function testDeprecatedAttribute(): void
7074
]);
7175
}
7276

77+
#[RequiresPhp('>= 8.5')]
78+
public function testOverrideAttributeAllowed(): void
79+
{
80+
$this->analyse([__DIR__ . '/data/override-attr-on-property.php'], []);
81+
}
82+
83+
#[RequiresPhp('< 8.5')]
84+
public function testOverrideAttributeNotAllowed(): void
85+
{
86+
$this->analyse([__DIR__ . '/data/override-attr-on-property.php'], [
87+
[
88+
'Attribute class Override can be used with properties only on PHP 8.5 and later.',
89+
11,
90+
],
91+
[
92+
'Attribute class Override can be used with properties only on PHP 8.5 and later.',
93+
14,
94+
],
95+
]);
96+
}
97+
7398
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php // lint >= 8.5
2+
3+
namespace OverrideAttrOnProperty;
4+
5+
use Override;
6+
7+
class Foo
8+
{
9+
10+
#[Override]
11+
public int $foo;
12+
13+
public function __construct(
14+
#[Override]
15+
public int $bar,
16+
) {}
17+
18+
}

0 commit comments

Comments
 (0)