Skip to content

Report exceptions thrown from ResultCacheMetaExtension during result cache restore as internal errors#5853

Open
phpstan-bot wants to merge 6 commits into
phpstan:2.2.xfrom
phpstan-bot:create-pull-request/patch-blyb8rf
Open

Report exceptions thrown from ResultCacheMetaExtension during result cache restore as internal errors#5853
phpstan-bot wants to merge 6 commits into
phpstan:2.2.xfrom
phpstan-bot:create-pull-request/patch-blyb8rf

Conversation

@phpstan-bot

@phpstan-bot phpstan-bot commented Jun 12, 2026

Copy link
Copy Markdown
Collaborator

Summary

When a ResultCacheMetaExtension::getHash() threw during result-cache restore (before any file was analysed), the exception was not caught anywhere in PHPStan. On its own it surfaced as an uncaught fatal (exit 255), and — more dangerously — when a global set_exception_handler() was installed by a bootstrap file (e.g. larastan booting Laravel), that handler swallowed it and the process exited 0: a silent no-op with green CI and nothing analysed.

This change makes PHPStan catch such exceptions itself and report them as internal errors, so the run always fails with a non-zero exit code, exactly as it does under --debug.

Changes

  • src/Analyser/ResultCache/ResultCacheMetaExtensionException.php (new): wraps the original Throwable together with the offending extension class name.
  • src/Analyser/ResultCache/ResultCacheManager.php: getMetaFromPhpStanExtensions() now wraps the getKey()/getHash() calls in a try/catch and rethrows any Throwable as ResultCacheMetaExtensionException.
  • src/Command/AnalyseApplication.php: analyse() catches ResultCacheMetaExtensionException around restore() and converts it into an InternalError (via the new createResultCacheMetaExtensionErrorResult() helper), producing an AnalysisResult that exits non-zero. In debug mode the original exception is rethrown so --debug still prints the full trace.
  • Analogous cases probed:
    • getKey() (sibling of getHash()): the same unguarded-extension-callback bug — now also guarded.
    • ResultCacheManager::process() (the cache-save path): it reuses the meta already computed during restore() rather than calling getHash() again, so it cannot throw this exception and needs no guard.

Root cause

ResultCacheManager::restore()getMeta()getMetaFromPhpStanExtensions() invoked each ResultCacheMetaExtension::getKey()/getHash() with no exception handling, before any file was analysed. Because the throw happened while computing cache metadata (not while analysing a file), it was never attributed to a file and never converted into an internal error. In normal mode the bare exception escaped AnalyseApplication::analyse() and was rethrown by AnalyseCommand, leaving its fate to PHP's default handler or, worse, to a user-installed global exception handler that turned it into a silent exit 0.

The pattern is "third-party extension callback invoked during the result-cache lifecycle without exception guarding." Both extension methods on that axis (getKey() and getHash()) are now guarded, and the failure is funnelled through PHPStan's existing internal-error reporting so it can never be swallowed by external handlers.

Fixes phpstan/phpstan#14805

Comment thread tests/PHPStan/Analyser/ResultCache/ResultCacheMetaExtensionTest.php Outdated
Comment thread src/Analyser/ResultCache/ResultCacheManager.php Outdated
phpstan-bot and others added 6 commits June 12, 2026 12:03
…t cache restore as internal errors

- Wrap `ResultCacheMetaExtension::getKey()`/`getHash()` calls in `ResultCacheManager::getMetaFromPhpStanExtensions()` and rethrow any `Throwable` as a new `ResultCacheMetaExtensionException` carrying the extension class and original exception.
- Catch `ResultCacheMetaExtensionException` around `restore()` in `AnalyseApplication::analyse()` and convert it into an `InternalError` so the run fails with a non-zero exit code instead of the exception escaping as a fatal that a global `set_exception_handler()` can swallow into exit 0.
- In debug mode the original exception is rethrown, matching the previous `--debug` behaviour.
- Guard both `getKey()` and `getHash()` (sibling extension methods) since either can throw the same way.
- `process()` reuses the meta already computed during `restore()` and does not call `getHash()` again, so no guard is needed there.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…rows

Reproduces phpstan/phpstan#14805 end-to-end: a ResultCacheMetaExtension
whose getHash() throws during result cache restore, combined with a
bootstrap file that installs a global set_exception_handler (as a framework
like Laravel/larastan would). Without the fix the handler swallows the
uncaught exception and the process exits 0, silently skipping analysis;
with the fix the throw is reported as an internal error and the run exits
non-zero.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reverts the in-PHPStan handling of exceptions thrown from
ResultCacheMetaExtension::getKey()/getHash() during result cache restore.
This will be replaced by catching \Throwable in the Symfony Console
Application, which fixes the swallowed-exit-0 problem generally.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
An uncaught \Error (e.g. thrown from a ResultCacheMetaExtension during
result cache restore) escaped Symfony Console's `catch (\Exception $e)`
in Application::run() and propagated to a user-installed global
set_exception_handler() (e.g. a framework bootstrap), which could swallow
it into a clean exit 0 - silently skipping analysis with green CI.

Patch the catch to \Throwable (PHP 7.x compatible) so any uncaught
Error/Exception is rendered and produces a non-zero exit code, the same
way `--debug` (setCatchExceptions(false)) already rethrows it.

Update the e2e test to assert the throw surfaces (exit 1, 'boom from
getHash') and is not swallowed by the global handler.

Closes phpstan/phpstan#14805

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@staabm staabm force-pushed the create-pull-request/patch-blyb8rf branch from 08e3ab8 to cd98d85 Compare June 12, 2026 10:03
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.

Exception from ResultCacheMetaExtension::getHash() during cache restore is swallowed → exits 0, analysis silently skipped

2 participants