Fix phpstan/phpstan#14333: Setting an array key doesn't update a reference#5257
Open
phpstan-bot wants to merge 15 commits intophpstan:2.1.xfrom
Open
Fix phpstan/phpstan#14333: Setting an array key doesn't update a reference#5257phpstan-bot wants to merge 15 commits intophpstan:2.1.xfrom
phpstan-bot wants to merge 15 commits intophpstan:2.1.xfrom
Conversation
…rence - When creating an array with by-reference items (e.g. $b = ['key' => &$a]), register IntertwinedVariableByReferenceWithExpr entries so that subsequent assignments to array offsets propagate type changes to the referenced variables - Preserve non-variable-to-variable intertwined refs in assignVariable() so they survive the recursive propagation chain without being invalidated - New regression test in tests/PHPStan/Analyser/nsrt/bug-14333.php
Contributor
VincentLanglet
left a comment
There was a problem hiding this comment.
Add test for nested array
function testNested(): void
{
$a = 1;
$b = [[&$a]];
assertType('1', $a);
$b[0][0] = 2;
assertType('2', $a);
$b[0] = [];
assertType('2', $a);
$b[0][0] = 3;
assertType('2', $a);
}
Nested arrays like `[[&$a]]` don't propagate reference updates through `$b[0][0] = 2` to `$a`. This documents the current behavior as a known limitation of the array reference tracking implementation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Collaborator
Author
|
Already retrieved — all 11,625 tests passed. Everything is done and pushed. |
…constant keys When a non-constant key appears in an array literal, the implicit index counter becomes unreliable. Stop setting up intertwined refs for implicit-keyed by-ref items after that point, since we can't know their actual indices. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When a variable is referenced inside a nested array literal (e.g. $b = [[&$a]]), assignments to the nested path ($b[0][0] = 2) now correctly propagate to the referenced variable ($a). The array reference setup in AssignHandler is refactored into a recursive method that builds chained ArrayDimFetch expressions for arbitrarily nested arrays. When an intermediate array path is reassigned ($b[0] = []), the nested intertwined refs are invalidated by checking whether the dim-fetch chain still resolves to valid offsets, preventing stale references from propagating incorrect types. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ant keys Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
VincentLanglet
requested changes
Mar 20, 2026
…pression Instead of preserving and restoring non-variable-to-variable intertwined refs in assignVariable, handle them in invalidateExpression by skipping refs whose endpoint variables (variableName, root of expr, root of assignedExpr) match the variable being invalidated. Refs are still invalidated when a non-endpoint variable (e.g. a dim index like $k in $a[$k]) changes. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ession Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move the `$assignedExpr->var instanceof ArrayDimFetch` guard into the method itself and rename from isNestedDimFetchPathValid to isDimFetchPathReachable for clarity. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Contributor
VincentLanglet
left a comment
There was a problem hiding this comment.
I added a failing test
function foo(array &$a): void {}
function testFunctionCall() {
$b = 1;
$c = [&$b];
assertType('array{1}', $c);
foo($c);
assertType('array', $c);
assertType('mixed', $b);
}
Currently PHPStan thinks $c is a non-empty-array and keep the offset but this is maybe not true since I might have unset the key 0.
VincentLanglet
requested changes
Mar 20, 2026
Contributor
VincentLanglet
left a comment
There was a problem hiding this comment.
The test
function testFunctionCall() {
$b = 1;
$c = [&$b];
assertType('array{1}', $c);
foo($c);
assertType('array', $c);
assertType('mixed', $b);
}
is not passing because it currently think $c is a non-empty-array.
Fix the code.
… reassignment When a variable like $c is completely reassigned (e.g. by a by-ref function call), the propagation chain would update referenced variables correctly ($b = mixed), but then the reverse ref would assign back to $c[0], making $c appear as non-empty-array&hasOffsetValue(0, mixed) instead of array. Skip the reverse assignment when the target's root variable is already in the propagation chain. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Collaborator
Author
|
Already processed — static analysis passed with no errors. The fix has been committed and pushed. |
VincentLanglet
approved these changes
Mar 20, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
When a variable is assigned as a by-reference item inside an array literal (e.g.
$b = ['key' => &$a]), subsequent assignments to that array key (e.g.$b['key'] = 42) should propagate the type change to the referenced variable ($a). PHPStan was not tracking these array-to-variable references.Changes
src/Analyser/ExprHandler/AssignHandler.php: After assigning a variable with anArray_expression, inspect each array item forbyRefreferences to variables. For each such item, create bidirectionalIntertwinedVariableByReferenceWithExprentries that link the array offset to the referenced variable, enabling type propagation in both directions.src/Analyser/MutatingScope.php: InassignVariable(), preserve non-variable-to-variable intertwined refs (those involvingArrayDimFetch) across the propagation loop. Without this, the recursiveassignVariablecalls during propagation would inadvertently invalidate the intertwined refs, preventing subsequent array key assignments from propagating to referenced variables.Root cause
PHPStan's reference tracking mechanism (
IntertwinedVariableByReferenceWithExpr) was only set up for direct variable-to-variable references ($b = &$a), not for by-reference items inside array literals ($b = ['key' => &$a]). Additionally, the intertwined refs for array-to-variable references were being lost during the type propagation chain because each recursiveassignVariablecall would invalidate expressions containing the target variable, which included the intertwined ref nodes.Test
Added
tests/PHPStan/Analyser/nsrt/bug-14333.phpwith two test cases:testByRefInArrayWithKey()— named key:$b = ['key' => &$a]; $b['key'] = 42;verifies$abecomes42testMultipleByRefInArray()— implicit indices with multiple refs:$b = [&$a, 'normal', &$c]; $b[0] = 2; $b[2] = 'bar';verifies both$aand$care updatedFixes phpstan/phpstan#14333