1717use PHPStan \DependencyInjection \GenerateFactory ;
1818use PHPStan \DependencyInjection \ProjectConfigHelper ;
1919use PHPStan \File \CouldNotReadFileException ;
20- use PHPStan \File \CouldNotWriteFileException ;
2120use PHPStan \File \FileFinder ;
2221use PHPStan \File \FileHelper ;
22+ use PHPStan \File \FileReader ;
23+ use PHPStan \File \FileWriter ;
2324use PHPStan \Internal \ArrayHelper ;
2425use PHPStan \Internal \ComposerHelper ;
2526use PHPStan \PhpDoc \StubFilesProvider ;
3637use function array_unique ;
3738use function array_values ;
3839use function count ;
39- use function error_get_last ;
4040use function explode ;
41- use function fclose ;
42- use function fopen ;
43- use function fwrite ;
4441use function get_loaded_extensions ;
4542use function getenv ;
4643use function hash_file ;
5047use function is_file ;
5148use function ksort ;
5249use function microtime ;
50+ use function serialize ;
5351use function sort ;
5452use function sprintf ;
5553use function str_starts_with ;
54+ use function strlen ;
5655use function substr ;
5756use function time ;
5857use function unlink ;
59- use function var_export ;
58+ use function unserialize ;
6059use const PHP_VERSION_ID ;
6160
6261/**
@@ -69,6 +68,15 @@ final class ResultCacheManager
6968
7069 private const CACHE_VERSION = 'v13-packageDependencies ' ;
7170
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+
7280 /** @var array<string, string> */
7381 private array $ fileHashes = [];
7482
@@ -206,7 +214,16 @@ public function restore(array $allAnalysedFiles, bool $debug, bool $onlyFiles, ?
206214 }
207215
208216 try {
209- $ 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 );
210227 } catch (Throwable $ e ) {
211228 if ($ output ->isVeryVerbose ()) {
212229 $ output ->writeLineFormatted (sprintf ('Result cache not used because an error occurred while loading the cache file: %s ' , $ e ->getMessage ()));
@@ -402,12 +419,12 @@ public function restore(array $allAnalysedFiles, bool $debug, bool $onlyFiles, ?
402419 $ filesToAnalyse = [];
403420 $ invertedDependenciesToReturn = [];
404421 $ invertedUsedTraitDependenciesToReturn = [];
405- $ errors = $ data ['errorsCallback ' ]() ;
406- $ locallyIgnoredErrors = $ data ['locallyIgnoredErrorsCallback ' ]() ;
422+ $ errors = $ data ['errors ' ] ;
423+ $ locallyIgnoredErrors = $ data ['locallyIgnoredErrors ' ] ;
407424 $ linesToIgnore = $ data ['linesToIgnore ' ];
408425 $ unmatchedLineIgnores = $ data ['unmatchedLineIgnores ' ];
409- $ collectedData = $ data ['collectedDataCallback ' ]() ;
410- $ exportedNodes = $ data ['exportedNodesCallback ' ]() ;
426+ $ collectedData = $ data ['collectedData ' ] ;
427+ $ exportedNodes = $ data ['exportedNodes ' ] ;
411428 $ filteredErrors = [];
412429 $ filteredLocallyIgnoredErrors = [];
413430 $ filteredLinesToIgnore = [];
@@ -1158,89 +1175,22 @@ private function save(
11581175
11591176 $ file = $ this ->cacheFilePath ;
11601177
1161- // streamed to the file section by section - building the whole
1162- // var_export()ed contents in memory at once would take up roughly
1163- // twice the size of the resulting file in the main process
1164- $ handle = @fopen ($ file , 'w ' );
1165- if ($ handle === false ) {
1166- $ error = error_get_last ();
1167- throw new CouldNotWriteFileException ($ file , $ error !== null ? $ error ['message ' ] : 'unknown cause ' );
1168- }
1169-
1170- try {
1171- $ this ->writeToHandle ($ handle , $ file , "<?php declare(strict_types = 1);
1172-
1173- return [
1174- 'lastFullAnalysisTime' => " . var_export ($ lastFullAnalysisTime , true ) . ",
1175- 'meta' => " . var_export ($ meta , true ) . ",
1176- 'projectExtensionFiles' => " . var_export ($ projectExtensionFiles , true ) . ",
1177- 'errorsCallback' => static function (): array { return " );
1178- $ this ->streamArrayVarExportToHandle ($ handle , $ file , $ errors );
1179- $ this ->writeToHandle ($ handle , $ file , "; },
1180- 'locallyIgnoredErrorsCallback' => static function (): array { return " );
1181- $ this ->streamArrayVarExportToHandle ($ handle , $ file , $ locallyIgnoredErrors );
1182- $ this ->writeToHandle ($ handle , $ file , "; },
1183- 'linesToIgnore' => " );
1184- $ this ->streamArrayVarExportToHandle ($ handle , $ file , $ linesToIgnore );
1185- $ this ->writeToHandle ($ handle , $ file , ",
1186- 'unmatchedLineIgnores' => " );
1187- $ this ->streamArrayVarExportToHandle ($ handle , $ file , $ unmatchedLineIgnores );
1188- $ this ->writeToHandle ($ handle , $ file , ",
1189- 'collectedDataCallback' => static function (): array { return " );
1190- $ this ->streamArrayVarExportToHandle ($ handle , $ file , $ collectedData );
1191- $ this ->writeToHandle ($ handle , $ file , "; },
1192- 'dependencies' => " );
1193- $ this ->streamArrayVarExportToHandle ($ handle , $ file , $ invertedDependencies );
1194- $ this ->writeToHandle ($ handle , $ file , ",
1195- 'packageDependencies' => " );
1196- $ this ->streamArrayVarExportToHandle ($ handle , $ file , $ packageDependencies );
1197- $ this ->writeToHandle ($ handle , $ file , ",
1198- 'exportedNodesCallback' => static function (): array { return " );
1199- $ this ->streamArrayVarExportToHandle ($ handle , $ file , $ exportedNodes );
1200- $ this ->writeToHandle ($ handle , $ file , '; },
1201- ];
1202- ' );
1203- } finally {
1204- fclose ($ handle );
1205- }
1206- }
1207-
1208- /**
1209- * @param resource $handle
1210- */
1211- private function writeToHandle ($ handle , string $ file , string $ contents ): void
1212- {
1213- if (@fwrite ($ handle , $ contents ) === false ) {
1214- $ error = error_get_last ();
1215- throw new CouldNotWriteFileException ($ file , $ error !== null ? $ error ['message ' ] : 'unknown cause ' );
1216- }
1217- }
1218-
1219- /**
1220- * Streams the var_export() representation of an array to the file entry
1221- * by entry, producing output byte-identical to var_export($values, true).
1222- *
1223- * var_export() builds the whole export in memory even when told to print it,
1224- * so exporting a big section in one call would take up as much memory
1225- * as the resulting file section itself.
1226- *
1227- * Each entry is exported wrapped in a single-entry array whose "array (\n"
1228- * prefix and "\n)" suffix are stripped, yielding the same bytes (including
1229- * indentation) the entry would get inside the full export. Indenting the lines
1230- * of a standalone value export would corrupt multi-line string contents instead.
1231- *
1232- * @param resource $handle
1233- * @param array<mixed> $values
1234- */
1235- private function streamArrayVarExportToHandle ($ handle , string $ file , array $ values ): void
1236- {
1237- $ this ->writeToHandle ($ handle , $ file , 'array ( ' );
1238- foreach ($ values as $ key => $ value ) {
1239- $ entry = var_export ([$ key => $ value ], true );
1240- $ this ->writeToHandle ($ handle , $ file , "\n" . substr ($ entry , 8 , -2 ));
1241- }
1242-
1243- $ this ->writeToHandle ($ handle , $ file , "\n) " );
1178+ FileWriter::write (
1179+ $ file ,
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+ ]),
1193+ );
12441194 }
12451195
12461196 /**
0 commit comments