Skip to content

Re-run only a changed rule on the files that need it - #6733

Draft
SanderMuller wants to merge 5 commits into
phpstan:2.3.xfrom
SanderMuller:rule-scoped-reanalysis
Draft

SanderMuller wants to merge 5 commits into
phpstan:2.3.xfrom
SanderMuller:rule-scoped-reanalysis

Conversation

@SanderMuller

@SanderMuller SanderMuller commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

Draft for phpstan/phpstan#15460. The issue has the measurements and the edge cases that I checked.

When a project edits one of its own rules, PHPStan now re-runs only that rule. It runs the rule only on the files that contain a node of the rule's type. Today the same edit starts a full analysis.

How it works:

  • Error records the class of the rule that reported it. When the error is in another file than the analysed one, it also records the analysed file.

  • FileAnalyserCallback records the node classes of each analysed file. The result cache stores them, and the node types of each rule, in a new ruleScope frame.

  • ResultCacheManager::restore() checks each changed extension file. For a class that implements only Rule, it selects the files with a node of the rule's old or new type. Any other changed extension file starts a full analysis, as before.

  • In these files, FileAnalyser runs only the changed rules and no collectors, and it does not match the local ignores.

  • ParallelAnalyser adds the changed rules and the job's rule-scoped files to each job request, and WorkerRunner sets them before it analyses the job. I checked this with spawned and with forked workers.

  • e2e/result-cache-rule-edit edits the project's rules five times. After each edit, it checks the errors that a cold run reports. The cases are ignores, a trait, an error reported into another file, a CollectedDataNode rule and a collector.

  • ResultCacheManager::process() keeps the cached data of these files and replaces the errors of the changed rules. Then it matches the line ignores of each file again with LocalIgnoresProcessor. Only the errors that the file's own analysis reported take part.

  • The ruleScope frame names each node class once in a table, and each file refers to the classes by position. On rector-src the cache file is 2.9% larger than on 2.3.x (45.7 MB to 47.1 MB).

Before this can leave draft:

  • A change of the rules: list in the config still starts a full analysis.

🤖 Generated with Claude Code

SanderMuller and others added 5 commits October 11, 2026 22:15
When an extension file that changed is a class that implements only Rule,
the result cache no longer starts a full analysis. It re-analyses only the
files that passed a node of the rule's old or new type to the rules, and in
these files only that rule runs.

- Error records the class of the rule that reported it, and the analysed
  file when the error is in another file.
- FileAnalyserCallback records the node classes of each analysed file, and
  the result cache stores them with the node types of each rule.
- In a rule-scoped file, FileAnalyser runs only the changed rules, no
  collectors, and leaves the local ignores to the result cache.
- ResultCacheManager::process() keeps the cached data of the rule-scoped
  files, replaces the errors of the changed rules and matches the line
  ignores of each rule-scoped file again.

The parallel workers get the rule scope from a file that an environment
variable names. This is a prototype for phpstan/phpstan#15460.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… tests

Only a rule of the project itself can change without a full analysis, so the
result cache now stores node types only for those rules. Before, it stored
them for every registered rule, which made the cache file of
e2e/result-cache-truncated four times larger and moved where its cut lands.

An edited project rule is now re-run instead of starting a full analysis, so
the result-cache-extension e2e tests check for that.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The main process now adds the changed rules and the job's rule-scoped files to
each job request, and the worker sets them before it analyses the job. This
replaces the environment variable and the temporary file, and works the same
for spawned and forked workers.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The new e2e test edits the project's own rules and collector, and runs
PHPStan after each edit. It checks message changes, an ignore comment
that stops or starts to match, an error in a trait, an error that a
rule reports into another file, a CollectedDataNode rule, and a
collector. The expected lines come from a cold run of the same files.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The rule scope stored a list of node class names for each file, so the
cache file repeated the same names for every file. It now stores the
classes once in a table, and each file stores a list of positions in it,
as the dependency graph does with paths. restore() decodes the section
back to class names, so findRuleScopedFiles() and mergeNodeTypes() do
not change.

A malformed rule scope now makes restore() discard the cache, as other
decode errors do. This includes a cache written in the earlier format of
this section.

On rector-src the rule scope section was 4,104,301 bytes and is now
1,265,225.

Co-Authored-By: Claude Opus 5.5 <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.

1 participant