From cbd9ff1ca76f9dafcbaf30e87c2942f45f2441dc Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Wed, 2 Sep 2026 16:21:47 +0200 Subject: [PATCH] [DeadCode] Keep empty method when parent class or interface is out of analyzed scope RemoveEmptyClassMethodRector removed an empty method whose abstract requirement lives in a parent class or interface outside the analyzed paths, because the reflection could not see it. Keep the method when the parent class cannot be resolved (protected override) or an implemented interface is out of scope. Fixes rectorphp/rector#9881 Claude-Session: https://claude.ai/code/session_01QswFbKfL2DRR9FTdAAKZPL --- .../skip_unresolvable_interface.php.inc | 10 +++++++ .../Fixture/skip_unresolvable_parent.php.inc | 10 +++++++ .../RemoveEmptyClassMethodRector.php | 27 +++++++++++++++++++ 3 files changed, 47 insertions(+) create mode 100644 rules-tests/DeadCode/Rector/ClassMethod/RemoveEmptyClassMethodRector/Fixture/skip_unresolvable_interface.php.inc create mode 100644 rules-tests/DeadCode/Rector/ClassMethod/RemoveEmptyClassMethodRector/Fixture/skip_unresolvable_parent.php.inc diff --git a/rules-tests/DeadCode/Rector/ClassMethod/RemoveEmptyClassMethodRector/Fixture/skip_unresolvable_interface.php.inc b/rules-tests/DeadCode/Rector/ClassMethod/RemoveEmptyClassMethodRector/Fixture/skip_unresolvable_interface.php.inc new file mode 100644 index 00000000000..1eb02db29e7 --- /dev/null +++ b/rules-tests/DeadCode/Rector/ClassMethod/RemoveEmptyClassMethodRector/Fixture/skip_unresolvable_interface.php.inc @@ -0,0 +1,10 @@ +hasUnresolvableParentOrInterface($class, $classMethod, $scope->getClassReflection())) { + return true; + } + if ($this->paramAnalyzer->hasPropertyPromotion($classMethod->params)) { return true; } @@ -184,6 +189,28 @@ private function shouldSkipClassMethod(Class_ $class, ClassMethod $classMethod): return $this->isAttributeMarkerConstructor($classMethod, $classReflection); } + private function hasUnresolvableParentOrInterface( + Class_ $class, + ClassMethod $classMethod, + ?ClassReflection $classReflection + ): bool { + if (! $classReflection instanceof ClassReflection) { + return false; + } + + // a protected empty method only exists to override/implement a parent's method; + // if the parent is out of scope, keep it to avoid breaking an abstract requirement + if ($classMethod->isProtected() && $class->extends instanceof Name && ! $classReflection->getParentClass() instanceof ClassReflection) { + return true; + } + + $interfaceNames = array_map( + static fn (ClassReflection $classReflection): string => $classReflection->getName(), + $classReflection->getInterfaces() + ); + return array_any($class->implements, fn (Name $name): bool => ! in_array($this->getName($name), $interfaceNames, true)); + } + private function hasDeprecatedAnnotation(ClassMethod $classMethod): bool { $phpDocInfo = $this->phpDocInfoFactory->createFromNode($classMethod);