Include the PHP_INT_MIN float overflow in abs() and unary minus of unbounded integer types - #6207
Include the PHP_INT_MIN float overflow in abs() and unary minus of unbounded integer types#6207phpstan-bot wants to merge 2 commits into
PHP_INT_MIN float overflow in abs() and unary minus of unbounded integer types#6207Conversation
…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.
|
The Static Analysis failures point at That overflow float equals var_dump(9.223372036854776E+18 > PHP_INT_MAX); // false
|
staabm
left a comment
There was a problem hiding this comment.
adjust per feedback in #6207 (comment)
`(float) PHP_INT_MAX` rounds up to `2 ** 63`, so `$value > PHP_INT_MAX` is false for it and `createAllSmallerThan()` and `createAllGreaterThanOrEqualTo()` fell through to `(int) ceil($value)`, which is out of range and wrapped around to `PHP_INT_MIN`. Both returned the exact opposite of the correct answer: IntegerRangeType::createAllSmallerThan(9.223372036854776E+18); // *NEVER* instead of int IntegerRangeType::createAllGreaterThanOrEqualTo(9.223372036854776E+18); // int instead of *NEVER* So `if ($i < 9223372036854775808)` narrowed `int` to `*NEVER*` in the truthy branch. That float is what `abs()` and unary minus of `PHP_INT_MIN` overflow into, which is how it started showing up in comparisons. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Addressed the review. One commit, pushed to What was wrongThe reviewer's diagnosis was right, and it's worse than an unreachable
if ($i < 9223372036854775808) {
\PHPStan\dumpType($i); // *NEVER* — every int is smaller than 2 ** 63
} else {
\PHPStan\dumpType($i); // int
}That float is precisely what Change
Test
Verified it fails without the Verification
One adjacent thing I found but did not change: |
Summary
abs(PHP_INT_MIN)cannot be represented as an int – it overflows to the float9.223372036854776E+18. PHPStan already knew this for constant operands (#6028), butabs()of a non-constantintwas still inferred asint<0, max>. That made WordPress core'sabsint()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 holdPHP_INT_MIN, so the missing return-type error is reported.Changes
src/Type/IntegerType.php–toAbsoluteNumber()returnsint<0, max>|9.223372036854776E+18.src/Type/IntegerRangeType.php–toAbsoluteNumber()unions the overflow float when the range can holdPHP_INT_MIN(unbounded lower bound, ormin === PHP_INT_MIN), and returns the float on its own for the range that holds nothing butPHP_INT_MIN. The remaining integers can only reachPHP_INT_MAX, so the integer part of the result is unchanged.src/Reflection/InitializerExprTypeResolver.php–getUnaryMinusType()applies the same overflow to-$xfor non-constant integer operands (the analogous case; constants were already handled). The now-unnecessary/** @var int|float */ingetUnaryMinusTypeFromType()was removed along with itsphpstan-baseline.neonentry.src/Type/Constant/ConstantIntegerType.php–toAbsoluteNumber()now branches onis_int(abs($this->value))instead of comparing againstPHP_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$expectedNewlinesCountasint<0, max>, which is what the data provider supplies;-($count + 1)is otherwise correctly reported as possibly overflowing.Analogous cases probed:
-$xonint,int<min, N>,negative-int,?intand unions containing them.abs()/negation ofint<0, max>,positive-int,int<-5, 5>and every other range excludingPHP_INT_MIN; unary plus;FloatType,ConstantFloatType,StringType, array and object types (theirtoAbsoluteNumber()cannot produce the overflow);UnionType/IntersectionType, which just delegate; first-class-callable /array_map('abs', …)paths, which go through the sameType::toAbsoluteNumber().$x * -1,$x + $y,$x - $yon unbounded ints. PHPStan does not model integer overflow for binary arithmetic at all, and doing so would turn every int arithmetic expression intofloat|int. Unary minus andabs()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_MINis the one input whose result does not fit in an int. The previous fix handled it only where the value was known exactly (ConstantIntegerType), andIntegerRangeType::toAbsoluteNumber()explicitly approximated the overflow away by treatingabs(PHP_INT_MIN)as "unbounded int" (see the comment removed in this PR). Everything reachable through an unbounded orPHP_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()andInitializerExprTypeResolver::getUnaryMinusType()– and keeps them in agreement about the boundary: the overflow is included exactly whenPHP_INT_MINis a possible value, and excluded fromPHP_INT_MIN + 1upwards.Test
tests/PHPStan/Analyser/data/abs-64bit.php– the reported reproducerabs((int) $maybeint), plus the boundary:int<-9223372036854775808, …>includes the float whileint<-9223372036854775807, …>does not, andint<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>, thePHP_INT_MIN + 1boundary, plusint<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); }andfunction negate(int $i): int { return -$i; }are now reported, while the guarded and bounded-range variants are not.nsrt/abs.php,nsrt/binary.php,nsrt/integer-range-types.php,nsrt/bug-9224b.phpandFiber/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