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
Open
Conversation
…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.
Contributor
|
The Static Analysis failures point at That overflow float equals var_dump(9.223372036854776E+18 > PHP_INT_MAX); // false
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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