Skip to content

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
phpstan-bot:create-pull-request/patch-bsc1yhl
Open

Fix phpstan/phpstan#14333: Setting an array key doesn't update a reference#5257
phpstan-bot wants to merge 15 commits intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-bsc1yhl

Conversation

@phpstan-bot
Copy link
Collaborator

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 an Array_ expression, inspect each array item for byRef references to variables. For each such item, create bidirectional IntertwinedVariableByReferenceWithExpr entries that link the array offset to the referenced variable, enabling type propagation in both directions.

  • src/Analyser/MutatingScope.php: In assignVariable(), preserve non-variable-to-variable intertwined refs (those involving ArrayDimFetch) across the propagation loop. Without this, the recursive assignVariable calls 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 recursive assignVariable call would invalidate expressions containing the target variable, which included the intertwined ref nodes.

Test

Added tests/PHPStan/Analyser/nsrt/bug-14333.php with two test cases:

  1. testByRefInArrayWithKey() — named key: $b = ['key' => &$a]; $b['key'] = 42; verifies $a becomes 42
  2. testMultipleByRefInArray() — implicit indices with multiple refs: $b = [&$a, 'normal', &$c]; $b[0] = 2; $b[2] = 'bar'; verifies both $a and $c are updated

Fixes phpstan/phpstan#14333

…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
Copy link
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.

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>
@phpstan-bot
Copy link
Collaborator Author

Already retrieved — all 11,625 tests passed. Everything is done and pushed.

phpstan-bot and others added 2 commits March 20, 2026 11:46
…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>
VincentLanglet and others added 3 commits March 20, 2026 12:55
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>
…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>
phpstan-bot and others added 2 commits March 20, 2026 13:09
…ession

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
phpstan-bot and others added 2 commits March 20, 2026 13:29
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>
Copy link
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 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.

Copy link
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.

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>
@phpstan-bot
Copy link
Collaborator Author

Already processed — static analysis passed with no errors. The fix has been committed and pushed.

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