Skip to content
Merged
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 @@ -64,6 +64,10 @@ public static class Accumulator {
public static class TransitiveDependency {
ResolvedGroupArtifactVersion gav;
Set<GroupArtifact> exclusions;
// The direct dependency whose closure supplied this entry, so that a dependency is never
// considered redundant on the strength of its own transitives. Without this a glob such as
// `*` would match every declaration and, in the absence of any other guard, delete them all.
GroupArtifact providedBy;
}

@Override
Expand Down Expand Up @@ -146,6 +150,7 @@ private void resolveTransitivesFromPom(
ExecutionContext ctx,
Set<TransitiveDependency> transitives) {
List<MavenRepository> repos = withMavenCentral(repositories);
GroupArtifact providedBy = new GroupArtifact(gav.getGroupId(), gav.getArtifactId());
try {
// Get the resolved dependencies for compile scope (which includes most transitives)
Pom pom = downloader.download(gav.asGroupArtifactVersion(), null, null, repos);
Expand All @@ -155,7 +160,7 @@ private void resolveTransitivesFromPom(
// Collect all dependencies (both direct and transitive of the parent)
Set<ResolvedGroupArtifactVersion> visited = new HashSet<>();
for (ResolvedDependency dep : patchedPom.resolveDependencies(Scope.Compile, downloader, ctx)) {
collectAllDependencies(dep, transitives, visited);
collectAllDependencies(dep, transitives, visited, providedBy);
}
} catch (MavenDownloadingException | MavenDownloadingExceptions e) {
// If we can't download/resolve the POM, fall back to not detecting redundancies
Expand All @@ -173,11 +178,11 @@ private ResolvedPom applyExclusions(ResolvedPom resolvedPom, List<GroupArtifact>
}

private void collectAllDependencies(ResolvedDependency dep, Set<TransitiveDependency> transitives,
Set<ResolvedGroupArtifactVersion> visited) {
Set<ResolvedGroupArtifactVersion> visited, GroupArtifact providedBy) {
if (visited.add(dep.getGav())) {
transitives.add(new TransitiveDependency(dep.getGav(), declaredExclusions(dep)));
transitives.add(new TransitiveDependency(dep.getGav(), declaredExclusions(dep), providedBy));
for (ResolvedDependency transitive : dep.getDependencies()) {
collectAllDependencies(transitive, transitives, visited);
collectAllDependencies(transitive, transitives, visited, providedBy);
}
}
}
Expand Down Expand Up @@ -239,7 +244,6 @@ public TreeVisitor<?, ExecutionContext> getVisitor(Accumulator acc) {

for (ResolvedDependency dep : conf.getResolved()) {
if (dep.isDirect() &&
doesNotMatchArguments(dep) &&
isRedundant(dep, transitives)) {
// This direct dependency is transitively provided, remove it
// Don't specify configuration - Gradle's resolved config names differ from declaration names
Expand All @@ -264,7 +268,6 @@ public TreeVisitor<?, ExecutionContext> getVisitor(Accumulator acc) {
for (List<ResolvedDependency> deps : maven.getDependencies().values()) {
for (ResolvedDependency dep : deps) {
if (dep.isDirect() &&
doesNotMatchArguments(dep) &&
processed.add(dep.getGroupId() + ":" + dep.getArtifactId())) {
Scope depScope = Scope.fromName(dep.getRequested().getScope());
Set<TransitiveDependency> transitives = scopeToTransitives.getOrDefault(
Expand All @@ -282,25 +285,27 @@ public TreeVisitor<?, ExecutionContext> getVisitor(Accumulator acc) {
return result;
}

private boolean doesNotMatchArguments(ResolvedDependency dep) {
return !StringUtils.matchesGlob(dep.getGroupId(), groupId) ||
!StringUtils.matchesGlob(dep.getArtifactId(), artifactId);
}

private boolean isRedundant(ResolvedDependency dep, Set<TransitiveDependency> transitives) {
Set<GroupArtifact> depExclusions = declaredExclusions(dep);
for (TransitiveDependency transitive : transitives) {
ResolvedGroupArtifactVersion gav = transitive.getGav();
if (dep.getGroupId().equals(gav.getGroupId()) &&
dep.getArtifactId().equals(gav.getArtifactId()) &&
dep.getVersion().equals(gav.getVersion()) &&
depExclusions.equals(transitive.getExclusions())) {
depExclusions.equals(transitive.getExclusions()) &&
!isProvidedByItself(dep, transitive)) {
return true;
}
}
return false;
}

private boolean isProvidedByItself(ResolvedDependency dep, TransitiveDependency transitive) {
GroupArtifact providedBy = transitive.getProvidedBy();
return dep.getGroupId().equals(providedBy.getGroupId()) &&
dep.getArtifactId().equals(providedBy.getArtifactId());
}

private Set<TransitiveDependency> getCompatibleGradleTransitives(
Map<String, Set<TransitiveDependency>> scopeToTransitives,
String targetScope) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -780,4 +780,245 @@ void removeRedundantGradleDependency() {
)
);
}

@Test
void globGroupIdMatchesProvider() {
rewriteRun(
spec -> spec.recipe(new RemoveRedundantDependencies(
"com.fasterxml.jackson.*", "jackson-databind")),
//language=xml
pomXml(JACKSON_BEFORE, JACKSON_AFTER)
);
}

@Test
void singleCharacterWildcardMatchesProvider() {
rewriteRun(
spec -> spec.recipe(new RemoveRedundantDependencies(
"com.fasterxml.jackson.cor?", "jackson-databin?")),
//language=xml
pomXml(JACKSON_BEFORE, JACKSON_AFTER)
);
}

@Test
void wildcardArtifactIdRemovesDependenciesProvidedBySibling() {
rewriteRun(
spec -> spec.recipe(new RemoveRedundantDependencies(
"com.fasterxml.jackson.core", "*")),
//language=xml
pomXml(JACKSON_BEFORE, JACKSON_AFTER)
);
}

@Test
void matchAllWildcardsRemoveEveryTransitivelyProvidedDependency() {
rewriteRun(
spec -> spec.recipe(new RemoveRedundantDependencies("*", "*")),
//language=xml
pomXml(
"""
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>com.mycompany.app</groupId>
<artifactId>my-app</artifactId>
<version>1</version>
<dependencies>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.17.0</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.17.0</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>2.17.0</version>
</dependency>
</dependencies>
</project>
""",
"""
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>com.mycompany.app</groupId>
<artifactId>my-app</artifactId>
<version>1</version>
<dependencies>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.17.0</version>
</dependency>
</dependencies>
</project>
"""
)
);
}

@Test
void matchAllWildcardsKeepDependenciesNobodyElseProvides() {
rewriteRun(
spec -> spec.recipe(new RemoveRedundantDependencies("*", "*")),
//language=xml
pomXml(
"""
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>com.mycompany.app</groupId>
<artifactId>my-app</artifactId>
<version>1</version>
<dependencies>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.17.0</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.14.0</version>
</dependency>
</dependencies>
</project>
"""
)
);
}

@Test
void removesStarterMatchedByTheSameGlobAsItsProvider() {
// spring-boot-starter-web transitively provides spring-boot-starter-json. Both match the
// `spring-boot-starter-*` glob, so the redundant one must still be removed.
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.springframework.boot</groupId>
<artifactId>spring-boot-starter-json</artifactId>
</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 removeRedundantGradleDependencyWithWildcardArtifactId() {
rewriteRun(
spec -> spec.beforeRecipe(withToolingApi())
.recipe(new RemoveRedundantDependencies(
"com.fasterxml.jackson.core", "*")),
mavenProject("my-app",
//language=groovy
buildGradle(
"""
plugins {
id 'java-library'
}
repositories {
mavenCentral()
}
dependencies {
implementation 'com.fasterxml.jackson.core:jackson-databind:2.17.0'
implementation 'com.fasterxml.jackson.core:jackson-core:2.17.0'
}
""",
"""
plugins {
id 'java-library'
}
repositories {
mavenCentral()
}
dependencies {
implementation 'com.fasterxml.jackson.core:jackson-databind:2.17.0'
}
"""
)
)
);
}

private static final String JACKSON_BEFORE = """
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>com.mycompany.app</groupId>
<artifactId>my-app</artifactId>
<version>1</version>
<dependencies>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.17.0</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.17.0</version>
</dependency>
</dependencies>
</project>
""";

private static final String JACKSON_AFTER = """
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>com.mycompany.app</groupId>
<artifactId>my-app</artifactId>
<version>1</version>
<dependencies>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.17.0</version>
</dependency>
</dependencies>
</project>
""";
}
Loading