diff --git a/src/Reflection/InitializerExprTypeResolver.php b/src/Reflection/InitializerExprTypeResolver.php index 9e68af55371..8b54b83b213 100644 --- a/src/Reflection/InitializerExprTypeResolver.php +++ b/src/Reflection/InitializerExprTypeResolver.php @@ -2623,7 +2623,9 @@ public function getUnaryMinusTypeFromType(Expr $expr, Type $type): Type /** @var int|float $newValue */ $newValue = -$scalarValue; if (!is_int($newValue)) { - return $type; + // Negating the smallest integer overflows into a float. + $newTypes[] = new ConstantFloatType($newValue); + continue; } $newTypes[] = new ConstantIntegerType($newValue); } elseif (is_float($scalarValue)) { diff --git a/src/Type/Constant/ConstantIntegerType.php b/src/Type/Constant/ConstantIntegerType.php index 3dfb9bff2eb..36bd3d8ada2 100644 --- a/src/Type/Constant/ConstantIntegerType.php +++ b/src/Type/Constant/ConstantIntegerType.php @@ -18,6 +18,7 @@ use PHPStan\Type\VerbosityLevel; use function abs; use function sprintf; +use const PHP_INT_MIN; /** @api */ class ConstantIntegerType extends IntegerType implements ConstantScalarType @@ -85,6 +86,11 @@ public function toBitwiseNotType(): Type public function toAbsoluteNumber(): Type { + if ($this->value === PHP_INT_MIN) { + // The absolute value of the smallest integer is not representable as an int. + return new ConstantFloatType(-(float) $this->value); + } + return new self(abs($this->value)); } diff --git a/src/Type/IntegerRangeType.php b/src/Type/IntegerRangeType.php index a931894c084..fbbd42092e5 100644 --- a/src/Type/IntegerRangeType.php +++ b/src/Type/IntegerRangeType.php @@ -13,6 +13,7 @@ use PHPStan\Type\Accessory\AccessoryDecimalIntegerStringType; use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; use PHPStan\Type\Constant\ConstantBooleanType; +use PHPStan\Type\Constant\ConstantFloatType; use PHPStan\Type\Constant\ConstantIntegerType; use function array_filter; use function array_map; @@ -485,13 +486,22 @@ public function toAbsoluteNumber(): Type return $this; } - if ($this->max === null || $this->max >= 0) { - $inversedMin = $this->min !== null ? $this->min * -1 : null; + if ($this->max === PHP_INT_MIN) { + // Nothing is smaller than the smallest integer, so this range holds a single value + // whose absolute value is not representable as an int. + return new ConstantFloatType(-(float) PHP_INT_MIN); + } + // Negating the smallest integer overflows, so its absolute value is treated as unbounded, + // the same way an unbounded lower bound is. This keeps abs(int) and + // abs(int<-9223372036854775808, 0>) in agreement. + $inversedMin = $this->min !== null && $this->min !== PHP_INT_MIN ? -$this->min : null; + + if ($this->max === null || $this->max >= 0) { return self::fromInterval(0, $inversedMin !== null && $this->max !== null ? max($inversedMin, $this->max) : null); } - return self::fromInterval($this->max * -1, $this->min !== null ? $this->min * -1 : null); + return self::fromInterval(-$this->max, $inversedMin); } public function toString(): Type diff --git a/tests/PHPStan/Analyser/NodeScopeResolverTest.php b/tests/PHPStan/Analyser/NodeScopeResolverTest.php index 348f9315934..ed2e6f788ac 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -112,6 +112,8 @@ private static function findTestFiles(): iterable if (PHP_INT_SIZE === 8) { yield __DIR__ . '/data/predefined-constants-64bit.php'; + yield __DIR__ . '/data/abs-64bit.php'; + yield __DIR__ . '/data/unary-minus-64bit.php'; } else { yield __DIR__ . '/data/predefined-constants-32bit.php'; } diff --git a/tests/PHPStan/Analyser/data/abs-64bit.php b/tests/PHPStan/Analyser/data/abs-64bit.php new file mode 100644 index 00000000000..814e1c40143 --- /dev/null +++ b/tests/PHPStan/Analyser/data/abs-64bit.php @@ -0,0 +1,29 @@ + $int */ + assertType('int<0, max>', abs($int)); + + /** @var int<-9223372036854775808, -1> $int */ + assertType('int<1, max>', abs($int)); + + /** @var int<-9223372036854775808, 9223372036854775807> $int */ + assertType('int<0, max>', abs($int)); + + // The only value in this range is the smallest integer. + /** @var int $int */ + assertType('9.223372036854776E+18', abs($int)); +} diff --git a/tests/PHPStan/Analyser/data/unary-minus-64bit.php b/tests/PHPStan/Analyser/data/unary-minus-64bit.php new file mode 100644 index 00000000000..23105acd860 --- /dev/null +++ b/tests/PHPStan/Analyser/data/unary-minus-64bit.php @@ -0,0 +1,44 @@ + $int */ + assertType('int<1, max>', -$int); + + /** @var int<-9223372036854775808, -1> $int */ + assertType('int<1, max>', -$int); +} + +function constantUnion(int $int): void +{ + /** @var -1|-2 $int */ + assertType('1|2', -$int); + + /** @var 9223372036854775807|25 $int */ + assertType('-9223372036854775807|-25', -$int); +} + +// The union has one member that overflows and one that does not. +function partiallyOverflowingUnion(bool $bool): void +{ + $int = $bool ? -9223372036854775807 - 1 : 25; + assertType('-9223372036854775808|25', $int); + assertType('-25|9.223372036854776E+18', -$int); +}