1
0
mirror of https://github.com/danog/psalm.git synced 2025-01-08 05:58:38 +01:00
psalm/src/Psalm/Internal/Analyzer/Statements/Block/IfElseAnalyzer.php

1931 lines
71 KiB
PHP
Raw Normal View History

2016-10-22 19:23:18 +02:00
<?php
2018-11-06 03:57:36 +01:00
namespace Psalm\Internal\Analyzer\Statements\Block;
2016-10-22 19:23:18 +02:00
2016-11-02 07:29:00 +01:00
use PhpParser;
2018-11-06 03:57:36 +01:00
use Psalm\Codebase;
2020-11-03 22:15:44 +01:00
use Psalm\Internal\Algebra\FormulaGenerator;
2018-11-06 03:57:36 +01:00
use Psalm\Internal\Analyzer\AlgebraAnalyzer;
use Psalm\Internal\Analyzer\ScopeAnalyzer;
use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer;
use Psalm\Internal\Analyzer\StatementsAnalyzer;
2020-07-22 01:40:35 +02:00
use Psalm\Internal\Type\Comparator\UnionTypeComparator;
2018-11-06 03:57:36 +01:00
use Psalm\Internal\Clause;
use Psalm\CodeLocation;
2016-10-22 19:23:18 +02:00
use Psalm\Context;
use Psalm\Issue\ConflictingReferenceConstraint;
use Psalm\Issue\DocblockTypeContradiction;
use Psalm\Issue\RedundantConditionGivenDocblockType;
use Psalm\Issue\TypeDoesNotContainType;
use Psalm\Issue\RedundantCondition;
use Psalm\IssueBuffer;
2018-11-06 03:57:36 +01:00
use Psalm\Internal\Scope\IfScope;
use Psalm\Internal\Scope\IfConditionalScope;
2016-10-22 19:23:18 +02:00
use Psalm\Type;
2020-11-03 22:15:44 +01:00
use Psalm\Internal\Algebra;
use Psalm\Type\Reconciler;
use function array_merge;
use function array_map;
use function array_diff_key;
use function array_filter;
use function array_values;
use function array_keys;
use function array_reduce;
use function array_combine;
use function preg_match;
use function preg_quote;
use function array_unique;
use function count;
use function in_array;
use function array_intersect;
use function strpos;
use function substr;
use function array_intersect_key;
2016-10-22 19:23:18 +02:00
/**
* @internal
*/
2020-11-07 02:51:14 +01:00
class IfElseAnalyzer
2016-10-22 19:23:18 +02:00
{
/**
* System of type substitution and deletion
*
* for example
*
* x: A|null
*
* if (x)
* (x: A)
* x = B -- effects: remove A from the type of x, add B
* else
* (x: null)
* x = C -- effects: remove null from the type of x, add C
*
*
* x: A|null
*
* if (!x)
* (x: null)
* throw new Exception -- effects: remove null from the type of x
*
2017-05-27 02:16:18 +02:00
*
2016-10-22 19:23:18 +02:00
* @return null|false
*/
public static function analyze(
2018-11-11 18:01:14 +01:00
StatementsAnalyzer $statements_analyzer,
2016-11-02 07:29:00 +01:00
PhpParser\Node\Stmt\If_ $stmt,
Context $context
): ?bool {
2018-11-11 18:01:14 +01:00
$codebase = $statements_analyzer->getCodebase();
2018-11-06 03:57:36 +01:00
$if_scope = new IfScope();
// We need to clone the original context for later use if we're exiting in this if conditional
if (!$stmt->else && !$stmt->elseifs
&& ($stmt->cond instanceof PhpParser\Node\Expr\BinaryOp
|| ($stmt->cond instanceof PhpParser\Node\Expr\BooleanNot
&& $stmt->cond->expr instanceof PhpParser\Node\Expr\BinaryOp))
) {
$final_actions = ScopeAnalyzer::getControlActions(
$stmt->stmts,
null,
$codebase->config->exit_functions,
$context->break_types
);
$has_leaving_statements = $final_actions === [ScopeAnalyzer::ACTION_END]
|| (count($final_actions) && !in_array(ScopeAnalyzer::ACTION_NONE, $final_actions, true));
if ($has_leaving_statements) {
$if_scope->mic_drop_context = clone $context;
}
}
2019-08-27 04:16:06 +02:00
try {
$if_conditional_scope = self::analyzeIfConditional(
$statements_analyzer,
$stmt->cond,
2019-08-27 04:16:06 +02:00
$context,
$codebase,
$if_scope,
$context->branch_point ?: (int) $stmt->getAttribute('startFilePos')
2018-06-07 21:04:16 +02:00
);
2019-08-27 04:16:06 +02:00
$if_context = $if_conditional_scope->if_context;
2019-12-08 06:49:34 +01:00
2019-08-27 04:16:06 +02:00
$original_context = $if_conditional_scope->original_context;
$cond_referenced_var_ids = $if_conditional_scope->cond_referenced_var_ids;
$cond_assigned_var_ids = $if_conditional_scope->cond_assigned_var_ids;
} catch (\Psalm\Exception\ScopeAnalysisException $e) {
return false;
2016-10-22 19:23:18 +02:00
}
$mixed_var_ids = [];
foreach ($if_context->vars_in_scope as $var_id => $type) {
2019-10-17 07:09:21 +02:00
if ($type->hasMixed() && isset($context->vars_in_scope[$var_id])) {
$mixed_var_ids[] = $var_id;
}
}
2020-08-26 21:35:29 +02:00
$cond_object_id = \spl_object_id($stmt->cond);
2020-11-03 22:15:44 +01:00
$if_clauses = FormulaGenerator::getFormula(
2020-08-26 21:35:29 +02:00
$cond_object_id,
$cond_object_id,
$stmt->cond,
2017-01-07 20:35:07 +01:00
$context->self,
2018-11-11 18:01:14 +01:00
$statements_analyzer,
2018-11-06 03:57:36 +01:00
$codebase
);
2016-10-22 19:23:18 +02:00
if (count($if_clauses) > 200) {
$if_clauses = [];
}
$if_clauses = array_values(
array_map(
/**
* @return Clause
*/
function (Clause $c) use ($mixed_var_ids, $cond_object_id): Clause {
$keys = array_keys($c->possibilities);
2019-11-12 16:21:27 +01:00
$mixed_var_ids = \array_diff($mixed_var_ids, $keys);
foreach ($keys as $key) {
foreach ($mixed_var_ids as $mixed_var_id) {
if (preg_match('/^' . preg_quote($mixed_var_id, '/') . '(\[|-)/', $key)) {
2020-08-26 21:35:29 +02:00
return new Clause([], $cond_object_id, $cond_object_id, true);
}
}
}
return $c;
},
$if_clauses
)
);
2019-12-08 06:49:34 +01:00
$entry_clauses = $context->clauses;
2017-04-02 21:26:10 +02:00
// this will see whether any of the clauses in set A conflict with the clauses in set B
2018-11-06 03:57:36 +01:00
AlgebraAnalyzer::checkForParadox(
$context->clauses,
$if_clauses,
2018-11-11 18:01:14 +01:00
$statements_analyzer,
$stmt->cond,
$cond_assigned_var_ids
);
// if we have assignments in the if, we may have duplicate clauses
if ($cond_assigned_var_ids) {
2018-05-07 07:26:06 +02:00
$if_clauses = Algebra::simplifyCNF($if_clauses);
}
2017-04-02 21:26:10 +02:00
2019-12-08 06:49:34 +01:00
$if_context_clauses = array_merge($entry_clauses, $if_clauses);
$if_context->clauses = Algebra::simplifyCNF($if_context_clauses);
if ($if_context->reconciled_expression_clauses) {
$reconciled_expression_clauses = $if_context->reconciled_expression_clauses;
$if_context->clauses = array_values(
array_filter(
$if_context->clauses,
function ($c) use ($reconciled_expression_clauses): bool {
return !in_array($c->hash, $reconciled_expression_clauses);
2019-12-08 06:49:34 +01:00
}
)
);
if (count($if_context->clauses) === 1
&& $if_context->clauses[0]->wedge
&& !$if_context->clauses[0]->possibilities
) {
$if_context->clauses = [];
$if_context->reconciled_expression_clauses = [];
}
2019-12-08 06:49:34 +01:00
}
2016-10-22 19:23:18 +02:00
// define this before we alter local claues after reconciliation
$if_scope->reasonable_clauses = $if_context->clauses;
2019-01-08 15:57:14 +01:00
try {
$if_scope->negated_clauses = Algebra::negateFormula($if_clauses);
} catch (\Psalm\Exception\ComplicatedExpressionException $e) {
try {
2020-11-03 22:15:44 +01:00
$if_scope->negated_clauses = FormulaGenerator::getFormula(
$cond_object_id,
$cond_object_id,
new PhpParser\Node\Expr\BooleanNot($stmt->cond),
$context->self,
$statements_analyzer,
$codebase,
false
);
} catch (\Psalm\Exception\ComplicatedExpressionException $e) {
$if_scope->negated_clauses = [];
}
2019-01-08 15:57:14 +01:00
}
2018-05-07 07:26:06 +02:00
$if_scope->negated_types = Algebra::getTruthsFromFormula(
Algebra::simplifyCNF(
2018-05-06 02:52:10 +02:00
array_merge($context->clauses, $if_scope->negated_clauses)
)
);
2016-10-22 19:23:18 +02:00
$active_if_types = [];
$reconcilable_if_types = Algebra::getTruthsFromFormula(
$if_context->clauses,
\spl_object_id($stmt->cond),
$cond_referenced_var_ids,
$active_if_types
);
2016-10-22 19:23:18 +02:00
if (array_filter(
$context->clauses,
function ($clause): bool {
return !!$clause->possibilities;
}
)) {
$omit_keys = array_reduce(
$context->clauses,
/**
* @param array<string> $carry
* @return array<string>
*/
function (array $carry, Clause $clause): array {
return array_merge($carry, array_keys($clause->possibilities));
},
[]
);
$omit_keys = array_combine($omit_keys, $omit_keys);
$omit_keys = array_diff_key($omit_keys, Algebra::getTruthsFromFormula($context->clauses));
$cond_referenced_var_ids = array_diff_key(
$cond_referenced_var_ids,
$omit_keys
);
}
2016-10-22 19:23:18 +02:00
// if the if has an || in the conditional, we cannot easily reason about it
if ($reconcilable_if_types) {
$changed_var_ids = [];
2016-10-22 19:23:18 +02:00
$if_vars_in_scope_reconciled =
Reconciler::reconcileKeyedTypes(
2016-10-22 19:23:18 +02:00
$reconcilable_if_types,
$active_if_types,
2016-10-22 19:23:18 +02:00
$if_context->vars_in_scope,
$changed_var_ids,
$cond_referenced_var_ids,
2018-11-11 18:01:14 +01:00
$statements_analyzer,
$statements_analyzer->getTemplateTypeMap() ?: [],
$if_context->inside_loop,
$context->check_variables
? new CodeLocation(
2018-11-11 18:01:14 +01:00
$statements_analyzer->getSource(),
$stmt->cond instanceof PhpParser\Node\Expr\BooleanNot
? $stmt->cond->expr
: $stmt->cond,
$context->include_location
) : null
2016-10-22 19:23:18 +02:00
);
$if_context->vars_in_scope = $if_vars_in_scope_reconciled;
foreach ($reconcilable_if_types as $var_id => $_) {
2017-12-02 19:32:20 +01:00
$if_context->vars_possibly_in_scope[$var_id] = true;
}
if ($changed_var_ids) {
2019-12-08 06:49:34 +01:00
$if_context->clauses = Context::removeReconciledClauses($if_context->clauses, $changed_var_ids)[0];
}
2018-01-20 17:48:16 +01:00
$if_scope->if_cond_changed_var_ids = $changed_var_ids;
2016-10-22 19:23:18 +02:00
}
2020-11-03 22:44:24 +01:00
$if_context->reconciled_expression_clauses = [];
2016-10-22 19:23:18 +02:00
$old_if_context = clone $if_context;
2016-11-02 07:29:00 +01:00
$context->vars_possibly_in_scope = array_merge(
$if_context->vars_possibly_in_scope,
$context->vars_possibly_in_scope
);
2016-10-22 19:23:18 +02:00
$context->referenced_var_ids = array_merge(
$if_context->referenced_var_ids,
$context->referenced_var_ids
);
$temp_else_context = clone $original_context;
2016-10-22 19:23:18 +02:00
$changed_var_ids = [];
if ($if_scope->negated_types) {
$else_vars_reconciled = Reconciler::reconcileKeyedTypes(
$if_scope->negated_types,
[],
$temp_else_context->vars_in_scope,
$changed_var_ids,
[],
2018-11-11 18:01:14 +01:00
$statements_analyzer,
$statements_analyzer->getTemplateTypeMap() ?: [],
$context->inside_loop,
$context->check_variables
? new CodeLocation(
2018-11-11 18:01:14 +01:00
$statements_analyzer->getSource(),
$stmt->cond instanceof PhpParser\Node\Expr\BooleanNot
? $stmt->cond->expr
: $stmt->cond,
$context->include_location
) : null
2016-10-22 19:23:18 +02:00
);
$temp_else_context->vars_in_scope = $else_vars_reconciled;
2016-10-22 19:23:18 +02:00
}
// we calculate the vars redefined in a hypothetical else statement to determine
// which vars of the if we can safely change
$pre_assignment_else_redefined_vars = array_intersect_key(
$temp_else_context->getRedefinedVars($context->vars_in_scope, true),
$changed_var_ids
);
// check the if
if (self::analyzeIfBlock(
2018-11-11 18:01:14 +01:00
$statements_analyzer,
$stmt,
$if_scope,
$if_conditional_scope,
$if_context,
$old_if_context,
$context,
$pre_assignment_else_redefined_vars
) === false) {
return false;
}
2016-10-22 19:23:18 +02:00
2019-08-27 04:16:06 +02:00
// check the else
$else_context = clone $original_context;
// check the elseifs
foreach ($stmt->elseifs as $elseif) {
if (self::analyzeElseIfBlock(
2018-11-11 18:01:14 +01:00
$statements_analyzer,
$elseif,
$if_scope,
2019-08-27 04:16:06 +02:00
$else_context,
2018-11-06 03:57:36 +01:00
$context,
2019-08-27 04:16:06 +02:00
$codebase,
$else_context->branch_point ?: (int) $stmt->getAttribute('startFilePos')
) === false) {
return false;
}
2016-10-22 19:23:18 +02:00
}
if ($stmt->else) {
2018-11-06 03:57:36 +01:00
if ($codebase->alter_code) {
$else_context->branch_point =
$else_context->branch_point ?: (int) $stmt->getAttribute('startFilePos');
}
}
if (self::analyzeElseBlock(
2018-11-11 18:01:14 +01:00
$statements_analyzer,
$stmt->else,
$if_scope,
$else_context,
$context
) === false) {
return false;
}
if ($context->loop_scope) {
$context->loop_scope->final_actions = array_unique(
2017-12-03 00:28:18 +01:00
array_merge(
$context->loop_scope->final_actions,
2017-12-03 00:28:18 +01:00
$if_scope->final_actions
)
);
}
2016-10-22 19:23:18 +02:00
$context->vars_possibly_in_scope = array_merge(
$context->vars_possibly_in_scope,
$if_scope->new_vars_possibly_in_scope
);
2016-10-22 19:23:18 +02:00
$context->possibly_assigned_var_ids = array_merge(
$context->possibly_assigned_var_ids,
$if_scope->possibly_assigned_var_ids ?: []
);
// vars can only be defined/redefined if there was an else (defined in every block)
$context->assigned_var_ids = array_merge(
$context->assigned_var_ids,
$if_scope->assigned_var_ids ?: []
);
if ($if_scope->new_vars) {
foreach ($if_scope->new_vars as $var_id => $type) {
if (isset($context->vars_possibly_in_scope[$var_id])
&& $statements_analyzer->data_flow_graph
) {
$type->parent_nodes += $statements_analyzer->getParentNodesForPossiblyUndefinedVariable($var_id);
}
$context->vars_in_scope[$var_id] = $type;
}
}
2016-10-22 19:23:18 +02:00
if ($if_scope->redefined_vars) {
foreach ($if_scope->redefined_vars as $var_id => $type) {
$context->vars_in_scope[$var_id] = $type;
$if_scope->updated_vars[$var_id] = true;
2016-10-22 19:23:18 +02:00
if ($if_scope->reasonable_clauses) {
$if_scope->reasonable_clauses = Context::filterClauses(
$var_id,
$if_scope->reasonable_clauses,
isset($context->vars_in_scope[$var_id])
? $context->vars_in_scope[$var_id]
: null,
2018-11-11 18:01:14 +01:00
$statements_analyzer
);
}
}
}
2018-05-14 22:29:51 +02:00
if ($if_scope->possible_param_types) {
foreach ($if_scope->possible_param_types as $var => $type) {
$context->possible_param_types[$var] = $type;
2017-12-03 00:28:18 +01:00
}
}
2016-10-22 19:23:18 +02:00
if ($if_scope->reasonable_clauses
&& (count($if_scope->reasonable_clauses) > 1 || !$if_scope->reasonable_clauses[0]->wedge)
) {
$context->clauses = Algebra::simplifyCNF(
array_merge(
$if_scope->reasonable_clauses,
$context->clauses
)
);
}
if ($if_scope->possibly_redefined_vars) {
foreach ($if_scope->possibly_redefined_vars as $var_id => $type) {
if (isset($context->vars_in_scope[$var_id])) {
if (!$type->failed_reconciliation
&& !isset($if_scope->updated_vars[$var_id])
) {
$combined_type = Type::combineUnionTypes(
$context->vars_in_scope[$var_id],
$type,
$codebase
);
2018-05-14 22:29:51 +02:00
if (!$combined_type->equals($context->vars_in_scope[$var_id])) {
$context->removeDescendents($var_id, $combined_type);
}
2016-10-22 19:23:18 +02:00
$context->vars_in_scope[$var_id] = $combined_type;
2018-06-17 02:01:33 +02:00
} else {
$context->vars_in_scope[$var_id]->parent_nodes += $type->parent_nodes;
2018-06-17 02:01:33 +02:00
}
}
}
}
$context->possibly_assigned_var_ids += $if_scope->possibly_assigned_var_ids;
if (!in_array(ScopeAnalyzer::ACTION_NONE, $if_scope->final_actions, true)) {
$context->has_returned = true;
}
return null;
}
2016-10-22 19:23:18 +02:00
public static function analyzeIfConditional(
2019-08-27 04:16:06 +02:00
StatementsAnalyzer $statements_analyzer,
PhpParser\Node\Expr $cond,
Context $outer_context,
Codebase $codebase,
IfScope $if_scope,
?int $branch_point
): IfConditionalScope {
$entry_clauses = [];
2019-12-08 06:49:34 +01:00
// used when evaluating elseifs
if ($if_scope->negated_clauses) {
$entry_clauses = array_merge($outer_context->clauses, $if_scope->negated_clauses);
$changed_var_ids = [];
if ($if_scope->negated_types) {
$vars_reconciled = Reconciler::reconcileKeyedTypes(
$if_scope->negated_types,
[],
$outer_context->vars_in_scope,
$changed_var_ids,
[],
$statements_analyzer,
[],
$outer_context->inside_loop,
new CodeLocation(
$statements_analyzer->getSource(),
$cond instanceof PhpParser\Node\Expr\BooleanNot
? $cond->expr
: $cond,
$outer_context->include_location,
false
)
);
if ($changed_var_ids) {
$outer_context = clone $outer_context;
$outer_context->vars_in_scope = $vars_reconciled;
$entry_clauses = array_values(
array_filter(
$entry_clauses,
function (Clause $c) use ($changed_var_ids): bool {
return count($c->possibilities) > 1
|| $c->wedge
|| !isset($changed_var_ids[array_keys($c->possibilities)[0]]);
}
)
);
}
}
}
2019-08-27 04:16:06 +02:00
2019-12-08 06:49:34 +01:00
// get the first expression in the if, which should be evaluated on its own
// this allows us to update the context of $matches in
// if (!preg_match('/a/', 'aa', $matches)) {
// exit
// }
// echo $matches[0];
$externally_applied_if_cond_expr = self::getDefinitelyEvaluatedExpressionAfterIf($cond);
$internally_applied_if_cond_expr = self::getDefinitelyEvaluatedExpressionInsideIf($cond);
$was_inside_conditional = $outer_context->inside_conditional;
$outer_context->inside_conditional = true;
2019-08-27 04:16:06 +02:00
$pre_condition_vars_in_scope = $outer_context->vars_in_scope;
2019-08-27 04:16:06 +02:00
$referenced_var_ids = $outer_context->referenced_var_ids;
$outer_context->referenced_var_ids = [];
2019-08-27 04:16:06 +02:00
$pre_assigned_var_ids = $outer_context->assigned_var_ids;
$outer_context->assigned_var_ids = [];
2019-08-27 04:16:06 +02:00
2019-12-08 06:49:34 +01:00
$if_context = null;
if ($internally_applied_if_cond_expr !== $externally_applied_if_cond_expr) {
$if_context = clone $outer_context;
}
if ($externally_applied_if_cond_expr) {
if (ExpressionAnalyzer::analyze(
$statements_analyzer,
$externally_applied_if_cond_expr,
$outer_context
) === false) {
2019-08-27 04:16:06 +02:00
throw new \Psalm\Exception\ScopeAnalysisException();
}
}
$first_cond_assigned_var_ids = $outer_context->assigned_var_ids;
$outer_context->assigned_var_ids = array_merge(
2019-08-27 04:16:06 +02:00
$pre_assigned_var_ids,
$first_cond_assigned_var_ids
);
$first_cond_referenced_var_ids = $outer_context->referenced_var_ids;
$outer_context->referenced_var_ids = array_merge(
2019-08-27 04:16:06 +02:00
$referenced_var_ids,
$first_cond_referenced_var_ids
);
if (!$was_inside_conditional) {
$outer_context->inside_conditional = false;
}
2019-08-27 04:16:06 +02:00
2019-12-08 06:49:34 +01:00
if (!$if_context) {
$if_context = clone $outer_context;
}
$if_conditional_context = clone $if_context;
$if_conditional_context->if_context = $if_context;
$if_conditional_context->if_scope = $if_scope;
2019-08-27 04:16:06 +02:00
if ($codebase->alter_code) {
$if_context->branch_point = $branch_point;
2019-08-27 04:16:06 +02:00
}
2019-12-07 07:38:24 +01:00
// we need to clone the current context so our ongoing updates
// to $outer_context don't mess with elseif/else blocks
$original_context = clone $outer_context;
2019-08-27 04:16:06 +02:00
2019-12-08 06:49:34 +01:00
if ($internally_applied_if_cond_expr !== $cond
|| $externally_applied_if_cond_expr !== $cond
) {
2020-10-18 00:44:42 +02:00
$assigned_var_ids = $first_cond_assigned_var_ids;
2019-12-08 06:49:34 +01:00
$if_conditional_context->assigned_var_ids = [];
2019-08-27 04:16:06 +02:00
2020-10-18 00:44:42 +02:00
$referenced_var_ids = $first_cond_referenced_var_ids;
2019-12-08 06:49:34 +01:00
$if_conditional_context->referenced_var_ids = [];
2019-08-27 04:16:06 +02:00
$if_conditional_context->inside_conditional = true;
2019-12-08 06:49:34 +01:00
if (ExpressionAnalyzer::analyze($statements_analyzer, $cond, $if_conditional_context) === false) {
2019-08-27 04:16:06 +02:00
throw new \Psalm\Exception\ScopeAnalysisException();
}
$if_conditional_context->inside_conditional = false;
2019-08-27 04:16:06 +02:00
/** @var array<string, bool> */
2019-12-08 06:49:34 +01:00
$more_cond_referenced_var_ids = $if_conditional_context->referenced_var_ids;
$if_conditional_context->referenced_var_ids = array_merge(
2019-08-27 04:16:06 +02:00
$more_cond_referenced_var_ids,
$referenced_var_ids
);
$cond_referenced_var_ids = array_merge(
$first_cond_referenced_var_ids,
$more_cond_referenced_var_ids
);
/** @var array<string, int> */
2019-12-08 06:49:34 +01:00
$more_cond_assigned_var_ids = $if_conditional_context->assigned_var_ids;
$if_conditional_context->assigned_var_ids = array_merge(
2019-08-27 04:16:06 +02:00
$more_cond_assigned_var_ids,
$assigned_var_ids
);
$cond_assigned_var_ids = array_merge(
$first_cond_assigned_var_ids,
$more_cond_assigned_var_ids
);
} else {
$cond_referenced_var_ids = $first_cond_referenced_var_ids;
$cond_assigned_var_ids = $first_cond_assigned_var_ids;
}
$newish_var_ids = array_map(
/**
* @param Type\Union $_
*
* @return true
*/
function (Type\Union $_): bool {
2019-08-27 04:16:06 +02:00
return true;
},
array_diff_key(
2019-12-08 06:49:34 +01:00
$if_conditional_context->vars_in_scope,
2019-08-27 04:16:06 +02:00
$pre_condition_vars_in_scope,
$cond_referenced_var_ids,
$cond_assigned_var_ids
)
);
$cond_type = $statements_analyzer->node_data->getType($cond);
if ($cond_type !== null) {
if ($cond_type->isFalse()) {
if ($cond_type->from_docblock) {
if (IssueBuffer::accepts(
new DocblockTypeContradiction(
'if (false) is impossible',
2020-09-11 04:44:35 +02:00
new CodeLocation($statements_analyzer, $cond),
2020-09-14 04:39:03 +02:00
'false falsy'
),
$statements_analyzer->getSuppressedIssues()
)) {
// fall through
}
} else {
if (IssueBuffer::accepts(
new TypeDoesNotContainType(
'if (false) is impossible',
2020-09-11 04:44:35 +02:00
new CodeLocation($statements_analyzer, $cond),
2020-09-14 04:39:03 +02:00
'false falsy'
),
$statements_analyzer->getSuppressedIssues()
)) {
// fall through
}
}
} elseif ($cond_type->isTrue()) {
if ($cond_type->from_docblock) {
if (IssueBuffer::accepts(
new RedundantConditionGivenDocblockType(
'if (true) is redundant',
2020-09-11 04:44:35 +02:00
new CodeLocation($statements_analyzer, $cond),
2020-09-14 04:39:03 +02:00
'true falsy'
),
$statements_analyzer->getSuppressedIssues()
)) {
// fall through
}
} else {
if (IssueBuffer::accepts(
new RedundantCondition(
'if (true) is redundant',
2020-09-11 04:44:35 +02:00
new CodeLocation($statements_analyzer, $cond),
2020-09-14 04:39:03 +02:00
'true falsy'
),
$statements_analyzer->getSuppressedIssues()
)) {
// fall through
}
}
}
}
2019-08-27 04:16:06 +02:00
// get all the var ids that were referened in the conditional, but not assigned in it
$cond_referenced_var_ids = array_diff_key($cond_referenced_var_ids, $cond_assigned_var_ids);
$cond_referenced_var_ids = array_merge($newish_var_ids, $cond_referenced_var_ids);
return new \Psalm\Internal\Scope\IfConditionalScope(
$if_context,
$original_context,
$cond_referenced_var_ids,
$cond_assigned_var_ids,
$entry_clauses
2019-08-27 04:16:06 +02:00
);
}
/**
* @param array<string,Type\Union> $pre_assignment_else_redefined_vars
2017-05-27 02:16:18 +02:00
*
* @return false|null
*/
protected static function analyzeIfBlock(
2018-11-11 18:01:14 +01:00
StatementsAnalyzer $statements_analyzer,
PhpParser\Node\Stmt\If_ $stmt,
IfScope $if_scope,
IfConditionalScope $if_conditional_scope,
Context $if_context,
Context $old_if_context,
Context $outer_context,
array $pre_assignment_else_redefined_vars
): ?bool {
2018-11-11 18:01:14 +01:00
$codebase = $statements_analyzer->getCodebase();
2019-01-03 00:14:47 +01:00
$if_context->parent_context = $outer_context;
$assigned_var_ids = $if_context->assigned_var_ids;
$possibly_assigned_var_ids = $if_context->possibly_assigned_var_ids;
$if_context->assigned_var_ids = [];
$if_context->possibly_assigned_var_ids = [];
if ($statements_analyzer->analyze(
$stmt->stmts,
$if_context
) === false
) {
return false;
}
$final_actions = ScopeAnalyzer::getControlActions(
$stmt->stmts,
$statements_analyzer->node_data,
2018-11-06 03:57:36 +01:00
$codebase->config->exit_functions,
$outer_context->break_types
);
2016-10-22 19:23:18 +02:00
2018-11-06 03:57:36 +01:00
$has_ending_statements = $final_actions === [ScopeAnalyzer::ACTION_END];
$has_leaving_statements = $has_ending_statements
2018-11-06 03:57:36 +01:00
|| (count($final_actions) && !in_array(ScopeAnalyzer::ACTION_NONE, $final_actions, true));
2018-11-06 03:57:36 +01:00
$has_break_statement = $final_actions === [ScopeAnalyzer::ACTION_BREAK];
$has_continue_statement = $final_actions === [ScopeAnalyzer::ACTION_CONTINUE];
2017-12-03 00:28:18 +01:00
$if_scope->final_actions = $final_actions;
/** @var array<string, int> */
$new_assigned_var_ids = $if_context->assigned_var_ids;
/** @var array<string, bool> */
$new_possibly_assigned_var_ids = $if_context->possibly_assigned_var_ids;
$if_context->assigned_var_ids = array_merge($assigned_var_ids, $new_assigned_var_ids);
$if_context->possibly_assigned_var_ids = array_merge(
$possibly_assigned_var_ids,
$new_possibly_assigned_var_ids
);
foreach ($if_context->byref_constraints as $var_id => $byref_constraint) {
if (isset($outer_context->byref_constraints[$var_id])
&& $byref_constraint->type
&& ($outer_constraint_type = $outer_context->byref_constraints[$var_id]->type)
&& !UnionTypeComparator::isContainedBy(
$codebase,
$byref_constraint->type,
$outer_constraint_type
)
) {
if (IssueBuffer::accepts(
new ConflictingReferenceConstraint(
'There is more than one pass-by-reference constraint on ' . $var_id,
new CodeLocation($statements_analyzer, $stmt, $outer_context->include_location, true)
),
$statements_analyzer->getSuppressedIssues()
)) {
// fall through
}
} else {
$outer_context->byref_constraints[$var_id] = $byref_constraint;
}
}
$mic_drop = false;
2016-11-02 07:29:00 +01:00
if (!$has_leaving_statements) {
$if_scope->new_vars = array_diff_key($if_context->vars_in_scope, $outer_context->vars_in_scope);
2017-12-03 00:28:18 +01:00
$if_scope->redefined_vars = $if_context->getRedefinedVars($outer_context->vars_in_scope);
$if_scope->possibly_redefined_vars = $if_scope->redefined_vars;
$if_scope->assigned_var_ids = $new_assigned_var_ids;
$if_scope->possibly_assigned_var_ids = $new_possibly_assigned_var_ids;
$changed_var_ids = $new_assigned_var_ids;
2018-01-20 17:48:16 +01:00
// if the variable was only set in the conditional, it's not possibly redefined
foreach ($if_scope->possibly_redefined_vars as $var_id => $_) {
if (!isset($new_possibly_assigned_var_ids[$var_id])
&& isset($if_scope->if_cond_changed_var_ids[$var_id])
2018-01-20 17:48:16 +01:00
) {
unset($if_scope->possibly_redefined_vars[$var_id]);
}
}
if ($if_scope->reasonable_clauses) {
// remove all reasonable clauses that would be negated by the if stmts
foreach ($changed_var_ids as $var_id => $_) {
$if_scope->reasonable_clauses = Context::filterClauses(
$var_id,
$if_scope->reasonable_clauses,
isset($if_context->vars_in_scope[$var_id]) ? $if_context->vars_in_scope[$var_id] : null,
2018-11-11 18:01:14 +01:00
$statements_analyzer
);
}
}
} else {
2017-12-03 00:28:18 +01:00
if (!$has_break_statement) {
$if_scope->reasonable_clauses = [];
}
}
2017-12-03 00:28:18 +01:00
if ($has_leaving_statements && !$has_break_statement && !$stmt->else && !$stmt->elseifs) {
// If we're assigning inside
if ($if_conditional_scope->cond_assigned_var_ids
&& $if_scope->mic_drop_context
) {
self::addConditionallyAssignedVarsToContext(
$statements_analyzer,
$stmt->cond,
$if_scope->mic_drop_context,
$outer_context,
$if_conditional_scope->cond_assigned_var_ids
);
}
if ($if_scope->negated_types) {
$changed_var_ids = [];
$outer_context_vars_reconciled = Reconciler::reconcileKeyedTypes(
$if_scope->negated_types,
[],
$outer_context->vars_in_scope,
$changed_var_ids,
[],
2018-11-11 18:01:14 +01:00
$statements_analyzer,
$statements_analyzer->getTemplateTypeMap() ?: [],
$outer_context->inside_loop,
new CodeLocation(
2018-11-11 18:01:14 +01:00
$statements_analyzer->getSource(),
$stmt->cond instanceof PhpParser\Node\Expr\BooleanNot
? $stmt->cond->expr
: $stmt->cond,
$outer_context->include_location,
false
)
);
2016-11-02 07:29:00 +01:00
foreach ($changed_var_ids as $changed_var_id => $_) {
$outer_context->removeVarFromConflictingClauses($changed_var_id);
}
$changed_var_ids += $new_assigned_var_ids;
foreach ($changed_var_ids as $var_id => $_) {
$if_scope->negated_clauses = Context::filterClauses(
$var_id,
$if_scope->negated_clauses
);
}
2016-12-29 00:55:16 +01:00
2020-10-04 23:53:26 +02:00
foreach ($changed_var_ids as $var_id => $_) {
$first_appearance = $statements_analyzer->getFirstAppearance($var_id);
if ($first_appearance
&& isset($outer_context->vars_in_scope[$var_id])
&& isset($outer_context_vars_reconciled[$var_id])
&& $outer_context->vars_in_scope[$var_id]->hasMixed()
&& !$outer_context_vars_reconciled[$var_id]->hasMixed()
) {
if (!$outer_context->collect_initializations
&& !$outer_context->collect_mutations
&& $statements_analyzer->getFilePath() === $statements_analyzer->getRootFilePath()
&& (!(($parent_source = $statements_analyzer->getSource())
instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer)
|| !$parent_source->getSource() instanceof \Psalm\Internal\Analyzer\TraitAnalyzer)
) {
$codebase->analyzer->decrementMixedCount($statements_analyzer->getFilePath());
}
IssueBuffer::remove(
$statements_analyzer->getFilePath(),
'MixedAssignment',
$first_appearance->raw_file_start
);
}
}
$outer_context->vars_in_scope = $outer_context_vars_reconciled;
$mic_drop = true;
2016-10-22 19:23:18 +02:00
}
2018-05-07 07:26:06 +02:00
$outer_context->clauses = Algebra::simplifyCNF(
array_merge($outer_context->clauses, $if_scope->negated_clauses)
);
}
2016-10-22 19:23:18 +02:00
// update the parent context as necessary, but only if we can safely reason about type negation.
// We only update vars that changed both at the start of the if block and then again by an assignment
// in the if statement.
if ($if_scope->negated_types && !$mic_drop) {
2016-12-29 00:55:16 +01:00
$vars_to_update = array_intersect(
array_keys($pre_assignment_else_redefined_vars),
array_keys($if_scope->negated_types)
);
$extra_vars_to_update = [];
// if there's an object-like array in there, we also need to update the root array variable
foreach ($vars_to_update as $var_id) {
$bracked_pos = strpos($var_id, '[');
if ($bracked_pos !== false) {
$extra_vars_to_update[] = substr($var_id, 0, $bracked_pos);
}
}
if ($extra_vars_to_update) {
$vars_to_update = array_unique(array_merge($extra_vars_to_update, $vars_to_update));
}
//update $if_context vars to include the pre-assignment else vars
if (!$stmt->else && !$has_leaving_statements) {
foreach ($pre_assignment_else_redefined_vars as $var_id => $type) {
if (isset($if_context->vars_in_scope[$var_id])) {
$if_context->vars_in_scope[$var_id] = Type::combineUnionTypes(
$if_context->vars_in_scope[$var_id],
$type,
$codebase
);
}
}
}
$outer_context->update(
$old_if_context,
$if_context,
$has_leaving_statements,
2016-12-29 00:55:16 +01:00
$vars_to_update,
$if_scope->updated_vars
2016-11-02 07:29:00 +01:00
);
}
2016-10-22 19:23:18 +02:00
if (!$has_ending_statements) {
$vars_possibly_in_scope = array_diff_key(
$if_context->vars_possibly_in_scope,
$outer_context->vars_possibly_in_scope
);
if ($if_context->loop_scope) {
2017-12-03 00:28:18 +01:00
if (!$has_continue_statement && !$has_break_statement) {
$if_scope->new_vars_possibly_in_scope = $vars_possibly_in_scope;
2016-10-22 19:23:18 +02:00
}
2017-12-03 00:28:18 +01:00
$if_context->loop_scope->vars_possibly_in_scope = array_merge(
$vars_possibly_in_scope,
$if_context->loop_scope->vars_possibly_in_scope
2017-12-03 00:28:18 +01:00
);
} elseif (!$has_leaving_statements) {
$if_scope->new_vars_possibly_in_scope = $vars_possibly_in_scope;
2016-10-22 19:23:18 +02:00
}
}
if ($outer_context->collect_exceptions) {
$outer_context->mergeExceptions($if_context);
}
return null;
}
2016-10-22 19:23:18 +02:00
/**
* @param Context $elseif_context
2017-05-27 02:16:18 +02:00
*
* @return false|null
*/
protected static function analyzeElseIfBlock(
2018-11-11 18:01:14 +01:00
StatementsAnalyzer $statements_analyzer,
PhpParser\Node\Stmt\ElseIf_ $elseif,
IfScope $if_scope,
2019-08-27 04:16:06 +02:00
Context $else_context,
2018-11-06 03:57:36 +01:00
Context $outer_context,
2019-08-27 04:16:06 +02:00
Codebase $codebase,
?int $branch_point
): ?bool {
$pre_conditional_context = clone $else_context;
2019-08-27 04:16:06 +02:00
try {
$if_conditional_scope = self::analyzeIfConditional(
2018-11-11 18:01:14 +01:00
$statements_analyzer,
$elseif->cond,
$else_context,
$codebase,
$if_scope,
$branch_point
);
2016-10-22 19:23:18 +02:00
$elseif_context = $if_conditional_scope->if_context;
$cond_referenced_var_ids = $if_conditional_scope->cond_referenced_var_ids;
$cond_assigned_var_ids = $if_conditional_scope->cond_assigned_var_ids;
$entry_clauses = $if_conditional_scope->entry_clauses;
} catch (\Psalm\Exception\ScopeAnalysisException $e) {
2016-12-09 18:06:14 +01:00
return false;
}
$mixed_var_ids = [];
foreach ($elseif_context->vars_in_scope as $var_id => $type) {
if ($type->hasMixed()) {
$mixed_var_ids[] = $var_id;
}
}
2020-08-26 21:35:29 +02:00
$elseif_cond_id = \spl_object_id($elseif->cond);
2020-11-03 22:15:44 +01:00
$elseif_clauses = FormulaGenerator::getFormula(
2020-08-26 21:35:29 +02:00
$elseif_cond_id,
$elseif_cond_id,
$elseif->cond,
$else_context->self,
2018-11-11 18:01:14 +01:00
$statements_analyzer,
2018-11-06 03:57:36 +01:00
$codebase
);
$elseif_clauses = array_map(
/**
* @return Clause
*/
function (Clause $c) use ($mixed_var_ids, $elseif_cond_id): Clause {
$keys = array_keys($c->possibilities);
2019-11-12 16:21:27 +01:00
$mixed_var_ids = \array_diff($mixed_var_ids, $keys);
foreach ($keys as $key) {
foreach ($mixed_var_ids as $mixed_var_id) {
if (preg_match('/^' . preg_quote($mixed_var_id, '/') . '(\[|-)/', $key)) {
2020-08-26 21:35:29 +02:00
return new Clause([], $elseif_cond_id, $elseif_cond_id, true);
}
}
}
return $c;
},
$elseif_clauses
);
$entry_clauses = array_map(
/**
* @return Clause
*/
function (Clause $c) use ($cond_assigned_var_ids, $elseif_cond_id): Clause {
$keys = array_keys($c->possibilities);
foreach ($keys as $key) {
foreach ($cond_assigned_var_ids as $conditional_assigned_var_id => $_) {
if (preg_match('/^' . preg_quote($conditional_assigned_var_id, '/') . '(\[|-|$)/', $key)) {
2020-08-26 21:35:29 +02:00
return new Clause([], $elseif_cond_id, $elseif_cond_id, true);
}
}
}
return $c;
},
$entry_clauses
);
2017-04-02 21:26:10 +02:00
// this will see whether any of the clauses in set A conflict with the clauses in set B
2018-11-06 03:57:36 +01:00
AlgebraAnalyzer::checkForParadox(
$entry_clauses,
$elseif_clauses,
2018-11-11 18:01:14 +01:00
$statements_analyzer,
$elseif->cond,
$cond_assigned_var_ids
);
2017-04-02 21:26:10 +02:00
2019-12-08 06:49:34 +01:00
$elseif_context_clauses = array_merge($entry_clauses, $elseif_clauses);
if ($elseif_context->reconciled_expression_clauses) {
$reconciled_expression_clauses = $elseif_context->reconciled_expression_clauses;
$elseif_context_clauses = array_values(
array_filter(
$elseif_context_clauses,
function ($c) use ($reconciled_expression_clauses): bool {
return !in_array($c->hash, $reconciled_expression_clauses);
2019-12-08 06:49:34 +01:00
}
)
);
}
$elseif_context->clauses = Algebra::simplifyCNF($elseif_context_clauses);
$active_elseif_types = [];
try {
if (array_filter(
$entry_clauses,
function ($clause): bool {
return !!$clause->possibilities;
}
)) {
$omit_keys = array_reduce(
$entry_clauses,
/**
* @param array<string> $carry
* @return array<string>
*/
function (array $carry, Clause $clause): array {
return array_merge($carry, array_keys($clause->possibilities));
},
[]
);
$omit_keys = array_combine($omit_keys, $omit_keys);
$omit_keys = array_diff_key($omit_keys, Algebra::getTruthsFromFormula($entry_clauses));
$cond_referenced_var_ids = array_diff_key(
$cond_referenced_var_ids,
$omit_keys
);
}
$reconcilable_elseif_types = Algebra::getTruthsFromFormula(
$elseif_context->clauses,
\spl_object_id($elseif->cond),
$cond_referenced_var_ids,
$active_elseif_types
);
$negated_elseif_types = Algebra::getTruthsFromFormula(
Algebra::negateFormula($elseif_clauses)
);
} catch (\Psalm\Exception\ComplicatedExpressionException $e) {
$reconcilable_elseif_types = [];
$negated_elseif_types = [];
}
$all_negated_vars = array_unique(
array_merge(
array_keys($negated_elseif_types),
array_keys($if_scope->negated_types)
)
);
foreach ($all_negated_vars as $var_id) {
if (isset($negated_elseif_types[$var_id])) {
if (isset($if_scope->negated_types[$var_id])) {
$if_scope->negated_types[$var_id] = array_merge(
$if_scope->negated_types[$var_id],
$negated_elseif_types[$var_id]
);
} else {
$if_scope->negated_types[$var_id] = $negated_elseif_types[$var_id];
}
2016-10-22 19:23:18 +02:00
}
}
2016-10-22 19:23:18 +02:00
$changed_var_ids = [];
2018-01-20 17:48:16 +01:00
// if the elseif has an || in the conditional, we cannot easily reason about it
if ($reconcilable_elseif_types) {
$elseif_vars_reconciled = Reconciler::reconcileKeyedTypes(
$reconcilable_elseif_types,
$active_elseif_types,
$elseif_context->vars_in_scope,
$changed_var_ids,
$cond_referenced_var_ids,
2018-11-11 18:01:14 +01:00
$statements_analyzer,
$statements_analyzer->getTemplateTypeMap() ?: [],
$elseif_context->inside_loop,
new CodeLocation(
$statements_analyzer->getSource(),
$elseif->cond instanceof PhpParser\Node\Expr\BooleanNot
? $elseif->cond->expr
: $elseif->cond,
$outer_context->include_location
)
);
2016-10-22 19:23:18 +02:00
$elseif_context->vars_in_scope = $elseif_vars_reconciled;
if ($changed_var_ids) {
2019-12-08 06:49:34 +01:00
$elseif_context->clauses = Context::removeReconciledClauses(
$elseif_context->clauses,
$changed_var_ids
)[0];
}
}
2018-01-20 17:48:16 +01:00
$pre_stmts_assigned_var_ids = $elseif_context->assigned_var_ids;
$elseif_context->assigned_var_ids = [];
2018-06-17 02:01:33 +02:00
$pre_stmts_possibly_assigned_var_ids = $elseif_context->possibly_assigned_var_ids;
$elseif_context->possibly_assigned_var_ids = [];
2018-01-20 17:48:16 +01:00
2018-11-11 18:01:14 +01:00
if ($statements_analyzer->analyze(
2018-01-03 03:23:48 +01:00
$elseif->stmts,
$elseif_context
2018-01-03 03:23:48 +01:00
) === false
2017-12-03 00:28:18 +01:00
) {
return false;
}
2016-10-22 19:23:18 +02:00
/** @var array<string, int> */
2018-01-20 17:48:16 +01:00
$new_stmts_assigned_var_ids = $elseif_context->assigned_var_ids;
2018-06-17 02:01:33 +02:00
$elseif_context->assigned_var_ids = $pre_stmts_assigned_var_ids + $new_stmts_assigned_var_ids;
/** @var array<string, bool> */
$new_stmts_possibly_assigned_var_ids = $elseif_context->possibly_assigned_var_ids;
$elseif_context->possibly_assigned_var_ids =
$pre_stmts_possibly_assigned_var_ids + $new_stmts_possibly_assigned_var_ids;
2018-01-20 17:48:16 +01:00
foreach ($elseif_context->byref_constraints as $var_id => $byref_constraint) {
if (isset($outer_context->byref_constraints[$var_id])
&& ($outer_constraint_type = $outer_context->byref_constraints[$var_id]->type)
&& $byref_constraint->type
&& !UnionTypeComparator::isContainedBy(
$codebase,
$byref_constraint->type,
$outer_constraint_type
)
) {
if (IssueBuffer::accepts(
new ConflictingReferenceConstraint(
'There is more than one pass-by-reference constraint on ' . $var_id,
new CodeLocation($statements_analyzer, $elseif, $outer_context->include_location, true)
),
$statements_analyzer->getSuppressedIssues()
)) {
// fall through
}
} else {
$outer_context->byref_constraints[$var_id] = $byref_constraint;
}
}
$final_actions = ScopeAnalyzer::getControlActions(
$elseif->stmts,
$statements_analyzer->node_data,
2018-11-06 03:57:36 +01:00
$codebase->config->exit_functions,
$outer_context->break_types
);
2017-12-03 00:28:18 +01:00
// has a return/throw at end
2018-11-06 03:57:36 +01:00
$has_ending_statements = $final_actions === [ScopeAnalyzer::ACTION_END];
2017-12-03 00:28:18 +01:00
$has_leaving_statements = $has_ending_statements
2018-11-06 03:57:36 +01:00
|| (count($final_actions) && !in_array(ScopeAnalyzer::ACTION_NONE, $final_actions, true));
2018-11-06 03:57:36 +01:00
$has_break_statement = $final_actions === [ScopeAnalyzer::ACTION_BREAK];
$has_continue_statement = $final_actions === [ScopeAnalyzer::ACTION_CONTINUE];
2017-12-03 00:28:18 +01:00
$if_scope->final_actions = array_merge($final_actions, $if_scope->final_actions);
2018-01-20 17:48:16 +01:00
// update the parent context as necessary
$elseif_redefined_vars = $elseif_context->getRedefinedVars($outer_context->vars_in_scope);
2018-01-20 17:48:16 +01:00
if (!$has_leaving_statements) {
if ($if_scope->new_vars === null) {
$if_scope->new_vars = array_diff_key($elseif_context->vars_in_scope, $outer_context->vars_in_scope);
} else {
foreach ($if_scope->new_vars as $new_var => $type) {
if (!$elseif_context->hasVariable($new_var)) {
2018-01-20 17:48:16 +01:00
unset($if_scope->new_vars[$new_var]);
} else {
$if_scope->new_vars[$new_var] = Type::combineUnionTypes(
$type,
$elseif_context->vars_in_scope[$new_var],
$codebase
2018-01-20 17:48:16 +01:00
);
2016-10-22 19:23:18 +02:00
}
}
2018-01-20 17:48:16 +01:00
}
2016-10-22 19:23:18 +02:00
2018-01-20 17:48:16 +01:00
$possibly_redefined_vars = $elseif_redefined_vars;
2018-01-20 17:48:16 +01:00
foreach ($possibly_redefined_vars as $var_id => $_) {
if (!isset($new_stmts_assigned_var_ids[$var_id])
&& isset($changed_var_ids[$var_id])
2018-01-20 17:48:16 +01:00
) {
unset($possibly_redefined_vars[$var_id]);
}
}
$assigned_var_ids = array_merge($new_stmts_assigned_var_ids, $cond_assigned_var_ids);
if ($if_scope->assigned_var_ids === null) {
$if_scope->assigned_var_ids = $assigned_var_ids;
} else {
$if_scope->assigned_var_ids = array_intersect_key($assigned_var_ids, $if_scope->assigned_var_ids);
}
2018-01-20 17:48:16 +01:00
if ($if_scope->redefined_vars === null) {
$if_scope->redefined_vars = $elseif_redefined_vars;
$if_scope->possibly_redefined_vars = $possibly_redefined_vars;
} else {
foreach ($if_scope->redefined_vars as $redefined_var => $type) {
if (!isset($elseif_redefined_vars[$redefined_var])) {
unset($if_scope->redefined_vars[$redefined_var]);
} else {
$if_scope->redefined_vars[$redefined_var] = Type::combineUnionTypes(
$elseif_redefined_vars[$redefined_var],
$type,
$codebase
2018-01-20 17:48:16 +01:00
);
2018-05-14 22:29:51 +02:00
if (isset($outer_context->vars_in_scope[$redefined_var])
&& $if_scope->redefined_vars[$redefined_var]->equals(
$outer_context->vars_in_scope[$redefined_var]
)
) {
unset($if_scope->redefined_vars[$redefined_var]);
}
}
}
2018-01-20 17:48:16 +01:00
foreach ($possibly_redefined_vars as $var => $type) {
if (isset($if_scope->possibly_redefined_vars[$var])) {
2018-01-20 17:48:16 +01:00
$if_scope->possibly_redefined_vars[$var] = Type::combineUnionTypes(
$type,
$if_scope->possibly_redefined_vars[$var],
$codebase
2018-01-20 17:48:16 +01:00
);
} else {
$if_scope->possibly_redefined_vars[$var] = $type;
}
}
2018-01-20 17:48:16 +01:00
}
2018-05-30 19:57:45 +02:00
$reasonable_clause_count = count($if_scope->reasonable_clauses);
if ($reasonable_clause_count && $reasonable_clause_count < 20000 && $elseif_clauses) {
2018-05-07 07:26:06 +02:00
$if_scope->reasonable_clauses = Algebra::combineOredClauses(
2018-01-20 17:48:16 +01:00
$if_scope->reasonable_clauses,
2020-08-26 21:35:29 +02:00
$elseif_clauses,
\spl_object_id($elseif->cond)
2018-01-20 17:48:16 +01:00
);
} else {
$if_scope->reasonable_clauses = [];
}
2018-01-20 17:48:16 +01:00
} else {
$if_scope->reasonable_clauses = [];
}
2018-01-20 17:48:16 +01:00
if ($negated_elseif_types) {
if ($has_leaving_statements) {
$changed_var_ids = [];
2018-01-20 17:48:16 +01:00
$leaving_vars_reconciled = Reconciler::reconcileKeyedTypes(
$negated_elseif_types,
[],
2018-01-20 17:48:16 +01:00
$pre_conditional_context->vars_in_scope,
$changed_var_ids,
[],
2018-11-11 18:01:14 +01:00
$statements_analyzer,
$statements_analyzer->getTemplateTypeMap() ?: [],
$elseif_context->inside_loop,
new CodeLocation($statements_analyzer->getSource(), $elseif, $outer_context->include_location)
2018-01-20 17:48:16 +01:00
);
2018-01-20 17:48:16 +01:00
$implied_outer_context = clone $elseif_context;
$implied_outer_context->vars_in_scope = $leaving_vars_reconciled;
2018-01-20 17:48:16 +01:00
$outer_context->update(
$elseif_context,
$implied_outer_context,
false,
array_keys($negated_elseif_types),
$if_scope->updated_vars
);
}
2018-01-20 17:48:16 +01:00
}
2018-01-20 17:48:16 +01:00
if (!$has_ending_statements) {
$vars_possibly_in_scope = array_diff_key(
$elseif_context->vars_possibly_in_scope,
$outer_context->vars_possibly_in_scope
);
2018-06-17 02:01:33 +02:00
$possibly_assigned_var_ids = $new_stmts_possibly_assigned_var_ids;
2016-10-22 19:23:18 +02:00
if ($has_leaving_statements && $elseif_context->loop_scope) {
2018-01-20 17:48:16 +01:00
if (!$has_continue_statement && !$has_break_statement) {
$if_scope->new_vars_possibly_in_scope = array_merge(
$vars_possibly_in_scope,
2018-01-20 17:48:16 +01:00
$if_scope->new_vars_possibly_in_scope
2016-11-02 07:29:00 +01:00
);
$if_scope->possibly_assigned_var_ids = array_merge(
$possibly_assigned_var_ids,
$if_scope->possibly_assigned_var_ids
);
2016-10-22 19:23:18 +02:00
}
2018-01-20 17:48:16 +01:00
$elseif_context->loop_scope->vars_possibly_in_scope = array_merge(
$vars_possibly_in_scope,
$elseif_context->loop_scope->vars_possibly_in_scope
2018-01-20 17:48:16 +01:00
);
} elseif (!$has_leaving_statements) {
$if_scope->new_vars_possibly_in_scope = array_merge(
$vars_possibly_in_scope,
$if_scope->new_vars_possibly_in_scope
);
$if_scope->possibly_assigned_var_ids = array_merge(
$possibly_assigned_var_ids,
$if_scope->possibly_assigned_var_ids
);
}
}
if ($outer_context->collect_exceptions) {
$outer_context->mergeExceptions($elseif_context);
}
try {
$if_scope->negated_clauses = Algebra::simplifyCNF(
array_merge(
$if_scope->negated_clauses,
Algebra::negateFormula($elseif_clauses)
)
);
} catch (\Psalm\Exception\ComplicatedExpressionException $e) {
$if_scope->negated_clauses = [];
}
return null;
}
2016-10-22 19:23:18 +02:00
/**
* @return false|null
*/
protected static function analyzeElseBlock(
2018-11-11 18:01:14 +01:00
StatementsAnalyzer $statements_analyzer,
?PhpParser\Node\Stmt\Else_ $else,
IfScope $if_scope,
Context $else_context,
Context $outer_context
): ?bool {
2018-11-11 18:01:14 +01:00
$codebase = $statements_analyzer->getCodebase();
2018-06-01 04:20:38 +02:00
if (!$else && !$if_scope->negated_clauses && !$else_context->clauses) {
2018-11-06 03:57:36 +01:00
$if_scope->final_actions = array_merge([ScopeAnalyzer::ACTION_NONE], $if_scope->final_actions);
2018-06-01 04:20:38 +02:00
$if_scope->assigned_var_ids = [];
$if_scope->new_vars = [];
$if_scope->redefined_vars = [];
$if_scope->reasonable_clauses = [];
return null;
2018-06-01 04:20:38 +02:00
}
2016-10-22 19:23:18 +02:00
2018-05-07 07:26:06 +02:00
$else_context->clauses = Algebra::simplifyCNF(
array_merge(
2018-05-06 02:52:10 +02:00
$else_context->clauses,
$if_scope->negated_clauses
)
);
2018-05-07 07:26:06 +02:00
$else_types = Algebra::getTruthsFromFormula($else_context->clauses);
2018-06-01 04:20:38 +02:00
if (!$else && !$else_types) {
2018-11-06 03:57:36 +01:00
$if_scope->final_actions = array_merge([ScopeAnalyzer::ACTION_NONE], $if_scope->final_actions);
2018-06-01 04:20:38 +02:00
$if_scope->assigned_var_ids = [];
$if_scope->new_vars = [];
$if_scope->redefined_vars = [];
$if_scope->reasonable_clauses = [];
return null;
2018-06-01 04:20:38 +02:00
}
$original_context = clone $else_context;
if ($else_types) {
$changed_var_ids = [];
$else_vars_reconciled = Reconciler::reconcileKeyedTypes(
$else_types,
[],
$else_context->vars_in_scope,
$changed_var_ids,
[],
2018-11-11 18:01:14 +01:00
$statements_analyzer,
[],
$else_context->inside_loop,
$else
2018-11-11 18:01:14 +01:00
? new CodeLocation($statements_analyzer->getSource(), $else, $outer_context->include_location)
: null
);
2016-10-22 19:23:18 +02:00
$else_context->vars_in_scope = $else_vars_reconciled;
2019-12-08 06:49:34 +01:00
$else_context->clauses = Context::removeReconciledClauses($else_context->clauses, $changed_var_ids)[0];
}
2016-10-22 19:23:18 +02:00
$old_else_context = clone $else_context;
2016-11-02 07:29:00 +01:00
$pre_stmts_assigned_var_ids = $else_context->assigned_var_ids;
$else_context->assigned_var_ids = [];
2018-06-17 02:01:33 +02:00
$pre_possibly_assigned_var_ids = $else_context->possibly_assigned_var_ids;
$else_context->possibly_assigned_var_ids = [];
if ($else) {
2018-11-11 18:01:14 +01:00
if ($statements_analyzer->analyze(
$else->stmts,
$else_context
) === false
) {
return false;
}
}
2016-10-22 19:23:18 +02:00
/** @var array<string, int> */
$new_assigned_var_ids = $else_context->assigned_var_ids;
$else_context->assigned_var_ids = $pre_stmts_assigned_var_ids;
2018-06-17 02:01:33 +02:00
/** @var array<string, bool> */
$new_possibly_assigned_var_ids = $else_context->possibly_assigned_var_ids;
$else_context->possibly_assigned_var_ids = $pre_possibly_assigned_var_ids + $new_possibly_assigned_var_ids;
if ($else) {
foreach ($else_context->byref_constraints as $var_id => $byref_constraint) {
if (isset($outer_context->byref_constraints[$var_id])
&& ($outer_constraint_type = $outer_context->byref_constraints[$var_id]->type)
&& $byref_constraint->type
2020-07-22 01:40:35 +02:00
&& !UnionTypeComparator::isContainedBy(
2018-11-06 03:57:36 +01:00
$codebase,
$byref_constraint->type,
$outer_constraint_type
)
) {
if (IssueBuffer::accepts(
new ConflictingReferenceConstraint(
'There is more than one pass-by-reference constraint on ' . $var_id,
2018-11-11 18:01:14 +01:00
new CodeLocation($statements_analyzer, $else, $outer_context->include_location, true)
),
2018-11-11 18:01:14 +01:00
$statements_analyzer->getSuppressedIssues()
)) {
// fall through
}
} else {
$outer_context->byref_constraints[$var_id] = $byref_constraint;
}
}
}
2018-06-17 02:01:33 +02:00
$final_actions = $else
? ScopeAnalyzer::getControlActions(
$else->stmts,
$statements_analyzer->node_data,
2018-11-06 03:57:36 +01:00
$codebase->config->exit_functions,
$outer_context->break_types
)
2018-11-06 03:57:36 +01:00
: [ScopeAnalyzer::ACTION_NONE];
2017-12-03 00:28:18 +01:00
// has a return/throw at end
2018-11-06 03:57:36 +01:00
$has_ending_statements = $final_actions === [ScopeAnalyzer::ACTION_END];
2017-12-03 00:28:18 +01:00
$has_leaving_statements = $has_ending_statements
2018-11-06 03:57:36 +01:00
|| (count($final_actions) && !in_array(ScopeAnalyzer::ACTION_NONE, $final_actions, true));
2017-12-03 00:28:18 +01:00
2018-11-06 03:57:36 +01:00
$has_break_statement = $final_actions === [ScopeAnalyzer::ACTION_BREAK];
$has_continue_statement = $final_actions === [ScopeAnalyzer::ACTION_CONTINUE];
2016-10-22 19:23:18 +02:00
2017-12-03 00:28:18 +01:00
$if_scope->final_actions = array_merge($final_actions, $if_scope->final_actions);
2018-01-20 17:48:16 +01:00
$else_redefined_vars = $else_context->getRedefinedVars($original_context->vars_in_scope);
2016-10-22 19:23:18 +02:00
2018-01-20 17:48:16 +01:00
// if it doesn't end in a return
if (!$has_leaving_statements) {
2018-06-01 04:20:38 +02:00
if ($if_scope->new_vars === null && $else) {
2018-01-20 17:48:16 +01:00
$if_scope->new_vars = array_diff_key($else_context->vars_in_scope, $outer_context->vars_in_scope);
2018-06-01 04:20:38 +02:00
} elseif ($if_scope->new_vars !== null) {
2018-01-20 17:48:16 +01:00
foreach ($if_scope->new_vars as $new_var => $type) {
if (!$else_context->hasVariable($new_var)) {
unset($if_scope->new_vars[$new_var]);
} else {
$if_scope->new_vars[$new_var] = Type::combineUnionTypes(
$type,
$else_context->vars_in_scope[$new_var],
$codebase
2018-01-20 17:48:16 +01:00
);
}
}
2018-01-20 17:48:16 +01:00
}
2016-10-22 19:23:18 +02:00
if ($if_scope->assigned_var_ids === null) {
$if_scope->assigned_var_ids = $new_assigned_var_ids;
} else {
$if_scope->assigned_var_ids = array_intersect_key($new_assigned_var_ids, $if_scope->assigned_var_ids);
}
2018-01-20 17:48:16 +01:00
if ($if_scope->redefined_vars === null) {
$if_scope->redefined_vars = $else_redefined_vars;
$if_scope->possibly_redefined_vars = $if_scope->redefined_vars;
} else {
foreach ($if_scope->redefined_vars as $redefined_var => $type) {
if (!isset($else_redefined_vars[$redefined_var])) {
unset($if_scope->redefined_vars[$redefined_var]);
} else {
$if_scope->redefined_vars[$redefined_var] = Type::combineUnionTypes(
$else_redefined_vars[$redefined_var],
$type,
$codebase
2018-01-20 17:48:16 +01:00
);
}
2018-01-20 17:48:16 +01:00
}
2016-10-22 19:23:18 +02:00
2018-01-20 17:48:16 +01:00
foreach ($else_redefined_vars as $var => $type) {
if (isset($if_scope->possibly_redefined_vars[$var])) {
2018-01-20 17:48:16 +01:00
$if_scope->possibly_redefined_vars[$var] = Type::combineUnionTypes(
$type,
$if_scope->possibly_redefined_vars[$var],
$codebase
2018-01-20 17:48:16 +01:00
);
} else {
$if_scope->possibly_redefined_vars[$var] = $type;
2016-10-22 19:23:18 +02:00
}
}
}
2018-05-14 22:29:51 +02:00
$if_scope->reasonable_clauses = [];
2018-01-20 17:48:16 +01:00
}
2018-01-20 17:48:16 +01:00
// update the parent context as necessary
if ($if_scope->negatable_if_types) {
$outer_context->update(
$old_else_context,
$else_context,
$has_leaving_statements,
array_keys($if_scope->negatable_if_types),
$if_scope->updated_vars
);
}
2018-01-20 17:48:16 +01:00
if (!$has_ending_statements) {
$vars_possibly_in_scope = array_diff_key(
$else_context->vars_possibly_in_scope,
$outer_context->vars_possibly_in_scope
);
2018-06-17 02:01:33 +02:00
$possibly_assigned_var_ids = $new_possibly_assigned_var_ids;
2016-10-22 19:23:18 +02:00
if ($has_leaving_statements && $else_context->loop_scope) {
2018-01-20 17:48:16 +01:00
if (!$has_continue_statement && !$has_break_statement) {
$if_scope->new_vars_possibly_in_scope = array_merge(
$vars_possibly_in_scope,
2018-01-20 17:48:16 +01:00
$if_scope->new_vars_possibly_in_scope
2016-11-02 07:29:00 +01:00
);
$if_scope->possibly_assigned_var_ids = array_merge(
$possibly_assigned_var_ids,
$if_scope->possibly_assigned_var_ids
);
2016-10-22 19:23:18 +02:00
}
2018-01-20 17:48:16 +01:00
$else_context->loop_scope->vars_possibly_in_scope = array_merge(
$vars_possibly_in_scope,
$else_context->loop_scope->vars_possibly_in_scope
2018-01-20 17:48:16 +01:00
);
} elseif (!$has_leaving_statements) {
$if_scope->new_vars_possibly_in_scope = array_merge(
$vars_possibly_in_scope,
$if_scope->new_vars_possibly_in_scope
);
$if_scope->possibly_assigned_var_ids = array_merge(
$possibly_assigned_var_ids,
$if_scope->possibly_assigned_var_ids
);
2016-10-22 19:23:18 +02:00
}
}
if ($outer_context->collect_exceptions) {
$outer_context->mergeExceptions($else_context);
}
return null;
2016-10-22 19:23:18 +02:00
}
/**
2018-05-14 18:25:58 +02:00
* Returns statements that are definitely evaluated before any statements after the end of the
* if/elseif/else blocks
2016-10-22 19:23:18 +02:00
*/
private static function getDefinitelyEvaluatedExpressionAfterIf(PhpParser\Node\Expr $stmt): ?PhpParser\Node\Expr
2016-10-22 19:23:18 +02:00
{
2020-06-12 16:58:44 +02:00
if ($stmt instanceof PhpParser\Node\Expr\BinaryOp\Equal
|| $stmt instanceof PhpParser\Node\Expr\BinaryOp\Identical
) {
if ($stmt->left instanceof PhpParser\Node\Expr\ConstFetch
&& $stmt->left->name->parts === ['true']
) {
return self::getDefinitelyEvaluatedExpressionAfterIf($stmt->right);
}
if ($stmt->right instanceof PhpParser\Node\Expr\ConstFetch
&& $stmt->right->name->parts === ['true']
) {
return self::getDefinitelyEvaluatedExpressionAfterIf($stmt->left);
}
}
2016-10-22 19:23:18 +02:00
if ($stmt instanceof PhpParser\Node\Expr\BinaryOp) {
if ($stmt instanceof PhpParser\Node\Expr\BinaryOp\BooleanAnd
|| $stmt instanceof PhpParser\Node\Expr\BinaryOp\LogicalAnd
|| $stmt instanceof PhpParser\Node\Expr\BinaryOp\LogicalXor
2016-12-28 23:04:03 +01:00
) {
2019-12-08 06:49:34 +01:00
return self::getDefinitelyEvaluatedExpressionAfterIf($stmt->left);
2016-12-28 23:04:03 +01:00
}
return $stmt;
}
2019-12-08 06:49:34 +01:00
if ($stmt instanceof PhpParser\Node\Expr\BooleanNot) {
$inner_stmt = self::getDefinitelyEvaluatedExpressionInsideIf($stmt->expr);
if ($inner_stmt !== $stmt->expr) {
return $inner_stmt;
}
}
return $stmt;
}
/**
* Returns statements that are definitely evaluated before any statements inside
* the if block
*/
private static function getDefinitelyEvaluatedExpressionInsideIf(PhpParser\Node\Expr $stmt): ?PhpParser\Node\Expr
2019-12-08 06:49:34 +01:00
{
2020-06-12 16:58:44 +02:00
if ($stmt instanceof PhpParser\Node\Expr\BinaryOp\Equal
|| $stmt instanceof PhpParser\Node\Expr\BinaryOp\Identical
) {
if ($stmt->left instanceof PhpParser\Node\Expr\ConstFetch
&& $stmt->left->name->parts === ['true']
) {
return self::getDefinitelyEvaluatedExpressionInsideIf($stmt->right);
}
if ($stmt->right instanceof PhpParser\Node\Expr\ConstFetch
&& $stmt->right->name->parts === ['true']
) {
return self::getDefinitelyEvaluatedExpressionInsideIf($stmt->left);
}
}
2019-12-08 06:49:34 +01:00
if ($stmt instanceof PhpParser\Node\Expr\BinaryOp) {
if ($stmt instanceof PhpParser\Node\Expr\BinaryOp\BooleanOr
|| $stmt instanceof PhpParser\Node\Expr\BinaryOp\LogicalOr
|| $stmt instanceof PhpParser\Node\Expr\BinaryOp\LogicalXor
) {
return self::getDefinitelyEvaluatedExpressionInsideIf($stmt->left);
}
return $stmt;
}
if ($stmt instanceof PhpParser\Node\Expr\BooleanNot) {
$inner_stmt = self::getDefinitelyEvaluatedExpressionAfterIf($stmt->expr);
if ($inner_stmt !== $stmt->expr) {
return $inner_stmt;
}
}
2017-01-07 20:35:07 +01:00
return $stmt;
2016-10-22 19:23:18 +02:00
}
/**
* Returns all expressions inside an ored expression
* @return non-empty-list<PhpParser\Node\Expr>
*/
private static function getDefinitelyEvaluatedOredExpressions(PhpParser\Node\Expr $stmt): array
{
if ($stmt instanceof PhpParser\Node\Expr\BinaryOp\BooleanOr
|| $stmt instanceof PhpParser\Node\Expr\BinaryOp\LogicalOr
|| $stmt instanceof PhpParser\Node\Expr\BinaryOp\LogicalXor
) {
return array_merge(
self::getDefinitelyEvaluatedOredExpressions($stmt->left),
self::getDefinitelyEvaluatedOredExpressions($stmt->right)
);
}
return [$stmt];
}
/**
* @param array<string, int> $cond_assigned_var_ids
*/
public static function addConditionallyAssignedVarsToContext(
StatementsAnalyzer $statements_analyzer,
PhpParser\Node\Expr $cond,
Context $mic_drop_context,
Context $outer_context,
array $cond_assigned_var_ids
) : void {
2020-10-18 05:35:24 +02:00
// this filters out coercions to expeccted types in ArgumentAnalyzer
$cond_assigned_var_ids = \array_filter($cond_assigned_var_ids);
if (!$cond_assigned_var_ids) {
return;
}
$exprs = self::getDefinitelyEvaluatedOredExpressions($cond);
// if there was no assignment in the first expression it's safe to proceed
$old_node_data = $statements_analyzer->node_data;
$statements_analyzer->node_data = clone $old_node_data;
IssueBuffer::startRecording();
2020-10-18 00:35:55 +02:00
foreach ($exprs as $expr) {
if ($expr instanceof PhpParser\Node\Expr\BinaryOp\BooleanAnd) {
$fake_not = new PhpParser\Node\Expr\BinaryOp\BooleanOr(
self::negateExpr($expr->left),
self::negateExpr($expr->right),
$expr->getAttributes()
);
} else {
$fake_not = self::negateExpr($expr);
}
$fake_negated_expr = new PhpParser\Node\Expr\FuncCall(
new PhpParser\Node\Name\FullyQualified('assert'),
[new PhpParser\Node\Arg(
$fake_not,
false,
false,
$expr->getAttributes()
)],
$expr->getAttributes()
);
2020-10-17 23:46:00 +02:00
$mic_drop_context->inside_negation = !$mic_drop_context->inside_negation;
ExpressionAnalyzer::analyze(
$statements_analyzer,
$fake_negated_expr,
$mic_drop_context
);
2020-10-17 23:46:00 +02:00
$mic_drop_context->inside_negation = !$mic_drop_context->inside_negation;
}
IssueBuffer::clearRecordingLevel();
IssueBuffer::stopRecording();
2020-10-18 00:35:55 +02:00
$statements_analyzer->node_data = $old_node_data;
foreach ($cond_assigned_var_ids as $var_id => $_) {
if (isset($mic_drop_context->vars_in_scope[$var_id])) {
$outer_context->vars_in_scope[$var_id] = clone $mic_drop_context->vars_in_scope[$var_id];
}
}
}
private static function negateExpr(PhpParser\Node\Expr $expr) : PhpParser\Node\Expr
{
if ($expr instanceof PhpParser\Node\Expr\BooleanNot) {
return $expr->expr;
}
return new PhpParser\Node\Expr\BooleanNot($expr, $expr->getAttributes());
}
2016-10-22 19:23:18 +02:00
}