diff --git a/src/Analyser/TypeSpecifier.php b/src/Analyser/TypeSpecifier.php index 68864c18b6..088236bac5 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -43,6 +43,7 @@ use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantArrayTypeBuilder; use PHPStan\Type\Constant\ConstantBooleanType; +use PHPStan\Type\Constant\ConstantFloatType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\ConstantScalarType; @@ -1610,7 +1611,7 @@ private function processBooleanNotSureConditionalTypes(Scope $scope, SpecifiedTy } /** - * @return array{Expr, ConstantScalarType}|null + * @return array{Expr, ConstantScalarType, Type}|null */ private function findTypeExpressionsFromBinaryOperation(Scope $scope, Node\Expr\BinaryOp $binaryOperation): ?array { @@ -1632,13 +1633,13 @@ private function findTypeExpressionsFromBinaryOperation(Scope $scope, Node\Expr\ && !$rightExpr instanceof ConstFetch && !$rightExpr instanceof ClassConstFetch ) { - return [$binaryOperation->right, $leftType]; + return [$binaryOperation->right, $leftType, $rightType]; } elseif ( $rightType instanceof ConstantScalarType && !$leftExpr instanceof ConstFetch && !$leftExpr instanceof ClassConstFetch ) { - return [$binaryOperation->left, $rightType]; + return [$binaryOperation->left, $rightType, $leftType]; } return null; @@ -1949,7 +1950,21 @@ public function resolveEqual(Expr\BinaryOp\Equal $expr, Scope $scope, TypeSpecif if ($expressions !== null) { $exprNode = $expressions[0]; $constantType = $expressions[1]; - if (!$context->null() && ($constantType->getValue() === false || $constantType->getValue() === null)) { + $otherType = $expressions[2]; + + if (!$context->null() && $constantType->getValue() === null) { + $trueTypes = [ + new NullType(), + new ConstantBooleanType(false), + new ConstantIntegerType(0), + new ConstantFloatType(0.0), + new ConstantStringType(''), + new ConstantArrayType([], []), + ]; + return $this->create($exprNode, new UnionType($trueTypes), $context, false, $scope, $rootExpr); + } + + if (!$context->null() && $constantType->getValue() === false) { return $this->specifyTypesInCondition( $scope, $exprNode, @@ -1967,6 +1982,52 @@ public function resolveEqual(Expr\BinaryOp\Equal $expr, Scope $scope, TypeSpecif ); } + if (!$context->null() && $constantType->getValue() === 0 && !$otherType->isInteger()->yes() && !$otherType->isBoolean()->yes()) { + /* There is a difference between php 7.x and 8.x on the equality + * behavior between zero and the empty string, so to be conservative + * we leave it untouched regardless of the language version */ + if ($context->true()) { + $trueTypes = [ + new NullType(), + new ConstantBooleanType(false), + new ConstantIntegerType(0), + new ConstantFloatType(0.0), + new StringType(), + ]; + } else { + $trueTypes = [ + new NullType(), + new ConstantBooleanType(false), + new ConstantIntegerType(0), + new ConstantFloatType(0.0), + new ConstantStringType('0'), + ]; + } + return $this->create($exprNode, new UnionType($trueTypes), $context, false, $scope, $rootExpr); + } + + if (!$context->null() && $constantType->getValue() === '') { + /* There is a difference between php 7.x and 8.x on the equality + * behavior between zero and the empty string, so to be conservative + * we leave it untouched regardless of the language version */ + if ($context->true()) { + $trueTypes = [ + new NullType(), + new ConstantBooleanType(false), + new ConstantIntegerType(0), + new ConstantFloatType(0.0), + new ConstantStringType(''), + ]; + } else { + $trueTypes = [ + new NullType(), + new ConstantBooleanType(false), + new ConstantStringType(''), + ]; + } + return $this->create($exprNode, new UnionType($trueTypes), $context, false, $scope, $rootExpr); + } + if ( $exprNode instanceof FuncCall && $exprNode->name instanceof Name @@ -2060,11 +2121,13 @@ public function resolveEqual(Expr\BinaryOp\Equal $expr, Scope $scope, TypeSpecif public function resolveIdentical(Expr\BinaryOp\Identical $expr, Scope $scope, TypeSpecifierContext $context, ?Expr $rootExpr): SpecifiedTypes { + // Normalize to: fn() === expr $leftExpr = $expr->left; $rightExpr = $expr->right; if ($rightExpr instanceof FuncCall && !$leftExpr instanceof FuncCall) { [$leftExpr, $rightExpr] = [$rightExpr, $leftExpr]; } + $unwrappedLeftExpr = $leftExpr; if ($leftExpr instanceof AlwaysRememberedExpr) { $unwrappedLeftExpr = $leftExpr->getExpr(); @@ -2073,8 +2136,10 @@ public function resolveIdentical(Expr\BinaryOp\Identical $expr, Scope $scope, Ty if ($rightExpr instanceof AlwaysRememberedExpr) { $unwrappedRightExpr = $rightExpr->getExpr(); } + $rightType = $scope->getType($rightExpr); + // (count($a) === $b) if ( !$context->null() && $unwrappedLeftExpr instanceof FuncCall @@ -2139,6 +2204,7 @@ public function resolveIdentical(Expr\BinaryOp\Identical $expr, Scope $scope, Ty } } + // strlen($a) === $b if ( !$context->null() && $unwrappedLeftExpr instanceof FuncCall @@ -2175,6 +2241,7 @@ public function resolveIdentical(Expr\BinaryOp\Identical $expr, Scope $scope, Ty } } + // preg_match($a) === $b if ( $context->true() && $unwrappedLeftExpr instanceof FuncCall @@ -2190,6 +2257,7 @@ public function resolveIdentical(Expr\BinaryOp\Identical $expr, Scope $scope, Ty ); } + // get_class($a) === 'Foo' if ( $context->true() && $unwrappedLeftExpr instanceof FuncCall @@ -2209,6 +2277,7 @@ public function resolveIdentical(Expr\BinaryOp\Identical $expr, Scope $scope, Ty } } + // get_class($a) === 'Foo' if ( $context->truthy() && $unwrappedLeftExpr instanceof FuncCall @@ -2289,6 +2358,7 @@ public function resolveIdentical(Expr\BinaryOp\Identical $expr, Scope $scope, Ty } } + // $a::class === 'Foo' if ( $context->true() && $unwrappedLeftExpr instanceof ClassConstFetch && @@ -2311,6 +2381,8 @@ public function resolveIdentical(Expr\BinaryOp\Identical $expr, Scope $scope, Ty } $leftType = $scope->getType($leftExpr); + + // 'Foo' === $a::class if ( $context->true() && $unwrappedRightExpr instanceof ClassConstFetch && @@ -2356,7 +2428,11 @@ public function resolveIdentical(Expr\BinaryOp\Identical $expr, Scope $scope, Ty $types = null; if ( count($leftType->getFiniteTypes()) === 1 - || ($context->true() && $leftType->isConstantValue()->yes() && !$rightType->equals($leftType) && $rightType->isSuperTypeOf($leftType)->yes()) + || ( + $context->true() + && $leftType->isConstantValue()->yes() + && !$rightType->equals($leftType) + && $rightType->isSuperTypeOf($leftType)->yes()) ) { $types = $this->create( $rightExpr, @@ -2379,7 +2455,12 @@ public function resolveIdentical(Expr\BinaryOp\Identical $expr, Scope $scope, Ty } if ( count($rightType->getFiniteTypes()) === 1 - || ($context->true() && $rightType->isConstantValue()->yes() && !$leftType->equals($rightType) && $leftType->isSuperTypeOf($rightType)->yes()) + || ( + $context->true() + && $rightType->isConstantValue()->yes() + && !$leftType->equals($rightType) + && $leftType->isSuperTypeOf($rightType)->yes() + ) ) { $leftTypes = $this->create( $leftExpr, diff --git a/tests/PHPStan/Analyser/TypeSpecifierTest.php b/tests/PHPStan/Analyser/TypeSpecifierTest.php index ced1959085..999c7169a3 100644 --- a/tests/PHPStan/Analyser/TypeSpecifierTest.php +++ b/tests/PHPStan/Analyser/TypeSpecifierTest.php @@ -477,8 +477,8 @@ public function dataCondition(): iterable new Variable('foo'), new Expr\ConstFetch(new Name('null')), ), - ['$foo' => self::SURE_NOT_TRUTHY], - ['$foo' => self::SURE_NOT_FALSEY], + ['$foo' => '0|0.0|\'\'|array{}|false|null'], + ['$foo' => '~0|0.0|\'\'|array{}|false|null'], ], [ new Expr\BinaryOp\Identical( diff --git a/tests/PHPStan/Analyser/nsrt/equal-narrow.php b/tests/PHPStan/Analyser/nsrt/equal-narrow.php new file mode 100644 index 0000000000..774377f400 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/equal-narrow.php @@ -0,0 +1,180 @@ +|int<1, max>|non-empty-string", $y); + } + + if ($z == null) { + assertType("0|0.0|''|array{}|false|null", $z); + } else { + assertType("mixed~(0|0.0|''|array{}|false|null)", $z); + } +} + +/** + * @param 0|0.0|1|''|'0'|'x'|array{}|bool|object|null $x + * @param int|string|null $y + * @param mixed $z + */ +function doFalse($x, $y, $z): void +{ + if ($x == false) { + assertType("0|0.0|''|'0'|array{}|false|null", $x); + } else { + assertType("1|'x'|object|true", $x); + } + if (false != $x) { + assertType("1|'x'|object|true", $x); + } else { + assertType("0|0.0|''|'0'|array{}|false|null", $x); + } + + if (!$x) { + assertType("0|0.0|''|'0'|array{}|false|null", $x); + } else { + assertType("1|'x'|object|true", $x); + } + + if ($y == false) { + assertType("0|''|'0'|null", $y); + } else { + assertType("int|int<1, max>|non-falsy-string", $y); + } + + if ($z == false) { + assertType("0|0.0|''|'0'|array{}|false|null", $z); + } else { + assertType("mixed~(0|0.0|''|'0'|array{}|false|null)", $z); + } +} + +/** + * @param 0|0.0|1|''|'0'|'x'|array{}|bool|object|null $x + * @param int|string|null $y + * @param mixed $z + */ +function doTrue($x, $y, $z): void +{ + if ($x == true) { + assertType("1|'x'|object|true", $x); + } else { + assertType("0|0.0|''|'0'|array{}|false|null", $x); + } + if (true != $x) { + assertType("0|0.0|''|'0'|array{}|false|null", $x); + } else { + assertType("1|'x'|object|true", $x); + } + + if ($x) { + assertType("1|'x'|object|true", $x); + } else { + assertType("0|0.0|''|'0'|array{}|false|null", $x); + } + + if ($y == true) { + assertType("int|int<1, max>|non-falsy-string", $y); + } else { + assertType("0|''|'0'|null", $y); + } + + if ($z == true) { + assertType("mixed~(0|0.0|''|'0'|array{}|false|null)", $z); + } else { + assertType("0|0.0|''|'0'|array{}|false|null", $z); + } +} + +/** + * @param 0|0.0|1|''|'0'|'x'|array{}|bool|object|null $x + * @param int|string|null $y + * @param mixed $z + */ +function doZero($x, $y, $z): void +{ + // PHP 7.x/8.x compatibility: Keep zero in both cases + if ($x == 0) { + assertType("0|0.0|''|'0'|'x'|false|null", $x); + } else { + assertType("1|''|'x'|array{}|object|true", $x); + } + if (0 != $x) { + assertType("1|''|'x'|array{}|object|true", $x); + } else { + assertType("0|0.0|''|'0'|'x'|false|null", $x); + } + + if ($y == 0) { + assertType("0|string|null", $y); + } else { + assertType("int|int<1, max>|string", $y); + } + + if ($z == 0) { + assertType("0|0.0|string|false|null", $z); + } else { + assertType("mixed~(0|0.0|'0'|false|null)", $z); + } +} + +/** + * @param 0|0.0|1|''|'0'|'x'|array{}|bool|object|null $x + * @param int|string|null $y + * @param mixed $z + */ +function doEmptyString($x, $y, $z): void +{ + // PHP 7.x/8.x compatibility: Keep zero in both cases + if ($x == '') { + assertType("0|0.0|''|false|null", $x); + } else { + assertType("0|0.0|1|'0'|'x'|array{}|object|true", $x); + } + if ('' != $x) { + assertType("0|0.0|1|'0'|'x'|array{}|object|true", $x); + } else { + assertType("0|0.0|''|false|null", $x); + } + + if ($y == '') { + assertType("0|''|null", $y); + } else { + assertType("int|non-empty-string", $y); + } + + if ($z == '') { + assertType("0|0.0|''|false|null", $z); + } else { + assertType("mixed~(''|false|null)", $z); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/integer-range-types.php b/tests/PHPStan/Analyser/nsrt/integer-range-types.php index cd35c77953..876a791352 100644 --- a/tests/PHPStan/Analyser/nsrt/integer-range-types.php +++ b/tests/PHPStan/Analyser/nsrt/integer-range-types.php @@ -449,7 +449,7 @@ public function zeroIssues($positive, $negative) function subtract($m) { if ($m != 0) { - assertType("mixed", $m); // could be "mixed~0|0.0|''|'0'|array{}|false|null" + assertType("mixed~(0|0.0|'0'|false|null)", $m); // could be "mixed~(0|0.0|''|'0'|false|null)" assertType('int', (int) $m); } if ($m !== 0) { diff --git a/tests/PHPStan/Analyser/nsrt/narrow-cast.php b/tests/PHPStan/Analyser/nsrt/narrow-cast.php index 82e09e0bd3..1da8858473 100644 --- a/tests/PHPStan/Analyser/nsrt/narrow-cast.php +++ b/tests/PHPStan/Analyser/nsrt/narrow-cast.php @@ -33,7 +33,7 @@ function castString($x, string $s, bool $b) { if ((string) $x) { assertType('int<-5, 5>', $x); } else { - assertType('int<-5, 5>', $x); + assertType('0', $x); } if ((string) $b) { diff --git a/tests/PHPStan/Analyser/nsrt/non-empty-string.php b/tests/PHPStan/Analyser/nsrt/non-empty-string.php index da8426c905..5400b1d31a 100644 --- a/tests/PHPStan/Analyser/nsrt/non-empty-string.php +++ b/tests/PHPStan/Analyser/nsrt/non-empty-string.php @@ -422,8 +422,8 @@ function subtract($m) { assertType('non-falsy-string', (string) $m); } if ($m != '') { - assertType("mixed", $m); - assertType('string', (string) $m); + assertType("mixed~(''|false|null)", $m); + assertType('non-empty-string', (string) $m); } if ($m !== '') { assertType("mixed~''", $m); diff --git a/tests/PHPStan/Rules/Comparison/ElseIfConstantConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/ElseIfConstantConditionRuleTest.php index bf67146684..6c51033309 100644 --- a/tests/PHPStan/Rules/Comparison/ElseIfConstantConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/ElseIfConstantConditionRuleTest.php @@ -135,6 +135,10 @@ public function testBug11674(): void 'Elseif condition is always false.', 28, ], + [ + 'Elseif condition is always false.', + 36, + ], ]); } diff --git a/tests/PHPStan/Rules/Comparison/data/bug-11674.php b/tests/PHPStan/Rules/Comparison/data/bug-11674.php index 7af6660da4..6156b8e1cb 100644 --- a/tests/PHPStan/Rules/Comparison/data/bug-11674.php +++ b/tests/PHPStan/Rules/Comparison/data/bug-11674.php @@ -10,7 +10,7 @@ function show() : void { if ((int) $this->param) { echo 1; } elseif ($this->param) { - echo 2; + echo 2; // might be "0" } } @@ -18,7 +18,7 @@ function show2() : void { if ((float) $this->param) { echo 1; } elseif ($this->param) { - echo 2; + echo 2; // might be "0" } } @@ -26,7 +26,7 @@ function show3() : void { if ((bool) $this->param) { echo 1; } elseif ($this->param) { - echo 2; + echo 2; // not possible } } @@ -34,7 +34,7 @@ function show4() : void { if ((string) $this->param) { echo 1; } elseif ($this->param) { - echo 2; + echo 2; // not possible } } }