1
0
mirror of https://github.com/danog/psalm.git synced 2024-12-16 11:26:55 +01:00
psalm/src/Psalm/Internal/Analyzer/Statements/Block/LoopAnalyzer.php

725 lines
29 KiB
PHP
Raw Normal View History

2017-11-30 07:07:20 +01:00
<?php
2018-11-06 03:57:36 +01:00
namespace Psalm\Internal\Analyzer\Statements\Block;
2017-11-30 07:07:20 +01:00
use PhpParser;
2018-11-06 03:57:36 +01:00
use Psalm\Internal\Analyzer\ScopeAnalyzer;
use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer;
use Psalm\Internal\Analyzer\StatementsAnalyzer;
use Psalm\Internal\Clause;
2017-11-30 07:07:20 +01:00
use Psalm\CodeLocation;
use Psalm\Config;
2017-11-30 07:07:20 +01:00
use Psalm\Context;
use Psalm\IssueBuffer;
2018-11-06 03:57:36 +01:00
use Psalm\Internal\Scope\LoopScope;
2017-11-30 07:07:20 +01:00
use Psalm\Type;
2018-05-07 07:26:06 +02:00
use Psalm\Type\Algebra;
use Psalm\Type\Reconciler;
use function array_merge;
use function array_keys;
use function array_unique;
use function array_intersect_key;
use function in_array;
2017-11-30 07:07:20 +01:00
/**
* @internal
*/
2018-11-06 03:57:36 +01:00
class LoopAnalyzer
2017-11-30 07:07:20 +01:00
{
/**
* Checks an array of statements in a loop
*
* @param array<PhpParser\Node\Stmt> $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
2017-11-30 07:07:20 +01:00
*
* @return false|null
*/
public static function analyze(
2018-11-11 18:01:14 +01:00
StatementsAnalyzer $statements_analyzer,
2017-11-30 07:07:20 +01:00
array $stmts,
array $pre_conditions,
2018-01-29 23:18:03 +01:00
array $post_expressions,
LoopScope $loop_scope,
Context &$inner_context = null,
bool $is_do = false,
bool $always_enters_loop = false
): ?bool {
2017-11-30 07:07:20 +01:00
$traverser = new PhpParser\NodeTraverser;
2020-03-15 04:54:42 +01:00
$assignment_mapper = new \Psalm\Internal\PhpVisitor\AssignmentMapVisitor($loop_scope->loop_context->self);
2017-11-30 07:07:20 +01:00
$traverser->addVisitor($assignment_mapper);
$traverser->traverse(array_merge($pre_conditions, $stmts, $post_expressions));
2017-11-30 07:07:20 +01:00
$assignment_map = $assignment_mapper->getAssignmentMap();
$assignment_depth = 0;
$always_assigned_before_loop_body_vars = [];
2017-11-30 07:07:20 +01:00
$pre_condition_clauses = [];
$original_protected_var_ids = $loop_scope->loop_parent_context->protected_var_ids;
2018-11-11 18:01:14 +01:00
$codebase = $statements_analyzer->getCodebase();
2018-11-06 03:57:36 +01:00
$inner_do_context = null;
2017-11-30 07:07:20 +01:00
if ($pre_conditions) {
foreach ($pre_conditions as $i => $pre_condition) {
2020-08-26 21:35:29 +02:00
$pre_condition_id = \spl_object_id($pre_condition);
$pre_condition_clauses[$i] = Algebra::getFormula(
2020-08-26 21:35:29 +02:00
$pre_condition_id,
$pre_condition_id,
$pre_condition,
$loop_scope->loop_context->self,
$statements_analyzer,
$codebase
2017-11-30 07:07:20 +01:00
);
}
} else {
$always_assigned_before_loop_body_vars = Context::getNewOrUpdatedVarIds(
2017-12-03 00:28:18 +01:00
$loop_scope->loop_parent_context,
$loop_scope->loop_context
);
2017-11-30 07:07:20 +01:00
}
$final_actions = ScopeAnalyzer::getControlActions(
$stmts,
$statements_analyzer->node_data,
Config::getInstance()->exit_functions,
$loop_scope->loop_context->break_types
);
2019-07-31 23:14:00 +02:00
$does_always_break = $final_actions === [ScopeAnalyzer::ACTION_BREAK];
2017-12-03 00:28:18 +01:00
$has_continue = in_array(ScopeAnalyzer::ACTION_CONTINUE, $final_actions, true);
2017-11-30 07:07:20 +01:00
if ($assignment_map) {
$first_var_id = array_keys($assignment_map)[0];
$assignment_depth = self::getAssignmentMapDepth($first_var_id, $assignment_map);
}
if ($has_continue) {
// this intuuitively feels right to me if there's a continue statement,
// maybe more assignment intrigue is possible
$assignment_depth++;
}
2017-12-03 00:28:18 +01:00
$loop_scope->loop_context->parent_context = $loop_scope->loop_parent_context;
$pre_outer_context = $loop_scope->loop_parent_context;
2019-07-31 23:14:00 +02:00
if ($assignment_depth === 0 || $does_always_break) {
2017-12-03 00:28:18 +01:00
$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;
2017-12-03 00:28:18 +01:00
$inner_context->parent_context = $loop_scope->loop_context;
2018-06-17 02:01:33 +02:00
$old_referenced_var_ids = $inner_context->referenced_var_ids;
$inner_context->referenced_var_ids = [];
2017-11-30 07:07:20 +01:00
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
);
2017-11-30 07:07:20 +01:00
}
$inner_context->protected_var_ids = $loop_scope->protected_var_ids;
2018-11-11 18:01:14 +01:00
$statements_analyzer->analyze($stmts, $inner_context);
2017-12-03 00:28:18 +01:00
self::updateLoopScopeContexts($loop_scope, $loop_scope->loop_parent_context);
2017-11-30 07:07:20 +01:00
2018-01-29 23:18:03 +01:00
foreach ($post_expressions as $post_expression) {
2018-11-06 03:57:36 +01:00
if (ExpressionAnalyzer::analyze(
2018-11-11 18:01:14 +01:00
$statements_analyzer,
2018-01-29 23:18:03 +01:00
$post_expression,
2017-12-03 00:28:18 +01:00
$loop_scope->loop_context
) === false
) {
2017-11-30 07:07:20 +01:00
return false;
}
}
2017-12-03 00:28:18 +01:00
2018-06-17 02:01:33 +02:00
$inner_context->referenced_var_ids = $old_referenced_var_ids + $inner_context->referenced_var_ids;
2017-12-03 00:28:18 +01:00
$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
);
2017-11-30 07:07:20 +01:00
} else {
2017-12-03 00:28:18 +01:00
$pre_outer_context = clone $loop_scope->loop_parent_context;
2017-11-30 07:07:20 +01:00
2018-11-11 18:01:14 +01:00
$analyzer = $statements_analyzer->getCodebase()->analyzer;
2018-11-11 18:01:14 +01:00
$original_mixed_counts = $analyzer->getMixedCountsForFile($statements_analyzer->getFilePath());
$pre_condition_vars_in_scope = $loop_scope->loop_context->vars_in_scope;
2017-11-30 07:07:20 +01:00
IssueBuffer::startRecording();
if (!$is_do) {
foreach ($pre_conditions as $condition_offset => $pre_condition) {
self::applyPreConditionToLoopContext(
$statements_analyzer,
$pre_condition,
$pre_condition_clauses[$condition_offset],
$loop_scope->loop_context,
$loop_scope->loop_parent_context,
$is_do
);
}
2017-11-30 07:07:20 +01:00
}
2017-12-03 00:28:18 +01:00
// 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;
}
2017-12-03 00:28:18 +01:00
$inner_context->parent_context = $loop_scope->loop_context;
$inner_context->loop_scope = $loop_scope;
2017-12-03 00:28:18 +01:00
2018-06-17 02:01:33 +02:00
$old_referenced_var_ids = $inner_context->referenced_var_ids;
$inner_context->referenced_var_ids = [];
$inner_context->protected_var_ids = $loop_scope->protected_var_ids;
2018-11-11 18:01:14 +01:00
$statements_analyzer->analyze($stmts, $inner_context);
2017-12-03 00:28:18 +01:00
self::updateLoopScopeContexts($loop_scope, $pre_outer_context);
2017-11-30 07:07:20 +01:00
$inner_context->protected_var_ids = $original_protected_var_ids;
if ($is_do) {
$inner_do_context = clone $inner_context;
foreach ($pre_conditions as $condition_offset => $pre_condition) {
$always_assigned_before_loop_body_vars = array_merge(
self::applyPreConditionToLoopContext(
$statements_analyzer,
$pre_condition,
$pre_condition_clauses[$condition_offset],
$inner_context,
$loop_scope->loop_parent_context,
$is_do
),
$always_assigned_before_loop_body_vars
);
}
}
$always_assigned_before_loop_body_vars = array_unique($always_assigned_before_loop_body_vars);
2018-01-29 23:18:03 +01:00
foreach ($post_expressions as $post_expression) {
2018-11-11 18:01:14 +01:00
if (ExpressionAnalyzer::analyze($statements_analyzer, $post_expression, $inner_context) === false) {
2017-11-30 07:07:20 +01:00
return false;
}
}
$inner_context->referenced_var_ids = array_intersect_key(
$old_referenced_var_ids,
$inner_context->referenced_var_ids
);
2018-06-17 02:01:33 +02:00
2017-11-30 07:07:20 +01:00
$recorded_issues = IssueBuffer::clearRecordingLevel();
IssueBuffer::stopRecording();
for ($i = 0; $i < $assignment_depth; ++$i) {
$vars_to_remove = [];
2018-11-10 20:06:31 +01:00
$loop_scope->iteration_count++;
2017-11-30 07:07:20 +01:00
$has_changes = false;
2017-12-03 00:28:18 +01:00
// 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, $always_assigned_before_loop_body_vars, true)) {
2017-11-30 07:07:20 +01:00
// set the vars to whatever the while/foreach loop expects them to be
2017-12-03 00:28:18 +01:00
if (!isset($pre_loop_context->vars_in_scope[$var_id])
|| !$type->equals($pre_loop_context->vars_in_scope[$var_id])
2017-12-03 00:28:18 +01:00
) {
2017-11-30 07:07:20 +01:00
$has_changes = true;
}
} elseif (isset($pre_outer_context->vars_in_scope[$var_id])) {
if (!$type->equals($pre_outer_context->vars_in_scope[$var_id])) {
2017-11-30 07:07:20 +01:00
$has_changes = true;
// widen the foreach context type with the initial context type
2017-12-03 00:28:18 +01:00
$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);
$loop_scope->loop_parent_context->possibly_assigned_var_ids[$var_id] = true;
2017-12-03 00:28:18 +01:00
}
2018-07-06 03:03:44 +02:00
if (isset($loop_scope->loop_context->vars_in_scope[$var_id])
&& !$type->equals($loop_scope->loop_context->vars_in_scope[$var_id])
) {
2017-12-03 00:28:18 +01:00
$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]
2017-11-30 07:07:20 +01:00
);
// 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;
}
2018-11-10 20:06:31 +01:00
// 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;
}
2017-11-30 07:07:20 +01:00
}
}
$inner_context->has_returned = false;
2017-12-03 00:28:18 +01:00
$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
);
2017-11-30 07:07:20 +01:00
// if there are no changes to the types, no need to re-examine
if (!$has_changes) {
break;
}
// remove vars that were defined in the foreach
foreach ($vars_to_remove as $var_id) {
2017-12-03 00:28:18 +01:00
unset($inner_context->vars_in_scope[$var_id]);
2017-11-30 07:07:20 +01:00
}
2019-12-08 06:49:34 +01:00
$inner_context->clauses = $pre_loop_context->clauses;
2018-11-11 18:01:14 +01:00
$analyzer->setMixedCountsForFile($statements_analyzer->getFilePath(), $original_mixed_counts);
2017-11-30 07:07:20 +01:00
IssueBuffer::startRecording();
foreach ($pre_loop_context->vars_in_scope as $var_id => $_) {
if (!isset($pre_condition_vars_in_scope[$var_id])
2020-05-11 15:34:07 +02:00
&& isset($inner_context->vars_in_scope[$var_id])
2020-05-10 17:35:35 +02:00
&& \strpos($var_id, '->') === false
&& \strpos($var_id, '[') === false
) {
$inner_context->vars_in_scope[$var_id]->possibly_undefined = true;
}
}
if (!$is_do) {
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
);
}
2017-11-30 07:07:20 +01:00
}
foreach ($always_assigned_before_loop_body_vars as $var_id) {
2020-08-17 21:25:13 +02:00
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
)
) {
2020-08-17 21:25:13 +02:00
if (isset($pre_loop_context->vars_in_scope[$var_id])) {
$inner_context->vars_in_scope[$var_id] = clone $pre_loop_context->vars_in_scope[$var_id];
} else {
unset($inner_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(
2020-03-15 04:54:42 +01:00
new \Psalm\Internal\PhpVisitor\NodeCleanerVisitor(
$statements_analyzer->node_data
)
);
$traverser->traverse($stmts);
2018-11-11 18:01:14 +01:00
$statements_analyzer->analyze($stmts, $inner_context);
2017-12-03 00:28:18 +01:00
self::updateLoopScopeContexts($loop_scope, $pre_outer_context);
2017-11-30 07:07:20 +01:00
$inner_context->protected_var_ids = $original_protected_var_ids;
if ($is_do) {
$inner_do_context = clone $inner_context;
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
);
}
}
2018-01-29 23:18:03 +01:00
foreach ($post_expressions as $post_expression) {
2018-11-11 18:01:14 +01:00
if (ExpressionAnalyzer::analyze($statements_analyzer, $post_expression, $inner_context) === false) {
2017-11-30 07:07:20 +01:00
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);
}
}
}
2018-11-06 03:57:36 +01:00
$does_sometimes_break = in_array(ScopeAnalyzer::ACTION_BREAK, $loop_scope->final_actions, true);
$does_always_break = $loop_scope->final_actions === [ScopeAnalyzer::ACTION_BREAK];
2017-11-30 07:07:20 +01:00
2017-12-03 00:28:18 +01:00
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]
);
$loop_scope->loop_parent_context->possibly_assigned_var_ids[$var] = true;
2017-12-03 00:28:18 +01:00
}
2017-11-30 07:07:20 +01:00
}
2017-12-03 00:28:18 +01:00
}
2017-11-30 07:07:20 +01:00
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);
} else {
$loop_scope->loop_parent_context->vars_in_scope[$var_id]->parent_nodes
+= $loop_scope->loop_context->vars_in_scope[$var_id]->parent_nodes;
}
}
2017-12-03 00:28:18 +01:00
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()) {
$inner_context->vars_in_scope[$var_id]->parent_nodes
+= $loop_scope->loop_parent_context->vars_in_scope[$var_id]->parent_nodes;
2017-12-03 00:28:18 +01:00
$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);
2017-12-03 00:28:18 +01:00
continue;
}
2017-11-30 07:07:20 +01:00
if ($inner_context->vars_in_scope[$var_id]->getId() !== $type->getId()) {
2017-12-03 00:28:18 +01:00
$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);
} else {
$loop_scope->loop_parent_context->vars_in_scope[$var_id]->parent_nodes = array_merge(
$loop_scope->loop_parent_context->vars_in_scope[$var_id]->parent_nodes,
$inner_context->vars_in_scope[$var_id]->parent_nodes
);
2017-12-03 00:28:18 +01:00
}
2017-11-30 07:07:20 +01:00
}
}
2018-11-06 03:57:36 +01:00
if ($pre_conditions && $pre_condition_clauses && !ScopeAnalyzer::doesEverBreak($stmts)) {
2017-11-30 07:07:20 +01:00
// 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);
2017-11-30 07:07:20 +01:00
if ($negated_pre_condition_types) {
$changed_var_ids = [];
$vars_in_scope_reconciled = Reconciler::reconcileKeyedTypes(
2017-11-30 07:07:20 +01:00
$negated_pre_condition_types,
[],
2017-12-03 00:28:18 +01:00
$inner_context->vars_in_scope,
2017-11-30 07:07:20 +01:00
$changed_var_ids,
[],
2018-11-11 18:01:14 +01:00
$statements_analyzer,
[],
true,
new CodeLocation($statements_analyzer->getSource(), $pre_conditions[0])
2017-11-30 07:07:20 +01:00
);
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])
) {
2017-12-03 00:28:18 +01:00
$loop_scope->loop_parent_context->vars_in_scope[$var_id] = $vars_in_scope_reconciled[$var_id];
2017-11-30 07:07:20 +01:00
}
$loop_scope->loop_parent_context->removeVarFromConflictingClauses($var_id);
2017-12-03 00:28:18 +01:00
}
}
}
2017-12-04 05:50:51 +01:00
$loop_scope->loop_context->referenced_var_ids = array_merge(
array_intersect_key(
$inner_context->referenced_var_ids,
$pre_outer_context->vars_in_scope
),
2017-12-04 05:50:51 +01:00
$loop_scope->loop_context->referenced_var_ids
);
if ($always_enters_loop) {
foreach ($inner_context->vars_in_scope as $var_id => $type) {
// if there are break statements in the loop it's not certain
// that the loop has finished executing, so the assertions at the end
// the loop in the while conditional may not hold
if (in_array(ScopeAnalyzer::ACTION_BREAK, $loop_scope->final_actions, true)
|| in_array(ScopeAnalyzer::ACTION_CONTINUE, $loop_scope->final_actions, true)
) {
if (isset($loop_scope->possibly_defined_loop_parent_vars[$var_id])) {
$loop_scope->loop_parent_context->vars_in_scope[$var_id] = Type::combineUnionTypes(
$type,
$loop_scope->possibly_defined_loop_parent_vars[$var_id]
);
}
} else {
$loop_scope->loop_parent_context->vars_in_scope[$var_id] = $type;
}
}
}
if ($inner_do_context) {
$inner_context = $inner_do_context;
}
return null;
2017-12-03 00:28:18 +01:00
}
private static function updateLoopScopeContexts(
LoopScope $loop_scope,
Context $pre_outer_context
): void {
2017-12-03 00:28:18 +01:00
$updated_loop_vars = [];
2018-11-06 03:57:36 +01:00
if (!in_array(ScopeAnalyzer::ACTION_CONTINUE, $loop_scope->final_actions, true)) {
2017-12-03 00:28:18 +01:00
$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)) {
if (!isset($updated_loop_vars[$var])) {
$loop_scope->loop_context->vars_in_scope[$var] = Type::combineUnionTypes(
$loop_scope->loop_context->vars_in_scope[$var],
$type
);
} else {
$loop_scope->loop_context->vars_in_scope[$var]->parent_nodes
+= $type->parent_nodes;
}
2017-12-03 00:28:18 +01:00
}
2017-11-30 07:07:20 +01:00
}
}
}
2017-12-03 00:28:18 +01:00
// 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
);
2017-11-30 07:07:20 +01:00
}
/**
* @param array<int, Clause> $pre_condition_clauses
*
2017-12-03 00:28:18 +01:00
* @return string[]
2017-11-30 07:07:20 +01:00
*/
private static function applyPreConditionToLoopContext(
2018-11-11 18:01:14 +01:00
StatementsAnalyzer $statements_analyzer,
2017-11-30 07:07:20 +01:00
PhpParser\Node\Expr $pre_condition,
array $pre_condition_clauses,
Context $loop_context,
Context $outer_context,
bool $is_do
): array {
2017-11-30 07:07:20 +01:00
$pre_referenced_var_ids = $loop_context->referenced_var_ids;
$loop_context->referenced_var_ids = [];
$loop_context->inside_conditional = true;
2017-12-03 00:28:18 +01:00
$suppressed_issues = $statements_analyzer->getSuppressedIssues();
2020-10-08 02:24:50 +02:00
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']);
}
2018-11-11 18:01:14 +01:00
if (ExpressionAnalyzer::analyze($statements_analyzer, $pre_condition, $loop_context) === false) {
2017-12-03 00:28:18 +01:00
return [];
2017-11-30 07:07:20 +01:00
}
2017-12-03 00:28:18 +01:00
2017-11-30 07:07:20 +01:00
$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);
$always_assigned_before_loop_body_vars = Context::getNewOrUpdatedVarIds($outer_context, $loop_context);
2017-11-30 07:07:20 +01:00
2018-05-07 07:26:06 +02:00
$loop_context->clauses = Algebra::simplifyCNF(
2017-11-30 07:07:20 +01:00
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
);
2017-11-30 07:07:20 +01:00
$changed_var_ids = [];
2017-11-30 07:07:20 +01:00
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;
}
2017-12-03 00:28:18 +01:00
2020-10-08 02:24:50 +02:00
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 ($always_assigned_before_loop_body_vars as $var_id) {
$loop_context->clauses = Context::filterClauses(
$var_id,
$loop_context->clauses,
null,
2018-11-11 18:01:14 +01:00
$statements_analyzer
);
}
return $always_assigned_before_loop_body_vars;
2017-11-30 07:07:20 +01:00
}
/**
* @param array<string, array<string, bool>> $assignment_map
*
*/
private static function getAssignmentMapDepth(string $first_var_id, array $assignment_map): int
2017-11-30 07:07:20 +01:00
{
$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;
}
}