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 @@ + +----- + 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..ca33e9d2551 --- /dev/null +++ b/rules/DeadCode/Rector/ClassMethod/RemoveParentDelegatingClassMethodRector.php @@ -0,0 +1,285 @@ +> + */ + 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', + '@deprecated', + ] + ); + } + + 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; + } + + $extendedMethodReflection = $parentClassReflection->getNativeMethod($methodName); + + // a private parent method is not visible in the child scope + if ($extendedMethodReflection->isPrivate()) { + return null; + } + + return $extendedMethodReflection; + } + + /** + * 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 instanceof ExtendedParameterReflection) { + return false; + } + + // no type override + if ($param->type === null) { + 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->getNativeType(), + 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->getNativeReturnType(), + 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,