1
0
mirror of https://github.com/danog/psalm.git synced 2024-11-27 12:55:26 +01:00

Improve binary op || analysis for chain

This commit is contained in:
Matt Brown 2020-10-17 17:16:47 -04:00
parent 559abb7b6a
commit c0f6afbd87
3 changed files with 76 additions and 36 deletions

View File

@ -884,39 +884,13 @@ class IfAnalyzer
&& $stmt->cond instanceof PhpParser\Node\Expr\BinaryOp\BooleanOr && $stmt->cond instanceof PhpParser\Node\Expr\BinaryOp\BooleanOr
&& $if_scope->mic_drop_context && $if_scope->mic_drop_context
) { ) {
$exprs = self::getDefinitelyEvaluatedOredExpressions($stmt->cond); self::addConditionallyAssignedVarsToContext(
$statements_analyzer,
// if there was no assignment in the first expression it's safe to proceed $stmt->cond,
$old_node_data = $statements_analyzer->node_data; $if_scope->mic_drop_context,
$statements_analyzer->node_data = clone $old_node_data; $outer_context,
$if_conditional_scope->cond_assigned_var_ids
foreach ($exprs as $expr) { );
$fake_negated_expr = new PhpParser\Node\Expr\FuncCall(
new PhpParser\Node\Name\FullyQualified('assert'),
[new PhpParser\Node\Arg(
new PhpParser\Node\Expr\BooleanNot($expr, $expr->getAttributes()),
false,
false,
$expr->getAttributes()
)],
$expr->getAttributes()
);
ExpressionAnalyzer::analyze(
$statements_analyzer,
$fake_negated_expr,
$if_scope->mic_drop_context
);
}
$statements_analyzer->node_data = $old_node_data;
foreach ($if_conditional_scope->cond_assigned_var_ids as $var_id => $_) {
if (isset($if_scope->mic_drop_context->vars_in_scope[$var_id])) {
$outer_context->vars_in_scope[$var_id]
= clone $if_scope->mic_drop_context->vars_in_scope[$var_id];
}
}
} }
if ($if_scope->negated_types) { if ($if_scope->negated_types) {
@ -1868,4 +1842,48 @@ class IfAnalyzer
return [$stmt]; return [$stmt];
} }
/**
* @param array<string, bool> $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 {
$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;
foreach ($exprs as $expr) {
$fake_negated_expr = new PhpParser\Node\Expr\FuncCall(
new PhpParser\Node\Name\FullyQualified('assert'),
[new PhpParser\Node\Arg(
new PhpParser\Node\Expr\BooleanNot($expr, $expr->getAttributes()),
false,
false,
$expr->getAttributes()
)],
$expr->getAttributes()
);
ExpressionAnalyzer::analyze(
$statements_analyzer,
$fake_negated_expr,
$mic_drop_context
);
}
$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];
}
}
}
} }

View File

@ -47,9 +47,11 @@ class OrAnalyzer
$codebase = $statements_analyzer->getCodebase(); $codebase = $statements_analyzer->getCodebase();
$mic_drop_context = null;
if (!$stmt->left instanceof PhpParser\Node\Expr\BinaryOp\BooleanOr if (!$stmt->left instanceof PhpParser\Node\Expr\BinaryOp\BooleanOr
&& !($stmt->left instanceof PhpParser\Node\Expr\BooleanNot || !$stmt->left->left instanceof PhpParser\Node\Expr\BinaryOp\BooleanOr
&& $stmt->left->expr instanceof PhpParser\Node\Expr\BinaryOp\BooleanAnd) || !$stmt->left->left->left instanceof PhpParser\Node\Expr\BinaryOp\BooleanOr
) { ) {
$if_scope = new \Psalm\Internal\Scope\IfScope(); $if_scope = new \Psalm\Internal\Scope\IfScope();
@ -66,6 +68,11 @@ class OrAnalyzer
$left_context = $if_conditional_scope->if_context; $left_context = $if_conditional_scope->if_context;
$left_referenced_var_ids = $if_conditional_scope->cond_referenced_var_ids; $left_referenced_var_ids = $if_conditional_scope->cond_referenced_var_ids;
$left_assigned_var_ids = $if_conditional_scope->cond_assigned_var_ids;
if ($stmt->left instanceof PhpParser\Node\Expr\BinaryOp\BooleanOr) {
$mic_drop_context = clone $context;
}
} catch (\Psalm\Exception\ScopeAnalysisException $e) { } catch (\Psalm\Exception\ScopeAnalysisException $e) {
return false; return false;
} }
@ -75,6 +82,8 @@ class OrAnalyzer
$pre_assigned_var_ids = $context->assigned_var_ids; $pre_assigned_var_ids = $context->assigned_var_ids;
$mic_drop_context = clone $context;
$left_context = clone $context; $left_context = clone $context;
$left_context->parent_context = $context; $left_context->parent_context = $context;
$left_context->if_context = null; $left_context->if_context = null;
@ -176,6 +185,19 @@ class OrAnalyzer
$right_context = clone $context; $right_context = clone $context;
if ($stmt->left instanceof PhpParser\Node\Expr\BinaryOp\BooleanOr
&& $left_assigned_var_ids
&& $mic_drop_context
) {
IfAnalyzer::addConditionallyAssignedVarsToContext(
$statements_analyzer,
$stmt->left,
$mic_drop_context,
$right_context,
$left_assigned_var_ids
);
}
if ($negated_type_assertions) { if ($negated_type_assertions) {
// while in an or, we allow scope to boil over to support // while in an or, we allow scope to boil over to support
// statements of the form if ($x === null || $x->foo()) // statements of the form if ($x === null || $x->foo())

View File

@ -1935,7 +1935,7 @@ class ConditionalTest extends \Psalm\Tests\TestCase
'$a' => 'bool', '$a' => 'bool',
] ]
], ],
'SKIPPED-assertVarRedefinedInOpWithOr' => [ 'assertVarRedefinedInOpWithOr' => [
'<?php '<?php
class O { class O {
public function foo() : bool { return true; } public function foo() : bool { return true; }