Skip to content

Fix phpstan/phpstan#9691: Preserve per-key array types during loop generalization when values mix arrays and scalars#5473

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

Fix phpstan/phpstan#9691: Preserve per-key array types during loop generalization when values mix arrays and scalars#5473
phpstan-bot wants to merge 1 commit intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-f0r36zo

Conversation

@phpstan-bot
Copy link
Copy Markdown
Collaborator

Summary

Fixes phpstan/phpstan#9691

When an array inside a loop has different keys holding structurally different value types (e.g., key 0 holds an int and key 1 holds an array{abc: 'def'}), the loop type generalization in MutatingScope::generalizeType() would collapse all per-key type information into a general array<K, V> type. This caused false positive "Cannot access offset 'abc' on 0|array{abc: 'def'}" errors when accessing keys that are known to hold array values.

Root cause

In generalizeType(), when constant arrays A and B have different key sets (B has more keys than A), the code always fell through to the general ArrayType fallback. This merged all value types into a single union, losing per-key precision. For the reported bug:

  • A = array{1: array{abc: 'def'}} (from first loop iteration)
  • B = array{0?: 0, 1: array{abc: 'def'}} (from second iteration, conditional assignment added key 0)
  • Generalized to array<0|1, 0|array{abc: 'def'}> — accessing key 1 now gives 0|array{abc: 'def'} instead of array{abc: 'def'}

Fix

Added a new elseif branch in the constant array generalization section that activates when:

  1. B's key type is a supertype of A's key type (B has all of A's keys plus more)
  2. The key types are not equal (B strictly has more keys)
  3. B's value types are structurally mixed — containing both array-typed and non-array-typed values

When all three conditions are met, the code performs per-key value type merging using B's key set, preserving the structural type information for each key. Keys present only in one side are handled correctly (with optional markers for keys not present in both).

The structural mixing check (hasStructurallyMixedValueTypes) prevents this branch from activating for growing-list patterns (where all values are the same scalar type), which must still be generalized to list<T>.

Test plan

  • Added regression test tests/PHPStan/Analyser/nsrt/bug-9691.php that asserts $issues[1] has type array{abc: 'def'} inside the loop
  • Full test suite passes (11856 tests, 0 failures)
  • PHPStan self-analysis passes with no errors
  • Code style check passes

…neralization when values mix arrays and scalars

When an array has different keys holding structurally different value
types (e.g., one key holds an int, another holds an array), loop type
generalization would collapse all per-key type information into a
general array<K, V>, causing false "Cannot access offset" errors when
accessing keys that are known to hold array values.

Add a new branch in MutatingScope::generalizeType() that detects when
the wider array (B) has keys that are a superset of the narrower (A)
and the value types span both array and non-array types. In this case,
perform per-key value type merging to preserve structural type info.
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.

1 participant