Skip to content

Commit 0398dc7

Browse files
committed
[NodeAnalyzer] Skip trailing comments and require \Throwable catch in try/catch termination check
1 parent 5ac25a8 commit 0398dc7

5 files changed

Lines changed: 93 additions & 19 deletions

File tree

rules-tests/DeadCode/Rector/Stmt/RemoveUnreachableStatementRector/Fixture/always_terminated_try_catch.php.inc renamed to rules-tests/DeadCode/Rector/Stmt/RemoveUnreachableStatementRector/Fixture/always_terminated_try_catch_comment_in_catch.php.inc

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,15 @@ namespace Rector\Tests\DeadCode\Rector\Stmt\RemoveUnreachableStatementRector\Fix
44

55
use Exception;
66

7-
class AlwaysTerminatedTryCatch
7+
class AlwaysTerminatedTryCatchCommentInCatch
88
{
99
public function run()
1010
{
1111
try {
1212
return something();
13-
} catch (Exception $e) {
13+
} catch (\Throwable $e) {
1414
return null;
15+
// some comment
1516
}
1617

1718
echo 'never executed';
@@ -26,14 +27,15 @@ namespace Rector\Tests\DeadCode\Rector\Stmt\RemoveUnreachableStatementRector\Fix
2627

2728
use Exception;
2829

29-
class AlwaysTerminatedTryCatch
30+
class AlwaysTerminatedTryCatchCommentInCatch
3031
{
3132
public function run()
3233
{
3334
try {
3435
return something();
35-
} catch (Exception $e) {
36+
} catch (\Throwable $e) {
3637
return null;
38+
// some comment
3739
}
3840
}
3941
}

rules-tests/DeadCode/Rector/Stmt/RemoveUnreachableStatementRector/Fixture/always_terminated_try_catch_with_stmts_before_return.php.inc renamed to rules-tests/DeadCode/Rector/Stmt/RemoveUnreachableStatementRector/Fixture/always_terminated_try_infinite_while.php.inc

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,15 @@ namespace Rector\Tests\DeadCode\Rector\Stmt\RemoveUnreachableStatementRector\Fix
44

55
use Exception;
66

7-
class AlwaysTerminatedTryCatchWithStmtsBeforeReturn
7+
class AlwaysTerminatedTryInfiniteWhile
88
{
99
public function run()
1010
{
1111
try {
12-
echo 'try';
13-
return something();
14-
} catch (Exception $e) {
15-
echo 'catch';
12+
while (true) {
13+
doThings();
14+
}
15+
} catch (\Throwable $e) {
1616
return null;
1717
}
1818

@@ -28,15 +28,15 @@ namespace Rector\Tests\DeadCode\Rector\Stmt\RemoveUnreachableStatementRector\Fix
2828

2929
use Exception;
3030

31-
class AlwaysTerminatedTryCatchWithStmtsBeforeReturn
31+
class AlwaysTerminatedTryInfiniteWhile
3232
{
3333
public function run()
3434
{
3535
try {
36-
echo 'try';
37-
return something();
38-
} catch (Exception $e) {
39-
echo 'catch';
36+
while (true) {
37+
doThings();
38+
}
39+
} catch (\Throwable $e) {
4040
return null;
4141
}
4242
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
namespace Rector\Tests\DeadCode\Rector\Stmt\RemoveUnreachableStatementRector\Fixture;
4+
5+
use Exception;
6+
7+
class SkipNotThrowableTryCatch
8+
{
9+
public function run()
10+
{
11+
try {
12+
return something();
13+
} catch (Exception $e) {
14+
return null;
15+
}
16+
17+
echo 'reachable when an Error escapes the catch';
18+
}
19+
}
20+
21+
?>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
2+
3+
namespace Rector\Tests\DeadCode\Rector\Stmt\RemoveUnreachableStatementRector\Fixture;
4+
5+
use Exception;
6+
7+
class SkipNotThrowableTryCatchWithStmtsBeforeReturn
8+
{
9+
public function run()
10+
{
11+
try {
12+
echo 'try';
13+
return something();
14+
} catch (Exception $e) {
15+
echo 'catch';
16+
return null;
17+
}
18+
19+
echo 'reachable when an Error escapes the catch';
20+
}
21+
}
22+
23+
?>

‎src/NodeAnalyzer/TerminatedNodeAnalyzer.php‎

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -241,9 +241,28 @@ private function isTerminatedInLastStmtsTryCatch(TryCatch $tryCatch): bool
241241
}
242242
}
243243

244+
// catching less than \Throwable lets an Error escape the try,
245+
// so the try/catch is not guaranteed to end control flow here
246+
if ($tryCatch->catches !== [] && ! $this->hasThrowableCatch($tryCatch)) {
247+
return false;
248+
}
249+
244250
return $this->isTerminatedInLastStmts($tryCatch->stmts);
245251
}
246252

253+
private function hasThrowableCatch(TryCatch $tryCatch): bool
254+
{
255+
foreach ($tryCatch->catches as $catch) {
256+
foreach ($catch->types as $type) {
257+
if ($type->toString() === 'Throwable') {
258+
return true;
259+
}
260+
}
261+
}
262+
263+
return false;
264+
}
265+
247266
private function isTerminatedInLastStmtsIf(If_ $if): bool
248267
{
249268
// Without ElseIf_[] and Else_, after If_ is possibly executable
@@ -273,17 +292,26 @@ private function isTerminatedInLastStmtsIf(If_ $if): bool
273292
*/
274293
private function isTerminatedInLastStmts(array $stmts): bool
275294
{
295+
// trailing comments are parsed as Nop stmts, skip them to reach the real last stmt
296+
while ($stmts !== [] && end($stmts) instanceof Nop) {
297+
array_pop($stmts);
298+
}
299+
276300
if ($stmts === []) {
277301
return false;
278302
}
279303

280-
$lastKey = array_key_last($stmts);
281-
$lastNode = $stmts[$lastKey];
304+
$stmt = end($stmts);
305+
306+
if ($stmt instanceof Expression) {
307+
return $stmt->expr instanceof Exit_ || $stmt->expr instanceof Throw_;
308+
}
282309

283-
if ($lastNode instanceof Expression) {
284-
return $lastNode->expr instanceof Exit_ || $lastNode->expr instanceof Throw_;
310+
// an infinite loop with no break never falls through to the next stmt
311+
if ($stmt instanceof While_ || $stmt instanceof Do_ || $stmt instanceof For_) {
312+
return $this->isTerminatedInfiniteLoop($stmt);
285313
}
286314

287-
return $lastNode instanceof Return_;
315+
return $stmt instanceof Return_;
288316
}
289317
}

0 commit comments

Comments
 (0)