Skip to content

Return dependencies in the results of the handlers - #6662

Merged
ondrejmirtes merged 5 commits into
2.3.xfrom
dependency-recording-in-handlers
Oct 2, 2026
Merged

ondrejmirtes merged 5 commits into
2.3.xfrom
dependency-recording-in-handlers

Conversation

@ondrejmirtes

@ondrejmirtes ondrejmirtes commented Oct 2, 2026 •

Copy link
Copy Markdown
Member

Follows #6659, which is merged now.

DependencyResolver looked at every node the rules saw and resolved again what the walk had already resolved: the method a call calls, the type of the expression, the class a declaration extends. This PR moves that into the handlers. Each handler puts what it depends on into the ExpressionResult or InternalStatementResult it returns, together with what the results of its parts depend on, the same way impure points and variable flow are carried. NodeScopeResolver::processFileNodes() returns what the whole file depends on, and DependencyResolver::resolveFileDependencies() turns that into file and package dependencies once per file. Exported nodes are resolved per node as before.

  • Dependencies is an immutable value. Merging only points to the merged values, and the tree is walked once at the end of the file.
  • Loop handlers keep only the dependencies of their final pass.
  • Nodes that a rule passes to the node callback through NodeCallbackInvoker are not walked by the handlers, so their dependencies are no longer collected.
  • Dependencies is shadowed natively and the native twins return the dependencies the same way.

The first commit fixes a printer bug that this change exposed in the walk-trace check. Printer::p() reused a multi-line form that ExprPrinter::printExpr() had cached at the top level, so an expression key depended on which expression was printed first. That order differs with turbo loaded. It shows up with a remembered possibly impure call that has a closure in a closure in its arguments.

The last commit makes FileAnalyserCallback ask DependencyResolver about exported nodes only for the node classes that can have one, instead of calling it for every node. It also makes resolveFileDependencies() expand each class to its ancestors once per file.

Same dependencies as before

The per-file results (files, used-trait files, packages and exported nodes) were compared with 2.3.x (ed978d4):

  • WordPress (1,288 files): identical.
  • Slevomat (14,319 files): identical except one more file dependency in one file: an enum case fetched in the arguments of a nullsafe method call, which 2.3.x missed.
  • phpstan-src (2,686 files unchanged by this PR): identical except one missing package, jetbrains/phpstorm-stubs in ExportedInterfaceNode.php. It came from stdClass, the declaring class of a dummy method on mixed.
  • phpstan-src with and without turbo: identical.

Reported errors are the same as on 2.3.x in all three projects.

Benchmark

phpstan-src src, turbo on, result cache cleared, one process (--debug), instructions retired (stable to about 0.05G between runs):

instructions
2.3.x 270.87G
this PR 266.94G -1.45%

User CPU with parallel workers is too noisy on this machine to show the difference reliably. Without turbo the change is within noise.

🤖 Generated with Claude Code

https://claude.ai/code/session_01GdwtgiEHwkF5BTz73jgHQQ

@ondrejmirtes
ondrejmirtes force-pushed the dependency-tracker-scenarios branch from 6f58af6 to 2c7c9cf Compare October 2, 2026 15:29
Base automatically changed from dependency-tracker-scenarios to 2.3.x October 2, 2026 15:29
ExprPrinter::printExpr() remembers the printed form of every expression
on its node, a multi-line one too, and Printer::p() reused it when the
same node was printed nested in another expression, where it needs the
indentation of that level. The printed key then depended on which
expression was printed first, which differs with the turbo extension
loaded: a remembered possibly impure call with a closure in a closure
in its arguments was a different expression in the two.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GdwtgiEHwkF5BTz73jgHQQ
@ondrejmirtes
ondrejmirtes force-pushed the dependency-recording-in-handlers branch from 11f05d9 to 6eab5a4 Compare October 2, 2026 15:40
ondrejmirtes and others added 2 commits October 2, 2026 22:23
DependencyResolver looked at every node the rules saw and resolved again
what the walk had just resolved: the method a call calls, the type of the
expression, the class a declaration extends. Now each handler puts what it
depends on into the ExpressionResult or InternalStatementResult it returns,
together with what the results of its parts depend on, like impure points
and variable flow. NodeScopeResolver::processFileNodes() returns what the
whole file depends on and DependencyResolver turns that into file and
package dependencies once per file. Exported nodes are resolved per node as
before.

Dependencies is an immutable value. Merging only points to the merged
values, the tree is walked once, at the end of the file. Loop handlers keep
only the dependencies of their final pass. Nodes that a rule passes to the
node callback through NodeCallbackInvoker are not walked by the handlers,
so their dependencies are no longer collected.

Dependencies is shadowed natively and the native twins return the
dependencies the same way.

Compared with DependencyResolver on phpstan-src and WordPress, the
dependencies are the same except for stdClass, the declaring class of a
dummy method on mixed, which shows up or goes away in a few files.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GdwtgiEHwkF5BTz73jgHQQ
@ondrejmirtes
ondrejmirtes force-pushed the dependency-recording-in-handlers branch from 6eab5a4 to 4cf9d34 Compare October 2, 2026 20:47
@ondrejmirtes ondrejmirtes changed the title Collect dependencies in NodeScopeResolver and the handlers Return dependencies in the results of the handlers Oct 2, 2026
ondrejmirtes and others added 2 commits October 2, 2026 23:24
… that can have one

FileAnalyserCallback called resolveExportedNode() for every node the walk
reported. Now it asks once per node class whether a node of the class can be
exported or change the PHPDoc name scope, and skips the call for the rest.

resolveFileDependencies() now expands each class found in a file to its
ancestors once, and reads the referenced classes of each type object once.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GdwtgiEHwkF5BTz73jgHQQ
DirectoryResultCacheValueExtensionTest built the key from __DIR__ . '/data',
which a round trip through the result cache turns into backslashes on
Windows. ValueDependencyCollector normalizes the directory before it creates
the key, so the test now does that too.

ValueDependencyCollectorTest counted all calls of the extension, which the
container shares with the other tests in the class. It now counts the calls
the test itself makes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GdwtgiEHwkF5BTz73jgHQQ
@ondrejmirtes
ondrejmirtes merged commit 681809d into 2.3.x Oct 2, 2026
475 of 477 checks passed
@ondrejmirtes
ondrejmirtes deleted the dependency-recording-in-handlers branch October 2, 2026 21:42
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.

1 participant