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;
|
2018-07-13 05:26:08 +02:00
|
|
|
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;
|
2017-12-29 16:55:41 +01:00
|
|
|
use Psalm\Type\Reconciler;
|
2019-06-26 22:52:29 +02:00
|
|
|
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
|
|
|
|
2018-12-02 00:37:49 +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
|
|
|
|
*
|
2018-04-17 18:16:25 +02:00
|
|
|
* @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
|
|
|
|
* @param bool $is_do
|
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,
|
2017-12-05 17:05:10 +01:00
|
|
|
LoopScope $loop_scope,
|
2018-02-06 17:07:27 +01:00
|
|
|
Context &$inner_context = null,
|
|
|
|
$is_do = false
|
2017-11-30 07:07:20 +01:00
|
|
|
) {
|
|
|
|
$traverser = new PhpParser\NodeTraverser;
|
|
|
|
|
2018-11-06 03:57:36 +01:00
|
|
|
$assignment_mapper = new \Psalm\Internal\Visitor\AssignmentMapVisitor($loop_scope->loop_context->self);
|
2017-11-30 07:07:20 +01:00
|
|
|
$traverser->addVisitor($assignment_mapper);
|
|
|
|
|
2018-01-29 23:18:03 +01:00
|
|
|
$traverser->traverse(array_merge($stmts, $post_expressions));
|
2017-11-30 07:07:20 +01:00
|
|
|
|
|
|
|
$assignment_map = $assignment_mapper->getAssignmentMap();
|
|
|
|
|
|
|
|
$assignment_depth = 0;
|
|
|
|
|
2017-12-03 02:02:29 +01:00
|
|
|
$asserted_var_ids = [];
|
2017-11-30 07:07:20 +01:00
|
|
|
|
|
|
|
$pre_condition_clauses = [];
|
|
|
|
|
2017-12-17 16:58:03 +01:00
|
|
|
$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
|
|
|
|
2017-11-30 07:07:20 +01:00
|
|
|
if ($pre_conditions) {
|
|
|
|
foreach ($pre_conditions as $pre_condition) {
|
|
|
|
$pre_condition_clauses = array_merge(
|
|
|
|
$pre_condition_clauses,
|
2018-05-07 07:26:06 +02:00
|
|
|
Algebra::getFormula(
|
2019-12-08 22:35:56 +01:00
|
|
|
\spl_object_id($pre_condition),
|
2017-11-30 07:07:20 +01:00
|
|
|
$pre_condition,
|
2017-12-03 00:28:18 +01:00
|
|
|
$loop_scope->loop_context->self,
|
2018-11-11 18:01:14 +01:00
|
|
|
$statements_analyzer,
|
2018-11-06 03:57:36 +01:00
|
|
|
$codebase
|
2017-11-30 07:07:20 +01:00
|
|
|
)
|
|
|
|
);
|
|
|
|
}
|
|
|
|
} else {
|
2017-12-03 02:02:29 +01:00
|
|
|
$asserted_var_ids = 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
|
|
|
}
|
|
|
|
|
2019-11-25 17:44:54 +01:00
|
|
|
$final_actions = ScopeAnalyzer::getFinalControlActions(
|
|
|
|
$stmts,
|
|
|
|
$statements_analyzer->node_data,
|
2020-01-27 18:17:12 +01:00
|
|
|
Config::getInstance()->exit_functions,
|
|
|
|
$loop_scope->loop_context->break_types
|
2019-11-25 17:44:54 +01:00
|
|
|
);
|
|
|
|
|
2019-07-31 23:14:00 +02:00
|
|
|
$does_always_break = $final_actions === [ScopeAnalyzer::ACTION_BREAK];
|
2017-12-03 00:28:18 +01:00
|
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
2017-12-03 00:28:18 +01:00
|
|
|
$loop_scope->loop_context->parent_context = $loop_scope->loop_parent_context;
|
|
|
|
|
2019-05-13 05:31:36 +02:00
|
|
|
$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;
|
2019-06-26 06:14:06 +02:00
|
|
|
|
|
|
|
foreach ($inner_context->vars_in_scope as $context_var_id => $context_type) {
|
|
|
|
$inner_context->vars_in_scope[$context_var_id] = clone $context_type;
|
|
|
|
}
|
|
|
|
|
2018-06-17 03:54:44 +02:00
|
|
|
$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
|
|
|
|
2019-10-20 20:18:30 +02:00
|
|
|
foreach ($pre_conditions as $pre_condition) {
|
|
|
|
self::applyPreConditionToLoopContext(
|
|
|
|
$statements_analyzer,
|
|
|
|
$pre_condition,
|
|
|
|
$pre_condition_clauses,
|
|
|
|
$inner_context,
|
|
|
|
$loop_scope->loop_parent_context,
|
|
|
|
$is_do
|
|
|
|
);
|
2017-11-30 07:07:20 +01:00
|
|
|
}
|
|
|
|
|
2017-12-17 16:58:03 +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
|
|
|
$new_referenced_var_ids = $inner_context->referenced_var_ids;
|
|
|
|
$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-04-13 23:26:07 +02:00
|
|
|
|
2018-11-11 18:01:14 +01:00
|
|
|
$original_mixed_counts = $analyzer->getMixedCountsForFile($statements_analyzer->getFilePath());
|
2018-04-13 23:26:07 +02:00
|
|
|
|
2017-11-30 07:07:20 +01:00
|
|
|
IssueBuffer::startRecording();
|
|
|
|
|
2019-10-20 20:18:30 +02:00
|
|
|
foreach ($pre_conditions as $pre_condition) {
|
|
|
|
$asserted_var_ids = array_merge(
|
|
|
|
self::applyPreConditionToLoopContext(
|
|
|
|
$statements_analyzer,
|
|
|
|
$pre_condition,
|
|
|
|
$pre_condition_clauses,
|
|
|
|
$loop_scope->loop_context,
|
|
|
|
$loop_scope->loop_parent_context,
|
|
|
|
$is_do
|
|
|
|
),
|
|
|
|
$asserted_var_ids
|
|
|
|
);
|
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;
|
2019-06-26 06:14:06 +02:00
|
|
|
|
|
|
|
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;
|
2018-06-17 03:54:44 +02:00
|
|
|
$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 = [];
|
|
|
|
|
2017-12-03 02:02:29 +01:00
|
|
|
$asserted_var_ids = array_unique($asserted_var_ids);
|
2017-12-03 00:28:18 +01:00
|
|
|
|
2017-12-17 16:58:03 +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);
|
2018-05-31 00:56:44 +02:00
|
|
|
|
2017-12-03 00:28:18 +01:00
|
|
|
self::updateLoopScopeContexts($loop_scope, $pre_outer_context);
|
2017-11-30 07:07:20 +01:00
|
|
|
|
2017-12-17 16:58:03 +01:00
|
|
|
$inner_context->protected_var_ids = $original_protected_var_ids;
|
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-06-17 02:01:33 +02:00
|
|
|
/**
|
|
|
|
* @var array<string, bool>
|
|
|
|
*/
|
|
|
|
$new_referenced_var_ids = $inner_context->referenced_var_ids;
|
2019-05-21 00:25:11 +02:00
|
|
|
$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) {
|
2017-12-03 02:02:29 +01:00
|
|
|
if (in_array($var_id, $asserted_var_ids, 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])
|
2018-05-18 17:02:50 +02:00
|
|
|
|| !$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])) {
|
2018-05-18 17:02:50 +02:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
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 {
|
2018-06-01 16:52:26 +02:00
|
|
|
// 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
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-05-13 05:31:36 +02:00
|
|
|
if ($inner_context->collect_references) {
|
2019-08-12 22:01:24 +02:00
|
|
|
foreach ($inner_context->unreferenced_vars as $var_id => $locations) {
|
2019-05-13 05:31:36 +02:00
|
|
|
if (!isset($pre_outer_context->vars_in_scope[$var_id])) {
|
2019-08-12 22:01:24 +02:00
|
|
|
$loop_scope->unreferenced_vars[$var_id] = $locations;
|
2019-05-13 05:31:36 +02:00
|
|
|
unset($inner_context->unreferenced_vars[$var_id]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2018-01-28 23:28:34 +01:00
|
|
|
if ($inner_context->collect_references) {
|
2018-06-17 02:01:33 +02:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-11-30 07:07:20 +01:00
|
|
|
// 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_conditions as $pre_condition) {
|
2017-12-03 00:28:18 +01:00
|
|
|
self::applyPreConditionToLoopContext(
|
2018-11-11 18:01:14 +01:00
|
|
|
$statements_analyzer,
|
2017-11-30 07:07:20 +01:00
|
|
|
$pre_condition,
|
|
|
|
$pre_condition_clauses,
|
2017-12-03 00:28:18 +01:00
|
|
|
$inner_context,
|
2019-10-20 20:18:30 +02:00
|
|
|
$loop_scope->loop_parent_context,
|
|
|
|
false
|
2017-12-03 00:28:18 +01:00
|
|
|
);
|
2017-11-30 07:07:20 +01:00
|
|
|
}
|
|
|
|
|
2018-02-07 20:13:57 +01:00
|
|
|
foreach ($asserted_var_ids as $var_id) {
|
2018-02-07 21:20:47 +01: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
|
|
|
|
) {
|
2018-02-07 20:13:57 +01:00
|
|
|
$inner_context->vars_in_scope[$var_id] = clone $pre_loop_context->vars_in_scope[$var_id];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-12-15 23:34:21 +01:00
|
|
|
$inner_context->clauses = $pre_loop_context->clauses;
|
|
|
|
|
2017-12-17 16:58:03 +01:00
|
|
|
$inner_context->protected_var_ids = $loop_scope->protected_var_ids;
|
|
|
|
|
2018-07-06 06:36:11 +02:00
|
|
|
$traverser = new PhpParser\NodeTraverser;
|
|
|
|
|
2019-11-25 17:44:54 +01:00
|
|
|
$traverser->addVisitor(
|
|
|
|
new \Psalm\Internal\Visitor\NodeCleanerVisitor(
|
|
|
|
$statements_analyzer->node_data
|
|
|
|
)
|
|
|
|
);
|
2018-07-06 06:36:11 +02:00
|
|
|
$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
|
|
|
|
2017-12-17 16:58:03 +01:00
|
|
|
$inner_context->protected_var_ids = $original_protected_var_ids;
|
|
|
|
|
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]
|
|
|
|
);
|
|
|
|
}
|
2017-11-30 07:07:20 +01:00
|
|
|
}
|
2017-12-03 00:28:18 +01:00
|
|
|
}
|
2017-11-30 07:07:20 +01:00
|
|
|
|
2017-12-07 01:20:24 +01:00
|
|
|
foreach ($loop_scope->loop_parent_context->vars_in_scope as $var_id => $type) {
|
2019-02-18 17:39:05 +01:00
|
|
|
if (!isset($loop_scope->loop_context->vars_in_scope[$var_id])) {
|
2017-12-07 01:20:24 +01:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2018-12-08 19:18:55 +01:00
|
|
|
if ($inner_context->vars_in_scope[$var_id]->hasMixed()) {
|
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);
|
|
|
|
continue;
|
|
|
|
}
|
2017-11-30 07:07:20 +01:00
|
|
|
|
2017-12-03 18:44:08 +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);
|
|
|
|
}
|
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
|
2018-05-07 07:26:06 +02:00
|
|
|
$negated_pre_condition_types = Algebra::getTruthsFromFormula(
|
|
|
|
Algebra::negateFormula($pre_condition_clauses)
|
2017-11-30 07:07:20 +01:00
|
|
|
);
|
|
|
|
|
|
|
|
if ($negated_pre_condition_types) {
|
|
|
|
$changed_var_ids = [];
|
|
|
|
|
2017-12-29 16:55:41 +01:00
|
|
|
$vars_in_scope_reconciled = Reconciler::reconcileKeyedTypes(
|
2017-11-30 07:07:20 +01:00
|
|
|
$negated_pre_condition_types,
|
2019-12-08 22:35:56 +01:00
|
|
|
[],
|
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,
|
2019-01-23 05:42:54 +01:00
|
|
|
[],
|
2018-12-08 19:18:55 +01:00
|
|
|
true,
|
2018-11-14 19:44:20 +01:00
|
|
|
new CodeLocation($statements_analyzer->getSource(), $pre_conditions[0])
|
2017-11-30 07:07:20 +01:00
|
|
|
);
|
|
|
|
|
2019-12-07 07:23:35 +01:00
|
|
|
foreach ($changed_var_ids as $var_id => $_) {
|
2017-12-11 18:03:50 +01:00
|
|
|
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
|
|
|
}
|
|
|
|
|
2017-12-11 18:03:50 +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(
|
2019-05-13 05:31:36 +02:00
|
|
|
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
|
|
|
|
);
|
2018-01-25 07:04:26 +01:00
|
|
|
|
|
|
|
if ($inner_context->collect_references) {
|
2018-06-17 02:01:33 +02:00
|
|
|
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) {
|
2019-05-13 05:31:36 +02:00
|
|
|
if (!isset($new_referenced_var_ids[$var_id])
|
|
|
|
|| !isset($pre_outer_context->vars_in_scope[$var_id])
|
2019-07-31 23:14:00 +02:00
|
|
|
|| $does_always_break
|
2019-05-13 05:31:36 +02:00
|
|
|
) {
|
2018-06-17 02:01:33 +02:00
|
|
|
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 {
|
2018-11-11 18:01:14 +01:00
|
|
|
$statements_analyzer->registerVariableUses($locations);
|
2018-06-17 02:01:33 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
foreach ($loop_scope->unreferenced_vars as $var_id => $locations) {
|
2020-01-03 16:56:47 +01:00
|
|
|
if (isset($loop_scope->referenced_var_ids[$var_id])) {
|
|
|
|
$statements_analyzer->registerVariableUses($locations);
|
2018-06-17 02:01:33 +02:00
|
|
|
} else {
|
2020-01-03 16:56:47 +01:00
|
|
|
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;
|
|
|
|
}
|
2018-01-28 23:28:34 +01:00
|
|
|
}
|
|
|
|
}
|
2018-01-25 07:04:26 +01:00
|
|
|
}
|
2017-12-03 00:28:18 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @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 = [];
|
|
|
|
|
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) {
|
2018-01-28 23:28:34 +01:00
|
|
|
if ($loop_scope->loop_context->hasVariable($var)
|
|
|
|
&& !isset($updated_loop_vars[$var])
|
|
|
|
) {
|
2017-12-03 00:28:18 +01:00
|
|
|
$loop_scope->loop_context->vars_in_scope[$var] = Type::combineUnionTypes(
|
|
|
|
$loop_scope->loop_context->vars_in_scope[$var],
|
|
|
|
$type
|
|
|
|
);
|
|
|
|
}
|
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 PhpParser\Node\Expr $pre_condition
|
|
|
|
* @param array<int, Clause> $pre_condition_clauses
|
|
|
|
* @param Context $loop_context
|
|
|
|
* @param Context $outer_context
|
|
|
|
*
|
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,
|
2019-10-20 20:18:30 +02:00
|
|
|
Context $outer_context,
|
|
|
|
bool $is_do
|
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
|
|
|
|
2019-10-20 20:18:30 +02:00
|
|
|
$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']);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
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);
|
|
|
|
|
2017-12-03 02:02:29 +01:00
|
|
|
$asserted_var_ids = 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)
|
|
|
|
);
|
|
|
|
|
2019-12-08 22:35:56 +01:00
|
|
|
$active_while_types = [];
|
|
|
|
|
2018-07-03 18:27:14 +02:00
|
|
|
$reconcilable_while_types = Algebra::getTruthsFromFormula(
|
|
|
|
$loop_context->clauses,
|
2019-12-08 22:35:56 +01:00
|
|
|
\spl_object_id($pre_condition),
|
2018-07-03 18:27:14 +02:00
|
|
|
$new_referenced_var_ids
|
|
|
|
);
|
2017-11-30 07:07:20 +01:00
|
|
|
|
2018-07-03 05:46:51 +02:00
|
|
|
$changed_var_ids = [];
|
2017-11-30 07:07:20 +01:00
|
|
|
|
2018-12-17 21:23:56 +01:00
|
|
|
if ($reconcilable_while_types) {
|
|
|
|
$pre_condition_vars_in_scope_reconciled = Reconciler::reconcileKeyedTypes(
|
|
|
|
$reconcilable_while_types,
|
2019-12-08 22:35:56 +01:00
|
|
|
$active_while_types,
|
2018-12-17 21:23:56 +01:00
|
|
|
$loop_context->vars_in_scope,
|
|
|
|
$changed_var_ids,
|
|
|
|
$new_referenced_var_ids,
|
|
|
|
$statements_analyzer,
|
2019-01-23 05:42:54 +01:00
|
|
|
[],
|
2018-12-17 21:23:56 +01:00
|
|
|
true,
|
|
|
|
new CodeLocation($statements_analyzer->getSource(), $pre_condition)
|
|
|
|
);
|
2018-07-03 05:46:51 +02:00
|
|
|
|
2018-12-17 21:23:56 +01:00
|
|
|
$loop_context->vars_in_scope = $pre_condition_vars_in_scope_reconciled;
|
|
|
|
}
|
2017-12-03 00:28:18 +01:00
|
|
|
|
2019-10-20 20:18:30 +02:00
|
|
|
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 [];
|
|
|
|
}
|
|
|
|
|
2017-12-03 02:02:29 +01:00
|
|
|
foreach ($asserted_var_ids as $var_id) {
|
|
|
|
$loop_context->clauses = Context::filterClauses(
|
|
|
|
$var_id,
|
|
|
|
$loop_context->clauses,
|
|
|
|
null,
|
2018-11-11 18:01:14 +01:00
|
|
|
$statements_analyzer
|
2017-12-03 02:02:29 +01:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
return $asserted_var_ids;
|
2017-11-30 07:07:20 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param string $first_var_id
|
|
|
|
* @param array<string, array<string, bool>> $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;
|
|
|
|
}
|
|
|
|
}
|