Skip to content

Commit d222675

Browse files
committed
Fix and improve loose equality type narrowing
1 parent b7fe990 commit d222675

File tree

2 files changed

+174
-1
lines changed

2 files changed

+174
-1
lines changed

src/Analyser/TypeSpecifier.php

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,10 @@
3939
use PHPStan\Type\ArrayType;
4040
use PHPStan\Type\BooleanType;
4141
use PHPStan\Type\ConditionalTypeForParameter;
42+
use PHPStan\Type\Constant\ConstantArrayType;
4243
use PHPStan\Type\Constant\ConstantArrayTypeBuilder;
4344
use PHPStan\Type\Constant\ConstantBooleanType;
45+
use PHPStan\Type\Constant\ConstantFloatType;
4446
use PHPStan\Type\Constant\ConstantIntegerType;
4547
use PHPStan\Type\Constant\ConstantStringType;
4648
use PHPStan\Type\ConstantScalarType;
@@ -1892,7 +1894,20 @@ public function resolveEqual(Expr\BinaryOp\Equal $expr, Scope $scope, TypeSpecif
18921894
if ($expressions !== null) {
18931895
$exprNode = $expressions[0];
18941896
$constantType = $expressions[1];
1895-
if (!$context->null() && ($constantType->getValue() === false || $constantType->getValue() === null)) {
1897+
1898+
if (!$context->null() && $constantType->getValue() === null) {
1899+
$trueTypes = [
1900+
new NullType(),
1901+
new ConstantBooleanType(false),
1902+
new ConstantIntegerType(0),
1903+
new ConstantFloatType(0.0),
1904+
new ConstantStringType(''),
1905+
new ConstantArrayType([], []),
1906+
];
1907+
return $this->create($exprNode, new UnionType($trueTypes), $context, false, $scope, $rootExpr);
1908+
}
1909+
1910+
if (!$context->null() && $constantType->getValue() === false) {
18961911
return $this->specifyTypesInCondition(
18971912
$scope,
18981913
$exprNode,
@@ -1910,6 +1925,28 @@ public function resolveEqual(Expr\BinaryOp\Equal $expr, Scope $scope, TypeSpecif
19101925
);
19111926
}
19121927

1928+
if (!$context->null() && $constantType->getValue() === '') {
1929+
/* There is a difference between php 7.x and 8.x on the equality
1930+
* behavior between zero and the empty string, so to be conservative
1931+
* we leave it untouched regardless of the language version */
1932+
if ($context->true()) {
1933+
$trueTypes = [
1934+
new NullType(),
1935+
new ConstantBooleanType(false),
1936+
new ConstantIntegerType(0),
1937+
new ConstantFloatType(0.0),
1938+
new ConstantStringType(''),
1939+
];
1940+
} else {
1941+
$trueTypes = [
1942+
new NullType(),
1943+
new ConstantBooleanType(false),
1944+
new ConstantStringType(''),
1945+
];
1946+
}
1947+
return $this->create($exprNode, new UnionType($trueTypes), $context, false, $scope, $rootExpr);
1948+
}
1949+
19131950
if (
19141951
$exprNode instanceof FuncCall
19151952
&& $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)