Repository navigation
Consume only the nullsafe twin's receiver from stored results instead of its whole subtree - #6782
Merged
ondrejmirtes merged 1 commit intoOct 23, 2026
Conversation
ondrejmirtes
force-pushed
the
create-pull-request/patch-qe8gvoc
branch
2 times, most recently
from
October 23, 2026 08:27
523c7b6 to
e05877e
Compare
… of its whole subtree - NodeScopeResolver::processExprNodeConsumingStored() now takes the node to consume (the twin's receiver). The bool $consumeStoredExpressionResults flag becomes ?Expr $consumeStoredExpression, and processExprNode() consumes a stored result unconditionally only for that exact node. Everything else in the plain MethodCall / PropertyFetch twin (arguments, a dynamic method or property name) takes the normal path. - Since phpstan#6765, later loop convergence passes are seeded with the previous pass's stored results. The over-broad consume mode then answered the twin's arguments from pass-1 results without walking them. Rules never saw those nodes and gatherers never recorded them, so errors went missing and property.onlyWritten was reported falsely. - NullsafeMethodCallHandler / NullsafePropertyFetchHandler pass $expr->var as the consumed node. - ArgumentsHandler no longer consumes stored closure / arrow-function argument results in consume mode (removed NodeScopeResolver::isConsumingStoredExpressionResults()). Consume mode no longer reaches arguments, and that branch was the same bug for closures in `?->` arguments. - Mirrored in turbo-ext (NodeScopeResolver.cpp, ArgumentsHandler.cpp, Nullsafe*Handler.cpp, support.h, generated/NodeScopeResolver.h). - Probed and covered: foreach/while/do-while/for, closure and arrow-function arguments, a dynamic nullsafe property name, a dynamic nullsafe method name, nested `?->` in arguments. Also checked chained `?->`, `??`/isset around `?->`, spread and named arguments; all are fixed by the same change.
ondrejmirtes
force-pushed
the
create-pull-request/patch-qe8gvoc
branch
from
October 23, 2026 10:56
e05877e to
fd5c6e2
Compare
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
Arguments of a nullsafe method call (and the dynamic name of a nullsafe property fetch or method call) inside a loop that needs more than one convergence pass were never walked in later passes. Rules saw none of the nodes inside them, so errors went missing (
method.notFound,property.notFound,binaryOp.invalid, …) andproperty.onlyWrittenwas reported falsely for properties read only there.This change limits the nullsafe handlers' "consume stored results" mode to the one node it was introduced for: the twin's receiver.
Changes
src/Analyser/NodeScopeResolver.php:processExprNodeConsumingStored()takes the node to consume (Expr $consumedExpr).bool $consumeStoredExpressionResultsflag becomes?Expr $consumeStoredExpression.processExprNode()consumes a stored result unconditionally only when$expr === $consumeStoredExpression. Every other node in the twin subtree takes the normal path.isConsumingStoredExpressionResults()was removed.src/Analyser/ExprHandler/NullsafeMethodCallHandler.php,NullsafePropertyFetchHandler.php: pass$expr->varas the consumed node.src/Analyser/ArgumentsHandler.php: closure / arrow-function arguments no longer answer from storage in consume mode. Consume mode never reaches arguments now. That branch was the same bug for a closure passed to a?->call in a loop: its body was skipped in later passes.turbo-ext/src/NodeScopeResolver.cpp,ArgumentsHandler.cpp,NullsafeMethodCallHandler.cpp,NullsafePropertyFetchHandler.cpp,support.h,generated/NodeScopeResolver.h: the same changes in the C++ mirror.Analogous cases I probed:
foreach/while/do-while/for, closure and arrow-function arguments, a dynamic?->{…}property name, a dynamic?->{…}()method name, nested?->in arguments.?->,?->under??/isset, spread and named arguments.Root cause
The nullsafe handlers walk the receiver once, then walk a plain
MethodCall/PropertyFetchtwin in consume mode so the receiver isn't walked twice. The flag covered the whole twin subtree, though, and in consume modeprocessExprNode()returned any stored result without walking the node.Before #6765 that was harmless, because every pass started from fresh storage. Since #6765, later passes are seeded with the previous pass's results, so the twin's arguments found pass-1 results and were consumed instead of walked. As a result:
ClassStatementsGathererrecorded no property reads;Consuming only the receiver brings back the intended behaviour and keeps #6765's seeding.
Note:
turbo-ext/srcchanged, somake bump-turbois needed after this commit lands.Test
AnalyserIntegrationTest::testBug15520(tests/PHPStan/Analyser/data/bug-15520.php): the issue's "missing errors" playground snippet, verbatim, pluswhile,do-while,for, closure argument, arrow-function argument, dynamic nullsafe property name, nested?->and dynamic nullsafe method name variants. All expected errors are asserted.UnusedPrivatePropertyRuleTest::testBug15520(tests/PHPStan/Rules/DeadCode/data/bug-15520.php): the issue'sproperty.onlyWrittenplayground snippet, verbatim, pluswhileand dynamic-property-name variants.property.onlyWritten. Both pass with the fix, with and without the turbo extension loaded.Fixes phpstan/phpstan#15520