Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ public class RemoveRedundantDependencies extends ScanningRecipe<RemoveRedundantD
public static class Accumulator {
// Map from project identifier -> scope/configuration -> Set of transitive dependencies
Map<String, Map<String, Set<TransitiveDependency>>> transitivesByProjectAndScope;
// Cache of each coordinate's own clean dependency closure, shared across all source files in the run
Map<ResolvedGroupArtifactVersion, Set<GroupArtifact>> closureCache;
}

@Value
Expand All @@ -68,7 +70,7 @@ public static class TransitiveDependency {

@Override
public Accumulator getInitialValue(ExecutionContext ctx) {
return new Accumulator(new HashMap<>());
return new Accumulator(new HashMap<>(), new HashMap<>());
}

@Override
Expand Down Expand Up @@ -145,24 +147,17 @@ private void resolveTransitivesFromPom(
MavenPomDownloader downloader,
ExecutionContext ctx,
Set<TransitiveDependency> transitives) {
List<MavenRepository> effectiveRepos = withMavenCentral(repositories);
try {
// Ensure we have Maven Central in the repositories
List<MavenRepository> effectiveRepos = new ArrayList<>(repositories);
if (effectiveRepos.stream().noneMatch(r -> r.getUri().contains("repo.maven.apache.org") ||
r.getUri().contains("repo1.maven.org"))) {
effectiveRepos.add(MavenRepository.MAVEN_CENTRAL);
}

// Get the resolved dependencies for compile scope (which includes most transitives)
Pom pom = downloader.download(gav.asGroupArtifactVersion(), null, null, effectiveRepos);
ResolvedPom resolvedPom = pom.resolve(emptyList(), downloader, effectiveRepos, ctx);
ResolvedPom resolvedPom = resolvePom(gav, effectiveRepos, downloader, ctx);
ResolvedPom patchedPom = applyExclusions(resolvedPom, effectiveExclusions);
List<ResolvedDependency> resolved = patchedPom.resolveDependencies(Scope.Compile, downloader, ctx);

// Collect all dependencies (both direct and transitive of the parent)
Set<ResolvedGroupArtifactVersion> visited = new HashSet<>();
for (ResolvedDependency dep : resolved) {
collectAllDependencies(dep, transitives, visited);
collectAllDependencies(dep, transitives, visited, effectiveRepos, downloader, ctx);
}
} catch (MavenDownloadingException | MavenDownloadingExceptions e) {
// If we can't download/resolve the POM, fall back to not detecting redundancies
Expand All @@ -180,11 +175,65 @@ private ResolvedPom applyExclusions(ResolvedPom resolvedPom, List<GroupArtifact>
}

private void collectAllDependencies(ResolvedDependency dep, Set<TransitiveDependency> transitives,
Set<ResolvedGroupArtifactVersion> visited) {
Set<ResolvedGroupArtifactVersion> visited, List<MavenRepository> repositories,
MavenPomDownloader downloader, ExecutionContext ctx) {
if (visited.add(dep.getGav())) {
transitives.add(new TransitiveDependency(dep.getGav(), new HashSet<>(dep.getEffectiveExclusions())));
transitives.add(new TransitiveDependency(dep.getGav(),
relevantExclusions(dep, repositories, downloader, ctx)));
for (ResolvedDependency transitive : dep.getDependencies()) {
collectAllDependencies(transitive, transitives, visited, repositories, downloader, ctx);
}
}
}

// Effective exclusions are resolved in the parent's whole tree, so they pick up no-op exclusions
// from sibling branches; keep only those targeting this coordinate's own dependency closure.
private Set<GroupArtifact> relevantExclusions(ResolvedDependency dep, List<MavenRepository> repositories,
MavenPomDownloader downloader, ExecutionContext ctx) {
Set<GroupArtifact> exclusions = new HashSet<>(dep.getEffectiveExclusions());
if (!exclusions.isEmpty()) {
exclusions.retainAll(dependencyClosure(dep.getGav(), repositories, downloader, ctx));
}
return exclusions;
}

private Set<GroupArtifact> dependencyClosure(ResolvedGroupArtifactVersion gav, List<MavenRepository> repositories,
MavenPomDownloader downloader, ExecutionContext ctx) {
return acc.closureCache.computeIfAbsent(gav, g -> {
Set<GroupArtifact> closure = new HashSet<>();
try {
for (ResolvedDependency d : resolvePom(g, repositories, downloader, ctx)
.resolveDependencies(Scope.Compile, downloader, ctx)) {
collectClosure(d, closure);
}
} catch (MavenDownloadingException | MavenDownloadingExceptions e) {
// Best-effort: an unresolvable closure leaves the exclusions unfiltered
}
return closure;
});
}

private ResolvedPom resolvePom(ResolvedGroupArtifactVersion gav, List<MavenRepository> repositories,
MavenPomDownloader downloader, ExecutionContext ctx)
throws MavenDownloadingException, MavenDownloadingExceptions {
List<MavenRepository> repos = withMavenCentral(repositories);
Pom pom = downloader.download(gav.asGroupArtifactVersion(), null, null, repos);
return pom.resolve(emptyList(), downloader, repos, ctx);
}

private List<MavenRepository> withMavenCentral(List<MavenRepository> repositories) {
List<MavenRepository> effectiveRepos = new ArrayList<>(repositories);
if (effectiveRepos.stream().noneMatch(r -> r.getUri().contains("repo.maven.apache.org") ||
r.getUri().contains("repo1.maven.org"))) {
effectiveRepos.add(MavenRepository.MAVEN_CENTRAL);
}
return effectiveRepos;
}

private void collectClosure(ResolvedDependency dep, Set<GroupArtifact> closure) {
if (closure.add(dep.getGav().asGroupArtifact())) {
for (ResolvedDependency transitive : dep.getDependencies()) {
collectAllDependencies(transitive, transitives, visited);
collectClosure(transitive, closure);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -495,6 +495,168 @@ void keepsDirectCompileTomcatEmbedCoreWhenProviderIsProvidedScoped() {
);
}

@Test
void removesJakartaClientWhenTransitiveExclusionsAreEquivalent() {
// In the starter's tree the transitive jakarta.ws.rs-api gains a spurious exclusion of
// jakarta.activation-api (which it can never bring on its own); the direct declaration has none,
// so the two are effectively equivalent and the direct one is redundant.
rewriteRun(
spec -> spec.recipe(new RemoveRedundantDependencies(
"org.springframework.boot", "spring-boot-starter-*")),
//language=xml
pomXml(
"""
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>com.sample</groupId>
<artifactId>sample</artifactId>
<version>1.0-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.3</version>
<relativePath/>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jersey</artifactId>
</dependency>
<dependency>
<groupId>jakarta.ws.rs</groupId>
<artifactId>jakarta.ws.rs-api</artifactId>
<version>3.1.0</version>
</dependency>
</dependencies>
</project>
""",
"""
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>com.sample</groupId>
<artifactId>sample</artifactId>
<version>1.0-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.3</version>
<relativePath/>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jersey</artifactId>
</dependency>
</dependencies>
</project>
"""
)
);
}

@Test
void keepsJerseyClientWhenDirectExclusionsDifferFromTransitive() {
rewriteRun(
spec -> spec.recipe(new RemoveRedundantDependencies(
"org.springframework.boot", "spring-boot-starter-*")),
//language=xml
pomXml(
"""
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>com.sample</groupId>
<artifactId>sample</artifactId>
<version>1.0-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.3</version>
<relativePath/>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jersey</artifactId>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.core</groupId>
<artifactId>jersey-client</artifactId>
<version>3.1.5</version>
<exclusions>
<exclusion>
<groupId>jakarta.inject</groupId>
<artifactId>jakarta.inject-api</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
</project>
"""
)
);
}

@Test
void removesTomcatEmbedCoreWhenExclusionsMatchTransitive() {
rewriteRun(
spec -> spec.recipe(new RemoveRedundantDependencies(
"org.springframework.boot", "spring-boot-starter-*")),
//language=xml
pomXml(
"""
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>com.sample</groupId>
<artifactId>sample</artifactId>
<version>1.0-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.3</version>
<relativePath/>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-core</artifactId>
<exclusions>
<exclusion>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-annotations-api</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
</project>
""",
"""
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>com.sample</groupId>
<artifactId>sample</artifactId>
<version>1.0-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.3</version>
<relativePath/>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
</project>
"""
)
);
}

@Test
void removeRedundantGradleDependency() {
rewriteRun(
Expand Down
Loading