From dda12a2ae5d03aecf2dfbab626c516827caa9f4c Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Sat, 22 Aug 2026 07:13:41 +0000 Subject: [PATCH 1/2] Notify in CI when a full analysis ran without a pre-existing result cache In long-running CI pipelines that do not persist PHPStan's result cache, every run is a full analysis and can be slow. Hint the user to persist the result cache directory when a full run happened and no result cache existed when PHPStan started. The hint is deliberately not shown when the result cache existed but was invalid (stale metadata, changed extension files, ...) and therefore triggered a full re-analysis. Closes https://github.com/phpstan/phpstan/issues/14881 --- .github/workflows/e2e-tests.yml | 14 +++++++++++ e2e/result-cache-ci-notification/phpstan.neon | 4 ++++ e2e/result-cache-ci-notification/src/Foo.php | 13 ++++++++++ .../ResultCache/ResultCacheManager.php | 9 +++++++ src/Command/AnalyseApplication.php | 2 ++ src/Command/AnalyseCommand.php | 24 +++++++++++++++++++ src/Command/AnalysisResult.php | 12 ++++++++++ 7 files changed, 78 insertions(+) create mode 100644 e2e/result-cache-ci-notification/phpstan.neon create mode 100644 e2e/result-cache-ci-notification/src/Foo.php diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 32f11ad1c1c..d9a9c44d9a3 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -523,6 +523,20 @@ jobs: echo "$OUTPUT" ../bashunit -a contains 'and register a class in the container; re-analysing everything.' "$OUTPUT" ../bashunit -a contains 'Subject.php:5:installer rule version 2' "$OUTPUT" + - script: | + cd e2e/result-cache-ci-notification + # https://github.com/phpstan/phpstan/issues/14881 + # A full analysis in CI without a pre-existing result cache should hint + # the user to persist the result cache directory between runs. + ../../bin/phpstan clear-result-cache + OUTPUT=$(../../bin/phpstan analyse 2>&1) + echo "$OUTPUT" + ../bashunit -a contains 'Persist PHPStan' "$OUTPUT" + ../bashunit -a contains 'to make your pipeline dramatically faster' "$OUTPUT" + # The second run finds the cache present, so the hint is not shown again. + OUTPUT=$(../../bin/phpstan analyse 2>&1) + echo "$OUTPUT" + ../bashunit -a not_contains 'to make your pipeline dramatically faster' "$OUTPUT" - script: | cd e2e/bug-12606 export CONFIGTEST=test diff --git a/e2e/result-cache-ci-notification/phpstan.neon b/e2e/result-cache-ci-notification/phpstan.neon new file mode 100644 index 00000000000..c3c98d3f20a --- /dev/null +++ b/e2e/result-cache-ci-notification/phpstan.neon @@ -0,0 +1,4 @@ +parameters: + level: 0 + paths: + - src diff --git a/e2e/result-cache-ci-notification/src/Foo.php b/e2e/result-cache-ci-notification/src/Foo.php new file mode 100644 index 00000000000..fa70dd29416 --- /dev/null +++ b/e2e/result-cache-ci-notification/src/Foo.php @@ -0,0 +1,13 @@ +pathTransformer ??= new ResultCachePathTransformer($this->anchorDirectory); } + /** + * Whether the result cache file was present on disk when PHPStan started. + * Distinguishes "the cache never existed" from "the cache existed but was invalid". + */ + public function resultCacheExists(): bool + { + return is_file($this->cacheFilePath); + } + /** * @param string[] $allAnalysedFiles * @param mixed[]|null $projectConfigArray diff --git a/src/Command/AnalyseApplication.php b/src/Command/AnalyseApplication.php index 2cc886eb0f9..7ec2169d4fb 100644 --- a/src/Command/AnalyseApplication.php +++ b/src/Command/AnalyseApplication.php @@ -71,6 +71,7 @@ public function analyse( $fileReplacements = [$insteadOfFile => $tmpFile]; } $resultCacheManager = $this->resultCacheManagerFactory->create($fileReplacements); + $resultCacheExisted = $resultCacheManager->resultCacheExists(); $ignoredErrorHelperResult = $this->ignoredErrorHelper->initialize(); $fileSpecificErrors = []; @@ -190,6 +191,7 @@ public function analyse( $isResultCacheUsed, $changedProjectExtensionFilesOutsideOfAnalysedPaths, $processedFiles, + $resultCacheExisted, ); } diff --git a/src/Command/AnalyseCommand.php b/src/Command/AnalyseCommand.php index b874dff3183..1c0fe944f79 100644 --- a/src/Command/AnalyseCommand.php +++ b/src/Command/AnalyseCommand.php @@ -510,6 +510,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $analysisResult->isResultCacheUsed(), $analysisResult->getChangedProjectExtensionFilesOutsideOfAnalysedPaths(), $analysisResult->getProcessedFiles(), + $analysisResult->resultCacheExisted(), ); $exitCode = $errorFormatter->formatErrors($analysisResult, $inceptionResult->getStdOutput()); @@ -666,6 +667,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int } } + $this->reportMissingResultCacheInCi($errorOutput, $analysisResult, $onlyFiles); + $this->runDiagnoseExtensions($container, $inceptionResult->getErrorOutput(), $analysisResult->getProcessedFiles()); return $inceptionResult->handleReturn( @@ -675,6 +678,27 @@ protected function execute(InputInterface $input, OutputInterface $output): int ); } + private function reportMissingResultCacheInCi(Output $errorOutput, AnalysisResult $analysisResult, bool $onlyFiles): void + { + if ( + $onlyFiles + || $analysisResult->isResultCacheUsed() + || $analysisResult->resultCacheExisted() + ) { + return; + } + + if (!(new CiDetector())->isCiDetected()) { + return; + } + + $errorOutput->writeLineFormatted('Tip: This CI run analysed your whole project from scratch because there was no result cache.'); + $errorOutput->writeLineFormatted('Persist PHPStan\'s result cache directory between CI runs to make your pipeline dramatically faster.'); + $errorOutput->writeLineFormatted('Only changed files and their dependencies are re-analysed, so most runs finish in a fraction of the time.'); + $errorOutput->writeLineFormatted('Learn how to set it up: https://phpstan.org/user-guide/result-cache'); + $errorOutput->writeLineFormatted(''); + } + private function createStreamOutput(): StreamOutput { $resource = fopen('php://memory', 'w', false); diff --git a/src/Command/AnalysisResult.php b/src/Command/AnalysisResult.php index bdcb5fd1961..3a100dbc4de 100644 --- a/src/Command/AnalysisResult.php +++ b/src/Command/AnalysisResult.php @@ -39,6 +39,7 @@ public function __construct( private bool $isResultCacheUsed, private array $changedProjectExtensionFilesOutsideOfAnalysedPaths, private array $processedFiles = [], + private bool $resultCacheExisted = true, ) { usort( @@ -142,6 +143,16 @@ public function isResultCacheUsed(): bool return $this->isResultCacheUsed; } + /** + * Whether the result cache file existed when PHPStan started. + * False means the cache was never created; a stale/invalid cache that + * triggered a full re-analysis still counts as existing. + */ + public function resultCacheExisted(): bool + { + return $this->resultCacheExisted; + } + /** * @return array */ @@ -177,6 +188,7 @@ public function withFileSpecificErrors(array $fileSpecificErrors): self $this->isResultCacheUsed, $this->changedProjectExtensionFilesOutsideOfAnalysedPaths, $this->processedFiles, + $this->resultCacheExisted, ); } From dfab227100229fae56ed8327c46ab2d64f9a8f10 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Sat, 22 Aug 2026 07:19:57 +0000 Subject: [PATCH 2/2] Show the result cache CI notification only for slow analysis runs A full analysis that finishes quickly does not benefit enough from a persisted result cache to be worth nagging about, so require the run to take longer than RESULT_CACHE_CI_NOTIFICATION_ELAPSED_LIMIT (60 seconds). The E2E scenario gets a bootstrap file that sleeps past the limit when PHPSTAN_E2E_SLOW_ANALYSIS=1, so all three conditions are covered: a slow run without a cache notifies, a slow run with a cache does not, and a fast run without a cache does not either. --- .github/workflows/e2e-tests.yml | 16 ++++++++++++---- e2e/result-cache-ci-notification/bootstrap.php | 10 ++++++++++ e2e/result-cache-ci-notification/phpstan.neon | 2 ++ src/Command/AnalyseCommand.php | 14 +++++++++++++- 4 files changed, 37 insertions(+), 5 deletions(-) create mode 100644 e2e/result-cache-ci-notification/bootstrap.php diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index d9a9c44d9a3..e398bcafb15 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -526,14 +526,22 @@ jobs: - script: | cd e2e/result-cache-ci-notification # https://github.com/phpstan/phpstan/issues/14881 - # A full analysis in CI without a pre-existing result cache should hint - # the user to persist the result cache directory between runs. + # A slow full analysis in CI without a pre-existing result cache should + # hint the user to persist the result cache directory between runs. + # PHPSTAN_E2E_SLOW_ANALYSIS makes bootstrap.php sleep past the + # elapsed-time limit the notification requires. ../../bin/phpstan clear-result-cache - OUTPUT=$(../../bin/phpstan analyse 2>&1) + OUTPUT=$(PHPSTAN_E2E_SLOW_ANALYSIS=1 ../../bin/phpstan analyse 2>&1) echo "$OUTPUT" ../bashunit -a contains 'Persist PHPStan' "$OUTPUT" ../bashunit -a contains 'to make your pipeline dramatically faster' "$OUTPUT" - # The second run finds the cache present, so the hint is not shown again. + # The second run is just as slow, but finds the cache present, + # so the hint is not shown again. + OUTPUT=$(PHPSTAN_E2E_SLOW_ANALYSIS=1 ../../bin/phpstan analyse 2>&1) + echo "$OUTPUT" + ../bashunit -a not_contains 'to make your pipeline dramatically faster' "$OUTPUT" + # A fast run without any result cache does not show the hint either. + ../../bin/phpstan clear-result-cache OUTPUT=$(../../bin/phpstan analyse 2>&1) echo "$OUTPUT" ../bashunit -a not_contains 'to make your pipeline dramatically faster' "$OUTPUT" diff --git a/e2e/result-cache-ci-notification/bootstrap.php b/e2e/result-cache-ci-notification/bootstrap.php new file mode 100644 index 00000000000..513b95bc872 --- /dev/null +++ b/e2e/result-cache-ci-notification/bootstrap.php @@ -0,0 +1,10 @@ +analysisStartTime < self::RESULT_CACHE_CI_NOTIFICATION_ELAPSED_LIMIT) { + return; + } + if (!(new CiDetector())->isCiDetected()) { return; } - $errorOutput->writeLineFormatted('Tip: This CI run analysed your whole project from scratch because there was no result cache.'); + $errorOutput->writeLineFormatted('Tip: This CI run was slow because it analysed your whole project from scratch - there was no result cache.'); $errorOutput->writeLineFormatted('Persist PHPStan\'s result cache directory between CI runs to make your pipeline dramatically faster.'); $errorOutput->writeLineFormatted('Only changed files and their dependencies are re-analysed, so most runs finish in a fraction of the time.'); $errorOutput->writeLineFormatted('Learn how to set it up: https://phpstan.org/user-guide/result-cache');