diff --git a/dependencies/src/main/java/com/google/cloud/tools/opensource/classpath/LinkageProblemCauseAnnotator.java b/dependencies/src/main/java/com/google/cloud/tools/opensource/classpath/LinkageProblemCauseAnnotator.java index 4b1242f04e..c54be34144 100644 --- a/dependencies/src/main/java/com/google/cloud/tools/opensource/classpath/LinkageProblemCauseAnnotator.java +++ b/dependencies/src/main/java/com/google/cloud/tools/opensource/classpath/LinkageProblemCauseAnnotator.java @@ -19,13 +19,17 @@ import static com.google.common.base.Preconditions.checkNotNull; import com.google.cloud.tools.opensource.dependencies.DependencyPath; +import com.google.common.collect.ImmutableList; import java.io.IOException; import java.util.HashMap; import java.util.Map; +import java.util.logging.Logger; import org.eclipse.aether.artifact.Artifact; /** Annotates {@link LinkageProblem}s with {@link LinkageProblemCause}s. */ public final class LinkageProblemCauseAnnotator { + private static final Logger logger = + Logger.getLogger(LinkageProblemCauseAnnotator.class.getName()); private LinkageProblemCauseAnnotator() {} @@ -35,74 +39,100 @@ private LinkageProblemCauseAnnotator() {} * @param classPathBuilder class path builder to resolve dependency graphs * @param rootResult the class path used for generating the linkage problems * @param linkageProblems linkage problems to annotate - * @throws IOException when there is a problem reading JAR files */ public static void annotate( ClassPathBuilder classPathBuilder, ClassPathResult rootResult, - Iterable linkageProblems) - throws IOException { + Iterable linkageProblems) { checkNotNull(classPathBuilder); checkNotNull(rootResult); checkNotNull(linkageProblems); - Map cache = new HashMap<>(); + Map artifactResolutionCache = new HashMap<>(); for (LinkageProblem linkageProblem : linkageProblems) { - ClassFile sourceClass = linkageProblem.getSourceClass(); - ClassPathEntry sourceEntry = sourceClass.getClassPathEntry(); + // Annotating linkage errors is a nice-to-have feature for Linkage Checker plugins. Let's not + // fail the entire process if there are problems, such as classPathBuilder unable to resolve + // one artifact or to return correct dependency tree. + try { + annotateProblem(classPathBuilder, rootResult, artifactResolutionCache, linkageProblem); + } catch (Exception ex) { + logger.warning("Failed to annotate: " + linkageProblem); + linkageProblem.setCause(UnknownCause.getInstance()); + } + } + } - Artifact sourceArtifact = sourceEntry.getArtifact(); + private static void annotateProblem( + ClassPathBuilder classPathBuilder, + ClassPathResult rootResult, + Map artifactResolutionCache, + LinkageProblem linkageProblem) + throws IOException { + ClassFile sourceClass = linkageProblem.getSourceClass(); + ClassPathEntry sourceEntry = sourceClass.getClassPathEntry(); - ClassPathResult subtreeResult = cache.get(sourceArtifact); - if (subtreeResult == null) { - // Resolves the dependency graph with the source artifact at the root. - subtreeResult = classPathBuilder.resolveWithMaven(sourceArtifact); - cache.put(sourceArtifact, subtreeResult); - } + Artifact sourceArtifact = sourceEntry.getArtifact(); - Symbol symbol = linkageProblem.getSymbol(); - ClassPathEntry entryInSubtree = subtreeResult.findEntryBySymbol(symbol); - if (entryInSubtree == null) { - linkageProblem.setCause(UnknownCause.getInstance()); - } else { - Artifact artifactInSubtree = entryInSubtree.getArtifact(); - DependencyPath pathToSourceEntry = rootResult.getDependencyPaths(sourceEntry).get(0); - DependencyPath pathFromSourceEntryToUnselectedEntry = - subtreeResult.getDependencyPaths(entryInSubtree).get(0); - DependencyPath pathToUnselectedEntry = - pathToSourceEntry.concat(pathFromSourceEntryToUnselectedEntry); + ClassPathResult subtreeResult = artifactResolutionCache.get(sourceArtifact); + if (subtreeResult == null) { + // Resolves the dependency graph with the source artifact at the root. + subtreeResult = classPathBuilder.resolveWithMaven(sourceArtifact); + artifactResolutionCache.put(sourceArtifact, subtreeResult); + } - ClassPathEntry selectedEntry = - rootResult.findEntryById( - artifactInSubtree.getGroupId(), artifactInSubtree.getArtifactId()); - if (selectedEntry != null) { - Artifact selectedArtifact = selectedEntry.getArtifact(); - if (!selectedArtifact.getVersion().equals(artifactInSubtree.getVersion())) { - // Different version of that artifact is selected in rootResult - linkageProblem.setCause( - new DependencyConflict( - linkageProblem, - rootResult.getDependencyPaths(selectedEntry).get(0), - pathToUnselectedEntry)); - } else { - // A linkage error was already there when sourceArtifact was built. - linkageProblem.setCause(UnknownCause.getInstance()); - } - } else { - // No artifact that matches groupId and artifactId in rootResult. + Symbol symbol = linkageProblem.getSymbol(); + ClassPathEntry entryInSubtree = subtreeResult.findEntryBySymbol(symbol); + if (entryInSubtree == null) { + linkageProblem.setCause(UnknownCause.getInstance()); + return; + } + Artifact artifactInSubtree = entryInSubtree.getArtifact(); + ImmutableList dependencyPathsToSource = + rootResult.getDependencyPaths(sourceEntry); + if (dependencyPathsToSource.isEmpty()) { + // When an artifact is excluded, it's possible to have no dependency path to sourceEntry. + linkageProblem.setCause(UnknownCause.getInstance()); + return; + } + DependencyPath pathToSourceEntry = dependencyPathsToSource.get(0); + DependencyPath pathFromSourceEntryToUnselectedEntry = + subtreeResult.getDependencyPaths(entryInSubtree).get(0); + DependencyPath pathToUnselectedEntry = + pathToSourceEntry.concat(pathFromSourceEntryToUnselectedEntry); - // Checking exclusion elements in the dependency path - Artifact excludingArtifact = - pathToSourceEntry.findExclusion( - artifactInSubtree.getGroupId(), artifactInSubtree.getArtifactId()); - if (excludingArtifact != null) { - linkageProblem.setCause( - new ExcludedDependency(pathToUnselectedEntry, excludingArtifact)); - } else { - linkageProblem.setCause(new MissingDependency(pathToUnselectedEntry)); - } + ClassPathEntry selectedEntry = + rootResult.findEntryById( + artifactInSubtree.getGroupId(), artifactInSubtree.getArtifactId()); + if (selectedEntry != null) { + Artifact selectedArtifact = selectedEntry.getArtifact(); + if (!selectedArtifact.getVersion().equals(artifactInSubtree.getVersion())) { + // Different version of that artifact is selected in rootResult + ImmutableList pathToSelectedArtifact = + rootResult.getDependencyPaths(selectedEntry); + if (pathToSelectedArtifact.isEmpty()) { + linkageProblem.setCause(UnknownCause.getInstance()); + return; } + linkageProblem.setCause( + new DependencyConflict( + linkageProblem, pathToSelectedArtifact.get(0), pathToUnselectedEntry)); + } else { + // A linkage error was already there when sourceArtifact was built. + linkageProblem.setCause(UnknownCause.getInstance()); + } + } else { + // No artifact that matches groupId and artifactId in rootResult. + + // Checking exclusion elements in the dependency path + Artifact excludingArtifact = + pathToSourceEntry.findExclusion( + artifactInSubtree.getGroupId(), artifactInSubtree.getArtifactId()); + if (excludingArtifact != null) { + linkageProblem.setCause(new ExcludedDependency(pathToUnselectedEntry, excludingArtifact)); + } else { + linkageProblem.setCause(new MissingDependency(pathToUnselectedEntry)); } } } + } diff --git a/dependencies/src/test/java/com/google/cloud/tools/opensource/classpath/LinkageProblemCauseAnnotatorTest.java b/dependencies/src/test/java/com/google/cloud/tools/opensource/classpath/LinkageProblemCauseAnnotatorTest.java index 710548e67f..e63480629f 100644 --- a/dependencies/src/test/java/com/google/cloud/tools/opensource/classpath/LinkageProblemCauseAnnotatorTest.java +++ b/dependencies/src/test/java/com/google/cloud/tools/opensource/classpath/LinkageProblemCauseAnnotatorTest.java @@ -198,4 +198,26 @@ public void testAnnotate_dependencyInSpringRepository() throws IOException, Repo "org.reactivestreams:reactive-streams:1.0.3", Artifacts.toCoordinates(conflict.getPathToArtifactThruSource().getLeaf())); } + + @Test + public void testAnnotate_missingArtifactInTree() throws IOException, RepositoryException { + // The dependency tree of the google-api-client does not contain class "com.Foo" + ClassPathBuilder builder = new ClassPathBuilder(); + ClassPathResult classPathResult = + builder.resolve( + ImmutableList.of(new DefaultArtifact("com.google.api-client:google-api-client:1.27.0")), + false, + DependencyMediation.MAVEN); + + ClassPathEntry googleApiClient = classPathResult.getClassPath().get(0); + ClassNotFoundProblem dummyProblem = + new ClassNotFoundProblem( + new ClassFile(googleApiClient, "com.Foo"), new ClassSymbol("com.Bar")); + + LinkageProblemCauseAnnotator.annotate( + classPathBuilder, classPathResult, ImmutableSet.of(dummyProblem)); + + LinkageProblemCause cause = dummyProblem.getCause(); + assertTrue(cause instanceof UnknownCause); + } } diff --git a/gradle-plugin/src/functionalTest/groovy/com/google/cloud/tools/dependencies/gradle/BuildStatusFunctionalTest.groovy b/gradle-plugin/src/functionalTest/groovy/com/google/cloud/tools/dependencies/gradle/BuildStatusFunctionalTest.groovy index b9e7d05991..05d5bb43c4 100644 --- a/gradle-plugin/src/functionalTest/groovy/com/google/cloud/tools/dependencies/gradle/BuildStatusFunctionalTest.groovy +++ b/gradle-plugin/src/functionalTest/groovy/com/google/cloud/tools/dependencies/gradle/BuildStatusFunctionalTest.groovy @@ -224,4 +224,38 @@ class BuildStatusFunctionalTest extends Specification { result.output.count("Circular dependency for: com.fasterxml.jackson:jackson-bom:2.12.1") == 1 result.task(":linkageCheck").outcome == TaskOutcome.FAILED } + + def "can handle artifacts with pom packaging"() { + buildFile << """ + repositories { + mavenCentral() + } + + dependencies { + // This artifact is pom-packaging and does not have JAR artifacts + compile 'org.eclipse.jetty.aggregate:jetty-all:9.4.7.v20170914' + } + + linkageChecker { + configurations = ['compile'] + } + """ + + when: + def result = GradleRunner.create() + .withProjectDir(testProjectDir.root) + .withArguments('linkageCheck', '--stacktrace') + .withPluginClasspath() + .buildAndFail() + + then: + result.output.contains("Task :linkageCheck") + result.output.contains( + "The valid symbol is in org.eclipse.jetty.alpn:alpn-api:jar:1.1.3.v20160715 at "+ + "org.eclipse.jetty.aggregate:jetty-all:9.4.7.v20170914 (compile) / "+ + "org.eclipse.jetty:jetty-alpn-client:9.4.7.v20170914 (compile) / "+ + "org.eclipse.jetty.alpn:alpn-api:1.1.3.v20160715 (provided) but it was not selected "+ + "because the path contains a provided-scope dependency") + result.task(":linkageCheck").outcome == TaskOutcome.FAILED + } } diff --git a/gradle-plugin/src/main/java/com/google/cloud/tools/dependencies/gradle/LinkageCheckTask.java b/gradle-plugin/src/main/java/com/google/cloud/tools/dependencies/gradle/LinkageCheckTask.java index b4dc7d96c0..fbb1cd2010 100644 --- a/gradle-plugin/src/main/java/com/google/cloud/tools/dependencies/gradle/LinkageCheckTask.java +++ b/gradle-plugin/src/main/java/com/google/cloud/tools/dependencies/gradle/LinkageCheckTask.java @@ -34,6 +34,7 @@ import com.google.common.collect.ImmutableSet; import com.google.common.collect.ListMultimap; import com.google.common.collect.MultimapBuilder; +import java.io.File; import java.io.IOException; import java.nio.file.Path; import java.nio.file.Paths; @@ -314,6 +315,20 @@ private String dependencyPathToArtifacts( return output.toString(); } + private static Artifact artifactWithPomFrom(ResolvedDependency resolvedDependency) { + ModuleVersionIdentifier moduleVersionId = resolvedDependency.getModule().getId(); + DefaultArtifact artifact = + new DefaultArtifact( + moduleVersionId.getGroup(), + moduleVersionId.getName(), + null, + "pom", + moduleVersionId.getVersion(), + null, + (File) null); // no JAR file + return artifact; + } + private static Artifact artifactFrom( ResolvedDependency resolvedDependency, ResolvedArtifact resolvedArtifact) { ModuleVersionIdentifier moduleVersionId = resolvedDependency.getModule().getId(); @@ -357,6 +372,7 @@ private DependencyGraph createDependencyGraph(ResolvedConfiguration configuratio PathToNode item = queue.poll(); ResolvedDependency node = item.getNode(); + // Never null DependencyPath parentPath = item.getParentPath(); // If there are multiple artifacts (with different classifiers) in this node, then the path is @@ -372,18 +388,15 @@ private DependencyGraph createDependencyGraph(ResolvedConfiguration configuratio getLogger() .warn( "The dependency node " + node.getName() + " does not have any artifact. Skipping."); - continue; + path = parentPath.append(new Dependency(artifactWithPomFrom(node), "compile")); + } else { + // For artifacts with classifiers, there can be multiple resolved artifacts for one node + for (ResolvedArtifact artifact : moduleArtifacts) { + path = parentPath.append(dependencyFrom(node, artifact)); + graph.addPath(path); + } } - // For artifacts with classifiers, there can be multiple resolved artifacts for one node - for (ResolvedArtifact artifact : moduleArtifacts) { - // parentPath is null for the first item - path = - parentPath == null - ? new DependencyPath(artifactFrom(node, artifact)) - : parentPath.append(dependencyFrom(node, artifact)); - graph.addPath(path); - } for (ResolvedDependency child : node.getChildren()) { if (visited.add(child)) {