diff --git a/src/Analyser/TypeSpecifier.php b/src/Analyser/TypeSpecifier.php index 75000f1956..09746b2cec 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -1604,6 +1604,36 @@ private function specifyTypesForConstantBinaryExpression( )->setRootExpr($rootExpr)); } + if ( + !$context->null() + && ($exprNode instanceof Expr\Cast\Int_ || $exprNode instanceof Expr\Cast\Double) + ) { + $innerExpr = $exprNode->expr; + $castTypes = $this->create($exprNode, $constantType, $context, $scope)->setRootExpr($rootExpr); + + $zeroType = $exprNode instanceof Expr\Cast\Int_ + ? new ConstantIntegerType(0) + : new ConstantFloatType(0.0); + $isZeroValue = $zeroType->isSuperTypeOf($constantType)->yes(); + + if ($isZeroValue && $context->false()) { + $producesZero = TypeCombinator::union(new NullType(), new ConstantBooleanType(false)); + return $castTypes->unionWith( + $this->create($innerExpr, $producesZero, $context, $scope)->setRootExpr($rootExpr), + ); + } + + if (!$isZeroValue && $context->true()) { + return $castTypes->unionWith( + $this->create($innerExpr, new NullType(), TypeSpecifierContext::createFalse(), $scope)->setRootExpr($rootExpr), + )->unionWith( + $this->create($innerExpr, new ConstantBooleanType(false), TypeSpecifierContext::createFalse(), $scope)->setRootExpr($rootExpr), + ); + } + + return $castTypes; + } + return null; } @@ -1726,6 +1756,32 @@ private function specifyTypesForConstantStringBinaryExpression( } } + if ($exprNode instanceof Expr\Cast\String_) { + $innerExpr = $exprNode->expr; + $castTypes = $this->create($exprNode, $constantType, $context, $scope)->setRootExpr($rootExpr); + + if ($constantStringValue === '') { + $producesEmpty = TypeCombinator::union( + new NullType(), + new ConstantBooleanType(false), + new ConstantStringType(''), + ); + return $castTypes->unionWith( + $this->create($innerExpr, $producesEmpty, $context, $scope)->setRootExpr($rootExpr), + ); + } + + if ($context->true()) { + return $castTypes->unionWith( + $this->create($innerExpr, new NullType(), TypeSpecifierContext::createFalse(), $scope)->setRootExpr($rootExpr), + )->unionWith( + $this->create($innerExpr, new ConstantBooleanType(false), TypeSpecifierContext::createFalse(), $scope)->setRootExpr($rootExpr), + ); + } + + return $castTypes; + } + return null; } @@ -2858,7 +2914,20 @@ public function resolveEqual(Expr\BinaryOp\Equal $expr, Scope $scope, TypeSpecif new ConstantStringType(''), ]; } - return $this->create($exprNode, new UnionType($trueTypes), $context, $scope)->setRootExpr($expr); + $types = $this->create($exprNode, new UnionType($trueTypes), $context, $scope)->setRootExpr($expr); + + if ($exprNode instanceof Expr\Cast\String_) { + $producesEmpty = TypeCombinator::union( + new NullType(), + new ConstantBooleanType(false), + new ConstantStringType(''), + ); + $types = $types->unionWith( + $this->create($exprNode->expr, $producesEmpty, $context, $scope)->setRootExpr($expr), + ); + } + + return $types; } if ( @@ -2966,8 +3035,11 @@ public function resolveIdentical(Expr\BinaryOp\Identical $expr, Scope $scope, Ty $leftExpr = $expr->left; $rightExpr = $expr->right; - // Normalize to: fn() === expr - if ($rightExpr instanceof FuncCall && !$leftExpr instanceof FuncCall) { + // Normalize to: fn()/cast === expr + if ( + ($rightExpr instanceof FuncCall || $rightExpr instanceof Expr\Cast) + && !($leftExpr instanceof FuncCall || $leftExpr instanceof Expr\Cast) + ) { $specifiedTypes = $this->resolveNormalizedIdentical(new Expr\BinaryOp\Identical( $rightExpr, $leftExpr, diff --git a/tests/PHPStan/Analyser/nsrt/bug-8231-analogous.php b/tests/PHPStan/Analyser/nsrt/bug-8231-analogous.php new file mode 100644 index 0000000000..cc3ea17a41 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-8231-analogous.php @@ -0,0 +1,70 @@ += 8.0 + +declare(strict_types = 1); + +namespace Bug8231Analogous; + +use function PHPStan\Testing\assertType; + +// Analogous case: (int) cast in strict comparison +function testIntCastNotIdenticalZero(int|null $x): void { + if ((int)$x !== 0) { + assertType('int', $x); + } else { + assertType('int|null', $x); + } +} + +function testIntCastIdenticalZero(int|null $x): void { + if ((int)$x === 0) { + assertType('int|null', $x); + } else { + assertType('int', $x); + } +} + +// Analogous case: (float) cast in strict comparison +function testFloatCastNotIdenticalZero(float|null $x): void { + if ((float)$x !== 0.0) { + assertType('float', $x); + } else { + assertType('float|null', $x); + } +} + +// Analogous case: loose comparison with string cast +function testStringCastLooseNotEqual(string|null $x): void { + if ((string)$x != '') { + assertType('non-empty-string', $x); + } else { + assertType("''|null", $x); + } +} + +// (bool) cast already works via existing specifyTypesForConstantBinaryExpression +function testBoolCastIdenticalTrue(string|null $x): void { + if ((bool)$x === true) { + assertType('non-falsy-string', $x); + } +} + +// Analogous: (int) cast with non-zero constant +function testIntCastIdenticalNonZero(int|null $x): void { + if ((int)$x === 5) { + assertType('int', $x); + } +} + +// Analogous: (float) cast with non-zero constant +function testFloatCastIdenticalNonZero(float|null $x): void { + if ((float)$x === 3.14) { + assertType('float', $x); + } +} + +// Reversed ordering: 0 !== (int)$x +function testIntCastReversed(int|null $x): void { + if (0 !== (int)$x) { + assertType('int', $x); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-8231.php b/tests/PHPStan/Analyser/nsrt/bug-8231.php new file mode 100644 index 0000000000..31d03e2ade --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-8231.php @@ -0,0 +1,67 @@ += 8.0 + +declare(strict_types = 1); + +namespace Bug8231; + +use function PHPStan\Testing\assertType; + +function foo(string $x): void {} + +function testStringCastNotIdenticalEmpty(string|null $x): void { + if ((string)$x !== '') { + assertType('non-empty-string', $x); + foo($x); + } else { + assertType("''|null", $x); + } + assertType('string|null', $x); +} + +function testStringCastIdenticalEmpty(string|null $x): void { + if ((string)$x === '') { + assertType("''|null", $x); + } else { + assertType('non-empty-string', $x); + } +} + +function testStringCastNotIdenticalEmptyReversed(string|null $x): void { + if ('' !== (string)$x) { + assertType('non-empty-string', $x); + } else { + assertType("''|null", $x); + } +} + +function testStringCastIntNull(int|null $y): void { + if ((string)$y !== '') { + assertType('int', $y); + } else { + assertType('null', $y); + } +} + +/** @param string|false $z */ +function testStringCastStringFalse(string|false $z): void { + if ((string)$z !== '') { + assertType('non-empty-string', $z); + } else { + assertType("''|false", $z); + } +} + +/** @param bool|null $b */ +function testStringCastBoolNull(bool|null $b): void { + if ((string)$b !== '') { + assertType('true', $b); + } else { + assertType('false|null', $b); + } +} + +function testStringCastIdenticalNonEmpty(string|null $x): void { + if ((string)$x === 'hello') { + assertType('string', $x); + } +}