Skip to content

Fix phpstan/phpstan#14275: Variable passed by reference are not updated#5217

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

Fix phpstan/phpstan#14275: Variable passed by reference are not updated#5217
phpstan-bot wants to merge 6 commits intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-qqhkbas

Conversation

@phpstan-bot
Copy link
Collaborator

@phpstan-bot phpstan-bot commented Mar 13, 2026

Summary

When two variables are linked by reference ($b = &$a), modifying one should update the type of the other. Previously, PHPStan treated $b = &$a as a one-time type copy with no lasting link, so $a's type was not updated when $b was subsequently modified.

Changes

  • Added bidirectional IntertwinedVariableByReferenceWithExpr registration in src/Analyser/ExprHandler/AssignHandler.php when processing AssignRef between two simple variables
  • When $b = &$a is encountered, two intertwined entries are created:
    • One that updates $a when $b is assigned
    • One that updates $b when $a is assigned

Root cause

PHPStan already had the IntertwinedVariableByReferenceWithExpr mechanism for propagating type changes through references in foreach-by-reference loops, but this mechanism was not used for simple variable-to-variable reference assignments ($b = &$a). The fix extends the same mechanism to cover this case.

Test

Added tests/PHPStan/Analyser/nsrt/bug-14275.php with three scenarios:

  • Array modification through reference ($b[0] = 1 updating $a)
  • Scalar reassignment through reference ($d = 2 updating $c)
  • Type-changing reassignment through reference ($f = 42 updating string $e)

Fixes phpstan/phpstan#14275
Fixes phpstan/phpstan#8056

…ferences

- Register bidirectional IntertwinedVariableByReferenceWithExpr entries when processing AssignRef between two simple variables
- When $b = &$a, modifying $b now updates $a's type and vice versa
- Reuses existing IntertwinedVariableByReferenceWithExpr mechanism that was already used for foreach-by-reference
- New regression test in tests/PHPStan/Analyser/nsrt/bug-14275.php
Copy link
Member

@ondrejmirtes ondrejmirtes left a comment

Choose a reason for hiding this comment

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

Needs regression test for first code snippet in phpstan/phpstan#8056

Also update PR description that will close this issue

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

Tests also completed successfully (already retrieved - 11566 tests, 78796 assertions, all passing). Everything is done.

@VincentLanglet
Copy link
Contributor

This is a good improvement for now but I feel like IntertwinedVariableByReferenceWithExpr will needs something more ondrej since we're currently having

$e = 'hello';
$f = &$e;
$f = 42;
assertType('42', $e);
assertType('42', $f);

$e = 22;
assertType('22', $e);
assertType('22', $f); // Currently 42 but should be 22

$f = 33;
assertType('33', $e); // Currently 22 but should be 33
assertType('33', $f);

Copy link
Member

@ondrejmirtes ondrejmirtes left a comment

Choose a reason for hiding this comment

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

@phpstan-bot Check the comments about $e and $f

@phpstan-bot
Copy link
Collaborator Author

The working tree is clean and the previous review's requested changes (adding regression test for #8056) have already been addressed in commit a7f79acee.

VincentLanglet's review is APPROVED with a comment noting a limitation for future improvement - that chained reference updates (modifying $e after $f = &$e and expecting $f to update, and vice versa for subsequent assignments) don't work yet. This is acknowledged as a future enhancement, not a blocking issue.

Since the review is an approval with no changes requested, and the working tree is clean with all previous review feedback addressed, there's nothing more to do here. The PR is ready to merge.

Copy link
Member

@ondrejmirtes ondrejmirtes left a comment

Choose a reason for hiding this comment

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

Plaese try to improve the code and the use cases now in this branch instead of later.

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 fix does not seems enough since I expects

$e = 'hello';
$f = &$e;
$f = 42;
assertType('42', $e);
assertType('42', $f);

$e = 22;
assertType('22', $e);
assertType('22', $f); // Currently 42 but should be 22

$f = 33;
assertType('33', $e); // Currently 22 but should be 33
assertType('33', $f);

…uent assignments

When a reference like `$f = &$e` is created, intertwined entries are
registered so that assigning to one variable updates the other. However,
the `invalidateExpression` mechanism removed these entries during
propagation, so only the first assignment after the reference was created
would propagate. This fix re-registers both directions of the intertwined
entries after each propagation, ensuring that subsequent assignments
(e.g. `$e = 22` after `$f = 42`) continue to update the linked variable.

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

Already processed - all 11566 tests passed. Everything has been committed and pushed.

Copy link
Member

@ondrejmirtes ondrejmirtes left a comment

Choose a reason for hiding this comment

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

Instead of 9ac8874 come up with a better more universal fix.

When $b = &$a creates a bidirectional reference link, assigning to either
variable should propagate the type to the other for ALL subsequent
assignments, not just the first one. The issue was that
invalidateExpression() removed the reverse-direction intertwined entries
when a variable was assigned, because the entries' sub-nodes contained
the assigned variable. This fix preserves those entries by collecting
them before invalidation and restoring reverse-direction entries after
propagation.

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

That was the final PHPStan run confirming 0 errors. All work is complete and pushed.

…gistering

Replace the re-register-after-propagation approach with a cleaner design:
- Skip invalidation of simple variable-to-variable IntertwinedVariableByReferenceWithExpr
  entries in invalidateExpression() so reference links persist across assignments
- Add propagateReferences parameter to assignVariable() to prevent infinite
  recursion when propagating types through bidirectional reference links

This is simpler and more efficient than the previous approach of letting
entries be invalidated and then re-creating them after each propagation.

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

Earlier background task, already completed. All work is done and pushed.

The boolean propagateReferences=false approach blocks ALL intertwined
propagation in recursive calls, breaking nested foreach-by-reference
(e.g. bug-13676 where value→row→rows must chain).

Replace with intertwinedPropagatedFrom: a list of variable names already
visited in the current propagation chain. This prevents circular
back-propagation (A→B→A) while allowing legitimate chains (value→row→rows).

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

All background tasks complete. The fix is pushed and all checks pass.

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.

3 participants