From 1955da7bdc120867f8c60cec27f5295e60cc07f0 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Wed, 2 Sep 2026 22:08:45 +0200 Subject: [PATCH] [CodeQuality] Skip CompleteDynamicPropertiesRector when a not autoloaded ancestor may declare the property The rule only checked the direct parent for autoloadability. When a property is declared in a not autoloaded grandparent (or higher ancestor), reflection cannot see it, so the rule wrongly added a redeclaration. Walk the whole ancestor chain instead. --- ...ty_from_not_autoloaded_grandparent.php.inc | 15 +++++++++++ .../CompleteDynamicPropertiesRector.php | 25 ++++++++++++++++--- 2 files changed, 36 insertions(+), 4 deletions(-) create mode 100644 rules-tests/CodeQuality/Rector/Class_/CompleteDynamicPropertiesRector/Fixture/skip_property_from_not_autoloaded_grandparent.php.inc diff --git a/rules-tests/CodeQuality/Rector/Class_/CompleteDynamicPropertiesRector/Fixture/skip_property_from_not_autoloaded_grandparent.php.inc b/rules-tests/CodeQuality/Rector/Class_/CompleteDynamicPropertiesRector/Fixture/skip_property_from_not_autoloaded_grandparent.php.inc new file mode 100644 index 00000000000..8bffac21901 --- /dev/null +++ b/rules-tests/CodeQuality/Rector/Class_/CompleteDynamicPropertiesRector/Fixture/skip_property_from_not_autoloaded_grandparent.php.inc @@ -0,0 +1,15 @@ +configFactory->get(''); + } +} + +class CancelableHandler extends \NotAutoloaded\EmailHandler +{ +} diff --git a/rules/CodeQuality/Rector/Class_/CompleteDynamicPropertiesRector.php b/rules/CodeQuality/Rector/Class_/CompleteDynamicPropertiesRector.php index 9def47c7b58..aa888569847 100644 --- a/rules/CodeQuality/Rector/Class_/CompleteDynamicPropertiesRector.php +++ b/rules/CodeQuality/Rector/Class_/CompleteDynamicPropertiesRector.php @@ -5,7 +5,6 @@ namespace Rector\CodeQuality\Rector\Class_; use PhpParser\Node; -use PhpParser\Node\Name\FullyQualified; use PhpParser\Node\Stmt\Class_; use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\ReflectionProvider; @@ -15,6 +14,7 @@ use Rector\NodeAnalyzer\ClassAnalyzer; use Rector\Php80\NodeAnalyzer\PhpAttributeAnalyzer; use Rector\Rector\AbstractRector; +use Rector\Reflection\ClassReflectionAnalyzer; use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample; use Symplify\RuleDocGenerator\ValueObject\RuleDefinition; @@ -30,6 +30,7 @@ public function __construct( private readonly ClassAnalyzer $classAnalyzer, private readonly PhpAttributeAnalyzer $phpAttributeAnalyzer, private readonly MissingPropertiesResolver $missingPropertiesResolver, + private readonly ClassReflectionAnalyzer $classReflectionAnalyzer, ) { } @@ -140,9 +141,25 @@ private function shouldSkipClass(Class_ $class): bool return true; } - return $class->extends instanceof FullyQualified && ! $this->reflectionProvider->hasClass( - $class->extends->toString() - ); + // any not autoloaded ancestor may already declare the property, so we cannot safely add it + return $this->hasNotAutoloadedAncestor($classReflection); + } + + private function hasNotAutoloadedAncestor(ClassReflection $classReflection): bool + { + $currentClassReflection = $classReflection; + + while ($currentClassReflection instanceof ClassReflection) { + $parentClassName = $this->classReflectionAnalyzer->resolveParentClassName($currentClassReflection); + + if ($parentClassName !== null && ! $this->reflectionProvider->hasClass($parentClassName)) { + return true; + } + + $currentClassReflection = $currentClassReflection->getParentClass(); + } + + return false; } private function matchClassReflection(Class_ $class): ?ClassReflection