@@ -52,7 +52,7 @@ public class RemoveRedundantDependencies extends ScanningRecipe<RemoveRedundantD
5252 "This recipe downloads and resolves the parent dependency's POM to determine its true transitive " +
5353 "dependencies, allowing it to detect redundancies even when both dependencies are explicitly declared. " +
5454 "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." ;
55+ "with the same effective exclusions, so that removing it does not change the effective classpath." ;
5656
5757 @ Value
5858 public static class Accumulator {
@@ -150,7 +150,8 @@ private void resolveTransitivesFromPom(
150150 List <MavenRepository > effectiveRepos = withMavenCentral (repositories );
151151 try {
152152 // Get the resolved dependencies for compile scope (which includes most transitives)
153- ResolvedPom resolvedPom = resolvePom (gav , effectiveRepos , downloader , ctx );
153+ Pom pom = downloader .download (gav .asGroupArtifactVersion (), null , null , effectiveRepos );
154+ ResolvedPom resolvedPom = pom .resolve (emptyList (), downloader , effectiveRepos , ctx );
154155 ResolvedPom patchedPom = applyExclusions (resolvedPom , effectiveExclusions );
155156 List <ResolvedDependency > resolved = patchedPom .resolveDependencies (Scope .Compile , downloader , ctx );
156157
@@ -179,65 +180,73 @@ private void collectAllDependencies(ResolvedDependency dep, Set<TransitiveDepend
179180 MavenPomDownloader downloader , ExecutionContext ctx ) {
180181 if (visited .add (dep .getGav ())) {
181182 transitives .add (new TransitiveDependency (dep .getGav (),
182- relevantExclusions (dep , repositories , downloader , ctx )));
183+ relevantExclusions (dep , repositories , downloader , ctx , acc . closureCache )));
183184 for (ResolvedDependency transitive : dep .getDependencies ()) {
184185 collectAllDependencies (transitive , transitives , visited , repositories , downloader , ctx );
185186 }
186187 }
187188 }
189+ };
190+ }
188191
189- // Effective exclusions are resolved in the parent's whole tree, so they pick up no-op exclusions
190- // from sibling branches; keep only those targeting this coordinate's own dependency closure.
191- private Set <GroupArtifact > relevantExclusions (ResolvedDependency dep , List <MavenRepository > repositories ,
192- MavenPomDownloader downloader , ExecutionContext ctx ) {
193- Set <GroupArtifact > exclusions = new HashSet <>(dep .getEffectiveExclusions ());
194- if (!exclusions .isEmpty ()) {
195- exclusions .retainAll (dependencyClosure (dep .getGav (), repositories , downloader , ctx ));
196- }
197- return exclusions ;
198- }
192+ // Compares a coordinate's declared (requested) exclusions rather than its effective ones: within a large
193+ // tree the effective set is corrupted by dependency mediation (an artifact excluded here may already have
194+ // been pruned by a shared node elsewhere, so it silently drops out of getEffectiveExclusions). The declared
195+ // exclusions are then intersected with the coordinate's own clean closure, dropping no-op exclusions that
196+ // target artifacts the coordinate could never bring on its own. Comparing this set on the direct and the
197+ // transitively-provided declaration tells us whether removing the direct one changes the effective classpath.
198+ private static Set <GroupArtifact > relevantExclusions (ResolvedDependency dep , List <MavenRepository > repositories ,
199+ MavenPomDownloader downloader , ExecutionContext ctx ,
200+ Map <ResolvedGroupArtifactVersion , Set <GroupArtifact >> closureCache ) {
201+ List <GroupArtifact > requested = dep .getRequested () == null ? null : dep .getRequested ().getExclusions ();
202+ if (requested == null || requested .isEmpty ()) {
203+ return emptySet ();
204+ }
205+ Set <GroupArtifact > exclusions = new HashSet <>(requested );
206+ exclusions .retainAll (dependencyClosure (dep .getGav (), repositories , downloader , ctx , closureCache ));
207+ return exclusions ;
208+ }
199209
200- private Set <GroupArtifact > dependencyClosure (ResolvedGroupArtifactVersion gav , List <MavenRepository > repositories ,
201- MavenPomDownloader downloader , ExecutionContext ctx ) {
202- return acc .closureCache .computeIfAbsent (gav , g -> {
203- Set <GroupArtifact > closure = new HashSet <>();
204- try {
205- for (ResolvedDependency d : resolvePom (g , repositories , downloader , ctx )
206- .resolveDependencies (Scope .Compile , downloader , ctx )) {
207- collectClosure (d , closure );
208- }
209- } catch (MavenDownloadingException | MavenDownloadingExceptions e ) {
210- // Best-effort: an unresolvable closure leaves the exclusions unfiltered
211- }
212- return closure ;
213- });
210+ private static Set <GroupArtifact > dependencyClosure (ResolvedGroupArtifactVersion gav , List <MavenRepository > repositories ,
211+ MavenPomDownloader downloader , ExecutionContext ctx ,
212+ Map <ResolvedGroupArtifactVersion , Set <GroupArtifact >> closureCache ) {
213+ return closureCache .computeIfAbsent (gav , g -> {
214+ Set <GroupArtifact > closure = new HashSet <>();
215+ try {
216+ for (ResolvedDependency d : resolvePom (g , repositories , downloader , ctx )
217+ .resolveDependencies (Scope .Compile , downloader , ctx )) {
218+ collectClosure (d , closure );
219+ }
220+ } catch (MavenDownloadingException | MavenDownloadingExceptions e ) {
221+ // Best-effort: an unresolvable closure leaves the exclusions unfiltered
214222 }
223+ return closure ;
224+ });
225+ }
215226
216- private ResolvedPom resolvePom (ResolvedGroupArtifactVersion gav , List <MavenRepository > repositories ,
217- MavenPomDownloader downloader , ExecutionContext ctx )
218- throws MavenDownloadingException , MavenDownloadingExceptions {
219- List <MavenRepository > repos = withMavenCentral (repositories );
220- Pom pom = downloader .download (gav .asGroupArtifactVersion (), null , null , repos );
221- return pom .resolve (emptyList (), downloader , repos , ctx );
222- }
227+ private static ResolvedPom resolvePom (ResolvedGroupArtifactVersion gav , List <MavenRepository > repositories ,
228+ MavenPomDownloader downloader , ExecutionContext ctx )
229+ throws MavenDownloadingException , MavenDownloadingExceptions {
230+ List <MavenRepository > repos = withMavenCentral (repositories );
231+ Pom pom = downloader .download (gav .asGroupArtifactVersion (), null , null , repos );
232+ return pom .resolve (emptyList (), downloader , repos , ctx );
233+ }
223234
224- private List <MavenRepository > withMavenCentral (List <MavenRepository > repositories ) {
225- List <MavenRepository > effectiveRepos = new ArrayList <>(repositories );
226- if (effectiveRepos .stream ().noneMatch (r -> r .getUri ().contains ("repo.maven.apache.org" ) ||
227- r .getUri ().contains ("repo1.maven.org" ))) {
228- effectiveRepos .add (MavenRepository .MAVEN_CENTRAL );
229- }
230- return effectiveRepos ;
231- }
235+ private static List <MavenRepository > withMavenCentral (List <MavenRepository > repositories ) {
236+ List <MavenRepository > effectiveRepos = new ArrayList <>(repositories );
237+ if (effectiveRepos .stream ().noneMatch (r -> r .getUri ().contains ("repo.maven.apache.org" ) ||
238+ r .getUri ().contains ("repo1.maven.org" ))) {
239+ effectiveRepos .add (MavenRepository .MAVEN_CENTRAL );
240+ }
241+ return effectiveRepos ;
242+ }
232243
233- private void collectClosure (ResolvedDependency dep , Set <GroupArtifact > closure ) {
234- if (closure .add (dep .getGav ().asGroupArtifact ())) {
235- for (ResolvedDependency transitive : dep .getDependencies ()) {
236- collectClosure (transitive , closure );
237- }
238- }
244+ private static void collectClosure (ResolvedDependency dep , Set <GroupArtifact > closure ) {
245+ if (closure .add (dep .getGav ().asGroupArtifact ())) {
246+ for (ResolvedDependency transitive : dep .getDependencies ()) {
247+ collectClosure (transitive , closure );
239248 }
240- };
249+ }
241250 }
242251
243252 @ Override
@@ -268,6 +277,7 @@ public TreeVisitor<?, ExecutionContext> getVisitor(Accumulator acc) {
268277 String projectId = gradle .getGroup () + ":" + gradle .getName ();
269278 Map <String , Set <TransitiveDependency >> scopeToTransitives =
270279 acc .transitivesByProjectAndScope .getOrDefault (projectId , emptyMap ());
280+ MavenPomDownloader downloader = new MavenPomDownloader (ctx );
271281
272282 for (GradleDependencyConfiguration conf : gradle .getConfigurations ()) {
273283 Set <TransitiveDependency > transitives = getCompatibleGradleTransitives (
@@ -279,7 +289,7 @@ public TreeVisitor<?, ExecutionContext> getVisitor(Accumulator acc) {
279289 for (ResolvedDependency dep : conf .getResolved ()) {
280290 if (dep .isDirect () &&
281291 doesNotMatchArguments (dep ) &&
282- isRedundant (dep , transitives )) {
292+ isRedundant (dep , transitives , gradle . getMavenRepositories (), downloader , ctx )) {
283293 // This direct dependency is transitively provided, remove it
284294 // Don't specify configuration - Gradle's resolved config names differ from declaration names
285295 result = new RemoveDependency (
@@ -295,6 +305,7 @@ public TreeVisitor<?, ExecutionContext> getVisitor(Accumulator acc) {
295305 String projectId = maven .getPom ().getGroupId () + ":" + maven .getPom ().getArtifactId ();
296306 Map <String , Set <TransitiveDependency >> scopeToTransitives =
297307 acc .transitivesByProjectAndScope .getOrDefault (projectId , emptyMap ());
308+ MavenPomDownloader downloader = new MavenPomDownloader (ctx );
298309
299310 // A direct dependency appears under every scope bucket it is visible in; evaluate each
300311 // one once using its own effective scope so a wider transitive scope does not falsely
@@ -308,7 +319,7 @@ public TreeVisitor<?, ExecutionContext> getVisitor(Accumulator acc) {
308319 Scope depScope = Scope .fromName (dep .getRequested ().getScope ());
309320 Set <TransitiveDependency > transitives = scopeToTransitives .getOrDefault (
310321 depScope .name ().toLowerCase (), emptySet ());
311- if (isRedundant (dep , transitives )) {
322+ if (isRedundant (dep , transitives , maven . getPom (). getRepositories (), downloader , ctx )) {
312323 // This direct dependency is transitively provided at the same scope and
313324 // with the same exclusions, remove it.
314325 result = new RemoveDependency (
@@ -326,8 +337,9 @@ private boolean doesNotMatchArguments(ResolvedDependency dep) {
326337 !StringUtils .matchesGlob (dep .getArtifactId (), artifactId );
327338 }
328339
329- private boolean isRedundant (ResolvedDependency dep , Set <TransitiveDependency > transitives ) {
330- Set <GroupArtifact > depExclusions = new HashSet <>(dep .getEffectiveExclusions ());
340+ private boolean isRedundant (ResolvedDependency dep , Set <TransitiveDependency > transitives ,
341+ List <MavenRepository > repositories , MavenPomDownloader downloader , ExecutionContext ctx ) {
342+ Set <GroupArtifact > depExclusions = relevantExclusions (dep , repositories , downloader , ctx , acc .closureCache );
331343 for (TransitiveDependency transitive : transitives ) {
332344 ResolvedGroupArtifactVersion gav = transitive .getGav ();
333345 if (dep .getGroupId ().equals (gav .getGroupId ()) &&
0 commit comments