diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/EmptyAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/EmptyAnalyzer.php index 3b9014f85..40bf489ea 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/EmptyAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/EmptyAnalyzer.php @@ -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); } } diff --git a/tests/TypeReconciliation/EmptyTest.php b/tests/TypeReconciliation/EmptyTest.php index c79918044..944ae2218 100644 --- a/tests/TypeReconciliation/EmptyTest.php +++ b/tests/TypeReconciliation/EmptyTest.php @@ -612,6 +612,14 @@ class EmptyTest extends TestCase '$GLOBALS[\'sql_query\']===' => 'string', ], ], + 'emptyLiteralTrueFalse' => [ + 'code' => ' [ + '$x===' => 'true', + ], + ], ]; } @@ -720,6 +728,30 @@ class EmptyTest extends TestCase }', 'error_message' => 'LessSpecificReturnStatement', ], + 'impossibleEmptyOnFalsyFunctionCall' => [ + 'code' => ' 'DocblockTypeContradiction', + ], + 'redundantEmptyOnFalsyFunctionCall' => [ + 'code' => ' 'RedundantConditionGivenDocblockType', + ], ]; } }