@@ -25,9 +25,9 @@ public function __construct(
2525
2626 /**
2727 * Resolves skips configured via "->withSkip()" that never matched any element during the run.
28- * Rule-scoped skips are returned as "rule => path" so the user knows exactly what to remove;
29- * global skips are returned as a plain path. Returns an empty array unless
30- * "->reportUnusedSkips()" is enabled.
28+ * Rule-scoped skips are grouped under their rule ( "rule => path", or "rule => [ path\n path ]"
29+ * for multiple paths) so the user knows exactly what to remove; global skips are returned as a
30+ * plain path. Returns an empty array unless "->reportUnusedSkips()" is enabled.
3131 *
3232 * @return string[]
3333 */
@@ -37,37 +37,66 @@ public function resolve(ProcessResult $processResult): array
3737 return [];
3838 }
3939
40- // map of trackable skip path => human-readable display; skips are tracked at runtime by
41- // their path, but rule-scoped ones are printed as "rule => path" so the user knows exactly
42- // what to remove. Skip-everywhere rule skips (null path) are forgotten from the container
43- // at boot, so they never reach the skipper and cannot be tracked.
44- $ skipDisplaysByPath = [];
40+ // a narrowed run (cli paths, "--only" or "--only-suffix") only touches part of the codebase,
41+ // so skips outside that scope look falsely unused - reporting them would be noise
42+ if (SimpleParameterProvider::provideBoolParameter (Option::IS_RUN_NARROWED , false )) {
43+ return [];
44+ }
45+
46+ // map of rule => (trackable skip path => relative display path); skips are tracked at
47+ // runtime by their path, but rule-scoped ones are printed grouped under their rule so the
48+ // user knows exactly what to remove. Skip-everywhere rule skips (null path) are forgotten
49+ // from the container at boot, so they never reach the skipper and cannot be tracked.
50+ $ relativePathsByClass = [];
4551 foreach ($ this ->skippedClassResolver ->resolve () as $ rectorClass => $ paths ) {
4652 if ($ paths === null ) {
4753 continue ;
4854 }
4955
5056 // rule-scoped paths are intentional, so they are reported even as mask paths
5157 foreach ($ paths as $ path ) {
52- $ skipDisplaysByPath [ $ path ] = $ rectorClass . ' => ' . $ this ->filePathHelper ->relativePath ($ path );
58+ $ relativePathsByClass [ $ rectorClass ][ $ path ] = $ this ->filePathHelper ->relativePath ($ path );
5359 }
5460 }
5561
5662 // global mask paths like "*/some/*" are hard to spot and report false positives, skip them
63+ $ globalRelativePaths = [];
5764 foreach ($ this ->skippedPathsResolver ->resolve () as $ globalPath ) {
5865 if (str_contains ($ globalPath , '* ' )) {
5966 continue ;
6067 }
6168
62- $ skipDisplaysByPath [$ globalPath ] = $ this ->filePathHelper ->relativePath ($ globalPath );
69+ $ globalRelativePaths [$ globalPath ] = $ this ->filePathHelper ->relativePath ($ globalPath );
6370 }
6471
6572 $ usedSkips = $ processResult ->getUsedSkips ();
6673
6774 $ unusedSkips = [];
68- foreach ($ skipDisplaysByPath as $ path => $ skipDisplay ) {
75+
76+ // group unused rule-scoped paths under their rule, matching the "->withSkip()" config shape
77+ foreach ($ relativePathsByClass as $ rectorClass => $ relativePaths ) {
78+ $ unusedRelativePaths = [];
79+ foreach ($ relativePaths as $ path => $ relativePath ) {
80+ if (! in_array ($ path , $ usedSkips , true )) {
81+ $ unusedRelativePaths [] = $ relativePath ;
82+ }
83+ }
84+
85+ if ($ unusedRelativePaths === []) {
86+ continue ;
87+ }
88+
89+ if (count ($ unusedRelativePaths ) === 1 ) {
90+ $ unusedSkips [] = $ rectorClass . ' => ' . $ unusedRelativePaths [0 ];
91+ continue ;
92+ }
93+
94+ $ unusedSkips [] = $ rectorClass . ' => [ ' . implode ("\n " , $ unusedRelativePaths ) . ' ] ' ;
95+ }
96+
97+ foreach ($ globalRelativePaths as $ path => $ relativePath ) {
6998 if (! in_array ($ path , $ usedSkips , true )) {
70- $ unusedSkips [] = $ skipDisplay ;
99+ $ unusedSkips [] = $ relativePath ;
71100 }
72101 }
73102
0 commit comments