Skip to content

Commit 44e2839

Browse files
staabmondrejmirtes
authored andcommittedJan 4, 2025·
Improve loose comparison on IntegerRange containing zero
1 parent a245a64 commit 44e2839

File tree

3 files changed

+119
-6
lines changed

3 files changed

+119
-6
lines changed
 

‎src/Type/IntegerRangeType.php

+54-1
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,16 @@ public function isSmallerThan(Type $otherType, PhpVersion $phpVersion): TrinaryL
315315
$maxIsSmaller = (new ConstantIntegerType($this->max))->isSmallerThan($otherType, $phpVersion);
316316
}
317317

318+
// 0 can have different results in contrast to the interval edges, see https://3v4l.org/iGoti
319+
$zeroInt = new ConstantIntegerType(0);
320+
if (!$zeroInt->isSuperTypeOf($this)->no()) {
321+
return TrinaryLogic::extremeIdentity(
322+
$zeroInt->isSmallerThan($otherType, $phpVersion),
323+
$minIsSmaller,
324+
$maxIsSmaller,
325+
);
326+
}
327+
318328
return TrinaryLogic::extremeIdentity($minIsSmaller, $maxIsSmaller);
319329
}
320330

@@ -332,6 +342,16 @@ public function isSmallerThanOrEqual(Type $otherType, PhpVersion $phpVersion): T
332342
$maxIsSmaller = (new ConstantIntegerType($this->max))->isSmallerThanOrEqual($otherType, $phpVersion);
333343
}
334344

345+
// 0 can have different results in contrast to the interval edges, see https://3v4l.org/iGoti
346+
$zeroInt = new ConstantIntegerType(0);
347+
if (!$zeroInt->isSuperTypeOf($this)->no()) {
348+
return TrinaryLogic::extremeIdentity(
349+
$zeroInt->isSmallerThanOrEqual($otherType, $phpVersion),
350+
$minIsSmaller,
351+
$maxIsSmaller,
352+
);
353+
}
354+
335355
return TrinaryLogic::extremeIdentity($minIsSmaller, $maxIsSmaller);
336356
}
337357

@@ -349,6 +369,16 @@ public function isGreaterThan(Type $otherType, PhpVersion $phpVersion): TrinaryL
349369
$maxIsSmaller = $otherType->isSmallerThan((new ConstantIntegerType($this->max)), $phpVersion);
350370
}
351371

372+
// 0 can have different results in contrast to the interval edges, see https://3v4l.org/iGoti
373+
$zeroInt = new ConstantIntegerType(0);
374+
if (!$zeroInt->isSuperTypeOf($this)->no()) {
375+
return TrinaryLogic::extremeIdentity(
376+
$otherType->isSmallerThan($zeroInt, $phpVersion),
377+
$minIsSmaller,
378+
$maxIsSmaller,
379+
);
380+
}
381+
352382
return TrinaryLogic::extremeIdentity($minIsSmaller, $maxIsSmaller);
353383
}
354384

@@ -366,6 +396,16 @@ public function isGreaterThanOrEqual(Type $otherType, PhpVersion $phpVersion): T
366396
$maxIsSmaller = $otherType->isSmallerThanOrEqual((new ConstantIntegerType($this->max)), $phpVersion);
367397
}
368398

399+
// 0 can have different results in contrast to the interval edges, see https://3v4l.org/iGoti
400+
$zeroInt = new ConstantIntegerType(0);
401+
if (!$zeroInt->isSuperTypeOf($this)->no()) {
402+
return TrinaryLogic::extremeIdentity(
403+
$otherType->isSmallerThanOrEqual($zeroInt, $phpVersion),
404+
$minIsSmaller,
405+
$maxIsSmaller,
406+
);
407+
}
408+
369409
return TrinaryLogic::extremeIdentity($minIsSmaller, $maxIsSmaller);
370410
}
371411

@@ -694,7 +734,20 @@ public function toPhpDocNode(): TypeNode
694734

695735
public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType
696736
{
697-
if ($this->isSmallerThan($type, $phpVersion)->yes() || $this->isGreaterThan($type, $phpVersion)->yes()) {
737+
$zeroInt = new ConstantIntegerType(0);
738+
if ($zeroInt->isSuperTypeOf($this)->no()) {
739+
if ($type->isTrue()->yes()) {
740+
return new ConstantBooleanType(true);
741+
}
742+
if ($type->isFalse()->yes()) {
743+
return new ConstantBooleanType(false);
744+
}
745+
}
746+
747+
if (
748+
$this->isSmallerThan($type, $phpVersion)->yes()
749+
|| $this->isGreaterThan($type, $phpVersion)->yes()
750+
) {
698751
return new ConstantBooleanType(false);
699752
}
700753

‎tests/PHPStan/Analyser/nsrt/loose-comparisons.php

+57-3
Original file line numberDiff line numberDiff line change
@@ -712,7 +712,9 @@ public function sayEmptyStr(
712712
* @param array{} $emptyArr
713713
* @param 'php' $phpStr
714714
* @param '' $emptyStr
715-
* @param int<10, 20> $intRange
715+
* @param int<10, 20> $positiveIntRange
716+
* @param int<-20, -10> $negativeIntRange
717+
* @param int<-10, 10> $minusTenToTen
716718
*/
717719
public function sayInt(
718720
$true,
@@ -731,6 +733,9 @@ public function sayInt(
731733
int $intRange,
732734
string $emptyStr,
733735
string $phpStr,
736+
int $positiveIntRange,
737+
int $negativeIntRange,
738+
int $minusTenToTen,
734739
): void
735740
{
736741
assertType('bool', $int == $true);
@@ -746,8 +751,57 @@ public function sayInt(
746751
assertType('false', $int == $emptyArr);
747752
assertType('false', $int == $array);
748753

749-
assertType('false', $intRange == $emptyArr);
750-
assertType('false', $intRange == $array);
754+
assertType('true', $positiveIntRange == $true);
755+
assertType('false', $positiveIntRange == $false);
756+
assertType('false', $positiveIntRange == $one);
757+
assertType('false', $positiveIntRange == $zero);
758+
assertType('false', $positiveIntRange == $minusOne);
759+
assertType('false', $positiveIntRange == $oneStr);
760+
assertType('false', $positiveIntRange == $zeroStr);
761+
assertType('false', $positiveIntRange == $minusOneStr);
762+
assertType('false', $positiveIntRange == $plusOneStr);
763+
assertType('false', $positiveIntRange == $null);
764+
assertType('false', $positiveIntRange == $emptyArr);
765+
assertType('false', $positiveIntRange == $array);
766+
767+
assertType('true', $negativeIntRange == $true);
768+
assertType('false', $negativeIntRange == $false);
769+
assertType('false', $negativeIntRange == $one);
770+
assertType('false', $negativeIntRange == $zero);
771+
assertType('false', $negativeIntRange == $minusOne);
772+
assertType('false', $negativeIntRange == $oneStr);
773+
assertType('false', $negativeIntRange == $zeroStr);
774+
assertType('false', $negativeIntRange == $minusOneStr);
775+
assertType('false', $negativeIntRange == $plusOneStr);
776+
assertType('false', $negativeIntRange == $null);
777+
assertType('false', $negativeIntRange == $emptyArr);
778+
assertType('false', $negativeIntRange == $array);
779+
780+
// see https://3v4l.org/VudDK
781+
assertType('bool', $minusTenToTen == $true);
782+
assertType('bool', $minusTenToTen == $false);
783+
assertType('bool', $minusTenToTen == $one);
784+
assertType('bool', $minusTenToTen == $zero);
785+
assertType('bool', $minusTenToTen == $minusOne);
786+
assertType('bool', $minusTenToTen == $oneStr);
787+
assertType('bool', $minusTenToTen == $zeroStr);
788+
assertType('bool', $minusTenToTen == $minusOneStr);
789+
assertType('bool', $minusTenToTen == $plusOneStr);
790+
assertType('bool', $minusTenToTen == $null);
791+
assertType('false', $minusTenToTen == $emptyArr);
792+
assertType('false', $minusTenToTen == $array);
793+
794+
// see https://3v4l.org/oJl3K
795+
assertType('false', $minusTenToTen < $null);
796+
assertType('bool', $minusTenToTen > $null);
797+
assertType('bool', $minusTenToTen <= $null);
798+
assertType('true', $minusTenToTen >= $null);
799+
800+
// see https://3v4l.org/oRSgU
801+
assertType('bool', $null < $minusTenToTen);
802+
assertType('false', $null > $minusTenToTen);
803+
assertType('true', $null <= $minusTenToTen);
804+
assertType('bool', $null >= $minusTenToTen);
751805

752806
assertType('false', 5 == $emptyArr);
753807
assertType('false', $emptyArr == 5);

‎tests/PHPStan/Rules/Comparison/ConstantLooseComparisonRuleTest.php

+8-2
Original file line numberDiff line numberDiff line change
@@ -210,15 +210,21 @@ public function testBug11694(): void
210210
39,
211211
'Because the type is coming from a PHPDoc, you can turn off this check by setting <fg=cyan>treatPhpDocTypesAsCertain: false</> in your <fg=cyan>%configurationFile%</>.',
212212
],
213+
[
214+
'Loose comparison using == between true and int<10, 20> will always evaluate to true.',
215+
41,
216+
],
217+
[
218+
'Loose comparison using == between int<10, 20> and true will always evaluate to true.',
219+
42,
220+
],
213221
[
214222
'Loose comparison using == between false and int<10, 20> will always evaluate to false.',
215223
44,
216-
'Because the type is coming from a PHPDoc, you can turn off this check by setting <fg=cyan>treatPhpDocTypesAsCertain: false</> in your <fg=cyan>%configurationFile%</>.',
217224
],
218225
[
219226
'Loose comparison using == between int<10, 20> and false will always evaluate to false.',
220227
45,
221-
'Because the type is coming from a PHPDoc, you can turn off this check by setting <fg=cyan>treatPhpDocTypesAsCertain: false</> in your <fg=cyan>%configurationFile%</>.',
222228
],
223229
]);
224230

0 commit comments

Comments
 (0)
Please sign in to comment.