feat(parser): add continue statement support in switch cases within loops

- Add isContinueExpr method to AstNodeType for continue statement detection
- Introduce inContinuableLoop context flag to track for/foreach/while/do-while loops
- Update CompilerBase to maintain continuable loop state during compilation
- Modify LoopControlTrait to validate continue statements only within continuable loops
- Enhance SwitchTrait to allow continue as valid case ending expression
- Add comprehensive tests for continue in switch nested in various loop types
- Create test cases for invalid continue usage outside loops
- Update version number from 1094 to 1095
pull/43/head
韩天峰 1 month ago
parent c86e847c8f
commit 9a7de8ee31
  1. 53
      phpunit/code/control-flow/loop-switch-continue.php
  2. 11
      phpunit/code/control-flow/standalone-dynamic-switch-continue.php
  3. 13
      phpunit/code/control-flow/standalone-switch-continue.php
  4. 25
      phpunit/src/LoopControlTest.php
  5. 9
      src/CompilerBase.php
  6. 5
      src/Context/FunctionContext.php
  7. 5
      src/Parser/AstNodeType.php
  8. 2
      src/Parser/LoopControlTrait.php
  9. 3
      src/Parser/SwitchTrait.php
  10. 2
      version.txt

@ -0,0 +1,53 @@
<?php
use native_types;
function switch_continue_in_for(): void
{
for ($i = 0; $i < 2; $i++) {
switch ($i) {
case 0:
continue;
default:
break;
}
}
}
function switch_continue_in_foreach(array $values): void
{
foreach ($values as $value) {
switch ($value) {
case 0:
continue;
default:
break;
}
}
}
function switch_continue_in_while(): void
{
$i = 0;
while ($i++ < 2) {
switch ($i) {
case 1:
continue;
default:
break;
}
}
}
function switch_continue_in_do_while(): void
{
$i = 0;
do {
switch ($i++) {
case 0:
continue;
default:
break;
}
} while ($i < 2);
}

@ -0,0 +1,11 @@
<?php
function standalone_dynamic_switch_continue(mixed $value): void
{
switch ($value) {
case 1:
continue;
default:
break;
}
}

@ -0,0 +1,13 @@
<?php
use native_types;
function standalone_switch_continue(int $value): void
{
switch ($value) {
case 1:
continue;
default:
break;
}
}

@ -0,0 +1,25 @@
<?php
class LoopControlTest extends \BaseTest
{
public function testContinueInStandaloneSwitchIsRejected(): void
{
$this->exec(
'Cannot continue outside loop',
'control-flow/standalone-switch-continue.php',
);
}
public function testContinueInStandaloneDynamicSwitchIsRejected(): void
{
$this->exec(
'Cannot continue outside loop',
'control-flow/standalone-dynamic-switch-continue.php',
);
}
public function testContinueInSwitchNestedInLoopsIsAllowed(): void
{
$this->compile('control-flow/loop-switch-continue.php');
}
}

@ -1516,6 +1516,7 @@ class CompilerBase implements PropertyAccessContext
$this->context->enterScope(); $this->context->enterScope();
$lines = []; $lines = [];
$inLoopTop = $this->context->inLoop; $inLoopTop = $this->context->inLoop;
$inContinuableLoopTop = $this->context->inContinuableLoop;
$last = array_key_last($stmts); $last = array_key_last($stmts);
foreach ($stmts as $i => $v) { foreach ($stmts as $i => $v) {
$class = $v->getType(); $class = $v->getType();
@ -1545,13 +1546,17 @@ class CompilerBase implements PropertyAccessContext
break; break;
case 'Stmt_For': case 'Stmt_For':
$this->context->inLoop = true; $this->context->inLoop = true;
$this->context->inContinuableLoop = true;
$result = $this->parseFor($v); $result = $this->parseFor($v);
$this->context->inLoop = $inLoopTop; $this->context->inLoop = $inLoopTop;
$this->context->inContinuableLoop = $inContinuableLoopTop;
break; break;
case 'Stmt_Foreach': case 'Stmt_Foreach':
$this->context->inLoop = true; $this->context->inLoop = true;
$this->context->inContinuableLoop = true;
$result = $this->parseForeach($v); $result = $this->parseForeach($v);
$this->context->inLoop = $inLoopTop; $this->context->inLoop = $inLoopTop;
$this->context->inContinuableLoop = $inContinuableLoopTop;
break; break;
case 'Stmt_Switch': case 'Stmt_Switch':
$this->context->inLoop = true; $this->context->inLoop = true;
@ -1560,13 +1565,17 @@ class CompilerBase implements PropertyAccessContext
break; break;
case 'Stmt_While': case 'Stmt_While':
$this->context->inLoop = true; $this->context->inLoop = true;
$this->context->inContinuableLoop = true;
$result = $this->parseWhile($v); $result = $this->parseWhile($v);
$this->context->inLoop = $inLoopTop; $this->context->inLoop = $inLoopTop;
$this->context->inContinuableLoop = $inContinuableLoopTop;
break; break;
case 'Stmt_Do': case 'Stmt_Do':
$this->context->inLoop = true; $this->context->inLoop = true;
$this->context->inContinuableLoop = true;
$result = $this->parseDo($v); $result = $this->parseDo($v);
$this->context->inLoop = $inLoopTop; $this->context->inLoop = $inLoopTop;
$this->context->inContinuableLoop = $inContinuableLoopTop;
break; break;
case 'Stmt_If': case 'Stmt_If':
$result = $this->parseIf($v); $result = $this->parseIf($v);

@ -55,7 +55,10 @@ class FunctionContext
public array $ceWrappers = []; public array $ceWrappers = [];
public int $tmpVarIndex = 0; public int $tmpVarIndex = 0;
public array $arguments = []; public array $arguments = [];
/** True while parsing a breakable loop or switch. */
public bool $inLoop = false; public bool $inLoop = false;
/** True while parsing a for/foreach/while/do-while body. */
public bool $inContinuableLoop = false;
public bool $inClosure = false; public bool $inClosure = false;
public ?array $closureReturnTypeCheck = null; public ?array $closureReturnTypeCheck = null;
public string $closureReturnTypeStr = ''; public string $closureReturnTypeStr = '';
@ -97,6 +100,7 @@ class FunctionContext
$this->scopeLayouts = []; $this->scopeLayouts = [];
$this->scopeLevel = 0; $this->scopeLevel = 0;
$this->inLoop = false; $this->inLoop = false;
$this->inContinuableLoop = false;
$this->inClosure = false; $this->inClosure = false;
$this->closureReturnTypeCheck = null; $this->closureReturnTypeCheck = null;
$this->closureReturnTypeStr = ''; $this->closureReturnTypeStr = '';
@ -127,5 +131,6 @@ class FunctionContext
$this->scopeLayouts = []; $this->scopeLayouts = [];
$this->scopeLevel = 0; $this->scopeLevel = 0;
$this->inLoop = false; $this->inLoop = false;
$this->inContinuableLoop = false;
} }
} }

@ -152,6 +152,11 @@ trait AstNodeType
return $expr instanceof Node\Stmt\Break_; return $expr instanceof Node\Stmt\Break_;
} }
protected function isContinueExpr(NodeAbstract $expr): bool
{
return $expr instanceof Node\Stmt\Continue_;
}
protected function isThrowExpr(NodeAbstract $expr): bool protected function isThrowExpr(NodeAbstract $expr): bool
{ {
if ($expr instanceof Node\Stmt\Expression) { if ($expr instanceof Node\Stmt\Expression) {

@ -183,7 +183,7 @@ trait LoopControlTrait
protected function parseContinue(Node\Stmt\Continue_ $v): string protected function parseContinue(Node\Stmt\Continue_ $v): string
{ {
if (!$this->context->inLoop) { if (!$this->context->inContinuableLoop) {
$this->fatalError($v, 'Cannot continue outside loop'); $this->fatalError($v, 'Cannot continue outside loop');
} }
$num = $v->num; $num = $v->num;

@ -90,9 +90,10 @@ trait SwitchTrait
if (!$this->isReturnExpr($lastExpr) if (!$this->isReturnExpr($lastExpr)
and !$this->isExitExpr($lastExpr) and !$this->isExitExpr($lastExpr)
and !$this->isBreakExpr($lastExpr) and !$this->isBreakExpr($lastExpr)
and !$this->isContinueExpr($lastExpr)
and !$this->isThrowExpr($lastExpr) and !$this->isThrowExpr($lastExpr)
) { ) {
$this->fatalError($case, 'switch case must end with return/break/exit/throw, ' . $lastExpr->getType() . ' given'); $this->fatalError($case, 'switch case must end with return/break/continue/exit/throw, ' . $lastExpr->getType() . ' given');
} }
$target = count($caseGroups); $target = count($caseGroups);
if ($hasDefault) { if ($hasDefault) {

@ -1 +1 @@
1094 1095

Loading…
Cancel
Save