Skip to content

Commit aa4a123

Browse files
authored
Improve loose comparison on integer
1 parent 4d2883b commit aa4a123

8 files changed

+131
-4
lines changed

src/Php/PhpVersion.php

+5
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,11 @@ public function castsNumbersToStringsOnLooseComparison(): bool
284284
return $this->versionId >= 80000;
285285
}
286286

287+
public function nonNumericStringAndIntegerIsFalseOnLooseComparison(): bool
288+
{
289+
return $this->versionId >= 80000;
290+
}
291+
287292
public function supportsCallableInstanceMethods(): bool
288293
{
289294
return $this->versionId < 80000;

src/Type/IntegerType.php

+13
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
use PHPStan\Type\Accessory\AccessoryNumericStringType;
1111
use PHPStan\Type\Accessory\AccessoryUppercaseStringType;
1212
use PHPStan\Type\Constant\ConstantArrayType;
13+
use PHPStan\Type\Constant\ConstantBooleanType;
1314
use PHPStan\Type\Constant\ConstantIntegerType;
1415
use PHPStan\Type\Traits\NonArrayTypeTrait;
1516
use PHPStan\Type\Traits\NonCallableTypeTrait;
@@ -139,6 +140,18 @@ public function isScalar(): TrinaryLogic
139140

140141
public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType
141142
{
143+
if ($type->isArray()->yes()) {
144+
return new ConstantBooleanType(false);
145+
}
146+
147+
if (
148+
$phpVersion->nonNumericStringAndIntegerIsFalseOnLooseComparison()
149+
&& $type->isString()->yes()
150+
&& $type->isNumericString()->no()
151+
) {
152+
return new ConstantBooleanType(false);
153+
}
154+
142155
return new BooleanType();
143156
}
144157

src/Type/Traits/ConstantScalarTypeTrait.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType
5858
}
5959

6060
if ($type->isConstantArray()->yes() && $type->isIterableAtLeastOnce()->no()) {
61-
// @phpstan-ignore equal.notAllowed, equal.invalid
61+
// @phpstan-ignore equal.notAllowed, equal.invalid, equal.alwaysFalse
6262
return new ConstantBooleanType($this->getValue() == []); // phpcs:ignore
6363
}
6464

tests/PHPStan/Analyser/nsrt/equal.php

+1-2
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ public static function createStdClass(): \stdClass
135135
class Baz
136136
{
137137

138-
public function doFoo(string $a, int $b, float $c): void
138+
public function doFoo(string $a, float $c): void
139139
{
140140
$nullableA = $a;
141141
if (rand(0, 1)) {
@@ -152,7 +152,6 @@ public function doFoo(string $a, int $b, float $c): void
152152
assertType('false', 'a' != 'a');
153153
assertType('true', 'a' != 'b');
154154

155-
assertType('bool', $b == 'a');
156155
assertType('bool', $a == 1);
157156
assertType('true', 1 == 1);
158157
assertType('false', 1 == 0);

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

+15
Original file line numberDiff line numberDiff line change
@@ -46,4 +46,19 @@ public function sayEmptyStr(
4646
{
4747
assertType('true', $emptyStr == $zero);
4848
}
49+
50+
/**
51+
* @param 'php' $phpStr
52+
* @param '' $emptyStr
53+
*/
54+
public function sayInt(
55+
$emptyStr,
56+
$phpStr,
57+
int $int
58+
): void
59+
{
60+
assertType('bool', $int == $emptyStr);
61+
assertType('bool', $int == $phpStr);
62+
assertType('bool', $int == 'a');
63+
}
4964
}

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

+22
Original file line numberDiff line numberDiff line change
@@ -46,4 +46,26 @@ public function sayEmptyStr(
4646
{
4747
assertType('false', $emptyStr == $zero); // PHP8+ only
4848
}
49+
50+
/**
51+
* @param 'php' $phpStr
52+
* @param '' $emptyStr
53+
* @param int<10, 20> $intRange
54+
*/
55+
public function sayInt(
56+
$emptyStr,
57+
$phpStr,
58+
int $int,
59+
int $intRange
60+
): void
61+
{
62+
assertType('false', $int == $emptyStr);
63+
assertType('false', $int == $phpStr);
64+
assertType('false', $int == 'a');
65+
66+
assertType('false', $intRange == $emptyStr);
67+
assertType('false', $intRange == $phpStr);
68+
assertType('false', $intRange == 'a');
69+
}
70+
4971
}

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

+52
Original file line numberDiff line numberDiff line change
@@ -601,4 +601,56 @@ public function sayEmptyStr(
601601
assertType('false', $emptyStr == $phpStr);
602602
assertType('true', $emptyStr == $emptyStr);
603603
}
604+
605+
/**
606+
* @param true $true
607+
* @param false $false
608+
* @param 1 $one
609+
* @param 0 $zero
610+
* @param -1 $minusOne
611+
* @param '1' $oneStr
612+
* @param '0' $zeroStr
613+
* @param '-1' $minusOneStr
614+
* @param '+1' $plusOneStr
615+
* @param null $null
616+
* @param array{} $emptyArr
617+
* @param 'php' $phpStr
618+
* @param '' $emptyStr
619+
* @param int<10, 20> $intRange
620+
*/
621+
public function sayInt(
622+
$true,
623+
$false,
624+
$one,
625+
$zero,
626+
$minusOne,
627+
$oneStr,
628+
$zeroStr,
629+
$minusOneStr,
630+
$plusOneStr,
631+
$null,
632+
$emptyArr,
633+
array $array,
634+
int $int,
635+
int $intRange,
636+
): void
637+
{
638+
assertType('bool', $int == $true);
639+
assertType('bool', $int == $false);
640+
assertType('bool', $int == $one);
641+
assertType('bool', $int == $zero);
642+
assertType('bool', $int == $minusOne);
643+
assertType('bool', $int == $oneStr);
644+
assertType('bool', $int == $zeroStr);
645+
assertType('bool', $int == $minusOneStr);
646+
assertType('bool', $int == $plusOneStr);
647+
assertType('bool', $int == $null);
648+
assertType('false', $int == $emptyArr);
649+
assertType('false', $int == $array);
650+
651+
assertType('false', $intRange == $emptyArr);
652+
assertType('false', $intRange == $array);
653+
654+
}
655+
604656
}

tests/PHPStan/Rules/Comparison/ConstantLooseComparisonRuleTest.php

+22-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use PHPStan\Rules\Rule;
66
use PHPStan\Testing\RuleTestCase;
7+
use function array_merge;
78
use const PHP_VERSION_ID;
89

910
/**
@@ -142,7 +143,7 @@ public function testTreatPhpDocTypesAsCertain(bool $treatPhpDocTypesAsCertain, a
142143

143144
public function testBug11694(): void
144145
{
145-
$this->analyse([__DIR__ . '/data/bug-11694.php'], [
146+
$expectedErrors = [
146147
[
147148
'Loose comparison using == between 3 and int<10, 20> will always evaluate to false.',
148149
17,
@@ -173,6 +174,24 @@ public function testBug11694(): void
173174
27,
174175
'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%</>.',
175176
],
177+
];
178+
179+
if (PHP_VERSION_ID >= 80000) {
180+
$expectedErrors = array_merge($expectedErrors, [
181+
[
182+
"Loose comparison using == between '13foo' and int<10, 20> will always evaluate to false.",
183+
29,
184+
'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%</>.',
185+
],
186+
[
187+
"Loose comparison using == between int<10, 20> and '13foo' will always evaluate to false.",
188+
30,
189+
'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%</>.',
190+
],
191+
]);
192+
}
193+
194+
$expectedErrors = array_merge($expectedErrors, [
176195
[
177196
'Loose comparison using == between \' 3\' and int<10, 20> will always evaluate to false.',
178197
32,
@@ -204,6 +223,8 @@ public function testBug11694(): void
204223
'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%</>.',
205224
],
206225
]);
226+
227+
$this->analyse([__DIR__ . '/data/bug-11694.php'], $expectedErrors);
207228
}
208229

209230
}

0 commit comments

Comments
 (0)