All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.spotify.missinglink.maven.CheckMojo Maven / Gradle / Ivy

/*
 * Copyright (c) 2015 Spotify AB
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package com.spotify.missinglink.maven;

import com.google.common.base.Joiner;
import com.google.common.base.Stopwatch;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.common.collect.Ordering;
import com.google.common.io.Files;
import com.spotify.missinglink.ArtifactLoader;
import com.spotify.missinglink.Conflict;
import com.spotify.missinglink.Conflict.ConflictCategory;
import com.spotify.missinglink.ConflictChecker;
import com.spotify.missinglink.Java9ModuleLoader;
import com.spotify.missinglink.datamodel.Artifact;
import com.spotify.missinglink.datamodel.ArtifactBuilder;
import com.spotify.missinglink.datamodel.ArtifactName;
import com.spotify.missinglink.datamodel.ClassTypeDescriptor;
import com.spotify.missinglink.datamodel.DeclaredClass;
import com.spotify.missinglink.datamodel.Dependency;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
import org.apache.maven.model.Exclusion;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugins.annotations.LifecyclePhase;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.plugins.annotations.ResolutionScope;
import org.apache.maven.project.MavenProject;

@Mojo(name = "check", requiresDependencyResolution = ResolutionScope.COMPILE_PLUS_RUNTIME,
    defaultPhase = LifecyclePhase.PROCESS_CLASSES)
public class CheckMojo extends AbstractMojo {

  @Parameter(defaultValue = "${project}", readonly = true, required = true)
  protected MavenProject project;

  @Parameter(property = "missinglink.skip")
  protected boolean skip = false;

  /**
   * Controls whether the Maven build should be failed if any dependency conflicts are found.
   * Defaults to false.
   */
  @Parameter(defaultValue = "false", property = "failOnConflicts")
  protected boolean failOnConflicts;

  /**
   * Log verbose output. Defaults to false. When false, logs at debug instead, so use `mvn -X` to
   * see output.
   */
  @Parameter(property = "verbose", defaultValue = "false")
  protected boolean verbose;

  /**
   * Limit the conflict output to only the specified categories. If not set, uses all categories of
   * conflicts.
   */
  @Parameter(property = "missinglink.includeCategories")
  protected List includeCategories = new ArrayList<>();

  /**
   * Include dependencies with the following scopes in conflict checks. Default is "compile, test".
   */
  @Parameter(property = "missinglink.includeScopes", defaultValue = "compile,test")
  protected List includeScopes = new ArrayList<>();

  /**
   * Dependencies of the project to exclude from analysis. Defaults to an empty list. The
   * dependency should be specified as an {@link Exclusion} containing a groupId and artifactId.
   * Classes in these artifacts will not be checked for conflicts.
   */
  @Parameter
  protected List excludeDependencies = new ArrayList<>();

  /**
   * Optional list of packages to ignore conflicts in where the source of the conflict is in one of
   * the specified packages.
   * 

* This parameter does not exclude those packages from analysis, but the plugin will not * output the conflicts that are found in those packages when the caller side of the conflict is * in this package, and they will not count against the {@link #failOnConflicts} setting.

*

* For example, if the package "javax.foo" is in ignoreSourcePackages, then any conflict * found originating in a javax.foo class is ignored. This is mostly the same behavior as {@link * #excludeDependencies} but operates at a package name level instead of a groupId/artifactId * level.

*/ @Parameter protected List ignoreSourcePackages = new ArrayList<>(); /** * Optional list of packages to ignore conflicts in where the destination/called-side of the * conflict is in one of the specified packages. *

* This parameter does not exclude those packages from analysis, but the plugin will not output * the conflicts that are found in those packages when the called side of the conflict is in this * package, and they will not count against the {@link #failOnConflicts} setting.

*

* For example, if the package "javax.bar" is in ignoreDestinationPackages, then any conflict * found having to do with calling a method in a class in javax.bar is ignored.

*/ @Parameter protected List ignoreDestinationPackages = new ArrayList<>(); /** * Optional: can be set to explicitly define the path to use for the bootclasspath * containing the java.* / standard library classes. Note that this value is expected to look like * a classpath - various file paths separated by the path separator. *

* When not set, the bootclasspath is determined by examining the "sun.boot.class.path" system * on java 8 and below. On java 9 and above will use the modules. * property.

*/ @Parameter(property = "misslink.bootClasspath") protected String bootClasspath; // TODO 6/1/15 mbrown -- how to hook into the Plexus container for proper DI lookups and the conventional maven plugin way of how to set up things like this protected ArtifactLoader artifactLoader = new ArtifactLoader(); protected ConflictChecker conflictChecker = new ConflictChecker(); public void execute() throws MojoExecutionException, MojoFailureException { if (skip) { getLog().info("skipping plugin execution since missinglink.skip=true"); return; } // when verbose flag is set, log detailed messages to info log. otherwise log to debug. This is // so that verbose output from this plugin can be seen easily without having to specify mvn -X. final Consumer log = verbose ? msg -> getLog().info(msg) : msg -> getLog().debug(msg); logDependencies(log); final Set categoriesToInclude; try { categoriesToInclude = includeCategories.stream() .map(ConflictCategory::valueOf) .collect(Collectors.toSet()); } catch (IllegalArgumentException e) { getLog().error(e); throw new MojoExecutionException( "Invalid value(s) for 'includeCategories': " + includeCategories + ". " + "Valid choices are: " + Joiner.on(", ").join(ConflictCategory.values())); } Collection conflicts = loadArtifactsAndCheckConflicts(); final int initialCount = conflicts.size(); conflicts = filterConflicts(conflicts, categoriesToInclude); if (conflicts.isEmpty()) { getLog().info("No conflicts found"); } else { String warning = conflicts.size() + " conflicts found!"; if (initialCount != conflicts.size()) { warning += " (" + initialCount + " conflicts were found before applying filters)"; } getLog().warn(warning); outputConflicts(conflicts); if (failOnConflicts) { final String message = conflicts.size() + " class/method conflicts found between source " + "code in this project and the runtime dependencies from the Maven" + " project. Look above for specific descriptions of each conflict"; throw new MojoFailureException(message); } } } private void logDependencies(Consumer log) { // project.getDependencies() only lists the declared dependencies, use .getArtifacts for // the transitive dependencies as well final ArrayList mavenDependencies = Lists.newArrayList(project.getArtifacts()); Collections.sort(mavenDependencies, Ordering.usingToString()); log.accept("Project has " + mavenDependencies.size() + " dependencies"); mavenDependencies.stream() .map(art -> "Dependency: " + art.toString()) .forEach(log); } private Collection filterConflicts(Collection conflicts, Set categoriesToInclude) { if (!categoriesToInclude.isEmpty()) { getLog().debug("Only including conflicts from categories: " + Joiner.on(", ").join(categoriesToInclude)); conflicts = filterConflictsBy(conflicts, categoriesToInclude::contains, num -> num + " conflicts removed based on includeCategories=" + Joiner.on(", ").join(includeCategories) + ". " + "Run plugin again without the 'includeCategories' parameter to see " + "all conflicts that were found."); } if (!ignoreSourcePackages.isEmpty()) { getLog().debug("Ignoring source packages: " + Joiner.on(", ").join(ignoreSourcePackages)); final Predicate predicate = conflict -> !packageIsIgnored(ignoreSourcePackages, conflict.dependency().fromClass()); conflicts = filterConflictsBy(conflicts, predicate, num -> num + " conflicts found in ignored source packages. " + "Run plugin again without the 'ignoreSourcePackages' parameter to see " + "all conflicts that were found."); } if (!ignoreDestinationPackages.isEmpty()) { getLog().debug( "Ignoring destination packages: " + Joiner.on(", ").join(ignoreDestinationPackages)); final Predicate predicate = conflict -> !packageIsIgnored(ignoreDestinationPackages, conflict.dependency().targetClass()); conflicts = filterConflictsBy(conflicts, predicate, num -> num + " conflicts found in ignored destination packages. " + "Run plugin again without the 'ignoreDestinationPackages' parameter to see " + "all conflicts that were found." ); } return conflicts; } /** * Repeated logic for filtering the collection of Conflicts based on a predicate. * * @param conflicts conflicts to filter * @param predicate predicate to filter by * @param logMessage a function that when give the difference in size between the original * collection and filtered collection, produces a message that will be logged * as a warning to the user. * @return filtered conflicts */ private Collection filterConflictsBy(Collection conflicts, Predicate predicate, Function logMessage) { final Set filteredConflicts = conflicts.stream() .filter(predicate) .collect(Collectors.toSet()); if (filteredConflicts.size() != conflicts.size()) { final int diff = conflicts.size() - filteredConflicts.size(); getLog().warn(logMessage.apply(diff)); } return filteredConflicts; } /** * Tests if the Conflict represented by this ClassTypeDescriptor (whether on the source-side or * destination-side) is ignored based on the collection of IgnoredPackages. Reusable logic * between * ignoring source/destination packages. */ private boolean packageIsIgnored(Collection ignoredPackages, ClassTypeDescriptor classTypeDescriptor) { final String className = classTypeDescriptor.getClassName().replace('/', '.'); // this might be missing some corner-cases on naming rules: final String conflictPackageName = className.substring(0, className.lastIndexOf('.')); return ignoredPackages.stream() .anyMatch(p -> { final String ignoredPackageName = p.getPackage(); return conflictPackageName.equals(ignoredPackageName) || (p.isIgnoreSubpackages() && conflictPackageName .startsWith(ignoredPackageName + ".")); }); } private Collection loadArtifactsAndCheckConflicts() { // includes declared and transitive dependencies, anything in the scopes configured to be // included final List projectDeps = this.project.getArtifacts().stream() .filter(artifact -> includeScopes.contains(Scope.valueOf(artifact.getScope()))) .collect(Collectors.toList()); getLog().debug("project dependencies: " + projectDeps.stream() .map(this::mavenCoordinates) .collect(Collectors.toList()) ); Stopwatch stopwatch = Stopwatch.createStarted(); // artifacts in runtime scope from the maven project (including transitives) final ImmutableList runtimeProjectArtifacts = constructArtifacts(projectDeps); stopwatch.stop(); getLog().debug("constructing runtime artifacts took: " + asMillis(stopwatch) + " ms"); // also need to load JDK classes from the bootstrap classpath final String bootstrapClasspath = bootClassPathToUse(); stopwatch.reset().start(); final List bootstrapArtifacts = loadBootstrapArtifacts(bootstrapClasspath); stopwatch.stop(); getLog().debug("constructing bootstrap artifacts took: " + asMillis(stopwatch) + " ms"); final ImmutableList allArtifacts = ImmutableList.builder() .addAll(runtimeProjectArtifacts) .addAll(bootstrapArtifacts) .build(); final ImmutableList runtimeArtifactsAfterExclusions = ImmutableList.copyOf( runtimeProjectArtifacts.stream() .filter(artifact -> !isExcluded(artifact)) .collect(Collectors.toSet()) ); final Artifact projectArtifact = toArtifact(project.getBuild().getOutputDirectory()); if (projectArtifact.classes().isEmpty()) { getLog().warn("No classes found in project build directory" + " - did you run 'mvn compile' first?"); } stopwatch.reset().start(); getLog().debug("Checking for conflicts starting from " + projectArtifact.name().name()); getLog().debug("Artifacts included in the project: "); for (Artifact artifact : runtimeArtifactsAfterExclusions) { getLog().debug(" " + artifact.name().name()); } final Collection conflicts = conflictChecker.check( projectArtifact, runtimeArtifactsAfterExclusions, allArtifacts); stopwatch.stop(); getLog().debug("conflict checking took: " + asMillis(stopwatch) + " ms"); getLog().debug(conflicts.size() + " total conflicts found"); return conflicts; } private List loadBootstrapArtifacts(final String bootstrapClasspath) { if (bootstrapClasspath == null) { return Java9ModuleLoader.getJava9ModuleArtifacts((s, ex) -> getLog().warn(s, ex)); } else { return constructArtifacts(Arrays.asList( bootstrapClasspath.split(System.getProperty("path.separator")))); } } private String bootClassPathToUse() { if (this.bootClasspath != null) { getLog().debug("using configured boot classpath: " + this.bootClasspath); return this.bootClasspath; } // Maven executes plugins with a customized ClassLoader to provide isolation between // plugins and the Maven installation. If we tried to inspect the 'java.class.path' property, // all we would see is a single entry for plexus-classworlds.jar. // (more info at https://cwiki.apache.org/confluence/display/MAVEN/Maven+3.x+Class+Loading ) // // To be able to load the Java platform classes (i.e. java.util.*), we have to look at the // bootstrap class path - not sure about the standard way to find this. // (http://docs.oracle.com/javase/7/docs/technotes/tools/findingclasses.html) // TODO 6/4/15 mbrown -- warn users that bootclasspath might be a different version (JAVA_HOME probably) than what they use for javac final String bootClasspath = System.getProperty("sun.boot.class.path"); getLog().debug("derived bootclasspath: " + bootClasspath); return bootClasspath; } private String mavenCoordinates(org.apache.maven.artifact.Artifact dep) { return dep.getGroupId() + ":" + dep.getArtifactId() + ":" + dep.getVersion() + ":" + dep .getScope(); } private boolean isExcluded(Artifact artifact) { if (artifact.name() instanceof MavenArtifactName) { MavenArtifactName name = (MavenArtifactName) artifact.name(); // excluded if the exclusions lists contains a match return excludeDependencies.stream() .anyMatch(excl -> excl.getGroupId().equals(name.groupId()) && excl.getArtifactId().equals(name.artifactId())); } return false; } private static long asMillis(Stopwatch stopwatch) { return stopwatch.elapsed(TimeUnit.MILLISECONDS); } private void outputConflicts(Collection conflicts) { Map descriptions = new EnumMap<>(ConflictCategory.class); descriptions.put(ConflictCategory.CLASS_NOT_FOUND, "Class being called not found"); descriptions.put(ConflictCategory.METHOD_SIGNATURE_NOT_FOUND, "Method being called not found"); // group conflict by category final Map> byCategory = conflicts.stream() .collect(Collectors.groupingBy(Conflict::category)); byCategory.forEach((category, conflictsInCategory) -> { final String desc = descriptions.getOrDefault(category, category.name().replace('_', ' ')); getLog().warn(""); getLog().warn("Category: " + desc); // next group by artifact containing the conflict final Map> byArtifact = conflictsInCategory.stream() .collect(Collectors.groupingBy(Conflict::usedBy)); byArtifact.forEach((artifactName, conflictsInArtifact) -> { getLog().warn(" In artifact: " + artifactName.name()); // next group by class containing the conflict final Map> byClassName = conflictsInArtifact.stream() .collect(Collectors.groupingBy(c -> c.dependency().fromClass())); byClassName.forEach((classDesc, conflictsInClass) -> { getLog().warn(" In class: " + classDesc.toString()); conflictsInClass.stream() .forEach(c -> { final Dependency dep = c.dependency(); getLog().warn(" In method: " + dep.fromMethod().prettyWithoutReturnType() + optionalLineNumber(dep.fromLineNumber())); getLog().warn(" " + dep.describe()); getLog().warn(" Problem: " + c.reason()); if (c.existsIn() != ConflictChecker.UNKNOWN_ARTIFACT_NAME) { getLog().warn(" Found in: " + c.existsIn().name()); } // this could be smarter about separating each blob of warnings by method, but for // now just output a bunch of dashes always getLog().warn(" --------"); }); }); }); }); } private String optionalLineNumber(int lineNumber) { return lineNumber != 0 ? ":" + lineNumber : ""; } private Artifact toArtifact(String outputDirectory) { return new ArtifactBuilder() .name(new ArtifactName("project")) .classes(Files.fileTreeTraverser().breadthFirstTraversal(new File(outputDirectory)) .filter(f -> f.getName().endsWith(".class")) .transform(this::loadClass) .uniqueIndex(DeclaredClass::className)) .build(); } private DeclaredClass loadClass(File f) { try { return com.spotify.missinglink.ClassLoader.load(new FileInputStream(f)); } catch (IOException e) { throw Throwables.propagate(e); } } private ImmutableList constructArtifacts(Iterable entries) { final List list = StreamSupport.stream(entries.spliterator(), false) // don't inspect paths that don't exist. // some bootclasspath entries, like sunrsasign.jar, are reported even if they // don't exist on disk - ¯\_(ツ)_/¯ .distinct() .filter(this::filterValidClasspathEntries) .map(this::filepathToArtifact) .collect(Collectors.toList()); return ImmutableList.copyOf(list); } private boolean filterValidClasspathEntries(String element) { return filterValid(new File(element)); } private boolean filterValid(File file) { if (file == null) { return false; } final boolean isJarFile = file.isFile() && file.getName().endsWith(".jar"); final boolean isClassDirectory = file.isDirectory(); return isClassDirectory || isJarFile; } private boolean filterValidClasspathEntries(org.apache.maven.artifact.Artifact artifact) { return filterValid(artifact.getFile()); } private ImmutableList constructArtifacts( List mavenDeps) { final List list = mavenDeps.stream() .filter(this::filterValidClasspathEntries) .map(this::mavenDepToArtifact) .collect(Collectors.toList()); return ImmutableList.copyOf(list); } private Artifact filepathToArtifact(String path) { getLog().debug("loading artifact for path: " + path); return doArtifactLoad(() -> artifactLoader.load(new File(path))); } private Artifact mavenDepToArtifact(org.apache.maven.artifact.Artifact dep) { final File path = dep.getFile(); getLog().debug("loading artifact for path: " + path); final MavenArtifactName name = new MavenArtifactName( dep.getGroupId(), dep.getArtifactId(), dep.getVersion() ); return doArtifactLoad(() -> artifactLoader.load(name, path)); } private Artifact doArtifactLoad(ArtifactSupplier supplier) { Stopwatch stopwatch = Stopwatch.createStarted(); Artifact artifact; try { artifact = supplier.load(); } catch (IOException e) { throw Throwables.propagate(e); } stopwatch.stop(); getLog().debug("artifact loading took " + asMillis(stopwatch) + " ms"); return artifact; } // workaround for java.util.function.Supplier not allowing exceptions to be thrown @FunctionalInterface private interface ArtifactSupplier { Artifact load() throws IOException; } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy