diff --git a/src/Parser/SwitchTrait.php b/src/Parser/SwitchTrait.php index 33d2b6a1..102865ab 100644 --- a/src/Parser/SwitchTrait.php +++ b/src/Parser/SwitchTrait.php @@ -87,12 +87,8 @@ trait SwitchTrait $stmts = $stmts[0]->stmts; } $lastExpr = end($stmts); - if (!$this->isReturnExpr($lastExpr) - and !$this->isExitExpr($lastExpr) - and !$this->isBreakExpr($lastExpr) - and !$this->isThrowExpr($lastExpr) - ) { - $this->fatalError($case, 'switch case must end with return/break/exit/throw, ' . $lastExpr->getType() . ' given'); + if (!$this->caseBodyTerminates($stmts)) { + $this->fatalError($case, 'switch case must end with return/break/exit/throw or a fully terminating if/else, ' . $lastExpr->getType() . ' given'); } $target = count($caseGroups); if ($hasDefault) { @@ -162,4 +158,66 @@ trait SwitchTrait return $var_def . $code; } + /** + * 判断一组语句是否总是终止(所有控制流路径都以 return/break/continue/exit/throw 结束)。 + * + * 在 switch 降级为 if 链时,每个 case 必须是终止的,否则原本的穿透(fall-through) + * 语义无法在「每个 case 独立成 if 块」的实现中表达,会产生错误行为。 + */ + private function caseBodyTerminates(array $stmts): bool + { + if (empty($stmts)) { + return false; + } + + return $this->stmtAlwaysTerminates(end($stmts)); + } + + /** + * 判断单条语句是否总是终止控制流。 + */ + private function stmtAlwaysTerminates(Node $stmt): bool + { + if ($stmt instanceof Node\Stmt\Return_ + or $stmt instanceof Node\Stmt\Break_ + or $stmt instanceof Node\Stmt\Continue_ + or $stmt instanceof Node\Expr\Exit_ + or $stmt instanceof Node\Expr\Throw_ + ) { + return true; + } + + // Stmt_Expression 包裹的 exit/die/throw + if ($stmt instanceof Node\Stmt\Expression) { + $expr = $stmt->expr; + if ($expr instanceof Node\Expr\Exit_ or $expr instanceof Node\Expr\Throw_) { + return true; + } + } + + if ($stmt instanceof Node\Stmt\Block) { + return $this->caseBodyTerminates($stmt->stmts); + } + + if ($stmt instanceof Node\Stmt\If_) { + // 必须有 else 分支覆盖所有路径,且 if / 每个 elseif / else 分支都终止 + if ($stmt->else === null) { + return false; + } + if (!$this->caseBodyTerminates($stmt->stmts)) { + return false; + } + foreach ($stmt->elseifs as $elseif) { + if (!$this->caseBodyTerminates($elseif->stmts)) { + return false; + } + } + + return $this->caseBodyTerminates($stmt->else->stmts); + } + + // 循环、try/catch、嵌套 switch 等结构保守认为不终止 + return false; + } + } diff --git a/tests/compiler/switch/default-before-matching-case-ifelse.phpt b/tests/compiler/switch/default-before-matching-case-ifelse.phpt new file mode 100644 index 00000000..15a49b29 --- /dev/null +++ b/tests/compiler/switch/default-before-matching-case-ifelse.phpt @@ -0,0 +1,20 @@ +--TEST-- +default placed before a later matching case, both using if/else return +--FILE-- + +--EXPECT-- +2 diff --git a/tests/compiler/switch/if-else-return-001.phpt b/tests/compiler/switch/if-else-return-001.phpt new file mode 100644 index 00000000..9b5d69be --- /dev/null +++ b/tests/compiler/switch/if-else-return-001.phpt @@ -0,0 +1,36 @@ +--TEST-- +switch case ending with if/else where both branches return (regression) +--FILE-- + +--EXPECT-- +string(1) "c" diff --git a/tests/compiler/switch/if-else-return-002.phpt b/tests/compiler/switch/if-else-return-002.phpt new file mode 100644 index 00000000..bb046cc0 --- /dev/null +++ b/tests/compiler/switch/if-else-return-002.phpt @@ -0,0 +1,36 @@ +--TEST-- +switch case matching with if/else return, default not taken +--FILE-- + +--EXPECT-- +string(1) "a" diff --git a/tests/compiler/switch/if-else-return-no-default.phpt b/tests/compiler/switch/if-else-return-no-default.phpt new file mode 100644 index 00000000..dabb7e35 --- /dev/null +++ b/tests/compiler/switch/if-else-return-no-default.phpt @@ -0,0 +1,19 @@ +--TEST-- +switch without default, if/else return case, non-matching value falls through +--FILE-- + +--EXPECT-- +after diff --git a/tests/compiler/switch/if-elseif-else-return.phpt b/tests/compiler/switch/if-elseif-else-return.phpt new file mode 100644 index 00000000..f397b992 --- /dev/null +++ b/tests/compiler/switch/if-elseif-else-return.phpt @@ -0,0 +1,20 @@ +--TEST-- +switch case ending with if/elseif/else where every branch returns +--FILE-- + +--EXPECT-- +two diff --git a/tests/compiler/switch/mixed-return-ifelse.phpt b/tests/compiler/switch/mixed-return-ifelse.phpt new file mode 100644 index 00000000..30a891e6 --- /dev/null +++ b/tests/compiler/switch/mixed-return-ifelse.phpt @@ -0,0 +1,20 @@ +--TEST-- +switch mixing plain return case with if/else return case +--FILE-- + +--EXPECT-- +2if diff --git a/tests/compiler/switch/nested-if-else-return.phpt b/tests/compiler/switch/nested-if-else-return.phpt new file mode 100644 index 00000000..7dcc1371 --- /dev/null +++ b/tests/compiler/switch/nested-if-else-return.phpt @@ -0,0 +1,22 @@ +--TEST-- +switch case ending with nested if/else where all paths return +--FILE-- + 0) { echo "inner\n"; return; } else { echo "inner-else\n"; return; } + } else { + echo "outer-else\n"; return; + } + default: + if (true) { echo "D\n"; return; } else { echo "De\n"; return; } + } +} +?> +--EXPECT-- +inner diff --git a/tests/compiler/switch/shared-case-if-else-return.phpt b/tests/compiler/switch/shared-case-if-else-return.phpt new file mode 100644 index 00000000..94b12ccc --- /dev/null +++ b/tests/compiler/switch/shared-case-if-else-return.phpt @@ -0,0 +1,19 @@ +--TEST-- +multiple case labels sharing an if/else return body +--FILE-- + +--EXPECT-- +one-or-two diff --git a/tests/compiler/switch/string-if-else-return.phpt b/tests/compiler/switch/string-if-else-return.phpt new file mode 100644 index 00000000..0e7b066f --- /dev/null +++ b/tests/compiler/switch/string-if-else-return.phpt @@ -0,0 +1,20 @@ +--TEST-- +string switch with if/else return cases and default +--FILE-- + +--EXPECT-- +DEFAULT