diff --git a/conf/config.level4.neon b/conf/config.level4.neon index d68be91709..6d774fcd46 100644 --- a/conf/config.level4.neon +++ b/conf/config.level4.neon @@ -38,6 +38,12 @@ conditionalTags: phpstan.collector: %featureToggles.pure% PHPStan\Rules\DeadCode\ConstructorWithoutImpurePointsCollector: phpstan.collector: %featureToggles.pure% + PHPStan\Rules\DeadCode\CallToFunctionStatementWithoutImpurePointsRule: + phpstan.rules.rule: %featureToggles.pure% + PHPStan\Rules\DeadCode\PossiblyPureFuncCallCollector: + phpstan.collector: %featureToggles.pure% + PHPStan\Rules\DeadCode\FunctionWithoutImpurePointsCollector: + phpstan.collector: %featureToggles.pure% parameters: checkAdvancedIsset: true @@ -94,6 +100,15 @@ services: - class: PHPStan\Rules\DeadCode\PossiblyPureNewCollector + - + class: PHPStan\Rules\DeadCode\CallToFunctionStatementWithoutImpurePointsRule + + - + class: PHPStan\Rules\DeadCode\FunctionWithoutImpurePointsCollector + + - + class: PHPStan\Rules\DeadCode\PossiblyPureFuncCallCollector + - class: PHPStan\Rules\DeadCode\UnusedPrivatePropertyRule arguments: diff --git a/src/Rules/DeadCode/CallToFunctionStatementWithoutImpurePointsRule.php b/src/Rules/DeadCode/CallToFunctionStatementWithoutImpurePointsRule.php new file mode 100644 index 0000000000..1635b12050 --- /dev/null +++ b/src/Rules/DeadCode/CallToFunctionStatementWithoutImpurePointsRule.php @@ -0,0 +1,54 @@ + + */ +class CallToFunctionStatementWithoutImpurePointsRule implements Rule +{ + + public function getNodeType(): string + { + return CollectedDataNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $functions = []; + foreach ($node->get(FunctionWithoutImpurePointsCollector::class) as [$functionName]) { + $functions[strtolower($functionName)] = $functionName; + } + + $errors = []; + foreach ($node->get(PossiblyPureFuncCallCollector::class) as $filePath => $data) { + foreach ($data as [$func, $line]) { + $lowerFunc = strtolower($func); + if (!array_key_exists($lowerFunc, $functions)) { + continue; + } + + $originalFunctionName = $functions[$lowerFunc]; + $errors[] = RuleErrorBuilder::message(sprintf( + 'Call to function %s() on a separate line has no effect.', + $originalFunctionName, + ))->file($filePath) + ->line($line) + ->identifier('function.resultUnused') + ->build(); + } + } + + return $errors; + } + +} diff --git a/src/Rules/DeadCode/FunctionWithoutImpurePointsCollector.php b/src/Rules/DeadCode/FunctionWithoutImpurePointsCollector.php new file mode 100644 index 0000000000..528e5c76e6 --- /dev/null +++ b/src/Rules/DeadCode/FunctionWithoutImpurePointsCollector.php @@ -0,0 +1,57 @@ + + */ +class FunctionWithoutImpurePointsCollector implements Collector +{ + + public function getNodeType(): string + { + return FunctionReturnStatementsNode::class; + } + + public function processNode(Node $node, Scope $scope) + { + $function = $node->getFunctionReflection(); + if (!$function->isPure()->maybe()) { + return null; + } + if (!$function->hasSideEffects()->maybe()) { + return null; + } + + if (count($node->getImpurePoints()) !== 0) { + return null; + } + + if (count($node->getStatementResult()->getThrowPoints()) !== 0) { + return null; + } + + $variant = ParametersAcceptorSelector::selectSingle($function->getVariants()); + foreach ($variant->getParameters() as $parameter) { + if (!$parameter->passedByReference()->createsNewVariable()) { + continue; + } + + return null; + } + + if (count($function->getAsserts()->getAll()) !== 0) { + return null; + } + + return $function->getName(); + } + +} diff --git a/src/Rules/DeadCode/PossiblyPureFuncCallCollector.php b/src/Rules/DeadCode/PossiblyPureFuncCallCollector.php new file mode 100644 index 0000000000..74b6e91548 --- /dev/null +++ b/src/Rules/DeadCode/PossiblyPureFuncCallCollector.php @@ -0,0 +1,47 @@ + + */ +class PossiblyPureFuncCallCollector implements Collector +{ + + public function __construct(private ReflectionProvider $reflectionProvider) + { + } + + public function getNodeType(): string + { + return FuncCall::class; + } + + public function processNode(Node $node, Scope $scope) + { + if (!$node->name instanceof Node\Name) { + return null; + } + + if (!$this->reflectionProvider->hasFunction($node->name, $scope)) { + return null; + } + + $functionReflection = $this->reflectionProvider->getFunction($node->name, $scope); + if (!$functionReflection->isPure()->maybe()) { + return null; + } + if (!$functionReflection->hasSideEffects()->maybe()) { + return null; + } + + return [$functionReflection->getName(), $node->getStartLine()]; + } + +} diff --git a/tests/PHPStan/Levels/data/callableVariance-4.json b/tests/PHPStan/Levels/data/callableVariance-4.json new file mode 100644 index 0000000000..1af09ec95a --- /dev/null +++ b/tests/PHPStan/Levels/data/callableVariance-4.json @@ -0,0 +1,27 @@ +[ + { + "message": "Call to function Levels\\CallableVariance\\d() on a separate line has no effect.", + "line": 81, + "ignorable": true + }, + { + "message": "Call to function Levels\\CallableVariance\\d() on a separate line has no effect.", + "line": 82, + "ignorable": true + }, + { + "message": "Call to function Levels\\CallableVariance\\d() on a separate line has no effect.", + "line": 83, + "ignorable": true + }, + { + "message": "Call to function Levels\\CallableVariance\\d() on a separate line has no effect.", + "line": 84, + "ignorable": true + }, + { + "message": "Call to function Levels\\CallableVariance\\d() on a separate line has no effect.", + "line": 85, + "ignorable": true + } +] \ No newline at end of file diff --git a/tests/PHPStan/Rules/DeadCode/CallToFunctionStatementWithoutImpurePointsRuleTest.php b/tests/PHPStan/Rules/DeadCode/CallToFunctionStatementWithoutImpurePointsRuleTest.php new file mode 100644 index 0000000000..3c5fe7a2b9 --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/CallToFunctionStatementWithoutImpurePointsRuleTest.php @@ -0,0 +1,37 @@ + + */ +class CallToFunctionStatementWithoutImpurePointsRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new CallToFunctionStatementWithoutImpurePointsRule(); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/call-to-function-without-impure-points.php'], [ + [ + 'Call to function CallToFunctionWithoutImpurePoints\myFunc() on a separate line has no effect.', + 29, + ], + ]); + } + + protected function getCollectors(): array + { + return [ + new PossiblyPureFuncCallCollector($this->createReflectionProvider()), + new FunctionWithoutImpurePointsCollector(), + ]; + } + +} diff --git a/tests/PHPStan/Rules/DeadCode/data/call-to-function-without-impure-points.php b/tests/PHPStan/Rules/DeadCode/data/call-to-function-without-impure-points.php new file mode 100644 index 0000000000..d1ae604b3e --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/call-to-function-without-impure-points.php @@ -0,0 +1,34 @@ +