Skip to content

Track by-reference value modifications in constant array foreach unrolling#5549

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

Track by-reference value modifications in constant array foreach unrolling#5549
phpstan-bot wants to merge 1 commit intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-v8pmfdd

Conversation

@phpstan-bot
Copy link
Copy Markdown
Collaborator

Summary

When iterating over a constant array (array shape) with foreach ($arr as &$value), modifications to $value were not propagated back to the array type. After the loop, the array retained its original type as if no modifications had occurred. This caused false positives when assigning the modified array to a typed property or returning it from a typed method.

Changes

  • src/Analyser/NodeScopeResolver.php: Removed the if ($stmt->byRef) return null; bail-out in tryProcessUnrolledConstantArrayForeach(). After each unrolled iteration's body is processed, the new value of the by-reference variable is read from the result scope and written back to the array type at the specific constant key using setExistingOffsetValueType(). This gives per-element precision — e.g., [1, 2, 3] after casting each element to string becomes array{'1', '2', '3'}.

  • src/Analyser/MutatingScope.php: Removed the $iterateeType->isConstantArray()->no() guard from both IntertwinedVariableByReferenceWithExpr setups in enterForeach(). This serves as a fallback for constant arrays that exceed the unroll limit (>16 elements), where the general loop path's by-ref tracking via SetExistingOffsetValueTypeExpr now also applies to constant arrays.

Root cause

Two guards excluded constant arrays from by-reference tracking in foreach loops:

  1. tryProcessUnrolledConstantArrayForeach() returned null when $stmt->byRef was true, disabling the precise per-element unrolled processing
  2. MutatingScope::enterForeach() required $iterateeType->isConstantArray()->no() for the IntertwinedVariableByReferenceWithExpr setup

Together these meant constant arrays fell through to the general loop path without any by-ref tracking at all, so the array type was never updated.

Analogous cases probed:

  • Non-constant array foreach by-ref (array<int, array{a: string}>) — already worked correctly before this fix
  • List type preservation in foreach by-ref — already handled correctly via SetExistingOffsetValueTypeExpr
  • For-loop array modification — fundamentally different (can't guarantee all elements visited), produces int|string union which is the correct conservative approximation
  • Foreach by-ref with key variable — fixed by both changes (unrolled path + fallback)

Test

Added tests/PHPStan/Analyser/nsrt/bug-1311.php with regression tests covering:

  • Non-constant array foreach by-ref adding a key to array shapes (already passing)
  • Constant array foreach by-ref with string cast (the reported issue)
  • Constant array foreach by-ref with key variable
  • Shaped array (array{a: int, b: int}) foreach by-ref
  • List type preservation during by-ref modification
  • Conditional modification in by-ref loop (only some elements modified)
  • Compound value replacement (replacing scalars with arrays)
  • For-loop baseline (documents current conservative approximation)

Fixes phpstan/phpstan#1311

…lling

- Enable `tryProcessUnrolledConstantArrayForeach` to handle by-ref foreach
  by removing the early `$stmt->byRef` bail-out and propagating value
  variable changes back to the array after each unrolled iteration via
  `setExistingOffsetValueType`
- Remove `isConstantArray()->no()` guard in `MutatingScope::enterForeach`
  so constant arrays also get `IntertwinedVariableByReferenceWithExpr`
  tracking as a fallback for arrays exceeding the unroll limit
- Add regression tests covering: constant array by-ref cast, by-ref with
  key variable, shaped array by-ref, list type preservation, conditional
  modification, compound value replacement, and for-loop baseline
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