diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 8d205b058f7..7fb8b4d120c 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -1,5 +1,5 @@ - + @@ -75,7 +75,6 @@ - function_id]]> @@ -303,18 +302,10 @@ - - - branch_point]]> - - cond]]> - - branch_point]]> - @@ -325,7 +316,6 @@ var_id]]> var_id]]> - branch_point]]> template_types]]> getTemplateTypeMap()]]> line_number]]> @@ -356,7 +346,6 @@ branch_point]]> branch_point]]> - branch_point]]> assigned_var_ids]]> new_vars]]> redefined_vars]]> @@ -384,7 +373,6 @@ traverse([$switch_condition])[0]]]> - branch_point]]> @@ -392,16 +380,6 @@ - - - branch_point]]> - - - - - branch_point]]> - - @@ -865,8 +843,6 @@ self]]> mixin_declaring_fqcln]]> - parent_class]]> - parent_class]]> calling_method_id]]> calling_method_id]]> self]]> @@ -1104,7 +1080,6 @@ - @@ -1115,9 +1090,6 @@ error_baseline]]> - - - threads]]> @@ -1128,7 +1100,6 @@ - @@ -1140,7 +1111,6 @@ - @@ -1203,11 +1173,6 @@ - - - value]]> - - @@ -1441,7 +1406,6 @@ - children[0]]]> children[1]]]> @@ -1498,9 +1462,6 @@ - - - 0]]> @@ -1592,18 +1553,11 @@ - - getArgument('pluginName')]]> - getOption('config')]]> - - - getOption('config')]]> - @@ -1929,9 +1883,6 @@ strings]]> strings]]> strings]]> - value_types['string'] instanceof TNonFalsyString - ? $type->value - : $type->value !== '']]> @@ -2278,7 +2229,6 @@ - diff --git a/src/Psalm/Context.php b/src/Psalm/Context.php index bf73229560a..1385eba16b2 100644 --- a/src/Psalm/Context.php +++ b/src/Psalm/Context.php @@ -314,7 +314,7 @@ final class Context public $is_global = false; /** - * @var array + * @var array */ public $protected_var_ids = []; diff --git a/src/Psalm/Internal/Analyzer/Statements/Block/DoAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Block/DoAnalyzer.php index bb38643bd34..c3fb9c82b64 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Block/DoAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Block/DoAnalyzer.php @@ -39,8 +39,8 @@ public static function analyze( $codebase = $statements_analyzer->getCodebase(); - if ($codebase->alter_code) { - $do_context->branch_point = $do_context->branch_point ?: (int) $stmt->getAttribute('startFilePos'); + if ($codebase->alter_code && $do_context->branch_point === null) { + $do_context->branch_point = (int) $stmt->getAttribute('startFilePos'); } $loop_scope = new LoopScope($do_context, $context); diff --git a/src/Psalm/Internal/Analyzer/Statements/Block/ForAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Block/ForAnalyzer.php index 625d5409f3c..dcf44f244b5 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Block/ForAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Block/ForAnalyzer.php @@ -4,15 +4,10 @@ use PhpParser; use Psalm\Context; -use Psalm\Internal\Analyzer\ScopeAnalyzer; use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer; use Psalm\Internal\Analyzer\StatementsAnalyzer; -use Psalm\Internal\Scope\LoopScope; -use UnexpectedValueException; use function array_merge; -use function count; -use function in_array; use function is_string; /** @@ -56,113 +51,15 @@ public static function analyze( $while_true = !$stmt->cond && !$stmt->init && !$stmt->loop; - $pre_context = null; - - if ($while_true) { - $pre_context = clone $context; - } - - $for_context = clone $context; - - $for_context->inside_loop = true; - $for_context->break_types[] = 'loop'; - - $codebase = $statements_analyzer->getCodebase(); - - if ($codebase->alter_code) { - $for_context->branch_point = $for_context->branch_point ?: (int) $stmt->getAttribute('startFilePos'); - } - - $loop_scope = new LoopScope($for_context, $context); - - $loop_scope->protected_var_ids = array_merge( - $assigned_var_ids, - $context->protected_var_ids, - ); - - if (LoopAnalyzer::analyze( + return LoopAnalyzer::analyzeForOrWhile( $statements_analyzer, - $stmt->stmts, + $stmt, + $context, + $while_true, + $init_var_types, + $assigned_var_ids, $stmt->cond, $stmt->loop, - $loop_scope, - $inner_loop_context, - ) === false) { - return false; - } - - if (!$inner_loop_context) { - throw new UnexpectedValueException('There should be an inner loop context'); - } - - $always_enters_loop = false; - - foreach ($stmt->cond as $cond) { - if ($cond_type = $statements_analyzer->node_data->getType($cond)) { - $always_enters_loop = $cond_type->isAlwaysTruthy(); - } - - if (count($stmt->init) === 1 - && count($stmt->cond) === 1 - && $cond instanceof PhpParser\Node\Expr\BinaryOp - && ($cond_value = $statements_analyzer->node_data->getType($cond->right)) - && ($cond_value->isSingleIntLiteral() || $cond_value->isSingleStringLiteral()) - && $cond->left instanceof PhpParser\Node\Expr\Variable - && is_string($cond->left->name) - && isset($init_var_types[$cond->left->name]) - && $init_var_types[$cond->left->name]->isSingleIntLiteral() - ) { - $init_value = $init_var_types[$cond->left->name]->getSingleLiteral()->value; - $cond_value = $cond_value->getSingleLiteral()->value; - - if ($cond instanceof PhpParser\Node\Expr\BinaryOp\Smaller && $init_value < $cond_value) { - $always_enters_loop = true; - break; - } - - if ($cond instanceof PhpParser\Node\Expr\BinaryOp\SmallerOrEqual && $init_value <= $cond_value) { - $always_enters_loop = true; - break; - } - - if ($cond instanceof PhpParser\Node\Expr\BinaryOp\Greater && $init_value > $cond_value) { - $always_enters_loop = true; - break; - } - - if ($cond instanceof PhpParser\Node\Expr\BinaryOp\GreaterOrEqual && $init_value >= $cond_value) { - $always_enters_loop = true; - break; - } - } - } - - if ($while_true) { - $always_enters_loop = true; - } - - $can_leave_loop = !$while_true - || in_array(ScopeAnalyzer::ACTION_BREAK, $loop_scope->final_actions, true); - - if ($always_enters_loop && $can_leave_loop) { - LoopAnalyzer::setLoopVars($inner_loop_context, $context, $loop_scope); - } - - $for_context->loop_scope = null; - - if ($can_leave_loop) { - $context->vars_possibly_in_scope = array_merge( - $context->vars_possibly_in_scope, - $for_context->vars_possibly_in_scope, - ); - } elseif ($pre_context) { - $context->vars_possibly_in_scope = $pre_context->vars_possibly_in_scope; - } - - if ($context->collect_exceptions) { - $context->mergeExceptions($for_context); - } - - return null; + ); } } diff --git a/src/Psalm/Internal/Analyzer/Statements/Block/ForeachAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Block/ForeachAnalyzer.php index 19099c9c247..7bd6f6cbd87 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Block/ForeachAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Block/ForeachAnalyzer.php @@ -283,9 +283,8 @@ public static function analyze( $foreach_context->inside_loop = true; $foreach_context->break_types[] = 'loop'; - if ($codebase->alter_code) { - $foreach_context->branch_point = - $foreach_context->branch_point ?: (int) $stmt->getAttribute('startFilePos'); + if ($codebase->alter_code && $foreach_context->branch_point === null) { + $foreach_context->branch_point = (int) $stmt->getAttribute('startFilePos'); } if ($stmt->keyVar instanceof PhpParser\Node\Expr\Variable && is_string($stmt->keyVar->name)) { diff --git a/src/Psalm/Internal/Analyzer/Statements/Block/IfElseAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Block/IfElseAnalyzer.php index b4648cd59f2..5558b83481e 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Block/IfElseAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Block/IfElseAnalyzer.php @@ -292,9 +292,8 @@ public static function analyze( } if ($stmt->else) { - if ($codebase->alter_code) { - $else_context->branch_point = - $else_context->branch_point ?: (int) $stmt->getAttribute('startFilePos'); + if ($codebase->alter_code && $else_context->branch_point === null) { + $else_context->branch_point = (int) $stmt->getAttribute('startFilePos'); } } diff --git a/src/Psalm/Internal/Analyzer/Statements/Block/LoopAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Block/LoopAnalyzer.php index 420e8e08665..a67765bfed5 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Block/LoopAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Block/LoopAnalyzer.php @@ -18,11 +18,15 @@ use Psalm\IssueBuffer; use Psalm\Type; use Psalm\Type\Reconciler; +use Psalm\Type\Union; +use UnexpectedValueException; use function array_keys; use function array_merge; use function array_unique; +use function count; use function in_array; +use function is_string; use function spl_object_id; /** @@ -137,10 +141,7 @@ public static function analyze( } } - $loop_parent_context->vars_possibly_in_scope = array_merge( - $continue_context->vars_possibly_in_scope, - $loop_parent_context->vars_possibly_in_scope, - ); + $loop_parent_context->vars_possibly_in_scope += $continue_context->vars_possibly_in_scope; } else { $original_parent_context = clone $loop_parent_context; @@ -268,10 +269,7 @@ public static function analyze( $continue_context->has_returned = false; - $loop_parent_context->vars_possibly_in_scope = array_merge( - $continue_context->vars_possibly_in_scope, - $loop_parent_context->vars_possibly_in_scope, - ); + $loop_parent_context->vars_possibly_in_scope += $continue_context->vars_possibly_in_scope; // if there are no changes to the types, no need to re-examine if (!$has_changes) { @@ -506,6 +504,87 @@ public static function analyze( return null; } + /** + * @param PhpParser\Node\Stmt\For_|PhpParser\Node\Stmt\While_ $stmt + * @param array $init_var_types + * @param array $assigned_var_ids + * @param list $pre_conditions + * @param PhpParser\Node\Expr[] $post_expressions + * @return false|null + */ + public static function analyzeForOrWhile( + StatementsAnalyzer $statements_analyzer, + $stmt, + Context $context, + bool $while_true, + array $init_var_types, + array $assigned_var_ids, + array $pre_conditions, + array $post_expressions + ): ?bool { + $pre_context = null; + + if ($while_true) { + $pre_context = clone $context; + } + + $for_context = clone $context; + + $for_context->inside_loop = true; + $for_context->break_types[] = 'loop'; + + $codebase = $statements_analyzer->getCodebase(); + + if ($codebase->alter_code && $for_context->branch_point === null) { + $for_context->branch_point = (int) $stmt->getAttribute('startFilePos'); + } + + $loop_scope = new LoopScope($for_context, $context); + + $loop_scope->protected_var_ids = array_merge( + $assigned_var_ids, + $context->protected_var_ids, + ); + + if (self::analyze( + $statements_analyzer, + $stmt->stmts, + $pre_conditions, + $post_expressions, + $loop_scope, + $inner_loop_context, + ) === false) { + return false; + } + + if (!$inner_loop_context) { + throw new UnexpectedValueException('There should be an inner loop context'); + } + + $always_enters_loop = $while_true || self::doesEnterLoop($statements_analyzer, $stmt, $init_var_types); + + $can_leave_loop = !$while_true + || in_array(ScopeAnalyzer::ACTION_BREAK, $loop_scope->final_actions, true); + + if ($always_enters_loop && $can_leave_loop) { + self::setLoopVars($inner_loop_context, $context, $loop_scope); + } + + $for_context->loop_scope = null; + + if ($can_leave_loop) { + $context->vars_possibly_in_scope += $for_context->vars_possibly_in_scope; + } elseif ($pre_context) { + $context->vars_possibly_in_scope = $pre_context->vars_possibly_in_scope; + } + + if ($context->collect_exceptions) { + $context->mergeExceptions($for_context); + } + + return null; + } + public static function setLoopVars(Context $inner_context, Context $context, LoopScope $loop_scope): void { foreach ($inner_context->vars_in_scope as $var_id => $type) { @@ -527,6 +606,66 @@ public static function setLoopVars(Context $inner_context, Context $context, Loo } } + /** + * @param PhpParser\Node\Stmt\For_|PhpParser\Node\Stmt\While_ $stmt + * @param array $init_var_types + */ + private static function doesEnterLoop( + StatementsAnalyzer $statements_analyzer, + $stmt, + array $init_var_types + ): bool { + $always_enters_loop = false; + + if ($stmt instanceof PhpParser\Node\Stmt\While_) { + if ($cond_type = $statements_analyzer->node_data->getType($stmt->cond)) { + $always_enters_loop = $cond_type->isAlwaysTruthy(); + } + } else { + foreach ($stmt->cond as $cond) { + if ($cond_type = $statements_analyzer->node_data->getType($cond)) { + $always_enters_loop = $cond_type->isAlwaysTruthy(); + } + + if (count($stmt->init) === 1 + && count($stmt->cond) === 1 + && $cond instanceof PhpParser\Node\Expr\BinaryOp + && ($cond_value = $statements_analyzer->node_data->getType($cond->right)) + && ($cond_value->isSingleIntLiteral() || $cond_value->isSingleStringLiteral()) + && $cond->left instanceof PhpParser\Node\Expr\Variable + && is_string($cond->left->name) + && isset($init_var_types[$cond->left->name]) + && $init_var_types[$cond->left->name]->isSingleIntLiteral() + ) { + $init_value = $init_var_types[$cond->left->name]->getSingleLiteral()->value; + $cond_value = $cond_value->getSingleLiteral()->value; + + if ($cond instanceof PhpParser\Node\Expr\BinaryOp\Smaller && $init_value < $cond_value) { + $always_enters_loop = true; + break; + } + + if ($cond instanceof PhpParser\Node\Expr\BinaryOp\SmallerOrEqual && $init_value <= $cond_value) { + $always_enters_loop = true; + break; + } + + if ($cond instanceof PhpParser\Node\Expr\BinaryOp\Greater && $init_value > $cond_value) { + $always_enters_loop = true; + break; + } + + if ($cond instanceof PhpParser\Node\Expr\BinaryOp\GreaterOrEqual && $init_value >= $cond_value) { + $always_enters_loop = true; + break; + } + } + } + } + + return $always_enters_loop; + } + private static function updateLoopScopeContexts( LoopScope $loop_scope, Context $loop_context, @@ -559,10 +698,7 @@ private static function updateLoopScopeContexts( } // merge vars possibly in scope at the end of each loop - $loop_context->vars_possibly_in_scope = array_merge( - $loop_context->vars_possibly_in_scope, - $loop_scope->vars_possibly_in_scope, - ); + $loop_context->vars_possibly_in_scope += $loop_scope->vars_possibly_in_scope; } /** diff --git a/src/Psalm/Internal/Analyzer/Statements/Block/SwitchCaseAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Block/SwitchCaseAnalyzer.php index 54b7e7f3f01..d9c06e3551b 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Block/SwitchCaseAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Block/SwitchCaseAnalyzer.php @@ -78,8 +78,8 @@ public static function analyze( $case_context = clone $original_context; - if ($codebase->alter_code) { - $case_context->branch_point = $case_context->branch_point ?: (int) $stmt->getAttribute('startFilePos'); + if ($codebase->alter_code && $case_context->branch_point === null) { + $case_context->branch_point = (int) $stmt->getAttribute('startFilePos'); } $case_scope = $case_context->case_scope = new CaseScope($case_context); diff --git a/src/Psalm/Internal/Analyzer/Statements/Block/TryAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Block/TryAnalyzer.php index fab72f60413..b09a8837156 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Block/TryAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Block/TryAnalyzer.php @@ -65,8 +65,8 @@ public static function analyze( $try_context = clone $context; - if ($codebase->alter_code) { - $try_context->branch_point = $try_context->branch_point ?: (int) $stmt->getAttribute('startFilePos'); + if ($codebase->alter_code && $try_context->branch_point === null) { + $try_context->branch_point = (int) $stmt->getAttribute('startFilePos'); } if ($stmt->finally) { diff --git a/src/Psalm/Internal/Analyzer/Statements/Block/WhileAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Block/WhileAnalyzer.php index 1d8d8ef8278..c498e3c3cee 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Block/WhileAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Block/WhileAnalyzer.php @@ -4,13 +4,7 @@ use PhpParser; use Psalm\Context; -use Psalm\Internal\Analyzer\ScopeAnalyzer; use Psalm\Internal\Analyzer\StatementsAnalyzer; -use Psalm\Internal\Scope\LoopScope; -use UnexpectedValueException; - -use function array_merge; -use function in_array; /** * @internal @@ -30,74 +24,16 @@ public static function analyze( || (($t = $statements_analyzer->node_data->getType($stmt->cond)) && $t->isAlwaysTruthy()); - $pre_context = null; - - if ($while_true) { - $pre_context = clone $context; - } - - $while_context = clone $context; - - $while_context->inside_loop = true; - $while_context->break_types[] = 'loop'; - - $codebase = $statements_analyzer->getCodebase(); - - if ($codebase->alter_code) { - $while_context->branch_point = $while_context->branch_point ?: (int) $stmt->getAttribute('startFilePos'); - } - - $loop_scope = new LoopScope($while_context, $context); - $loop_scope->protected_var_ids = $context->protected_var_ids; - - if (LoopAnalyzer::analyze( + return LoopAnalyzer::analyzeForOrWhile( $statements_analyzer, - $stmt->stmts, + $stmt, + $context, + $while_true, + [], + [], self::getAndExpressions($stmt->cond), [], - $loop_scope, - $inner_loop_context, - ) === false) { - return false; - } - - if (!$inner_loop_context) { - throw new UnexpectedValueException('There should be an inner loop context'); - } - - $always_enters_loop = false; - - if ($stmt_cond_type = $statements_analyzer->node_data->getType($stmt->cond)) { - $always_enters_loop = $stmt_cond_type->isAlwaysTruthy(); - } - - if ($while_true) { - $always_enters_loop = true; - } - - $can_leave_loop = !$while_true - || in_array(ScopeAnalyzer::ACTION_BREAK, $loop_scope->final_actions, true); - - if ($always_enters_loop && $can_leave_loop) { - LoopAnalyzer::setLoopVars($inner_loop_context, $context, $loop_scope); - } - - $while_context->loop_scope = null; - - if ($can_leave_loop) { - $context->vars_possibly_in_scope = array_merge( - $context->vars_possibly_in_scope, - $while_context->vars_possibly_in_scope, - ); - } elseif ($pre_context) { - $context->vars_possibly_in_scope = $pre_context->vars_possibly_in_scope; - } - - if ($context->collect_exceptions) { - $context->mergeExceptions($while_context); - } - - return null; + ); } /** diff --git a/src/Psalm/Internal/Scope/LoopScope.php b/src/Psalm/Internal/Scope/LoopScope.php index 76b059329f1..ba2f30e498d 100644 --- a/src/Psalm/Internal/Scope/LoopScope.php +++ b/src/Psalm/Internal/Scope/LoopScope.php @@ -42,7 +42,7 @@ final class LoopScope public array $vars_possibly_in_scope = []; /** - * @var array + * @var array */ public array $protected_var_ids = [];