1
0
mirror of https://github.com/danog/psalm.git synced 2024-11-26 20:34:47 +01:00

Allow continue inside case statement as alias for break

Fixes #464
This commit is contained in:
Matthew Brown 2018-01-24 00:01:08 -05:00
parent 467702ea33
commit b06cfd025a
6 changed files with 86 additions and 32 deletions

View File

@ -50,10 +50,11 @@ class ScopeChecker
/**
* @param array<PhpParser\Node> $stmts
* @param bool $continue_is_break when checking inside a switch statement, continue is an alias of break
*
* @return string[] one or more of 'LEAVE', 'CONTINUE', 'BREAK' (or empty if no single action is found)
*/
public static function getFinalControlActions(array $stmts)
public static function getFinalControlActions(array $stmts, $continue_is_break = false)
{
if (empty($stmts)) {
return [self::ACTION_NONE];
@ -72,6 +73,12 @@ class ScopeChecker
}
if ($stmt instanceof PhpParser\Node\Stmt\Continue_) {
if ($continue_is_break
&& (!$stmt->num || !$stmt->num instanceof PhpParser\Node\Scalar\LNumber || $stmt->num->value < 2)
) {
return [self::ACTION_BREAK];
}
return [self::ACTION_CONTINUE];
}
@ -80,8 +87,10 @@ class ScopeChecker
}
if ($stmt instanceof PhpParser\Node\Stmt\If_) {
$if_statement_actions = self::getFinalControlActions($stmt->stmts);
$else_statement_actions = $stmt->else ? self::getFinalControlActions($stmt->else->stmts) : [];
$if_statement_actions = self::getFinalControlActions($stmt->stmts, $continue_is_break);
$else_statement_actions = $stmt->else
? self::getFinalControlActions($stmt->else->stmts, $continue_is_break)
: [];
$all_same = count($if_statement_actions) === 1
&& $if_statement_actions == $else_statement_actions
@ -91,7 +100,7 @@ class ScopeChecker
if ($stmt->elseifs) {
foreach ($stmt->elseifs as $elseif) {
$elseif_control_actions = self::getFinalControlActions($elseif->stmts);
$elseif_control_actions = self::getFinalControlActions($elseif->stmts, $continue_is_break);
$all_same = $all_same && $elseif_control_actions == $if_statement_actions;
@ -121,7 +130,7 @@ class ScopeChecker
for ($d = count($stmt->cases) - 1; $d >= 0; --$d) {
$case = $stmt->cases[$d];
$case_actions = self::getFinalControlActions($case->stmts);
$case_actions = self::getFinalControlActions($case->stmts, true);
if (array_intersect([self::ACTION_BREAK, self::ACTION_CONTINUE], $case_actions)) {
continue 2;
@ -162,13 +171,13 @@ class ScopeChecker
}
if ($stmt instanceof PhpParser\Node\Stmt\TryCatch) {
$try_statement_actions = self::getFinalControlActions($stmt->stmts);
$try_statement_actions = self::getFinalControlActions($stmt->stmts, $continue_is_break);
if ($stmt->catches) {
$all_same = count($try_statement_actions) === 1;
foreach ($stmt->catches as $catch) {
$catch_actions = self::getFinalControlActions($catch->stmts);
$catch_actions = self::getFinalControlActions($catch->stmts, $continue_is_break);
$all_same = $all_same && $try_statement_actions == $catch_actions;

View File

@ -55,7 +55,7 @@ class SwitchChecker
for ($i = count($stmt->cases) - 1; $i >= 0; --$i) {
$case = $stmt->cases[$i];
$case_actions = $case_action_map[$i] = ScopeChecker::getFinalControlActions($case->stmts);
$case_actions = $case_action_map[$i] = ScopeChecker::getFinalControlActions($case->stmts, true);
if (!in_array(ScopeChecker::ACTION_NONE, $case_actions, true)) {
if ($case_actions === [ScopeChecker::ACTION_END]) {
@ -84,6 +84,7 @@ class SwitchChecker
$case_context->branch_point = $case_context->branch_point ?: (int) $stmt->getAttribute('startFilePos');
}
$case_context->parent_context = $context;
$case_context->inside_case = true;
if ($case->cond) {
if (ExpressionChecker::analyze($statements_checker, $case->cond, $case_context) === false) {

View File

@ -244,14 +244,16 @@ class StatementsChecker extends SourceChecker implements StatementsSource
$has_returned = true;
} elseif ($stmt instanceof PhpParser\Node\Stmt\Continue_) {
if ($loop_scope === null) {
if (IssueBuffer::accepts(
new ContinueOutsideLoop(
'Continue call outside loop context',
new CodeLocation($this->source, $stmt)
),
$this->source->getSuppressedIssues()
)) {
return false;
if (!$context->inside_case) {
if (IssueBuffer::accepts(
new ContinueOutsideLoop(
'Continue call outside loop context',
new CodeLocation($this->source, $stmt)
),
$this->source->getSuppressedIssues()
)) {
return false;
}
}
} elseif ($original_context) {
$loop_scope->final_actions[] = ScopeChecker::ACTION_CONTINUE;

View File

@ -174,6 +174,13 @@ class Context
*/
public $branch_point;
/**
* If we're inside case statements we allow continue; statements as an alias of break;
*
* @var bool
*/
public $inside_case = false;
/**
* @param string|null $self
*/

View File

@ -14,16 +14,16 @@ class LoopScopeTest extends TestCase
return [
'switchVariableWithContinue' => [
'<?php
foreach ([\'a\', \'b\', \'c\'] as $letter) {
foreach (["a", "b", "c"] as $letter) {
switch ($letter) {
case \'a\':
case "b":
$foo = 1;
break;
case \'b\':
case "c":
$foo = 2;
break;
default:
continue;
continue 2;
}
$moo = $foo;
@ -31,22 +31,22 @@ class LoopScopeTest extends TestCase
],
'switchVariableWithContinueAndIfs' => [
'<?php
foreach ([\'a\', \'b\', \'c\'] as $letter) {
foreach (["a", "b", "c"] as $letter) {
switch ($letter) {
case \'a\':
case "a":
if (rand(0, 10) === 1) {
continue;
continue 2;
}
$foo = 1;
break;
case \'b\':
case "b":
if (rand(0, 10) === 1) {
continue;
continue 2;
}
$foo = 2;
break;
default:
continue;
continue 2;
}
$moo = $foo;
@ -54,10 +54,10 @@ class LoopScopeTest extends TestCase
],
'switchVariableWithFallthrough' => [
'<?php
foreach ([\'a\', \'b\', \'c\'] as $letter) {
foreach (["a", "b", "c"] as $letter) {
switch ($letter) {
case \'a\':
case \'b\':
case "a":
case "b":
$foo = 2;
break;
@ -71,12 +71,12 @@ class LoopScopeTest extends TestCase
],
'switchVariableWithFallthroughStatement' => [
'<?php
foreach ([\'a\', \'b\', \'c\'] as $letter) {
foreach (["a", "b", "c"] as $letter) {
switch ($letter) {
case \'a\':
case "a":
$bar = 1;
case \'b\':
case "b":
$foo = 2;
break;
@ -814,6 +814,24 @@ class LoopScopeTest extends TestCase
public function providerFileCheckerInvalidCodeParse()
{
return [
'switchVariableWithContinueOnce' => [
'<?php
foreach (["a", "b", "c"] as $letter) {
switch ($letter) {
case "b":
$foo = 1;
break;
case "c":
$foo = 2;
break;
default:
continue;
}
$moo = $foo;
}',
'error_message' => 'PossiblyUndefinedGlobalVariable',
],
'possiblyUndefinedArrayInForeach' => [
'<?php
foreach ([1, 2, 3, 4] as $b) {

View File

@ -237,6 +237,14 @@ class SwitchTypeTest extends TestCase
'$y' => 'bool',
],
],
'continueIsBreak' => [
'<?php
switch(2) {
case 2:
echo "two\n";
continue;
}',
],
];
}
@ -372,6 +380,15 @@ class SwitchTypeTest extends TestCase
}',
'error_message' => 'InvalidScalarArgument',
],
'continueIsNotBreak' => [
'<?php
switch(2) {
case 2:
echo "two\n";
continue 2;
}',
'error_message' => 'ContinueOutsideLoop',
],
];
}
}