Skip to content

Include the PHP_INT_MIN float overflow in abs() and unary minus of unbounded integer types - #6207

Open
phpstan-bot wants to merge 1 commit into
phpstan:2.2.xfrom
phpstan-bot:create-pull-request/patch-du7o7vj
Open

Include the PHP_INT_MIN float overflow in abs() and unary minus of unbounded integer types#6207
phpstan-bot wants to merge 1 commit into
phpstan:2.2.xfrom
phpstan-bot:create-pull-request/patch-du7o7vj

Conversation

@phpstan-bot

Copy link
Copy Markdown
Collaborator

Summary

abs(PHP_INT_MIN) cannot be represented as an int – it overflows to the float 9.223372036854776E+18. PHPStan already knew this for constant operands (#6028), but abs() of a non-constant int was still inferred as int<0, max>. That made WordPress core's absint()

function absint( $maybeint ): int {
	return abs( (int) $maybeint );
}

look correct even though it fatals on PHP_INT_MIN (Trac 65826).

abs() and unary minus now include the overflow float whenever the operand's type can hold PHP_INT_MIN, so the missing return-type error is reported.

Changes

  • src/Type/IntegerType.phptoAbsoluteNumber() returns int<0, max>|9.223372036854776E+18.
  • src/Type/IntegerRangeType.phptoAbsoluteNumber() unions the overflow float when the range can hold PHP_INT_MIN (unbounded lower bound, or min === PHP_INT_MIN), and returns the float on its own for the range that holds nothing but PHP_INT_MIN. The remaining integers can only reach PHP_INT_MAX, so the integer part of the result is unchanged.
  • src/Reflection/InitializerExprTypeResolver.phpgetUnaryMinusType() applies the same overflow to -$x for non-constant integer operands (the analogous case; constants were already handled). The now-unnecessary /** @var int|float */ in getUnaryMinusTypeFromType() was removed along with its phpstan-baseline.neon entry.
  • src/Type/Constant/ConstantIntegerType.phptoAbsoluteNumber() now branches on is_int(abs($this->value)) instead of comparing against PHP_INT_MIN. The old comment said that form was dead code to PHPStan itself; it no longer is, which doubles as a self-check of this fix.
  • tests/PHPStan/Command/ErrorFormatter/BaselineNeonErrorFormatterTest.php – documented $expectedNewlinesCount as int<0, max>, which is what the data provider supplies; -($count + 1) is otherwise correctly reported as possibly overflowing.

Analogous cases probed:

  • Fixed: unary minus -$x on int, int<min, N>, negative-int, ?int and unions containing them.
  • Already correct, left alone: abs()/negation of int<0, max>, positive-int, int<-5, 5> and every other range excluding PHP_INT_MIN; unary plus; FloatType, ConstantFloatType, StringType, array and object types (their toAbsoluteNumber() cannot produce the overflow); UnionType/IntersectionType, which just delegate; first-class-callable / array_map('abs', …) paths, which go through the same Type::toAbsoluteNumber().
  • Deliberately not changed: $x * -1, $x + $y, $x - $y on unbounded ints. PHPStan does not model integer overflow for binary arithmetic at all, and doing so would turn every int arithmetic expression into float|int. Unary minus and abs() are different: a single input value overflows and the resulting type stays precise.

Root cause

Type::toAbsoluteNumber() and unary minus modelled negation as a total function on integers. It is not: -PHP_INT_MIN is the one input whose result does not fit in an int. The previous fix handled it only where the value was known exactly (ConstantIntegerType), and IntegerRangeType::toAbsoluteNumber() explicitly approximated the overflow away by treating abs(PHP_INT_MIN) as "unbounded int" (see the comment removed in this PR). Everything reachable through an unbounded or PHP_INT_MIN-anchored integer type therefore lost the float.

The fix pushes the overflow into the three places that can produce it – IntegerType::toAbsoluteNumber(), IntegerRangeType::toAbsoluteNumber() and InitializerExprTypeResolver::getUnaryMinusType() – and keeps them in agreement about the boundary: the overflow is included exactly when PHP_INT_MIN is a possible value, and excluded from PHP_INT_MIN + 1 upwards.

Test

  • tests/PHPStan/Analyser/data/abs-64bit.php – the reported reproducer abs((int) $maybeint), plus the boundary: int<-9223372036854775808, …> includes the float while int<-9223372036854775807, …> does not, and int<min, -9223372036854775808> (which collapses to a single value) is the float alone.
  • tests/PHPStan/Analyser/data/unary-minus-64bit.php – the analogous unary-minus cases: -$int, -((int) $s), int<min, 5>, the PHP_INT_MIN + 1 boundary, plus int<0, max>, int<-5, 5> and unary plus as negative controls.
  • tests/PHPStan/Rules/Functions/data/bug-15069.php + ReturnTypeRuleTest::testBug15069() – the end-to-end consequence: function absint($maybeint): int { return abs((int) $maybeint); } and function negate(int $i): int { return -$i; } are now reported, while the guarded and bounded-range variants are not.
  • Updated existing expectations in nsrt/abs.php, nsrt/binary.php, nsrt/integer-range-types.php, nsrt/bug-9224b.php and Fiber/data/fnsr.php, which were asserting the old approximated types.

Both new tests were verified to fail without the src/ changes.

Fixes phpstan/phpstan#15069

…f unbounded integer types

* `IntegerType::toAbsoluteNumber()` now returns `int<0, max>|9.223372036854776E+18` instead of just `int<0, max>`, because `abs(PHP_INT_MIN)` is not representable as an int.
* `IntegerRangeType::toAbsoluteNumber()` unions the same constant float whenever the range can hold `PHP_INT_MIN` (unbounded lower bound or `min === PHP_INT_MIN`), and returns the float alone for the range that holds nothing but `PHP_INT_MIN`. Ranges starting at `PHP_INT_MIN + 1` or higher stay pure integer ranges.
* `InitializerExprTypeResolver::getUnaryMinusType()` applies the same overflow to `-$x` for non-constant integer types; constants were already handled. `int<0, max>`, `int<-5, 5>` and other ranges that exclude `PHP_INT_MIN` are unaffected, and unary plus is untouched.
* `ConstantIntegerType::toAbsoluteNumber()` now checks `is_int(abs($this->value))` directly – that used to be dead code to PHPStan itself and is exactly what this change fixes.
* Dropped the now-superfluous `@var int|float` in `getUnaryMinusTypeFromType()` plus its baseline entry, since PHPStan now infers `-$scalarValue` as `float|int` natively.
* Probed adjacent code paths: `UnionType`/`IntersectionType` delegate correctly, `FloatType`/`ConstantFloatType`/`StringType`/array types cannot overflow, and `int`-typed binary operators (`+`, `*`, `-`) intentionally keep ignoring overflow, so they were left alone.
@zonuexe

zonuexe commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

The Static Analysis failures point at IntegerRangeType.php:102 and 168: the (int) ceil($value) casts in createAllSmallerThan() and createAllGreaterThanOrEqualTo() now receive 9.223372036854776E+18.

That overflow float equals (float) PHP_INT_MAX, so it slips past the bound guards:

var_dump(9.223372036854776E+18 > PHP_INT_MAX); // false

$value > PHP_INT_MAX no longer returns early for it, and it reaches the (int) cast. The guards would need to treat a float at (float) PHP_INT_MAX as out of range.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Incorrect type inferred from abs( (int) $value ) as int<0, max> when should include float for PHP_MIN_INT

2 participants