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] : [];
|
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 [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -27,240 +27,52 @@ class CoalesceAnalyzer
|
|||||||
{
|
{
|
||||||
public static function analyze(
|
public static function analyze(
|
||||||
StatementsAnalyzer $statements_analyzer,
|
StatementsAnalyzer $statements_analyzer,
|
||||||
PhpParser\Node\Expr\BinaryOp $stmt,
|
PhpParser\Node\Expr\BinaryOp\Coalesce $stmt,
|
||||||
Context $context
|
Context $context
|
||||||
) : bool {
|
) : 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(
|
$condition_type = $statements_analyzer->node_data->getType($left_expr) ?: Type::getMixed();
|
||||||
$stmt_id,
|
|
||||||
$stmt_id,
|
$context->vars_in_scope[$left_var_id] = $condition_type;
|
||||||
$stmt,
|
|
||||||
$context->self,
|
$left_expr = new PhpParser\Node\Expr\Variable(
|
||||||
$statements_analyzer,
|
substr($left_var_id, 1),
|
||||||
$codebase
|
$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) {
|
$statements_analyzer->node_data = clone $statements_analyzer->node_data;
|
||||||
if ($type->hasMixed()) {
|
|
||||||
$mixed_var_ids[] = $var_id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach ($context->vars_possibly_in_scope as $var_id => $_) {
|
ExpressionAnalyzer::analyze($statements_analyzer, $ternary, clone $context);
|
||||||
if (!isset($context->vars_in_scope[$var_id])) {
|
|
||||||
$mixed_var_ids[] = $var_id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$if_clauses = array_values(
|
$ternary_type = $statements_analyzer->node_data->getType($ternary) ?: Type::getMixed();
|
||||||
array_map(
|
|
||||||
function (\Psalm\Internal\Clause $c) use ($mixed_var_ids, $stmt_id): \Psalm\Internal\Clause {
|
|
||||||
$keys = array_keys($c->possibilities);
|
|
||||||
|
|
||||||
$mixed_var_ids = \array_diff($mixed_var_ids, $keys);
|
$statements_analyzer->node_data = $old_node_data;
|
||||||
|
|
||||||
foreach ($keys as $key) {
|
$statements_analyzer->node_data->setType($stmt, $ternary_type);
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
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 {
|
function foo(?string $s) : string {
|
||||||
return ((string) $s) ?? "bar";
|
return ((string) $s) ?? "bar";
|
||||||
}',
|
}',
|
||||||
'error_message' => 'TypeDoesNotContainType'
|
'error_message' => 'RedundantCondition'
|
||||||
],
|
],
|
||||||
'allowEmptyScalarAndNonEmptyScalarAssertions1' => [
|
'allowEmptyScalarAndNonEmptyScalarAssertions1' => [
|
||||||
'<?php
|
'<?php
|
||||||
|
@ -1020,6 +1020,13 @@ class IssetTest extends \Psalm\Tests\TestCase
|
|||||||
return isset($a) ? $a : $b;
|
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];
|
$options = ["option" => true];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @psalm-suppress PossiblyUndefinedGlobalVariable */
|
||||||
$option = $options["option"] ?? false;
|
$option = $options["option"] ?? false;
|
||||||
|
|
||||||
if ($option) {}',
|
if ($option) {}',
|
||||||
|
Loading…
x
Reference in New Issue
Block a user