mirror of
https://github.com/danog/psalm.git
synced 2024-11-29 20:28:59 +01:00
Fix #4374 - prevent paradox and allow Psalm to understand more assignments in conditionals
This commit is contained in:
parent
26352d0e39
commit
7df404bfb5
@ -476,7 +476,7 @@ return [
|
||||
'asin' => ['float', 'number'=>'float'],
|
||||
'asinh' => ['float', 'number'=>'float'],
|
||||
'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'],
|
||||
'ast\get_kind_name' => ['string', 'kind'=>'int'],
|
||||
'ast\get_metadata' => ['array<int,ast\Metadata>'],
|
||||
|
@ -79,10 +79,14 @@ class IfAnalyzer
|
||||
$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\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(
|
||||
$stmt->stmts,
|
||||
$statements_analyzer->node_data,
|
||||
null,
|
||||
$codebase->config->exit_functions,
|
||||
$context->break_types
|
||||
);
|
||||
@ -881,7 +885,6 @@ class IfAnalyzer
|
||||
if ($has_leaving_statements && !$has_break_statement && !$stmt->else && !$stmt->elseifs) {
|
||||
// If we're assigning inside
|
||||
if ($if_conditional_scope->cond_assigned_var_ids
|
||||
&& $stmt->cond instanceof PhpParser\Node\Expr\BinaryOp\BooleanOr
|
||||
&& $if_scope->mic_drop_context
|
||||
) {
|
||||
self::addConditionallyAssignedVarsToContext(
|
||||
@ -1866,23 +1869,23 @@ class IfAnalyzer
|
||||
$old_node_data = $statements_analyzer->node_data;
|
||||
$statements_analyzer->node_data = clone $old_node_data;
|
||||
|
||||
$suppressed_issues = $statements_analyzer->getSuppressedIssues();
|
||||
|
||||
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']);
|
||||
}
|
||||
IssueBuffer::startRecording();
|
||||
|
||||
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(
|
||||
new PhpParser\Node\Expr\BooleanNot($expr, $expr->getAttributes()),
|
||||
$fake_not,
|
||||
false,
|
||||
false,
|
||||
$expr->getAttributes()
|
||||
@ -1901,15 +1904,8 @@ class IfAnalyzer
|
||||
$mic_drop_context->inside_negation = !$mic_drop_context->inside_negation;
|
||||
}
|
||||
|
||||
if (!in_array('RedundantCondition', $suppressed_issues, true)) {
|
||||
$statements_analyzer->removeSuppressedIssues(['RedundantCondition']);
|
||||
}
|
||||
if (!in_array('RedundantConditionGivenDocblockType', $suppressed_issues, true)) {
|
||||
$statements_analyzer->removeSuppressedIssues(['RedundantConditionGivenDocblockType']);
|
||||
}
|
||||
if (!in_array('TypeDoesNotContainType', $suppressed_issues, true)) {
|
||||
$statements_analyzer->removeSuppressedIssues(['TypeDoesNotContainType']);
|
||||
}
|
||||
IssueBuffer::clearRecordingLevel();
|
||||
IssueBuffer::stopRecording();
|
||||
|
||||
$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());
|
||||
}
|
||||
}
|
||||
|
@ -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',
|
||||
],
|
||||
'assignmentInBranchOfAnd' => [
|
||||
'<?php
|
||||
function foo(string $str): ?int {
|
||||
$pos = 5;
|
||||
|
||||
if (rand(0, 1) && !($pos = $str)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $pos;
|
||||
}',
|
||||
'error_message' => 'InvalidReturnStatement',
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user