diff --git a/e2e/parallel-unused-skips/expected-output.diff b/e2e/parallel-unused-skips/expected-output.diff index de57957a29e..4d79f6e708c 100644 --- a/e2e/parallel-unused-skips/expected-output.diff +++ b/e2e/parallel-unused-skips/expected-output.diff @@ -3,4 +3,4 @@ [WARNING] This skip is unused, it never matched any element. You can remove it from "->withSkip()" - * */NonexistentUnused/* + * Rector\CodeQuality\Rector\FunctionLike\SimplifyUselessVariableRector => */NonexistentUnused/* diff --git a/src/Reporting/MissConfigurationReporter.php b/src/Reporting/MissConfigurationReporter.php index bb0cd432458..05e4ae0fa9e 100644 --- a/src/Reporting/MissConfigurationReporter.php +++ b/src/Reporting/MissConfigurationReporter.php @@ -36,27 +36,40 @@ public function reportUnusedSkips(ProcessResult $processResult): void return; } - // rule-scoped path skips are trackable at runtime; skip-everywhere rule skips - // (null path) are forgotten from the container at boot, so they never reach the skipper. - // rule-scoped paths are intentional, so they are reported even as mask paths - $skippedClassPaths = []; - foreach ($this->skippedClassResolver->resolve() as $paths) { + // map of trackable skip path => human-readable display; skips are tracked at runtime by + // their path, but rule-scoped ones are printed as "rule => path" so the user knows exactly + // what to remove. Skip-everywhere rule skips (null path) are forgotten from the container + // at boot, so they never reach the skipper and cannot be tracked. + $skipDisplaysByPath = []; + foreach ($this->skippedClassResolver->resolve() as $rectorClass => $paths) { if ($paths === null) { continue; } - $skippedClassPaths = [...$skippedClassPaths, ...$paths]; + // rule-scoped paths are intentional, so they are reported even as mask paths + foreach ($paths as $path) { + $skipDisplaysByPath[$path] = $rectorClass . ' => ' . $path; + } } // global mask paths like "*/some/*" are hard to spot and report false positives, skip them - $globalPaths = array_filter( - $this->skippedPathsResolver->resolve(), - static fn (string $skip): bool => ! str_contains($skip, '*') - ); + foreach ($this->skippedPathsResolver->resolve() as $globalPath) { + if (str_contains($globalPath, '*')) { + continue; + } - $configuredSkips = [...$skippedClassPaths, ...$globalPaths]; + $skipDisplaysByPath[$globalPath] = $globalPath; + } + + $usedSkips = $processResult->getUsedSkips(); + + $unusedSkips = []; + foreach ($skipDisplaysByPath as $path => $skipDisplay) { + if (! in_array($path, $usedSkips, true)) { + $unusedSkips[] = $skipDisplay; + } + } - $unusedSkips = array_values(array_diff($configuredSkips, $processResult->getUsedSkips())); if ($unusedSkips === []) { return; } diff --git a/tests/Reporting/MissConfigurationReporterTest.php b/tests/Reporting/MissConfigurationReporterTest.php index b047f66a7dc..0c4e90fc1f3 100644 --- a/tests/Reporting/MissConfigurationReporterTest.php +++ b/tests/Reporting/MissConfigurationReporterTest.php @@ -73,12 +73,13 @@ public function testReportsOnlyTrackableUnusedSkips(): void $output = $this->bufferedOutput->fetch(); - // unused rule-scoped path is reported by path, not rule class + // unused rule-scoped path is reported as "rule => path", so both are shown $this->assertStringContainsString('dead-rule', $output); - $this->assertStringNotContainsString('FifthElement', $output); + $this->assertStringContainsString('FifthElement', $output); // matched rule-scoped path is excluded $this->assertStringNotContainsString('matched-rule', $output); + $this->assertStringNotContainsString('AnotherClassToSkip', $output); // global mask path is excluded (hard to spot, false-positive prone) $this->assertStringNotContainsString('global-mask', $output);