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:
parent
5228ff6369
commit
033a209950
@ -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 [];
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}'
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -373,6 +373,7 @@ class RedundantConditionTest extends \Psalm\Tests\TestCase
|
||||
$options = ["option" => true];
|
||||
}
|
||||
|
||||
/** @psalm-suppress PossiblyUndefinedGlobalVariable */
|
||||
$option = $options["option"] ?? false;
|
||||
|
||||
if ($option) {}',
|
||||
|
Loading…
x
Reference in New Issue
Block a user