77use PhpParser \Node ;
88use PhpParser \Node \Expr ;
99use PhpParser \Node \Expr \BinaryOp \Identical ;
10+ use PhpParser \Node \FunctionLike ;
1011use PhpParser \Node \Stmt ;
1112use PhpParser \Node \Stmt \Break_ ;
13+ use PhpParser \Node \Stmt \Class_ ;
14+ use PhpParser \Node \Stmt \Continue_ ;
1215use PhpParser \Node \Stmt \If_ ;
1316use PhpParser \Node \Stmt \Switch_ ;
17+ use PhpParser \NodeVisitor ;
1418use Rector \Rector \AbstractRector ;
1519use Rector \Renaming \NodeManipulator \SwitchManipulator ;
1620use Symplify \RuleDocGenerator \ValueObject \CodeSample \CodeSample ;
@@ -85,6 +89,11 @@ public function refactor(Node $node): array|If_|null
8589
8690 $ onlyCase = $ node ->cases [0 ];
8791
92+ // nested break/continue would lose the switch to target and cause a fatal error
93+ if ($ this ->hasNestedBreakOrContinue ($ onlyCase ->stmts )) {
94+ return null ;
95+ }
96+
8897 // only default → basically unwrap
8998 if (! $ onlyCase ->cond instanceof Expr) {
9099 // remove default clause because it cause syntax error
@@ -96,4 +105,34 @@ public function refactor(Node $node): array|If_|null
96105
97106 return $ if ;
98107 }
108+
109+ /**
110+ * @param Stmt[] $stmts
111+ */
112+ private function hasNestedBreakOrContinue (array $ stmts ): bool
113+ {
114+ $ hasNested = false ;
115+
116+ foreach ($ stmts as $ stmt ) {
117+ // top level break is removed by SwitchManipulator
118+ if ($ stmt instanceof Break_) {
119+ continue ;
120+ }
121+
122+ $ this ->traverseNodesWithCallable ($ stmt , static function (Node $ subNode ) use (&$ hasNested ): ?int {
123+ if ($ subNode instanceof Class_ || $ subNode instanceof FunctionLike) {
124+ return NodeVisitor::DONT_TRAVERSE_CURRENT_AND_CHILDREN ;
125+ }
126+
127+ if ($ subNode instanceof Break_ || $ subNode instanceof Continue_) {
128+ $ hasNested = true ;
129+ return NodeVisitor::STOP_TRAVERSAL ;
130+ }
131+
132+ return null ;
133+ });
134+ }
135+
136+ return $ hasNested ;
137+ }
99138}
0 commit comments