Skip to content

Commit 5e3a364

Browse files
authored
Add basic type narrowing for $a != ''
1 parent f83b559 commit 5e3a364

File tree

8 files changed

+281
-16
lines changed

8 files changed

+281
-16
lines changed

Diff for: src/Analyser/TypeSpecifier.php

+87-6
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;
@@ -1610,7 +1611,7 @@ private function processBooleanNotSureConditionalTypes(Scope $scope, SpecifiedTy
16101611
}
16111612

16121613
/**
1613-
* @return array{Expr, ConstantScalarType}|null
1614+
* @return array{Expr, ConstantScalarType, Type}|null
16141615
*/
16151616
private function findTypeExpressionsFromBinaryOperation(Scope $scope, Node\Expr\BinaryOp $binaryOperation): ?array
16161617
{
@@ -1632,13 +1633,13 @@ private function findTypeExpressionsFromBinaryOperation(Scope $scope, Node\Expr\
16321633
&& !$rightExpr instanceof ConstFetch
16331634
&& !$rightExpr instanceof ClassConstFetch
16341635
) {
1635-
return [$binaryOperation->right, $leftType];
1636+
return [$binaryOperation->right, $leftType, $rightType];
16361637
} elseif (
16371638
$rightType instanceof ConstantScalarType
16381639
&& !$leftExpr instanceof ConstFetch
16391640
&& !$leftExpr instanceof ClassConstFetch
16401641
) {
1641-
return [$binaryOperation->left, $rightType];
1642+
return [$binaryOperation->left, $rightType, $leftType];
16421643
}
16431644

16441645
return null;
@@ -1949,7 +1950,21 @@ 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+
$otherType = $expressions[2];
1954+
1955+
if (!$context->null() && $constantType->getValue() === null) {
1956+
$trueTypes = [
1957+
new NullType(),
1958+
new ConstantBooleanType(false),
1959+
new ConstantIntegerType(0),
1960+
new ConstantFloatType(0.0),
1961+
new ConstantStringType(''),
1962+
new ConstantArrayType([], []),
1963+
];
1964+
return $this->create($exprNode, new UnionType($trueTypes), $context, false, $scope, $rootExpr);
1965+
}
1966+
1967+
if (!$context->null() && $constantType->getValue() === false) {
19531968
return $this->specifyTypesInCondition(
19541969
$scope,
19551970
$exprNode,
@@ -1967,6 +1982,52 @@ public function resolveEqual(Expr\BinaryOp\Equal $expr, Scope $scope, TypeSpecif
19671982
);
19681983
}
19691984

1985+
if (!$context->null() && $constantType->getValue() === 0 && !$otherType->isInteger()->yes() && !$otherType->isBoolean()->yes()) {
1986+
/* There is a difference between php 7.x and 8.x on the equality
1987+
* behavior between zero and the empty string, so to be conservative
1988+
* we leave it untouched regardless of the language version */
1989+
if ($context->true()) {
1990+
$trueTypes = [
1991+
new NullType(),
1992+
new ConstantBooleanType(false),
1993+
new ConstantIntegerType(0),
1994+
new ConstantFloatType(0.0),
1995+
new StringType(),
1996+
];
1997+
} else {
1998+
$trueTypes = [
1999+
new NullType(),
2000+
new ConstantBooleanType(false),
2001+
new ConstantIntegerType(0),
2002+
new ConstantFloatType(0.0),
2003+
new ConstantStringType('0'),
2004+
];
2005+
}
2006+
return $this->create($exprNode, new UnionType($trueTypes), $context, false, $scope, $rootExpr);
2007+
}
2008+
2009+
if (!$context->null() && $constantType->getValue() === '') {
2010+
/* There is a difference between php 7.x and 8.x on the equality
2011+
* behavior between zero and the empty string, so to be conservative
2012+
* we leave it untouched regardless of the language version */
2013+
if ($context->true()) {
2014+
$trueTypes = [
2015+
new NullType(),
2016+
new ConstantBooleanType(false),
2017+
new ConstantIntegerType(0),
2018+
new ConstantFloatType(0.0),
2019+
new ConstantStringType(''),
2020+
];
2021+
} else {
2022+
$trueTypes = [
2023+
new NullType(),
2024+
new ConstantBooleanType(false),
2025+
new ConstantStringType(''),
2026+
];
2027+
}
2028+
return $this->create($exprNode, new UnionType($trueTypes), $context, false, $scope, $rootExpr);
2029+
}
2030+
19702031
if (
19712032
$exprNode instanceof FuncCall
19722033
&& $exprNode->name instanceof Name
@@ -2060,11 +2121,13 @@ public function resolveEqual(Expr\BinaryOp\Equal $expr, Scope $scope, TypeSpecif
20602121

20612122
public function resolveIdentical(Expr\BinaryOp\Identical $expr, Scope $scope, TypeSpecifierContext $context, ?Expr $rootExpr): SpecifiedTypes
20622123
{
2124+
// Normalize to: fn() === expr
20632125
$leftExpr = $expr->left;
20642126
$rightExpr = $expr->right;
20652127
if ($rightExpr instanceof FuncCall && !$leftExpr instanceof FuncCall) {
20662128
[$leftExpr, $rightExpr] = [$rightExpr, $leftExpr];
20672129
}
2130+
20682131
$unwrappedLeftExpr = $leftExpr;
20692132
if ($leftExpr instanceof AlwaysRememberedExpr) {
20702133
$unwrappedLeftExpr = $leftExpr->getExpr();
@@ -2073,8 +2136,10 @@ public function resolveIdentical(Expr\BinaryOp\Identical $expr, Scope $scope, Ty
20732136
if ($rightExpr instanceof AlwaysRememberedExpr) {
20742137
$unwrappedRightExpr = $rightExpr->getExpr();
20752138
}
2139+
20762140
$rightType = $scope->getType($rightExpr);
20772141

2142+
// (count($a) === $b)
20782143
if (
20792144
!$context->null()
20802145
&& $unwrappedLeftExpr instanceof FuncCall
@@ -2139,6 +2204,7 @@ public function resolveIdentical(Expr\BinaryOp\Identical $expr, Scope $scope, Ty
21392204
}
21402205
}
21412206

2207+
// strlen($a) === $b
21422208
if (
21432209
!$context->null()
21442210
&& $unwrappedLeftExpr instanceof FuncCall
@@ -2175,6 +2241,7 @@ public function resolveIdentical(Expr\BinaryOp\Identical $expr, Scope $scope, Ty
21752241
}
21762242
}
21772243

2244+
// preg_match($a) === $b
21782245
if (
21792246
$context->true()
21802247
&& $unwrappedLeftExpr instanceof FuncCall
@@ -2190,6 +2257,7 @@ public function resolveIdentical(Expr\BinaryOp\Identical $expr, Scope $scope, Ty
21902257
);
21912258
}
21922259

2260+
// get_class($a) === 'Foo'
21932261
if (
21942262
$context->true()
21952263
&& $unwrappedLeftExpr instanceof FuncCall
@@ -2209,6 +2277,7 @@ public function resolveIdentical(Expr\BinaryOp\Identical $expr, Scope $scope, Ty
22092277
}
22102278
}
22112279

2280+
// get_class($a) === 'Foo'
22122281
if (
22132282
$context->truthy()
22142283
&& $unwrappedLeftExpr instanceof FuncCall
@@ -2289,6 +2358,7 @@ public function resolveIdentical(Expr\BinaryOp\Identical $expr, Scope $scope, Ty
22892358
}
22902359
}
22912360

2361+
// $a::class === 'Foo'
22922362
if (
22932363
$context->true() &&
22942364
$unwrappedLeftExpr instanceof ClassConstFetch &&
@@ -2311,6 +2381,8 @@ public function resolveIdentical(Expr\BinaryOp\Identical $expr, Scope $scope, Ty
23112381
}
23122382

23132383
$leftType = $scope->getType($leftExpr);
2384+
2385+
// 'Foo' === $a::class
23142386
if (
23152387
$context->true() &&
23162388
$unwrappedRightExpr instanceof ClassConstFetch &&
@@ -2356,7 +2428,11 @@ public function resolveIdentical(Expr\BinaryOp\Identical $expr, Scope $scope, Ty
23562428
$types = null;
23572429
if (
23582430
count($leftType->getFiniteTypes()) === 1
2359-
|| ($context->true() && $leftType->isConstantValue()->yes() && !$rightType->equals($leftType) && $rightType->isSuperTypeOf($leftType)->yes())
2431+
|| (
2432+
$context->true()
2433+
&& $leftType->isConstantValue()->yes()
2434+
&& !$rightType->equals($leftType)
2435+
&& $rightType->isSuperTypeOf($leftType)->yes())
23602436
) {
23612437
$types = $this->create(
23622438
$rightExpr,
@@ -2379,7 +2455,12 @@ public function resolveIdentical(Expr\BinaryOp\Identical $expr, Scope $scope, Ty
23792455
}
23802456
if (
23812457
count($rightType->getFiniteTypes()) === 1
2382-
|| ($context->true() && $rightType->isConstantValue()->yes() && !$leftType->equals($rightType) && $leftType->isSuperTypeOf($rightType)->yes())
2458+
|| (
2459+
$context->true()
2460+
&& $rightType->isConstantValue()->yes()
2461+
&& !$leftType->equals($rightType)
2462+
&& $leftType->isSuperTypeOf($rightType)->yes()
2463+
)
23832464
) {
23842465
$leftTypes = $this->create(
23852466
$leftExpr,

Diff for: tests/PHPStan/Analyser/TypeSpecifierTest.php

+2-2
Original file line numberDiff line numberDiff line change
@@ -477,8 +477,8 @@ public function dataCondition(): iterable
477477
new Variable('foo'),
478478
new Expr\ConstFetch(new Name('null')),
479479
),
480-
['$foo' => self::SURE_NOT_TRUTHY],
481-
['$foo' => self::SURE_NOT_FALSEY],
480+
['$foo' => '0|0.0|\'\'|array{}|false|null'],
481+
['$foo' => '~0|0.0|\'\'|array{}|false|null'],
482482
],
483483
[
484484
new Expr\BinaryOp\Identical(

0 commit comments

Comments
 (0)