Infer the type of a static variable from what the function body does with it - #6617
Merged
Merged
Conversation
…with it A `static $x` without `@var` was `mixed`. Every call starts where a previous one left the variable, so its type at the `static` statement is the default joined with every type the variable takes in the body: at the end, at every return, throw and yield, and at every call that could run the function again (every node the walk reaches). The two-pass driver collects those types from the observation pass and observes the body again from the first `static` statement - walking only the statements that read a variable whose type changed - until the type at each `static` statement holds everything the variable takes after it, generalized like a loop's variables. The template arguments and closure signatures of the body are resolved from the facts of that last walk, so they observe the converged types. Bodies whose variables can change behind the analysis' back ($$name, extract(), parse_str(), include, eval), generators, and variables typed by `@var`, taken by reference or declared `global` keep `mixed`. StaticVarWithoutTypeRule (playground) is no longer needed. Gated by featureToggles.staticVariablesFromUsages (bleeding edge). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VpbB99tJfUmeArX74xqvHv
ondrejmirtes
force-pushed
the
static-variables-from-usages
branch
from
September 27, 2026 12:19
660c3dc to
a49a4e6
Compare
Closes phpstan/phpstan#7066 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VpbB99tJfUmeArX74xqvHv
ondrejmirtes
marked this pull request as ready for review
September 27, 2026 12:27
Collaborator
|
This pull request has been marked as ready for review. |
This was referenced Sep 27, 2026
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.
A
static $xwithout a@vartag wasmixed. It now gets its type from what the function body does with it:Bleeding edge only (
featureToggles.staticVariablesFromUsages). Like #6604, it runs inside the two-pass body walk ofunresolvedTemplateArguments, which bleeding edge enables too.How it works
Every call starts where a previous one left the variable. Its type at the
staticstatement is therefore the default joined with every type the variable takes in the body: at the end, at every return, throw and yield, and at every node the walk reaches (a call there could run the function again, recursion included).StatementsHandler::processBodyStmtNodesTwoPass) collects those types from the observation pass.staticstatement until the type at everystaticstatement holds everything the variable takes after it. Like pass 2, each round walks only the statements that read a variable whose type changed. The types are generalized like a loop's variables, within the loop iteration limit.StaticVariableHandlerassigns the converged type, or the default's type while observing.These keep
mixed:$$name,extract(),parse_str(),include,eval);@var, taken by reference, or declaredglobal.StaticVarWithoutTypeRule(playground) is removed; it only existed because the type wasmixed.Turbo. The new
StaticVariableInferenceservice is shadowed natively, and the native mirrors ofStatementsHandler,StaticVariableHandlerandTemplateArgumentFrameare updated.Verification
make testsis green with the extension off and loaded.make phpstanand cs are clean.walk-trace.phpshows the PHP and native walks identical. Side-by-side, smoke and signature parity pass.nsrt/static-variables-from-usages.php: counters, singletons, memoizing caches, loops, throws, by-ref closures, recursion, and the cases that keepmixed;StaticVariablesFromUsagesToggleOffTest: legacymixedwith the toggle off.bug-1870.php,bug-13810.php,new-in-initializers.php,LegacyNodeScopeResolverTest(array{}instead ofmixed), a newStrictComparisonOfDifferentTypesRuleTesterror on a static (int<0, 1>|null) compared against a value it can never hold, andReturnTypeAfterFinallyRuleTest, where a previous call leaves'test'in a by-ref static, so the wrong return is now reported byReturnTypeRuleat the return itself.Downstream
Measured on this change stacked on the previous revision of #6604/#6610, with turbo phars and user CPU:
array_udiff()results declared aslist;getSchemaName().Presenterpassed where the application'sPresenteris expected, a nullableparseFilterValue(), a deprecation, a redundantarray_values(), and fiveisset()checks made dead by regex patterns that are now known.Closes phpstan/phpstan#7066
🤖 Generated with Claude Code
https://claude.ai/code/session_01VpbB99tJfUmeArX74xqvHv