Skip to content

Commit 483baf9

Browse files
authored
UnusedVariableRule: detect variable usage in compact() calls (#56)
* unused-variables: adapt test and show compact is not honored * unused-variables: detect strings (=var use) in compact function call
1 parent 409d5c0 commit 483baf9

File tree

3 files changed

+66
-9
lines changed

3 files changed

+66
-9
lines changed

lib/UnusedVariableRule.php

Lines changed: 48 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,13 @@
88
use PhpParser\Node\Expr\ArrowFunction;
99
use PhpParser\Node\Expr\Assign;
1010
use PhpParser\Node\Expr\Closure;
11+
use PhpParser\Node\Expr\FuncCall;
1112
use PhpParser\Node\Expr\Variable;
1213
use PhpParser\Node\FunctionLike;
14+
use PhpParser\Node\Name;
1315
use PhpParser\Node\Scalar\String_;
1416
use PHPStan\Analyser\Scope;
17+
use PHPStan\Broker\Broker;
1518
use PHPStan\Rules\Rule;
1619
use PHPStan\Rules\RuleError;
1720
use PHPStan\Rules\RuleErrorBuilder;
@@ -33,6 +36,10 @@ final class UnusedVariableRule implements Rule
3336
'_SESSION' => true,
3437
];
3538

39+
public function __construct(
40+
private readonly Broker $broker,
41+
) {}
42+
3643
public function getNodeType(): string
3744
{
3845
return FunctionLike::class;
@@ -60,7 +67,7 @@ public function processNode(Node $node, Scope $scope): array
6067

6168
$unusedVariables = [];
6269
$usedVariables = [];
63-
$this->gatherVariablesUsage($node, $unusedVariables, $usedVariables, $parameters, $node);
70+
$this->gatherVariablesUsage($node, $scope, $unusedVariables, $usedVariables, $parameters, $node);
6471

6572
foreach ($unusedVariables as $varName => $var) {
6673
if (! isset($usedVariables[$varName])) {
@@ -82,7 +89,7 @@ public function processNode(Node $node, Scope $scope): array
8289
* @param bool[] $usedVariables
8390
* @param mixed[] $parameters
8491
*/
85-
private function gatherVariablesUsage(Node $node, array & $unusedVariables, array & $usedVariables, array $parameters = [], ?Node $originalNode = null): void
92+
private function gatherVariablesUsage(Node $node, Scope $scope, array & $unusedVariables, array & $usedVariables, array $parameters = [], ?Node $originalNode = null): void
8693
{
8794
if ($node instanceof FunctionLike
8895
&& $node !== $originalNode
@@ -98,20 +105,23 @@ private function gatherVariablesUsage(Node $node, array & $unusedVariables, arra
98105

99106
return;
100107
}
108+
109+
$this->captureVariablesUsedByCompactFunction($node, $scope, $usedVariables);
110+
101111
if ($node instanceof Assign) {
102112
if ($node->var instanceof Variable) {
103113
if (\is_string($node->var->name) && ! isset($parameters[$node->var->name]) && ! isset(self::GLOBAL_VARIABLES[$node->var->name])) {
104114
$unusedVariables[$node->var->name] = $node->var;
105115
}
106116
} else {
107117
if (\property_exists($node->var, 'var') && $node->var->var instanceof Node) {
108-
$this->gatherVariablesUsage($node->var->var, $unusedVariables, $usedVariables, $parameters);
118+
$this->gatherVariablesUsage($node->var->var, $scope, $unusedVariables, $usedVariables, $parameters);
109119
}
110120
if (\property_exists($node->var, 'dim') && $node->var->dim instanceof Node) {
111-
$this->gatherVariablesUsage($node->var->dim, $unusedVariables, $usedVariables, $parameters);
121+
$this->gatherVariablesUsage($node->var->dim, $scope, $unusedVariables, $usedVariables, $parameters);
112122
}
113123
if (\property_exists($node->var, 'name') && $node->var->name instanceof Node) {
114-
$this->gatherVariablesUsage($node->var->name, $unusedVariables, $usedVariables, $parameters);
124+
$this->gatherVariablesUsage($node->var->name, $scope, $unusedVariables, $usedVariables, $parameters);
115125
}
116126
}
117127
}
@@ -121,7 +131,7 @@ private function gatherVariablesUsage(Node $node, array & $unusedVariables, arra
121131
} elseif ($node->name instanceof String_) {
122132
$usedVariables[$node->name->value] = true;
123133
} else {
124-
$this->gatherVariablesUsage($node->name, $unusedVariables, $usedVariables, $parameters);
134+
$this->gatherVariablesUsage($node->name, $scope, $unusedVariables, $usedVariables, $parameters);
125135
}
126136
}
127137

@@ -130,14 +140,44 @@ private function gatherVariablesUsage(Node $node, array & $unusedVariables, arra
130140
continue;
131141
}
132142
if ($node->{$nodeName} instanceof Node) {
133-
$this->gatherVariablesUsage($node->{$nodeName}, $unusedVariables, $usedVariables, $parameters);
143+
$this->gatherVariablesUsage($node->{$nodeName}, $scope, $unusedVariables, $usedVariables, $parameters);
134144
} elseif (\is_iterable($node->{$nodeName})) {
135145
foreach ($node->{$nodeName} as $subNode) {
136146
if ($subNode instanceof Node) {
137-
$this->gatherVariablesUsage($subNode, $unusedVariables, $usedVariables, $parameters);
147+
$this->gatherVariablesUsage($subNode, $scope, $unusedVariables, $usedVariables, $parameters);
138148
}
139149
}
140150
}
141151
}
142152
}
153+
154+
/** @param bool[] $usedVariables */
155+
private function captureVariablesUsedByCompactFunction(Node $node, Scope $scope, array & $usedVariables): void
156+
{
157+
if (! $this->isCompactFunction($node, $scope)) {
158+
return;
159+
}
160+
// @phpstan-ignore-next-line: $node->args is valid due to $node being instanceof FuncCall
161+
foreach ($node->args as $arg) {
162+
if ($arg->value instanceof String_) {
163+
$usedVariables[$arg->value->value] = true;
164+
}
165+
}
166+
}
167+
168+
private function isCompactFunction(Node $node, Scope $scope): bool
169+
{
170+
if (! $node instanceof FuncCall) {
171+
return false;
172+
}
173+
if (! $node->name instanceof Name) {
174+
return false;
175+
}
176+
if (! $this->broker->hasFunction($node->name, $scope)) {
177+
return false;
178+
}
179+
$calledFunctionName = $this->broker->resolveFunctionName($node->name, $scope);
180+
181+
return 'compact' === $calledFunctionName;
182+
}
143183
}

tests/TestAsset/UnusedVariableRule/fixture.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,18 @@ function foo($ref)
7575

7676
$_SESSION = ['var19' => 1];
7777
$GLOBALS['foo'] = 'bar';
78+
79+
$usedInCompact = 'value';
80+
compact('usedInCompact');
81+
}
82+
83+
function bar()
84+
{
85+
$alsoUsedInCompact = 'value';
86+
87+
if ($alsoUsedInCompact) {
88+
compact('alsoUsedInCompact');
89+
}
7890
}
7991

8092
class Test
@@ -96,6 +108,9 @@ function foo(array $numVals)
96108
$this->key = 1;
97109
$this->key2();
98110
}
111+
112+
$usedInCompact = 'value';
113+
compact('usedInCompact');
99114
}
100115
}
101116

tests/UnusedVariableRuleTest.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@ final class UnusedVariableRuleTest extends RuleTestCase
1717
{
1818
protected function getRule(): Rule
1919
{
20-
return new UnusedVariableRule();
20+
$broker = $this->createBroker();
21+
22+
return new UnusedVariableRule($broker);
2123
}
2224

2325
public function testUnusedVariable(): void

0 commit comments

Comments
 (0)