mirror of
https://github.com/danog/psalm.git
synced 2025-01-21 21:31:13 +01:00
Split out SwitchChecker
This commit is contained in:
parent
dda4c52e03
commit
2bea64ca52
196
src/Psalm/Checker/Statements/Block/SwitchChecker.php
Normal file
196
src/Psalm/Checker/Statements/Block/SwitchChecker.php
Normal file
@ -0,0 +1,196 @@
|
||||
<?php
|
||||
namespace Psalm\Checker\Statements\Block;
|
||||
|
||||
use PhpParser;
|
||||
use Psalm\Context;
|
||||
use Psalm\IssueBuffer;
|
||||
use Psalm\Checker\ScopeChecker;
|
||||
use Psalm\Checker\StatementsChecker;
|
||||
use Psalm\Checker\Statements\ExpressionChecker;
|
||||
use Psalm\Issue\InvalidContinue;
|
||||
use Psalm\Type;
|
||||
|
||||
class SwitchChecker
|
||||
{
|
||||
/**
|
||||
* @param PhpParser\Node\Stmt\Switch_ $stmt
|
||||
* @param Context $context
|
||||
* @param Context|null $loop_context
|
||||
* @return false|null
|
||||
*/
|
||||
public static function check(StatementsChecker $statements_checker, PhpParser\Node\Stmt\Switch_ $stmt, Context $context, Context $loop_context = null)
|
||||
{
|
||||
$type_candidate_var = null;
|
||||
|
||||
if (ExpressionChecker::check($statements_checker, $stmt->cond, $context) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isset($stmt->cond->inferredType) && array_values($stmt->cond->inferredType->types)[0] instanceof Type\T) {
|
||||
$type_candidate_var = array_values($stmt->cond->inferredType->types)[0]->typeof;
|
||||
}
|
||||
|
||||
$original_context = clone $context;
|
||||
|
||||
/** @var array<string,Type\Union>|null */
|
||||
$new_vars_in_scope = null;
|
||||
|
||||
/** @var array<string,bool> */
|
||||
$new_vars_possibly_in_scope = [];
|
||||
|
||||
/** @var array<string,Type\Union>|null */
|
||||
$redefined_vars = null;
|
||||
|
||||
// the last statement always breaks, by default
|
||||
$last_case_exit_type = 'break';
|
||||
|
||||
$case_exit_types = new \SplFixedArray(count($stmt->cases));
|
||||
|
||||
$has_default = false;
|
||||
|
||||
// create a map of case statement -> ultimate exit type
|
||||
for ($i = count($stmt->cases) - 1; $i >= 0; $i--) {
|
||||
$case = $stmt->cases[$i];
|
||||
|
||||
if (ScopeChecker::doesAlwaysReturnOrThrow($case->stmts)) {
|
||||
$last_case_exit_type = 'return_throw';
|
||||
}
|
||||
elseif (ScopeChecker::doesAlwaysBreakOrContinue($case->stmts, true)) {
|
||||
$last_case_exit_type = 'continue';
|
||||
}
|
||||
elseif (ScopeChecker::doesAlwaysBreakOrContinue($case->stmts)) {
|
||||
$last_case_exit_type = 'break';
|
||||
}
|
||||
|
||||
$case_exit_types[$i] = $last_case_exit_type;
|
||||
}
|
||||
|
||||
$leftover_statements = [];
|
||||
|
||||
for ($i = count($stmt->cases) - 1; $i >= 0; $i--) {
|
||||
$case = $stmt->cases[$i];
|
||||
$case_exit_type = $case_exit_types[$i];
|
||||
$case_type = null;
|
||||
|
||||
if ($case->cond) {
|
||||
if (ExpressionChecker::check($statements_checker, $case->cond, $context) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($type_candidate_var && $case->cond instanceof PhpParser\Node\Scalar\String_) {
|
||||
$case_type = $case->cond->value;
|
||||
}
|
||||
}
|
||||
|
||||
$switch_vars = $type_candidate_var && $case_type
|
||||
? [$type_candidate_var => Type::parseString($case_type)]
|
||||
: [];
|
||||
|
||||
$case_context = clone $original_context;
|
||||
|
||||
$case_context->vars_in_scope = array_merge($case_context->vars_in_scope, $switch_vars);
|
||||
$case_context->vars_possibly_in_scope = array_merge($case_context->vars_possibly_in_scope, $switch_vars);
|
||||
|
||||
$old_case_context = clone $case_context;
|
||||
|
||||
$case_stmts = $case->stmts;
|
||||
|
||||
// has a return/throw at end
|
||||
$has_ending_statements = ScopeChecker::doesAlwaysReturnOrThrow($case_stmts);
|
||||
$has_leaving_statements = ScopeChecker::doesAlwaysBreakOrContinue($case_stmts);
|
||||
|
||||
if (!$case_stmts || (!$has_ending_statements && !$has_leaving_statements)) {
|
||||
$case_stmts = array_merge($case_stmts, $leftover_statements);
|
||||
$has_ending_statements = ScopeChecker::doesAlwaysReturnOrThrow($case_stmts);
|
||||
}
|
||||
else {
|
||||
$leftover_statements = [];
|
||||
}
|
||||
|
||||
$statements_checker->check($case_stmts, $case_context, $loop_context);
|
||||
|
||||
// has a return/throw at end
|
||||
$has_ending_statements = ScopeChecker::doesAlwaysReturnOrThrow($case_stmts);
|
||||
|
||||
if ($case_exit_type !== 'return_throw') {
|
||||
$vars = array_diff_key($case_context->vars_possibly_in_scope, $original_context->vars_possibly_in_scope);
|
||||
|
||||
// if we're leaving this block, add vars to outer for loop scope
|
||||
if ($case_exit_type === 'continue') {
|
||||
if ($loop_context) {
|
||||
$loop_context->vars_possibly_in_scope = array_merge($vars, $loop_context->vars_possibly_in_scope);
|
||||
}
|
||||
else {
|
||||
if (IssueBuffer::accepts(
|
||||
new InvalidContinue(
|
||||
'Continue called when not in loop',
|
||||
$statements_checker->getCheckedFileName(),
|
||||
$case->getLine()
|
||||
)
|
||||
)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
$case_redefined_vars = Context::getRedefinedVars($original_context, $case_context);
|
||||
|
||||
Type::redefineGenericUnionTypes($case_redefined_vars, $context);
|
||||
|
||||
if ($redefined_vars === null) {
|
||||
$redefined_vars = $case_redefined_vars;
|
||||
}
|
||||
else {
|
||||
foreach ($redefined_vars as $redefined_var => $type) {
|
||||
if (!isset($case_redefined_vars[$redefined_var])) {
|
||||
unset($redefined_vars[$redefined_var]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($new_vars_in_scope === null) {
|
||||
$new_vars_in_scope = array_diff_key($case_context->vars_in_scope, $context->vars_in_scope);
|
||||
$new_vars_possibly_in_scope = array_diff_key($case_context->vars_possibly_in_scope, $context->vars_possibly_in_scope);
|
||||
}
|
||||
else {
|
||||
foreach ($new_vars_in_scope as $new_var => $type) {
|
||||
if (!isset($case_context->vars_in_scope[$new_var])) {
|
||||
unset($new_vars_in_scope[$new_var]);
|
||||
}
|
||||
}
|
||||
|
||||
$new_vars_possibly_in_scope = array_merge(
|
||||
array_diff_key(
|
||||
$case_context->vars_possibly_in_scope,
|
||||
$context->vars_possibly_in_scope
|
||||
),
|
||||
$new_vars_possibly_in_scope
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($case->stmts) {
|
||||
$leftover_statements = array_merge($leftover_statements, $case->stmts);
|
||||
}
|
||||
|
||||
if (!$case->cond) {
|
||||
$has_default = true;
|
||||
}
|
||||
}
|
||||
|
||||
// only update vars if there is a default
|
||||
// if that default has a throw/return/continue, that should be handled above
|
||||
if ($has_default) {
|
||||
if ($new_vars_in_scope) {
|
||||
$context->vars_in_scope = array_merge($context->vars_in_scope, $new_vars_in_scope);
|
||||
}
|
||||
|
||||
if ($redefined_vars) {
|
||||
$context->vars_in_scope = array_merge($context->vars_in_scope, $redefined_vars);
|
||||
}
|
||||
}
|
||||
|
||||
$context->vars_possibly_in_scope = array_merge($context->vars_possibly_in_scope, $new_vars_possibly_in_scope);
|
||||
}
|
||||
}
|
@ -6,6 +6,7 @@ use PhpParser;
|
||||
|
||||
use Psalm\Checker\Statements\Block\ForeachChecker;
|
||||
use Psalm\Checker\Statements\Block\IfChecker;
|
||||
use Psalm\Checker\Statements\Block\SwitchChecker;
|
||||
use Psalm\Checker\Statements\ExpressionChecker;
|
||||
use Psalm\IssueBuffer;
|
||||
use Psalm\Issue\ContinueOutsideLoop;
|
||||
@ -181,7 +182,7 @@ class StatementsChecker
|
||||
$this->checkThrow($stmt, $context);
|
||||
|
||||
} elseif ($stmt instanceof PhpParser\Node\Stmt\Switch_) {
|
||||
$this->checkSwitch($stmt, $context, $loop_context);
|
||||
SwitchChecker::check($this, $stmt, $context, $loop_context);
|
||||
|
||||
} elseif ($stmt instanceof PhpParser\Node\Stmt\Break_) {
|
||||
// do nothing
|
||||
@ -665,189 +666,6 @@ class StatementsChecker
|
||||
return ExpressionChecker::check($this, $stmt->expr, $context);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param PhpParser\Node\Stmt\Switch_ $stmt
|
||||
* @param Context $context
|
||||
* @param Context|null $loop_context
|
||||
* @return false|null
|
||||
*/
|
||||
protected function checkSwitch(PhpParser\Node\Stmt\Switch_ $stmt, Context $context, Context $loop_context = null)
|
||||
{
|
||||
$type_candidate_var = null;
|
||||
|
||||
if (ExpressionChecker::check($this, $stmt->cond, $context) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isset($stmt->cond->inferredType) && array_values($stmt->cond->inferredType->types)[0] instanceof Type\T) {
|
||||
$type_candidate_var = array_values($stmt->cond->inferredType->types)[0]->typeof;
|
||||
}
|
||||
|
||||
$original_context = clone $context;
|
||||
|
||||
/** @var array<string,Type\Union>|null */
|
||||
$new_vars_in_scope = null;
|
||||
|
||||
/** @var array<string,bool> */
|
||||
$new_vars_possibly_in_scope = [];
|
||||
|
||||
/** @var array<string,Type\Union>|null */
|
||||
$redefined_vars = null;
|
||||
|
||||
// the last statement always breaks, by default
|
||||
$last_case_exit_type = 'break';
|
||||
|
||||
$case_exit_types = new \SplFixedArray(count($stmt->cases));
|
||||
|
||||
$has_default = false;
|
||||
|
||||
// create a map of case statement -> ultimate exit type
|
||||
for ($i = count($stmt->cases) - 1; $i >= 0; $i--) {
|
||||
$case = $stmt->cases[$i];
|
||||
|
||||
if (ScopeChecker::doesAlwaysReturnOrThrow($case->stmts)) {
|
||||
$last_case_exit_type = 'return_throw';
|
||||
}
|
||||
elseif (ScopeChecker::doesAlwaysBreakOrContinue($case->stmts, true)) {
|
||||
$last_case_exit_type = 'continue';
|
||||
}
|
||||
elseif (ScopeChecker::doesAlwaysBreakOrContinue($case->stmts)) {
|
||||
$last_case_exit_type = 'break';
|
||||
}
|
||||
|
||||
$case_exit_types[$i] = $last_case_exit_type;
|
||||
}
|
||||
|
||||
$leftover_statements = [];
|
||||
|
||||
for ($i = count($stmt->cases) - 1; $i >= 0; $i--) {
|
||||
$case = $stmt->cases[$i];
|
||||
$case_exit_type = $case_exit_types[$i];
|
||||
$case_type = null;
|
||||
|
||||
if ($case->cond) {
|
||||
if (ExpressionChecker::check($this, $case->cond, $context) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($type_candidate_var && $case->cond instanceof PhpParser\Node\Scalar\String_) {
|
||||
$case_type = $case->cond->value;
|
||||
}
|
||||
}
|
||||
|
||||
$switch_vars = $type_candidate_var && $case_type
|
||||
? [$type_candidate_var => Type::parseString($case_type)]
|
||||
: [];
|
||||
|
||||
$case_context = clone $original_context;
|
||||
|
||||
$case_context->vars_in_scope = array_merge($case_context->vars_in_scope, $switch_vars);
|
||||
$case_context->vars_possibly_in_scope = array_merge($case_context->vars_possibly_in_scope, $switch_vars);
|
||||
|
||||
$old_case_context = clone $case_context;
|
||||
|
||||
$case_stmts = $case->stmts;
|
||||
|
||||
// has a return/throw at end
|
||||
$has_ending_statements = ScopeChecker::doesAlwaysReturnOrThrow($case_stmts);
|
||||
$has_leaving_statements = ScopeChecker::doesAlwaysBreakOrContinue($case_stmts);
|
||||
|
||||
if (!$case_stmts || (!$has_ending_statements && !$has_leaving_statements)) {
|
||||
$case_stmts = array_merge($case_stmts, $leftover_statements);
|
||||
$has_ending_statements = ScopeChecker::doesAlwaysReturnOrThrow($case_stmts);
|
||||
}
|
||||
else {
|
||||
$leftover_statements = [];
|
||||
}
|
||||
|
||||
$this->check($case_stmts, $case_context, $loop_context);
|
||||
|
||||
// has a return/throw at end
|
||||
$has_ending_statements = ScopeChecker::doesAlwaysReturnOrThrow($case_stmts);
|
||||
|
||||
if ($case_exit_type !== 'return_throw') {
|
||||
$vars = array_diff_key($case_context->vars_possibly_in_scope, $original_context->vars_possibly_in_scope);
|
||||
|
||||
// if we're leaving this block, add vars to outer for loop scope
|
||||
if ($case_exit_type === 'continue') {
|
||||
if ($loop_context) {
|
||||
$loop_context->vars_possibly_in_scope = array_merge($vars, $loop_context->vars_possibly_in_scope);
|
||||
}
|
||||
else {
|
||||
if (IssueBuffer::accepts(
|
||||
new InvalidContinue(
|
||||
'Continue called when not in loop',
|
||||
$this->checked_file_name,
|
||||
$case->getLine()
|
||||
),
|
||||
$this->suppressed_issues
|
||||
)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
$case_redefined_vars = Context::getRedefinedVars($original_context, $case_context);
|
||||
|
||||
Type::redefineGenericUnionTypes($case_redefined_vars, $context);
|
||||
|
||||
if ($redefined_vars === null) {
|
||||
$redefined_vars = $case_redefined_vars;
|
||||
}
|
||||
else {
|
||||
foreach ($redefined_vars as $redefined_var => $type) {
|
||||
if (!isset($case_redefined_vars[$redefined_var])) {
|
||||
unset($redefined_vars[$redefined_var]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($new_vars_in_scope === null) {
|
||||
$new_vars_in_scope = array_diff_key($case_context->vars_in_scope, $context->vars_in_scope);
|
||||
$new_vars_possibly_in_scope = array_diff_key($case_context->vars_possibly_in_scope, $context->vars_possibly_in_scope);
|
||||
}
|
||||
else {
|
||||
foreach ($new_vars_in_scope as $new_var => $type) {
|
||||
if (!isset($case_context->vars_in_scope[$new_var])) {
|
||||
unset($new_vars_in_scope[$new_var]);
|
||||
}
|
||||
}
|
||||
|
||||
$new_vars_possibly_in_scope = array_merge(
|
||||
array_diff_key(
|
||||
$case_context->vars_possibly_in_scope,
|
||||
$context->vars_possibly_in_scope
|
||||
),
|
||||
$new_vars_possibly_in_scope
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($case->stmts) {
|
||||
$leftover_statements = array_merge($leftover_statements, $case->stmts);
|
||||
}
|
||||
|
||||
if (!$case->cond) {
|
||||
$has_default = true;
|
||||
}
|
||||
}
|
||||
|
||||
// only update vars if there is a default
|
||||
// if that default has a throw/return/continue, that should be handled above
|
||||
if ($has_default) {
|
||||
if ($new_vars_in_scope) {
|
||||
$context->vars_in_scope = array_merge($context->vars_in_scope, $new_vars_in_scope);
|
||||
}
|
||||
|
||||
if ($redefined_vars) {
|
||||
$context->vars_in_scope = array_merge($context->vars_in_scope, $redefined_vars);
|
||||
}
|
||||
}
|
||||
|
||||
$context->vars_possibly_in_scope = array_merge($context->vars_possibly_in_scope, $new_vars_possibly_in_scope);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $var_name
|
||||
* @param int $line_number
|
||||
|
Loading…
x
Reference in New Issue
Block a user