diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 6a8715514db..3a905bafc74 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -379,6 +379,40 @@ jobs: OUTPUT=$(../bashunit -a exit_code "1" "../../bin/phpstan analyse -vv --error-format raw") echo "$OUTPUT" ../bashunit -a contains 'ClassUsingDefine.php:13:Method ResultCacheE2EDefine\ClassUsingDefine::getMode() should return int<1, 3> but returns 5. [identifier=return.type]' "$OUTPUT" + - script: | + cd e2e/result-cache-composer-lock + composer install + ../../bin/phpstan analyse + # Change composer.lock without changing any installed package version/reference + # (vendor/composer/installed.php is left untouched), like a regenerated lock in CI + # when composer.lock is not committed. The result cache must be kept, not discarded. + jq '.["content-hash"]="0000000000000000000000000000000000000000"' composer.lock > composer.lock.new + mv composer.lock.new composer.lock + OUTPUT=$(../bashunit -a exit_code "0" "../../bin/phpstan analyse -vv") + echo "$OUTPUT" + ../bashunit -a contains 'Composer metadata changed but no package versions changed; keeping the result cache.' "$OUTPUT" + ../bashunit -a contains 'Result cache restored. 0 files will be reanalysed.' "$OUTPUT" + # Change the content hash again AND edit a single source file: the cache is still kept + # (no package changed), and only the one changed file is reanalysed. + printf '\n' >> src/Foo.php + jq '.["content-hash"]="1111111111111111111111111111111111111111"' composer.lock > composer.lock.new + mv composer.lock.new composer.lock + OUTPUT=$(../bashunit -a exit_code "0" "../../bin/phpstan analyse -vv") + echo "$OUTPUT" + ../bashunit -a contains 'Composer metadata changed but no package versions changed; keeping the result cache.' "$OUTPUT" + ../bashunit -a contains 'Result cache restored. 1 file will be reanalysed.' "$OUTPUT" + - script: | + cd e2e/result-cache-package-update + composer install + ../../bin/phpstan analyse + # Simulate a single Composer package being updated by changing its recorded reference in + # vendor/composer/installed.php. The cache must be invalidated only for files depending on + # that package (src/Foo.php uses psr/log), not for the whole project. + php -r '$f = "vendor/composer/installed.php"; $d = require $f; $d["versions"]["psr/log"]["reference"] = "0000000000000000000000000000000000000000"; file_put_contents($f, "logger->info('hello'); + } + +} diff --git a/e2e/result-cache-package-update/tmp/.gitignore b/e2e/result-cache-package-update/tmp/.gitignore new file mode 100644 index 00000000000..125e34294bf --- /dev/null +++ b/e2e/result-cache-package-update/tmp/.gitignore @@ -0,0 +1,2 @@ +* +!.* diff --git a/src/Analyser/ResultCache/ResultCacheManager.php b/src/Analyser/ResultCache/ResultCacheManager.php index 05062d05261..b603780145a 100644 --- a/src/Analyser/ResultCache/ResultCacheManager.php +++ b/src/Analyser/ResultCache/ResultCacheManager.php @@ -268,12 +268,13 @@ public function restore(array $allAnalysedFiles, bool $debug, bool $onlyFiles, ? // and analysis are unchanged except for code coming from packages whose version actually // changed. Re-analyse just the files depending on a changed package instead of everything; // the existing incremental loop below then propagates to their dependents on signature change. - // Any other meta difference, or an undetermined change set, falls back to a full re-analysis. + // Any other meta difference, or an undetermined change set (installed.php cannot be parsed), + // falls back to a full re-analysis. $changedPackages = array_diff($diffs, ['composerLocks', 'composerInstalled']) === [] ? $this->packageDependencyResolver->getChangedComposerPackages($data['meta'], $meta) : null; - if ($changedPackages === null || $changedPackages === []) { + if ($changedPackages === null) { if ($output->isVeryVerbose()) { $output->writeLineFormatted('Result cache not used because the metadata do not match: ' . implode(', ', $diffs)); } @@ -296,18 +297,29 @@ public function restore(array $allAnalysedFiles, bool $debug, bool $onlyFiles, ? ); } - if ($output->isVeryVerbose()) { - $output->writeLineFormatted(sprintf( - 'Composer packages changed (%s); re-analysing only the files depending on them.', - implode(', ', $changedPackages), - )); - } - $changedPackagesLookup = array_fill_keys($changedPackages, true); - foreach ($packageDependencies as $packageDependentFile => $filePackages) { - foreach ($filePackages as $filePackage) { - if (isset($changedPackagesLookup[$filePackage])) { - $packageSeededFiles[] = $packageDependentFile; - break; + if ($changedPackages === []) { + // The Composer lock/installed metadata changed but no installed package's version or + // reference did (e.g. a composer.lock regenerated with different formatting or dist/time + // metadata, common in CI where composer.lock is not committed). Nothing analysis-relevant + // changed, so keep the restored cache and fall through to the normal incremental analysis + // instead of re-analysing everything. + if ($output->isVeryVerbose()) { + $output->writeLineFormatted('Composer metadata changed but no package versions changed; keeping the result cache.'); + } + } else { + if ($output->isVeryVerbose()) { + $output->writeLineFormatted(sprintf( + 'Composer packages changed (%s); re-analysing only the files depending on them.', + implode(', ', $changedPackages), + )); + } + $changedPackagesLookup = array_fill_keys($changedPackages, true); + foreach ($packageDependencies as $packageDependentFile => $filePackages) { + foreach ($filePackages as $filePackage) { + if (isset($changedPackagesLookup[$filePackage])) { + $packageSeededFiles[] = $packageDependentFile; + break; + } } } }