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

add the fix for empty() too and fix empty returning bool on true/false only cases hiding errors when functions called

This commit is contained in:
kkmuffme 2024-01-12 22:40:47 +01:00
parent d5b713e439
commit f3543ca9ab
2 changed files with 100 additions and 14 deletions

View File

@ -8,8 +8,15 @@ use Psalm\Context;
use Psalm\Internal\Analyzer\StatementsAnalyzer;
use Psalm\Issue\ForbiddenCode;
use Psalm\Issue\InvalidArgument;
use Psalm\Issue\TypeDoesNotContainType;
use Psalm\IssueBuffer;
use Psalm\Type;
use Psalm\Type\Atomic\TBool;
use Psalm\Type\Atomic\TFalse;
use Psalm\Type\Atomic\TTrue;
use Psalm\Type\Union;
use function count;
/**
* @internal
@ -35,21 +42,68 @@ final class EmptyAnalyzer
);
}
if (($stmt_expr_type = $statements_analyzer->node_data->getType($stmt->expr))
&& $stmt_expr_type->hasBool()
&& $stmt_expr_type->isSingle()
&& !$stmt_expr_type->from_docblock
) {
IssueBuffer::maybeAdd(
new InvalidArgument(
'Calling empty on a boolean value is almost certainly unintended',
new CodeLocation($statements_analyzer->getSource(), $stmt->expr),
'empty',
),
$statements_analyzer->getSuppressedIssues(),
);
$expr_type = $statements_analyzer->node_data->getType($stmt->expr);
if ($expr_type) {
if ($expr_type->hasBool()
&& $expr_type->isSingle()
&& !$expr_type->from_docblock
) {
IssueBuffer::maybeAdd(
new InvalidArgument(
'Calling empty on a boolean value is almost certainly unintended',
new CodeLocation($statements_analyzer->getSource(), $stmt->expr),
'empty',
),
$statements_analyzer->getSuppressedIssues(),
);
}
if ($expr_type->isAlwaysTruthy() && $expr_type->possibly_undefined === false) {
$stmt_type = new TFalse($expr_type->from_docblock);
} elseif ($expr_type->isAlwaysFalsy()) {
$stmt_type = new TTrue($expr_type->from_docblock);
} else {
$has_both = false;
$both_types = $expr_type->getBuilder();
if (count($expr_type->getAtomicTypes()) > 1) {
foreach ($both_types->getAtomicTypes() as $key => $atomic_type) {
if ($atomic_type->isTruthy()
|| $atomic_type->isFalsy()
|| $atomic_type instanceof TBool) {
$both_types->removeType($key);
continue;
}
$has_both = true;
}
}
if ($has_both) {
$both_types = $both_types->freeze();
IssueBuffer::maybeAdd(
new TypeDoesNotContainType(
'Operand of type ' . $expr_type->getId() . ' contains ' .
'type' . (count($both_types->getAtomicTypes()) > 1 ? 's' : '') . ' ' .
$both_types->getId() . ', which can be falsy and truthy. ' .
'This can cause possibly unexpected behavior. Use strict comparison instead.',
new CodeLocation($statements_analyzer, $stmt),
$expr_type->getId() . ' truthy-falsy',
),
$statements_analyzer->getSuppressedIssues(),
);
}
$stmt_type = new TBool();
}
$stmt_type = new Union([$stmt_type], [
'parent_nodes' => $expr_type->parent_nodes,
]);
} else {
$stmt_type = Type::getBool();
}
$statements_analyzer->node_data->setType($stmt, Type::getBool());
$statements_analyzer->node_data->setType($stmt, $stmt_type);
}
}

View File

@ -612,6 +612,14 @@ class EmptyTest extends TestCase
'$GLOBALS[\'sql_query\']===' => 'string',
],
],
'emptyLiteralTrueFalse' => [
'code' => '<?php
$b = "asdf";
$x = !empty($b);',
'assertions' => [
'$x===' => 'true',
],
],
];
}
@ -720,6 +728,30 @@ class EmptyTest extends TestCase
}',
'error_message' => 'LessSpecificReturnStatement',
],
'impossibleEmptyOnFalsyFunctionCall' => [
'code' => '<?php
/** @return false|null */
function bar() {
return rand(0, 5) ? null : false;
}
if (!empty(bar())) {
echo "abc";
}',
'error_message' => 'DocblockTypeContradiction',
],
'redundantEmptyOnFalsyFunctionCall' => [
'code' => '<?php
/** @return false|null */
function bar() {
return rand(0, 5) ? null : false;
}
if (empty(bar())) {
echo "abc";
}',
'error_message' => 'RedundantConditionGivenDocblockType',
],
];
}
}