diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 99db5336ea1..dde586c3c03 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -1360,6 +1360,12 @@ jobs: echo "$OUTPUT" ../bashunit assert contains 'Composer packages changed (test/logger); re-analysing the files depending on them and the files with errors.' "$OUTPUT" ../bashunit assert contains 'Result cache restored. 1 file will be reanalysed.' "$OUTPUT" + - script: | + cd e2e/result-cache-deterministic-order + composer install + ../../bin/phpstan analyse + OUTPUT=$(php assert-sorted.php tmp/resultCache.php 2>&1) || { echo "$OUTPUT"; exit 1; } + echo "$OUTPUT" - script: | cd e2e/result-cache-path-repository composer install diff --git a/e2e/result-cache-deterministic-order/.gitignore b/e2e/result-cache-deterministic-order/.gitignore new file mode 100644 index 00000000000..de4a392c331 --- /dev/null +++ b/e2e/result-cache-deterministic-order/.gitignore @@ -0,0 +1,2 @@ +/vendor +/composer.lock diff --git a/e2e/result-cache-deterministic-order/assert-sorted.php b/e2e/result-cache-deterministic-order/assert-sorted.php new file mode 100644 index 00000000000..289805da0d3 --- /dev/null +++ b/e2e/result-cache-deterministic-order/assert-sorted.php @@ -0,0 +1,104 @@ +')); + exit(1); +} + +$handle = fopen($path, 'r'); +if ($handle === false) { + fwrite(STDERR, sprintf("Cannot open %s.\n", $path)); + exit(1); +} + +fgets($handle); // the PHP prefix line that keeps the file inert when included + +$unsorted = []; +$sections = 0; +$packageLists = []; +while (($header = fgets($handle)) !== false) { + $header = rtrim($header, "\n"); + if ($header === '') { + continue; + } + + $parts = explode(' ', $header, 2); + if (count($parts) !== 2) { + fwrite(STDERR, sprintf("Malformed frame header \"%s\".\n", $header)); + exit(1); + } + + [$name, $size] = $parts; + if (!str_ends_with($name, '*')) { + // a single value, or (with #) payloads described by the index frame before them + if ((int) $size > 0) { + fread($handle, (int) $size); + } + + continue; + } + + $name = substr($name, 0, -1); + $keys = []; + for ($i = 0; $i < (int) $size; $i++) { + [$keyLength, $valueLength] = array_map('intval', explode(' ', rtrim((string) fgets($handle), "\n"), 2)); + $key = unserialize((string) fread($handle, $keyLength)); + $value = unserialize((string) fread($handle, $valueLength)); + $keys[] = $key; + if ($name !== 'packageDependencies') { + continue; + } + + $packageLists[$key] = $value; + } + + $sections++; + $sorted = $keys; + sort($sorted); + if ($keys === $sorted) { + continue; + } + + $unsorted[$name] = $keys; +} + +fclose($handle); + +if ($sections === 0) { + fwrite(STDERR, "The result cache has no entry sections, so nothing was checked.\n"); + exit(1); +} + +if ($packageLists === []) { + fwrite(STDERR, "The result cache recorded no package dependencies, so nothing was checked.\n"); + exit(1); +} + +$problems = []; +foreach ($unsorted as $name => $keys) { + $problems[] = sprintf('Section "%s" is not in key order: %s', $name, implode(', ', array_map(static fn (int|string $key): string => basename((string) $key), $keys))); +} + +foreach ($packageLists as $file => $packages) { + $sortedPackages = $packages; + sort($sortedPackages, SORT_STRING); + if ($packages === $sortedPackages) { + continue; + } + + $problems[] = sprintf('The packages of %s are not in order: %s', basename($file), implode(', ', $packages)); +} + +if ($problems !== []) { + fwrite(STDERR, implode("\n", $problems) . "\n"); + exit(1); +} + +echo sprintf( + "All %d entry sections of the result cache are in key order, and so are the packages of all %d files that have package dependencies.\n", + $sections, + count($packageLists), +); diff --git a/e2e/result-cache-deterministic-order/composer.json b/e2e/result-cache-deterministic-order/composer.json new file mode 100644 index 00000000000..529d90d62ce --- /dev/null +++ b/e2e/result-cache-deterministic-order/composer.json @@ -0,0 +1,26 @@ +{ + "name": "phpstan/result-cache-deterministic-order-e2e", + "repositories": [ + { + "type": "path", + "url": "./logger", + "options": { + "symlink": false + } + }, + { + "type": "path", + "url": "./mailer", + "options": { + "symlink": false + } + } + ], + "require": { + "test/logger": "*", + "test/mailer": "*" + }, + "autoload": { + "classmap": ["src"] + } +} diff --git a/e2e/result-cache-deterministic-order/logger/composer.json b/e2e/result-cache-deterministic-order/logger/composer.json new file mode 100644 index 00000000000..8b33c945f93 --- /dev/null +++ b/e2e/result-cache-deterministic-order/logger/composer.json @@ -0,0 +1 @@ +{ "name": "test/logger", "version": "1.0.0", "autoload": { "classmap": ["src"] } } diff --git a/e2e/result-cache-deterministic-order/logger/src/Logger.php b/e2e/result-cache-deterministic-order/logger/src/Logger.php new file mode 100644 index 00000000000..fa37b4844fb --- /dev/null +++ b/e2e/result-cache-deterministic-order/logger/src/Logger.php @@ -0,0 +1,12 @@ +logger->info('hello'); + $this->mailer->send('hello'); + } + +} diff --git a/e2e/result-cache-deterministic-order/src/F2.php b/e2e/result-cache-deterministic-order/src/F2.php new file mode 100644 index 00000000000..d9710efa630 --- /dev/null +++ b/e2e/result-cache-deterministic-order/src/F2.php @@ -0,0 +1,22 @@ +logger->info('hello'); + $this->mailer->send('hello'); + } + +} diff --git a/e2e/result-cache-deterministic-order/src/F3.php b/e2e/result-cache-deterministic-order/src/F3.php new file mode 100644 index 00000000000..be311c5d8c9 --- /dev/null +++ b/e2e/result-cache-deterministic-order/src/F3.php @@ -0,0 +1,22 @@ +logger->info('hello'); + $this->mailer->send('hello'); + } + +} diff --git a/e2e/result-cache-deterministic-order/src/F4.php b/e2e/result-cache-deterministic-order/src/F4.php new file mode 100644 index 00000000000..9af9528da0d --- /dev/null +++ b/e2e/result-cache-deterministic-order/src/F4.php @@ -0,0 +1,22 @@ +logger->info('hello'); + $this->mailer->send('hello'); + } + +} diff --git a/e2e/result-cache-deterministic-order/tmp/.gitignore b/e2e/result-cache-deterministic-order/tmp/.gitignore new file mode 100644 index 00000000000..125e34294bf --- /dev/null +++ b/e2e/result-cache-deterministic-order/tmp/.gitignore @@ -0,0 +1,2 @@ +* +!.* diff --git a/src/Analyser/ResultCache/ResultCacheManager.php b/src/Analyser/ResultCache/ResultCacheManager.php index f43162ea10c..40a3310316a 100644 --- a/src/Analyser/ResultCache/ResultCacheManager.php +++ b/src/Analyser/ResultCache/ResultCacheManager.php @@ -2032,6 +2032,11 @@ private function save( ksort($exportedNodes); ksort($collectedDataRuleResults); + ksort($packageDependencies); + foreach ($packageDependencies as $file => $packages) { + sort($packages); + $packageDependencies[$file] = $packages; + } // The only point where the StubFilesExtensions may run: the analysis is over, bootstrapFiles // have been executed, so the extensions can rely on them. restore() reads these hashes back