diff --git a/rules-tests/DeadCode/Rector/Stmt/RemoveUnreachableStatementRector/Fixture/always_terminated_infinite_while.php.inc b/rules-tests/DeadCode/Rector/Stmt/RemoveUnreachableStatementRector/Fixture/always_terminated_infinite_while.php.inc new file mode 100644 index 00000000000..37c433ce84e --- /dev/null +++ b/rules-tests/DeadCode/Rector/Stmt/RemoveUnreachableStatementRector/Fixture/always_terminated_infinite_while.php.inc @@ -0,0 +1,41 @@ + +----- + diff --git a/rules-tests/DeadCode/Rector/Stmt/RemoveUnreachableStatementRector/Fixture/skip_while_true_with_break.php.inc b/rules-tests/DeadCode/Rector/Stmt/RemoveUnreachableStatementRector/Fixture/skip_while_true_with_break.php.inc new file mode 100644 index 00000000000..cbbf9c7bdff --- /dev/null +++ b/rules-tests/DeadCode/Rector/Stmt/RemoveUnreachableStatementRector/Fixture/skip_while_true_with_break.php.inc @@ -0,0 +1,19 @@ +isTerminatedInfiniteLoop($node); + } + if (! in_array($node::class, self::TERMINABLE_NODES_BY_ITS_STMTS, true)) { return $this->isTerminatedNode($node, $currentStmt); } @@ -81,6 +91,68 @@ public function isAlwaysTerminated(Node $stmtsAware, Stmt $node, Stmt $currentSt return $this->isTerminatedInLastStmtsSwitch($node); } + private function isTerminatedInfiniteLoop(While_|Do_|For_ $loop): bool + { + if (! $this->isInfiniteLoopCondition($loop)) { + return false; + } + + // a break/goto escaping the loop makes the following stmt reachable again; + // stay conservative and treat any break/goto in the body as escaping + return ! $this->hasBreakOrGoto($loop->stmts); + } + + private function isInfiniteLoopCondition(While_|Do_|For_ $loop): bool + { + if ($loop instanceof For_) { + // "for (;;)" has no condition and loops forever + if ($loop->cond === []) { + return true; + } + + $lastCond = end($loop->cond); + return $lastCond instanceof Expr && $this->isAlwaysTrue($lastCond); + } + + return $this->isAlwaysTrue($loop->cond); + } + + private function isAlwaysTrue(Expr $expr): bool + { + if ($expr instanceof ConstFetch) { + return $expr->name->toLowerString() === 'true'; + } + + return $expr instanceof Int_ && $expr->value !== 0; + } + + /** + * @param Stmt[] $stmts + */ + private function hasBreakOrGoto(array $stmts): bool + { + $hasBreakOrGoto = false; + + $this->simpleCallableNodeTraverser->traverseNodesWithCallable( + $stmts, + static function (Node $node) use (&$hasBreakOrGoto): ?int { + // nested scopes bring their own jump targets + if ($node instanceof FunctionLike || $node instanceof ClassLike) { + return NodeVisitor::DONT_TRAVERSE_CURRENT_AND_CHILDREN; + } + + if (! $node instanceof Break_ && ! $node instanceof Goto_) { + return null; + } + + $hasBreakOrGoto = true; + return NodeVisitor::STOP_TRAVERSAL; + } + ); + + return $hasBreakOrGoto; + } + private function isTerminatedNode(Stmt $previousNode, Stmt $currentStmt): bool { if (in_array($previousNode::class, self::TERMINABLE_NODES, true)) {