From c5889b178cdfd384a97e00c9327d3cf5e1b1261e Mon Sep 17 00:00:00 2001 From: USAMI Kenta Date: Fri, 10 Jul 2026 14:03:07 +0900 Subject: [PATCH 1/4] Handle abs() of the smallest integer overflowing to float --- src/Type/Constant/ConstantIntegerType.php | 6 +++++ src/Type/IntegerRangeType.php | 16 +++++++++--- .../Analyser/NodeScopeResolverTest.php | 1 + tests/PHPStan/Analyser/data/abs-64bit.php | 25 +++++++++++++++++++ 4 files changed, 45 insertions(+), 3 deletions(-) create mode 100644 tests/PHPStan/Analyser/data/abs-64bit.php 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..8b20315dd07 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -112,6 +112,7 @@ private static function findTestFiles(): iterable if (PHP_INT_SIZE === 8) { yield __DIR__ . '/data/predefined-constants-64bit.php'; + yield __DIR__ . '/data/abs-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..052a2198444 --- /dev/null +++ b/tests/PHPStan/Analyser/data/abs-64bit.php @@ -0,0 +1,25 @@ + $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)); +} From 56f8a79bb68dab94652259d6a07e4b374241a2f5 Mon Sep 17 00:00:00 2001 From: USAMI Kenta Date: Fri, 10 Jul 2026 17:24:40 +0900 Subject: [PATCH 2/4] Cover abs() one step away from the overflow --- tests/PHPStan/Analyser/data/abs-64bit.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/PHPStan/Analyser/data/abs-64bit.php b/tests/PHPStan/Analyser/data/abs-64bit.php index 052a2198444..814e1c40143 100644 --- a/tests/PHPStan/Analyser/data/abs-64bit.php +++ b/tests/PHPStan/Analyser/data/abs-64bit.php @@ -8,6 +8,10 @@ assertType('9.223372036854776E+18', abs(-9223372036854775807 - 1)); assertType('2147483648|9.223372036854776E+18', abs(PHP_INT_MIN)); +// One step away from the overflow, so still an integer. +assertType('9223372036854775807', abs(-9223372036854775807)); +assertType('2147483647|9223372036854775807', abs(-PHP_INT_MAX)); + function integerRanges(int $int): void { /** @var int<-9223372036854775808, 0> $int */ From 76cd664e83dcacd7ab349cf6f785d18d958fe933 Mon Sep 17 00:00:00 2001 From: USAMI Kenta Date: Fri, 10 Jul 2026 14:13:30 +0900 Subject: [PATCH 3/4] Negating the smallest integer results in a float --- .../InitializerExprTypeResolver.php | 4 ++- .../Analyser/NodeScopeResolverTest.php | 1 + .../Analyser/data/unary-minus-64bit.php | 30 +++++++++++++++++++ 3 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 tests/PHPStan/Analyser/data/unary-minus-64bit.php 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/tests/PHPStan/Analyser/NodeScopeResolverTest.php b/tests/PHPStan/Analyser/NodeScopeResolverTest.php index 8b20315dd07..ed2e6f788ac 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -113,6 +113,7 @@ 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/unary-minus-64bit.php b/tests/PHPStan/Analyser/data/unary-minus-64bit.php new file mode 100644 index 00000000000..1b6da3ad94c --- /dev/null +++ b/tests/PHPStan/Analyser/data/unary-minus-64bit.php @@ -0,0 +1,30 @@ + $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); +} From 3eac99fcd3a0ac4f9cf069d6d873257da17b81ce Mon Sep 17 00:00:00 2001 From: USAMI Kenta Date: Fri, 10 Jul 2026 16:54:22 +0900 Subject: [PATCH 4/4] Cover -PHP_INT_MAX and a partially overflowing union --- tests/PHPStan/Analyser/data/unary-minus-64bit.php | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/PHPStan/Analyser/data/unary-minus-64bit.php b/tests/PHPStan/Analyser/data/unary-minus-64bit.php index 1b6da3ad94c..23105acd860 100644 --- a/tests/PHPStan/Analyser/data/unary-minus-64bit.php +++ b/tests/PHPStan/Analyser/data/unary-minus-64bit.php @@ -8,6 +8,9 @@ assertType('9.223372036854776E+18', -(-9223372036854775807 - 1)); assertType('2147483648|9.223372036854776E+18', -PHP_INT_MIN); +// Negating the largest integer stays an integer. +assertType('-9223372036854775807|-2147483647', -PHP_INT_MAX); + $min = -9223372036854775807 - 1; assertType('9.223372036854776E+18', -$min); @@ -27,4 +30,15 @@ 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); }