Skip to content

Fix phpstan/phpstan#8926: Incorrect inference of instance variable modified from callback#5347

Merged
staabm merged 4 commits intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-pdlnzcu
Mar 30, 2026
Merged

Fix phpstan/phpstan#8926: Incorrect inference of instance variable modified from callback#5347
staabm merged 4 commits intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-pdlnzcu

Conversation

@phpstan-bot
Copy link
Copy Markdown
Collaborator

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

Summary

When a closure passed to array_filter (or similar functions that invoke their callback immediately) modifies $this->property, PHPStan incorrectly inferred the property retained its pre-call value. This caused false positive "If condition is always false" errors.

Changes

  • src/Analyser/NodeScopeResolver.php: In processClosureNode's closureStmtsCallback, emit InvalidateExprNode for PropertyAssignNode so that property assignments inside closures are tracked for invalidation. In processImmediatelyCalledCallable, use requireMoreCharacters=false for non-Variable expressions so that exact PropertyFetch matches are properly invalidated.
  • src/Analyser/ExprHandler/Helper/ClosureTypeResolver.php: Same InvalidateExprNode emission for property assignments in both arrow function and closure type resolution callbacks, for consistency.
  • tests/PHPStan/Rules/Comparison/data/bug-8926.php: Extended existing regression test with array_filter callback case and local variable use(&$test) case.

Root cause

Two issues combined:

  1. Property assignments ($this->test = true) inside closures only emitted PropertyAssignNode (for impure point tracking), but not InvalidateExprNode. Without InvalidateExprNode, processImmediatelyCalledCallable had nothing to invalidate after the function call returned.
  2. processImmediatelyCalledCallable always passed requireMoreCharacters=true to invalidateExpression(). This flag means "only invalidate expressions that contain the target as a sub-expression". For Variable expressions (the original use case), this correctly invalidates $x->foo without invalidating $x itself. But for PropertyFetch expressions like $this->test, it prevented the exact match from being invalidated.

Test

Extended the existing tests/PHPStan/Rules/Comparison/data/bug-8926.php with two new methods: errorArrayFilter (the reported bug - closure modifying $this->test via array_filter callback) and successLocal (local variable modified via use(&$test) in array_filter callback, which already worked). Both expect no errors.

Fixes phpstan/phpstan#8926
Fixes phpstan/phpstan#11417
Fixes phpstan/phpstan#10903

…dified from callback

- Added InvalidateExprNode emission for PropertyAssignNode inside closures
  in NodeScopeResolver::processClosureNode() and ClosureTypeResolver
- Changed processImmediatelyCalledCallable to use requireMoreCharacters=false
  for non-Variable expressions (PropertyFetch), so exact property matches
  are properly invalidated
- Extended existing regression test in bug-8926.php with array_filter case
Copy link
Copy Markdown
Contributor

@staabm staabm left a comment

Choose a reason for hiding this comment

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

add regression tests for



Both issues report the same root cause as #8926: property mutations inside
immediately-invoked closures (via @param-immediately-invoked-callable or
array_filter) were not tracked, causing false positive "always true/false"
condition errors.

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

All 11690 tests pass (78932 assertions), and make phpstan reports no errors. Everything is green. The commit has been pushed.

@staabm staabm requested a review from VincentLanglet March 30, 2026 06:57
@staabm
Copy link
Copy Markdown
Contributor

staabm commented Mar 30, 2026

new error in PMMP integration tests looks valid to me.


class Wrap {
/**
* @param-immediately-invoked-callable $cb
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.

Add the same test with later-invoked-callable ; $this->conn should not be invalidate for later-invoked-callable.

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.

make tests completed successfully as well — all tests pass. The commit has already been pushed.

Adds a test with @param-later-invoked-callable to verify that property
invalidation behavior is distinct from @param-immediately-invoked-callable.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
$this->conn = "conn";
});

if (is_null($this->conn)) { // should be always true - later-invoked callable doesn't invalidate
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.

Rather than adding a comment, find why it's not the case.

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. Here's what I did to address VincentLanglet's review:

Root cause: deferredInvalidateExpressions was populated unconditionally for all closures (line 3373 and 3421 in NodeScopeResolver.php), regardless of whether the callable was @param-immediately-invoked-callable or @param-later-invoked-callable. This meant property types were incorrectly invalidated even for later-invoked callables.

Fix: Gated deferredInvalidateExpressions[] behind $this->callCallbackImmediately() in both the closure path and the generic callable path, matching how throw/impure points are already gated.

Test update: The bug-11417.php test now properly asserts that is_null($this->conn) reports "If condition is always true" for the later-invoked case (line 66), instead of just having a comment about it.

…ot later-invoked ones

The deferredInvalidateExpressions were being collected unconditionally
for all closures, regardless of whether the callable was marked with
@param-immediately-invoked-callable or @param-later-invoked-callable.
This caused property types to be incorrectly invalidated even for
later-invoked callables where the callback hasn't executed yet.

Gate deferredInvalidateExpressions behind callCallbackImmediately() and
update the bug-11417 test to assert that the later-invoked-callable case
correctly reports "If condition is always true" instead of just a comment.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@VincentLanglet
Copy link
Copy Markdown
Contributor

Is it still ok for you @staabm ?
I avoided the error on PMMP by not invalidating the values for later-invoked-callable.

If it's ok for you, you can merge it.

@staabm staabm merged commit 0cfd9b0 into phpstan:2.1.x Mar 30, 2026
655 of 656 checks passed
@staabm
Copy link
Copy Markdown
Contributor

staabm commented Mar 30, 2026

thank you

@staabm staabm deleted the create-pull-request/patch-pdlnzcu branch March 30, 2026 09:09
phpstan-bot added a commit to phpstan-bot/phpstan-src that referenced this pull request Apr 7, 2026
…dified from callback (phpstan#5347)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
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