Skip to content

Commit ee5f26c

Browse files
SanderMullerclaude
andcommitted
Store the result cache with serialize() instead of a var_export'd PHP file
The result cache was written as a var_export'd PHP file and hydrated with include. Including a multi-megabyte PHP source retains its compiled op_arrays and interned strings for the process lifetime, and building the var_export string concatenates the whole file in memory on save. unserialize() produces only the values, and the retained compiled-code cost disappears. The errorsCallback/collectedDataCallback/exportedNodesCallback closures existed to embed object graphs in the PHP file; restore() invoked all of them unconditionally right after the include, so plain arrays are equivalent. A cache file in the old PHP format fails to unserialize and is discarded like any other corrupted cache file (unlink and full analysis). The serialized payload is prefixed with '<?php return; ?>' so that an older PHPStan including the new-format file returns null immediately instead of echoing megabytes of inline text to stdout, and then discards it the same way. The format transition therefore needs no cache version bump in either direction. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent d62655c commit ee5f26c

1 file changed

Lines changed: 41 additions & 22 deletions

File tree

src/Analyser/ResultCache/ResultCacheManager.php

Lines changed: 41 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
use PHPStan\File\CouldNotReadFileException;
2020
use PHPStan\File\FileFinder;
2121
use PHPStan\File\FileHelper;
22+
use PHPStan\File\FileReader;
2223
use PHPStan\File\FileWriter;
2324
use PHPStan\Internal\ArrayHelper;
2425
use PHPStan\Internal\ComposerHelper;
@@ -47,11 +48,14 @@
4748
use function ksort;
4849
use function microtime;
4950
use function sort;
51+
use function serialize;
5052
use function sprintf;
5153
use function str_starts_with;
54+
use function strlen;
55+
use function substr;
5256
use function time;
5357
use function unlink;
54-
use function var_export;
58+
use function unserialize;
5559
use const PHP_VERSION_ID;
5660

5761
/**
@@ -64,6 +68,15 @@ final class ResultCacheManager
6468

6569
private const CACHE_VERSION = 'v13-packageDependencies';
6670

71+
/**
72+
* The cache file is serialize() output, but an older PHPStan reading it would
73+
* include it as PHP and echo the whole multi-megabyte content to stdout as
74+
* inline text before discarding it. This prefix makes such an include return
75+
* null immediately (the text after ?> is never reached), so a downgrade
76+
* degrades to a silent full analysis instead.
77+
*/
78+
private const SERIALIZED_FILE_PREFIX = '<?php return; ?>';
79+
6780
/** @var array<string, string> */
6881
private array $fileHashes = [];
6982

@@ -201,7 +214,16 @@ public function restore(array $allAnalysedFiles, bool $debug, bool $onlyFiles, ?
201214
}
202215

203216
try {
204-
$data = require $cacheFilePath;
217+
// The cache used to be a var_export'd PHP file loaded via include. Including a
218+
// multi-megabyte PHP source retains its compiled op_arrays and interned strings
219+
// for the process lifetime; unserialize() produces only the values. A cache file
220+
// in the old PHP format fails to unserialize and is discarded below like any
221+
// other corrupted file, so no cache version bump is needed for the transition.
222+
$contents = FileReader::read($cacheFilePath);
223+
if (str_starts_with($contents, self::SERIALIZED_FILE_PREFIX)) {
224+
$contents = substr($contents, strlen(self::SERIALIZED_FILE_PREFIX));
225+
}
226+
$data = @unserialize($contents);
205227
} catch (Throwable $e) {
206228
if ($output->isVeryVerbose()) {
207229
$output->writeLineFormatted(sprintf('Result cache not used because an error occurred while loading the cache file: %s', $e->getMessage()));
@@ -397,12 +419,12 @@ public function restore(array $allAnalysedFiles, bool $debug, bool $onlyFiles, ?
397419
$filesToAnalyse = [];
398420
$invertedDependenciesToReturn = [];
399421
$invertedUsedTraitDependenciesToReturn = [];
400-
$errors = $data['errorsCallback']();
401-
$locallyIgnoredErrors = $data['locallyIgnoredErrorsCallback']();
422+
$errors = $data['errors'];
423+
$locallyIgnoredErrors = $data['locallyIgnoredErrors'];
402424
$linesToIgnore = $data['linesToIgnore'];
403425
$unmatchedLineIgnores = $data['unmatchedLineIgnores'];
404-
$collectedData = $data['collectedDataCallback']();
405-
$exportedNodes = $data['exportedNodesCallback']();
426+
$collectedData = $data['collectedData'];
427+
$exportedNodes = $data['exportedNodes'];
406428
$filteredErrors = [];
407429
$filteredLocallyIgnoredErrors = [];
408430
$filteredLinesToIgnore = [];
@@ -1155,22 +1177,19 @@ private function save(
11551177

11561178
FileWriter::write(
11571179
$file,
1158-
"<?php declare(strict_types = 1);
1159-
1160-
return [
1161-
'lastFullAnalysisTime' => " . var_export($lastFullAnalysisTime, true) . ",
1162-
'meta' => " . var_export($meta, true) . ",
1163-
'projectExtensionFiles' => " . var_export($projectExtensionFiles, true) . ",
1164-
'errorsCallback' => static function (): array { return " . var_export($errors, true) . "; },
1165-
'locallyIgnoredErrorsCallback' => static function (): array { return " . var_export($locallyIgnoredErrors, true) . "; },
1166-
'linesToIgnore' => " . var_export($linesToIgnore, true) . ",
1167-
'unmatchedLineIgnores' => " . var_export($unmatchedLineIgnores, true) . ",
1168-
'collectedDataCallback' => static function (): array { return " . var_export($collectedData, true) . "; },
1169-
'dependencies' => " . var_export($invertedDependencies, true) . ",
1170-
'packageDependencies' => " . var_export($packageDependencies, true) . ",
1171-
'exportedNodesCallback' => static function (): array { return " . var_export($exportedNodes, true) . '; },
1172-
];
1173-
',
1180+
self::SERIALIZED_FILE_PREFIX . serialize([
1181+
'lastFullAnalysisTime' => $lastFullAnalysisTime,
1182+
'meta' => $meta,
1183+
'projectExtensionFiles' => $projectExtensionFiles,
1184+
'errors' => $errors,
1185+
'locallyIgnoredErrors' => $locallyIgnoredErrors,
1186+
'linesToIgnore' => $linesToIgnore,
1187+
'unmatchedLineIgnores' => $unmatchedLineIgnores,
1188+
'collectedData' => $collectedData,
1189+
'dependencies' => $invertedDependencies,
1190+
'packageDependencies' => $packageDependencies,
1191+
'exportedNodes' => $exportedNodes,
1192+
]),
11741193
);
11751194
}
11761195

0 commit comments

Comments
 (0)