Report exceptions thrown from ResultCacheMetaExtension during result cache restore as internal errors#5853
Open
phpstan-bot wants to merge 6 commits into
Open
Conversation
staabm
reviewed
Jun 12, 2026
staabm
reviewed
Jun 12, 2026
7af6fbc to
08e3ab8
Compare
…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>
08e3ab8 to
cd98d85
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 globalset_exception_handler()was installed by a bootstrap file (e.g. larastan booting Laravel), that handler swallowed it and the process exited0: 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 originalThrowabletogether with the offending extension class name.src/Analyser/ResultCache/ResultCacheManager.php:getMetaFromPhpStanExtensions()now wraps thegetKey()/getHash()calls in atry/catchand rethrows anyThrowableasResultCacheMetaExtensionException.src/Command/AnalyseApplication.php:analyse()catchesResultCacheMetaExtensionExceptionaroundrestore()and converts it into anInternalError(via the newcreateResultCacheMetaExtensionErrorResult()helper), producing anAnalysisResultthat exits non-zero. In debug mode the original exception is rethrown so--debugstill prints the full trace.getKey()(sibling ofgetHash()): the same unguarded-extension-callback bug — now also guarded.ResultCacheManager::process()(the cache-save path): it reuses the meta already computed duringrestore()rather than callinggetHash()again, so it cannot throw this exception and needs no guard.Root cause
ResultCacheManager::restore()→getMeta()→getMetaFromPhpStanExtensions()invoked eachResultCacheMetaExtension::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 escapedAnalyseApplication::analyse()and was rethrown byAnalyseCommand, 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()andgetHash()) 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