mirror of
https://github.com/danog/psalm.git
synced 2024-11-26 20:34:47 +01:00
parent
467702ea33
commit
b06cfd025a
@ -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;
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
*/
|
||||
|
@ -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) {
|
||||
|
@ -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',
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user