Skip to content

Save the result cache when a file has a parse error - #6694

Merged
ondrejmirtes merged 1 commit into
phpstan:2.3.xfrom
SanderMuller:result-cache-nonignorable
Oct 8, 2026
Merged

ondrejmirtes merged 1 commit into
phpstan:2.3.xfrom
SanderMuller:result-cache-nonignorable

Conversation

@SanderMuller

@SanderMuller SanderMuller commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

A parse error in one analysed file stopped PHPStan from saving the result cache. So every run analysed the whole project again.

Now the result cache is saved without the results of a file with a parse error. Its recorded hash gets a reanalyse: prefix, so PHPStan analyses it again on every run. When the file is fixed and now declares a symbol, the usual rule applies: the files with errors are analysed again.

Any other exception still stops the save, including reflection errors. A rule that throws already stopped the save before this check, in a worker and in-process, because the dependencies are then missing. A parse error that is reported in a file that is not analysed also still stops the save.

Unchanged: while a parse error exists, AnalyseCommand still shows only the parse errors and hides the others.

This does not make the result cache work on symfony/symfony on its own. Its tests in src also contain class NotLoadableClass extends NotLoadableClass, which gives a reflection error in NotLoadableClass.php and ReflectionCasterTest.php. Those two files still stop the save.

Verification:

  • New e2e test result-cache-parse-error:
    • One file has a parse error and would declare a class that another file uses. The first run saves the result cache.
    • The second run analyses only the broken file and does not rewrite the cache.
    • After the parse error is fixed, the "class not found" error in the other file is gone.
    • A class that extends itself then still stops the save.
    • This test fails on the base. It also fails for each of these changes to the fix:
      • reflection errors are let through,
      • the reanalyse: prefix is left out,
      • the prefix is not excluded from the recorded file stat, or from the compared one,
      • the "unchanged" check counts the broken file.
    • The recorded file stat change only fails when the stat signatures are trusted. The files must then be older than the run, as after a CI checkout.
  • New e2e test result-cache-internal-error: a rule that throws. The result cache is not saved. This passes on the base too, and guards the behaviour.
  • symfony/symfony at 2bb7fc81a (12,098 files), level 8, from source without turbo, with the two reflection-error files excluded:
    • 5 interleaved cold rounds: the base took 38.5 to 40.3 s (median 38.9 s) and 399.1 to 407.8 s user CPU (median 403.4 s). This PR took 38.8 to 44.9 s (median 40.2 s) and 400.4 to 415.4 s user CPU (median 404.1 s). The base never wrote the 234 MB result cache because of Config/Tests/Fixtures/ParseError.php, and this PR writes it.
    • A second run now takes 1.35 to 1.83 s, analyses 1 file and does not rewrite the cache.
    • I fixed ParseError.php on a saved cache. The next run analysed 6,602 files, and its output matched a cold run on all 70,346 lines outside Form/Form.php and Form/FormErrorIterator.php. Those two files give different errors between two cold runs too.
  • make tests passes (22,503 tests, 74 skipped).
  • make phpstan reports no errors, and phpcs passes on ResultCacheManager.php.

🤖 Generated with Claude Code

@SanderMuller
SanderMuller force-pushed the result-cache-nonignorable branch from c685bef to 2bb1624 Compare October 7, 2026 13:08
@SanderMuller SanderMuller changed the title Save the result cache when a file has a parse or reflection error Save the result cache when a file has a parse error Oct 7, 2026
@SanderMuller
SanderMuller force-pushed the result-cache-nonignorable branch from 2bb1624 to b4754b6 Compare October 7, 2026 13:43
@staabm

staabm commented Oct 7, 2026

Copy link
Copy Markdown
Contributor

please test how this affects numbers reported for symfony with https://github.com/TomasVotruba/need-for-speed

@SanderMuller

Copy link
Copy Markdown
Contributor Author

I ran it with the Symfony setup of need-for-speed: their phpstan.neon (level 8, src, 24 processes) and their command, on symfony/symfony at c1dff4adf. All builds ran from source on an M4 Pro with 14 cores. Each value is the median of 3 rounds, and the order of the builds rotated every round.

Cold Hot
2.3.x (5f76acd3c) 52.8 s 44.1 s
This PR 50.4 s 43.2 s
This PR + the circular inheritance change 54.9 s 2.4 s

This PR alone does not change the Symfony number. The parse error in Config/Tests/Fixtures/ParseError.php no longer stops the save. The next blocker is the class that extends itself: Result cache was not saved because of non-ignorable exception: Reflection error: Circular reference to class "Symfony\Component\VarDumper\Tests\Fixtures\NotLoadableClass".

The last row adds the change for inheritance cycles: ondrejmirtes/BetterReflection#47, and the phpstan-src part that I have ready locally until a release of it. With both, the result cache is saved, and the hot run re-analyses only ParseError.php. The cycle is then a normal error, Class Symfony\Component\VarDumper\Tests\Fixtures\NotLoadableClass extends itself..

One note on their numbers: setup.sh downloads only phpstan.phar from the release, without turbo-ext/, so their PHPStan runs without Turbo. On the 2.3.0 phar downloaded that way, diagnose reports Turbo extension: not loaded. My runs from source are also without Turbo.

A file with a parse error is left out of the result cache and analysed
again on every run, so the rest of the result cache is saved. Other
exceptions, including reflection errors, still stop the save.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@SanderMuller
SanderMuller force-pushed the result-cache-nonignorable branch from b4754b6 to 206410f Compare October 7, 2026 21:26
@ondrejmirtes
ondrejmirtes merged commit 28f54de into phpstan:2.3.x Oct 8, 2026
914 of 926 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