Skip to content

Commit 1d32097

Browse files
thg2kondrejmirtes
authored andcommitted
Fix and improve loose equality type narrowing
1 parent f83b559 commit 1d32097

File tree

2 files changed

+173
-1
lines changed

2 files changed

+173
-1
lines changed

src/Analyser/TypeSpecifier.php

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
use PHPStan\Type\Constant\ConstantArrayType;
4444
use PHPStan\Type\Constant\ConstantArrayTypeBuilder;
4545
use PHPStan\Type\Constant\ConstantBooleanType;
46+
use PHPStan\Type\Constant\ConstantFloatType;
4647
use PHPStan\Type\Constant\ConstantIntegerType;
4748
use PHPStan\Type\Constant\ConstantStringType;
4849
use PHPStan\Type\ConstantScalarType;
@@ -1949,7 +1950,20 @@ public function resolveEqual(Expr\BinaryOp\Equal $expr, Scope $scope, TypeSpecif
19491950
if ($expressions !== null) {
19501951
$exprNode = $expressions[0];
19511952
$constantType = $expressions[1];
1952-
if (!$context->null() && ($constantType->getValue() === false || $constantType->getValue() === null)) {
1953+
1954+
if (!$context->null() && $constantType->getValue() === null) {
1955+
$trueTypes = [
1956+
new NullType(),
1957+
new ConstantBooleanType(false),
1958+
new ConstantIntegerType(0),
1959+
new ConstantFloatType(0.0),
1960+
new ConstantStringType(''),
1961+
new ConstantArrayType([], []),
1962+
];
1963+
return $this->create($exprNode, new UnionType($trueTypes), $context, false, $scope, $rootExpr);
1964+
}
1965+
1966+
if (!$context->null() && $constantType->getValue() === false) {
19531967
return $this->specifyTypesInCondition(
19541968
$scope,
19551969
$exprNode,
@@ -1967,6 +1981,28 @@ public function resolveEqual(Expr\BinaryOp\Equal $expr, Scope $scope, TypeSpecif
19671981
);
19681982
}
19691983

1984+
if (!$context->null() && $constantType->getValue() === '') {
1985+
/* There is a difference between php 7.x and 8.x on the equality
1986+
* behavior between zero and the empty string, so to be conservative
1987+
* we leave it untouched regardless of the language version */
1988+
if ($context->true()) {
1989+
$trueTypes = [
1990+
new NullType(),
1991+
new ConstantBooleanType(false),
1992+
new ConstantIntegerType(0),
1993+
new ConstantFloatType(0.0),
1994+
new ConstantStringType(''),
1995+
];
1996+
} else {
1997+
$trueTypes = [
1998+
new NullType(),
1999+
new ConstantBooleanType(false),
2000+
new ConstantStringType(''),
2001+
];
2002+
}
2003+
return $this->create($exprNode, new UnionType($trueTypes), $context, false, $scope, $rootExpr);
2004+
}
2005+
19702006
if (
19712007
$exprNode instanceof FuncCall
19722008
&& $exprNode->name instanceof Name
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
<?php declare(strict_types = 1);
2+
3+
// phpcs:disable SlevomatCodingStandard.ControlStructures.DisallowYodaComparison.DisallowedYodaComparison
4+
// phpcs:disable SlevomatCodingStandard.Operators.DisallowEqualOperators.DisallowedEqualOperator
5+
// phpcs:disable SlevomatCodingStandard.Operators.DisallowEqualOperators.DisallowedNotEqualOperator
6+
// phpcs:disable Squiz.Functions.GlobalFunction.Found
7+
// phpcs:disable Squiz.Strings.DoubleQuoteUsage.NotRequired
8+
9+
namespace EqualTypeNarrowing;
10+
11+
use function PHPStan\Testing\assertType;
12+
13+
/**
14+
* @param 0|0.0|1|''|'0'|'x'|array{}|bool|object|null $x
15+
* @param int|string|null $y
16+
* @param mixed $z
17+
*/
18+
function doNull($x, $y, $z): void
19+
{
20+
if ($x == null) {
21+
assertType("0|0.0|''|array{}|false|null", $x);
22+
} else {
23+
assertType("1|'0'|'x'|object|true", $x);
24+
}
25+
if (null != $x) {
26+
assertType("1|'0'|'x'|object|true", $x);
27+
} else {
28+
assertType("0|0.0|''|array{}|false|null", $x);
29+
}
30+
31+
if ($y == null) {
32+
assertType("0|''|null", $y);
33+
} else {
34+
assertType("int<min, -1>|int<1, max>|non-empty-string", $y);
35+
}
36+
37+
if ($z == null) {
38+
assertType("0|0.0|''|array{}|false|null", $z);
39+
} else {
40+
assertType("mixed~0|0.0|''|array{}|false|null", $z);
41+
}
42+
}
43+
44+
/**
45+
* @param 0|0.0|1|''|'0'|'x'|array{}|bool|object|null $x
46+
* @param int|string|null $y
47+
* @param mixed $z
48+
*/
49+
function doFalse($x, $y, $z): void
50+
{
51+
if ($x == false) {
52+
assertType("0|0.0|''|'0'|array{}|false|null", $x);
53+
} else {
54+
assertType("1|'x'|object|true", $x);
55+
}
56+
if (false != $x) {
57+
assertType("1|'x'|object|true", $x);
58+
} else {
59+
assertType("0|0.0|''|'0'|array{}|false|null", $x);
60+
}
61+
62+
if ($y == false) {
63+
assertType("0|''|'0'|null", $y);
64+
} else {
65+
assertType("int<min, -1>|int<1, max>|non-falsy-string", $y);
66+
}
67+
68+
if ($z == false) {
69+
assertType("0|0.0|''|'0'|array{}|false|null", $z);
70+
} else {
71+
assertType("mixed~0|0.0|''|'0'|array{}|false|null", $z);
72+
}
73+
}
74+
75+
/**
76+
* @param 0|0.0|1|''|'0'|'x'|array{}|bool|object|null $x
77+
* @param int|string|null $y
78+
* @param mixed $z
79+
*/
80+
function doTrue($x, $y, $z): void
81+
{
82+
if ($x == true) {
83+
assertType("1|'x'|object|true", $x);
84+
} else {
85+
assertType("0|0.0|''|'0'|array{}|false|null", $x);
86+
}
87+
if (true != $x) {
88+
assertType("0|0.0|''|'0'|array{}|false|null", $x);
89+
} else {
90+
assertType("1|'x'|object|true", $x);
91+
}
92+
93+
if ($y == true) {
94+
assertType("int<min, -1>|int<1, max>|non-falsy-string", $y);
95+
} else {
96+
assertType("0|''|'0'|null", $y);
97+
}
98+
99+
if ($z == true) {
100+
assertType("mixed~0|0.0|''|'0'|array{}|false|null", $z);
101+
} else {
102+
assertType("0|0.0|''|'0'|array{}|false|null", $z);
103+
}
104+
}
105+
106+
/**
107+
* @param 0|0.0|1|''|'0'|'x'|array{}|bool|object|null $x
108+
* @param int|string|null $y
109+
* @param mixed $z
110+
*/
111+
function doEmptyString($x, $y, $z): void
112+
{
113+
// PHP 7.x/8.x compatibility: Keep zero in both cases
114+
if ($x == '') {
115+
assertType("0|0.0|''|false|null", $x);
116+
} else {
117+
assertType("0|0.0|1|'0'|'x'|array{}|object|true", $x);
118+
}
119+
if ('' != $x) {
120+
assertType("0|0.0|1|'0'|'x'|array{}|object|true", $x);
121+
} else {
122+
assertType("0|0.0|''|false|null", $x);
123+
}
124+
125+
if ($y == '') {
126+
assertType("0|''|null", $y);
127+
} else {
128+
assertType("int|non-empty-string", $y);
129+
}
130+
131+
if ($z == '') {
132+
assertType("0|0.0|''|false|null", $z);
133+
} else {
134+
assertType("mixed~''|false|null", $z);
135+
}
136+
}

0 commit comments

Comments
 (0)