44
55use Closure ;
66use PHPStan \AnalysedCodeException ;
7+ use PHPStan \Analyser \Ignore \LineIgnoreFixer ;
78use PHPStan \Analyser \Ignore \UnmatchedIgnoreFixer ;
89use PHPStan \Analyser \ResultCache \LazyCollectedData ;
910use PHPStan \BetterReflection \NodeCompiler \Exception \UnableToCompileNode ;
1920use PHPStan \Rules \Registry as RuleRegistry ;
2021use Throwable ;
2122use function array_key_exists ;
23+ use function array_map ;
2224use function array_merge ;
2325use function count ;
2426use function get_class ;
@@ -47,6 +49,7 @@ public function __construct(
4749 private ScopeFactory $ scopeFactory ,
4850 private LocalIgnoresProcessor $ localIgnoresProcessor ,
4951 private UnmatchedIgnoreFixer $ unmatchedIgnoreFixer ,
52+ private LineIgnoreFixer $ lineIgnoreFixer ,
5053 #[AutowiredParameter]
5154 private bool $ reportUnmatchedIgnoredErrors ,
5255 private ValueDependencyCollector $ valueDependencyCollector ,
@@ -74,12 +77,12 @@ public function finalize(
7477 ): FinalizerResult
7578 {
7679 if ($ analyserResult ->getLazyCollectedData ()->isEmpty ()) {
77- return $ this ->addUnmatchedIgnoredErrors ($ this ->mergeFilteredPhpErrors ($ analyserResult ), [], []);
80+ return $ this ->processInlineIgnores ($ this ->mergeFilteredPhpErrors ($ analyserResult ), [], []);
7881 }
7982
8083 $ hasInternalErrors = count ($ analyserResult ->getInternalErrors ()) > 0 || $ analyserResult ->hasReachedInternalErrorsCountLimit ();
8184 if ($ hasInternalErrors ) {
82- return $ this ->addUnmatchedIgnoredErrors ($ this ->mergeFilteredPhpErrors ($ analyserResult ), [], []);
85+ return $ this ->processInlineIgnores ($ this ->mergeFilteredPhpErrors ($ analyserResult ), [], []);
8386 }
8487
8588 try {
@@ -89,7 +92,7 @@ public function finalize(
8992 throw $ t ;
9093 }
9194
92- return $ this ->addUnmatchedIgnoredErrors ($ this ->mergeFilteredPhpErrors ($ this ->withUnreadableCollectedData ($ analyserResult , $ t )), [], []);
95+ return $ this ->processInlineIgnores ($ this ->mergeFilteredPhpErrors ($ this ->withUnreadableCollectedData ($ analyserResult , $ t )), [], []);
9396 }
9497
9598 $ nodeType = CollectedDataNode::class;
@@ -230,7 +233,7 @@ public function finalize(
230233 $ allUnmatchedLineIgnores [$ file ] = $ localIgnoresProcessorResult ->getUnmatchedLineIgnores ();
231234 }
232235
233- return $ this ->addUnmatchedIgnoredErrors (new AnalyserResult (
236+ return $ this ->processInlineIgnores (new AnalyserResult (
234237 unorderedErrors: array_merge ($ errors , $ analyserResult ->getFilteredPhpErrors ()),
235238 filteredPhpErrors: [],
236239 allPhpErrors: $ analyserResult ->getAllPhpErrors (),
@@ -402,6 +405,83 @@ private function mergeFilteredPhpErrors(AnalyserResult $analyserResult): Analyse
402405 );
403406 }
404407
408+ /**
409+ * Replaces each ignore of all errors on a line with an ignore of the identifiers of the errors reported there,
410+ * including the errors of CollectedDataNode rules.
411+ *
412+ * @param list<Error> $errors
413+ * @return list<Error>
414+ */
415+ private function fixAllLineErrorsIgnores (array $ errors ): array
416+ {
417+ $ allLineErrors = [];
418+ foreach ($ errors as $ error ) {
419+ if ($ error ->getIdentifier () !== 'ignore.allLineErrors ' || $ error ->getLine () === null ) {
420+ continue ;
421+ }
422+
423+ $ allLineErrors [$ error ->getFile ()][$ error ->getLine ()] = true ;
424+ }
425+ if ($ allLineErrors === []) {
426+ return $ errors ;
427+ }
428+
429+ $ identifiers = [];
430+ foreach ($ errors as $ error ) {
431+ $ line = $ error ->getLine ();
432+ if ($ line === null || !isset ($ allLineErrors [$ error ->getFile ()][$ line ]) || !$ error ->canBeIgnored ()) {
433+ continue ;
434+ }
435+
436+ $ identifier = $ error ->getIdentifier ();
437+ if ($ identifier === null ) {
438+ // such an error cannot be ignored by an identifier
439+ $ identifiers [$ error ->getFile ()][$ line ] = false ;
440+ } elseif (($ identifiers [$ error ->getFile ()][$ line ] ?? null ) !== false ) {
441+ $ identifiers [$ error ->getFile ()][$ line ][] = $ identifier ;
442+ }
443+ }
444+
445+ $ fixes = [];
446+ foreach ($ identifiers as $ file => $ lines ) {
447+ if (!is_file ($ file )) {
448+ continue ;
449+ }
450+
451+ $ code = FileReader ::read ($ file );
452+ foreach ($ lines as $ line => $ lineIdentifiers ) {
453+ if ($ lineIdentifiers === false ) {
454+ continue ;
455+ }
456+
457+ $ newCode = $ this ->lineIgnoreFixer ->fix ($ code , $ line , $ lineIdentifiers );
458+ if ($ newCode === null ) {
459+ continue ;
460+ }
461+
462+ $ fixes [$ file ][$ line ] = $ this ->ruleErrorTransformer ->createFixedErrorDiff ($ file , $ code , $ newCode );
463+ }
464+ }
465+
466+ return array_map (static function (Error $ error ) use ($ fixes ): Error {
467+ $ line = $ error ->getLine ();
468+ if ($ line === null || $ error ->getIdentifier () !== 'ignore.allLineErrors ' || !isset ($ fixes [$ error ->getFile ()][$ line ])) {
469+ return $ error ;
470+ }
471+
472+ return new Error (
473+ $ error ->getMessage (),
474+ $ error ->getFile (),
475+ $ line ,
476+ false ,
477+ $ error ->getFilePath (),
478+ tip: $ error ->getTip (),
479+ identifier: 'ignore.allLineErrors ' ,
480+ fixedErrorDiffs: [$ fixes [$ error ->getFile ()][$ line ]],
481+ );
482+ }, $ errors );
483+ }
484+
405485 /**
406486 * @return list<FixedErrorDiff>
407487 */
@@ -420,23 +500,46 @@ private function fixUnmatchedIgnore(string $file, ?string $code, int $line, ?str
420500 }
421501
422502 /**
503+ * Fixes ignores of all errors on a line, and reports unmatched inline ignores. Runs after the errors of CollectedDataNode rules
504+ * are known, because they can be reported on the ignored lines too.
505+ *
423506 * @param list<Error> $collectorErrors
424507 * @param list<Error> $locallyIgnoredCollectorErrors
425508 * @param array<string, CollectedDataRuleResult> $collectedDataRuleResults
426509 */
427- private function addUnmatchedIgnoredErrors (
510+ private function processInlineIgnores (
428511 AnalyserResult $ analyserResult ,
429512 array $ collectorErrors ,
430513 array $ locallyIgnoredCollectorErrors ,
431514 array $ collectedDataRuleResults = [],
432515 int $ reusedRulesCount = 0 ,
433516 ): FinalizerResult
434517 {
518+ $ errors = $ this ->fixAllLineErrorsIgnores ($ analyserResult ->getUnorderedErrors ());
435519 if (!$ this ->reportUnmatchedIgnoredErrors ) {
520+ $ analyserResult = new AnalyserResult (
521+ unorderedErrors: $ errors ,
522+ filteredPhpErrors: $ analyserResult ->getFilteredPhpErrors (),
523+ allPhpErrors: $ analyserResult ->getAllPhpErrors (),
524+ locallyIgnoredErrors: $ analyserResult ->getLocallyIgnoredErrors (),
525+ linesToIgnore: $ analyserResult ->getLinesToIgnore (),
526+ unmatchedLineIgnores: $ analyserResult ->getUnmatchedLineIgnores (),
527+ internalErrors: $ analyserResult ->getInternalErrors (),
528+ collectedData: $ analyserResult ->getLazyCollectedData (),
529+ dependencies: $ analyserResult ->getDependencies (),
530+ usedTraitDependencies: $ analyserResult ->getUsedTraitDependencies (),
531+ valueDependencies: $ analyserResult ->getValueDependencies (),
532+ packageDependencies: $ analyserResult ->getPackageDependencies (),
533+ exportedNodes: $ analyserResult ->getExportedNodes (),
534+ reachedInternalErrorsCountLimit: $ analyserResult ->hasReachedInternalErrorsCountLimit (),
535+ peakMemoryUsageBytes: $ analyserResult ->getPeakMemoryUsageBytes (),
536+ processedFiles: $ analyserResult ->getProcessedFiles (),
537+ workerCount: $ analyserResult ->getWorkerCount (),
538+ );
539+
436540 return new FinalizerResult ($ analyserResult , $ collectorErrors , $ locallyIgnoredCollectorErrors , $ collectedDataRuleResults , $ reusedRulesCount );
437541 }
438542
439- $ errors = $ analyserResult ->getUnorderedErrors ();
440543 foreach ($ analyserResult ->getUnmatchedLineIgnores () as $ file => $ data ) {
441544 foreach ($ data as $ ignoredFile => $ lines ) {
442545 if ($ ignoredFile !== $ file ) {
0 commit comments