From fef2c93ab2c24985e2dc4c8a5e9a5f49b52ca838 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Thu, 2 Jan 2025 16:46:03 +0100 Subject: [PATCH] Improve loose comparison on IntegerRange containing zero --- src/Type/IntegerRangeType.php | 55 ++++++++++++++++- .../Analyser/nsrt/loose-comparisons.php | 61 +++++++++++++++++-- 2 files changed, 111 insertions(+), 5 deletions(-) diff --git a/src/Type/IntegerRangeType.php b/src/Type/IntegerRangeType.php index b90f4d31a7..1c956015dd 100644 --- a/src/Type/IntegerRangeType.php +++ b/src/Type/IntegerRangeType.php @@ -315,6 +315,16 @@ public function isSmallerThan(Type $otherType, PhpVersion $phpVersion): TrinaryL $maxIsSmaller = (new ConstantIntegerType($this->max))->isSmallerThan($otherType, $phpVersion); } + // 0 can have different results in contrast to the interval edges, see https://3v4l.org/iGoti + $zeroInt = new ConstantIntegerType(0); + if (!$zeroInt->isSuperTypeOf($this)->no()) { + return TrinaryLogic::extremeIdentity( + $zeroInt->isSmallerThan($otherType, $phpVersion), + $minIsSmaller, + $maxIsSmaller, + ); + } + return TrinaryLogic::extremeIdentity($minIsSmaller, $maxIsSmaller); } @@ -332,6 +342,16 @@ public function isSmallerThanOrEqual(Type $otherType, PhpVersion $phpVersion): T $maxIsSmaller = (new ConstantIntegerType($this->max))->isSmallerThanOrEqual($otherType, $phpVersion); } + // 0 can have different results in contrast to the interval edges, see https://3v4l.org/iGoti + $zeroInt = new ConstantIntegerType(0); + if (!$zeroInt->isSuperTypeOf($this)->no()) { + return TrinaryLogic::extremeIdentity( + $zeroInt->isSmallerThanOrEqual($otherType, $phpVersion), + $minIsSmaller, + $maxIsSmaller, + ); + } + return TrinaryLogic::extremeIdentity($minIsSmaller, $maxIsSmaller); } @@ -349,6 +369,16 @@ public function isGreaterThan(Type $otherType, PhpVersion $phpVersion): TrinaryL $maxIsSmaller = $otherType->isSmallerThan((new ConstantIntegerType($this->max)), $phpVersion); } + // 0 can have different results in contrast to the interval edges, see https://3v4l.org/iGoti + $zeroInt = new ConstantIntegerType(0); + if (!$zeroInt->isSuperTypeOf($this)->no()) { + return TrinaryLogic::extremeIdentity( + $otherType->isSmallerThan($zeroInt, $phpVersion), + $minIsSmaller, + $maxIsSmaller, + ); + } + return TrinaryLogic::extremeIdentity($minIsSmaller, $maxIsSmaller); } @@ -366,6 +396,16 @@ public function isGreaterThanOrEqual(Type $otherType, PhpVersion $phpVersion): T $maxIsSmaller = $otherType->isSmallerThanOrEqual((new ConstantIntegerType($this->max)), $phpVersion); } + // 0 can have different results in contrast to the interval edges, see https://3v4l.org/iGoti + $zeroInt = new ConstantIntegerType(0); + if (!$zeroInt->isSuperTypeOf($this)->no()) { + return TrinaryLogic::extremeIdentity( + $otherType->isSmallerThanOrEqual($zeroInt, $phpVersion), + $minIsSmaller, + $maxIsSmaller, + ); + } + return TrinaryLogic::extremeIdentity($minIsSmaller, $maxIsSmaller); } @@ -694,7 +734,20 @@ public function toPhpDocNode(): TypeNode public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType { - if ($this->isSmallerThan($type, $phpVersion)->yes() || $this->isGreaterThan($type, $phpVersion)->yes()) { + $zeroInt = new ConstantIntegerType(0); + if ($zeroInt->isSuperTypeOf($this)->no()) { + if ($type->isTrue()->yes()) { + return new ConstantBooleanType(true); + } + if ($type->isFalse()->yes()) { + return new ConstantBooleanType(false); + } + } + + if ( + $this->isSmallerThan($type, $phpVersion)->yes() + || $this->isGreaterThan($type, $phpVersion)->yes() + ) { return new ConstantBooleanType(false); } diff --git a/tests/PHPStan/Analyser/nsrt/loose-comparisons.php b/tests/PHPStan/Analyser/nsrt/loose-comparisons.php index 243b7672fd..1b9a2265b5 100644 --- a/tests/PHPStan/Analyser/nsrt/loose-comparisons.php +++ b/tests/PHPStan/Analyser/nsrt/loose-comparisons.php @@ -712,7 +712,9 @@ public function sayEmptyStr( * @param array{} $emptyArr * @param 'php' $phpStr * @param '' $emptyStr - * @param int<10, 20> $intRange + * @param int<10, 20> $positiveIntRange + * @param int<-20, -10> $negativeIntRange + * @param int<-10, 10> $minusTenToTen */ public function sayInt( $true, @@ -728,7 +730,9 @@ public function sayInt( $emptyArr, array $array, int $int, - int $intRange, + int $positiveIntRange, + int $negativeIntRange, + int $minusTenToTen, ): void { assertType('bool', $int == $true); @@ -744,8 +748,57 @@ public function sayInt( assertType('false', $int == $emptyArr); assertType('false', $int == $array); - assertType('false', $intRange == $emptyArr); - assertType('false', $intRange == $array); + assertType('true', $positiveIntRange == $true); + assertType('false', $positiveIntRange == $false); + assertType('false', $positiveIntRange == $one); + assertType('false', $positiveIntRange == $zero); + assertType('false', $positiveIntRange == $minusOne); + assertType('false', $positiveIntRange == $oneStr); + assertType('false', $positiveIntRange == $zeroStr); + assertType('false', $positiveIntRange == $minusOneStr); + assertType('false', $positiveIntRange == $plusOneStr); + assertType('false', $positiveIntRange == $null); + assertType('false', $positiveIntRange == $emptyArr); + assertType('false', $positiveIntRange == $array); + + assertType('true', $negativeIntRange == $true); + assertType('false', $negativeIntRange == $false); + assertType('false', $negativeIntRange == $one); + assertType('false', $negativeIntRange == $zero); + assertType('false', $negativeIntRange == $minusOne); + assertType('false', $negativeIntRange == $oneStr); + assertType('false', $negativeIntRange == $zeroStr); + assertType('false', $negativeIntRange == $minusOneStr); + assertType('false', $negativeIntRange == $plusOneStr); + assertType('false', $negativeIntRange == $null); + assertType('false', $negativeIntRange == $emptyArr); + assertType('false', $negativeIntRange == $array); + + // see https://3v4l.org/VudDK + assertType('bool', $minusTenToTen == $true); + assertType('bool', $minusTenToTen == $false); + assertType('bool', $minusTenToTen == $one); + assertType('bool', $minusTenToTen == $zero); + assertType('bool', $minusTenToTen == $minusOne); + assertType('bool', $minusTenToTen == $oneStr); + assertType('bool', $minusTenToTen == $zeroStr); + assertType('bool', $minusTenToTen == $minusOneStr); + assertType('bool', $minusTenToTen == $plusOneStr); + assertType('bool', $minusTenToTen == $null); + assertType('false', $minusTenToTen == $emptyArr); + assertType('false', $minusTenToTen == $array); + + // see https://3v4l.org/oJl3K + assertType('false', $minusTenToTen < $null); + assertType('bool', $minusTenToTen > $null); + assertType('bool', $minusTenToTen <= $null); + assertType('true', $minusTenToTen >= $null); + + // see https://3v4l.org/oRSgU + assertType('bool', $null < $minusTenToTen); + assertType('false', $null > $minusTenToTen); + assertType('true', $null <= $minusTenToTen); + assertType('bool', $null >= $minusTenToTen); }