diff --git a/src/main/java/org/openrewrite/java/dependencies/DependencyResolutionDiagnostic.java b/src/main/java/org/openrewrite/java/dependencies/DependencyResolutionDiagnostic.java new file mode 100644 index 0000000..8546df8 --- /dev/null +++ b/src/main/java/org/openrewrite/java/dependencies/DependencyResolutionDiagnostic.java @@ -0,0 +1,144 @@ +package org.openrewrite.java.dependencies; + +import lombok.EqualsAndHashCode; +import lombok.Value; +import org.openrewrite.*; +import org.openrewrite.gradle.marker.GradleDependencyConfiguration; +import org.openrewrite.gradle.marker.GradleProject; +import org.openrewrite.groovy.GroovyIsoVisitor; +import org.openrewrite.groovy.tree.G; +import org.openrewrite.internal.lang.Nullable; +import org.openrewrite.java.dependencies.table.RepositoryAccessibilityReport; +import org.openrewrite.marker.SearchResult; +import org.openrewrite.maven.MavenDownloadingException; +import org.openrewrite.maven.internal.MavenPomDownloader; +import org.openrewrite.maven.tree.GroupArtifactVersion; +import org.openrewrite.maven.tree.MavenRepository; +import org.openrewrite.maven.tree.MavenResolutionResult; + +import java.util.*; + +import static java.util.Collections.*; + +@Value +@EqualsAndHashCode(callSuper = true) +public class DependencyResolutionDiagnostic extends ScanningRecipe { + + transient RepositoryAccessibilityReport report = new RepositoryAccessibilityReport(this); + + @Override + public String getDisplayName() { + return "Dependency resolution diagnostic"; + } + + @Override + public String getDescription() { + return "Recipes which manipulate dependencies must be able to successfully access the repositories used by the " + + "project and retrieve dependency metadata from them. This recipe lists the repositories that were found " + + "and whether or not dependency metadata could successfully be resolved from them."; + } + + public static class Accumulator { + Set mavenRepositories = new HashSet<>(); + } + + @Override + public Accumulator getInitialValue(ExecutionContext ctx) { + return new Accumulator(); + } + + @Override + public TreeVisitor getScanner(Accumulator acc) { + return new TreeVisitor() { + @Override + public @Nullable Tree visit(@Nullable Tree tree, ExecutionContext ctx) { + if(tree == null) { + return null; + } + tree.getMarkers().findFirst(GradleProject.class).ifPresent(gp -> { + acc.mavenRepositories.addAll(gp.getMavenRepositories()); + acc.mavenRepositories.addAll(gp.getMavenPluginRepositories()); + }); + tree.getMarkers().findFirst(MavenResolutionResult.class).ifPresent(mrr -> + acc.mavenRepositories.addAll(mrr.getPom().getRepositories())); + return tree; + } + }; + } + + @Override + public Collection generate(Accumulator acc, ExecutionContext ctx) { + MavenPomDownloader mpd = new MavenPomDownloader(ctx); + // Since there's no dependency that every repository can be guaranteed to have, try to download one that + // doesn't exist and interpret non-404 results as failure + GroupArtifactVersion gav = new GroupArtifactVersion("org.openrewrite.nonexistent", "nonexistent", "0.0.0"); + Set succeeded = new HashSet<>(); + Map failed = new HashMap<>(); + for (MavenRepository repo : acc.mavenRepositories) { + String uri = noTrailingSlash(repo.getUri()); + if(succeeded.contains(uri) || failed.containsKey(uri)) { + continue; + } + try { + mpd.download(gav, null, null, Collections.singletonList(repo)); + } catch (MavenDownloadingException e) { + if(e.getRepositoryResponses().isEmpty()) { + failed.put(uri, "No response from repository"); + } + for (Map.Entry result : e.getRepositoryResponses().entrySet()) { + if (result.getValue().contains("404") || + "Did not attempt to download because of a previous failure to retrieve from this repository.".equals(result.getValue()) || + "Local repository does not contain pom".equals(result.getValue())) { + succeeded.add(noTrailingSlash(result.getKey().getUri())); + } else { + failed.put(noTrailingSlash(result.getKey().getUri()), result.getValue()); + } + } + } + } + for(String uri : succeeded) { + report.insertRow(ctx, new RepositoryAccessibilityReport.Row(uri, "")); + } + for (Map.Entry uriToFailure : failed.entrySet()) { + report.insertRow(ctx, new RepositoryAccessibilityReport.Row(uriToFailure.getKey(), uriToFailure.getValue())); + } + + return emptyList(); + } + + private static String noTrailingSlash(String uri) { + if(uri.endsWith("/")) { + return uri.substring(0, uri.length() - 1); + } + return uri; + } + + @Override + public TreeVisitor getVisitor(Accumulator acc) { + return new GroovyIsoVisitor() { + @Override + public G.CompilationUnit visitCompilationUnit(G.CompilationUnit cu, ExecutionContext ctx) { + Optional maybeGp = cu.getMarkers().findFirst(GradleProject.class); + if (!maybeGp.isPresent()) { + return cu; + } + GradleProject gp = maybeGp.get(); + G.CompilationUnit g = super.visitCompilationUnit(cu, ctx); + + for (GradleDependencyConfiguration conf : gp.getConfigurations()) { + //noinspection ConstantValue + if (conf.getExceptionType() == null) { + continue; + } + g = SearchResult.found(g, "Found Gradle dependency configuration which failed to resolve during parsing: " + + conf.getName() + ": " + conf.getExceptionType() + " - " + conf.getMessage()); + // If one configuration failed to resolve, others likely failed and probably for the same reasons + // Record only first failure to reduce noise + break; + } + + return g; + } + }; + } +} diff --git a/src/main/java/org/openrewrite/java/dependencies/table/RepositoryAccessibilityReport.java b/src/main/java/org/openrewrite/java/dependencies/table/RepositoryAccessibilityReport.java new file mode 100644 index 0000000..29020c1 --- /dev/null +++ b/src/main/java/org/openrewrite/java/dependencies/table/RepositoryAccessibilityReport.java @@ -0,0 +1,28 @@ +package org.openrewrite.java.dependencies.table; + +import lombok.Value; +import org.openrewrite.Column; +import org.openrewrite.DataTable; +import org.openrewrite.Recipe; + +public class RepositoryAccessibilityReport extends DataTable { + + public RepositoryAccessibilityReport(Recipe recipe) { + super(recipe, + "Repository accessibility report", + "Listing of all dependency repositories and whether they are accessible."); + + } + + @Value + public static class Row { + @Column(displayName = "Repository URI", + description = "The URI of the repository") + String uri; + + @Column(displayName = "Error message", + description = "Empty if the repository was accessible. Otherwise, the error message encountered when " + + "attempting to access the repository.") + String errorMessage; + } +} diff --git a/src/test/java/org/openrewrite/java/dependencies/DependencyResolutionDiagnosticTest.java b/src/test/java/org/openrewrite/java/dependencies/DependencyResolutionDiagnosticTest.java new file mode 100644 index 0000000..50876de --- /dev/null +++ b/src/test/java/org/openrewrite/java/dependencies/DependencyResolutionDiagnosticTest.java @@ -0,0 +1,72 @@ +package org.openrewrite.java.dependencies; + +import org.junit.jupiter.api.Test; +import org.openrewrite.java.dependencies.table.RepositoryAccessibilityReport; +import org.openrewrite.test.RecipeSpec; +import org.openrewrite.test.RewriteTest; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.openrewrite.gradle.Assertions.buildGradle; +import static org.openrewrite.gradle.Assertions.withToolingApi; + +public class DependencyResolutionDiagnosticTest implements RewriteTest { + + @Override + public void defaults(RecipeSpec spec) { + spec.recipe(new DependencyResolutionDiagnostic()); + } + + @Test + void gradle() { + rewriteRun( + spec -> spec.beforeRecipe(withToolingApi()) + .dataTable(RepositoryAccessibilityReport.Row.class, rows -> { + assertThat(rows).contains( + new RepositoryAccessibilityReport.Row("https://repo.maven.apache.org/maven2", "")); + assertThat(rows).filteredOn(row -> row.getUri().startsWith("file:/") && "".equals(row.getErrorMessage())).hasSize(1); + assertThat(rows).contains( + new RepositoryAccessibilityReport.Row("https://repo.maven.apache.org/maven2", "") + ); + assertThat(rows).contains( + new RepositoryAccessibilityReport.Row("https://nonexistent.moderne.io/maven2", "No response from repository") + ); + }), + //language=groovy + buildGradle(""" + plugins { + id("java") + } + repositories { + mavenLocal() + mavenCentral() + maven { + url "https://nonexistent.moderne.io/maven2" + } + } + + dependencies { + implementation("org.openrewrite.nonexistent:nonexistent:0.0.0") + } + """ + // It is a limitation of the tooling API which prevents configuration-granularity error information from being collected. + // When run with real Gradle this recipe _should_ produce the following result, included here for documentation. +// ,""" +// ~~(Found Gradle dependency configuration which failed to resolve during parsing)~~>plugins { +// id("java") +// } +// repositories { +// mavenLocal() +// mavenCentral() +// maven { +// url "https://nonexistent.moderne.io/maven2" +// } +// } +// +// dependencies { +// implementation("org.openrewrite.nonexistent:nonexistent:0.0.0") +// } +// """ + ) + ); + } +}