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' => '