Skip to content

Track array value type overwrites in by-reference foreach without key variable#5542

Open
phpstan-bot wants to merge 2 commits intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-vwbkpoi
Open

Track array value type overwrites in by-reference foreach without key variable#5542
phpstan-bot wants to merge 2 commits intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-vwbkpoi

Conversation

@phpstan-bot
Copy link
Copy Markdown
Collaborator

Summary

When iterating an array by reference without a key variable (foreach ($arr as &$v) { $v = X; }), PHPStan did not track that the array's element type was being overwritten. The same construct WITH a key variable (foreach ($arr as $k => &$v) { $v = X; }) already worked correctly.

This PR extends the post-loop array type rewrite mechanism in NodeScopeResolver to also handle the by-reference-without-key case.

Changes

  • src/Analyser/NodeScopeResolver.php: Extended the foreach post-loop array type rewrite (around line 1293-1475) to handle $stmt->byRef && $stmt->keyVar === null:

    • Added $byRefWithoutKey flag detection
    • Extended initial scope collection to use OriginalForeachValueExpr (value was overwritten) instead of OriginalForeachKeyExpr (key was not modified)
    • Extended continue exit point handling with the same OriginalForeachValueExpr logic
    • Added a branch inside the rewrite block that reads the value variable's type directly (instead of reading from ArrayDimFetch($expr, $keyVar))
    • Relaxed the outer condition from $stmt->keyVar !== null to $stmt->keyVar !== null || $byRefWithoutKey
  • Updated test assertions in existing files that were asserting the old imprecise behavior:

    • tests/PHPStan/Analyser/nsrt/overwritten-arrays.phpdoFoo7: array<int, 1|string>array<int, 1>
    • tests/PHPStan/Analyser/nsrt/bug-13809.phpfoo(): list<mixed>list<'foo'>, bar3(): list<mixed>list<'foo'|'maybe'>, baz(): list<string>list<'bar'>
    • tests/PHPStan/Analyser/nsrt/bug-14083.phpexample(): list<string>list<uppercase-string>
    • tests/PHPStan/Analyser/nsrt/bug-14084.phpexample() and example2(): list<string>list<uppercase-string>
    • tests/PHPStan/Rules/Variables/data/bug-14124.phpexample3a(): list<string>list<uppercase-string>
    • tests/PHPStan/Rules/Variables/data/bug-14124b.phpexample3a(): list<list<string>>list<list<uppercase-string>>

Root cause

The post-loop array type rewrite in NodeScopeResolver was gated on $stmt->keyVar !== null. This gate exists because the mechanism relies on:

  1. Checking OriginalForeachKeyExpr to verify the key wasn't modified
  2. Reading the type of $arr[$key] from the final scope to determine the new element type

Without a key variable, neither of these steps is possible. The fix adds a parallel mechanism for the by-reference case: it uses OriginalForeachValueExpr (already tracked for other purposes) to verify the value was overwritten, and reads the value variable's type directly instead of through an ArrayDimFetch.

Test

  • tests/PHPStan/Analyser/nsrt/bug-1940.php: Comprehensive regression test covering:
    • Basic by-ref overwrite without key
    • Conditional overwrite (only some iterations)
    • Always overwritten via if/else branches
    • List types, string keys, non-empty arrays
    • Break (should NOT rewrite — not all elements visited)
    • Continue with full overwrite, continue with branch overwrites, continue with partial overwrite
    • Property as iteratee ($this->prop)
    • Function-based transformation (intval)
    • No overwrite at all (read-only loop)

Fixes phpstan/phpstan#1940

…ey variable

- Extend post-loop array type rewrite in NodeScopeResolver to handle
  `foreach ($arr as &$v)` (without `$key =>`) by tracking the value
  variable's type via `OriginalForeachValueExpr` instead of relying on
  `OriginalForeachKeyExpr` + `ArrayDimFetch`
- When the value is always overwritten (OriginalForeachValueExpr absent
  in all paths), replace the array's value type with the final value
  variable type — matching the existing behavior for the key-based case
- Handle continue exit points for the by-ref without key case with the
  same logic: check OriginalForeachValueExpr presence in each continue scope
- Update assertions in existing tests (bug-13809, bug-14083, bug-14084,
  bug-14124, bug-14124b, overwritten-arrays) that were asserting the
  old imprecise behavior
Copy link
Copy Markdown
Contributor

@VincentLanglet VincentLanglet left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand how it's related to the fact it's "by ref without key" since the same issue exists when a key is used

foreach ($a as $k => &$testArray) {
	$testArray['two'] = 'two';
}

https://phpstan.org/r/b0dafda8-0a2f-4389-95c9-64a00039b0ff

…works

The reviewer pointed out that modifying array sub-elements through a
by-reference foreach with a key variable also had issues. Testing confirms
this case already works correctly on the current branch — both with and
without a key variable. The reviewer's example was likely tested against
the stable phpstan.org release which runs an older version.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@phpstan-bot
Copy link
Copy Markdown
Collaborator Author

I processed this review but have nothing to report.

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