org.apache.maven.plugins.dependency.analyze.AbstractAnalyzeMojo Maven / Gradle / Ivy
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 org.apache.maven.plugins.dependency.analyze;
import java.io.File;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.artifact.resolver.filter.ArtifactFilter;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugins.annotations.Component;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.plugins.dependency.utils.StringUtils;
import org.apache.maven.project.MavenProject;
import org.apache.maven.shared.artifact.filter.StrictPatternExcludesArtifactFilter;
import org.apache.maven.shared.dependency.analyzer.ProjectDependencyAnalysis;
import org.apache.maven.shared.dependency.analyzer.ProjectDependencyAnalyzer;
import org.apache.maven.shared.dependency.analyzer.ProjectDependencyAnalyzerException;
import org.codehaus.plexus.PlexusContainer;
import org.codehaus.plexus.component.repository.exception.ComponentLookupException;
import org.codehaus.plexus.util.xml.PrettyPrintXMLWriter;
/**
* Analyzes the dependencies of this project and determines which are: used and declared; used and undeclared; unused
* and declared; compile scoped but only used in tests.
*
* @author Mark Hobson
* @since 2.0-alpha-5
*/
public abstract class AbstractAnalyzeMojo extends AbstractMojo {
// fields -----------------------------------------------------------------
/**
* The plexusContainer to look-up the right {@link ProjectDependencyAnalyzer} implementation depending on the mojo
* configuration.
*/
@Component
private PlexusContainer plexusContainer;
/**
* The Maven project to analyze.
*/
@Component
private MavenProject project;
/**
* Specify the project dependency analyzer to use (plexus component role-hint). By default,
* maven-dependency-analyzer is used. To use this, you must declare
* a dependency for this plugin that contains the code for the analyzer. The analyzer must have a declared Plexus
* role name, and you specify the role name here.
*
* @since 2.2
*/
@Parameter(property = "analyzer", defaultValue = "default")
private String analyzer;
/**
* Whether to fail the build if a dependency warning is found.
*/
@Parameter(property = "failOnWarning", defaultValue = "false")
private boolean failOnWarning;
/**
* Output used dependencies.
*/
@Parameter(property = "verbose", defaultValue = "false")
private boolean verbose;
/**
* Ignore Runtime/Provided/Test/System scopes for unused dependency analysis.
*
* Non-test scoped
list will be not affected.
*/
@Parameter(property = "ignoreNonCompile", defaultValue = "false")
private boolean ignoreNonCompile;
/**
* Ignore Runtime scope for unused dependency analysis.
*
* @since 3.2.0
*/
@Parameter(property = "ignoreUnusedRuntime", defaultValue = "false")
private boolean ignoreUnusedRuntime;
/**
* Ignore all dependencies that are used only in test but not test-scoped. Setting
* this flag has the same effect as adding all dependencies that have been flagged with
* the Non-test scoped test only dependencies found warning to the
* <ignoredNonTestScopedDependencies>
configuration.
*
* @since 3.3.1-SNAPSHOT
*/
@Parameter(property = "ignoreAllNonTestScoped", defaultValue = "false")
private boolean ignoreAllNonTestScoped;
/**
* Output the xml for the missing dependencies (used but not declared).
*
* @since 2.0-alpha-5
*/
@Parameter(property = "outputXML", defaultValue = "false")
private boolean outputXML;
/**
* Output scriptable values for the missing dependencies (used but not declared).
*
* @since 2.0-alpha-5
*/
@Parameter(property = "scriptableOutput", defaultValue = "false")
private boolean scriptableOutput;
/**
* Flag to use for scriptable output.
*
* @since 2.0-alpha-5
*/
@Parameter(property = "scriptableFlag", defaultValue = "$$$%%%")
private String scriptableFlag;
/**
* Flag to use for scriptable output
*
* @since 2.0-alpha-5
*/
@Parameter(defaultValue = "${basedir}", readonly = true)
private File baseDir;
/**
* Target folder
*
* @since 2.0-alpha-5
*/
@Parameter(defaultValue = "${project.build.directory}", readonly = true)
private File outputDirectory;
/**
* Force dependencies as used, to override incomplete result caused by bytecode-level analysis. Dependency format is
* groupId:artifactId
.
*
* @since 2.6
*/
@Parameter
private String[] usedDependencies;
/**
* Skip plugin execution completely.
*
* @since 2.7
*/
@Parameter(property = "mdep.analyze.skip", defaultValue = "false")
private boolean skip;
/**
* List of dependencies that will be ignored. Any dependency on this list will be excluded from the "declared but
* unused", the "used but undeclared", and the "non-test scoped" list. The filter syntax is:
*
*
* [groupId]:[artifactId]:[type]:[version]
*
*
* where each pattern segment is optional and supports full and partial *
wildcards. An empty pattern
* segment is treated as an implicit wildcard. *
*
* For example, org.apache.*
will match all artifacts whose group id starts with
* org.apache.
, and :::*-SNAPSHOT
will match all snapshot artifacts.
*
*
* @since 2.10
*/
@Parameter
private String[] ignoredDependencies = new String[0];
/**
* List of dependencies that will be ignored if they are used but undeclared. The filter syntax is:
*
*
* [groupId]:[artifactId]:[type]:[version]
*
*
* where each pattern segment is optional and supports full and partial *
wildcards. An empty pattern
* segment is treated as an implicit wildcard. *
*
* For example, org.apache.*
will match all artifacts whose group id starts with
* org.apache.
, and :::*-SNAPSHOT
will match all snapshot artifacts.
*
*
* @since 2.10
*/
@Parameter
private String[] ignoredUsedUndeclaredDependencies = new String[0];
/**
* List of dependencies that will be ignored if they are declared but unused. The filter syntax is:
*
*
* [groupId]:[artifactId]:[type]:[version]
*
*
* where each pattern segment is optional and supports full and partial *
wildcards. An empty pattern
* segment is treated as an implicit wildcard. *
*
* For example, org.apache.*
will match all artifacts whose group id starts with
* org.apache.
, and :::*-SNAPSHOT
will match all snapshot artifacts.
*
*
* @since 2.10
*/
@Parameter
private String[] ignoredUnusedDeclaredDependencies = new String[0];
/**
* List of dependencies that will be ignored if they are in not test scope but are only used in test classes.
* The filter syntax is:
*
*
* [groupId]:[artifactId]:[type]:[version]
*
*
* where each pattern segment is optional and supports full and partial *
wildcards. An empty pattern
* segment is treated as an implicit wildcard. *
*
* For example, org.apache.*
will match all artifacts whose group id starts with
* org.apache.
, and :::*-SNAPSHOT
will match all snapshot artifacts.
*
*
* @since 3.3.0
*/
@Parameter
private String[] ignoredNonTestScopedDependencies = new String[0];
/**
* List of project packaging that will be ignored.
*
* Default value is: pom, ear
*
* @since 3.2.1
*/
// defaultValue value on @Parameter - not work with Maven 3.2.5
// When is set defaultValue always win, and there is no possibility to override by plugin configuration.
@Parameter
private List ignoredPackagings = Arrays.asList("pom", "ear");
/**
* List Excluded classes patterns from analyze. Java regular expression pattern is applied to full class name.
*
* @since 3.7.0
*/
@Parameter(property = "mdep.analyze.excludedClasses")
private Set excludedClasses;
// Mojo methods -----------------------------------------------------------
/*
* @see org.apache.maven.plugin.Mojo#execute()
*/
@Override
public void execute() throws MojoExecutionException, MojoFailureException {
if (isSkip()) {
getLog().info("Skipping plugin execution");
return;
}
if (ignoredPackagings.contains(project.getPackaging())) {
getLog().info("Skipping " + project.getPackaging() + " project");
return;
}
if (outputDirectory == null || !outputDirectory.exists()) {
getLog().info("Skipping project with no build directory");
return;
}
boolean warning = checkDependencies();
if (warning && failOnWarning) {
throw new MojoExecutionException("Dependency problems found");
}
}
/**
* @return {@link ProjectDependencyAnalyzer}
* @throws MojoExecutionException in case of an error.
*/
protected ProjectDependencyAnalyzer createProjectDependencyAnalyzer() throws MojoExecutionException {
try {
return plexusContainer.lookup(ProjectDependencyAnalyzer.class, analyzer);
} catch (ComponentLookupException exception) {
throw new MojoExecutionException(
"Failed to instantiate ProjectDependencyAnalyser" + " / role-hint " + analyzer, exception);
}
}
/**
* @return {@link #skip}
*/
protected final boolean isSkip() {
return skip;
}
// private methods --------------------------------------------------------
private boolean checkDependencies() throws MojoExecutionException {
ProjectDependencyAnalysis analysis;
try {
analysis = createProjectDependencyAnalyzer().analyze(project, excludedClasses);
if (usedDependencies != null) {
analysis = analysis.forceDeclaredDependenciesUsage(usedDependencies);
}
} catch (ProjectDependencyAnalyzerException exception) {
throw new MojoExecutionException("Cannot analyze dependencies", exception);
}
if (ignoreNonCompile) {
analysis = analysis.ignoreNonCompile();
}
Set usedDeclared = new LinkedHashSet<>(analysis.getUsedDeclaredArtifacts());
Map> usedUndeclaredWithClasses =
new LinkedHashMap<>(analysis.getUsedUndeclaredArtifactsWithClasses());
Set unusedDeclared = new LinkedHashSet<>(analysis.getUnusedDeclaredArtifacts());
Set nonTestScope = new LinkedHashSet<>(analysis.getTestArtifactsWithNonTestScope());
Set ignoredUsedUndeclared = new LinkedHashSet<>();
Set ignoredUnusedDeclared = new LinkedHashSet<>();
Set ignoredNonTestScope = new LinkedHashSet<>();
if (ignoreUnusedRuntime) {
filterArtifactsByScope(unusedDeclared, Artifact.SCOPE_RUNTIME);
}
ignoredUsedUndeclared.addAll(filterDependencies(usedUndeclaredWithClasses.keySet(), ignoredDependencies));
ignoredUsedUndeclared.addAll(
filterDependencies(usedUndeclaredWithClasses.keySet(), ignoredUsedUndeclaredDependencies));
ignoredUnusedDeclared.addAll(filterDependencies(unusedDeclared, ignoredDependencies));
ignoredUnusedDeclared.addAll(filterDependencies(unusedDeclared, ignoredUnusedDeclaredDependencies));
if (ignoreAllNonTestScoped) {
ignoredNonTestScope.addAll(filterDependencies(nonTestScope, new String[] {"*"}));
} else {
ignoredNonTestScope.addAll(filterDependencies(nonTestScope, ignoredDependencies));
ignoredNonTestScope.addAll(filterDependencies(nonTestScope, ignoredNonTestScopedDependencies));
}
boolean reported = false;
boolean warning = false;
if (verbose && !usedDeclared.isEmpty()) {
getLog().info("Used declared dependencies found:");
logArtifacts(analysis.getUsedDeclaredArtifacts(), false);
reported = true;
}
if (!usedUndeclaredWithClasses.isEmpty()) {
logDependencyWarning("Used undeclared dependencies found:");
if (verbose) {
logArtifacts(usedUndeclaredWithClasses, true);
} else {
logArtifacts(usedUndeclaredWithClasses.keySet(), true);
}
reported = true;
warning = true;
}
if (!unusedDeclared.isEmpty()) {
logDependencyWarning("Unused declared dependencies found:");
logArtifacts(unusedDeclared, true);
reported = true;
warning = true;
}
if (!nonTestScope.isEmpty()) {
logDependencyWarning("Non-test scoped test only dependencies found:");
logArtifacts(nonTestScope, true);
reported = true;
warning = true;
}
if (verbose && !ignoredUsedUndeclared.isEmpty()) {
getLog().info("Ignored used undeclared dependencies:");
logArtifacts(ignoredUsedUndeclared, false);
reported = true;
}
if (verbose && !ignoredUnusedDeclared.isEmpty()) {
getLog().info("Ignored unused declared dependencies:");
logArtifacts(ignoredUnusedDeclared, false);
reported = true;
}
if (verbose && !ignoredNonTestScope.isEmpty()) {
getLog().info("Ignored non-test scoped test only dependencies:");
logArtifacts(ignoredNonTestScope, false);
reported = true;
}
if (outputXML) {
writeDependencyXML(usedUndeclaredWithClasses.keySet());
}
if (scriptableOutput) {
writeScriptableOutput(usedUndeclaredWithClasses.keySet());
}
if (!reported) {
getLog().info("No dependency problems found");
}
return warning;
}
private void filterArtifactsByScope(Set artifacts, String scope) {
artifacts.removeIf(artifact -> artifact.getScope().equals(scope));
}
private void logArtifacts(Set artifacts, boolean warn) {
if (artifacts.isEmpty()) {
getLog().info(" None");
} else {
for (Artifact artifact : artifacts) {
// called because artifact will set the version to -SNAPSHOT only if I do this. MNG-2961
artifact.isSnapshot();
if (warn) {
logDependencyWarning(" " + artifact);
} else {
getLog().info(" " + artifact);
}
}
}
}
private void logArtifacts(Map> artifacts, boolean warn) {
if (artifacts.isEmpty()) {
getLog().info(" None");
} else {
for (Map.Entry> entry : artifacts.entrySet()) {
// called because artifact will set the version to -SNAPSHOT only if I do this. MNG-2961
entry.getKey().isSnapshot();
if (warn) {
logDependencyWarning(" " + entry.getKey());
for (String clazz : entry.getValue()) {
logDependencyWarning(" class " + clazz);
}
} else {
getLog().info(" " + entry.getKey());
for (String clazz : entry.getValue()) {
getLog().info(" class " + clazz);
}
}
}
}
}
private void logDependencyWarning(CharSequence content) {
if (failOnWarning) {
getLog().error(content);
} else {
getLog().warn(content);
}
}
private void writeDependencyXML(Set artifacts) {
if (!artifacts.isEmpty()) {
getLog().info("Add the following to your pom to correct the missing dependencies: ");
StringWriter out = new StringWriter();
PrettyPrintXMLWriter writer = new PrettyPrintXMLWriter(out);
for (Artifact artifact : artifacts) {
// called because artifact will set the version to -SNAPSHOT only if I do this. MNG-2961
artifact.isSnapshot();
writer.startElement("dependency");
writer.startElement("groupId");
writer.writeText(artifact.getGroupId());
writer.endElement();
writer.startElement("artifactId");
writer.writeText(artifact.getArtifactId());
writer.endElement();
writer.startElement("version");
writer.writeText(artifact.getBaseVersion());
String classifier = artifact.getClassifier();
if (!StringUtils.isEmpty(classifier)) {
writer.startElement("classifier");
writer.writeText(classifier);
writer.endElement();
}
writer.endElement();
if (!Artifact.SCOPE_COMPILE.equals(artifact.getScope())) {
writer.startElement("scope");
writer.writeText(artifact.getScope());
writer.endElement();
}
writer.endElement();
}
getLog().info(System.lineSeparator() + out.getBuffer());
}
}
private void writeScriptableOutput(Set artifacts) {
if (!artifacts.isEmpty()) {
getLog().info("Missing dependencies: ");
String pomFile = baseDir.getAbsolutePath() + File.separatorChar + "pom.xml";
StringBuilder buf = new StringBuilder();
for (Artifact artifact : artifacts) {
// called because artifact will set the version to -SNAPSHOT only if I do this. MNG-2961
artifact.isSnapshot();
buf.append(scriptableFlag)
.append(":")
.append(pomFile)
.append(":")
.append(artifact.getDependencyConflictId())
.append(":")
.append(artifact.getClassifier())
.append(":")
.append(artifact.getBaseVersion())
.append(":")
.append(artifact.getScope())
.append(System.lineSeparator());
}
getLog().info(System.lineSeparator() + buf);
}
}
private List filterDependencies(Set artifacts, String[] excludes) {
ArtifactFilter filter = new StrictPatternExcludesArtifactFilter(Arrays.asList(excludes));
List result = new ArrayList<>();
for (Iterator it = artifacts.iterator(); it.hasNext(); ) {
Artifact artifact = it.next();
if (!filter.include(artifact)) {
it.remove();
result.add(artifact);
}
}
return result;
}
}