| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
…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 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. |
Sorry, something went wrong.
There was a problem hiding this comment.
adjust per feedback in #6207 (comment)
Sorry, something went wrong.
`(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 create-pull-request/patch-du7o7vj (39ba96882). What was wrongThe reviewer's diagnosis was right, and it's worse than an unreachable (int) cast — the two guards return the exact opposite of the correct answer. Confirmed against the built types: before after createAllSmallerThan(9.223372036854776E+18) = *NEVER* → int createAllGreaterThanOrEqualTo(9.223372036854776E+18) = int → *NEVER* (float) PHP_INT_MAX rounds up to 2 ** 63, so $value > PHP_INT_MAX is false for a float that equals it, the early return is skipped, and (int) ceil($value) wraps around to PHP_INT_MIN. User-visible before the fix (bin/phpstan analyse -l 8, 9223372036854775808 is a float literal since it doesn't fit an int): if ($i < 9223372036854775808) {
\PHPStan\dumpType($i); // *NEVER* — every int is smaller than 2 ** 63
} else {
\PHPStan\dumpType($i); // int
}That float is precisely what abs()/unary minus of PHP_INT_MIN overflow into, which is how this PR started routing values into it. Changesrc/Type/IntegerRangeType.php — createAllSmallerThan() and createAllGreaterThanOrEqualTo() now guard with $value >= (float) PHP_INT_MAX, with a comment on why the boundary is >= and not >. The largest float below 2 ** 63 is 2 ** 63 - 1024, which is <= PHP_INT_MAX, so everything reaching (int) ceil($value) is now in range. createAllSmallerThanOrEqualTo() and createAllGreaterThan() already used >= PHP_INT_MAX / >= PHP_INT_MAX, which handles the boundary correctly; the PHP_INT_MIN guards are exact because (float) PHP_INT_MIN is -2 ** 63 exactly. Both verified, left alone. Testtests/PHPStan/Analyser/data/integer-range-float-bounds-64bit.php (registered under the PHP_INT_SIZE === 8 branch of NodeScopeResolverTest): all four comparison operators against the out-of-range float, the -9223372036854775808 counterpart as a negative control, and 9223372036854774784.0 — the largest float still inside the integer range — to pin the boundary from below. Verified it fails without the src/ change (4 inverted assertions on lines 15/17/33/35) and passes with it. Verification
One adjacent thing I found but did not change: NAN slips past all four guards too ((int) ceil(NAN) is 0 here), so if ($i < NAN) narrows to int<min, -1> instead of *NEVER*. It's pre-existing, unrelated to the overflow float, and only affects code that is dead at runtime — happy to fix it in a separate PR if you want it. |
Sorry, something went wrong.
|
Two follow-ups. The overflow float enters from two places: IntegerType/IntegerRangeType::toAbsoluteNumber() (abs) and getUnaryMinusType() (unary minus), both building ConstantFloatType(9.223372036854776E+18). Patching the two (int) casts in IntegerRangeType fixes the symptom for one path only; a bound guard that treats a float at (float) PHP_INT_MAX as out of range covers both. On CI, the red that traces to this change is Static Analysis, plus Compile PHAR and E2E (both run PHPStan on itself, same cause). "Tests with old PHPUnit (8.1)" fails on Interface "PHPUnit\...Subscriber" not found, a toolchain mismatch unrelated to the diff, and Mutation Testing is a score threshold. Not caused by this PR: if ($i > PHP_INT_MAX) narrowing plain int to int<2147483648, max> reproduces on 2.2.x. That's the 32/64-bit PHP_INT_MAX union (2147483647|9223372036854775807), phpstan/phpstan#14948, independent of the overflow float. |
Sorry, something went wrong.
| Back | FazBrowse Home | New Git URL |
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()
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
Analogous cases probed:
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
Both new tests were verified to fail without the src/ changes.
Fixes phpstan/phpstan#15069