Skip to content

Unroll foreach over union of constant arrays in tryProcessUnrolledConstantArrayForeach#5558

Merged
ondrejmirtes merged 1 commit intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-xyzdjxa
Apr 28, 2026
Merged

Unroll foreach over union of constant arrays in tryProcessUnrolledConstantArrayForeach#5558
ondrejmirtes merged 1 commit intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-xyzdjxa

Conversation

@phpstan-bot
Copy link
Copy Markdown
Collaborator

Summary

When building an array inside a foreach loop that iterates over a union of constant arrays (e.g. list{'username', 'password'}|list{'app_id', 'app_key'}), PHPStan previously merged all possible keys from all union members into a single array with optional keys, rather than preserving per-array shapes as a union type.

This caused false positives when the built array was passed to a function expecting a union of shaped arrays.

Changes

  • Modified tryProcessUnrolledConstantArrayForeach() in src/Analyser/NodeScopeResolver.php to handle unions of multiple ConstantArrayType values
  • Removed the count($constantArrays) !== 1 guard that previously rejected unions
  • Added a total key count check across all arrays against FOREACH_UNROLL_LIMIT
  • Each constant array in the union is now unrolled independently with its own chain scope starting from $originalScope
  • Per-array body scopes and chain scopes are computed separately, then merged across arrays for the final result
  • Native type matching updated to pair constant arrays by index when counts match

Root cause

The tryProcessUnrolledConstantArrayForeach() method only supported a single constant array (count($constantArrays) !== 1 early return). When the iteratee was a union of constant arrays — which commonly occurs when the outer foreach over a constant array assigns different array values to the loop variable — the method bailed out and the standard iterative foreach processing ran instead. The iterative processing sees the union's combined iterable value type (e.g. 'username'|'password'|'app_id'|'app_key') and produces a single array with all keys as optional, losing the per-array shape information.

The fix processes each constant array in the union independently through the existing unrolling logic, preserving per-array type precision, then merges the results.

Analogous cases probed:

  • Direct parameter union of constant arrays (@param list{'a','b'}|list{'x','y'}) — also fixed by this change
  • Different-length arrays in the union — also fixed
  • Single constant array (original behavior) — unchanged, verified by existing tests

Test

Added tests/PHPStan/Analyser/nsrt/bug-7978.php with three test cases:

  1. The original reported pattern: nested foreach over class constant with shaped array values
  2. Direct union parameter: @param list{'username','password'}|list{'app_id','app_key'}
  3. Different-length arrays: @param list{'a','b','c'}|list{'x'}

All verify the inferred type is a union of the correct shaped arrays rather than a single array with all optional keys.

Fixes phpstan/phpstan#7978

…nstantArrayForeach`

- Support iterating over a union of multiple ConstantArrayType values
  (e.g. `list{'a','b'}|list{'x','y'}`) in the foreach unrolling logic
- Previously, unrolling was restricted to exactly one constant array
  (`count($constantArrays) !== 1`), causing unions to fall back to
  the imprecise iterative processing that merged all possible keys
- Now each constant array in the union is unrolled independently with
  its own chain scope, and the results are merged across arrays
- This preserves per-array type precision when building arrays inside
  foreach loops over constant array unions

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@ondrejmirtes ondrejmirtes merged commit 37ef388 into phpstan:2.1.x Apr 28, 2026
653 of 655 checks passed
@ondrejmirtes ondrejmirtes deleted the create-pull-request/patch-xyzdjxa branch April 28, 2026 15:56
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