From df8e1f35bb3be4be221cf70e08f3095dd02cbc3a Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Thu, 18 Apr 2024 16:37:47 +0200 Subject: [PATCH] Detect unused static method call on a separate line with possibly pure method --- conf/config.level4.neon | 10 ++ ...MethodStatementWithoutImpurePointsRule.php | 68 ++++++++++++ .../PossiblyPureStaticCallCollector.php | 55 +++++++++ ...odStatementWithoutImpurePointsRuleTest.php | 69 ++++++++++++ ...to-static-method-without-impure-points.php | 104 ++++++++++++++++++ 5 files changed, 306 insertions(+) create mode 100644 src/Rules/DeadCode/CallToStaticMethodStatementWithoutImpurePointsRule.php create mode 100644 src/Rules/DeadCode/PossiblyPureStaticCallCollector.php create mode 100644 tests/PHPStan/Rules/DeadCode/CallToStaticMethodStatementWithoutImpurePointsRuleTest.php create mode 100644 tests/PHPStan/Rules/DeadCode/data/call-to-static-method-without-impure-points.php diff --git a/conf/config.level4.neon b/conf/config.level4.neon index 6514caa5a2..cb79c9cca5 100644 --- a/conf/config.level4.neon +++ b/conf/config.level4.neon @@ -50,6 +50,10 @@ conditionalTags: phpstan.collector: %featureToggles.pure% PHPStan\Rules\DeadCode\MethodWithoutImpurePointsCollector: phpstan.collector: %featureToggles.pure% + PHPStan\Rules\DeadCode\CallToStaticMethodStatementWithoutImpurePointsRule: + phpstan.rules.rule: %featureToggles.pure% + PHPStan\Rules\DeadCode\PossiblyPureStaticCallCollector: + phpstan.collector: %featureToggles.pure% parameters: checkAdvancedIsset: true @@ -124,6 +128,12 @@ services: - class: PHPStan\Rules\DeadCode\PossiblyPureMethodCallCollector + - + class: PHPStan\Rules\DeadCode\CallToStaticMethodStatementWithoutImpurePointsRule + + - + class: PHPStan\Rules\DeadCode\PossiblyPureStaticCallCollector + - class: PHPStan\Rules\DeadCode\UnusedPrivatePropertyRule arguments: diff --git a/src/Rules/DeadCode/CallToStaticMethodStatementWithoutImpurePointsRule.php b/src/Rules/DeadCode/CallToStaticMethodStatementWithoutImpurePointsRule.php new file mode 100644 index 0000000000..c64b29f8bf --- /dev/null +++ b/src/Rules/DeadCode/CallToStaticMethodStatementWithoutImpurePointsRule.php @@ -0,0 +1,68 @@ + + */ +class CallToStaticMethodStatementWithoutImpurePointsRule implements Rule +{ + + public function getNodeType(): string + { + return CollectedDataNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $methods = []; + foreach ($node->get(MethodWithoutImpurePointsCollector::class) as $collected) { + foreach ($collected as [$className, $methodName, $classDisplayName]) { + $lowerClassName = strtolower($className); + + if (!array_key_exists($lowerClassName, $methods)) { + $methods[$lowerClassName] = []; + } + $methods[$lowerClassName][strtolower($methodName)] = $classDisplayName . '::' . $methodName; + } + } + + $errors = []; + foreach ($node->get(PossiblyPureStaticCallCollector::class) as $filePath => $data) { + foreach ($data as [$className, $method, $line]) { + $lowerClassName = strtolower($className); + + if (!array_key_exists($lowerClassName, $methods)) { + continue; + } + + $lowerMethod = strtolower($method); + if (!array_key_exists($lowerMethod, $methods[$lowerClassName])) { + continue; + } + + $originalMethodName = $methods[$lowerClassName][$lowerMethod]; + + $errors[] = RuleErrorBuilder::message(sprintf( + 'Call to %s() on a separate line has no effect.', + $originalMethodName, + ))->file($filePath) + ->line($line) + ->identifier('staticMethod.resultUnused') + ->build(); + } + } + + return $errors; + } + +} diff --git a/src/Rules/DeadCode/PossiblyPureStaticCallCollector.php b/src/Rules/DeadCode/PossiblyPureStaticCallCollector.php new file mode 100644 index 0000000000..d934b57a6d --- /dev/null +++ b/src/Rules/DeadCode/PossiblyPureStaticCallCollector.php @@ -0,0 +1,55 @@ + + */ +class PossiblyPureStaticCallCollector implements Collector +{ + + public function __construct() + { + } + + public function getNodeType(): string + { + return Expression::class; + } + + public function processNode(Node $node, Scope $scope) + { + if (!$node->expr instanceof Node\Expr\StaticCall) { + return null; + } + if (!$node->expr->name instanceof Node\Identifier) { + return null; + } + + if (!$node->expr->class instanceof Node\Name) { + return null; + } + + $methodName = $node->expr->name->toString(); + $calledOnType = $scope->resolveTypeByName($node->expr->class); + $methodReflection = $scope->getMethodReflection($calledOnType, $methodName); + + if ($methodReflection === null) { + return null; + } + if (!$methodReflection->isPure()->maybe()) { + return null; + } + if (!$methodReflection->hasSideEffects()->maybe()) { + return null; + } + + return [$methodReflection->getDeclaringClass()->getName(), $methodReflection->getName(), $node->getStartLine()]; + } + +} diff --git a/tests/PHPStan/Rules/DeadCode/CallToStaticMethodStatementWithoutImpurePointsRuleTest.php b/tests/PHPStan/Rules/DeadCode/CallToStaticMethodStatementWithoutImpurePointsRuleTest.php new file mode 100644 index 0000000000..74258d25a1 --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/CallToStaticMethodStatementWithoutImpurePointsRuleTest.php @@ -0,0 +1,69 @@ + + */ +class CallToStaticMethodStatementWithoutImpurePointsRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new CallToStaticMethodStatementWithoutImpurePointsRule(); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/call-to-static-method-without-impure-points.php'], [ + [ + 'Call to CallToStaticMethodWithoutImpurePoints\X::myFunc() on a separate line has no effect.', + 6, + ], + [ + 'Call to CallToStaticMethodWithoutImpurePoints\X::myFunc() on a separate line has no effect.', + 7, + ], + [ + 'Call to CallToStaticMethodWithoutImpurePoints\X::myFunc() on a separate line has no effect.', + 16, + ], + [ + 'Call to CallToStaticMethodWithoutImpurePoints\y::myFunc() on a separate line has no effect.', + 18, + ], + [ + 'Call to CallToStaticMethodWithoutImpurePoints\y::myFunc() on a separate line has no effect.', + 20, + ], + [ + 'Call to CallToStaticMethodWithoutImpurePoints\SubSubY::mySubSubFunc() on a separate line has no effect.', + 21, + ], + [ + 'Call to CallToStaticMethodWithoutImpurePoints\y::myFunc() on a separate line has no effect.', + 48, + ], + [ + 'Call to CallToStaticMethodWithoutImpurePoints\y::myFunc() on a separate line has no effect.', + 53, + ], + [ + 'Call to CallToStaticMethodWithoutImpurePoints\y::myFunc() on a separate line has no effect.', + 58, + ], + ]); + } + + protected function getCollectors(): array + { + return [ + new PossiblyPureStaticCallCollector(), + new MethodWithoutImpurePointsCollector(), + ]; + } + +} diff --git a/tests/PHPStan/Rules/DeadCode/data/call-to-static-method-without-impure-points.php b/tests/PHPStan/Rules/DeadCode/data/call-to-static-method-without-impure-points.php new file mode 100644 index 0000000000..f236631f1d --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/call-to-static-method-without-impure-points.php @@ -0,0 +1,104 @@ +