diff --git a/src/Analyser/ExprHandler/BinaryOpHandler.php b/src/Analyser/ExprHandler/BinaryOpHandler.php index b9d0908f78f..31792777b00 100644 --- a/src/Analyser/ExprHandler/BinaryOpHandler.php +++ b/src/Analyser/ExprHandler/BinaryOpHandler.php @@ -109,7 +109,10 @@ public function resolveType(MutatingScope $scope, Expr $expr): Type && is_string($expr->right->name) && $expr->left->name === $expr->right->name ) { - return new ConstantBooleanType(true); + $varType = $scope->getType($expr->left); + if (!$this->initializerExprTypeResolver->containsNan($varType)) { + return new ConstantBooleanType(true); + } } $leftType = $scope->getType($expr->left); diff --git a/src/Analyser/RicherScopeGetTypeHelper.php b/src/Analyser/RicherScopeGetTypeHelper.php index 132c1875809..7ca0e00b6ba 100644 --- a/src/Analyser/RicherScopeGetTypeHelper.php +++ b/src/Analyser/RicherScopeGetTypeHelper.php @@ -36,7 +36,10 @@ public function getIdenticalResult(Scope $scope, Identical $expr): TypeResult && is_string($expr->right->name) && $expr->left->name === $expr->right->name ) { - return new TypeResult(new ConstantBooleanType(true), []); + $varType = $scope->getType($expr->left); + if (!$this->initializerExprTypeResolver->containsNan($varType)) { + return new TypeResult(new ConstantBooleanType(true), []); + } } $leftType = $scope->getType($expr->left); diff --git a/src/Reflection/InitializerExprTypeResolver.php b/src/Reflection/InitializerExprTypeResolver.php index b4578930acf..31d47604a7c 100644 --- a/src/Reflection/InitializerExprTypeResolver.php +++ b/src/Reflection/InitializerExprTypeResolver.php @@ -114,6 +114,7 @@ use function is_finite; use function is_float; use function is_int; +use function is_nan; use function is_numeric; use function is_string; use function max; @@ -1944,6 +1945,10 @@ public function resolveIdenticalType(Type $leftType, Type $rightType): TypeResul return new TypeResult(new ConstantBooleanType(false), []); } + if ($this->containsNan($leftType) || $this->containsNan($rightType)) { + return new TypeResult(new ConstantBooleanType(false), []); + } + if ($leftType instanceof ConstantScalarType && $rightType instanceof ConstantScalarType) { return new TypeResult(new ConstantBooleanType($leftType->getValue() === $rightType->getValue()), []); } @@ -1972,6 +1977,10 @@ public function resolveIdenticalType(Type $leftType, Type $rightType): TypeResul */ public function resolveEqualType(Type $leftType, Type $rightType): TypeResult { + if ($this->containsNan($leftType) || $this->containsNan($rightType)) { + return new TypeResult(new ConstantBooleanType(false), []); + } + if ( ($leftType->isEnum()->yes() && $rightType->isTrue()->no()) || ($rightType->isEnum()->yes() && $leftType->isTrue()->no()) @@ -2061,6 +2070,37 @@ private function resolveConstantArrayTypeComparison(ConstantArrayType $leftType, return new TypeResult($resultType->toBoolean(), []); } + public function containsNan(Type $type): bool + { + if ($type instanceof ConstantFloatType && is_nan($type->getValue())) { + return true; + } + + $constantArrays = $type->getConstantArrays(); + if ($constantArrays !== []) { + foreach ($constantArrays as $constantArray) { + $optionalKeys = $constantArray->getOptionalKeys(); + $hasNan = false; + foreach ($constantArray->getValueTypes() as $index => $valueType) { + if (in_array($index, $optionalKeys, true)) { + continue; + } + if ($this->containsNan($valueType)) { + $hasNan = true; + break; + } + } + if (!$hasNan) { + return false; + } + } + + return true; + } + + return false; + } + /** * @param BinaryOp\Plus|BinaryOp\Minus|BinaryOp\Mul|BinaryOp\Div|BinaryOp\ShiftLeft|BinaryOp\ShiftRight $expr */ diff --git a/tests/PHPStan/Rules/Comparison/ConstantLooseComparisonRuleTest.php b/tests/PHPStan/Rules/Comparison/ConstantLooseComparisonRuleTest.php index bb6aa370ac4..0ee1741cc95 100644 --- a/tests/PHPStan/Rules/Comparison/ConstantLooseComparisonRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/ConstantLooseComparisonRuleTest.php @@ -248,4 +248,27 @@ public function testBug13098(): void $this->analyse([__DIR__ . '/data/bug-13098.php'], []); } + public function testBug14394(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-14394.php'], [ + [ + 'Loose comparison using == between float and NAN will always evaluate to false.', + 8, + ], + [ + 'Loose comparison using == between list and array{NAN} will always evaluate to false.', + 10, + ], + [ + 'Loose comparison using == between NAN and NAN will always evaluate to false.', + 16, + ], + [ + 'Loose comparison using == between array{NAN} and array{NAN} will always evaluate to false.', + 18, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php b/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php index f2f43732108..cd2fd5075c3 100644 --- a/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php @@ -1164,11 +1164,45 @@ public function testPossiblyImpureTip(): void public function testBug11054(): void { $this->analyse([__DIR__ . '/data/bug-11054.php'], [ + [ + 'Strict comparison using === between mixed and array{NAN} will always evaluate to false.', + 24, + ], + [ + 'Strict comparison using === between mixed and array{NAN} will always evaluate to false.', + 28, + ], [ 'Strict comparison using === between mixed and array{INF} will always evaluate to false.', 47, 'Type array{INF} has already been eliminated from mixed.', ], + [ + 'Strict comparison using === between mixed and array{NAN} will always evaluate to false.', + 55, + ], + ]); + } + + public function testBug14394(): void + { + $this->analyse([__DIR__ . '/data/bug-14394.php'], [ + [ + 'Strict comparison using === between float and NAN will always evaluate to false.', + 9, + ], + [ + 'Strict comparison using === between list and array{NAN} will always evaluate to false.', + 11, + ], + [ + 'Strict comparison using === between NAN and NAN will always evaluate to false.', + 17, + ], + [ + 'Strict comparison using === between array{NAN} and array{NAN} will always evaluate to false.', + 19, + ], ]); } diff --git a/tests/PHPStan/Rules/Comparison/data/bug-14394.php b/tests/PHPStan/Rules/Comparison/data/bug-14394.php new file mode 100644 index 00000000000..4f4fad13df9 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-14394.php @@ -0,0 +1,27 @@ + $v2 */ + public static function test(float $v1, array $v2): void { + if ($v1 == NAN) { echo "never reached\n"; } + if ($v1 === NAN) { echo "never reached\n"; } + if ($v2 == [NAN]) { echo "never reached\n"; } + if ($v2 === [NAN]) { echo "never reached\n"; } + } + + public static function testSameVariable(): void { + $a = NAN; + if ($a == $a) { echo "never reached\n"; } + if ($a === $a) { echo "never reached\n"; } + if ([$a] == [$a]) { echo "never reached\n"; } + if ([$a] === [$a]) { echo "never reached\n"; } + } + + /** @param array{NAN}|array{1} $v */ + public static function testUnionArrayNotAlwaysFalse(array $v): void { + if ($v === [1]) { echo "maybe reached\n"; } + if ($v == [1]) { echo "maybe reached\n"; } + } +}