Skip to content

Answer per node class what resolveDependencies() can reach - #6478

Merged
ondrejmirtes merged 1 commit into
phpstan:2.2.xfrom
SanderMuller:node-profile-on-6475
Sep 19, 2026
Merged

ondrejmirtes merged 1 commit into
phpstan:2.2.xfrom
SanderMuller:node-profile-on-6475

Conversation

@SanderMuller

Copy link
Copy Markdown
Contributor

Follow-up to #6475, in the same place. That one halved the work on the reading side; this one is
about the calls that reach it with nothing to say.

resolveDependencies() runs once per analysed node, and 84.4% of those calls produce nothing at
all
. On a src/ analysis of this repo, 648270 of 767955 return no reflections and no file paths.
They walk all 29 branches of the chain to find that out. The most frequent nodes are
Expr\Variable, Arg, MethodCallExpressionNode and VariableAssignNode, and no branch matches
any of them.

Every question the method asks about a node depends only on the node's class.
Can it carry a @var tag, does a branch match it, can it produce an exported node, does it change
the PHPDoc name scope. So the answer is computed once per class and reused. A run
over src/ sees 152 distinct node classes.

Numbers

Single process (--debug), 1975 files, PHP 8.5.8, no turbo, hrtime() accumulator around
resolveDependencies(). Three rounds, medians, base spread 1.5247-1.5279s:

resolveDependencies()
base 1.528s
this branch 1.131s (-26%)

That is 0.40s of a 61s run, so 0.65% of it. Whole-run timings cannot separate the two arms, which is
why the figure comes from inside the method.

The lists mirror code elsewhere, so a test reads them back

Each of the three node-class lists mirrors a chain of instanceof checks in another method. A branch
added without an entry in its list would silently stop producing dependencies, which leaves a stale
result cache rather than a failing test. NodeProfileMirrorsBranchesTest parses the three methods
and compares them to the constants.

It failed on its first run, which is the reason it is here.
The export list named ClassMethod, Property, ClassConst and EnumCase.
ExportedNodeResolver::resolve() handles none of them.
A class member is exported through exportClassStatement(), as part of the class declaring it.
Dropping the four made this faster as well as correct.

Verification

  • Analysis output is byte-identical to the base branch over a full src/ run.
  • Cross-arm result cache: warm a cache on one arm, then run the other against it. Exactly one
    file is re-analysed, the changed one, in both directions, and a second run is 0. A different
    dependency graph or a different set of exported nodes could not produce that.
  • make tests passes, 21541 tests. make phpstan and phpcs are clean.
  • The e2e/result-cache-* scripts were run per case on both arms. Four fail on this machine, and the
    same four fail identically on the base branch, so I can report no difference between the arms
    rather than a clean local run. They need CI.

84.4 percent of the calls produce nothing at all: 648270 of 767955 on a
phpstan-src src/ analysis return no reflections and no file paths, after
walking all 29 branches of the chain to find out. The most frequent nodes are
Expr\Variable, Arg, MethodCallExpressionNode and VariableAssignNode, and no
branch matches any of them.

Every question the method asks about a node depends only on its class, so the
answer is computed once per class and reused: whether it can carry a @var tag,
whether a branch matches it, whether it can produce an exported node, and
whether it changes the PHPDoc name scope. There are 152 distinct node classes
in the run.

resolveDependencies() goes from 1.528s to 1.131s, 26 percent, on a
single-process src/ analysis of about 61s. The analysis output is identical.

Each of the three node-class lists mirrors a chain of instanceof checks
somewhere else, and a branch added without an entry in its list would silently
stop producing dependencies - a stale result cache rather than a failing test.
NodeProfileMirrorsBranchesTest reads the lists back out of the source they
mirror. It found four dead entries in the first version of this commit:
a class member reaches ExportedNodeResolver through exportClassStatement(),
never through resolve().

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@ondrejmirtes
ondrejmirtes merged commit e88417e into phpstan:2.2.x Sep 19, 2026
873 of 891 checks passed
@ondrejmirtes

Copy link
Copy Markdown
Member

Thank you!

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