@@ -356,6 +356,22 @@ public function restore(array $allAnalysedFiles, bool $debug, bool $onlyFiles, ?
356356 $ filesToAnalyse = [];
357357 $ invertedDependenciesToReturn = [];
358358 $ invertedUsedTraitDependenciesToReturn = [];
359+
360+ // Check external file dependencies for incremental re-analysis
361+ $ cachedExternalDependencies = $ data ['externalDependencies ' ] ?? [];
362+ $ externalDependenciesToReturn = $ cachedExternalDependencies ;
363+ foreach ($ cachedExternalDependencies as $ externalFile => $ externalData ) {
364+ if (!is_file ($ externalFile ) || $ this ->getFileHash ($ externalFile ) !== $ externalData ['fileHash ' ]) {
365+ if ($ output ->isVeryVerbose ()) {
366+ $ output ->writeLineFormatted (sprintf ('External file %s changed, re-analysing dependent files. ' , $ externalFile ));
367+ }
368+ foreach ($ externalData ['dependentFiles ' ] as $ dependentFile ) {
369+ if (is_file ($ dependentFile )) {
370+ $ filesToAnalyse [] = $ dependentFile ;
371+ }
372+ }
373+ }
374+ }
359375 $ errors = $ data ['errorsCallback ' ]();
360376 $ locallyIgnoredErrors = $ data ['locallyIgnoredErrorsCallback ' ]();
361377 $ linesToIgnore = $ data ['linesToIgnore ' ];
@@ -515,6 +531,7 @@ public function restore(array $allAnalysedFiles, bool $debug, bool $onlyFiles, ?
515531 exportedNodes: $ filteredExportedNodes ,
516532 projectExtensionFiles: $ data ['projectExtensionFiles ' ],
517533 currentFileHashes: $ currentFileHashes ,
534+ externalFileDependencies: $ externalDependenciesToReturn ,
518535 );
519536 }
520537
@@ -624,7 +641,7 @@ public function process(AnalyserResult $analyserResult, ResultCache $resultCache
624641 if ($ projectConfigArray !== null ) {
625642 $ meta ['projectConfig ' ] = Neon::encode ($ projectConfigArray );
626643 }
627- $ doSave = function (array $ errorsByFile , $ locallyIgnoredErrorsByFile , $ linesToIgnore , $ unmatchedLineIgnores , $ collectedDataByFile , ?array $ dependencies , ?array $ usedTraitDependencies , array $ exportedNodes , array $ projectExtensionFiles ) use ($ internalErrors , $ resultCache , $ output , $ onlyFiles , $ meta ): bool {
644+ $ doSave = function (array $ errorsByFile , $ locallyIgnoredErrorsByFile , $ linesToIgnore , $ unmatchedLineIgnores , $ collectedDataByFile , ?array $ dependencies , ?array $ usedTraitDependencies , array $ exportedNodes , array $ projectExtensionFiles, array $ externalFileDependencies = [] ) use ($ internalErrors , $ resultCache , $ output , $ onlyFiles , $ meta ): bool {
628645 if ($ onlyFiles ) {
629646 if ($ output ->isVeryVerbose ()) {
630647 $ output ->writeLineFormatted ('Result cache was not saved because only files were passed as analysed paths. ' );
@@ -672,7 +689,7 @@ public function process(AnalyserResult $analyserResult, ResultCache $resultCache
672689 }
673690 }
674691
675- $ this ->save ($ resultCache ->getLastFullAnalysisTime (), $ errorsByFile , $ locallyIgnoredErrorsByFile , $ linesToIgnore , $ unmatchedLineIgnores , $ collectedDataByFile , $ dependencies , $ usedTraitDependencies , $ exportedNodes , $ projectExtensionFiles , $ resultCache ->getCurrentFileHashes (), $ meta );
692+ $ this ->save ($ resultCache ->getLastFullAnalysisTime (), $ errorsByFile , $ locallyIgnoredErrorsByFile , $ linesToIgnore , $ unmatchedLineIgnores , $ collectedDataByFile , $ dependencies , $ usedTraitDependencies , $ exportedNodes , $ projectExtensionFiles , $ resultCache ->getCurrentFileHashes (), $ meta, $ externalFileDependencies );
676693
677694 if ($ output ->isVeryVerbose ()) {
678695 $ output ->writeLineFormatted ('Result cache is saved. ' );
@@ -688,7 +705,7 @@ public function process(AnalyserResult $analyserResult, ResultCache $resultCache
688705 if ($ analyserResult ->getDependencies () !== null ) {
689706 $ projectExtensionFiles = $ this ->getProjectExtensionFiles ($ projectConfigArray , $ analyserResult ->getDependencies ());
690707 }
691- $ saved = $ doSave ($ freshErrorsByFile , $ freshLocallyIgnoredErrorsByFile , $ analyserResult ->getLinesToIgnore (), $ analyserResult ->getUnmatchedLineIgnores (), $ freshCollectedDataByFile , $ analyserResult ->getDependencies (), $ analyserResult ->getUsedTraitDependencies (), $ analyserResult ->getExportedNodes (), $ projectExtensionFiles );
708+ $ saved = $ doSave ($ freshErrorsByFile , $ freshLocallyIgnoredErrorsByFile , $ analyserResult ->getLinesToIgnore (), $ analyserResult ->getUnmatchedLineIgnores (), $ freshCollectedDataByFile , $ analyserResult ->getDependencies (), $ analyserResult ->getUsedTraitDependencies (), $ analyserResult ->getExportedNodes (), $ projectExtensionFiles, $ analyserResult -> getExternalFileDependencies () ?? [] );
692709 } else {
693710 if ($ output ->isVeryVerbose ()) {
694711 $ output ->writeLineFormatted ('Result cache was not saved because it was not requested. ' );
@@ -706,6 +723,7 @@ public function process(AnalyserResult $analyserResult, ResultCache $resultCache
706723 $ exportedNodes = $ this ->mergeExportedNodes ($ resultCache , $ analyserResult ->getExportedNodes ());
707724 $ linesToIgnore = $ this ->mergeLinesToIgnore ($ resultCache , $ analyserResult ->getLinesToIgnore ());
708725 $ unmatchedLineIgnores = $ this ->mergeUnmatchedLineIgnores ($ resultCache , $ analyserResult ->getUnmatchedLineIgnores ());
726+ $ externalFileDependencies = $ this ->mergeExternalFileDependencies ($ resultCache ->getExternalFileDependencies (), $ resultCache ->getFilesToAnalyse (), $ analyserResult ->getExternalFileDependencies ());
709727
710728 $ saved = false ;
711729 if ($ save !== false ) {
@@ -729,7 +747,7 @@ public function process(AnalyserResult $analyserResult, ResultCache $resultCache
729747 $ projectExtensionFiles [$ file ] = [$ hash , true , $ className ];
730748 }
731749 }
732- $ saved = $ doSave ($ errorsByFile , $ locallyIgnoredErrorsByFile , $ linesToIgnore , $ unmatchedLineIgnores , $ collectedDataByFile , $ dependencies , $ usedTraitDependencies , $ exportedNodes , $ projectExtensionFiles );
750+ $ saved = $ doSave ($ errorsByFile , $ locallyIgnoredErrorsByFile , $ linesToIgnore , $ unmatchedLineIgnores , $ collectedDataByFile , $ dependencies , $ usedTraitDependencies , $ exportedNodes , $ projectExtensionFiles, $ externalFileDependencies );
733751 }
734752
735753 $ flatErrors = [];
@@ -760,6 +778,7 @@ public function process(AnalyserResult $analyserResult, ResultCache $resultCache
760778 exportedNodes: $ exportedNodes ,
761779 reachedInternalErrorsCountLimit: $ analyserResult ->hasReachedInternalErrorsCountLimit (),
762780 peakMemoryUsageBytes: $ analyserResult ->getPeakMemoryUsageBytes (),
781+ externalFileDependencies: $ externalFileDependencies !== [] ? $ externalFileDependencies : null ,
763782 ), $ saved );
764783 }
765784
@@ -944,6 +963,45 @@ private function mergeUnmatchedLineIgnores(ResultCache $resultCache, array $fres
944963 return $ newUnmatchedLineIgnores ;
945964 }
946965
966+ /**
967+ * Merges cached inverted external dependencies with fresh analysis results.
968+ *
969+ * @param array<string, array<string>> $cachedExternalDependencies Inverted: external file => dependent analyzed files
970+ * @param string[] $filesToAnalyse Files that were re-analyzed
971+ * @param array<string, list<string>>|null $freshExternalDependencies Non-inverted: analyzed file => external files
972+ * @return array<string, list<string>> Non-inverted: analyzed file => external files
973+ */
974+ private function mergeExternalFileDependencies (
975+ array $ cachedExternalDependencies ,
976+ array $ filesToAnalyse ,
977+ ?array $ freshExternalDependencies ,
978+ ): array
979+ {
980+ if ($ freshExternalDependencies === null ) {
981+ return [];
982+ }
983+
984+ // Un-invert cached external dependencies: external file => [dependents] → dependent => [external files]
985+ $ cachedPerFile = [];
986+ foreach ($ cachedExternalDependencies as $ externalFile => $ externalData ) {
987+ $ dependentFiles = $ externalData ['dependentFiles ' ] ?? $ externalData ;
988+ foreach ($ dependentFiles as $ dependentFile ) {
989+ $ cachedPerFile [$ dependentFile ][] = $ externalFile ;
990+ }
991+ }
992+
993+ // Replace re-analyzed files with fresh data
994+ $ merged = $ cachedPerFile ;
995+ foreach ($ filesToAnalyse as $ file ) {
996+ unset($ merged [$ file ]);
997+ if (array_key_exists ($ file , $ freshExternalDependencies )) {
998+ $ merged [$ file ] = $ freshExternalDependencies [$ file ];
999+ }
1000+ }
1001+
1002+ return $ merged ;
1003+ }
1004+
9471005 /**
9481006 * @param array<string, list<Error>> $errors
9491007 * @param array<string, list<Error>> $locallyIgnoredErrors
@@ -956,6 +1014,7 @@ private function mergeUnmatchedLineIgnores(ResultCache $resultCache, array $fres
9561014 * @param array<string, array{string, bool, string}> $projectExtensionFiles
9571015 * @param array<string, string> $currentFileHashes
9581016 * @param mixed[] $meta
1017+ * @param array<string, list<string>> $externalFileDependencies
9591018 */
9601019 private function save (
9611020 int $ lastFullAnalysisTime ,
@@ -970,6 +1029,7 @@ private function save(
9701029 array $ projectExtensionFiles ,
9711030 array $ currentFileHashes ,
9721031 array $ meta ,
1032+ array $ externalFileDependencies = [],
9731033 ): void
9741034 {
9751035 $ invertedDependencies = [];
@@ -1043,6 +1103,31 @@ private function save(
10431103
10441104 ksort ($ exportedNodes );
10451105
1106+ // Build inverted external dependencies: external file => {hash, dependentFiles}
1107+ $ invertedExternalDependencies = [];
1108+ foreach ($ externalFileDependencies as $ analysedFile => $ externalFiles ) {
1109+ foreach ($ externalFiles as $ externalFile ) {
1110+ if (!array_key_exists ($ externalFile , $ invertedExternalDependencies )) {
1111+ if (!is_file ($ externalFile )) {
1112+ continue ;
1113+ }
1114+ $ invertedExternalDependencies [$ externalFile ] = [
1115+ 'fileHash ' => $ this ->getFileHash ($ externalFile ),
1116+ 'dependentFiles ' => [],
1117+ ];
1118+ }
1119+ $ invertedExternalDependencies [$ externalFile ]['dependentFiles ' ][] = $ analysedFile ;
1120+ }
1121+ }
1122+
1123+ foreach ($ invertedExternalDependencies as $ externalFile => $ externalData ) {
1124+ $ dependentFiles = array_values (array_unique ($ externalData ['dependentFiles ' ]));
1125+ sort ($ dependentFiles );
1126+ $ invertedExternalDependencies [$ externalFile ]['dependentFiles ' ] = $ dependentFiles ;
1127+ }
1128+
1129+ ksort ($ invertedExternalDependencies );
1130+
10461131 $ file = $ this ->cacheFilePath ;
10471132
10481133 FileWriter::write (
@@ -1059,7 +1144,8 @@ private function save(
10591144 'unmatchedLineIgnores' => " . var_export ($ unmatchedLineIgnores , true ) . ",
10601145 'collectedDataCallback' => static function (): array { return " . var_export ($ collectedData , true ) . "; },
10611146 'dependencies' => " . var_export ($ invertedDependencies , true ) . ",
1062- 'exportedNodesCallback' => static function (): array { return " . var_export ($ exportedNodes , true ) . '; },
1147+ 'exportedNodesCallback' => static function (): array { return " . var_export ($ exportedNodes , true ) . "; },
1148+ 'externalDependencies' => " . var_export ($ invertedExternalDependencies , true ) . ',
10631149];
10641150 ' ,
10651151 );
0 commit comments