Skip to content

Commit

Permalink
Detect unused function call on a separate line with possibly pure fun…
Browse files Browse the repository at this point in the history
…ction
  • Loading branch information
staabm committed Apr 17, 2024
1 parent c792050 commit a976987
Show file tree
Hide file tree
Showing 7 changed files with 271 additions and 0 deletions.
15 changes: 15 additions & 0 deletions conf/config.level4.neon
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\DeadCode;

use PhpParser\Node;
use PHPStan\Analyser\Scope;
use PHPStan\Node\CollectedDataNode;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;
use function array_key_exists;
use function sprintf;
use function strtolower;

/**
* @implements Rule<CollectedDataNode>
*/
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;
}

}
57 changes: 57 additions & 0 deletions src/Rules/DeadCode/FunctionWithoutImpurePointsCollector.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\DeadCode;

use PhpParser\Node;
use PHPStan\Analyser\Scope;
use PHPStan\Collectors\Collector;
use PHPStan\Node\FunctionReturnStatementsNode;
use PHPStan\Reflection\ParametersAcceptorSelector;
use function count;

/**
* @implements Collector<FunctionReturnStatementsNode, string>
*/
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();
}

}
47 changes: 47 additions & 0 deletions src/Rules/DeadCode/PossiblyPureFuncCallCollector.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\DeadCode;

use PhpParser\Node;
use PhpParser\Node\Expr\FuncCall;
use PHPStan\Analyser\Scope;
use PHPStan\Collectors\Collector;
use PHPStan\Reflection\ReflectionProvider;

/**
* @implements Collector<FuncCall, array{string, int}>
*/
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()];
}

}
27 changes: 27 additions & 0 deletions tests/PHPStan/Levels/data/callableVariance-4.json
Original file line number Diff line number Diff line change
@@ -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
}
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\DeadCode;

use PHPStan\Rules\Rule;
use PHPStan\Testing\RuleTestCase;

/**
* @extends RuleTestCase<CallToFunctionStatementWithoutImpurePointsRule>
*/
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(),
];
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

namespace CallToFunctionWithoutImpurePoints;

function myFunc()
{
}

function throwingFunc()
{
throw new \Exception();
}

function funcWithRef(&$a)
{
}

/** @phpstan-impure */
function impureFunc()
{
}

function callingImpureFunc()
{
impureFunc();
}

function (): void {
myFunc();
throwingFunc();
funcWithRef();
impureFunc();
callingImpureFunc();
};

0 comments on commit a976987

Please sign in to comment.