mirror of
https://github.com/danog/psalm.git
synced 2024-11-27 04:45:20 +01:00
Remove mic-drop hack from if analysis (#7484)
* Remove mic-drop hack from if analysis * Remove more special handling * Remove some unnecessary ElseAnalyzer code * Add back necessary code * Fix return type of method never returning null * Add a comment * Simplify && handling * Add comments to make stuff clearer * Move if-specfic logic to more appropriate setting
This commit is contained in:
parent
048025b1d6
commit
faaf7690f6
@ -349,7 +349,7 @@ class Context
|
||||
/**
|
||||
* @var Context|null
|
||||
*/
|
||||
public $if_context;
|
||||
public $if_body_context;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
|
@ -114,14 +114,12 @@ class IfConditionalAnalyzer
|
||||
|
||||
$outer_context->inside_conditional = true;
|
||||
|
||||
if ($externally_applied_if_cond_expr) {
|
||||
if (ExpressionAnalyzer::analyze(
|
||||
$statements_analyzer,
|
||||
$externally_applied_if_cond_expr,
|
||||
$outer_context
|
||||
) === false) {
|
||||
throw new ScopeAnalysisException();
|
||||
}
|
||||
if (ExpressionAnalyzer::analyze(
|
||||
$statements_analyzer,
|
||||
$externally_applied_if_cond_expr,
|
||||
$outer_context
|
||||
) === false) {
|
||||
throw new ScopeAnalysisException();
|
||||
}
|
||||
|
||||
$first_cond_assigned_var_ids = $outer_context->assigned_var_ids;
|
||||
@ -143,7 +141,10 @@ class IfConditionalAnalyzer
|
||||
}
|
||||
|
||||
$if_conditional_context = clone $if_context;
|
||||
$if_conditional_context->if_context = $if_context;
|
||||
|
||||
// here we set up a context specifically for the statements in the first `if`, which can
|
||||
// be affected by statements in the if condition
|
||||
$if_conditional_context->if_body_context = $if_context;
|
||||
|
||||
if ($codebase->alter_code) {
|
||||
$if_context->branch_point = $branch_point;
|
||||
@ -236,7 +237,7 @@ class IfConditionalAnalyzer
|
||||
* Returns statements that are definitely evaluated before any statements after the end of the
|
||||
* if/elseif/else blocks
|
||||
*/
|
||||
private static function getDefinitelyEvaluatedExpressionAfterIf(PhpParser\Node\Expr $stmt): ?PhpParser\Node\Expr
|
||||
private static function getDefinitelyEvaluatedExpressionAfterIf(PhpParser\Node\Expr $stmt): PhpParser\Node\Expr
|
||||
{
|
||||
if ($stmt instanceof PhpParser\Node\Expr\BinaryOp\Equal
|
||||
|| $stmt instanceof PhpParser\Node\Expr\BinaryOp\Identical
|
||||
@ -280,7 +281,7 @@ class IfConditionalAnalyzer
|
||||
* Returns statements that are definitely evaluated before any statements inside
|
||||
* the if block
|
||||
*/
|
||||
private static function getDefinitelyEvaluatedExpressionInsideIf(PhpParser\Node\Expr $stmt): ?PhpParser\Node\Expr
|
||||
private static function getDefinitelyEvaluatedExpressionInsideIf(PhpParser\Node\Expr $stmt): PhpParser\Node\Expr
|
||||
{
|
||||
if ($stmt instanceof PhpParser\Node\Expr\BinaryOp\Equal
|
||||
|| $stmt instanceof PhpParser\Node\Expr\BinaryOp\Identical
|
||||
|
@ -59,16 +59,6 @@ class ElseAnalyzer
|
||||
|
||||
$else_types = Algebra::getTruthsFromFormula($else_context->clauses);
|
||||
|
||||
if (!$else && !$else_types) {
|
||||
$if_scope->final_actions = array_merge([ScopeAnalyzer::ACTION_NONE], $if_scope->final_actions);
|
||||
$if_scope->assigned_var_ids = [];
|
||||
$if_scope->new_vars = [];
|
||||
$if_scope->redefined_vars = [];
|
||||
$if_scope->reasonable_clauses = [];
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
$original_context = clone $else_context;
|
||||
|
||||
if ($else_types) {
|
||||
@ -120,19 +110,19 @@ class ElseAnalyzer
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($else_context->parent_remove_vars as $var_id => $_) {
|
||||
$outer_context->removeVarFromConflictingClauses($var_id);
|
||||
}
|
||||
foreach ($else_context->parent_remove_vars as $var_id => $_) {
|
||||
$outer_context->removeVarFromConflictingClauses($var_id);
|
||||
}
|
||||
|
||||
/** @var array<string, int> */
|
||||
$new_assigned_var_ids = $else_context->assigned_var_ids;
|
||||
$else_context->assigned_var_ids = $pre_stmts_assigned_var_ids;
|
||||
$else_context->assigned_var_ids += $pre_stmts_assigned_var_ids;
|
||||
|
||||
/** @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;
|
||||
$else_context->possibly_assigned_var_ids += $pre_possibly_assigned_var_ids;
|
||||
|
||||
if ($else) {
|
||||
foreach ($else_context->byref_constraints as $var_id => $byref_constraint) {
|
||||
@ -185,7 +175,7 @@ class ElseAnalyzer
|
||||
$new_assigned_var_ids,
|
||||
$new_possibly_assigned_var_ids,
|
||||
[],
|
||||
(bool) $else
|
||||
true
|
||||
);
|
||||
|
||||
$if_scope->reasonable_clauses = [];
|
||||
@ -210,24 +200,21 @@ class ElseAnalyzer
|
||||
|
||||
$possibly_assigned_var_ids = $new_possibly_assigned_var_ids;
|
||||
|
||||
if ($has_leaving_statements && $else_context->loop_scope) {
|
||||
if (!$has_continue_statement && !$has_break_statement) {
|
||||
$if_scope->new_vars_possibly_in_scope = array_merge(
|
||||
$vars_possibly_in_scope,
|
||||
$if_scope->new_vars_possibly_in_scope
|
||||
);
|
||||
if ($has_leaving_statements) {
|
||||
if ($else_context->loop_scope) {
|
||||
if (!$has_continue_statement && !$has_break_statement) {
|
||||
$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
|
||||
$else_context->loop_scope->vars_possibly_in_scope = array_merge(
|
||||
$vars_possibly_in_scope,
|
||||
$else_context->loop_scope->vars_possibly_in_scope
|
||||
);
|
||||
}
|
||||
|
||||
$else_context->loop_scope->vars_possibly_in_scope = array_merge(
|
||||
$vars_possibly_in_scope,
|
||||
$else_context->loop_scope->vars_possibly_in_scope
|
||||
);
|
||||
} elseif (!$has_leaving_statements) {
|
||||
} else {
|
||||
$if_scope->new_vars_possibly_in_scope = array_merge(
|
||||
$vars_possibly_in_scope,
|
||||
$if_scope->new_vars_possibly_in_scope
|
||||
|
@ -7,11 +7,10 @@ use Psalm\CodeLocation;
|
||||
use Psalm\Codebase;
|
||||
use Psalm\Context;
|
||||
use Psalm\Internal\Algebra;
|
||||
use Psalm\Internal\Analyzer\FunctionLikeAnalyzer;
|
||||
use Psalm\Internal\Analyzer\ScopeAnalyzer;
|
||||
use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer;
|
||||
use Psalm\Internal\Analyzer\StatementsAnalyzer;
|
||||
use Psalm\Internal\Analyzer\TraitAnalyzer;
|
||||
use Psalm\Internal\Clause;
|
||||
use Psalm\Internal\Scope\IfConditionalScope;
|
||||
use Psalm\Internal\Scope\IfScope;
|
||||
use Psalm\Internal\Type\Comparator\UnionTypeComparator;
|
||||
@ -26,15 +25,21 @@ use Psalm\Type;
|
||||
use Psalm\Type\Reconciler;
|
||||
use Psalm\Type\Union;
|
||||
|
||||
use function array_combine;
|
||||
use function array_diff_key;
|
||||
use function array_filter;
|
||||
use function array_intersect;
|
||||
use function array_intersect_key;
|
||||
use function array_key_exists;
|
||||
use function array_keys;
|
||||
use function array_merge;
|
||||
use function array_reduce;
|
||||
use function array_unique;
|
||||
use function count;
|
||||
use function in_array;
|
||||
use function preg_match;
|
||||
use function preg_quote;
|
||||
use function spl_object_id;
|
||||
use function strpos;
|
||||
use function substr;
|
||||
|
||||
@ -54,10 +59,106 @@ class IfAnalyzer
|
||||
IfScope $if_scope,
|
||||
IfConditionalScope $if_conditional_scope,
|
||||
Context $if_context,
|
||||
Context $old_if_context,
|
||||
Context $outer_context,
|
||||
array $pre_assignment_else_redefined_vars
|
||||
): ?bool {
|
||||
$cond_referenced_var_ids = $if_conditional_scope->cond_referenced_var_ids;
|
||||
|
||||
$active_if_types = [];
|
||||
|
||||
$reconcilable_if_types = Algebra::getTruthsFromFormula(
|
||||
$if_context->clauses,
|
||||
spl_object_id($stmt->cond),
|
||||
$cond_referenced_var_ids,
|
||||
$active_if_types
|
||||
);
|
||||
|
||||
if (array_filter(
|
||||
$outer_context->clauses,
|
||||
fn($clause): bool => (bool)$clause->possibilities
|
||||
)) {
|
||||
$omit_keys = array_reduce(
|
||||
$outer_context->clauses,
|
||||
/**
|
||||
* @param array<string> $carry
|
||||
* @return array<string>
|
||||
*/
|
||||
fn(array $carry, Clause $clause): array => array_merge($carry, array_keys($clause->possibilities)),
|
||||
[]
|
||||
);
|
||||
|
||||
$omit_keys = array_combine($omit_keys, $omit_keys);
|
||||
$omit_keys = array_diff_key($omit_keys, Algebra::getTruthsFromFormula($outer_context->clauses));
|
||||
|
||||
$cond_referenced_var_ids = array_diff_key(
|
||||
$cond_referenced_var_ids,
|
||||
$omit_keys
|
||||
);
|
||||
}
|
||||
|
||||
// if the if has an || in the conditional, we cannot easily reason about it
|
||||
if ($reconcilable_if_types) {
|
||||
$changed_var_ids = [];
|
||||
|
||||
$if_vars_in_scope_reconciled =
|
||||
Reconciler::reconcileKeyedTypes(
|
||||
$reconcilable_if_types,
|
||||
$active_if_types,
|
||||
$if_context->vars_in_scope,
|
||||
$if_context->references_in_scope,
|
||||
$changed_var_ids,
|
||||
$cond_referenced_var_ids,
|
||||
$statements_analyzer,
|
||||
$statements_analyzer->getTemplateTypeMap() ?: [],
|
||||
$if_context->inside_loop,
|
||||
$outer_context->check_variables
|
||||
? new CodeLocation(
|
||||
$statements_analyzer->getSource(),
|
||||
$stmt->cond instanceof PhpParser\Node\Expr\BooleanNot
|
||||
? $stmt->cond->expr
|
||||
: $stmt->cond,
|
||||
$outer_context->include_location
|
||||
) : null
|
||||
);
|
||||
|
||||
$if_context->vars_in_scope = $if_vars_in_scope_reconciled;
|
||||
|
||||
foreach ($reconcilable_if_types as $var_id => $_) {
|
||||
$if_context->vars_possibly_in_scope[$var_id] = true;
|
||||
}
|
||||
|
||||
if ($changed_var_ids) {
|
||||
$if_context->clauses = Context::removeReconciledClauses($if_context->clauses, $changed_var_ids)[0];
|
||||
|
||||
foreach ($changed_var_ids as $changed_var_id => $_) {
|
||||
foreach ($if_context->vars_in_scope as $var_id => $_) {
|
||||
if (preg_match('/' . preg_quote($changed_var_id, '/') . '[\]\[\-]/', $var_id)
|
||||
&& !array_key_exists($var_id, $changed_var_ids)
|
||||
&& !array_key_exists($var_id, $cond_referenced_var_ids)
|
||||
) {
|
||||
$if_context->removePossibleReference($var_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$if_scope->if_cond_changed_var_ids = $changed_var_ids;
|
||||
}
|
||||
|
||||
$if_context->reconciled_expression_clauses = [];
|
||||
|
||||
$outer_context->vars_possibly_in_scope = array_merge(
|
||||
$if_context->vars_possibly_in_scope,
|
||||
$outer_context->vars_possibly_in_scope
|
||||
);
|
||||
|
||||
$outer_context->referenced_var_ids = array_merge(
|
||||
$if_context->referenced_var_ids,
|
||||
$outer_context->referenced_var_ids
|
||||
);
|
||||
|
||||
$old_if_context = clone $if_context;
|
||||
|
||||
$codebase = $statements_analyzer->getCodebase();
|
||||
|
||||
$assigned_var_ids = $if_context->assigned_var_ids;
|
||||
@ -77,7 +178,7 @@ class IfAnalyzer
|
||||
$outer_context->removeVarFromConflictingClauses($var_id);
|
||||
}
|
||||
|
||||
$final_actions = ScopeAnalyzer::getControlActions(
|
||||
$if_scope->if_actions = $final_actions = ScopeAnalyzer::getControlActions(
|
||||
$stmt->stmts,
|
||||
$statements_analyzer->node_data,
|
||||
[]
|
||||
@ -91,6 +192,7 @@ class IfAnalyzer
|
||||
$has_break_statement = $final_actions === [ScopeAnalyzer::ACTION_BREAK];
|
||||
$has_continue_statement = $final_actions === [ScopeAnalyzer::ACTION_CONTINUE];
|
||||
|
||||
$if_scope->if_actions = $final_actions;
|
||||
$if_scope->final_actions = $final_actions;
|
||||
|
||||
/** @var array<string, int> */
|
||||
@ -128,8 +230,6 @@ class IfAnalyzer
|
||||
}
|
||||
}
|
||||
|
||||
$mic_drop = false;
|
||||
|
||||
if (!$has_leaving_statements) {
|
||||
self::updateIfScope(
|
||||
$codebase,
|
||||
@ -168,27 +268,13 @@ class IfAnalyzer
|
||||
$if_conditional_scope->assigned_in_conditional_var_ids
|
||||
);
|
||||
}
|
||||
|
||||
if (!$stmt->else && !$stmt->elseifs) {
|
||||
$mic_drop = self::handleMicDrop(
|
||||
$statements_analyzer,
|
||||
$stmt->cond,
|
||||
$if_scope,
|
||||
$outer_context,
|
||||
$new_assigned_var_ids
|
||||
);
|
||||
|
||||
$outer_context->clauses = Algebra::simplifyCNF(
|
||||
array_merge($outer_context->clauses, $if_scope->negated_clauses)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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) {
|
||||
if ($if_scope->negated_types) {
|
||||
$vars_to_update = array_intersect(
|
||||
array_keys($pre_assignment_else_redefined_vars),
|
||||
array_keys($if_scope->negated_types)
|
||||
@ -208,19 +294,6 @@ class IfAnalyzer
|
||||
$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,
|
||||
@ -263,99 +336,6 @@ class IfAnalyzer
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* This handles the situation when returning inside an
|
||||
* if block with no else or elseifs
|
||||
*
|
||||
* @param array<string, int> $new_assigned_var_ids
|
||||
*/
|
||||
private static function handleMicDrop(
|
||||
StatementsAnalyzer $statements_analyzer,
|
||||
PhpParser\Node\Expr $cond,
|
||||
IfScope $if_scope,
|
||||
Context $post_if_context,
|
||||
array $new_assigned_var_ids
|
||||
): bool {
|
||||
if (!$if_scope->negated_types) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$newly_reconciled_var_ids = [];
|
||||
|
||||
$post_if_context_vars_reconciled = Reconciler::reconcileKeyedTypes(
|
||||
$if_scope->negated_types,
|
||||
[],
|
||||
$post_if_context->vars_in_scope,
|
||||
$post_if_context->references_in_scope,
|
||||
$newly_reconciled_var_ids,
|
||||
[],
|
||||
$statements_analyzer,
|
||||
$statements_analyzer->getTemplateTypeMap() ?: [],
|
||||
$post_if_context->inside_loop,
|
||||
new CodeLocation(
|
||||
$statements_analyzer->getSource(),
|
||||
$cond instanceof PhpParser\Node\Expr\BooleanNot
|
||||
? $cond->expr
|
||||
: $cond,
|
||||
$post_if_context->include_location,
|
||||
false
|
||||
)
|
||||
);
|
||||
|
||||
foreach ($newly_reconciled_var_ids as $changed_var_id => $_) {
|
||||
$post_if_context->removeVarFromConflictingClauses($changed_var_id);
|
||||
}
|
||||
|
||||
$newly_reconciled_var_ids += $new_assigned_var_ids;
|
||||
|
||||
foreach ($newly_reconciled_var_ids as $var_id => $_) {
|
||||
$if_scope->negated_clauses = Context::filterClauses(
|
||||
$var_id,
|
||||
$if_scope->negated_clauses
|
||||
);
|
||||
}
|
||||
|
||||
foreach ($newly_reconciled_var_ids as $var_id => $_) {
|
||||
$first_appearance = $statements_analyzer->getFirstAppearance($var_id);
|
||||
|
||||
if ($first_appearance
|
||||
&& isset($post_if_context->vars_in_scope[$var_id])
|
||||
&& isset($post_if_context_vars_reconciled[$var_id])
|
||||
&& $post_if_context->vars_in_scope[$var_id]->hasMixed()
|
||||
&& !$post_if_context_vars_reconciled[$var_id]->hasMixed()
|
||||
) {
|
||||
if (!$post_if_context->collect_initializations
|
||||
&& !$post_if_context->collect_mutations
|
||||
&& $statements_analyzer->getFilePath() === $statements_analyzer->getRootFilePath()
|
||||
) {
|
||||
$parent_source = $statements_analyzer->getSource();
|
||||
|
||||
$functionlike_storage = $parent_source instanceof FunctionLikeAnalyzer
|
||||
? $parent_source->getFunctionLikeStorage($statements_analyzer)
|
||||
: null;
|
||||
|
||||
if (!$functionlike_storage
|
||||
|| (!$parent_source->getSource() instanceof TraitAnalyzer
|
||||
&& !isset($functionlike_storage->param_lookup[substr($var_id, 1)]))
|
||||
) {
|
||||
$codebase = $statements_analyzer->getCodebase();
|
||||
$codebase->analyzer->decrementMixedCount($statements_analyzer->getFilePath());
|
||||
}
|
||||
}
|
||||
|
||||
IssueBuffer::remove(
|
||||
$statements_analyzer->getFilePath(),
|
||||
'MixedAssignment',
|
||||
$first_appearance->raw_file_start
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$post_if_context->vars_in_scope = $post_if_context_vars_reconciled;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, int> $assigned_in_conditional_var_ids
|
||||
*/
|
||||
|
@ -10,27 +10,26 @@ use Psalm\Exception\ScopeAnalysisException;
|
||||
use Psalm\Internal\Algebra;
|
||||
use Psalm\Internal\Algebra\FormulaGenerator;
|
||||
use Psalm\Internal\Analyzer\AlgebraAnalyzer;
|
||||
use Psalm\Internal\Analyzer\FunctionLikeAnalyzer;
|
||||
use Psalm\Internal\Analyzer\ScopeAnalyzer;
|
||||
use Psalm\Internal\Analyzer\Statements\Block\IfElse\ElseAnalyzer;
|
||||
use Psalm\Internal\Analyzer\Statements\Block\IfElse\ElseIfAnalyzer;
|
||||
use Psalm\Internal\Analyzer\Statements\Block\IfElse\IfAnalyzer;
|
||||
use Psalm\Internal\Analyzer\StatementsAnalyzer;
|
||||
use Psalm\Internal\Analyzer\TraitAnalyzer;
|
||||
use Psalm\Internal\Clause;
|
||||
use Psalm\Internal\Scope\IfScope;
|
||||
use Psalm\IssueBuffer;
|
||||
use Psalm\Node\Expr\VirtualBooleanNot;
|
||||
use Psalm\Type;
|
||||
use Psalm\Type\Reconciler;
|
||||
|
||||
use function array_combine;
|
||||
use function array_diff;
|
||||
use function array_diff_key;
|
||||
use function array_filter;
|
||||
use function array_intersect_key;
|
||||
use function array_key_exists;
|
||||
use function array_keys;
|
||||
use function array_map;
|
||||
use function array_merge;
|
||||
use function array_reduce;
|
||||
use function array_unique;
|
||||
use function array_values;
|
||||
use function count;
|
||||
@ -38,6 +37,7 @@ use function in_array;
|
||||
use function preg_match;
|
||||
use function preg_quote;
|
||||
use function spl_object_id;
|
||||
use function substr;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
@ -106,10 +106,11 @@ class IfElseAnalyzer
|
||||
$context->branch_point ?: (int) $stmt->getAttribute('startFilePos')
|
||||
);
|
||||
|
||||
// this is the context for stuff that happens within the `if` block
|
||||
$if_context = $if_conditional_scope->if_context;
|
||||
|
||||
// this is the context for stuff that happens after the `if` block
|
||||
$post_if_context = $if_conditional_scope->post_if_context;
|
||||
$cond_referenced_var_ids = $if_conditional_scope->cond_referenced_var_ids;
|
||||
$assigned_in_conditional_var_ids = $if_conditional_scope->assigned_in_conditional_var_ids;
|
||||
} catch (ScopeAnalysisException $e) {
|
||||
return false;
|
||||
@ -226,100 +227,6 @@ class IfElseAnalyzer
|
||||
)
|
||||
);
|
||||
|
||||
$active_if_types = [];
|
||||
|
||||
$reconcilable_if_types = Algebra::getTruthsFromFormula(
|
||||
$if_context->clauses,
|
||||
spl_object_id($stmt->cond),
|
||||
$cond_referenced_var_ids,
|
||||
$active_if_types
|
||||
);
|
||||
|
||||
if (array_filter(
|
||||
$context->clauses,
|
||||
fn($clause): bool => (bool)$clause->possibilities
|
||||
)) {
|
||||
$omit_keys = array_reduce(
|
||||
$context->clauses,
|
||||
/**
|
||||
* @param array<string> $carry
|
||||
* @return array<string>
|
||||
*/
|
||||
fn(array $carry, Clause $clause): array => 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
|
||||
);
|
||||
}
|
||||
|
||||
// if the if has an || in the conditional, we cannot easily reason about it
|
||||
if ($reconcilable_if_types) {
|
||||
$changed_var_ids = [];
|
||||
|
||||
$if_vars_in_scope_reconciled =
|
||||
Reconciler::reconcileKeyedTypes(
|
||||
$reconcilable_if_types,
|
||||
$active_if_types,
|
||||
$if_context->vars_in_scope,
|
||||
$if_context->references_in_scope,
|
||||
$changed_var_ids,
|
||||
$cond_referenced_var_ids,
|
||||
$statements_analyzer,
|
||||
$statements_analyzer->getTemplateTypeMap() ?: [],
|
||||
$if_context->inside_loop,
|
||||
$context->check_variables
|
||||
? new CodeLocation(
|
||||
$statements_analyzer->getSource(),
|
||||
$stmt->cond instanceof PhpParser\Node\Expr\BooleanNot
|
||||
? $stmt->cond->expr
|
||||
: $stmt->cond,
|
||||
$context->include_location
|
||||
) : null
|
||||
);
|
||||
|
||||
$if_context->vars_in_scope = $if_vars_in_scope_reconciled;
|
||||
|
||||
foreach ($reconcilable_if_types as $var_id => $_) {
|
||||
$if_context->vars_possibly_in_scope[$var_id] = true;
|
||||
}
|
||||
|
||||
if ($changed_var_ids) {
|
||||
$if_context->clauses = Context::removeReconciledClauses($if_context->clauses, $changed_var_ids)[0];
|
||||
|
||||
foreach ($changed_var_ids as $changed_var_id => $_) {
|
||||
foreach ($if_context->vars_in_scope as $var_id => $_) {
|
||||
if (preg_match('/' . preg_quote($changed_var_id, '/') . '[\]\[\-]/', $var_id)
|
||||
&& !array_key_exists($var_id, $changed_var_ids)
|
||||
&& !array_key_exists($var_id, $cond_referenced_var_ids)
|
||||
) {
|
||||
$if_context->removePossibleReference($var_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$if_scope->if_cond_changed_var_ids = $changed_var_ids;
|
||||
}
|
||||
|
||||
$if_context->reconciled_expression_clauses = [];
|
||||
|
||||
$old_if_context = clone $if_context;
|
||||
$context->vars_possibly_in_scope = array_merge(
|
||||
$if_context->vars_possibly_in_scope,
|
||||
$context->vars_possibly_in_scope
|
||||
);
|
||||
|
||||
$context->referenced_var_ids = array_merge(
|
||||
$if_context->referenced_var_ids,
|
||||
$context->referenced_var_ids
|
||||
);
|
||||
|
||||
$temp_else_context = clone $post_if_context;
|
||||
|
||||
$changed_var_ids = [];
|
||||
@ -362,7 +269,6 @@ class IfElseAnalyzer
|
||||
$if_scope,
|
||||
$if_conditional_scope,
|
||||
$if_context,
|
||||
$old_if_context,
|
||||
$context,
|
||||
$pre_assignment_else_redefined_vars
|
||||
) === false) {
|
||||
@ -405,6 +311,50 @@ class IfElseAnalyzer
|
||||
return false;
|
||||
}
|
||||
|
||||
if (count($if_scope->if_actions) && !in_array(ScopeAnalyzer::ACTION_NONE, $if_scope->if_actions, true)
|
||||
&& !$stmt->elseifs
|
||||
) {
|
||||
$context->clauses = $else_context->clauses;
|
||||
foreach ($else_context->vars_in_scope as $var_id => $type) {
|
||||
$context->vars_in_scope[$var_id] = clone $type;
|
||||
}
|
||||
|
||||
foreach ($pre_assignment_else_redefined_vars as $var_id => $reconciled_type) {
|
||||
$first_appearance = $statements_analyzer->getFirstAppearance($var_id);
|
||||
|
||||
if ($first_appearance
|
||||
&& isset($post_if_context->vars_in_scope[$var_id])
|
||||
&& $post_if_context->vars_in_scope[$var_id]->hasMixed()
|
||||
&& !$reconciled_type->hasMixed()
|
||||
) {
|
||||
if (!$post_if_context->collect_initializations
|
||||
&& !$post_if_context->collect_mutations
|
||||
&& $statements_analyzer->getFilePath() === $statements_analyzer->getRootFilePath()
|
||||
) {
|
||||
$parent_source = $statements_analyzer->getSource();
|
||||
|
||||
$functionlike_storage = $parent_source instanceof FunctionLikeAnalyzer
|
||||
? $parent_source->getFunctionLikeStorage($statements_analyzer)
|
||||
: null;
|
||||
|
||||
if (!$functionlike_storage
|
||||
|| (!$parent_source->getSource() instanceof TraitAnalyzer
|
||||
&& !isset($functionlike_storage->param_lookup[substr($var_id, 1)]))
|
||||
) {
|
||||
$codebase = $statements_analyzer->getCodebase();
|
||||
$codebase->analyzer->decrementMixedCount($statements_analyzer->getFilePath());
|
||||
}
|
||||
}
|
||||
|
||||
IssueBuffer::remove(
|
||||
$statements_analyzer->getFilePath(),
|
||||
'MixedAssignment',
|
||||
$first_appearance->raw_file_start
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($context->loop_scope) {
|
||||
$context->loop_scope->final_actions = array_unique(
|
||||
array_merge(
|
||||
|
@ -112,7 +112,8 @@ class AssignmentAnalyzer
|
||||
?Union $assign_value_type,
|
||||
Context $context,
|
||||
?PhpParser\Comment\Doc $doc_comment,
|
||||
array $not_ignored_docblock_var_ids = []
|
||||
array $not_ignored_docblock_var_ids = [],
|
||||
?PhpParser\Node\Expr $assign_expr = null
|
||||
) {
|
||||
$var_id = ExpressionIdentifier::getVarId(
|
||||
$assign_var,
|
||||
@ -616,6 +617,26 @@ class AssignmentAnalyzer
|
||||
$added_taints
|
||||
);
|
||||
}
|
||||
|
||||
if ($assign_expr) {
|
||||
$new_parent_node = DataFlowNode::getForAssignment(
|
||||
'assignment_expr',
|
||||
new CodeLocation($statements_analyzer->getSource(), $assign_expr)
|
||||
);
|
||||
|
||||
$data_flow_graph->addNode($new_parent_node);
|
||||
|
||||
foreach ($context->vars_in_scope[$var_id]->parent_nodes as $old_parent_node) {
|
||||
$data_flow_graph->addPath(
|
||||
$old_parent_node,
|
||||
$new_parent_node,
|
||||
'=',
|
||||
);
|
||||
}
|
||||
|
||||
$assign_value_type = clone $assign_value_type;
|
||||
$assign_value_type->parent_nodes = [$new_parent_node->id => $new_parent_node];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1083,7 +1104,7 @@ class AssignmentAnalyzer
|
||||
$by_ref_out_type->parent_nodes += $existing_type->parent_nodes;
|
||||
}
|
||||
|
||||
if (!$existing_type->isEmptyArray()) {
|
||||
if (!$context->inside_conditional) {
|
||||
$context->vars_in_scope[$var_id] = $by_ref_out_type;
|
||||
|
||||
if (!($stmt_type = $statements_analyzer->node_data->getType($stmt))
|
||||
@ -1505,7 +1526,7 @@ class AssignmentAnalyzer
|
||||
$assignment_node->id => $assignment_node
|
||||
];
|
||||
} else {
|
||||
if ($statements_analyzer->data_flow_graph instanceof TaintFlowGraph
|
||||
if ($data_flow_graph instanceof TaintFlowGraph
|
||||
&& in_array('TaintedInput', $statements_analyzer->getSuppressedIssues())
|
||||
) {
|
||||
$context->vars_in_scope[$list_var_id]->parent_nodes = [];
|
||||
|
@ -180,42 +180,38 @@ class AndAnalyzer
|
||||
);
|
||||
}
|
||||
|
||||
if ($context->if_context && !$context->inside_negation) {
|
||||
if ($context->if_body_context && !$context->inside_negation) {
|
||||
$if_body_context = $context->if_body_context;
|
||||
$context->vars_in_scope = $right_context->vars_in_scope;
|
||||
$if_context = $context->if_context;
|
||||
$if_body_context->vars_in_scope = array_merge(
|
||||
$if_body_context->vars_in_scope,
|
||||
$context->vars_in_scope
|
||||
);
|
||||
|
||||
foreach ($right_context->vars_in_scope as $var_id => $type) {
|
||||
if (!isset($if_context->vars_in_scope[$var_id])) {
|
||||
$if_context->vars_in_scope[$var_id] = $type;
|
||||
} elseif (isset($context->vars_in_scope[$var_id])) {
|
||||
$if_context->vars_in_scope[$var_id] = $context->vars_in_scope[$var_id];
|
||||
}
|
||||
}
|
||||
|
||||
$if_context->referenced_var_ids = array_merge(
|
||||
$if_body_context->referenced_var_ids = array_merge(
|
||||
$if_body_context->referenced_var_ids,
|
||||
$context->referenced_var_ids,
|
||||
$if_context->referenced_var_ids
|
||||
);
|
||||
|
||||
$if_context->assigned_var_ids = array_merge(
|
||||
$if_body_context->assigned_var_ids = array_merge(
|
||||
$if_body_context->assigned_var_ids,
|
||||
$context->assigned_var_ids,
|
||||
$if_context->assigned_var_ids
|
||||
);
|
||||
|
||||
$if_context->reconciled_expression_clauses = array_merge(
|
||||
$if_context->reconciled_expression_clauses,
|
||||
$if_body_context->reconciled_expression_clauses = array_merge(
|
||||
$if_body_context->reconciled_expression_clauses,
|
||||
array_map(
|
||||
fn($c) => $c->hash,
|
||||
$partitioned_clauses[1]
|
||||
)
|
||||
);
|
||||
|
||||
$if_context->vars_possibly_in_scope = array_merge(
|
||||
$if_body_context->vars_possibly_in_scope = array_merge(
|
||||
$if_body_context->vars_possibly_in_scope,
|
||||
$context->vars_possibly_in_scope,
|
||||
$if_context->vars_possibly_in_scope
|
||||
);
|
||||
|
||||
$if_context->updateChecks($context);
|
||||
$if_body_context->updateChecks($context);
|
||||
} else {
|
||||
$context->vars_in_scope = $left_context->vars_in_scope;
|
||||
}
|
||||
|
@ -64,6 +64,8 @@ class OrAnalyzer
|
||||
|
||||
$post_leaving_if_context = null;
|
||||
|
||||
// we cap this at max depth of 4 to prevent quadratic behaviour
|
||||
// when analysing <expr> || <expr> || <expr> || <expr> || <expr>
|
||||
if (!$stmt->left instanceof PhpParser\Node\Expr\BinaryOp\BooleanOr
|
||||
|| !$stmt->left->left instanceof PhpParser\Node\Expr\BinaryOp\BooleanOr
|
||||
|| !$stmt->left->left->left instanceof PhpParser\Node\Expr\BinaryOp\BooleanOr
|
||||
@ -100,7 +102,7 @@ class OrAnalyzer
|
||||
$post_leaving_if_context = clone $context;
|
||||
|
||||
$left_context = clone $context;
|
||||
$left_context->if_context = null;
|
||||
$left_context->if_body_context = null;
|
||||
$left_context->assigned_var_ids = [];
|
||||
|
||||
if (ExpressionAnalyzer::analyze($statements_analyzer, $stmt->left, $left_context) === false) {
|
||||
@ -259,7 +261,7 @@ class OrAnalyzer
|
||||
);
|
||||
}
|
||||
|
||||
$right_context->if_context = null;
|
||||
$right_context->if_body_context = null;
|
||||
|
||||
$pre_referenced_var_ids = $right_context->referenced_var_ids;
|
||||
$right_context->referenced_var_ids = [];
|
||||
@ -369,18 +371,18 @@ class OrAnalyzer
|
||||
$right_context->assigned_var_ids
|
||||
);
|
||||
|
||||
if ($context->if_context) {
|
||||
$if_context = $context->if_context;
|
||||
if ($context->if_body_context) {
|
||||
$if_body_context = $context->if_body_context;
|
||||
|
||||
foreach ($right_context->vars_in_scope as $var_id => $type) {
|
||||
if (isset($if_context->vars_in_scope[$var_id])) {
|
||||
$if_context->vars_in_scope[$var_id] = Type::combineUnionTypes(
|
||||
if (isset($if_body_context->vars_in_scope[$var_id])) {
|
||||
$if_body_context->vars_in_scope[$var_id] = Type::combineUnionTypes(
|
||||
$type,
|
||||
$if_context->vars_in_scope[$var_id],
|
||||
$if_body_context->vars_in_scope[$var_id],
|
||||
$codebase
|
||||
);
|
||||
} elseif (isset($left_context->vars_in_scope[$var_id])) {
|
||||
$if_context->vars_in_scope[$var_id] = Type::combineUnionTypes(
|
||||
$if_body_context->vars_in_scope[$var_id] = Type::combineUnionTypes(
|
||||
$type,
|
||||
$left_context->vars_in_scope[$var_id],
|
||||
$codebase
|
||||
@ -388,17 +390,17 @@ class OrAnalyzer
|
||||
}
|
||||
}
|
||||
|
||||
$if_context->referenced_var_ids = array_merge(
|
||||
$if_body_context->referenced_var_ids = array_merge(
|
||||
$context->referenced_var_ids,
|
||||
$if_context->referenced_var_ids
|
||||
$if_body_context->referenced_var_ids
|
||||
);
|
||||
|
||||
$if_context->assigned_var_ids = array_merge(
|
||||
$if_body_context->assigned_var_ids = array_merge(
|
||||
$context->assigned_var_ids,
|
||||
$if_context->assigned_var_ids
|
||||
$if_body_context->assigned_var_ids
|
||||
);
|
||||
|
||||
$if_context->updateChecks($context);
|
||||
$if_body_context->updateChecks($context);
|
||||
}
|
||||
|
||||
$context->vars_possibly_in_scope = array_merge(
|
||||
|
@ -129,7 +129,7 @@ class IncDecExpressionAnalyzer
|
||||
if ($stmt instanceof PreInc || $stmt instanceof PreDec) {
|
||||
$old_node_data->setType(
|
||||
$stmt,
|
||||
$statements_analyzer->node_data->getType($operation) ?? Type::getMixed()
|
||||
$statements_analyzer->node_data->getType($fake_assignment) ?? Type::getMixed()
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -180,7 +180,9 @@ class ExpressionAnalyzer
|
||||
$stmt->expr,
|
||||
null,
|
||||
$context,
|
||||
$stmt->getDocComment()
|
||||
$stmt->getDocComment(),
|
||||
[],
|
||||
!$from_stmt ? $stmt : null
|
||||
);
|
||||
|
||||
if ($assignment_type === false) {
|
||||
|
@ -160,6 +160,7 @@ class VariableUseGraph extends DataFlowGraph
|
||||
|| $path->type === 'use-inside-conditional'
|
||||
|| $path->type === 'use-inside-isset'
|
||||
|| $path->type === 'arg'
|
||||
|| $path->type === 'comparison'
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
@ -76,6 +76,11 @@ class IfScope
|
||||
*/
|
||||
public $reasonable_clauses = [];
|
||||
|
||||
/**
|
||||
* @var string[]
|
||||
*/
|
||||
public $if_actions = [];
|
||||
|
||||
/**
|
||||
* @var string[]
|
||||
*/
|
||||
|
@ -121,6 +121,7 @@ class DoTest extends TestCase
|
||||
break;
|
||||
}
|
||||
|
||||
/** @psalm-suppress MixedArgument */
|
||||
foo($a);
|
||||
}
|
||||
while (rand(0,100) === 10);',
|
||||
|
@ -2083,6 +2083,7 @@ class ConditionalTest extends TestCase
|
||||
/**
|
||||
* @psalm-suppress MixedReturnStatement
|
||||
* @psalm-suppress MixedInferredReturnType
|
||||
* @psalm-suppress MixedArrayAccess
|
||||
*/
|
||||
public static function get(string $k1, string $k2) : ?string {
|
||||
if (!isset(static::$cache[$k1][$k2])) {
|
||||
|
@ -1527,6 +1527,19 @@ class RedundantConditionTest extends TestCase
|
||||
}',
|
||||
'error_message' => 'RedundantCondition'
|
||||
],
|
||||
'secondFalsyTwiceWithoutChangeWithElse' => [
|
||||
'code' => '<?php
|
||||
/**
|
||||
* @param array{a?:int,b?:string} $p
|
||||
*/
|
||||
function f(array $p) : void {
|
||||
if (!$p) {
|
||||
throw new RuntimeException("");
|
||||
} else {}
|
||||
assert(!!$p);
|
||||
}',
|
||||
'error_message' => 'RedundantCondition'
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -19,18 +19,16 @@ class TypeAlgebraTest extends TestCase
|
||||
return [
|
||||
'twoVarLogicSimple' => [
|
||||
'code' => '<?php
|
||||
function takesString(string $s): void {}
|
||||
|
||||
function foo(?string $a, ?string $b): void {
|
||||
function foo(?string $a, ?string $b): string {
|
||||
if ($a !== null || $b !== null) {
|
||||
if ($a !== null) {
|
||||
$c = $a;
|
||||
return $a;
|
||||
} else {
|
||||
$c = $b;
|
||||
return $b;
|
||||
}
|
||||
|
||||
takesString($c);
|
||||
}
|
||||
|
||||
return "foo";
|
||||
}',
|
||||
],
|
||||
'threeVarLogic' => [
|
||||
@ -1194,8 +1192,6 @@ class TypeAlgebraTest extends TestCase
|
||||
],
|
||||
'threeVarLogicWithException' => [
|
||||
'code' => '<?php
|
||||
function takesString(string $s): void {}
|
||||
|
||||
function foo(?string $a, ?string $b, ?string $c): void {
|
||||
if ($a !== null || $b !== null || $c !== null) {
|
||||
if ($c !== null) {
|
||||
@ -1209,11 +1205,9 @@ class TypeAlgebraTest extends TestCase
|
||||
} else {
|
||||
$d = $c;
|
||||
}
|
||||
|
||||
takesString($d);
|
||||
}
|
||||
}',
|
||||
'error_message' => 'PossiblyNullArgument',
|
||||
'error_message' => 'RedundantCondition',
|
||||
],
|
||||
'invertedTwoVarLogicNotNestedWithVarChange' => [
|
||||
'code' => '<?php
|
||||
|
@ -2461,18 +2461,6 @@ class UnusedVariableTest extends TestCase
|
||||
$a = false;
|
||||
}'
|
||||
],
|
||||
'usedPlusInAddition' => [
|
||||
'code' => '<?php
|
||||
function takesAnInt(): void {
|
||||
$i = 0;
|
||||
|
||||
while (rand(0, 1)) {
|
||||
if (++$i > 10) {
|
||||
break;
|
||||
} else {}
|
||||
}
|
||||
}',
|
||||
],
|
||||
'referenceUseUsesReferencedVariable' => [
|
||||
'code' => '<?php
|
||||
$a = 1;
|
||||
@ -2537,6 +2525,30 @@ class UnusedVariableTest extends TestCase
|
||||
function takesArray(array $_arr): void {}
|
||||
',
|
||||
],
|
||||
'usedPlusInAddition' => [
|
||||
'code' => '<?php
|
||||
function takesAnInt(): void {
|
||||
$i = 0;
|
||||
|
||||
while (rand(0, 1)) {
|
||||
if (($i = $i + 1) > 10) {
|
||||
break;
|
||||
} else {}
|
||||
}
|
||||
}',
|
||||
],
|
||||
'usedPlusInUnaryAddition' => [
|
||||
'code' => '<?php
|
||||
function takesAnInt(): void {
|
||||
$i = 0;
|
||||
|
||||
while (rand(0, 1)) {
|
||||
if (++$i > 10) {
|
||||
break;
|
||||
} else {}
|
||||
}
|
||||
}',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user