From f4d45e2ebda06d2ff0344a8b4fa78511e389f901 Mon Sep 17 00:00:00 2001 From: Carlos Granados Date: Wed, 4 Feb 2026 11:03:31 +0000 Subject: [PATCH] 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 --- config/set/php85.php | 2 + ...ributeToOverriddenPropertiesRectorTest.php | 28 +++ .../Fixture/fixture.php.inc | 26 +++ .../skip_already_has_attribute.php.inc | 11 ++ .../Fixture/skip_no_parent.php.inc | 8 + .../skip_parent_private_property.php.inc | 10 + .../Fixture/skip_private_property.php.inc | 10 + .../Source/ParentWithPrivateProperty.php | 10 + .../Source/ParentWithPublicProperty.php | 10 + .../config/configured_rule.php | 10 + ...eAttributeToOverriddenPropertiesRector.php | 171 ++++++++++++++++++ src/ValueObject/PhpVersionFeature.php | 5 + 12 files changed, 301 insertions(+) create mode 100644 rules-tests/Php85/Rector/Property/AddOverrideAttributeToOverriddenPropertiesRector/AddOverrideAttributeToOverriddenPropertiesRectorTest.php create mode 100644 rules-tests/Php85/Rector/Property/AddOverrideAttributeToOverriddenPropertiesRector/Fixture/fixture.php.inc create mode 100644 rules-tests/Php85/Rector/Property/AddOverrideAttributeToOverriddenPropertiesRector/Fixture/skip_already_has_attribute.php.inc create mode 100644 rules-tests/Php85/Rector/Property/AddOverrideAttributeToOverriddenPropertiesRector/Fixture/skip_no_parent.php.inc create mode 100644 rules-tests/Php85/Rector/Property/AddOverrideAttributeToOverriddenPropertiesRector/Fixture/skip_parent_private_property.php.inc create mode 100644 rules-tests/Php85/Rector/Property/AddOverrideAttributeToOverriddenPropertiesRector/Fixture/skip_private_property.php.inc create mode 100644 rules-tests/Php85/Rector/Property/AddOverrideAttributeToOverriddenPropertiesRector/Source/ParentWithPrivateProperty.php create mode 100644 rules-tests/Php85/Rector/Property/AddOverrideAttributeToOverriddenPropertiesRector/Source/ParentWithPublicProperty.php create mode 100644 rules-tests/Php85/Rector/Property/AddOverrideAttributeToOverriddenPropertiesRector/config/configured_rule.php create mode 100644 rules/Php85/Rector/Property/AddOverrideAttributeToOverriddenPropertiesRector.php diff --git a/config/set/php85.php b/config/set/php85.php index 1117c693375..e860f28b587 100644 --- a/config/set/php85.php +++ b/config/set/php85.php @@ -16,6 +16,7 @@ use Rector\Php85\Rector\FuncCall\ChrArgModuloRector; use Rector\Php85\Rector\FuncCall\OrdSingleByteRector; use Rector\Php85\Rector\FuncCall\RemoveFinfoBufferContextArgRector; +use Rector\Php85\Rector\Property\AddOverrideAttributeToOverriddenPropertiesRector; use Rector\Php85\Rector\ShellExec\ShellExecFunctionCallOverBackticksRector; use Rector\Php85\Rector\Switch_\ColonAfterSwitchCaseRector; use Rector\Removing\Rector\FuncCall\RemoveFuncCallArgRector; @@ -43,6 +44,7 @@ OrdSingleByteRector::class, WakeupToUnserializeRector::class, ShellExecFunctionCallOverBackticksRector::class, + AddOverrideAttributeToOverriddenPropertiesRector::class, ]); $rectorConfig->ruleWithConfiguration( diff --git a/rules-tests/Php85/Rector/Property/AddOverrideAttributeToOverriddenPropertiesRector/AddOverrideAttributeToOverriddenPropertiesRectorTest.php b/rules-tests/Php85/Rector/Property/AddOverrideAttributeToOverriddenPropertiesRector/AddOverrideAttributeToOverriddenPropertiesRectorTest.php new file mode 100644 index 00000000000..219fba364ee --- /dev/null +++ b/rules-tests/Php85/Rector/Property/AddOverrideAttributeToOverriddenPropertiesRector/AddOverrideAttributeToOverriddenPropertiesRectorTest.php @@ -0,0 +1,28 @@ +doTestFile($filePath); + } + + public static function provideData(): Iterator + { + return self::yieldFilesFromDirectory(__DIR__ . '/Fixture'); + } + + public function provideConfigFilePath(): string + { + return __DIR__ . '/config/configured_rule.php'; + } +} diff --git a/rules-tests/Php85/Rector/Property/AddOverrideAttributeToOverriddenPropertiesRector/Fixture/fixture.php.inc b/rules-tests/Php85/Rector/Property/AddOverrideAttributeToOverriddenPropertiesRector/Fixture/fixture.php.inc new file mode 100644 index 00000000000..bbaa5743fd7 --- /dev/null +++ b/rules-tests/Php85/Rector/Property/AddOverrideAttributeToOverriddenPropertiesRector/Fixture/fixture.php.inc @@ -0,0 +1,26 @@ + +----- + diff --git a/rules-tests/Php85/Rector/Property/AddOverrideAttributeToOverriddenPropertiesRector/Fixture/skip_already_has_attribute.php.inc b/rules-tests/Php85/Rector/Property/AddOverrideAttributeToOverriddenPropertiesRector/Fixture/skip_already_has_attribute.php.inc new file mode 100644 index 00000000000..b6acbb30e0e --- /dev/null +++ b/rules-tests/Php85/Rector/Property/AddOverrideAttributeToOverriddenPropertiesRector/Fixture/skip_already_has_attribute.php.inc @@ -0,0 +1,11 @@ +rule(AddOverrideAttributeToOverriddenPropertiesRector::class); +}; diff --git a/rules/Php85/Rector/Property/AddOverrideAttributeToOverriddenPropertiesRector.php b/rules/Php85/Rector/Property/AddOverrideAttributeToOverriddenPropertiesRector.php new file mode 100644 index 00000000000..81957e33bb6 --- /dev/null +++ b/rules/Php85/Rector/Property/AddOverrideAttributeToOverriddenPropertiesRector.php @@ -0,0 +1,171 @@ +> + */ + public function getNodeTypes(): array + { + return [Class_::class]; + } + + public function provideMinPhpVersion(): int + { + return PhpVersionFeature::OVERRIDE_ATTRIBUTE_ON_PROPERTIES; + } + + /** + * @param Class_ $node + */ + public function refactor(Node $node): ?Node + { + if ($this->classAnalyzer->isAnonymousClass($node)) { + return null; + } + + $className = (string) $this->getName($node); + if (! $this->reflectionProvider->hasClass($className)) { + return null; + } + + $classReflection = $this->reflectionProvider->getClass($className); + $parentClassReflections = $classReflection->getParents(); + + if ($parentClassReflections === []) { + return null; + } + + $this->hasChanged = false; + + foreach ($node->getProperties() as $property) { + $this->processProperty($property, $parentClassReflections); + } + + if ($this->hasChanged) { + return $node; + } + + return null; + } + + /** + * @param ClassReflection[] $parentClassReflections + */ + private function processProperty(Property $property, array $parentClassReflections): void + { + if ($this->shouldSkipProperty($property)) { + return; + } + + foreach ($property->props as $propertyProperty) { + $propertyName = $this->getName($propertyProperty); + if ($propertyName === null) { + continue; + } + + if ($this->isPropertyOverridden($propertyName, $parentClassReflections)) { + $property->attrGroups[] = new AttributeGroup([new Attribute(new FullyQualified(self::OVERRIDE_CLASS))]); + $this->hasChanged = true; + return; + } + } + } + + private function shouldSkipProperty(Property $property): bool + { + if ($property->isPrivate()) { + return true; + } + + return $this->phpAttributeAnalyzer->hasPhpAttribute($property, self::OVERRIDE_CLASS); + } + + /** + * @param ClassReflection[] $parentClassReflections + */ + private function isPropertyOverridden(string $propertyName, array $parentClassReflections): bool + { + foreach ($parentClassReflections as $parentClassReflection) { + if (! $parentClassReflection->hasNativeProperty($propertyName)) { + continue; + } + + $parentProperty = $parentClassReflection->getNativeProperty($propertyName); + return ! $parentProperty->isPrivate(); + } + + return false; + } +} diff --git a/src/ValueObject/PhpVersionFeature.php b/src/ValueObject/PhpVersionFeature.php index 1cfe19b9df6..2d2762f55cd 100644 --- a/src/ValueObject/PhpVersionFeature.php +++ b/src/ValueObject/PhpVersionFeature.php @@ -495,4 +495,9 @@ final class PhpVersionFeature * @see https://wiki.php.net/rfc/pipe-operator-v3 */ public const int PIPE_OPERATOER = PhpVersion::PHP_85; + + /** + * @see https://wiki.php.net/rfc/override_properties + */ + public const int OVERRIDE_ATTRIBUTE_ON_PROPERTIES = PhpVersion::PHP_85; }