Skip to content

Commit d20c8cf

Browse files
Do not let a trailing comment or an infinite loop hide a terminated block (#8476)
isTerminatedInLastStmts() judges a block by its last stmt and accepts only a Return_ or an exit/throw Expression. Two shapes slip past it: try { return something(); // nothing to do here <- parsed as Nop, and it is the last stmt } catch (Exception $e) { return null; } try { while (true) { <- never falls through, but is not a Return_ if ($a) { return 'A'; } $a = doSomething(); } } catch (Exception $e) { return null; } Both blocks always terminate, so isAlwaysTerminated() should say so, and callers that ask "does this method fall through?" get false. Reported in rectorphp/rector#9897, where it makes ConsoleExecuteReturnIntRector append an unreachable `return Command::SUCCESS;` that PHPStan then flags as deadCode.unreachable. Trailing Nops are now popped before the last stmt is read, and a trailing infinite loop goes through isTerminatedInfiniteLoop(), the same helper isAlwaysTerminated() already uses one level up -- so a loop carrying a break/goto still counts as falling through. Three fixtures for RemoveUnreachableStatementRector: a try/catch with a trailing comment, a try/catch ending in while (true), and an if/else with a trailing comment.
1 parent 63a2f16 commit d20c8cf

4 files changed

Lines changed: 147 additions & 0 deletions

File tree

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php
2+
3+
namespace Rector\Tests\DeadCode\Rector\Stmt\RemoveUnreachableStatementRector\Fixture;
4+
5+
class AlwaysTerminatedIfElseWithTrailingComment
6+
{
7+
public function run($a)
8+
{
9+
if ($a) {
10+
return 'A';
11+
// and that is all
12+
} else {
13+
return 'B';
14+
}
15+
16+
echo 'never executed';
17+
}
18+
}
19+
20+
?>
21+
-----
22+
<?php
23+
24+
namespace Rector\Tests\DeadCode\Rector\Stmt\RemoveUnreachableStatementRector\Fixture;
25+
26+
class AlwaysTerminatedIfElseWithTrailingComment
27+
{
28+
public function run($a)
29+
{
30+
if ($a) {
31+
return 'A';
32+
// and that is all
33+
} else {
34+
return 'B';
35+
}
36+
}
37+
}
38+
39+
?>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?php
2+
3+
namespace Rector\Tests\DeadCode\Rector\Stmt\RemoveUnreachableStatementRector\Fixture;
4+
5+
use Exception;
6+
7+
class AlwaysTerminatedTryCatchWithInfiniteWhile
8+
{
9+
public function run($a)
10+
{
11+
try {
12+
while (true) {
13+
if ($a) {
14+
return 'A';
15+
}
16+
17+
$a = doSomething();
18+
}
19+
} catch (Exception $e) {
20+
return null;
21+
}
22+
23+
echo 'never executed';
24+
}
25+
}
26+
27+
?>
28+
-----
29+
<?php
30+
31+
namespace Rector\Tests\DeadCode\Rector\Stmt\RemoveUnreachableStatementRector\Fixture;
32+
33+
use Exception;
34+
35+
class AlwaysTerminatedTryCatchWithInfiniteWhile
36+
{
37+
public function run($a)
38+
{
39+
try {
40+
while (true) {
41+
if ($a) {
42+
return 'A';
43+
}
44+
45+
$a = doSomething();
46+
}
47+
} catch (Exception $e) {
48+
return null;
49+
}
50+
}
51+
}
52+
53+
?>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<?php
2+
3+
namespace Rector\Tests\DeadCode\Rector\Stmt\RemoveUnreachableStatementRector\Fixture;
4+
5+
use Exception;
6+
7+
class AlwaysTerminatedTryCatchWithTrailingComment
8+
{
9+
public function run()
10+
{
11+
try {
12+
return something();
13+
// nothing to do here
14+
} catch (Exception $e) {
15+
return null;
16+
}
17+
18+
echo 'never executed';
19+
}
20+
}
21+
22+
?>
23+
-----
24+
<?php
25+
26+
namespace Rector\Tests\DeadCode\Rector\Stmt\RemoveUnreachableStatementRector\Fixture;
27+
28+
use Exception;
29+
30+
class AlwaysTerminatedTryCatchWithTrailingComment
31+
{
32+
public function run()
33+
{
34+
try {
35+
return something();
36+
// nothing to do here
37+
} catch (Exception $e) {
38+
return null;
39+
}
40+
}
41+
}
42+
43+
?>

‎src/NodeAnalyzer/TerminatedNodeAnalyzer.php‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,13 +273,25 @@ private function isTerminatedInLastStmtsIf(If_ $if): bool
273273
*/
274274
private function isTerminatedInLastStmts(array $stmts): bool
275275
{
276+
// a trailing comment is parsed as a Nop, which executes nothing and so
277+
// must not hide the terminating stmt in front of it
278+
while ($stmts !== [] && end($stmts) instanceof Nop) {
279+
array_pop($stmts);
280+
}
281+
276282
if ($stmts === []) {
277283
return false;
278284
}
279285

280286
$lastKey = array_key_last($stmts);
281287
$lastNode = $stmts[$lastKey];
282288

289+
// an infinite loop with no break terminates its block as surely as a
290+
// return does, the same way isAlwaysTerminated() reads it one level up
291+
if ($lastNode instanceof While_ || $lastNode instanceof Do_ || $lastNode instanceof For_) {
292+
return $this->isTerminatedInfiniteLoop($lastNode);
293+
}
294+
283295
if ($lastNode instanceof Expression) {
284296
return $lastNode->expr instanceof Exit_ || $lastNode->expr instanceof Throw_;
285297
}

0 commit comments

Comments
 (0)