1212use PHPStan \DependencyInjection \ProjectConfigHelper ;
1313use PHPStan \File \CouldNotReadFileException ;
1414use PHPStan \File \FileFinder ;
15+ use PHPStan \File \FileHelper ;
1516use PHPStan \File \FileWriter ;
1617use PHPStan \Internal \ComposerHelper ;
1718use PHPStan \PhpDoc \StubFilesProvider ;
3435use function sha1_file ;
3536use function sort ;
3637use function sprintf ;
38+ use function str_starts_with ;
3739use function time ;
3840use function unlink ;
3941use function var_export ;
@@ -62,6 +64,7 @@ public function __construct(
6264 private FileFinder $ scanFileFinder ,
6365 private ReflectionProvider $ reflectionProvider ,
6466 private StubFilesProvider $ stubFilesProvider ,
67+ private FileHelper $ fileHelper ,
6568 private string $ cacheFilePath ,
6669 private array $ analysedPaths ,
6770 private array $ composerAutoloaderProjectPaths ,
@@ -85,21 +88,21 @@ public function restore(array $allAnalysedFiles, bool $debug, bool $onlyFiles, ?
8588 if ($ output ->isDebug ()) {
8689 $ output ->writeLineFormatted ('Result cache not used because of debug mode. ' );
8790 }
88- return new ResultCache ($ allAnalysedFiles , true , time (), $ this ->getMeta ($ allAnalysedFiles , $ projectConfigArray ), [], [], [], []);
91+ return new ResultCache ($ allAnalysedFiles , true , time (), $ this ->getMeta ($ allAnalysedFiles , $ projectConfigArray ), [], [], [], [], [] );
8992 }
9093 if ($ onlyFiles ) {
9194 if ($ output ->isDebug ()) {
9295 $ output ->writeLineFormatted ('Result cache not used because only files were passed as analysed paths. ' );
9396 }
94- return new ResultCache ($ allAnalysedFiles , true , time (), $ this ->getMeta ($ allAnalysedFiles , $ projectConfigArray ), [], [], [], []);
97+ return new ResultCache ($ allAnalysedFiles , true , time (), $ this ->getMeta ($ allAnalysedFiles , $ projectConfigArray ), [], [], [], [], [] );
9598 }
9699
97100 $ cacheFilePath = $ this ->cacheFilePath ;
98101 if (!is_file ($ cacheFilePath )) {
99102 if ($ output ->isDebug ()) {
100103 $ output ->writeLineFormatted ('Result cache not used because the cache file does not exist. ' );
101104 }
102- return new ResultCache ($ allAnalysedFiles , true , time (), $ this ->getMeta ($ allAnalysedFiles , $ projectConfigArray ), [], [], [], []);
105+ return new ResultCache ($ allAnalysedFiles , true , time (), $ this ->getMeta ($ allAnalysedFiles , $ projectConfigArray ), [], [], [], [], [] );
103106 }
104107
105108 try {
@@ -111,7 +114,7 @@ public function restore(array $allAnalysedFiles, bool $debug, bool $onlyFiles, ?
111114
112115 @unlink ($ cacheFilePath );
113116
114- return new ResultCache ($ allAnalysedFiles , true , time (), $ this ->getMeta ($ allAnalysedFiles , $ projectConfigArray ), [], [], [], []);
117+ return new ResultCache ($ allAnalysedFiles , true , time (), $ this ->getMeta ($ allAnalysedFiles , $ projectConfigArray ), [], [], [], [], [] );
115118 }
116119
117120 if (!is_array ($ data )) {
@@ -120,7 +123,7 @@ public function restore(array $allAnalysedFiles, bool $debug, bool $onlyFiles, ?
120123 $ output ->writeLineFormatted ('Result cache not used because the cache file is corrupted. ' );
121124 }
122125
123- return new ResultCache ($ allAnalysedFiles , true , time (), $ this ->getMeta ($ allAnalysedFiles , $ projectConfigArray ), [], [], [], []);
126+ return new ResultCache ($ allAnalysedFiles , true , time (), $ this ->getMeta ($ allAnalysedFiles , $ projectConfigArray ), [], [], [], [], [] );
124127 }
125128
126129 $ meta = $ this ->getMeta ($ allAnalysedFiles , $ projectConfigArray );
@@ -129,23 +132,30 @@ public function restore(array $allAnalysedFiles, bool $debug, bool $onlyFiles, ?
129132 $ diffs = $ this ->getMetaKeyDifferences ($ data ['meta ' ], $ meta );
130133 $ output ->writeLineFormatted ('Result cache not used because the metadata do not match: ' . implode (', ' , $ diffs ));
131134 }
132- return new ResultCache ($ allAnalysedFiles , true , time (), $ meta , [], [], [], []);
135+ return new ResultCache ($ allAnalysedFiles , true , time (), $ meta , [], [], [], [], [] );
133136 }
134137
135138 if (time () - $ data ['lastFullAnalysisTime ' ] >= 60 * 60 * 24 * 7 ) {
136139 if ($ output ->isDebug ()) {
137140 $ output ->writeLineFormatted ('Result cache not used because it \'s more than 7 days since last full analysis. ' );
138141 }
139142 // run full analysis if the result cache is older than 7 days
140- return new ResultCache ($ allAnalysedFiles , true , time (), $ meta , [], [], [], []);
143+ return new ResultCache ($ allAnalysedFiles , true , time (), $ meta , [], [], [], [], [] );
141144 }
142145
143- foreach ($ data ['projectExtensionFiles ' ] as $ extensionFile => $ fileHash ) {
146+ /**
147+ * @var string $fileHash
148+ * @var bool $isAnalysed
149+ */
150+ foreach ($ data ['projectExtensionFiles ' ] as $ extensionFile => [$ fileHash , $ isAnalysed ]) {
151+ if (!$ isAnalysed ) {
152+ continue ;
153+ }
144154 if (!is_file ($ extensionFile )) {
145155 if ($ output ->isDebug ()) {
146156 $ output ->writeLineFormatted (sprintf ('Result cache not used because extension file %s was not found. ' , $ extensionFile ));
147157 }
148- return new ResultCache ($ allAnalysedFiles , true , time (), $ meta , [], [], [], []);
158+ return new ResultCache ($ allAnalysedFiles , true , time (), $ meta , [], [], [], [], [] );
149159 }
150160
151161 if ($ this ->getFileHash ($ extensionFile ) === $ fileHash ) {
@@ -156,7 +166,7 @@ public function restore(array $allAnalysedFiles, bool $debug, bool $onlyFiles, ?
156166 $ output ->writeLineFormatted (sprintf ('Result cache not used because extension file %s hash does not match. ' , $ extensionFile ));
157167 }
158168
159- return new ResultCache ($ allAnalysedFiles , true , time (), $ meta , [], [], [], []);
169+ return new ResultCache ($ allAnalysedFiles , true , time (), $ meta , [], [], [], [], [] );
160170 }
161171
162172 $ invertedDependencies = $ data ['dependencies ' ];
@@ -252,7 +262,7 @@ public function restore(array $allAnalysedFiles, bool $debug, bool $onlyFiles, ?
252262 }
253263 }
254264
255- return new ResultCache (array_unique ($ filesToAnalyse ), false , $ data ['lastFullAnalysisTime ' ], $ meta , $ filteredErrors , $ filteredCollectedData , $ invertedDependenciesToReturn , $ filteredExportedNodes );
265+ return new ResultCache (array_unique ($ filesToAnalyse ), false , $ data ['lastFullAnalysisTime ' ], $ meta , $ filteredErrors , $ filteredCollectedData , $ invertedDependenciesToReturn , $ filteredExportedNodes, $ data [ ' projectExtensionFiles ' ] );
256266 }
257267
258268 /**
@@ -355,7 +365,11 @@ public function process(AnalyserResult $analyserResult, ResultCache $resultCache
355365 }
356366
357367 $ meta = $ resultCache ->getMeta ();
358- $ doSave = function (array $ errorsByFile , $ collectedDataByFile , ?array $ dependencies , array $ exportedNodes ) use ($ internalErrors , $ resultCache , $ output , $ onlyFiles , $ meta ): bool {
368+ $ projectConfigArray = $ meta ['projectConfig ' ];
369+ if ($ projectConfigArray !== null ) {
370+ $ meta ['projectConfig ' ] = Neon::encode ($ projectConfigArray );
371+ }
372+ $ doSave = function (array $ errorsByFile , $ collectedDataByFile , ?array $ dependencies , array $ exportedNodes , array $ projectExtensionFiles ) use ($ internalErrors , $ resultCache , $ output , $ onlyFiles , $ meta ): bool {
359373 if ($ onlyFiles ) {
360374 if ($ output ->isDebug ()) {
361375 $ output ->writeLineFormatted ('Result cache was not saved because only files were passed as analysed paths. ' );
@@ -390,7 +404,7 @@ public function process(AnalyserResult $analyserResult, ResultCache $resultCache
390404 }
391405 }
392406
393- $ this ->save ($ resultCache ->getLastFullAnalysisTime (), $ errorsByFile , $ collectedDataByFile , $ dependencies , $ exportedNodes , $ meta );
407+ $ this ->save ($ resultCache ->getLastFullAnalysisTime (), $ errorsByFile , $ collectedDataByFile , $ dependencies , $ exportedNodes , $ projectExtensionFiles , $ meta );
394408
395409 if ($ output ->isDebug ()) {
396410 $ output ->writeLineFormatted ('Result cache is saved. ' );
@@ -402,7 +416,11 @@ public function process(AnalyserResult $analyserResult, ResultCache $resultCache
402416 if ($ resultCache ->isFullAnalysis ()) {
403417 $ saved = false ;
404418 if ($ save !== false ) {
405- $ saved = $ doSave ($ freshErrorsByFile , $ freshCollectedDataByFile , $ analyserResult ->getDependencies (), $ analyserResult ->getExportedNodes ());
419+ $ projectExtensionFiles = [];
420+ if ($ analyserResult ->getDependencies () !== null ) {
421+ $ projectExtensionFiles = $ this ->getProjectExtensionFiles ($ projectConfigArray , $ analyserResult ->getDependencies ());
422+ }
423+ $ saved = $ doSave ($ freshErrorsByFile , $ freshCollectedDataByFile , $ analyserResult ->getDependencies (), $ analyserResult ->getExportedNodes (), $ projectExtensionFiles );
406424 } else {
407425 if ($ output ->isDebug ()) {
408426 $ output ->writeLineFormatted ('Result cache was not saved because it was not requested. ' );
@@ -419,7 +437,27 @@ public function process(AnalyserResult $analyserResult, ResultCache $resultCache
419437
420438 $ saved = false ;
421439 if ($ save !== false ) {
422- $ saved = $ doSave ($ errorsByFile , $ collectedDataByFile , $ dependencies , $ exportedNodes );
440+ $ projectExtensionFiles = [];
441+ foreach ($ resultCache ->getProjectExtensionFiles () as $ file => [$ hash , $ isAnalysed , $ className ]) {
442+ if ($ isAnalysed ) {
443+ continue ;
444+ }
445+
446+ // keep the same file hashes from the old run
447+ // so that the message "When you edit them and re-run PHPStan, the result cache will get stale."
448+ // keeps being shown on subsequent runs
449+ $ projectExtensionFiles [$ file ] = [$ hash , false , $ className ];
450+ }
451+ if ($ dependencies !== null ) {
452+ foreach ($ this ->getProjectExtensionFiles ($ projectConfigArray , $ dependencies ) as $ file => [$ hash , $ isAnalysed , $ className ]) {
453+ if (!$ isAnalysed ) {
454+ continue ;
455+ }
456+
457+ $ projectExtensionFiles [$ file ] = [$ hash , true , $ className ];
458+ }
459+ }
460+ $ saved = $ doSave ($ errorsByFile , $ collectedDataByFile , $ dependencies , $ exportedNodes , $ projectExtensionFiles );
423461 }
424462
425463 $ flatErrors = [];
@@ -548,6 +586,7 @@ private function mergeExportedNodes(ResultCache $resultCache, array $freshExport
548586 * @param array<string, array<CollectedData>> $collectedData
549587 * @param array<string, array<string>> $dependencies
550588 * @param array<string, array<RootExportedNode>> $exportedNodes
589+ * @param array<string, array{string, bool, string}> $projectExtensionFiles
551590 * @param mixed[] $meta
552591 */
553592 private function save (
@@ -556,6 +595,7 @@ private function save(
556595 array $ collectedData ,
557596 array $ dependencies ,
558597 array $ exportedNodes ,
598+ array $ projectExtensionFiles ,
559599 array $ meta ,
560600 ): void
561601 {
@@ -602,10 +642,6 @@ private function save(
602642 ksort ($ exportedNodes );
603643
604644 $ file = $ this ->cacheFilePath ;
605- $ projectConfigArray = $ meta ['projectConfig ' ];
606- if ($ projectConfigArray !== null ) {
607- $ meta ['projectConfig ' ] = Neon::encode ($ projectConfigArray );
608- }
609645
610646 FileWriter::write (
611647 $ file ,
@@ -614,7 +650,7 @@ private function save(
614650return [
615651 'lastFullAnalysisTime' => " . var_export ($ lastFullAnalysisTime , true ) . ",
616652 'meta' => " . var_export ($ meta , true ) . ",
617- 'projectExtensionFiles' => " . var_export ($ this -> getProjectExtensionFiles ( $ projectConfigArray , $ dependencies ) , true ) . ",
653+ 'projectExtensionFiles' => " . var_export ($ projectExtensionFiles , true ) . ",
618654 'errorsCallback' => static function (): array { return " . var_export ($ errors , true ) . "; },
619655 'collectedDataCallback' => static function (): array { return " . var_export ($ collectedData , true ) . "; },
620656 'dependencies' => " . var_export ($ invertedDependencies , true ) . ",
@@ -627,13 +663,23 @@ private function save(
627663 /**
628664 * @param mixed[]|null $projectConfig
629665 * @param array<string, mixed> $dependencies
630- * @return array<string, string>
666+ * @return array<string, array{ string, bool, string} >
631667 */
632668 private function getProjectExtensionFiles (?array $ projectConfig , array $ dependencies ): array
633669 {
634670 $ this ->alreadyProcessed = [];
635671 $ projectExtensionFiles = [];
636672 if ($ projectConfig !== null ) {
673+ $ vendorDirs = [];
674+ foreach ($ this ->composerAutoloaderProjectPaths as $ autoloaderProjectPath ) {
675+ $ composer = ComposerHelper::getComposerConfig ($ autoloaderProjectPath );
676+ if ($ composer === null ) {
677+ continue ;
678+ }
679+ $ vendorDirectory = ComposerHelper::getVendorDirFromComposerConfig ($ autoloaderProjectPath , $ composer );
680+ $ vendorDirs [] = $ this ->fileHelper ->normalizePath ($ vendorDirectory );
681+ }
682+
637683 $ classes = ProjectConfigHelper::getServiceClassNames ($ projectConfig );
638684 foreach ($ classes as $ class ) {
639685 if (!$ this ->reflectionProvider ->hasClass ($ class )) {
@@ -647,12 +693,23 @@ private function getProjectExtensionFiles(?array $projectConfig, array $dependen
647693 }
648694
649695 $ allServiceFiles = $ this ->getAllDependencies ($ fileName , $ dependencies );
696+ if (count ($ allServiceFiles ) === 0 ) {
697+ $ normalizedFileName = $ this ->fileHelper ->normalizePath ($ fileName );
698+ foreach ($ vendorDirs as $ vendorDir ) {
699+ if (str_starts_with ($ normalizedFileName , $ vendorDir )) {
700+ continue 2 ;
701+ }
702+ }
703+ $ projectExtensionFiles [$ fileName ] = [$ this ->getFileHash ($ fileName ), false , $ class ];
704+ continue ;
705+ }
706+
650707 foreach ($ allServiceFiles as $ serviceFile ) {
651708 if (array_key_exists ($ serviceFile , $ projectExtensionFiles )) {
652709 continue ;
653710 }
654711
655- $ projectExtensionFiles [$ serviceFile ] = $ this ->getFileHash ($ serviceFile );
712+ $ projectExtensionFiles [$ serviceFile ] = [ $ this ->getFileHash ($ serviceFile ), true , $ class ] ;
656713 }
657714 }
658715 }
0 commit comments