22
33namespace PHPStan \Analyser ;
44
5+ use Closure ;
56use PHPStan \AnalysedCodeException ;
67use PHPStan \BetterReflection \NodeCompiler \Exception \UnableToCompileNode ;
78use PHPStan \BetterReflection \Reflection \Exception \CircularReference ;
1112use PHPStan \DependencyInjection \AutowiredService ;
1213use PHPStan \DependencyInjection \ExtensionsCollection ;
1314use PHPStan \Node \CollectedDataNode ;
15+ use PHPStan \Node \CollectedDataNodeReads ;
1416use PHPStan \Rules \Registry as RuleRegistry ;
1517use Throwable ;
18+ use function array_key_exists ;
1619use function array_merge ;
1720use function count ;
1821use function get_class ;
22+ use function ksort ;
23+ use function md5 ;
24+ use function serialize ;
1925use function sprintf ;
2026
2127#[AutowiredService]
@@ -38,7 +44,11 @@ public function __construct(
3844 {
3945 }
4046
41- public function finalize (AnalyserResult $ analyserResult , bool $ onlyFiles , bool $ debug ): FinalizerResult
47+ /**
48+ * @param array<string, CollectedDataRuleResult> $previousCollectedDataRuleResults the rule results of the
49+ * previous run, from the result cache - a rule whose input did not change reuses its errors
50+ */
51+ public function finalize (AnalyserResult $ analyserResult , bool $ onlyFiles , bool $ debug , array $ previousCollectedDataRuleResults = []): FinalizerResult
4252 {
4353 if (count ($ analyserResult ->getCollectedData ()) === 0 ) {
4454 return $ this ->addUnmatchedIgnoredErrors ($ this ->mergeFilteredPhpErrors ($ analyserResult ), [], []);
@@ -50,15 +60,52 @@ public function finalize(AnalyserResult $analyserResult, bool $onlyFiles, bool $
5060 }
5161
5262 $ nodeType = CollectedDataNode::class;
53- $ node = new CollectedDataNode ($ analyserResult ->getCollectedData (), $ onlyFiles );
63+ $ collectedData = $ analyserResult ->getCollectedData ();
64+ $ node = new CollectedDataNode ($ collectedData , $ onlyFiles );
65+
66+ // Hashed once per collector type and run, in the same order however the files were merged.
67+ $ collectorHashes = [];
68+ $ hashCollector = static function (string $ collectorType ) use ($ collectedData , &$ collectorHashes ): string {
69+ if (!array_key_exists ($ collectorType , $ collectorHashes )) {
70+ $ data = [];
71+ foreach ($ collectedData as $ filePath => $ collectedDataPerCollector ) {
72+ if (!isset ($ collectedDataPerCollector [$ collectorType ])) {
73+ continue ;
74+ }
75+
76+ $ data [$ filePath ] = $ collectedDataPerCollector [$ collectorType ];
77+ }
78+ ksort ($ data );
79+ $ collectorHashes [$ collectorType ] = md5 (serialize ($ data ));
80+ }
81+
82+ return $ collectorHashes [$ collectorType ];
83+ };
5484
5585 $ file = 'N/A ' ;
5686 $ scope = $ this ->scopeFactory ->create (ScopeContext::create ($ file ));
5787 $ tempCollectorErrors = [];
5888 $ internalErrors = $ analyserResult ->getInternalErrors ();
89+ $ collectedDataRuleResults = [];
90+ $ reusedRulesCount = 0 ;
91+ $ ruleCountsByClass = [];
5992 foreach ($ this ->ruleRegistry ->getRules ($ nodeType ) as $ rule ) {
93+ $ ruleClass = get_class ($ rule );
94+ $ ruleCountsByClass [$ ruleClass ] = ($ ruleCountsByClass [$ ruleClass ] ?? 0 ) + 1 ;
95+ $ ruleKey = sprintf ('%s#%d ' , $ ruleClass , $ ruleCountsByClass [$ ruleClass ]);
96+
97+ $ previousResult = $ previousCollectedDataRuleResults [$ ruleKey ] ?? null ;
98+ if ($ previousResult !== null && $ this ->canReuse ($ previousResult , $ onlyFiles , $ hashCollector )) {
99+ $ collectedDataRuleResults [$ ruleKey ] = $ previousResult ;
100+ $ reusedRulesCount ++;
101+ $ this ->addRuleErrors ($ previousResult ->getErrors (), $ node , $ scope , $ tempCollectorErrors );
102+ continue ;
103+ }
104+
105+ $ reads = new CollectedDataNodeReads ();
106+ $ ruleNode = new CollectedDataNode ($ collectedData , $ onlyFiles , $ reads );
60107 try {
61- $ ruleErrors = $ rule ->processNode ($ node , $ scope );
108+ $ ruleErrors = $ rule ->processNode ($ ruleNode , $ scope );
62109 } catch (AnalysedCodeException $ e ) {
63110 $ tempCollectorErrors [] = (new Error ($ e ->getMessage (), $ file , $ node ->getStartLine (), $ e , tip: $ e ->getTip ()))
64111 ->withIdentifier ('phpstan.internal ' )
@@ -98,19 +145,22 @@ public function finalize(AnalyserResult $analyserResult, bool $onlyFiles, bool $
98145 continue ;
99146 }
100147
148+ $ errors = [];
101149 foreach ($ ruleErrors as $ ruleError ) {
102- $ error = $ this ->ruleErrorTransformer ->transform ($ ruleError , $ scope , [], $ node );
103-
104- if ($ error ->canBeIgnored ()) {
105- foreach ($ this ->ignoreErrorExtensions ->getAll () as $ ignoreErrorExtension ) {
106- if ($ ignoreErrorExtension ->shouldIgnore ($ error , $ node , $ scope )) {
107- continue 2 ;
108- }
109- }
110- }
150+ $ errors [] = $ this ->ruleErrorTransformer ->transform ($ ruleError , $ scope , [], $ ruleNode );
151+ }
111152
112- $ tempCollectorErrors [] = $ error ;
153+ $ readCollectorHashes = [];
154+ foreach ($ reads ->getCollectorTypes () as $ collectorType ) {
155+ $ readCollectorHashes [$ collectorType ] = $ hashCollector ($ collectorType );
113156 }
157+ $ collectedDataRuleResults [$ ruleKey ] = new CollectedDataRuleResult (
158+ $ readCollectorHashes ,
159+ $ reads ->isOnlyFilesRead () ? $ onlyFiles : null ,
160+ $ errors ,
161+ );
162+
163+ $ this ->addRuleErrors ($ errors , $ node , $ scope , $ tempCollectorErrors );
114164 }
115165
116166 $ errors = $ analyserResult ->getUnorderedErrors ();
@@ -158,7 +208,45 @@ public function finalize(AnalyserResult $analyserResult, bool $onlyFiles, bool $
158208 peakMemoryUsageBytes: $ analyserResult ->getPeakMemoryUsageBytes (),
159209 processedFiles: $ analyserResult ->getProcessedFiles (),
160210 workerCount: $ analyserResult ->getWorkerCount (),
161- ), $ collectorErrors , $ locallyIgnoredCollectorErrors );
211+ ), $ collectorErrors , $ locallyIgnoredCollectorErrors , $ collectedDataRuleResults , $ reusedRulesCount );
212+ }
213+
214+ /**
215+ * @param Closure(string): string $hashCollector
216+ */
217+ private function canReuse (CollectedDataRuleResult $ previousResult , bool $ onlyFiles , Closure $ hashCollector ): bool
218+ {
219+ $ previousOnlyFiles = $ previousResult ->getOnlyFiles ();
220+ if ($ previousOnlyFiles !== null && $ previousOnlyFiles !== $ onlyFiles ) {
221+ return false ;
222+ }
223+
224+ foreach ($ previousResult ->getCollectorHashes () as $ collectorType => $ hash ) {
225+ if ($ hashCollector ($ collectorType ) !== $ hash ) {
226+ return false ;
227+ }
228+ }
229+
230+ return true ;
231+ }
232+
233+ /**
234+ * @param list<Error> $errors
235+ * @param list<Error> $tempCollectorErrors
236+ */
237+ private function addRuleErrors (array $ errors , CollectedDataNode $ node , Scope $ scope , array &$ tempCollectorErrors ): void
238+ {
239+ foreach ($ errors as $ error ) {
240+ if ($ error ->canBeIgnored ()) {
241+ foreach ($ this ->ignoreErrorExtensions ->getAll () as $ ignoreErrorExtension ) {
242+ if ($ ignoreErrorExtension ->shouldIgnore ($ error , $ node , $ scope )) {
243+ continue 2 ;
244+ }
245+ }
246+ }
247+
248+ $ tempCollectorErrors [] = $ error ;
249+ }
162250 }
163251
164252 private function mergeFilteredPhpErrors (AnalyserResult $ analyserResult ): AnalyserResult
@@ -187,15 +275,18 @@ private function mergeFilteredPhpErrors(AnalyserResult $analyserResult): Analyse
187275 /**
188276 * @param list<Error> $collectorErrors
189277 * @param list<Error> $locallyIgnoredCollectorErrors
278+ * @param array<string, CollectedDataRuleResult> $collectedDataRuleResults
190279 */
191280 private function addUnmatchedIgnoredErrors (
192281 AnalyserResult $ analyserResult ,
193282 array $ collectorErrors ,
194283 array $ locallyIgnoredCollectorErrors ,
284+ array $ collectedDataRuleResults = [],
285+ int $ reusedRulesCount = 0 ,
195286 ): FinalizerResult
196287 {
197288 if (!$ this ->reportUnmatchedIgnoredErrors ) {
198- return new FinalizerResult ($ analyserResult , $ collectorErrors , $ locallyIgnoredCollectorErrors );
289+ return new FinalizerResult ($ analyserResult , $ collectorErrors , $ locallyIgnoredCollectorErrors, $ collectedDataRuleResults , $ reusedRulesCount );
199290 }
200291
201292 $ errors = $ analyserResult ->getUnorderedErrors ();
@@ -252,6 +343,8 @@ private function addUnmatchedIgnoredErrors(
252343 ),
253344 $ collectorErrors ,
254345 $ locallyIgnoredCollectorErrors ,
346+ $ collectedDataRuleResults ,
347+ $ reusedRulesCount ,
255348 );
256349 }
257350
0 commit comments