Skip to content

Add an option to re-run only a changed rule on the files that need it - #6733

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

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

Conversation

@SanderMuller

@SanderMuller SanderMuller commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

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

With the new option resultCacheRuleScopedReanalysis: true, a project that edits one of its own rules re-runs only that rule. It runs the rule on every file that contains a node of the rule's type, whether the file reported errors before or not. The option is off by default, so a rule change still starts a full analysis unless the project turns it on. It is meant for projects that develop their own rules. On rector-src a rule edit took 3.0 s, against 6.9-7.2 s for the full analysis, with the same errors.

Why the result is the same as a full analysis: a rule's processNode() only gets nodes of its node type. The result cache records every node class that each file passes to the rules. So the changed rule sees the same nodes as in 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. With the option on, the result cache stores them, and the node types of each project rule, in a new ruleScope frame. RuleScopeCodec names each node class once in a table. On rector-src the cache file is 2.9% larger than on 2.3.x (45.7 MB to 47.1 MB). With the option off, the frame is not written.
  • 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.
  • 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.

Tests:

  • e2e/result-cache-rule-edit turns the option on and 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.
  • The result-cache-extension-* e2e tests keep their 2.3.x assertions: without the option, a rule edit starts a full analysis.
  • RuleScopeCodecTest covers the cache format, including malformed data.
  • I also compared rule edits with cold runs on rector-src and on a small project. The runs used spawned and forked workers, from source and from a phar with Turbo.

Not in this PR: a change of the rules: list in the config still starts a full analysis.

🤖 Generated with Claude Code

SanderMuller and others added 6 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>
RuleScopeCodec holds the encode and decode steps of the ruleScope
section, which were private methods of ResultCacheManager. The manager
still turns the file keys into absolute paths after decode.

The test checks that each node class is named once, that a round trip
gives the same data, and that each malformed shape throws.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@SanderMuller
SanderMuller marked this pull request as ready for review October 12, 2026 07:41
@phpstan-bot

Copy link
Copy Markdown
Collaborator

This pull request has been marked as ready for review.

By default, a changed rule of the project starts a full analysis again,
as on 2.3.x. A project turns the rule-scoped re-analysis on with
resultCacheRuleScopedReanalysis: true. When the option is off, the
result cache does not store the rule scope, so the cache file does not
grow.

The result-cache-extension e2e tests assert the full analysis again,
and result-cache-rule-edit turns the option on.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@SanderMuller SanderMuller changed the title Re-run only a changed rule on the files that need it Add an option to re-run only a changed rule on the files that need it Oct 12, 2026
@SanderMuller

Copy link
Copy Markdown
Contributor Author

The red extension-tests / phpstan-doctrine (8.1) make tests job on 0b4a99d comes from the random test order in phpstan-doctrine, not from this change. The test fails with a phar of 2.3.x too, whenever its data set #1 runs before data set #0. The fix is in phpstan/phpstan-doctrine#798.

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.

2 participants