Skip to content

Restrict list type preservation in IntersectionType::setOffsetValueType to offsets within list key range#5455

Open
phpstan-bot wants to merge 1 commit intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-mfl2dg7
Open

Restrict list type preservation in IntersectionType::setOffsetValueType to offsets within list key range#5455
phpstan-bot wants to merge 1 commit intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-mfl2dg7

Conversation

@phpstan-bot
Copy link
Copy Markdown
Collaborator

Summary

When assigning to a list<T> with an arbitrary int key (e.g., $list[$intKey] = value where $intKey has type int), the list type was incorrectly preserved. The list should degrade to array<int, T> because an arbitrary int offset can include negative values or values that create gaps in the sequential key structure.

Changes

  • src/Type/IntersectionType.php: Changed the condition in setOffsetValueType that re-adds AccessoryArrayListType after an offset assignment. Previously it checked $offsetType->toArrayKey()->isInteger()->yes() (any integer offset), now it checks $this->getIterableKeyType()->isSuperTypeOf($offsetType)->yes() (offset must be within the list's key type range, i.e., int<0, max>).
  • tests/PHPStan/Analyser/nsrt/bug-10089.php: Updated assertion that was relying on the buggy behavior. $matrix[$size - 1][8] = 3 where $size is int<min, 8> (could be negative) correctly degrades the list since int<min, 7> is not a subtype of int<0, max>.
  • tests/PHPStan/Analyser/nsrt/bug-14336.php: New regression test covering:
    • Arbitrary int key in loop → list degraded to array
    • Arbitrary int key outside loop → list degraded
    • Negative int key → list degraded
    • int<0, max> key with array value → list preserved (valid key range for nested modifications)
    • int<0, max> key with scalar value → list degraded (lines 987-994 only apply for array values)
    • Append ([]) → list preserved
    • Constant int 0 → list preserved
    • Nested assignment with foreach key → outer list preserved

Root cause

IntersectionType::setOffsetValueType had a guard condition that re-added AccessoryArrayListType when the original type was a list, the offset was any integer, and the iterable value type was an array. This was originally added to preserve list types during nested dim fetch operations (e.g., $list[$k]['key'] = value), but the condition $offsetType->toArrayKey()->isInteger()->yes() was too broad — it also fired for arbitrary int offsets like those from foreach ($intMap as $intKey => ...) where $intKey has type int (including negatives).

The fix tightens this to $this->getIterableKeyType()->isSuperTypeOf($offsetType)->yes(), which for lists checks that the offset is within int<0, max>. This correctly rejects int (includes negatives) while still accepting int<0, max> (the type of foreach keys when iterating a list).

Analogous constructs probed

  • IntersectionType::setExistingOffsetValueType: No similar pattern; delegates directly to member types. AccessoryArrayListType::setExistingOffsetValueType returns $this which is correct for known-existing offsets.
  • IntersectionType::unsetOffset: No list re-addition logic; delegates to member types.
  • UnionType::setOffsetValueType: No AccessoryArrayListType handling.
  • ConstantArrayType::setOffsetValueType: Uses ConstantArrayTypeBuilder which properly checks for gaps via internal $isList tracking. Already correct.

Test

  • tests/PHPStan/Analyser/nsrt/bug-14336.php: Regression test for the reported bug (arbitrary int key in loop) plus 7 additional edge cases covering negative ints, int<0, max> offsets, nested assignments, and safe patterns (append, constant 0).
  • Updated tests/PHPStan/Analyser/nsrt/bug-10089.php to reflect corrected behavior.

Fixes phpstan/phpstan#14336

…ype` to offsets within list key range

- In `IntersectionType::setOffsetValueType`, change the condition for
  re-adding `AccessoryArrayListType` from checking if the offset is any
  integer (`$offsetType->toArrayKey()->isInteger()->yes()`) to checking
  if the offset is within the list's key type range
  (`$this->getIterableKeyType()->isSuperTypeOf($offsetType)->yes()`).
- This prevents list type from being incorrectly preserved when
  assigning with an arbitrary `int` key (which includes negative
  values and could create gaps), while still preserving list type for
  `int<0, max>` offsets (valid list key range) used in nested
  modifications like `$list[$k]['key'] = value`.
- Update bug-10089 test assertion: `$matrix[$size - 1][8] = 3` where
  `$size` is `int<min, 8>` correctly degrades the list since
  `int<min, 7>` is not within the `int<0, max>` key range.
@staabm
Copy link
Copy Markdown
Contributor

staabm commented Apr 14, 2026

the suggested change does not fix the reproducer of phpstan/phpstan#14336

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.

2 participants