Skip to content

Commit 4c95bd5

Browse files
committed
Add an option enabling DependencyList to validate the resolvability of each dependency
1 parent b77e11c commit 4c95bd5

File tree

3 files changed

+135
-19
lines changed

3 files changed

+135
-19
lines changed

src/main/java/org/openrewrite/java/dependencies/DependencyList.java

Lines changed: 62 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -21,21 +21,28 @@
2121
import org.openrewrite.*;
2222
import org.openrewrite.gradle.marker.GradleDependencyConfiguration;
2323
import org.openrewrite.gradle.marker.GradleProject;
24+
import org.openrewrite.internal.ExceptionUtils;
2425
import org.openrewrite.java.dependencies.table.DependencyListReport;
2526
import org.openrewrite.marker.Markers;
26-
import org.openrewrite.maven.tree.GroupArtifactVersion;
27-
import org.openrewrite.maven.tree.MavenResolutionResult;
28-
import org.openrewrite.maven.tree.ResolvedDependency;
29-
import org.openrewrite.maven.tree.ResolvedGroupArtifactVersion;
27+
import org.openrewrite.maven.MavenDownloadingException;
28+
import org.openrewrite.maven.MavenExecutionContextView;
29+
import org.openrewrite.maven.MavenSettings;
30+
import org.openrewrite.maven.internal.MavenPomDownloader;
31+
import org.openrewrite.maven.table.MavenMetadataFailures;
32+
import org.openrewrite.maven.tree.*;
3033

3134
import java.util.HashSet;
35+
import java.util.Optional;
3236
import java.util.Set;
3337

38+
import static java.util.Collections.emptyMap;
39+
3440
@Value
3541
@EqualsAndHashCode(callSuper = false)
3642
public class DependencyList extends Recipe {
3743

3844
transient DependencyListReport report = new DependencyListReport(this);
45+
transient MavenMetadataFailures metadataFailures = new MavenMetadataFailures(this);
3946

4047
@Option(displayName = "Scope",
4148
description = "The scope of the dependencies to include in the report.",
@@ -49,6 +56,13 @@ public class DependencyList extends Recipe {
4956
example = "true")
5057
boolean includeTransitive;
5158

59+
@Option(displayName = "Validate dependencies are resolvable",
60+
description = "When enabled the recipe will attempt to download every dependency it encounters, reporting on any failures. " +
61+
"This can be useful for identifying dependencies that have become unavailable since an LST was produced.",
62+
valid = {"true", "false"},
63+
example = "true")
64+
boolean validateResolvable;
65+
5266
/**
5367
* Freestanding gradle script plugins get assigned the same GradleProject marker with the build script in the project.
5468
* Keep track of the ones which have been seen to minimize duplicate entries in the report.
@@ -79,16 +93,16 @@ public TreeVisitor<?, ExecutionContext> getVisitor() {
7993
m.findFirst(GradleProject.class)
8094
.filter(gradle -> seenGradleProjects.add(new GroupArtifactVersion(gradle.getGroup(), gradle.getName(), gradle.getVersion())))
8195
.ifPresent(gradle -> {
82-
GradleDependencyConfiguration conf = gradle.getConfiguration(scope.asGradleConfigurationName());
83-
if (conf != null) {
84-
for (ResolvedDependency dep : conf.getResolved()) {
85-
if (dep.getDepth() > 0) {
86-
continue;
96+
GradleDependencyConfiguration conf = gradle.getConfiguration(scope.asGradleConfigurationName());
97+
if (conf != null) {
98+
for (ResolvedDependency dep : conf.getResolved()) {
99+
if (dep.getDepth() > 0) {
100+
continue;
101+
}
102+
insertDependency(ctx, gradle, seen, dep, true);
103+
}
87104
}
88-
insertDependency(ctx, gradle, seen, dep, true);
89-
}
90-
}
91-
});
105+
});
92106
m.findFirst(MavenResolutionResult.class).ifPresent(maven -> {
93107
for (ResolvedDependency dep : maven.getDependencies().get(scope.asMavenScope())) {
94108
if (dep.getDepth() > 0) {
@@ -106,6 +120,19 @@ private void insertDependency(ExecutionContext ctx, GradleProject gradle, Set<Re
106120
if (!seen.add(dep.getGav())) {
107121
return;
108122
}
123+
String resolutionFailure = "";
124+
if (validateResolvable) {
125+
try {
126+
//noinspection DataFlowIssue
127+
metadataFailures.insertRows(ctx, () -> new MavenPomDownloader(
128+
emptyMap(), ctx,
129+
null,
130+
null)
131+
.downloadMetadata(new GroupArtifact(gradle.getGroup(), gradle.getName()), null, gradle.getMavenRepositories()));
132+
} catch (MavenDownloadingException e) {
133+
resolutionFailure = ExceptionUtils.sanitizeStackTrace(e, RecipeScheduler.class);
134+
}
135+
}
109136
report.insertRow(ctx, new DependencyListReport.Row(
110137
"Gradle",
111138
gradle.getGroup(),
@@ -114,7 +141,8 @@ private void insertDependency(ExecutionContext ctx, GradleProject gradle, Set<Re
114141
dep.getGroupId(),
115142
dep.getArtifactId(),
116143
dep.getVersion(),
117-
direct
144+
direct,
145+
resolutionFailure
118146
));
119147
if (includeTransitive) {
120148
for (ResolvedDependency transitive : dep.getDependencies()) {
@@ -127,6 +155,24 @@ private void insertDependency(ExecutionContext ctx, MavenResolutionResult maven,
127155
if (!seen.add(dep.getGav())) {
128156
return;
129157
}
158+
String resolutionFailure = "";
159+
if (validateResolvable) {
160+
try {
161+
MavenExecutionContextView mctx = MavenExecutionContextView.view(ctx);
162+
metadataFailures.insertRows(ctx, () -> new MavenPomDownloader(
163+
emptyMap(), ctx,
164+
mctx.getSettings() == null ? maven.getMavenSettings() :
165+
maven.getMavenSettings() == null ? mctx.getSettings() :
166+
mctx.getSettings().merge(maven.getMavenSettings()),
167+
Optional.ofNullable(mctx.getSettings())
168+
.map(MavenSettings::getActiveProfiles)
169+
.map(MavenSettings.ActiveProfiles::getActiveProfiles)
170+
.orElse(maven.getActiveProfiles()))
171+
.downloadMetadata(new GroupArtifact(maven.getPom().getGroupId(), maven.getPom().getArtifactId()), null, maven.getPom().getRepositories()));
172+
} catch (MavenDownloadingException e) {
173+
resolutionFailure = ExceptionUtils.sanitizeStackTrace(e, RecipeScheduler.class);
174+
}
175+
}
130176
report.insertRow(ctx, new DependencyListReport.Row(
131177
"Maven",
132178
maven.getPom().getGroupId(),
@@ -135,7 +181,8 @@ private void insertDependency(ExecutionContext ctx, MavenResolutionResult maven,
135181
dep.getGroupId(),
136182
dep.getArtifactId(),
137183
dep.getVersion(),
138-
direct
184+
direct,
185+
resolutionFailure
139186
));
140187
if (includeTransitive) {
141188
for (ResolvedDependency transitive : dep.getDependencies()) {

src/main/java/org/openrewrite/java/dependencies/table/DependencyListReport.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,5 +64,9 @@ public static class Row {
6464
description = "When `true` the project directly depends on the dependency. When `false` the project " +
6565
"depends on the dependency transitively through at least one direct dependency.")
6666
boolean direct;
67+
68+
@Column(displayName = "Resolution failure",
69+
description = "The reason why the dependency could not be resolved. Blank when resolution was not attempted.")
70+
String resolutionFailure;
6771
}
6872
}

src/test/java/org/openrewrite/java/dependencies/DependencyListTest.java

Lines changed: 69 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,21 +17,28 @@
1717

1818
import org.junit.jupiter.api.Test;
1919
import org.openrewrite.java.dependencies.table.DependencyListReport;
20+
import org.openrewrite.maven.tree.*;
2021
import org.openrewrite.test.RecipeSpec;
2122
import org.openrewrite.test.RewriteTest;
2223

24+
import java.nio.file.Paths;
25+
import java.util.Collections;
26+
27+
import static java.util.Collections.*;
2328
import static org.assertj.core.api.Assertions.assertThat;
29+
import static org.openrewrite.Tree.randomId;
2430
import static org.openrewrite.gradle.Assertions.buildGradle;
2531
import static org.openrewrite.gradle.Assertions.settingsGradle;
2632
import static org.openrewrite.gradle.toolingapi.Assertions.withToolingApi;
2733
import static org.openrewrite.maven.Assertions.pomXml;
34+
import static org.openrewrite.xml.Assertions.xml;
2835

2936
@SuppressWarnings("GroovyUnusedAssignment")
3037
class DependencyListTest implements RewriteTest {
3138

3239
@Override
3340
public void defaults(RecipeSpec spec) {
34-
spec.recipe(new DependencyList(DependencyList.Scope.Compile, true));
41+
spec.recipe(new DependencyList(DependencyList.Scope.Compile, true, false));
3542
}
3643

3744
@Test
@@ -84,13 +91,13 @@ void basic() {
8491
@Test
8592
void directOnly() {
8693
rewriteRun(
87-
spec -> spec.recipe(new DependencyList(DependencyList.Scope.Compile, false))
94+
spec -> spec.recipe(new DependencyList(DependencyList.Scope.Compile, false, false))
8895
.beforeRecipe(withToolingApi())
8996
.dataTable(DependencyListReport.Row.class, rows -> {
9097
assertThat(rows)
9198
.containsExactlyInAnyOrder(
92-
new DependencyListReport.Row("Gradle", "com.test", "test", "1.0.0","io.micrometer.prometheus", "prometheus-rsocket-client", "1.5.3", true),
93-
new DependencyListReport.Row("Maven", "com.test", "test", "1.0.0","io.micrometer.prometheus", "prometheus-rsocket-client", "1.5.3", true));
99+
new DependencyListReport.Row("Gradle", "com.test", "test", "1.0.0", "io.micrometer.prometheus", "prometheus-rsocket-client", "1.5.3", true, ""),
100+
new DependencyListReport.Row("Maven", "com.test", "test", "1.0.0", "io.micrometer.prometheus", "prometheus-rsocket-client", "1.5.3", true, ""));
94101
}),
95102
settingsGradle("rootProject.name = 'test'"),
96103
buildGradle(
@@ -126,4 +133,62 @@ void directOnly() {
126133
""")
127134
);
128135
}
136+
137+
@Test
138+
void validateResolvable() {
139+
rewriteRun(
140+
spec -> spec.recipe(new DependencyList(DependencyList.Scope.Compile, false, true))
141+
.dataTable(DependencyListReport.Row.class, rows -> assertThat(rows)
142+
.singleElement()
143+
.extracting(DependencyListReport.Row::getResolutionFailure)
144+
.matches(it -> it.startsWith("org.openrewrite.maven.MavenDownloadingException"))),
145+
xml(
146+
//language=xml
147+
"""
148+
<project>
149+
<groupId>com.test</groupId>
150+
<artifactId>test</artifactId>
151+
<version>1.0.0</version>
152+
<dependencies>
153+
<dependency>
154+
<groupId>com.test</groupId>
155+
<artifactId>doesnotexist</artifactId>
156+
<version>1.0.0</version>
157+
</dependency>
158+
</dependencies>
159+
</project>
160+
""",
161+
spec -> {
162+
// Manually construct a resolution result since, obviously, the above cannot be resolved
163+
MavenRepository pretendRepo = MavenRepository.builder()
164+
.id("nonexistent")
165+
.uri("https://nonexistent")
166+
.build();
167+
Dependency requested = Dependency.builder()
168+
.gav(new GroupArtifactVersion("com.test", "doesnotexist", "1.0.0"))
169+
.build();
170+
ResolvedGroupArtifactVersion rgav = new ResolvedGroupArtifactVersion(pretendRepo.getId(), "com.test", "doesnotexist", "1.0.0", null);
171+
spec.path(Paths.get("pom.xml"))
172+
.markers(new MavenResolutionResult(
173+
randomId(),
174+
null,
175+
ResolvedPom.builder()
176+
.requested(Pom.builder()
177+
.gav(rgav)
178+
.build())
179+
.repositories(singletonList(pretendRepo))
180+
.build(),
181+
emptyList(),
182+
null,
183+
singletonMap(Scope.Compile, singletonList(new ResolvedDependency(
184+
pretendRepo,
185+
rgav, requested, emptyList(), emptyList(), null, null, null, 0, null)
186+
)),
187+
null,
188+
Collections.emptyList()
189+
));
190+
}
191+
)
192+
);
193+
}
129194
}

0 commit comments

Comments
 (0)