From 4db4c9f72f0a03a92d409a7a2771a77df5b9f8fd Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Thu, 23 Jul 2026 23:25:45 +0200 Subject: [PATCH 1/2] [DeadCode] Add RemoveParentDelegatingClassMethodRector --- .../Fixture/skip_attributed_method.php.inc | 14 + .../Fixture/skip_construct.php.inc | 13 + .../skip_different_parent_method.php.inc | 13 + .../Fixture/skip_extra_stmt.php.inc | 15 + .../Fixture/skip_final_method.php.inc | 13 + .../Fixture/skip_narrowed_param_type.php.inc | 13 + .../Fixture/skip_no_parent_class.php.inc | 10 + .../Fixture/skip_param_docblock.php.inc | 16 ++ .../Fixture/skip_swapped_args.php.inc | 13 + .../Fixture/skip_wider_visibility.php.inc | 13 + .../Fixture/some_command.php.inc | 27 ++ .../Fixture/with_params_and_return.php.inc | 27 ++ ...eParentDelegatingClassMethodRectorTest.php | 28 ++ .../Source/SomeParentCommand.php | 25 ++ .../Source/SomeParentWithConstructor.php | 12 + .../Source/SomeParentWithTwoParams.php | 13 + .../config/configured_rule.php | 9 + ...emoveParentDelegatingClassMethodRector.php | 267 ++++++++++++++++++ src/Config/Level/DeadCodeLevel.php | 2 + 19 files changed, 543 insertions(+) create mode 100644 rules-tests/DeadCode/Rector/ClassMethod/RemoveParentDelegatingClassMethodRector/Fixture/skip_attributed_method.php.inc create mode 100644 rules-tests/DeadCode/Rector/ClassMethod/RemoveParentDelegatingClassMethodRector/Fixture/skip_construct.php.inc create mode 100644 rules-tests/DeadCode/Rector/ClassMethod/RemoveParentDelegatingClassMethodRector/Fixture/skip_different_parent_method.php.inc create mode 100644 rules-tests/DeadCode/Rector/ClassMethod/RemoveParentDelegatingClassMethodRector/Fixture/skip_extra_stmt.php.inc create mode 100644 rules-tests/DeadCode/Rector/ClassMethod/RemoveParentDelegatingClassMethodRector/Fixture/skip_final_method.php.inc create mode 100644 rules-tests/DeadCode/Rector/ClassMethod/RemoveParentDelegatingClassMethodRector/Fixture/skip_narrowed_param_type.php.inc create mode 100644 rules-tests/DeadCode/Rector/ClassMethod/RemoveParentDelegatingClassMethodRector/Fixture/skip_no_parent_class.php.inc create mode 100644 rules-tests/DeadCode/Rector/ClassMethod/RemoveParentDelegatingClassMethodRector/Fixture/skip_param_docblock.php.inc create mode 100644 rules-tests/DeadCode/Rector/ClassMethod/RemoveParentDelegatingClassMethodRector/Fixture/skip_swapped_args.php.inc create mode 100644 rules-tests/DeadCode/Rector/ClassMethod/RemoveParentDelegatingClassMethodRector/Fixture/skip_wider_visibility.php.inc create mode 100644 rules-tests/DeadCode/Rector/ClassMethod/RemoveParentDelegatingClassMethodRector/Fixture/some_command.php.inc create mode 100644 rules-tests/DeadCode/Rector/ClassMethod/RemoveParentDelegatingClassMethodRector/Fixture/with_params_and_return.php.inc create mode 100644 rules-tests/DeadCode/Rector/ClassMethod/RemoveParentDelegatingClassMethodRector/RemoveParentDelegatingClassMethodRectorTest.php create mode 100644 rules-tests/DeadCode/Rector/ClassMethod/RemoveParentDelegatingClassMethodRector/Source/SomeParentCommand.php create mode 100644 rules-tests/DeadCode/Rector/ClassMethod/RemoveParentDelegatingClassMethodRector/Source/SomeParentWithConstructor.php create mode 100644 rules-tests/DeadCode/Rector/ClassMethod/RemoveParentDelegatingClassMethodRector/Source/SomeParentWithTwoParams.php create mode 100644 rules-tests/DeadCode/Rector/ClassMethod/RemoveParentDelegatingClassMethodRector/config/configured_rule.php create mode 100644 rules/DeadCode/Rector/ClassMethod/RemoveParentDelegatingClassMethodRector.php diff --git a/rules-tests/DeadCode/Rector/ClassMethod/RemoveParentDelegatingClassMethodRector/Fixture/skip_attributed_method.php.inc b/rules-tests/DeadCode/Rector/ClassMethod/RemoveParentDelegatingClassMethodRector/Fixture/skip_attributed_method.php.inc new file mode 100644 index 00000000000..1abe8b80d60 --- /dev/null +++ b/rules-tests/DeadCode/Rector/ClassMethod/RemoveParentDelegatingClassMethodRector/Fixture/skip_attributed_method.php.inc @@ -0,0 +1,14 @@ + +----- + diff --git a/rules-tests/DeadCode/Rector/ClassMethod/RemoveParentDelegatingClassMethodRector/Fixture/with_params_and_return.php.inc b/rules-tests/DeadCode/Rector/ClassMethod/RemoveParentDelegatingClassMethodRector/Fixture/with_params_and_return.php.inc new file mode 100644 index 00000000000..4f96644a3f8 --- /dev/null +++ b/rules-tests/DeadCode/Rector/ClassMethod/RemoveParentDelegatingClassMethodRector/Fixture/with_params_and_return.php.inc @@ -0,0 +1,27 @@ + +----- + diff --git a/rules-tests/DeadCode/Rector/ClassMethod/RemoveParentDelegatingClassMethodRector/RemoveParentDelegatingClassMethodRectorTest.php b/rules-tests/DeadCode/Rector/ClassMethod/RemoveParentDelegatingClassMethodRector/RemoveParentDelegatingClassMethodRectorTest.php new file mode 100644 index 00000000000..941b97043cf --- /dev/null +++ b/rules-tests/DeadCode/Rector/ClassMethod/RemoveParentDelegatingClassMethodRector/RemoveParentDelegatingClassMethodRectorTest.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/DeadCode/Rector/ClassMethod/RemoveParentDelegatingClassMethodRector/Source/SomeParentCommand.php b/rules-tests/DeadCode/Rector/ClassMethod/RemoveParentDelegatingClassMethodRector/Source/SomeParentCommand.php new file mode 100644 index 00000000000..af0cd22e709 --- /dev/null +++ b/rules-tests/DeadCode/Rector/ClassMethod/RemoveParentDelegatingClassMethodRector/Source/SomeParentCommand.php @@ -0,0 +1,25 @@ +withRules([RemoveParentDelegatingClassMethodRector::class]); diff --git a/rules/DeadCode/Rector/ClassMethod/RemoveParentDelegatingClassMethodRector.php b/rules/DeadCode/Rector/ClassMethod/RemoveParentDelegatingClassMethodRector.php new file mode 100644 index 00000000000..77e50d40191 --- /dev/null +++ b/rules/DeadCode/Rector/ClassMethod/RemoveParentDelegatingClassMethodRector.php @@ -0,0 +1,267 @@ +> + */ + public function getNodeTypes(): array + { + return [ClassMethod::class]; + } + + /** + * @param ClassMethod $node + */ + public function refactor(Node $node): ?int + { + // constructors are handled by RemoveParentDelegatingConstructorRector + if ($this->isName($node, MethodName::CONSTRUCT)) { + return null; + } + + if ($node->isFinal() || $node->isAbstract() || $node->isPrivate() || $node->byRef) { + return null; + } + + if ($node->stmts === null || count($node->stmts) !== 1) { + return null; + } + + if ($node->attrGroups !== []) { + return null; + } + + if ($this->hasRefiningDocblock($node)) { + return null; + } + + $parentMethodReflection = $this->matchParentMethodReflection($node); + if (! $parentMethodReflection instanceof ExtendedMethodReflection) { + return null; + } + + if (! $this->isParentCallDelegatingParams($node, $node->stmts[0])) { + return null; + } + + if (! $this->isSignatureMatching($node, $parentMethodReflection)) { + return null; + } + + return NodeVisitor::REMOVE_NODE; + } + + private function hasRefiningDocblock(ClassMethod $classMethod): bool + { + $phpDocInfo = $this->phpDocInfoFactory->createFromNodeOrEmpty($classMethod); + + return $phpDocInfo->hasByNames( + ['@param', '@phpstan-param', '@psalm-param', '@return', '@phpstan-return', '@psalm-return'] + ); + } + + private function matchParentMethodReflection(ClassMethod $classMethod): ?ExtendedMethodReflection + { + $scope = ScopeFetcher::fetch($classMethod); + + $classReflection = $scope->getClassReflection(); + if (! $classReflection instanceof ClassReflection) { + return null; + } + + if (! $classReflection->isClass()) { + return null; + } + + $parentClassReflection = $classReflection->getParentClass(); + if (! $parentClassReflection instanceof ClassReflection) { + return null; + } + + $methodName = $classMethod->name->toString(); + if (! $parentClassReflection->hasNativeMethod($methodName)) { + return null; + } + + return $parentClassReflection->getNativeMethod($methodName); + } + + /** + * Body must be a sole parent::sameMethod($sameParams, ...) call, passing every param in the same order + */ + private function isParentCallDelegatingParams(ClassMethod $classMethod, Stmt $soleStmt): bool + { + $staticCall = $this->matchParentStaticCall($soleStmt, $classMethod->name->toString()); + if (! $staticCall instanceof StaticCall) { + return false; + } + + $args = $staticCall->getArgs(); + $params = $classMethod->getParams(); + if (count($args) !== count($params)) { + return false; + } + + $paramNames = []; + foreach ($params as $param) { + if ($param->variadic || $param->byRef || $param->default instanceof Node || $param->attrGroups !== []) { + return false; + } + + $paramNames[] = $this->getName($param->var); + } + + $argNames = []; + foreach ($args as $arg) { + if ($arg->name instanceof Node || $arg->unpack) { + return false; + } + + if (! $arg->value instanceof Variable) { + return false; + } + + $argNames[] = $this->getName($arg->value); + } + + return $paramNames === $argNames; + } + + private function matchParentStaticCall(Stmt $stmt, string $methodName): ?StaticCall + { + if ($stmt instanceof Return_) { + $expr = $stmt->expr; + } elseif ($stmt instanceof Expression) { + $expr = $stmt->expr; + } else { + return null; + } + + if (! $expr instanceof StaticCall) { + return null; + } + + if ($expr->isFirstClassCallable()) { + return null; + } + + if (! $this->isName($expr->class, ObjectReference::PARENT)) { + return null; + } + + if (! $this->isName($expr->name, $methodName)) { + return null; + } + + return $expr; + } + + private function isSignatureMatching( + ClassMethod $classMethod, + ExtendedMethodReflection $extendedMethodReflection + ): bool { + if ($classMethod->isStatic() !== $extendedMethodReflection->isStatic()) { + return false; + } + + if ($classMethod->isPublic() !== $extendedMethodReflection->isPublic()) { + return false; + } + + $extendedParametersAcceptor = $extendedMethodReflection->getOnlyVariant(); + $parameterReflections = $extendedParametersAcceptor->getParameters(); + + foreach ($classMethod->getParams() as $position => $param) { + $parameterReflection = $parameterReflections[$position] ?? null; + if ($parameterReflection === null) { + return false; + } + + // no type override + if ($param->type === null) { + continue; + } + + $parentParameterType = $this->staticTypeMapper->mapPHPStanTypeToPhpParserNode( + $parameterReflection->getType(), + TypeKind::PARAM + ); + + if (! $this->nodeComparator->areNodesEqual($param->type, $parentParameterType)) { + return false; + } + } + + // no return type override + if (! $classMethod->returnType instanceof Node) { + return true; + } + + $parentReturnType = $this->staticTypeMapper->mapPHPStanTypeToPhpParserNode( + $extendedParametersAcceptor->getReturnType(), + TypeKind::RETURN + ); + + return $this->nodeComparator->areNodesEqual($classMethod->returnType, $parentReturnType); + } +} diff --git a/src/Config/Level/DeadCodeLevel.php b/src/Config/Level/DeadCodeLevel.php index e51ea974fb9..0c689290a8c 100644 --- a/src/Config/Level/DeadCodeLevel.php +++ b/src/Config/Level/DeadCodeLevel.php @@ -19,6 +19,7 @@ use Rector\DeadCode\Rector\ClassMethod\RemoveEmptyClassMethodRector; use Rector\DeadCode\Rector\ClassMethod\RemoveMixedDocblockOverruledByNativeTypeRector; use Rector\DeadCode\Rector\ClassMethod\RemoveNullTagValueNodeRector; +use Rector\DeadCode\Rector\ClassMethod\RemoveParentDelegatingClassMethodRector; use Rector\DeadCode\Rector\ClassMethod\RemoveParentDelegatingConstructorRector; use Rector\DeadCode\Rector\ClassMethod\RemoveReturnTagIncompatibleWithNativeTypeRector; use Rector\DeadCode\Rector\ClassMethod\RemoveUnusedConstructorParamRector; @@ -153,6 +154,7 @@ final class DeadCodeLevel RemoveParentCallWithoutParentRector::class, RemoveParentDelegatingConstructorRector::class, + RemoveParentDelegatingClassMethodRector::class, RemoveDeadConditionAboveReturnRector::class, RemoveDeadLoopRector::class, From a0c4611866b4c7f6230964d33b97be136731e977 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Thu, 23 Jul 2026 23:39:56 +0200 Subject: [PATCH 2/2] guard added native types, private parent method, @deprecated docblock --- .../Fixture/skip_added_native_types.php.inc | 13 +++++++++ .../Fixture/skip_deprecated_docblock.php.inc | 16 +++++++++++ .../skip_private_parent_method.php.inc | 13 +++++++++ .../Source/SomeParentWithDocblockTypes.php | 17 +++++++++++ .../Source/SomeParentWithPrivateMethod.php | 12 ++++++++ ...emoveParentDelegatingClassMethodRector.php | 28 +++++++++++++++---- 6 files changed, 94 insertions(+), 5 deletions(-) create mode 100644 rules-tests/DeadCode/Rector/ClassMethod/RemoveParentDelegatingClassMethodRector/Fixture/skip_added_native_types.php.inc create mode 100644 rules-tests/DeadCode/Rector/ClassMethod/RemoveParentDelegatingClassMethodRector/Fixture/skip_deprecated_docblock.php.inc create mode 100644 rules-tests/DeadCode/Rector/ClassMethod/RemoveParentDelegatingClassMethodRector/Fixture/skip_private_parent_method.php.inc create mode 100644 rules-tests/DeadCode/Rector/ClassMethod/RemoveParentDelegatingClassMethodRector/Source/SomeParentWithDocblockTypes.php create mode 100644 rules-tests/DeadCode/Rector/ClassMethod/RemoveParentDelegatingClassMethodRector/Source/SomeParentWithPrivateMethod.php diff --git a/rules-tests/DeadCode/Rector/ClassMethod/RemoveParentDelegatingClassMethodRector/Fixture/skip_added_native_types.php.inc b/rules-tests/DeadCode/Rector/ClassMethod/RemoveParentDelegatingClassMethodRector/Fixture/skip_added_native_types.php.inc new file mode 100644 index 00000000000..f9cd0205f43 --- /dev/null +++ b/rules-tests/DeadCode/Rector/ClassMethod/RemoveParentDelegatingClassMethodRector/Fixture/skip_added_native_types.php.inc @@ -0,0 +1,13 @@ +phpDocInfoFactory->createFromNodeOrEmpty($classMethod); return $phpDocInfo->hasByNames( - ['@param', '@phpstan-param', '@psalm-param', '@return', '@phpstan-return', '@psalm-return'] + [ + '@param', + '@phpstan-param', + '@psalm-param', + '@return', + '@phpstan-return', + '@psalm-return', + '@deprecated', + ] ); } @@ -143,7 +152,14 @@ private function matchParentMethodReflection(ClassMethod $classMethod): ?Extende return null; } - return $parentClassReflection->getNativeMethod($methodName); + $extendedMethodReflection = $parentClassReflection->getNativeMethod($methodName); + + // a private parent method is not visible in the child scope + if ($extendedMethodReflection->isPrivate()) { + return null; + } + + return $extendedMethodReflection; } /** @@ -233,7 +249,7 @@ private function isSignatureMatching( foreach ($classMethod->getParams() as $position => $param) { $parameterReflection = $parameterReflections[$position] ?? null; - if ($parameterReflection === null) { + if (! $parameterReflection instanceof ExtendedParameterReflection) { return false; } @@ -242,8 +258,10 @@ private function isSignatureMatching( continue; } + // compare native types only, as the child method may add a native type + // the parent only has in a docblock - removing it would drop a runtime check $parentParameterType = $this->staticTypeMapper->mapPHPStanTypeToPhpParserNode( - $parameterReflection->getType(), + $parameterReflection->getNativeType(), TypeKind::PARAM ); @@ -258,7 +276,7 @@ private function isSignatureMatching( } $parentReturnType = $this->staticTypeMapper->mapPHPStanTypeToPhpParserNode( - $extendedParametersAcceptor->getReturnType(), + $extendedParametersAcceptor->getNativeReturnType(), TypeKind::RETURN );