diff --git a/phpstan.neon b/phpstan.neon index 616fe4cdfa7..9720808bdc2 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -326,6 +326,11 @@ parameters: - '#expects array, array given#' - '#should return non\-empty\-string but returns string#' + # known non-empty class method + - + message: '#Offset 0 might not exist on array\|null#' + path: rules/DeadCode/Rector/ClassMethod/RemoveParentDelegatingConstructorRector.php + # false positive, can accept non-class string - '#Parameter \#1 \$name of method PHPStan\\BetterReflection\\Reflection\\Adapter\\ReflectionClass\:\:getAttributes\(\) expects class\-string\|null, string given#' diff --git a/rules-tests/DeadCode/Rector/ClassMethod/RemoveParentDelegatingConstructorRector/Fixture/skip_different_args.php.inc b/rules-tests/DeadCode/Rector/ClassMethod/RemoveParentDelegatingConstructorRector/Fixture/skip_different_args.php.inc new file mode 100644 index 00000000000..f9acb9e5ce5 --- /dev/null +++ b/rules-tests/DeadCode/Rector/ClassMethod/RemoveParentDelegatingConstructorRector/Fixture/skip_different_args.php.inc @@ -0,0 +1,13 @@ + +----- + diff --git a/rules-tests/DeadCode/Rector/ClassMethod/RemoveParentDelegatingConstructorRector/RemoveParentDelegatingConstructorRectorTest.php b/rules-tests/DeadCode/Rector/ClassMethod/RemoveParentDelegatingConstructorRector/RemoveParentDelegatingConstructorRectorTest.php new file mode 100644 index 00000000000..e65a7af8078 --- /dev/null +++ b/rules-tests/DeadCode/Rector/ClassMethod/RemoveParentDelegatingConstructorRector/RemoveParentDelegatingConstructorRectorTest.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/RemoveParentDelegatingConstructorRector/config/configured_rule.php b/rules-tests/DeadCode/Rector/ClassMethod/RemoveParentDelegatingConstructorRector/config/configured_rule.php new file mode 100644 index 00000000000..8f49ec2f813 --- /dev/null +++ b/rules-tests/DeadCode/Rector/ClassMethod/RemoveParentDelegatingConstructorRector/config/configured_rule.php @@ -0,0 +1,9 @@ +withRules([RemoveParentDelegatingConstructorRector::class]); diff --git a/rules/DeadCode/Rector/ClassMethod/RemoveParentDelegatingConstructorRector.php b/rules/DeadCode/Rector/ClassMethod/RemoveParentDelegatingConstructorRector.php new file mode 100644 index 00000000000..8f9e6bee211 --- /dev/null +++ b/rules/DeadCode/Rector/ClassMethod/RemoveParentDelegatingConstructorRector.php @@ -0,0 +1,238 @@ +> + */ + public function getNodeTypes(): array + { + return [ClassMethod::class]; + } + + /** + * @param ClassMethod $node + */ + public function refactor(Node $node): ?int + { + if (! $this->isName($node, MethodName::CONSTRUCT)) { + return null; + } + + if ($node->stmts === null || count($node->stmts) !== 1) { + return null; + } + + $parentMethodReflection = $this->matchParentConstructorReflection($node); + if (! $parentMethodReflection instanceof ExtendedMethodReflection) { + return null; + } + + $soleStmt = $node->stmts[0]; + $parentCallArgs = $this->matchParentConstructorCallArgs($soleStmt); + if ($parentCallArgs === null) { + return null; + } + + // match count and order + if (! $this->isParameterAndArgCountAndOrderIdentical($node)) { + return null; + } + + // match parameter types and parent constructor types + if (! $this->areConstructorAndParentParameterTypesMatching($node, $parentMethodReflection)) { + return null; + } + + return NodeVisitor::REMOVE_NODE; + } + + private function matchParentConstructorReflection(ClassMethod $classMethod): ?ExtendedMethodReflection + { + $scope = ScopeFetcher::fetch($classMethod); + + $classReflection = $scope->getClassReflection(); + if (! $classReflection instanceof ClassReflection) { + return null; + } + + $parentClassReflection = $classReflection->getParentClass(); + if (! $parentClassReflection instanceof ClassReflection) { + return null; + } + + if (! $parentClassReflection->hasConstructor()) { + return null; + } + + return $parentClassReflection->getConstructor(); + } + + /** + * Looking for parent::__construct() + * + * @return Arg[]|null + */ + private function matchParentConstructorCallArgs(Stmt $stmt): ?array + { + if (! $stmt instanceof Expression) { + return null; + } + + if (! $stmt->expr instanceof StaticCall) { + return null; + } + + $staticCall = $stmt->expr; + if ($staticCall->isFirstClassCallable()) { + return null; + } + + if (! $this->isName($staticCall->class, ObjectReference::PARENT)) { + return null; + } + + if (! $this->isName($staticCall->name, MethodName::CONSTRUCT)) { + return null; + } + + return $staticCall->getArgs(); + } + + private function isParameterAndArgCountAndOrderIdentical(ClassMethod $classMethod): bool + { + $soleStmt = $classMethod->stmts[0]; + + $parentCallArgs = $this->matchParentConstructorCallArgs($soleStmt); + if ($parentCallArgs === null) { + return false; + } + + $constructorParams = $classMethod->getParams(); + if (count($constructorParams) !== count($parentCallArgs)) { + return false; + } + + // match passed names in the same order + $paramNames = []; + foreach ($constructorParams as $constructorParam) { + $paramNames[] = $this->getName($constructorParam->var); + } + + $argNames = []; + foreach ($parentCallArgs as $parentCallArg) { + $argValue = $parentCallArg->value; + if (! $argValue instanceof Variable) { + return false; + } + + $argNames[] = $this->getName($argValue); + } + + return $paramNames === $argNames; + } + + private function areConstructorAndParentParameterTypesMatching( + ClassMethod $classMethod, + ExtendedMethodReflection $extendedMethodReflection + ): bool { + foreach ($classMethod->getParams() as $position => $param) { + $parameterType = $param->type; + + // no type override + if ($parameterType === null) { + continue; + } + + $parametersSelector = $extendedMethodReflection->getOnlyVariant(); + + foreach ($parametersSelector->getParameters() as $index => $parameterReflection) { + if ($index !== $position) { + continue; + } + + $parentParameterType = $this->staticTypeMapper->mapPHPStanTypeToPhpParserNode( + $parameterReflection->getType(), + TypeKind::PARAM + ); + + if (! $this->nodeComparator->areNodesEqual($parameterType, $parentParameterType)) { + return false; + } + } + } + + return true; + } +} diff --git a/src/Config/Level/DeadCodeLevel.php b/src/Config/Level/DeadCodeLevel.php index 9c0c8715b8c..70a2ab4d44e 100644 --- a/src/Config/Level/DeadCodeLevel.php +++ b/src/Config/Level/DeadCodeLevel.php @@ -17,6 +17,7 @@ use Rector\DeadCode\Rector\ClassMethod\RemoveArgumentFromDefaultParentCallRector; use Rector\DeadCode\Rector\ClassMethod\RemoveEmptyClassMethodRector; use Rector\DeadCode\Rector\ClassMethod\RemoveNullTagValueNodeRector; +use Rector\DeadCode\Rector\ClassMethod\RemoveParentDelegatingConstructorRector; use Rector\DeadCode\Rector\ClassMethod\RemoveUnusedConstructorParamRector; use Rector\DeadCode\Rector\ClassMethod\RemoveUnusedPrivateMethodParameterRector; use Rector\DeadCode\Rector\ClassMethod\RemoveUnusedPrivateMethodRector; @@ -132,7 +133,10 @@ final class DeadCodeLevel RemoveConditionExactReturnRector::class, RemoveDeadStmtRector::class, UnwrapFutureCompatibleIfPhpVersionRector::class, + RemoveParentCallWithoutParentRector::class, + RemoveParentDelegatingConstructorRector::class, + RemoveDeadConditionAboveReturnRector::class, RemoveDeadLoopRector::class, diff --git a/src/Reporting/DeprecatedRulesReporter.php b/src/Reporting/DeprecatedRulesReporter.php index 5f941be6faa..aa5ef694428 100644 --- a/src/Reporting/DeprecatedRulesReporter.php +++ b/src/Reporting/DeprecatedRulesReporter.php @@ -4,13 +4,13 @@ namespace Rector\Reporting; -use Rector\PhpParserNode\FileNode; use Rector\Configuration\Deprecation\Contract\DeprecatedInterface; use Rector\Configuration\Option; use Rector\Configuration\Parameter\SimpleParameterProvider; use Rector\Contract\PhpParser\Node\StmtsAwareInterface; use Rector\Contract\Rector\RectorInterface; use Rector\PhpParser\Enum\NodeGroup; +use Rector\PhpParserNode\FileNode; use ReflectionMethod; use Symfony\Component\Console\Style\SymfonyStyle;