fix(parser): 支持判断 if/else 终止的 switch case

pull/34/head
Yurun 1 month ago
parent 2287695b44
commit 62ad786c13
  1. 70
      src/Parser/SwitchTrait.php
  2. 20
      tests/compiler/switch/default-before-matching-case-ifelse.phpt
  3. 36
      tests/compiler/switch/if-else-return-001.phpt
  4. 36
      tests/compiler/switch/if-else-return-002.phpt
  5. 19
      tests/compiler/switch/if-else-return-no-default.phpt
  6. 20
      tests/compiler/switch/if-elseif-else-return.phpt
  7. 20
      tests/compiler/switch/mixed-return-ifelse.phpt
  8. 22
      tests/compiler/switch/nested-if-else-return.phpt
  9. 19
      tests/compiler/switch/shared-case-if-else-return.phpt
  10. 20
      tests/compiler/switch/string-if-else-return.phpt

@ -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;
}
}

@ -0,0 +1,20 @@
--TEST--
default placed before a later matching case, both using if/else return
--FILE--
<?php
function main()
{
$a = 2;
switch ($a)
{
default:
if (true) { echo "D\n"; return; } else { echo "De\n"; return; }
case 1:
if (true) { echo "1\n"; return; } else { echo "1e\n"; return; }
case 2:
if (true) { echo "2\n"; return; } else { echo "2e\n"; return; }
}
}
?>
--EXPECT--
2

@ -0,0 +1,36 @@
--TEST--
switch case ending with if/else where both branches return (regression)
--FILE--
<?php
function main()
{
$a = 1;
switch ($a)
{
case 0:
if (true)
{
var_dump('a');
return;
}
else
{
var_dump('b');
return;
}
default:
if (true)
{
var_dump('c');
return;
}
else
{
var_dump('d');
return;
}
}
}
?>
--EXPECT--
string(1) "c"

@ -0,0 +1,36 @@
--TEST--
switch case matching with if/else return, default not taken
--FILE--
<?php
function main()
{
$a = 0;
switch ($a)
{
case 0:
if (true)
{
var_dump('a');
return;
}
else
{
var_dump('b');
return;
}
default:
if (true)
{
var_dump('c');
return;
}
else
{
var_dump('d');
return;
}
}
}
?>
--EXPECT--
string(1) "a"

@ -0,0 +1,19 @@
--TEST--
switch without default, if/else return case, non-matching value falls through
--FILE--
<?php
function main()
{
$a = 9;
switch ($a)
{
case 1:
if (true) { echo "1\n"; return; } else { echo "1e\n"; return; }
case 2:
if (true) { echo "2\n"; return; } else { echo "2e\n"; return; }
}
echo "after\n";
}
?>
--EXPECT--
after

@ -0,0 +1,20 @@
--TEST--
switch case ending with if/elseif/else where every branch returns
--FILE--
<?php
function main()
{
$a = 2;
switch ($a)
{
case 1:
if (true) { echo "1\n"; return; } else { echo "1e\n"; return; }
case 2:
if ($a === 2) { echo "two\n"; return; } elseif ($a === 3) { echo "three\n"; return; } else { echo "other\n"; return; }
default:
if (true) { echo "D\n"; return; } else { echo "De\n"; return; }
}
}
?>
--EXPECT--
two

@ -0,0 +1,20 @@
--TEST--
switch mixing plain return case with if/else return case
--FILE--
<?php
function main()
{
$a = 2;
switch ($a)
{
case 1:
echo "1\n"; return;
case 2:
if (true) { echo "2if\n"; return; } else { echo "2else\n"; return; }
default:
echo "D\n"; return;
}
}
?>
--EXPECT--
2if

@ -0,0 +1,22 @@
--TEST--
switch case ending with nested if/else where all paths return
--FILE--
<?php
function main()
{
$a = 5;
switch ($a)
{
case 5:
if (true) {
if ($a > 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

@ -0,0 +1,19 @@
--TEST--
multiple case labels sharing an if/else return body
--FILE--
<?php
function main()
{
$a = 1;
switch ($a)
{
case 1:
case 2:
if (true) { echo "one-or-two\n"; return; } else { echo "never\n"; return; }
default:
if (true) { echo "D\n"; return; } else { echo "De\n"; return; }
}
}
?>
--EXPECT--
one-or-two

@ -0,0 +1,20 @@
--TEST--
string switch with if/else return cases and default
--FILE--
<?php
function main()
{
$v = 'x';
switch ($v)
{
case 'a':
if (true) { echo "A\n"; return; } else { echo "a\n"; return; }
case 'b':
if (true) { echo "B\n"; return; } else { echo "b\n"; return; }
default:
if (true) { echo "DEFAULT\n"; return; } else { echo "default\n"; return; }
}
}
?>
--EXPECT--
DEFAULT
Loading…
Cancel
Save