1
0
mirror of https://github.com/danog/psalm.git synced 2025-01-22 05:41:20 +01:00

Treat $a ?? $b identically to isset($a) ? $a : $b

This commit is contained in:
Matt Brown 2020-11-25 14:34:05 -05:00 committed by Daniil Gentili
parent 5228ff6369
commit 033a209950
Signed by: danog
GPG Key ID: 8C1BE3B34B230CA7
5 changed files with 42 additions and 241 deletions

View File

@ -520,25 +520,6 @@ class AssertionFinder
return $if_types ? [$if_types] : [];
}
if ($conditional instanceof PhpParser\Node\Expr\BinaryOp\Coalesce) {
return self::scrapeAssertions(
new PhpParser\Node\Expr\Ternary(
new PhpParser\Node\Expr\Isset_(
[$conditional->left]
),
$conditional->left,
$conditional->right,
$conditional->getAttributes()
),
$this_class_name,
$source,
$codebase,
$inside_negation,
false,
$inside_conditional
);
}
return [];
}

View File

@ -27,240 +27,52 @@ class CoalesceAnalyzer
{
public static function analyze(
StatementsAnalyzer $statements_analyzer,
PhpParser\Node\Expr\BinaryOp $stmt,
PhpParser\Node\Expr\BinaryOp\Coalesce $stmt,
Context $context
) : bool {
$t_if_context = clone $context;
$left_expr = $stmt->left;
$codebase = $statements_analyzer->getCodebase();
if ($left_expr instanceof PhpParser\Node\Expr\FuncCall
|| $left_expr instanceof PhpParser\Node\Expr\MethodCall
|| $left_expr instanceof PhpParser\Node\Expr\StaticCall
|| $left_expr instanceof PhpParser\Node\Expr\Cast
) {
$left_var_id = '$<tmp coalesce var>' . (int) $left_expr->getAttribute('startFilePos');
$stmt_id = \spl_object_id($stmt);
ExpressionAnalyzer::analyze($statements_analyzer, $left_expr, clone $context);
$if_clauses = FormulaGenerator::getFormula(
$stmt_id,
$stmt_id,
$stmt,
$context->self,
$statements_analyzer,
$codebase
$condition_type = $statements_analyzer->node_data->getType($left_expr) ?: Type::getMixed();
$context->vars_in_scope[$left_var_id] = $condition_type;
$left_expr = new PhpParser\Node\Expr\Variable(
substr($left_var_id, 1),
$left_expr->getAttributes()
);
}
$ternary = new PhpParser\Node\Expr\Ternary(
new PhpParser\Node\Expr\Isset_(
[$left_expr],
$stmt->left->getAttributes()
),
$left_expr,
$stmt->right,
$stmt->getAttributes()
);
$mixed_var_ids = [];
$old_node_data = $statements_analyzer->node_data;
foreach ($context->vars_in_scope as $var_id => $type) {
if ($type->hasMixed()) {
$mixed_var_ids[] = $var_id;
}
}
$statements_analyzer->node_data = clone $statements_analyzer->node_data;
foreach ($context->vars_possibly_in_scope as $var_id => $_) {
if (!isset($context->vars_in_scope[$var_id])) {
$mixed_var_ids[] = $var_id;
}
}
ExpressionAnalyzer::analyze($statements_analyzer, $ternary, clone $context);
$if_clauses = array_values(
array_map(
function (\Psalm\Internal\Clause $c) use ($mixed_var_ids, $stmt_id): \Psalm\Internal\Clause {
$keys = array_keys($c->possibilities);
$ternary_type = $statements_analyzer->node_data->getType($ternary) ?: Type::getMixed();
$mixed_var_ids = \array_diff($mixed_var_ids, $keys);
$statements_analyzer->node_data = $old_node_data;
foreach ($keys as $key) {
foreach ($mixed_var_ids as $mixed_var_id) {
if (preg_match('/^' . preg_quote($mixed_var_id, '/') . '(\[|-)/', $key)) {
return new \Psalm\Internal\Clause([], $stmt_id, $stmt_id, true);
}
}
}
return $c;
},
$if_clauses
)
);
$ternary_clauses = Algebra::simplifyCNF(array_merge($context->clauses, $if_clauses));
$negated_clauses = Algebra::negateFormula($if_clauses);
$negated_if_types = Algebra::getTruthsFromFormula($negated_clauses);
$reconcilable_if_types = Algebra::getTruthsFromFormula($ternary_clauses);
$changed_var_ids = [];
if ($reconcilable_if_types) {
$t_if_vars_in_scope_reconciled = Reconciler::reconcileKeyedTypes(
$reconcilable_if_types,
[],
$t_if_context->vars_in_scope,
$changed_var_ids,
[],
$statements_analyzer,
[],
$t_if_context->inside_loop,
new CodeLocation($statements_analyzer->getSource(), $stmt->left)
);
foreach ($context->vars_in_scope as $var_id => $_) {
if (isset($t_if_vars_in_scope_reconciled[$var_id])) {
$t_if_context->vars_in_scope[$var_id] = $t_if_vars_in_scope_reconciled[$var_id];
}
}
}
if (!self::hasArrayDimFetch($stmt->left)) {
// check first if the variable was good
IssueBuffer::startRecording();
ExpressionAnalyzer::analyze($statements_analyzer, $stmt->left, clone $context);
IssueBuffer::clearRecordingLevel();
IssueBuffer::stopRecording();
$naive_type = $statements_analyzer->node_data->getType($stmt->left);
if ($naive_type
&& !$naive_type->possibly_undefined
&& !$naive_type->hasMixed()
&& !$naive_type->isNullable()
) {
$var_id = ExpressionIdentifier::getVarId($stmt->left, $context->self);
if (!$var_id
|| ($var_id !== '$_SESSION' && $var_id !== '$_SERVER' && !isset($changed_var_ids[$var_id]))
) {
if ($naive_type->from_docblock) {
if (IssueBuffer::accepts(
new \Psalm\Issue\DocblockTypeContradiction(
$naive_type->getId() . ' does not contain null',
new CodeLocation($statements_analyzer, $stmt->left),
$naive_type->getId() . ' null'
),
$statements_analyzer->getSuppressedIssues()
)) {
// fall through
}
} else {
if (IssueBuffer::accepts(
new \Psalm\Issue\TypeDoesNotContainType(
$naive_type->getId() . ' is always defined and non-null',
new CodeLocation($statements_analyzer, $stmt->left),
null
),
$statements_analyzer->getSuppressedIssues()
)) {
// fall through
}
}
}
}
}
$t_if_context->inside_isset = true;
if (ExpressionAnalyzer::analyze($statements_analyzer, $stmt->left, $t_if_context) === false) {
return false;
}
$t_if_context->inside_isset = false;
foreach ($t_if_context->vars_in_scope as $var_id => $type) {
if (isset($context->vars_in_scope[$var_id])) {
$context->vars_in_scope[$var_id] = Type::combineUnionTypes(
$context->vars_in_scope[$var_id],
$type,
$codebase
);
} else {
$context->vars_in_scope[$var_id] = $type;
}
}
$context->referenced_var_ids = array_merge(
$context->referenced_var_ids,
$t_if_context->referenced_var_ids
);
$t_else_context = clone $context;
if ($negated_if_types) {
$t_else_vars_in_scope_reconciled = Reconciler::reconcileKeyedTypes(
$negated_if_types,
[],
$t_else_context->vars_in_scope,
$changed_var_ids,
[],
$statements_analyzer,
[],
$t_else_context->inside_loop,
new CodeLocation($statements_analyzer->getSource(), $stmt->right)
);
$t_else_context->vars_in_scope = $t_else_vars_in_scope_reconciled;
}
if (ExpressionAnalyzer::analyze($statements_analyzer, $stmt->right, $t_else_context) === false) {
return false;
}
$context->referenced_var_ids = array_merge(
$context->referenced_var_ids,
$t_else_context->referenced_var_ids
);
$lhs_type = null;
$stmt_left_type = $statements_analyzer->node_data->getType($stmt->left);
if ($stmt_left_type) {
$if_return_type_reconciled = AssertionReconciler::reconcile(
'isset',
clone $stmt_left_type,
'',
$statements_analyzer,
$context->inside_loop,
[],
new CodeLocation($statements_analyzer->getSource(), $stmt),
$statements_analyzer->getSuppressedIssues()
);
$lhs_type = clone $if_return_type_reconciled;
}
$stmt_right_type = null;
if (!$lhs_type || !($stmt_right_type = $statements_analyzer->node_data->getType($stmt->right))) {
$stmt_type = Type::getMixed();
$statements_analyzer->node_data->setType($stmt, $stmt_type);
} else {
$stmt_type = Type::combineUnionTypes(
$lhs_type,
$stmt_right_type,
$codebase
);
$statements_analyzer->node_data->setType($stmt, $stmt_type);
}
$statements_analyzer->node_data->setType($stmt, $ternary_type);
return true;
}
private static function hasArrayDimFetch(PhpParser\Node\Expr $expr) : bool
{
if ($expr instanceof PhpParser\Node\Expr\ArrayDimFetch) {
return true;
}
if ($expr instanceof PhpParser\Node\Expr\PropertyFetch
|| $expr instanceof PhpParser\Node\Expr\MethodCall
) {
return self::hasArrayDimFetch($expr->var);
}
return false;
}
}

View File

@ -2895,7 +2895,7 @@ class ConditionalTest extends \Psalm\Tests\TestCase
function foo(?string $s) : string {
return ((string) $s) ?? "bar";
}',
'error_message' => 'TypeDoesNotContainType'
'error_message' => 'RedundantCondition'
],
'allowEmptyScalarAndNonEmptyScalarAssertions1' => [
'<?php

View File

@ -1020,6 +1020,13 @@ class IssetTest extends \Psalm\Tests\TestCase
return isset($a) ? $a : $b;
}'
],
'assertComplexWithNullCoalesce' => [
'<?php
function returnsInt(?int $a, ?int $b): int {
assert($a !== null || $b !== null);
return $a ?? $b;
}'
],
];
}

View File

@ -373,6 +373,7 @@ class RedundantConditionTest extends \Psalm\Tests\TestCase
$options = ["option" => true];
}
/** @psalm-suppress PossiblyUndefinedGlobalVariable */
$option = $options["option"] ?? false;
if ($option) {}',