Skip to content

Commit e36bb83

Browse files
Introduce getNextStatements in UnreachableStatementNode
Co-authored-by: Ondrej Mirtes <[email protected]>
1 parent 068be33 commit e36bb83

6 files changed

+203
-16
lines changed

src/Analyser/NodeScopeResolver.php

Lines changed: 46 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -316,13 +316,38 @@ public function processNodes(
316316
}
317317

318318
$alreadyTerminated = true;
319-
$nextStmt = $this->getFirstUnreachableNode(array_slice($nodes, $i + 1), true);
320-
if (!$nextStmt instanceof Node\Stmt) {
319+
$nextStmts = $this->getNextUnreachableStatements(array_slice($nodes, $i + 1), true);
320+
$this->processUnreachableStatement($nextStmts, $scope, $nodeCallback);
321+
}
322+
}
323+
324+
/**
325+
* @param Node\Stmt[] $nextStmts
326+
* @param callable(Node $node, Scope $scope): void $nodeCallback
327+
*/
328+
private function processUnreachableStatement(array $nextStmts, MutatingScope $scope, callable $nodeCallback): void
329+
{
330+
if ($nextStmts === []) {
331+
return;
332+
}
333+
334+
$unreachableStatement = null;
335+
$nextStatements = [];
336+
337+
foreach ($nextStmts as $key => $nextStmt) {
338+
if ($key === 0) {
339+
$unreachableStatement = $nextStmt;
321340
continue;
322341
}
323342

324-
$nodeCallback(new UnreachableStatementNode($nextStmt), $scope);
343+
$nextStatements[] = $nextStmt;
344+
}
345+
346+
if (!$unreachableStatement instanceof Node\Stmt) {
347+
return;
325348
}
349+
350+
$nodeCallback(new UnreachableStatementNode($unreachableStatement, $nextStatements), $scope);
326351
}
327352

328353
/**
@@ -409,11 +434,8 @@ public function processStmtNodes(
409434
}
410435

411436
$alreadyTerminated = true;
412-
$nextStmt = $this->getFirstUnreachableNode(array_slice($stmts, $i + 1), $parentNode instanceof Node\Stmt\Namespace_);
413-
if ($nextStmt === null) {
414-
continue;
415-
}
416-
$nodeCallback(new UnreachableStatementNode($nextStmt), $scope);
437+
$nextStmts = $this->getNextUnreachableStatements(array_slice($stmts, $i + 1), $parentNode instanceof Node\Stmt\Namespace_);
438+
$this->processUnreachableStatement($nextStmts, $scope, $nodeCallback);
417439
}
418440

419441
$statementResult = new StatementResult($scope, $hasYield, $alreadyTerminated, $exitPoints, $throwPoints, $impurePoints);
@@ -6514,22 +6536,31 @@ private function getPhpDocReturnType(ResolvedPhpDocBlock $resolvedPhpDoc, Type $
65146536
}
65156537

65166538
/**
6517-
* @template T of Node
6518-
* @param array<T> $nodes
6519-
* @return T|null
6539+
* @param array<Node> $nodes
6540+
* @return list<Node\Stmt>
65206541
*/
6521-
private function getFirstUnreachableNode(array $nodes, bool $earlyBinding): ?Node
6542+
private function getNextUnreachableStatements(array $nodes, bool $earlyBinding): array
65226543
{
6544+
$stmts = [];
6545+
$isPassedUnreachableStatement = false;
65236546
foreach ($nodes as $node) {
6547+
if ($earlyBinding && ($node instanceof Node\Stmt\Function_ || $node instanceof Node\Stmt\ClassLike || $node instanceof Node\Stmt\HaltCompiler)) {
6548+
continue;
6549+
}
6550+
if ($isPassedUnreachableStatement && $node instanceof Node\Stmt) {
6551+
$stmts[] = $node;
6552+
continue;
6553+
}
65246554
if ($node instanceof Node\Stmt\Nop) {
65256555
continue;
65266556
}
6527-
if ($earlyBinding && ($node instanceof Node\Stmt\Function_ || $node instanceof Node\Stmt\ClassLike || $node instanceof Node\Stmt\HaltCompiler)) {
6557+
if (!$node instanceof Node\Stmt) {
65286558
continue;
65296559
}
6530-
return $node;
6560+
$stmts[] = $node;
6561+
$isPassedUnreachableStatement = true;
65316562
}
6532-
return null;
6563+
return $stmts;
65336564
}
65346565

65356566
}

src/Node/UnreachableStatementNode.php

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,12 @@
1010
final class UnreachableStatementNode extends Stmt implements VirtualNode
1111
{
1212

13-
public function __construct(private Stmt $originalStatement)
13+
/** @param Stmt[] $nextStatements */
14+
public function __construct(private Stmt $originalStatement, private array $nextStatements = [])
1415
{
1516
parent::__construct($originalStatement->getAttributes());
17+
18+
$this->nextStatements = $nextStatements;
1619
}
1720

1821
public function getOriginalStatement(): Stmt
@@ -33,4 +36,12 @@ public function getSubNodeNames(): array
3336
return [];
3437
}
3538

39+
/**
40+
* @return Stmt[]
41+
*/
42+
public function getNextStatements(): array
43+
{
44+
return $this->nextStatements;
45+
}
46+
3647
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\DeadCode;
4+
5+
use PhpParser\Node;
6+
use PHPStan\Analyser\Scope;
7+
use PHPStan\Node\UnreachableStatementNode;
8+
use PHPStan\Rules\Rule;
9+
use PHPStan\Rules\RuleErrorBuilder;
10+
use PHPStan\Testing\RuleTestCase;
11+
12+
/**
13+
* @extends RuleTestCase<Rule>
14+
*/
15+
class UnreachableStatementNextStatementsRuleTest extends RuleTestCase
16+
{
17+
18+
/**
19+
* @return Rule<Node>
20+
*/
21+
protected function getRule(): Rule
22+
{
23+
return new class implements Rule {
24+
25+
public function getNodeType(): string
26+
{
27+
return UnreachableStatementNode::class;
28+
}
29+
30+
/**
31+
* @param UnreachableStatementNode $node
32+
*/
33+
public function processNode(Node $node, Scope $scope): array
34+
{
35+
$errors = [
36+
RuleErrorBuilder::message('First unreachable')
37+
->identifier('tests.nextUnreachableStatements')
38+
->build(),
39+
];
40+
41+
foreach ($node->getNextStatements() as $nextStatement) {
42+
$errors[] = RuleErrorBuilder::message('Another unreachable')
43+
->line($nextStatement->getStartLine())
44+
->identifier('tests.nextUnreachableStatements')
45+
->build();
46+
}
47+
48+
return $errors;
49+
}
50+
51+
};
52+
}
53+
54+
public function testRule(): void
55+
{
56+
$this->analyse([__DIR__ . '/data/multiple_unreachable.php'], [
57+
[
58+
'First unreachable',
59+
14,
60+
],
61+
[
62+
'Another unreachable',
63+
15,
64+
],
65+
[
66+
'Another unreachable',
67+
17,
68+
],
69+
[
70+
'Another unreachable',
71+
22,
72+
],
73+
]);
74+
}
75+
76+
public function testRuleTopLevel(): void
77+
{
78+
$this->analyse([__DIR__ . '/data/multiple_unreachable_top_level.php'], [
79+
[
80+
'First unreachable',
81+
9,
82+
],
83+
[
84+
'Another unreachable',
85+
10,
86+
],
87+
[
88+
'Another unreachable',
89+
17,
90+
],
91+
]);
92+
}
93+
94+
}

tests/PHPStan/Rules/DeadCode/UnreachableStatementRuleTest.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,4 +230,15 @@ public function testBug11992(): void
230230
$this->analyse([__DIR__ . '/data/bug-11992.php'], []);
231231
}
232232

233+
public function testMultipleUnreachable(): void
234+
{
235+
$this->treatPhpDocTypesAsCertain = true;
236+
$this->analyse([__DIR__ . '/data/multiple_unreachable.php'], [
237+
[
238+
'Unreachable statement - code above always terminates.',
239+
14,
240+
],
241+
]);
242+
}
243+
233244
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
2+
3+
namespace MultipleUnreachable;
4+
5+
/**
6+
* @param 'foo' $foo
7+
*/
8+
function foo($foo)
9+
{
10+
if ($foo === 'foo') {
11+
return 1;
12+
}
13+
14+
echo 'statement 1';
15+
echo 'statement 2';
16+
17+
function innerFunction()
18+
{
19+
echo 'statement 3';
20+
}
21+
22+
echo innerFunction();
23+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
3+
namespace MultipleUnreachableTopLevel;
4+
5+
if (true) {
6+
return 1;
7+
}
8+
9+
echo 'statement 1';
10+
echo 'statement 2';
11+
12+
function func()
13+
{
14+
echo 'statement 3';
15+
}
16+
17+
echo func();

0 commit comments

Comments
 (0)