Repository navigation
Stop tracking array auto-indices past PHP_INT_MAX and past unpacked items - #6464
Merged
ondrejmirtes merged 1 commit intoSep 17, 2026
Merged
Conversation
…d past `PHP_INT_MAX`
* `AssignHandler::processArrayByRefItems()` no longer gives up on implicit indices at every
unpacked item. `advanceImplicitIndexByUnpackedArray()` advances the index by the number of
renumbered integer keys when the unpacked array is a single sealed constant array without
optional keys (string keys are preserved by unpacking and don't consume an index), and makes
it unpredictable otherwise. `&$x` in `$a = [...[10, 20], &$x]` is now linked to `$a[2]`, so
`$x = 7` gives `array{10, 20, 7}` instead of
`array{7|10, 7|20, int, ...<int<min, -1>|int<3, max>, 7>}`.
* Unpacked items are no longer walked as if they were a keyless item holding a nested array
literal. `[...[&$x], &$y]` used to link `$x` to `$a[int][0]`, and writing to `$x` or `$y`
turned the array's values into `*ERROR*`.
* `ArrayType::setOffsetValueType()` no longer unions a float key into the key type of
`array<9223372036854775807, string>` when appending, because there is no offset past
`PHP_INT_MAX`.
Closes phpstan/phpstan#15247
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ondrejmirtes
force-pushed
the
create-pull-request/patch-vgo0lew
branch
from
September 17, 2026 15:37
b89e827 to
2380433
Compare
ondrejmirtes
approved these changes
Sep 17, 2026
This was referenced Sep 17, 2026
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
Assigning an array literal that mixes the explicit key
9223372036854775807(PHP_INT_MAX) with a later keyless or unpacked item crashed the analysis of the whole file withInt_::__construct(): Argument #1 ($value) must be of type int, float given.Everything that walks an array literal to work out which key each item gets was computing the next auto index as
$index + 1, which silently overflows to a float atPHP_INT_MAX. That float then reached constructors typedint. The same walk also treated an unpacked item (...$rest) as a single keyless item, which shifts every following index by the wrong amount.This PR makes "there is no next auto index" and "the next auto index is unknown" first-class states in all five places that track array auto indices.
Changes
src/Analyser/ExprHandler/AssignHandler.phpprocessArrayByRefItems()gets anextImplicitIndex()helper that returnsnullforPHP_INT_MAX; anullimplicit index already made following items use a non-constantintdim, so the crash becomes correct imprecision.advanceImplicitIndexByUnpackedArray()advances the index by the number of integer keys the unpacked array contributes when that array is a sealed constant array with no optional keys (string keys are preserved by unpacking, so they don't consume an index), and returnsnullotherwise. This also fixes By-reference item after a spread in an array literal ([...$list, &$x]) is linked to the wrong offset phpstan#15247, where&$xin[...$list, &$x]was linked to$a[1]regardless of the spread's real length.src/Rules/Arrays/DuplicateKeysInLiteralArraysRule.phpPHP_INT_MAXhas been used —[9223372036854775807 => 1, 2]crashed here with the analogousConstantIntegerType::__construct()TypeError.[...$arr, 0 => 'x']was reported asArray has 2 duplicate keys with value 0 (0, 0). Genuine duplicates around a spread (['a' => 1, ...$rest, 'a' => 2]) are still reported.src/Type/Constant/OversizedArrayBuilder.php$nextAutoIndexis now nullable; a keyless item pastPHP_INT_MAXgets a plainIntegerType()key and marks the array as not a list. This crashed for array literals with more thanConstantArrayTypeBuilder::ARRAY_COUNT_LIMITitems.src/Type/Php/ArrayFillFunctionReturnTypeExtension.phparray_fill(9223372036854775807, 2, 'a')returnsnon-empty-array<int, 'a'>instead of crashing.array_fill(9223372036854775806, 2, 'a')keeps its precisearray{9223372036854775806: 'a', 9223372036854775807: 'a'}type.src/Type/ArrayType.phpsetOffsetValueType()with anulloffset no longer unions$key + 1into the key type when that overflows.array<9223372036854775806|9223372036854775807, string>followed by$a[] = 'x'used to infer the impossible key type…|9.223372036854776E+18.Probed and found already correct, so left untouched:
ConstantArrayTypeBuilder— all four auto-index computations already guard withis_float().TypeCombinator— its overflowed index is only compared for list-ness and never reaches aConstantIntegerTypeconstructor.array_merge,array_replace,array_pad,array_reverse,array_flip,array_combine,array_slice,array_chunk,array_unique,array_fill_keys,array_column,range,+,array_push,array_unshiftand list destructuring withPHP_INT_MAXkeys.Root cause
PHP generates an array key by incrementing the highest integer key used so far, and
PHP_INT_MAXis therefore the last key auto-generation can ever produce — appending to such an array is anErrorat runtime. PHPStan modelled the next key as plainintarithmetic, so atPHP_INT_MAXthe tracked index became a float and was handed to constructors declaredint(PhpParser\Node\Scalar\Int_,PHPStan\Type\Constant\ConstantIntegerType). PHPStan's own type system infersint + intasint, which is why none of these sites were flagged by self-analysis.The fix names the missing state instead of the overflow: every auto-index tracker now has an explicit "no next index" value and falls back to a non-constant
intkey.The second, adjacent pattern is that array unpacking was not represented in these walks at all. An unpacked item has
key === null, so it fell into the keyless branch and consumed exactly one index. The two sites that tracked indices for semantics (AssignHandlerfor by-reference linking,DuplicateKeysInLiteralArraysRulefor duplicate detection) now account for the unknown length of a spread.Test
tests/PHPStan/Analyser/nsrt/bug-15248.php— version-independent cases: keyless and by-reference items after aPHP_INT_MAXkey, by-reference items after a spread oflist<int>and of a constant array, appending toarray<9223372036854775806|9223372036854775807, string>, and thearray_fill()overflow. Without the fix the whole file crashes inAssignHandler.tests/PHPStan/Analyser/nsrt/bug-15248-string-key-unpacking.php(// lint >= 8.1) — the reporter's playground snippet verbatim plus the string-keyed unpacking variants.tests/PHPStan/Rules/Arrays/data/bug-15248.php+DuplicateKeysInLiteralArraysRuleTest::testBug15248()— the rule's ownPHP_INT_MAXcrash, the removed[...$arr, 0 => 'x']false positive, and the still-reported duplicate'a'across a spread.OversizedArrayBuilderTest::dataBuild()— three new cases ([9223372036854775807 => 1, 2],[9223372036854775806 => 1, 2, 3],[9223372036854775807 => 1, ...[2, 3]]) which all crash without the fix.Each of the five source changes was confirmed to be individually necessary by stashing it and watching the corresponding test fail.
Fixes phpstan/phpstan#15248
🤖 Generated with Claude Code