Skip to content

Fix phpstan/phpstan#14096: Reference is no longer honored since PHPStan 2.1.39#5401

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

Fix phpstan/phpstan#14096: Reference is no longer honored since PHPStan 2.1.39#5401
phpstan-bot wants to merge 2 commits intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-wld7wzk

Conversation

@phpstan-bot
Copy link
Copy Markdown
Collaborator

Summary

Since 2.1.39, PHPStan incorrectly typed by-reference variables as null after a method call where multiple closure arguments captured the same variable via &$var. The variable's type should reflect modifications made inside any of the closures.

Changes

  • src/Analyser/ProcessClosureResult.php: Added optional $prevScope parameter to applyByRefUseScope() so callers can provide the current scope for type merging
  • src/Analyser/NodeScopeResolver.php: Pass $scope as $prevScope when applying deferred by-ref closure results in processArgs(), so types from multiple closures are unioned instead of the last one winning
  • src/Analyser/MutatingScope.php: Guard processClosureScope() against undefined variables in $prevScope by checking hasVariableType() before getVariableType()

Root cause

Commit 996ad98 deferred by-ref closure scope modifications in processArgs() to prevent them from polluting subsequent argument evaluation. However, when applying the deferred results sequentially at the end, each closure's result overwrote the previous one. If the second closure didn't modify the by-ref variable, its stale type (from before the first closure's modifications) replaced the first closure's correct type. The fix passes the current accumulated scope as $prevScope so that processClosureScope() unions the types instead of replacing them.

Test

Added tests/PHPStan/Analyser/nsrt/bug-14096.php reproducing the issue from the playground sample. The test verifies that after a method call with two closures capturing &$view, the variable's type is T|null rather than just null.

Fixes phpstan/phpstan#14096

…pe after deferred application

- When multiple closures capture the same by-ref variable, deferred application
  caused later closures to overwrite earlier modifications with their stale types
- Pass current scope as prevScope when applying deferred by-ref results so types
  are unioned instead of replaced
- Guard processClosureScope against undefined variables in prevScope
- New regression test in tests/PHPStan/Analyser/nsrt/bug-14096.php
}

public function applyByRefUseScope(MutatingScope $scope): MutatingScope
public function applyByRefUseScope(MutatingScope $scope, ?MutatingScope $prevScope = null): MutatingScope
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Do not use null as default value, prefer being explicit when calling this method

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Done. Removed the = null default from applyByRefUseScope()'s $prevScope parameter and updated the caller in ClosureHandler.php to explicitly pass null. Both make phpstan and make tests pass.

Make the $prevScope parameter explicit at all call sites instead of
using a default null value, as suggested in code review.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@staabm staabm deleted the create-pull-request/patch-wld7wzk branch April 5, 2026 06:05
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