$stmts * @param PhpParser\Node\Expr[] $pre_conditions * @param PhpParser\Node\Expr[] $post_expressions * @param Context loop_scope->loop_context * @param Context $loop_scope->loop_parent_context * @param bool $is_do * * @return false|null */ public static function analyze( StatementsAnalyzer $statements_analyzer, array $stmts, array $pre_conditions, array $post_expressions, LoopScope $loop_scope, Context &$inner_context = null, $is_do = false ) { $traverser = new PhpParser\NodeTraverser; $assignment_mapper = new \Psalm\Internal\PhpVisitor\AssignmentMapVisitor($loop_scope->loop_context->self); $traverser->addVisitor($assignment_mapper); $traverser->traverse(array_merge($stmts, $post_expressions)); $assignment_map = $assignment_mapper->getAssignmentMap(); $assignment_depth = 0; $asserted_var_ids = []; $pre_condition_clauses = []; $original_protected_var_ids = $loop_scope->loop_parent_context->protected_var_ids; $codebase = $statements_analyzer->getCodebase(); if ($pre_conditions) { foreach ($pre_conditions as $i => $pre_condition) { $pre_condition_clauses[$i] = Algebra::getFormula( \spl_object_id($pre_condition), $pre_condition, $loop_scope->loop_context->self, $statements_analyzer, $codebase ); } } else { $asserted_var_ids = Context::getNewOrUpdatedVarIds( $loop_scope->loop_parent_context, $loop_scope->loop_context ); } $final_actions = ScopeAnalyzer::getFinalControlActions( $stmts, $statements_analyzer->node_data, Config::getInstance()->exit_functions, $loop_scope->loop_context->break_types ); $does_always_break = $final_actions === [ScopeAnalyzer::ACTION_BREAK]; if ($assignment_map) { $first_var_id = array_keys($assignment_map)[0]; $assignment_depth = self::getAssignmentMapDepth($first_var_id, $assignment_map); } $loop_scope->loop_context->parent_context = $loop_scope->loop_parent_context; $pre_outer_context = $loop_scope->loop_parent_context; if ($assignment_depth === 0 || $does_always_break) { $inner_context = clone $loop_scope->loop_context; foreach ($inner_context->vars_in_scope as $context_var_id => $context_type) { $inner_context->vars_in_scope[$context_var_id] = clone $context_type; } $inner_context->loop_scope = $loop_scope; $inner_context->parent_context = $loop_scope->loop_context; $old_referenced_var_ids = $inner_context->referenced_var_ids; $inner_context->referenced_var_ids = []; foreach ($pre_conditions as $condition_offset => $pre_condition) { self::applyPreConditionToLoopContext( $statements_analyzer, $pre_condition, $pre_condition_clauses[$condition_offset], $inner_context, $loop_scope->loop_parent_context, $is_do ); } $inner_context->protected_var_ids = $loop_scope->protected_var_ids; $statements_analyzer->analyze($stmts, $inner_context); self::updateLoopScopeContexts($loop_scope, $loop_scope->loop_parent_context); foreach ($post_expressions as $post_expression) { if (ExpressionAnalyzer::analyze( $statements_analyzer, $post_expression, $loop_scope->loop_context ) === false ) { return false; } } $new_referenced_var_ids = $inner_context->referenced_var_ids; $inner_context->referenced_var_ids = $old_referenced_var_ids + $inner_context->referenced_var_ids; $loop_scope->loop_parent_context->vars_possibly_in_scope = array_merge( $inner_context->vars_possibly_in_scope, $loop_scope->loop_parent_context->vars_possibly_in_scope ); } else { $pre_outer_context = clone $loop_scope->loop_parent_context; $analyzer = $statements_analyzer->getCodebase()->analyzer; $original_mixed_counts = $analyzer->getMixedCountsForFile($statements_analyzer->getFilePath()); $pre_condition_vars_in_scope = $loop_scope->loop_context->vars_in_scope; IssueBuffer::startRecording(); foreach ($pre_conditions as $condition_offset => $pre_condition) { $asserted_var_ids = array_merge( self::applyPreConditionToLoopContext( $statements_analyzer, $pre_condition, $pre_condition_clauses[$condition_offset], $loop_scope->loop_context, $loop_scope->loop_parent_context, $is_do ), $asserted_var_ids ); } // record all the vars that existed before we did the first pass through the loop $pre_loop_context = clone $loop_scope->loop_context; $inner_context = clone $loop_scope->loop_context; foreach ($inner_context->vars_in_scope as $context_var_id => $context_type) { $inner_context->vars_in_scope[$context_var_id] = clone $context_type; } $inner_context->parent_context = $loop_scope->loop_context; $inner_context->loop_scope = $loop_scope; $old_referenced_var_ids = $inner_context->referenced_var_ids; $inner_context->referenced_var_ids = []; $asserted_var_ids = array_unique($asserted_var_ids); $inner_context->protected_var_ids = $loop_scope->protected_var_ids; $statements_analyzer->analyze($stmts, $inner_context); self::updateLoopScopeContexts($loop_scope, $pre_outer_context); $inner_context->protected_var_ids = $original_protected_var_ids; foreach ($post_expressions as $post_expression) { if (ExpressionAnalyzer::analyze($statements_analyzer, $post_expression, $inner_context) === false) { return false; } } /** * @var array */ $new_referenced_var_ids = $inner_context->referenced_var_ids; $inner_context->referenced_var_ids = array_intersect_key( $old_referenced_var_ids, $inner_context->referenced_var_ids ); $recorded_issues = IssueBuffer::clearRecordingLevel(); IssueBuffer::stopRecording(); for ($i = 0; $i < $assignment_depth; ++$i) { $vars_to_remove = []; $loop_scope->iteration_count++; $has_changes = false; // reset the $inner_context to what it was before we started the analysis, // but union the types with what's in the loop scope foreach ($inner_context->vars_in_scope as $var_id => $type) { if (in_array($var_id, $asserted_var_ids, true)) { // set the vars to whatever the while/foreach loop expects them to be if (!isset($pre_loop_context->vars_in_scope[$var_id]) || !$type->equals($pre_loop_context->vars_in_scope[$var_id]) ) { $has_changes = true; } } elseif (isset($pre_outer_context->vars_in_scope[$var_id])) { if (!$type->equals($pre_outer_context->vars_in_scope[$var_id])) { $has_changes = true; // widen the foreach context type with the initial context type $inner_context->vars_in_scope[$var_id] = Type::combineUnionTypes( $inner_context->vars_in_scope[$var_id], $pre_outer_context->vars_in_scope[$var_id] ); // if there's a change, invalidate related clauses $pre_loop_context->removeVarFromConflictingClauses($var_id); } if (isset($loop_scope->loop_context->vars_in_scope[$var_id]) && !$type->equals($loop_scope->loop_context->vars_in_scope[$var_id]) ) { $has_changes = true; // widen the foreach context type with the initial context type $inner_context->vars_in_scope[$var_id] = Type::combineUnionTypes( $inner_context->vars_in_scope[$var_id], $loop_scope->loop_context->vars_in_scope[$var_id] ); // if there's a change, invalidate related clauses $pre_loop_context->removeVarFromConflictingClauses($var_id); } } else { // give an opportunity to redeemed UndefinedVariable issues if ($recorded_issues) { $has_changes = true; } // if we're in a do block we don't want to remove vars before evaluating // the where conditional if (!$is_do) { $vars_to_remove[] = $var_id; } } } $inner_context->has_returned = false; if ($codebase->find_unused_variables) { foreach ($inner_context->unreferenced_vars as $var_id => $locations) { if (!isset($pre_outer_context->vars_in_scope[$var_id])) { $loop_scope->unreferenced_vars[$var_id] = $locations; unset($inner_context->unreferenced_vars[$var_id]); } } } $loop_scope->loop_parent_context->vars_possibly_in_scope = array_merge( $inner_context->vars_possibly_in_scope, $loop_scope->loop_parent_context->vars_possibly_in_scope ); // if there are no changes to the types, no need to re-examine if (!$has_changes) { break; } if ($codebase->find_unused_variables) { foreach ($loop_scope->possibly_unreferenced_vars as $var_id => $locations) { if (isset($inner_context->unreferenced_vars[$var_id])) { $inner_context->unreferenced_vars[$var_id] += $locations; } else { $inner_context->unreferenced_vars[$var_id] = $locations; } } } // remove vars that were defined in the foreach foreach ($vars_to_remove as $var_id) { unset($inner_context->vars_in_scope[$var_id]); } $inner_context->clauses = $pre_loop_context->clauses; $analyzer->setMixedCountsForFile($statements_analyzer->getFilePath(), $original_mixed_counts); IssueBuffer::startRecording(); foreach ($pre_loop_context->vars_in_scope as $var_id => $_) { if (!isset($pre_condition_vars_in_scope[$var_id]) && \strpos($var_id, '->') === false && \strpos($var_id, '[') === false ) { $inner_context->vars_in_scope[$var_id]->possibly_undefined = true; } } foreach ($pre_conditions as $condition_offset => $pre_condition) { self::applyPreConditionToLoopContext( $statements_analyzer, $pre_condition, $pre_condition_clauses[$condition_offset], $inner_context, $loop_scope->loop_parent_context, false ); } foreach ($asserted_var_ids as $var_id) { if (!isset($inner_context->vars_in_scope[$var_id]) || $inner_context->vars_in_scope[$var_id]->getId() !== $pre_loop_context->vars_in_scope[$var_id]->getId() || $inner_context->vars_in_scope[$var_id]->from_docblock !== $pre_loop_context->vars_in_scope[$var_id]->from_docblock ) { $inner_context->vars_in_scope[$var_id] = clone $pre_loop_context->vars_in_scope[$var_id]; } } $inner_context->clauses = $pre_loop_context->clauses; $inner_context->protected_var_ids = $loop_scope->protected_var_ids; $traverser = new PhpParser\NodeTraverser; $traverser->addVisitor( new \Psalm\Internal\PhpVisitor\NodeCleanerVisitor( $statements_analyzer->node_data ) ); $traverser->traverse($stmts); $statements_analyzer->analyze($stmts, $inner_context); self::updateLoopScopeContexts($loop_scope, $pre_outer_context); $inner_context->protected_var_ids = $original_protected_var_ids; foreach ($post_expressions as $post_expression) { if (ExpressionAnalyzer::analyze($statements_analyzer, $post_expression, $inner_context) === false) { return false; } } $recorded_issues = IssueBuffer::clearRecordingLevel(); IssueBuffer::stopRecording(); } if ($recorded_issues) { foreach ($recorded_issues as $recorded_issue) { // if we're not in any loops then this will just result in the issue being emitted IssueBuffer::bubbleUp($recorded_issue); } } } $does_sometimes_break = in_array(ScopeAnalyzer::ACTION_BREAK, $loop_scope->final_actions, true); $does_always_break = $loop_scope->final_actions === [ScopeAnalyzer::ACTION_BREAK]; if ($does_sometimes_break) { if ($loop_scope->possibly_redefined_loop_parent_vars !== null) { foreach ($loop_scope->possibly_redefined_loop_parent_vars as $var => $type) { $loop_scope->loop_parent_context->vars_in_scope[$var] = Type::combineUnionTypes( $type, $loop_scope->loop_parent_context->vars_in_scope[$var] ); } } } foreach ($loop_scope->loop_parent_context->vars_in_scope as $var_id => $type) { if (!isset($loop_scope->loop_context->vars_in_scope[$var_id])) { continue; } if ($loop_scope->loop_context->vars_in_scope[$var_id]->getId() !== $type->getId()) { $loop_scope->loop_parent_context->vars_in_scope[$var_id] = Type::combineUnionTypes( $loop_scope->loop_parent_context->vars_in_scope[$var_id], $loop_scope->loop_context->vars_in_scope[$var_id] ); $loop_scope->loop_parent_context->removeVarFromConflictingClauses($var_id); } } if (!$does_always_break) { foreach ($loop_scope->loop_parent_context->vars_in_scope as $var_id => $type) { if (!isset($inner_context->vars_in_scope[$var_id])) { unset($loop_scope->loop_parent_context->vars_in_scope[$var_id]); continue; } if ($inner_context->vars_in_scope[$var_id]->hasMixed()) { $loop_scope->loop_parent_context->vars_in_scope[$var_id] = $inner_context->vars_in_scope[$var_id]; $loop_scope->loop_parent_context->removeVarFromConflictingClauses($var_id); continue; } if ($inner_context->vars_in_scope[$var_id]->getId() !== $type->getId()) { $loop_scope->loop_parent_context->vars_in_scope[$var_id] = Type::combineUnionTypes( $loop_scope->loop_parent_context->vars_in_scope[$var_id], $inner_context->vars_in_scope[$var_id] ); $loop_scope->loop_parent_context->removeVarFromConflictingClauses($var_id); } } } if ($pre_conditions && $pre_condition_clauses && !ScopeAnalyzer::doesEverBreak($stmts)) { // if the loop contains an assertion and there are no break statements, we can negate that assertion // and apply it to the current context try { $negated_pre_condition_clauses = Algebra::negateFormula(array_merge(...$pre_condition_clauses)); } catch (\Psalm\Exception\ComplicatedExpressionException $e) { $negated_pre_condition_clauses = []; } $negated_pre_condition_types = Algebra::getTruthsFromFormula($negated_pre_condition_clauses); if ($negated_pre_condition_types) { $changed_var_ids = []; $vars_in_scope_reconciled = Reconciler::reconcileKeyedTypes( $negated_pre_condition_types, [], $inner_context->vars_in_scope, $changed_var_ids, [], $statements_analyzer, [], true, new CodeLocation($statements_analyzer->getSource(), $pre_conditions[0]) ); foreach ($changed_var_ids as $var_id => $_) { if (isset($vars_in_scope_reconciled[$var_id]) && isset($loop_scope->loop_parent_context->vars_in_scope[$var_id]) ) { $loop_scope->loop_parent_context->vars_in_scope[$var_id] = $vars_in_scope_reconciled[$var_id]; } $loop_scope->loop_parent_context->removeVarFromConflictingClauses($var_id); } } } $loop_scope->loop_context->referenced_var_ids = array_merge( array_intersect_key( $inner_context->referenced_var_ids, $pre_outer_context->vars_in_scope ), $loop_scope->loop_context->referenced_var_ids ); if ($codebase->find_unused_variables) { foreach ($loop_scope->possibly_unreferenced_vars as $var_id => $locations) { if (isset($inner_context->unreferenced_vars[$var_id])) { $inner_context->unreferenced_vars[$var_id] += $locations; } } foreach ($inner_context->unreferenced_vars as $var_id => $locations) { if (!isset($new_referenced_var_ids[$var_id]) || !isset($pre_outer_context->vars_in_scope[$var_id]) || $does_always_break ) { if (!isset($loop_scope->loop_context->unreferenced_vars[$var_id])) { $loop_scope->loop_context->unreferenced_vars[$var_id] = $locations; } else { $loop_scope->loop_context->unreferenced_vars[$var_id] += $locations; } } else { $statements_analyzer->registerVariableUses($locations); } } foreach ($loop_scope->unreferenced_vars as $var_id => $locations) { if (isset($loop_scope->referenced_var_ids[$var_id])) { $statements_analyzer->registerVariableUses($locations); } else { if (!isset($loop_scope->loop_context->unreferenced_vars[$var_id])) { $loop_scope->loop_context->unreferenced_vars[$var_id] = $locations; } else { $loop_scope->loop_context->unreferenced_vars[$var_id] += $locations; } } } } } /** * @param LoopScope $loop_scope * @param Context $pre_outer_context * * @return void */ private static function updateLoopScopeContexts( LoopScope $loop_scope, Context $pre_outer_context ) { $updated_loop_vars = []; if (!in_array(ScopeAnalyzer::ACTION_CONTINUE, $loop_scope->final_actions, true)) { $loop_scope->loop_context->vars_in_scope = $pre_outer_context->vars_in_scope; } else { if ($loop_scope->redefined_loop_vars !== null) { foreach ($loop_scope->redefined_loop_vars as $var => $type) { $loop_scope->loop_context->vars_in_scope[$var] = $type; $updated_loop_vars[$var] = true; } } if ($loop_scope->possibly_redefined_loop_vars) { foreach ($loop_scope->possibly_redefined_loop_vars as $var => $type) { if ($loop_scope->loop_context->hasVariable($var) && !isset($updated_loop_vars[$var]) ) { $loop_scope->loop_context->vars_in_scope[$var] = Type::combineUnionTypes( $loop_scope->loop_context->vars_in_scope[$var], $type ); } } } } // merge vars possibly in scope at the end of each loop $loop_scope->loop_context->vars_possibly_in_scope = array_merge( $loop_scope->loop_context->vars_possibly_in_scope, $loop_scope->vars_possibly_in_scope ); } /** * @param PhpParser\Node\Expr $pre_condition * @param array $pre_condition_clauses * @param Context $loop_context * @param Context $outer_context * * @return string[] */ private static function applyPreConditionToLoopContext( StatementsAnalyzer $statements_analyzer, PhpParser\Node\Expr $pre_condition, array $pre_condition_clauses, Context $loop_context, Context $outer_context, bool $is_do ) { $pre_referenced_var_ids = $loop_context->referenced_var_ids; $loop_context->referenced_var_ids = []; $loop_context->inside_conditional = true; $suppressed_issues = $statements_analyzer->getSuppressedIssues(); if ($is_do) { if (!in_array('RedundantCondition', $suppressed_issues, true)) { $statements_analyzer->addSuppressedIssues(['RedundantCondition']); } if (!in_array('RedundantConditionGivenDocblockType', $suppressed_issues, true)) { $statements_analyzer->addSuppressedIssues(['RedundantConditionGivenDocblockType']); } if (!in_array('TypeDoesNotContainType', $suppressed_issues, true)) { $statements_analyzer->addSuppressedIssues(['TypeDoesNotContainType']); } } if (ExpressionAnalyzer::analyze($statements_analyzer, $pre_condition, $loop_context) === false) { return []; } $loop_context->inside_conditional = false; $new_referenced_var_ids = $loop_context->referenced_var_ids; $loop_context->referenced_var_ids = array_merge($pre_referenced_var_ids, $new_referenced_var_ids); $asserted_var_ids = Context::getNewOrUpdatedVarIds($outer_context, $loop_context); $loop_context->clauses = Algebra::simplifyCNF( array_merge($outer_context->clauses, $pre_condition_clauses) ); $active_while_types = []; $reconcilable_while_types = Algebra::getTruthsFromFormula( $loop_context->clauses, \spl_object_id($pre_condition), $new_referenced_var_ids ); $changed_var_ids = []; if ($reconcilable_while_types) { $pre_condition_vars_in_scope_reconciled = Reconciler::reconcileKeyedTypes( $reconcilable_while_types, $active_while_types, $loop_context->vars_in_scope, $changed_var_ids, $new_referenced_var_ids, $statements_analyzer, [], true, new CodeLocation($statements_analyzer->getSource(), $pre_condition) ); $loop_context->vars_in_scope = $pre_condition_vars_in_scope_reconciled; } if ($is_do) { if (!in_array('RedundantCondition', $suppressed_issues, true)) { $statements_analyzer->removeSuppressedIssues(['RedundantCondition']); } if (!in_array('RedundantConditionGivenDocblockType', $suppressed_issues, true)) { $statements_analyzer->removeSuppressedIssues(['RedundantConditionGivenDocblockType']); } if (!in_array('TypeDoesNotContainType', $suppressed_issues, true)) { $statements_analyzer->removeSuppressedIssues(['TypeDoesNotContainType']); } } if ($is_do) { return []; } foreach ($asserted_var_ids as $var_id) { $loop_context->clauses = Context::filterClauses( $var_id, $loop_context->clauses, null, $statements_analyzer ); } return $asserted_var_ids; } /** * @param string $first_var_id * @param array> $assignment_map * * @return int */ private static function getAssignmentMapDepth($first_var_id, array $assignment_map) { $max_depth = 0; $assignment_var_ids = $assignment_map[$first_var_id]; unset($assignment_map[$first_var_id]); foreach ($assignment_var_ids as $assignment_var_id => $_) { $depth = 1; if (isset($assignment_map[$assignment_var_id])) { $depth = 1 + self::getAssignmentMapDepth($assignment_var_id, $assignment_map); } if ($depth > $max_depth) { $max_depth = $depth; } } return $max_depth; } }