diff --git a/src/Type/Accessory/AccessoryLowercaseStringType.php b/src/Type/Accessory/AccessoryLowercaseStringType.php index c4b8b0b364..97edeb39ba 100644 --- a/src/Type/Accessory/AccessoryLowercaseStringType.php +++ b/src/Type/Accessory/AccessoryLowercaseStringType.php @@ -11,6 +11,7 @@ use PHPStan\Type\BooleanType; use PHPStan\Type\CompoundType; use PHPStan\Type\Constant\ConstantArrayType; +use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\ErrorType; use PHPStan\Type\FloatType; @@ -326,6 +327,14 @@ public function isScalar(): TrinaryLogic public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType { + if ( + $type->isString()->yes() + && $type->isLowercaseString()->no() + && ($type->isNumericString()->no() || $this->isNumericString()->no()) + ) { + return new ConstantBooleanType(false); + } + return new BooleanType(); } diff --git a/src/Type/Accessory/AccessoryNonEmptyStringType.php b/src/Type/Accessory/AccessoryNonEmptyStringType.php index 08f4790001..f31e3108b4 100644 --- a/src/Type/Accessory/AccessoryNonEmptyStringType.php +++ b/src/Type/Accessory/AccessoryNonEmptyStringType.php @@ -11,6 +11,7 @@ use PHPStan\Type\BooleanType; use PHPStan\Type\CompoundType; use PHPStan\Type\Constant\ConstantArrayType; +use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\ErrorType; @@ -322,6 +323,9 @@ public function isScalar(): TrinaryLogic public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType { + if ($type->isString()->yes() && $type->isNonEmptyString()->no()) { + return new ConstantBooleanType(false); + } return new BooleanType(); } diff --git a/src/Type/Accessory/AccessoryUppercaseStringType.php b/src/Type/Accessory/AccessoryUppercaseStringType.php index f9ae03d0ac..a034eea321 100644 --- a/src/Type/Accessory/AccessoryUppercaseStringType.php +++ b/src/Type/Accessory/AccessoryUppercaseStringType.php @@ -11,6 +11,7 @@ use PHPStan\Type\BooleanType; use PHPStan\Type\CompoundType; use PHPStan\Type\Constant\ConstantArrayType; +use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\ErrorType; use PHPStan\Type\FloatType; @@ -326,6 +327,14 @@ public function isScalar(): TrinaryLogic public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType { + if ( + $type->isString()->yes() + && $type->isUppercaseString()->no() + && ($type->isNumericString()->no() || $this->isNumericString()->no()) + ) { + return new ConstantBooleanType(false); + } + return new BooleanType(); } diff --git a/src/Type/Accessory/NonEmptyArrayType.php b/src/Type/Accessory/NonEmptyArrayType.php index e99bb4cbaa..a46eefc807 100644 --- a/src/Type/Accessory/NonEmptyArrayType.php +++ b/src/Type/Accessory/NonEmptyArrayType.php @@ -9,6 +9,7 @@ use PHPStan\Type\AcceptsResult; use PHPStan\Type\BooleanType; use PHPStan\Type\CompoundType; +use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantFloatType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\ErrorType; @@ -402,6 +403,10 @@ public function isScalar(): TrinaryLogic public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType { + if ($type->isArray()->yes() && $type->isIterableAtLeastOnce()->no()) { + return new ConstantBooleanType(false); + } + return new BooleanType(); } diff --git a/src/Type/ArrayType.php b/src/Type/ArrayType.php index 402c985b0e..9570359eaf 100644 --- a/src/Type/ArrayType.php +++ b/src/Type/ArrayType.php @@ -249,6 +249,10 @@ public function isConstantValue(): TrinaryLogic public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType { + if ($type->isInteger()->yes()) { + return new ConstantBooleanType(false); + } + return new BooleanType(); } diff --git a/src/Type/BooleanType.php b/src/Type/BooleanType.php index df059481e6..0b0eb798ec 100644 --- a/src/Type/BooleanType.php +++ b/src/Type/BooleanType.php @@ -163,4 +163,16 @@ public function toPhpDocNode(): TypeNode return new IdentifierTypeNode('bool'); } + public function toTrinaryLogic(): TrinaryLogic + { + if ($this->isTrue()->yes()) { + return TrinaryLogic::createYes(); + } + if ($this->isFalse()->yes()) { + return TrinaryLogic::createNo(); + } + + return TrinaryLogic::createMaybe(); + } + } diff --git a/src/Type/IntersectionType.php b/src/Type/IntersectionType.php index 255f56dd7a..b79f6ed7ea 100644 --- a/src/Type/IntersectionType.php +++ b/src/Type/IntersectionType.php @@ -716,7 +716,9 @@ public function isScalar(): TrinaryLogic public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType { - return new BooleanType(); + return $this->intersectResults( + static fn (Type $innerType): TrinaryLogic => $innerType->looseCompare($type, $phpVersion)->toTrinaryLogic() + )->toBooleanType(); } public function isOffsetAccessible(): TrinaryLogic diff --git a/src/Type/UnionType.php b/src/Type/UnionType.php index c69888cf30..9156bc09b7 100644 --- a/src/Type/UnionType.php +++ b/src/Type/UnionType.php @@ -676,7 +676,9 @@ public function isScalar(): TrinaryLogic public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType { - return new BooleanType(); + return $this->unionResults( + static fn (Type $innerType): TrinaryLogic => $innerType->looseCompare($type, $phpVersion)->toTrinaryLogic() + )->toBooleanType(); } public function isOffsetAccessible(): TrinaryLogic diff --git a/tests/PHPStan/Analyser/nsrt/loose-comparisons.php b/tests/PHPStan/Analyser/nsrt/loose-comparisons.php index cc3eba83f3..16c414170b 100644 --- a/tests/PHPStan/Analyser/nsrt/loose-comparisons.php +++ b/tests/PHPStan/Analyser/nsrt/loose-comparisons.php @@ -653,4 +653,108 @@ public function sayInt( } + /** + * @param true|1|"1" $looseOne + * @param false|0|"0" $looseZero + * @param false|1 $constMix + */ + public function sayConstUnion( + $looseOne, + $looseZero, + $constMix + ): void + { + assertType('true', $looseOne == 1); + assertType('false', $looseOne == 0); + assertType('true', $looseOne == true); + assertType('false', $looseOne == false); + assertType('true', $looseOne == "1"); + assertType('false', $looseOne == "0"); + assertType('false', $looseOne == []); + + assertType('false', $looseZero == 1); + assertType('true', $looseZero == 0); + assertType('false', $looseZero == true); + assertType('true', $looseZero == false); + assertType('false', $looseZero == "1"); + assertType('true', $looseZero == "0"); + assertType('bool', $looseZero == []); + + assertType('bool', $constMix == 0); + assertType('bool', $constMix == 1); + assertType('bool', $constMix == true); + assertType('bool', $constMix == false); + assertType('bool', $constMix == "1"); + assertType('bool', $constMix == "0"); + assertType('bool', $constMix == []); + + assertType('true', $looseOne == $looseOne); + assertType('true', $looseZero == $looseZero); + assertType('false', $looseOne == $looseZero); + assertType('false', $looseZero == $looseOne); + assertType('bool', $looseOne == $constMix); + assertType('bool', $constMix == $looseOne); + assertType('bool', $looseZero == $constMix); + assertType('bool', $constMix == $looseZero); + } + + /** + * @param uppercase-string $upper + * @param lowercase-string $lower + * @param array{} $emptyArr + * @param non-empty-array $nonEmptyArr + * @param int<10, 20> $intRange + */ + public function sayIntersection( + string $upper, + string $lower, + string $s, + array $emptyArr, + array $nonEmptyArr, + array $arr, + int $i, + int $intRange, + ): void + { + // https://3v4l.org/q8OP2 + assertType('true', '1e2' == '1E2'); + assertType('false', '1e2' === '1E2'); + + assertType('bool', '' == $upper); + assertType('bool', '0' == $upper); + assertType('false', 'a' == $upper); + assertType('false', 'abc' == $upper); + assertType('false', 'aBc' == $upper); + assertType('bool', '1e2' == $upper); + assertType('bool', strtoupper($s) == $upper); + assertType('bool', strtolower($s) == $upper); + assertType('bool', $upper == $lower); + + assertType('bool', '0' == $lower); + assertType('false', 'A' == $lower); + assertType('false', 'ABC' == $lower); + assertType('false', 'AbC' == $lower); + assertType('bool', '1E2' == $lower); + assertType('bool', strtoupper($s) == $lower); + assertType('bool', strtolower($s) == $lower); + assertType('bool', $lower == $upper); + + assertType('false', $arr == $i); + assertType('false', $nonEmptyArr == $i); + assertType('false', $arr == $intRange); + assertType('false', $nonEmptyArr == $intRange); + assertType('bool', $emptyArr == $nonEmptyArr); // should be false + assertType('false', $nonEmptyArr == $emptyArr); + assertType('bool', $arr == $nonEmptyArr); + assertType('bool', $nonEmptyArr == $arr); + + assertType('bool', '' == $lower); + if ($lower != '') { + assertType('false', '' == $lower); + } + if ($upper != '') { + assertType('false', '' == $upper); + } + } + } diff --git a/tests/PHPStan/Rules/Comparison/ConstantLooseComparisonRuleTest.php b/tests/PHPStan/Rules/Comparison/ConstantLooseComparisonRuleTest.php index c4a819d1fc..6e56f9dfcd 100644 --- a/tests/PHPStan/Rules/Comparison/ConstantLooseComparisonRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/ConstantLooseComparisonRuleTest.php @@ -181,12 +181,10 @@ public function testBug11694(): void [ "Loose comparison using == between '13foo' and int<10, 20> will always evaluate to false.", 29, - 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', ], [ "Loose comparison using == between int<10, 20> and '13foo' will always evaluate to false.", 30, - 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', ], ]); } @@ -227,4 +225,15 @@ public function testBug11694(): void $this->analyse([__DIR__ . '/data/bug-11694.php'], $expectedErrors); } + public function testBug8800(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-8800.php'], [ + [ + 'Loose comparison using == between 0|1|false and 2 will always evaluate to false.', + 9, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Comparison/data/bug-8800.php b/tests/PHPStan/Rules/Comparison/data/bug-8800.php new file mode 100644 index 0000000000..c8656f5da2 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-8800.php @@ -0,0 +1,11 @@ +