diff --git a/config.xsd b/config.xsd
index 5c176821e..0f3e88916 100644
--- a/config.xsd
+++ b/config.xsd
@@ -427,6 +427,7 @@
+
diff --git a/docs/running_psalm/error_levels.md b/docs/running_psalm/error_levels.md
index 55a18b8fa..df53f5227 100644
--- a/docs/running_psalm/error_levels.md
+++ b/docs/running_psalm/error_levels.md
@@ -173,6 +173,7 @@ Level 5 and above allows a more non-verifiable code, and higher levels are even
- [TooManyArguments](issues/TooManyArguments.md)
- [TypeDoesNotContainNull](issues/TypeDoesNotContainNull.md)
- [TypeDoesNotContainType](issues/TypeDoesNotContainType.md)
+- [RiskyTruthyFalsyComparison](issues/RiskyTruthyFalsyComparison.md)
- [UndefinedMagicMethod](issues/UndefinedMagicMethod.md)
- [UndefinedMagicPropertyAssignment](issues/UndefinedMagicPropertyAssignment.md)
- [UndefinedMagicPropertyFetch](issues/UndefinedMagicPropertyFetch.md)
diff --git a/docs/running_psalm/issues.md b/docs/running_psalm/issues.md
index ac8135c71..179f9bf7b 100644
--- a/docs/running_psalm/issues.md
+++ b/docs/running_psalm/issues.md
@@ -229,6 +229,7 @@
- [ReferenceReusedFromConfusingScope](issues/ReferenceReusedFromConfusingScope.md)
- [ReservedWord](issues/ReservedWord.md)
- [RiskyCast](issues/RiskyCast.md)
+ - [RiskyTruthyFalsyComparison](issues/RiskyTruthyFalsyComparison.md)
- [StringIncrement](issues/StringIncrement.md)
- [TaintedCallable](issues/TaintedCallable.md)
- [TaintedCookie](issues/TaintedCookie.md)
diff --git a/docs/running_psalm/issues/RiskyTruthyFalsyComparison.md b/docs/running_psalm/issues/RiskyTruthyFalsyComparison.md
new file mode 100644
index 000000000..8d6096963
--- /dev/null
+++ b/docs/running_psalm/issues/RiskyTruthyFalsyComparison.md
@@ -0,0 +1,29 @@
+# RiskyTruthyFalsyComparison
+
+Emitted when comparing a value with multiple types that can both contain truthy and falsy values.
+
+```php
+freeze();
IssueBuffer::maybeAdd(
- new TypeDoesNotContainType(
+ new RiskyTruthyFalsyComparison(
'Operand of type ' . $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),
- $type->getId() . ' truthy-falsy',
+ $type->getId(),
),
$statements_analyzer->getSuppressedIssues(),
);
diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/BooleanNotAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/BooleanNotAnalyzer.php
index bfe8d209e..71f6e1932 100644
--- a/src/Psalm/Internal/Analyzer/Statements/Expression/BooleanNotAnalyzer.php
+++ b/src/Psalm/Internal/Analyzer/Statements/Expression/BooleanNotAnalyzer.php
@@ -7,7 +7,7 @@ use Psalm\CodeLocation;
use Psalm\Context;
use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer;
use Psalm\Internal\Analyzer\StatementsAnalyzer;
-use Psalm\Issue\TypeDoesNotContainType;
+use Psalm\Issue\RiskyTruthyFalsyComparison;
use Psalm\IssueBuffer;
use Psalm\Type;
use Psalm\Type\Atomic\TBool;
@@ -63,13 +63,13 @@ final class BooleanNotAnalyzer
if ($has_both) {
$both_types = $both_types->freeze();
IssueBuffer::maybeAdd(
- new TypeDoesNotContainType(
+ new RiskyTruthyFalsyComparison(
'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',
+ $expr_type->getId(),
),
$statements_analyzer->getSuppressedIssues(),
);
diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/EmptyAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/EmptyAnalyzer.php
index 40bf489ea..02fae12fd 100644
--- a/src/Psalm/Internal/Analyzer/Statements/Expression/EmptyAnalyzer.php
+++ b/src/Psalm/Internal/Analyzer/Statements/Expression/EmptyAnalyzer.php
@@ -8,7 +8,7 @@ use Psalm\Context;
use Psalm\Internal\Analyzer\StatementsAnalyzer;
use Psalm\Issue\ForbiddenCode;
use Psalm\Issue\InvalidArgument;
-use Psalm\Issue\TypeDoesNotContainType;
+use Psalm\Issue\RiskyTruthyFalsyComparison;
use Psalm\IssueBuffer;
use Psalm\Type;
use Psalm\Type\Atomic\TBool;
@@ -82,13 +82,13 @@ final class EmptyAnalyzer
if ($has_both) {
$both_types = $both_types->freeze();
IssueBuffer::maybeAdd(
- new TypeDoesNotContainType(
+ new RiskyTruthyFalsyComparison(
'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',
+ $expr_type->getId(),
),
$statements_analyzer->getSuppressedIssues(),
);
diff --git a/src/Psalm/Issue/RiskyTruthyFalsyComparison.php b/src/Psalm/Issue/RiskyTruthyFalsyComparison.php
new file mode 100644
index 000000000..9150aa30b
--- /dev/null
+++ b/src/Psalm/Issue/RiskyTruthyFalsyComparison.php
@@ -0,0 +1,17 @@
+dupe_key = $dupe_key;
+ }
+}
diff --git a/tests/TypeReconciliation/ConditionalTest.php b/tests/TypeReconciliation/ConditionalTest.php
index bb16a2004..2209c2f16 100644
--- a/tests/TypeReconciliation/ConditionalTest.php
+++ b/tests/TypeReconciliation/ConditionalTest.php
@@ -3528,7 +3528,7 @@ class ConditionalTest extends TestCase
if ($arg) {
}
}',
- 'error_message' => 'TypeDoesNotContainType',
+ 'error_message' => 'RiskyTruthyFalsyComparison',
],
'nonStrictConditionTruthyFalsyNegated' => [
'code' => ' 'TypeDoesNotContainType',
+ 'error_message' => 'RiskyTruthyFalsyComparison',
],
'nonStrictConditionTruthyFalsyFuncCall' => [
'code' => ' 'TypeDoesNotContainType',
+ 'error_message' => 'RiskyTruthyFalsyComparison',
],
'nonStrictConditionTruthyFalsyFuncCallNegated' => [
'code' => ' 'TypeDoesNotContainType',
+ 'error_message' => 'RiskyTruthyFalsyComparison',
],
'redundantConditionForNonEmptyString' => [
'code' => '