From ab4ccd03b68a8a26e38125be29d4009283ea425d Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sun, 22 Dec 2024 09:22:00 +0100 Subject: [PATCH] Improve loose comparison on intersection type --- src/TrinaryLogic.php | 12 +++++++ .../AccessoryLowercaseStringType.php | 1 + .../Accessory/AccessoryNonEmptyStringType.php | 4 +++ .../AccessoryUppercaseStringType.php | 1 + src/Type/IntersectionType.php | 4 ++- src/Type/UnionType.php | 18 ++-------- .../Analyser/nsrt/loose-comparisons.php | 36 +++++++++++++++++++ 7 files changed, 60 insertions(+), 16 deletions(-) diff --git a/src/TrinaryLogic.php b/src/TrinaryLogic.php index a587099844..f466ae149a 100644 --- a/src/TrinaryLogic.php +++ b/src/TrinaryLogic.php @@ -47,6 +47,18 @@ public static function createFromBoolean(bool $value): self return self::$registry[$yesNo] ??= new self($yesNo); } + public static function createFromBooleanType(BooleanType $type): self + { + if ($type->isTrue()->yes()) { + return self::createYes(); + } + if ($type->isFalse()->yes()) { + return self::createNo(); + } + + return self::createMaybe(); + } + private static function create(int $value): self { self::$registry[$value] ??= new self($value); diff --git a/src/Type/Accessory/AccessoryLowercaseStringType.php b/src/Type/Accessory/AccessoryLowercaseStringType.php index c4b8b0b364..84a60c9121 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; 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..a6f160771d 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; diff --git a/src/Type/IntersectionType.php b/src/Type/IntersectionType.php index 255f56dd7a..85cc03c364 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 => TrinaryLogic::createFromBooleanType($innerType->looseCompare($type, $phpVersion)) + )->toBooleanType(); } public function isOffsetAccessible(): TrinaryLogic diff --git a/src/Type/UnionType.php b/src/Type/UnionType.php index 2a5f37b7fb..36c6a4ba8b 100644 --- a/src/Type/UnionType.php +++ b/src/Type/UnionType.php @@ -676,21 +676,9 @@ public function isScalar(): TrinaryLogic public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType { - $lastResult = null; - foreach ($this->types as $innerType) { - $result = $innerType->looseCompare($type, $phpVersion); - if ($lastResult === null) { - $lastResult = $result; - continue; - } - if ($lastResult->equals($result)) { - continue; - } - - return new BooleanType(); - } - - return $lastResult ?? new BooleanType(); + return $this->unionResults( + static fn (Type $innerType): TrinaryLogic => TrinaryLogic::createFromBooleanType($innerType->looseCompare($type, $phpVersion)) + )->toBooleanType(); } public function isOffsetAccessible(): TrinaryLogic diff --git a/tests/PHPStan/Analyser/nsrt/loose-comparisons.php b/tests/PHPStan/Analyser/nsrt/loose-comparisons.php index 9c76790f25..059689d91b 100644 --- a/tests/PHPStan/Analyser/nsrt/loose-comparisons.php +++ b/tests/PHPStan/Analyser/nsrt/loose-comparisons.php @@ -698,4 +698,40 @@ public function sayConstUnion( assertType('bool', $constMix == $looseZero); } + /** + * @param uppercase-string $upper + * @param lowercase-string $lower + */ + public function sayIntersection( + string $upper, + string $lower, + string $s, + ): void + { + assertType('bool', '' == $upper); + if ($upper != '') { + assertType('false', '' == $upper); + } + assertType('bool', '0' == $upper); + assertType('bool', 'a' == $upper); // should be false + assertType('bool', 'abc' == $upper); // should be false + assertType('bool', 'aBc' == $upper); + assertType('bool', strtoupper($s) == $upper); + assertType('bool', strtolower($s) == $upper); // should be false + assertType('bool', $upper == $lower); // should be false + + assertType('bool', '' == $lower); + if ($lower != '') { + assertType('false', '' == $lower); + } + assertType('bool', '0' == $lower); + assertType('bool', 'A' == $lower); // should be false + assertType('bool', 'ABC' == $lower); // should be false + assertType('bool', 'AbC' == $lower); + assertType('bool', strtoupper($s) == $lower); // should be false + assertType('bool', strtolower($s) == $lower); + assertType('bool', $lower == $upper); // should be false + } + + }