@@ -50,12 +50,20 @@ public class RemoveRedundantDependencies extends ScanningRecipe<RemoveRedundantD
5050
5151 String description = "Remove explicit dependencies that are already provided transitively by a specified dependency. " +
5252 "This recipe downloads and resolves the parent dependency's POM to determine its true transitive " +
53- "dependencies, allowing it to detect redundancies even when both dependencies are explicitly declared." ;
53+ "dependencies, allowing it to detect redundancies even when both dependencies are explicitly declared. " +
54+ "A direct dependency is only removed when the transitive one provides it at the exact same scope and " +
55+ "with the same exclusions, so that removing it does not change the effective classpath." ;
5456
5557 @ Value
5658 public static class Accumulator {
57- // Map from project identifier -> scope/configuration -> Set of transitive GAVs
58- Map <String , Map <String , Set <ResolvedGroupArtifactVersion >>> transitivesByProjectAndScope ;
59+ // Map from project identifier -> scope/configuration -> Set of transitive dependencies
60+ Map <String , Map <String , Set <TransitiveDependency >>> transitivesByProjectAndScope ;
61+ }
62+
63+ @ Value
64+ public static class TransitiveDependency {
65+ ResolvedGroupArtifactVersion gav ;
66+ Set <GroupArtifact > exclusions ;
5967 }
6068
6169 @ Override
@@ -82,7 +90,7 @@ public TreeVisitor<?, ExecutionContext> getScanner(Accumulator acc) {
8290 StringUtils .matchesGlob (dep .getGroupId (), groupId ) &&
8391 StringUtils .matchesGlob (dep .getArtifactId (), artifactId )) {
8492 // This is a matching parent dependency, resolve its transitives independently
85- Set <ResolvedGroupArtifactVersion > transitives = acc .transitivesByProjectAndScope
93+ Set <TransitiveDependency > transitives = acc .transitivesByProjectAndScope
8694 .computeIfAbsent (projectId , k -> new HashMap <>())
8795 .computeIfAbsent (conf .getName (), k -> new HashSet <>());
8896 resolveTransitivesFromPom (
@@ -101,14 +109,18 @@ public TreeVisitor<?, ExecutionContext> getScanner(Accumulator acc) {
101109 String projectId = maven .getPom ().getGroupId () + ":" + maven .getPom ().getArtifactId ();
102110 MavenPomDownloader downloader = new MavenPomDownloader (ctx );
103111
104- for (Map .Entry <Scope , List <ResolvedDependency >> entry : maven .getDependencies ().entrySet ()) {
105- Scope depScope = entry .getKey ();
106- for (ResolvedDependency dep : entry .getValue ()) {
112+ // A direct dependency appears under every scope bucket it is visible in, so process
113+ // each matching parent once, keyed by its own effective (declared) scope.
114+ Set <String > processed = new HashSet <>();
115+ for (List <ResolvedDependency > deps : maven .getDependencies ().values ()) {
116+ for (ResolvedDependency dep : deps ) {
107117 if (dep .isDirect () &&
108118 StringUtils .matchesGlob (dep .getGroupId (), groupId ) &&
109- StringUtils .matchesGlob (dep .getArtifactId (), artifactId )) {
119+ StringUtils .matchesGlob (dep .getArtifactId (), artifactId ) &&
120+ processed .add (dep .getGroupId () + ":" + dep .getArtifactId ())) {
110121 // This is a matching parent dependency, resolve its transitives independently
111- Set <ResolvedGroupArtifactVersion > transitives = acc .transitivesByProjectAndScope
122+ Scope depScope = Scope .fromName (dep .getRequested ().getScope ());
123+ Set <TransitiveDependency > transitives = acc .transitivesByProjectAndScope
112124 .computeIfAbsent (projectId , k -> new HashMap <>())
113125 .computeIfAbsent (depScope .name ().toLowerCase (), k -> new HashSet <>());
114126 resolveTransitivesFromPom (
@@ -132,7 +144,7 @@ private void resolveTransitivesFromPom(
132144 List <MavenRepository > repositories ,
133145 MavenPomDownloader downloader ,
134146 ExecutionContext ctx ,
135- Set <ResolvedGroupArtifactVersion > transitives ) {
147+ Set <TransitiveDependency > transitives ) {
136148 try {
137149 // Ensure we have Maven Central in the repositories
138150 List <MavenRepository > effectiveRepos = new ArrayList <>(repositories );
@@ -148,8 +160,9 @@ private void resolveTransitivesFromPom(
148160 List <ResolvedDependency > resolved = patchedPom .resolveDependencies (Scope .Compile , downloader , ctx );
149161
150162 // Collect all dependencies (both direct and transitive of the parent)
163+ Set <ResolvedGroupArtifactVersion > visited = new HashSet <>();
151164 for (ResolvedDependency dep : resolved ) {
152- collectAllDependencies (dep , transitives );
165+ collectAllDependencies (dep , transitives , visited );
153166 }
154167 } catch (MavenDownloadingException | MavenDownloadingExceptions e ) {
155168 // If we can't download/resolve the POM, fall back to not detecting redundancies
@@ -166,10 +179,12 @@ private ResolvedPom applyExclusions(ResolvedPom resolvedPom, List<GroupArtifact>
166179 return patchedPom ;
167180 }
168181
169- private void collectAllDependencies (ResolvedDependency dep , Set <ResolvedGroupArtifactVersion > transitives ) {
170- if (transitives .add (dep .getGav ())) {
182+ private void collectAllDependencies (ResolvedDependency dep , Set <TransitiveDependency > transitives ,
183+ Set <ResolvedGroupArtifactVersion > visited ) {
184+ if (visited .add (dep .getGav ())) {
185+ transitives .add (new TransitiveDependency (dep .getGav (), new HashSet <>(dep .getEffectiveExclusions ())));
171186 for (ResolvedDependency transitive : dep .getDependencies ()) {
172- collectAllDependencies (transitive , transitives );
187+ collectAllDependencies (transitive , transitives , visited );
173188 }
174189 }
175190 }
@@ -185,112 +200,112 @@ public TreeVisitor<?, ExecutionContext> getVisitor(Accumulator acc) {
185200 return tree ;
186201 }
187202
188- SourceFile sf = (SourceFile ) tree ;
189- Tree result = sf ;
190-
191- // Handle Gradle
192- Optional <GradleProject > gradleOpt = sf .getMarkers ().findFirst (GradleProject .class );
203+ Optional <GradleProject > gradleOpt = tree .getMarkers ().findFirst (GradleProject .class );
193204 if (gradleOpt .isPresent ()) {
194205 GradleProject gradle = gradleOpt .get ();
195- String projectId = gradle .getGroup () + ":" + gradle .getName ();
196- Map <String , Set <ResolvedGroupArtifactVersion >> scopeToTransitives =
197- acc .transitivesByProjectAndScope .getOrDefault (projectId , emptyMap ());
198-
199- for (GradleDependencyConfiguration conf : gradle .getConfigurations ()) {
200- Set <ResolvedGroupArtifactVersion > transitives = getCompatibleTransitives (
201- scopeToTransitives , conf .getName (), true );
202- if (transitives .isEmpty ()) {
203- continue ;
204- }
205-
206- for (ResolvedDependency dep : conf .getResolved ()) {
207- if (dep .isDirect () &&
208- doesNotMatchArguments (dep ) &&
209- isInTransitives (dep , transitives )) {
210- // This direct dependency is transitively provided, remove it
211- // Don't specify configuration - Gradle's resolved config names differ from declaration names
212- result = new RemoveDependency (
213- dep .getGroupId (), dep .getArtifactId (), null , null , null )
214- .getVisitor ().visit (result , ctx );
215- }
216- }
217- }
218- return result ;
206+ return handleGradle (ctx , gradle , tree );
219207 }
220208
221- // Handle Maven
222- Optional <MavenResolutionResult > mavenOpt = sf .getMarkers ().findFirst (MavenResolutionResult .class );
209+ Optional <MavenResolutionResult > mavenOpt = tree .getMarkers ().findFirst (MavenResolutionResult .class );
223210 if (mavenOpt .isPresent ()) {
224211 MavenResolutionResult maven = mavenOpt .get ();
225- String projectId = maven .getPom ().getGroupId () + ":" + maven .getPom ().getArtifactId ();
226- Map <String , Set <ResolvedGroupArtifactVersion >> scopeToTransitives =
227- acc .transitivesByProjectAndScope .getOrDefault (projectId , emptyMap ());
228-
229- for (Map .Entry <Scope , List <ResolvedDependency >> entry : maven .getDependencies ().entrySet ()) {
230- String scope = entry .getKey ().name ().toLowerCase ();
231- Set <ResolvedGroupArtifactVersion > transitives = getCompatibleTransitives (
232- scopeToTransitives , scope , false );
233- if (transitives .isEmpty ()) {
234- continue ;
212+ return handleMaven (ctx , maven , tree );
213+ }
214+
215+ return tree ;
216+ }
217+
218+ private @ Nullable Tree handleGradle (ExecutionContext ctx , GradleProject gradle , Tree result ) {
219+ String projectId = gradle .getGroup () + ":" + gradle .getName ();
220+ Map <String , Set <TransitiveDependency >> scopeToTransitives =
221+ acc .transitivesByProjectAndScope .getOrDefault (projectId , emptyMap ());
222+
223+ for (GradleDependencyConfiguration conf : gradle .getConfigurations ()) {
224+ Set <TransitiveDependency > transitives = getCompatibleGradleTransitives (
225+ scopeToTransitives , conf .getName ());
226+ if (transitives .isEmpty ()) {
227+ continue ;
228+ }
229+
230+ for (ResolvedDependency dep : conf .getResolved ()) {
231+ if (dep .isDirect () &&
232+ doesNotMatchArguments (dep ) &&
233+ isRedundant (dep , transitives )) {
234+ // This direct dependency is transitively provided, remove it
235+ // Don't specify configuration - Gradle's resolved config names differ from declaration names
236+ result = new RemoveDependency (
237+ dep .getGroupId (), dep .getArtifactId (), null , null , null )
238+ .getVisitor ().visit (result , ctx );
235239 }
240+ }
241+ }
242+ return result ;
243+ }
236244
237- for (ResolvedDependency dep : entry .getValue ()) {
238- if (dep .isDirect () &&
239- doesNotMatchArguments (dep ) &&
240- isInTransitives (dep , transitives )) {
241- // This direct dependency is transitively provided, remove it
245+ private @ Nullable Tree handleMaven (ExecutionContext ctx , MavenResolutionResult maven , Tree result ) {
246+ String projectId = maven .getPom ().getGroupId () + ":" + maven .getPom ().getArtifactId ();
247+ Map <String , Set <TransitiveDependency >> scopeToTransitives =
248+ acc .transitivesByProjectAndScope .getOrDefault (projectId , emptyMap ());
249+
250+ // A direct dependency appears under every scope bucket it is visible in; evaluate each
251+ // one once using its own effective scope so a wider transitive scope does not falsely
252+ // mark a narrower direct declaration as redundant.
253+ Set <String > processed = new HashSet <>();
254+ for (List <ResolvedDependency > deps : maven .getDependencies ().values ()) {
255+ for (ResolvedDependency dep : deps ) {
256+ if (dep .isDirect () &&
257+ doesNotMatchArguments (dep ) &&
258+ processed .add (dep .getGroupId () + ":" + dep .getArtifactId ())) {
259+ Scope depScope = Scope .fromName (dep .getRequested ().getScope ());
260+ Set <TransitiveDependency > transitives = scopeToTransitives .getOrDefault (
261+ depScope .name ().toLowerCase (), emptySet ());
262+ if (isRedundant (dep , transitives )) {
263+ // This direct dependency is transitively provided at the same scope and
264+ // with the same exclusions, remove it.
242265 result = new RemoveDependency (
243- dep .getGroupId (), dep .getArtifactId (), null , null , scope )
266+ dep .getGroupId (), dep .getArtifactId (), null , null , depScope . name (). toLowerCase () )
244267 .getVisitor ().visit (result , ctx );
245268 }
246269 }
247270 }
248- return result ;
249271 }
250-
251- return tree ;
272+ return result ;
252273 }
253274
254275 private boolean doesNotMatchArguments (ResolvedDependency dep ) {
255276 return !StringUtils .matchesGlob (dep .getGroupId (), groupId ) ||
256277 !StringUtils .matchesGlob (dep .getArtifactId (), artifactId );
257278 }
258279
259- private boolean isInTransitives (ResolvedDependency dep , Set <ResolvedGroupArtifactVersion > transitives ) {
260- // Check if this dependency's GAV matches any transitive
261- // We match on groupId:artifactId:version exactly
262- for (ResolvedGroupArtifactVersion transitive : transitives ) {
263- if (dep .getGroupId ().equals (transitive .getGroupId ()) &&
264- dep .getArtifactId ().equals (transitive .getArtifactId ()) &&
265- dep .getVersion ().equals (transitive .getVersion ())) {
280+ private boolean isRedundant (ResolvedDependency dep , Set <TransitiveDependency > transitives ) {
281+ Set <GroupArtifact > depExclusions = new HashSet <>(dep .getEffectiveExclusions ());
282+ for (TransitiveDependency transitive : transitives ) {
283+ ResolvedGroupArtifactVersion gav = transitive .getGav ();
284+ if (dep .getGroupId ().equals (gav .getGroupId ()) &&
285+ dep .getArtifactId ().equals (gav .getArtifactId ()) &&
286+ dep .getVersion ().equals (gav .getVersion ()) &&
287+ depExclusions .equals (transitive .getExclusions ())) {
266288 return true ;
267289 }
268290 }
269291 return false ;
270292 }
271293
272- /**
273- * Get transitives from this scope/configuration and any broader ones.
274- */
275- private Set <ResolvedGroupArtifactVersion > getCompatibleTransitives (
276- Map <String , Set <ResolvedGroupArtifactVersion >> scopeToTransitives ,
277- String targetScope ,
278- boolean isGradle ) {
294+ private Set <TransitiveDependency > getCompatibleGradleTransitives (
295+ Map <String , Set <TransitiveDependency >> scopeToTransitives ,
296+ String targetScope ) {
279297
280- Set <ResolvedGroupArtifactVersion > result = new HashSet <>();
298+ Set <TransitiveDependency > result = new HashSet <>();
281299
282300 // Always include transitives from the same scope
283- Set <ResolvedGroupArtifactVersion > sameScope = scopeToTransitives .get (targetScope );
301+ Set <TransitiveDependency > sameScope = scopeToTransitives .get (targetScope );
284302 if (sameScope != null ) {
285303 result .addAll (sameScope );
286304 }
287305
288306 // Include transitives from broader scopes
289- List <String > broaderScopes = isGradle ?
290- getBroaderGradleScopes (targetScope ) :
291- getBroaderMavenScopes (targetScope );
292- for (String broader : broaderScopes ) {
293- Set <ResolvedGroupArtifactVersion > broaderTransitives = scopeToTransitives .get (broader );
307+ for (String broader : getBroaderGradleScopes (targetScope )) {
308+ Set <TransitiveDependency > broaderTransitives = scopeToTransitives .get (broader );
294309 if (broaderTransitives != null ) {
295310 result .addAll (broaderTransitives );
296311 }
@@ -313,19 +328,6 @@ private List<String> getBroaderGradleScopes(String scope) {
313328 return emptyList ();
314329 }
315330 }
316-
317- private List <String > getBroaderMavenScopes (String scope ) {
318- switch (scope .toLowerCase ()) {
319- case "runtime" :
320- return singletonList ("compile" );
321- case "provided" :
322- return Arrays .asList ("compile" , "runtime" );
323- case "test" :
324- return Arrays .asList ("compile" , "runtime" , "provided" );
325- default :
326- return emptyList ();
327- }
328- }
329331 };
330332 }
331333}
0 commit comments