1
0
mirror of https://github.com/danog/psalm.git synced 2024-11-30 04:39:00 +01:00

Fix #4374 - prevent paradox and allow Psalm to understand more assignments in conditionals

This commit is contained in:
Matt Brown 2020-10-20 14:43:05 -04:00 committed by Daniil Gentili
parent 26352d0e39
commit 7df404bfb5
Signed by: danog
GPG Key ID: 8C1BE3B34B230CA7
3 changed files with 59 additions and 25 deletions

View File

@ -476,7 +476,7 @@ return [
'asin' => ['float', 'number'=>'float'], 'asin' => ['float', 'number'=>'float'],
'asinh' => ['float', 'number'=>'float'], 'asinh' => ['float', 'number'=>'float'],
'asort' => ['bool', '&rw_array'=>'array', 'sort_flags='=>'int'], 'asort' => ['bool', '&rw_array'=>'array', 'sort_flags='=>'int'],
'assert' => ['bool', 'assertion'=>'string|bool', 'description='=>'string|Throwable|null'], 'assert' => ['bool', 'assertion'=>'string|bool|int', 'description='=>'string|Throwable|null'],
'assert_options' => ['mixed|false', 'what'=>'int', 'value='=>'mixed'], 'assert_options' => ['mixed|false', 'what'=>'int', 'value='=>'mixed'],
'ast\get_kind_name' => ['string', 'kind'=>'int'], 'ast\get_kind_name' => ['string', 'kind'=>'int'],
'ast\get_metadata' => ['array<int,ast\Metadata>'], 'ast\get_metadata' => ['array<int,ast\Metadata>'],

View File

@ -79,10 +79,14 @@ class IfAnalyzer
$if_scope = new IfScope(); $if_scope = new IfScope();
// We need to clone the original context for later use if we're exiting in this if conditional // 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\BooleanOr) { 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( $final_actions = ScopeAnalyzer::getControlActions(
$stmt->stmts, $stmt->stmts,
$statements_analyzer->node_data, null,
$codebase->config->exit_functions, $codebase->config->exit_functions,
$context->break_types $context->break_types
); );
@ -881,7 +885,6 @@ class IfAnalyzer
if ($has_leaving_statements && !$has_break_statement && !$stmt->else && !$stmt->elseifs) { if ($has_leaving_statements && !$has_break_statement && !$stmt->else && !$stmt->elseifs) {
// If we're assigning inside // If we're assigning inside
if ($if_conditional_scope->cond_assigned_var_ids if ($if_conditional_scope->cond_assigned_var_ids
&& $stmt->cond instanceof PhpParser\Node\Expr\BinaryOp\BooleanOr
&& $if_scope->mic_drop_context && $if_scope->mic_drop_context
) { ) {
self::addConditionallyAssignedVarsToContext( self::addConditionallyAssignedVarsToContext(
@ -1866,23 +1869,23 @@ class IfAnalyzer
$old_node_data = $statements_analyzer->node_data; $old_node_data = $statements_analyzer->node_data;
$statements_analyzer->node_data = clone $old_node_data; $statements_analyzer->node_data = clone $old_node_data;
$suppressed_issues = $statements_analyzer->getSuppressedIssues(); IssueBuffer::startRecording();
if (!in_array('RedundantCondition', $suppressed_issues, true)) {
$statements_analyzer->addSuppressedIssues(['RedundantCondition']);
}
if (!in_array('RedundantConditionGivenDocblockType', $suppressed_issues, true)) {
$statements_analyzer->addSuppressedIssues(['RedundantConditionGivenDocblockType']);
}
if (!in_array('TypeDoesNotContainType', $suppressed_issues, true)) {
$statements_analyzer->addSuppressedIssues(['TypeDoesNotContainType']);
}
foreach ($exprs as $expr) { 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( $fake_negated_expr = new PhpParser\Node\Expr\FuncCall(
new PhpParser\Node\Name\FullyQualified('assert'), new PhpParser\Node\Name\FullyQualified('assert'),
[new PhpParser\Node\Arg( [new PhpParser\Node\Arg(
new PhpParser\Node\Expr\BooleanNot($expr, $expr->getAttributes()), $fake_not,
false, false,
false, false,
$expr->getAttributes() $expr->getAttributes()
@ -1901,15 +1904,8 @@ class IfAnalyzer
$mic_drop_context->inside_negation = !$mic_drop_context->inside_negation; $mic_drop_context->inside_negation = !$mic_drop_context->inside_negation;
} }
if (!in_array('RedundantCondition', $suppressed_issues, true)) { IssueBuffer::clearRecordingLevel();
$statements_analyzer->removeSuppressedIssues(['RedundantCondition']); IssueBuffer::stopRecording();
}
if (!in_array('RedundantConditionGivenDocblockType', $suppressed_issues, true)) {
$statements_analyzer->removeSuppressedIssues(['RedundantConditionGivenDocblockType']);
}
if (!in_array('TypeDoesNotContainType', $suppressed_issues, true)) {
$statements_analyzer->removeSuppressedIssues(['TypeDoesNotContainType']);
}
$statements_analyzer->node_data = $old_node_data; $statements_analyzer->node_data = $old_node_data;
@ -1919,4 +1915,13 @@ class IfAnalyzer
} }
} }
} }
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());
}
} }

View File

@ -2942,6 +2942,22 @@ class ConditionalTest extends \Psalm\Tests\TestCase
) {} ) {}
}' }'
], ],
'assertWithAssignmentInOr' => [
'function test(int $x = null): int {
\assert($x || ($x = rand(0, 10)));
return $x;
}'
],
'noParadoxicalConditionAfterTwoAssignments' => [
'<?php
function foo(string $str): ?int {
if (rand(0, 1) || (!($pos = strpos($str, "a")) && !($pos = strpos($str, "b")))) {
return null;
}
return $pos;
}'
],
]; ];
} }
@ -3322,6 +3338,19 @@ class ConditionalTest extends \Psalm\Tests\TestCase
}', }',
'error_message' => 'TypeDoesNotContainType', 'error_message' => 'TypeDoesNotContainType',
], ],
'assignmentInBranchOfAnd' => [
'<?php
function foo(string $str): ?int {
$pos = 5;
if (rand(0, 1) && !($pos = $str)) {
return null;
}
return $pos;
}',
'error_message' => 'InvalidReturnStatement',
],
]; ];
} }
} }