Skip to content

Commit f0fd451

Browse files
Add AddOverrideAttributeToOverriddenPropertiesRector for PHP 8.5
This rule adds the #[\Override] attribute to properties that override parent class properties. PHP 8.5 extends the Override attribute to support properties in addition to methods. The rule: - Adds #[\Override] to public/protected properties that override parent properties - Skips private properties (cannot truly override) - Skips properties where parent is private (not visible to child) - Skips properties that already have the attribute
1 parent 4997962 commit f0fd451

File tree

10 files changed

+300
-0
lines changed

10 files changed

+300
-0
lines changed

config/set/php85.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
use Rector\Php85\Rector\FuncCall\ChrArgModuloRector;
1717
use Rector\Php85\Rector\FuncCall\OrdSingleByteRector;
1818
use Rector\Php85\Rector\FuncCall\RemoveFinfoBufferContextArgRector;
19+
use Rector\Php85\Rector\Property\AddOverrideAttributeToOverriddenPropertiesRector;
1920
use Rector\Php85\Rector\ShellExec\ShellExecFunctionCallOverBackticksRector;
2021
use Rector\Php85\Rector\Switch_\ColonAfterSwitchCaseRector;
2122
use Rector\Removing\Rector\FuncCall\RemoveFuncCallArgRector;
@@ -43,6 +44,7 @@
4344
OrdSingleByteRector::class,
4445
WakeupToUnserializeRector::class,
4546
ShellExecFunctionCallOverBackticksRector::class,
47+
AddOverrideAttributeToOverriddenPropertiesRector::class,
4648
]);
4749

4850
$rectorConfig->ruleWithConfiguration(
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Rector\Tests\Php85\Rector\Property\AddOverrideAttributeToOverriddenPropertiesRector;
6+
7+
use Iterator;
8+
use PHPUnit\Framework\Attributes\DataProvider;
9+
use Rector\Testing\PHPUnit\AbstractRectorTestCase;
10+
11+
final class AddOverrideAttributeToOverriddenPropertiesRectorTest extends AbstractRectorTestCase
12+
{
13+
#[DataProvider('provideData')]
14+
public function test(string $filePath): void
15+
{
16+
$this->doTestFile($filePath);
17+
}
18+
19+
public static function provideData(): Iterator
20+
{
21+
return self::yieldFilesFromDirectory(__DIR__ . '/Fixture');
22+
}
23+
24+
public function provideConfigFilePath(): string
25+
{
26+
return __DIR__ . '/config/configured_rule.php';
27+
}
28+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php
2+
3+
namespace Rector\Tests\Php85\Rector\Property\AddOverrideAttributeToOverriddenPropertiesRector\Fixture;
4+
5+
class ParentClass
6+
{
7+
public string $name;
8+
}
9+
10+
final class ChildClass extends ParentClass
11+
{
12+
public string $name;
13+
}
14+
15+
?>
16+
-----
17+
<?php
18+
19+
namespace Rector\Tests\Php85\Rector\Property\AddOverrideAttributeToOverriddenPropertiesRector\Fixture;
20+
21+
class ParentClass
22+
{
23+
public string $name;
24+
}
25+
26+
final class ChildClass extends ParentClass
27+
{
28+
#[\Override]
29+
public string $name;
30+
}
31+
32+
?>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php
2+
3+
namespace Rector\Tests\Php85\Rector\Property\AddOverrideAttributeToOverriddenPropertiesRector\Fixture;
4+
5+
class ParentWithAttribute
6+
{
7+
public string $name;
8+
}
9+
10+
final class ChildWithAttribute extends ParentWithAttribute
11+
{
12+
#[\Override]
13+
public string $name;
14+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?php
2+
3+
namespace Rector\Tests\Php85\Rector\Property\AddOverrideAttributeToOverriddenPropertiesRector\Fixture;
4+
5+
final class NoParent
6+
{
7+
public string $name;
8+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
namespace Rector\Tests\Php85\Rector\Property\AddOverrideAttributeToOverriddenPropertiesRector\Fixture;
4+
5+
class ParentPrivate
6+
{
7+
private string $secret;
8+
}
9+
10+
final class ChildPublic extends ParentPrivate
11+
{
12+
public string $secret;
13+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
namespace Rector\Tests\Php85\Rector\Property\AddOverrideAttributeToOverriddenPropertiesRector\Fixture;
4+
5+
class ParentWithPrivate
6+
{
7+
private string $secret;
8+
}
9+
10+
final class ChildWithPrivate extends ParentWithPrivate
11+
{
12+
private string $secret;
13+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use Rector\Config\RectorConfig;
6+
use Rector\Php85\Rector\Property\AddOverrideAttributeToOverriddenPropertiesRector;
7+
8+
return static function (RectorConfig $rectorConfig): void {
9+
$rectorConfig->rule(AddOverrideAttributeToOverriddenPropertiesRector::class);
10+
};
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Rector\Php85\Rector\Property;
6+
7+
use PhpParser\Node;
8+
use PhpParser\Node\Attribute;
9+
use PhpParser\Node\AttributeGroup;
10+
use PhpParser\Node\Name\FullyQualified;
11+
use PhpParser\Node\Stmt\Class_;
12+
use PhpParser\Node\Stmt\Property;
13+
use PHPStan\Reflection\ClassReflection;
14+
use PHPStan\Reflection\ReflectionProvider;
15+
use Rector\NodeAnalyzer\ClassAnalyzer;
16+
use Rector\Php80\NodeAnalyzer\PhpAttributeAnalyzer;
17+
use Rector\Rector\AbstractRector;
18+
use Rector\ValueObject\PhpVersionFeature;
19+
use Rector\VersionBonding\Contract\MinPhpVersionInterface;
20+
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
21+
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
22+
23+
/**
24+
* @see https://wiki.php.net/rfc/override_properties
25+
*
26+
* @see \Rector\Tests\Php85\Rector\Property\AddOverrideAttributeToOverriddenPropertiesRector\AddOverrideAttributeToOverriddenPropertiesRectorTest
27+
*/
28+
final class AddOverrideAttributeToOverriddenPropertiesRector extends AbstractRector implements MinPhpVersionInterface
29+
{
30+
private const string OVERRIDE_CLASS = 'Override';
31+
32+
private bool $hasChanged = false;
33+
34+
public function __construct(
35+
private readonly ReflectionProvider $reflectionProvider,
36+
private readonly ClassAnalyzer $classAnalyzer,
37+
private readonly PhpAttributeAnalyzer $phpAttributeAnalyzer,
38+
) {
39+
}
40+
41+
public function getRuleDefinition(): RuleDefinition
42+
{
43+
return new RuleDefinition(
44+
'Add override attribute to overridden properties',
45+
[
46+
new CodeSample(
47+
<<<'CODE_SAMPLE'
48+
class ParentClass
49+
{
50+
public string $name;
51+
}
52+
53+
final class ChildClass extends ParentClass
54+
{
55+
public string $name;
56+
}
57+
CODE_SAMPLE
58+
,
59+
<<<'CODE_SAMPLE'
60+
class ParentClass
61+
{
62+
public string $name;
63+
}
64+
65+
final class ChildClass extends ParentClass
66+
{
67+
#[\Override]
68+
public string $name;
69+
}
70+
CODE_SAMPLE
71+
),
72+
]
73+
);
74+
}
75+
76+
/**
77+
* @return array<class-string<Node>>
78+
*/
79+
public function getNodeTypes(): array
80+
{
81+
return [Class_::class];
82+
}
83+
84+
public function provideMinPhpVersion(): int
85+
{
86+
return PhpVersionFeature::OVERRIDE_ATTRIBUTE_ON_PROPERTIES;
87+
}
88+
89+
/**
90+
* @param Class_ $node
91+
*/
92+
public function refactor(Node $node): ?Node
93+
{
94+
if ($this->classAnalyzer->isAnonymousClass($node)) {
95+
return null;
96+
}
97+
98+
$className = (string) $this->getName($node);
99+
if (! $this->reflectionProvider->hasClass($className)) {
100+
return null;
101+
}
102+
103+
$classReflection = $this->reflectionProvider->getClass($className);
104+
$parentClassReflections = $classReflection->getParents();
105+
106+
if ($parentClassReflections === []) {
107+
return null;
108+
}
109+
110+
$this->hasChanged = false;
111+
112+
foreach ($node->getProperties() as $property) {
113+
$this->processProperty($property, $parentClassReflections);
114+
}
115+
116+
if ($this->hasChanged) {
117+
return $node;
118+
}
119+
120+
return null;
121+
}
122+
123+
/**
124+
* @param ClassReflection[] $parentClassReflections
125+
*/
126+
private function processProperty(Property $property, array $parentClassReflections): void
127+
{
128+
if ($this->shouldSkipProperty($property)) {
129+
return;
130+
}
131+
132+
foreach ($property->props as $propertyProperty) {
133+
$propertyName = $this->getName($propertyProperty);
134+
if ($propertyName === null) {
135+
continue;
136+
}
137+
138+
if ($this->isPropertyOverridden($propertyName, $parentClassReflections)) {
139+
$property->attrGroups[] = new AttributeGroup([new Attribute(new FullyQualified(self::OVERRIDE_CLASS))]);
140+
$this->hasChanged = true;
141+
return;
142+
}
143+
}
144+
}
145+
146+
private function shouldSkipProperty(Property $property): bool
147+
{
148+
if ($property->isPrivate()) {
149+
return true;
150+
}
151+
152+
return $this->phpAttributeAnalyzer->hasPhpAttribute($property, self::OVERRIDE_CLASS);
153+
}
154+
155+
/**
156+
* @param ClassReflection[] $parentClassReflections
157+
*/
158+
private function isPropertyOverridden(string $propertyName, array $parentClassReflections): bool
159+
{
160+
foreach ($parentClassReflections as $parentClassReflection) {
161+
if (! $parentClassReflection->hasNativeProperty($propertyName)) {
162+
continue;
163+
}
164+
165+
$parentProperty = $parentClassReflection->getNativeProperty($propertyName);
166+
if ($parentProperty->isPrivate()) {
167+
continue;
168+
}
169+
170+
return true;
171+
}
172+
173+
return false;
174+
}
175+
}

src/ValueObject/PhpVersionFeature.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -495,4 +495,9 @@ final class PhpVersionFeature
495495
* @see https://wiki.php.net/rfc/pipe-operator-v3
496496
*/
497497
public const int PIPE_OPERATOER = PhpVersion::PHP_85;
498+
499+
/**
500+
* @see https://wiki.php.net/rfc/override_properties
501+
*/
502+
public const int OVERRIDE_ATTRIBUTE_ON_PROPERTIES = PhpVersion::PHP_85;
498503
}

0 commit comments

Comments
 (0)