Skip to content

Commit beba172

Browse files
authored
Improve loose comparison on union type
1 parent aa4a123 commit beba172

11 files changed

+175
-4
lines changed

src/Type/Accessory/AccessoryLowercaseStringType.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
use PHPStan\Type\BooleanType;
1212
use PHPStan\Type\CompoundType;
1313
use PHPStan\Type\Constant\ConstantArrayType;
14+
use PHPStan\Type\Constant\ConstantBooleanType;
1415
use PHPStan\Type\Constant\ConstantIntegerType;
1516
use PHPStan\Type\ErrorType;
1617
use PHPStan\Type\FloatType;
@@ -326,6 +327,14 @@ public function isScalar(): TrinaryLogic
326327

327328
public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType
328329
{
330+
if (
331+
$type->isString()->yes()
332+
&& $type->isLowercaseString()->no()
333+
&& ($type->isNumericString()->no() || $this->isNumericString()->no())
334+
) {
335+
return new ConstantBooleanType(false);
336+
}
337+
329338
return new BooleanType();
330339
}
331340

src/Type/Accessory/AccessoryNonEmptyStringType.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
use PHPStan\Type\BooleanType;
1212
use PHPStan\Type\CompoundType;
1313
use PHPStan\Type\Constant\ConstantArrayType;
14+
use PHPStan\Type\Constant\ConstantBooleanType;
1415
use PHPStan\Type\Constant\ConstantIntegerType;
1516
use PHPStan\Type\Constant\ConstantStringType;
1617
use PHPStan\Type\ErrorType;
@@ -322,6 +323,9 @@ public function isScalar(): TrinaryLogic
322323

323324
public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType
324325
{
326+
if ($type->isString()->yes() && $type->isNonEmptyString()->no()) {
327+
return new ConstantBooleanType(false);
328+
}
325329
return new BooleanType();
326330
}
327331

src/Type/Accessory/AccessoryUppercaseStringType.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
use PHPStan\Type\BooleanType;
1212
use PHPStan\Type\CompoundType;
1313
use PHPStan\Type\Constant\ConstantArrayType;
14+
use PHPStan\Type\Constant\ConstantBooleanType;
1415
use PHPStan\Type\Constant\ConstantIntegerType;
1516
use PHPStan\Type\ErrorType;
1617
use PHPStan\Type\FloatType;
@@ -326,6 +327,14 @@ public function isScalar(): TrinaryLogic
326327

327328
public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType
328329
{
330+
if (
331+
$type->isString()->yes()
332+
&& $type->isUppercaseString()->no()
333+
&& ($type->isNumericString()->no() || $this->isNumericString()->no())
334+
) {
335+
return new ConstantBooleanType(false);
336+
}
337+
329338
return new BooleanType();
330339
}
331340

src/Type/Accessory/NonEmptyArrayType.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
use PHPStan\Type\AcceptsResult;
1010
use PHPStan\Type\BooleanType;
1111
use PHPStan\Type\CompoundType;
12+
use PHPStan\Type\Constant\ConstantBooleanType;
1213
use PHPStan\Type\Constant\ConstantFloatType;
1314
use PHPStan\Type\Constant\ConstantIntegerType;
1415
use PHPStan\Type\ErrorType;
@@ -402,6 +403,10 @@ public function isScalar(): TrinaryLogic
402403

403404
public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType
404405
{
406+
if ($type->isArray()->yes() && $type->isIterableAtLeastOnce()->no()) {
407+
return new ConstantBooleanType(false);
408+
}
409+
405410
return new BooleanType();
406411
}
407412

src/Type/ArrayType.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,10 @@ public function isConstantValue(): TrinaryLogic
249249

250250
public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType
251251
{
252+
if ($type->isInteger()->yes()) {
253+
return new ConstantBooleanType(false);
254+
}
255+
252256
return new BooleanType();
253257
}
254258

src/Type/BooleanType.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,4 +163,16 @@ public function toPhpDocNode(): TypeNode
163163
return new IdentifierTypeNode('bool');
164164
}
165165

166+
public function toTrinaryLogic(): TrinaryLogic
167+
{
168+
if ($this->isTrue()->yes()) {
169+
return TrinaryLogic::createYes();
170+
}
171+
if ($this->isFalse()->yes()) {
172+
return TrinaryLogic::createNo();
173+
}
174+
175+
return TrinaryLogic::createMaybe();
176+
}
177+
166178
}

src/Type/IntersectionType.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -716,7 +716,9 @@ public function isScalar(): TrinaryLogic
716716

717717
public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType
718718
{
719-
return new BooleanType();
719+
return $this->intersectResults(
720+
static fn (Type $innerType): TrinaryLogic => $innerType->looseCompare($type, $phpVersion)->toTrinaryLogic()
721+
)->toBooleanType();
720722
}
721723

722724
public function isOffsetAccessible(): TrinaryLogic

src/Type/UnionType.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -676,7 +676,9 @@ public function isScalar(): TrinaryLogic
676676

677677
public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType
678678
{
679-
return new BooleanType();
679+
return $this->unionResults(
680+
static fn (Type $innerType): TrinaryLogic => $innerType->looseCompare($type, $phpVersion)->toTrinaryLogic()
681+
)->toBooleanType();
680682
}
681683

682684
public function isOffsetAccessible(): TrinaryLogic

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

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -653,4 +653,108 @@ public function sayInt(
653653

654654
}
655655

656+
/**
657+
* @param true|1|"1" $looseOne
658+
* @param false|0|"0" $looseZero
659+
* @param false|1 $constMix
660+
*/
661+
public function sayConstUnion(
662+
$looseOne,
663+
$looseZero,
664+
$constMix
665+
): void
666+
{
667+
assertType('true', $looseOne == 1);
668+
assertType('false', $looseOne == 0);
669+
assertType('true', $looseOne == true);
670+
assertType('false', $looseOne == false);
671+
assertType('true', $looseOne == "1");
672+
assertType('false', $looseOne == "0");
673+
assertType('false', $looseOne == []);
674+
675+
assertType('false', $looseZero == 1);
676+
assertType('true', $looseZero == 0);
677+
assertType('false', $looseZero == true);
678+
assertType('true', $looseZero == false);
679+
assertType('false', $looseZero == "1");
680+
assertType('true', $looseZero == "0");
681+
assertType('bool', $looseZero == []);
682+
683+
assertType('bool', $constMix == 0);
684+
assertType('bool', $constMix == 1);
685+
assertType('bool', $constMix == true);
686+
assertType('bool', $constMix == false);
687+
assertType('bool', $constMix == "1");
688+
assertType('bool', $constMix == "0");
689+
assertType('bool', $constMix == []);
690+
691+
assertType('true', $looseOne == $looseOne);
692+
assertType('true', $looseZero == $looseZero);
693+
assertType('false', $looseOne == $looseZero);
694+
assertType('false', $looseZero == $looseOne);
695+
assertType('bool', $looseOne == $constMix);
696+
assertType('bool', $constMix == $looseOne);
697+
assertType('bool', $looseZero == $constMix);
698+
assertType('bool', $constMix == $looseZero);
699+
}
700+
701+
/**
702+
* @param uppercase-string $upper
703+
* @param lowercase-string $lower
704+
* @param array{} $emptyArr
705+
* @param non-empty-array $nonEmptyArr
706+
* @param int<10, 20> $intRange
707+
*/
708+
public function sayIntersection(
709+
string $upper,
710+
string $lower,
711+
string $s,
712+
array $emptyArr,
713+
array $nonEmptyArr,
714+
array $arr,
715+
int $i,
716+
int $intRange,
717+
): void
718+
{
719+
// https://3v4l.org/q8OP2
720+
assertType('true', '1e2' == '1E2');
721+
assertType('false', '1e2' === '1E2');
722+
723+
assertType('bool', '' == $upper);
724+
assertType('bool', '0' == $upper);
725+
assertType('false', 'a' == $upper);
726+
assertType('false', 'abc' == $upper);
727+
assertType('false', 'aBc' == $upper);
728+
assertType('bool', '1e2' == $upper);
729+
assertType('bool', strtoupper($s) == $upper);
730+
assertType('bool', strtolower($s) == $upper);
731+
assertType('bool', $upper == $lower);
732+
733+
assertType('bool', '0' == $lower);
734+
assertType('false', 'A' == $lower);
735+
assertType('false', 'ABC' == $lower);
736+
assertType('false', 'AbC' == $lower);
737+
assertType('bool', '1E2' == $lower);
738+
assertType('bool', strtoupper($s) == $lower);
739+
assertType('bool', strtolower($s) == $lower);
740+
assertType('bool', $lower == $upper);
741+
742+
assertType('false', $arr == $i);
743+
assertType('false', $nonEmptyArr == $i);
744+
assertType('false', $arr == $intRange);
745+
assertType('false', $nonEmptyArr == $intRange);
746+
assertType('bool', $emptyArr == $nonEmptyArr); // should be false
747+
assertType('false', $nonEmptyArr == $emptyArr);
748+
assertType('bool', $arr == $nonEmptyArr);
749+
assertType('bool', $nonEmptyArr == $arr);
750+
751+
assertType('bool', '' == $lower);
752+
if ($lower != '') {
753+
assertType('false', '' == $lower);
754+
}
755+
if ($upper != '') {
756+
assertType('false', '' == $upper);
757+
}
758+
}
759+
656760
}

tests/PHPStan/Rules/Comparison/ConstantLooseComparisonRuleTest.php

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -181,12 +181,10 @@ public function testBug11694(): void
181181
[
182182
"Loose comparison using == between '13foo' and int<10, 20> will always evaluate to false.",
183183
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%</>.',
185184
],
186185
[
187186
"Loose comparison using == between int<10, 20> and '13foo' will always evaluate to false.",
188187
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%</>.',
190188
],
191189
]);
192190
}
@@ -227,4 +225,15 @@ public function testBug11694(): void
227225
$this->analyse([__DIR__ . '/data/bug-11694.php'], $expectedErrors);
228226
}
229227

228+
public function testBug8800(): void
229+
{
230+
$this->treatPhpDocTypesAsCertain = true;
231+
$this->analyse([__DIR__ . '/data/bug-8800.php'], [
232+
[
233+
'Loose comparison using == between 0|1|false and 2 will always evaluate to false.',
234+
9,
235+
],
236+
]);
237+
}
238+
230239
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace Bug8800;
4+
5+
class HelloWorld
6+
{
7+
public function sayHello(string $s): void
8+
{
9+
var_dump(preg_match('{[A-Z]}', $s) == 2);
10+
}
11+
}

0 commit comments

Comments
 (0)