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

se.kth.depclean.DepCleanMojo Maven / Gradle / Ivy

package se.kth.depclean;

import org.apache.maven.artifact.Artifact;
import org.apache.maven.execution.MavenSession;
import org.apache.maven.model.Dependency;
import org.apache.maven.model.Exclusion;
import org.apache.maven.model.Model;
import org.apache.maven.model.io.xpp3.MavenXpp3Reader;
import org.apache.maven.model.io.xpp3.MavenXpp3Writer;
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.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.DefaultProjectBuildingRequest;
import org.apache.maven.project.MavenProject;
import org.apache.maven.project.ProjectBuilder;
import org.apache.maven.project.ProjectBuildingRequest;
import org.apache.maven.repository.RepositorySystem;
import org.apache.maven.shared.dependency.graph.DependencyGraphBuilder;
import org.apache.maven.shared.dependency.graph.DependencyGraphBuilderException;
import org.apache.maven.shared.dependency.graph.DependencyNode;
import org.apache.maven.shared.dependency.graph.traversal.CollectingDependencyNodeVisitor;
import se.kth.depclean.core.analysis.DefaultProjectDependencyAnalyzer;
import se.kth.depclean.core.analysis.ProjectDependencyAnalysis;
import se.kth.depclean.core.analysis.ProjectDependencyAnalyzer;
import se.kth.depclean.core.analysis.ProjectDependencyAnalyzerException;
import se.kth.depclean.util.JarUtils;
import se.kth.depclean.util.MavenInvoker;

import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

/**
 * This Maven mojo produces a clean copy of the project's pom file without bloated dependencies.
 * It is built on top of the maven-dependency-analyzer component.
 *
 * @see 
 * @see 
 */
@Mojo(name = "depclean", defaultPhase = LifecyclePhase.PACKAGE,
        requiresDependencyCollection = ResolutionScope.TEST,
        requiresDependencyResolution = ResolutionScope.TEST, threadSafe = true)
public class DepCleanMojo extends AbstractMojo {

    //--------------------------------/
    //-------- CLASS FIELD/S --------/
    //------------------------------/

    /**
     * The Maven project to analyze.
     */
    @Parameter(defaultValue = "${project}", readonly = true)
    private MavenProject project;

    /**
     * The Maven session to analyze.
     */
    @Parameter(defaultValue = "${session}", readonly = true)
    private MavenSession session;

    /**
     * If this is true, DepClean creates a debloated version of the pom without unused dependencies,
     * called "debloated-pom.xml", in root of the project.
     */
    @Parameter(property = "create.pom.debloated", defaultValue = "false")
    private boolean createPomDebloated;

    /**
     * Add a list of dependencies, identified by their coordinates, to be ignored by DepClean during the analysis and
     * considered as used dependencies. Useful to override incomplete result caused by bytecode-level analysis
     * Dependency format is groupId:artifactId:version.
     */
    @Parameter(property = "ignore.dependencies")
    private Set ignoreDependencies;

    /**
     * If this is true, and DepClean reported any unused dependency in the dependency tree,
     * the build fails immediately after running DepClean.
     */
    @Parameter(defaultValue = "false")
    private boolean failIfUnusedDependency;

    /**
     * Skip plugin execution completely.
     */
    @Parameter(defaultValue = "false")
    private boolean skipDepClean;

    @Component
    private ProjectBuilder mavenProjectBuilder;

    @Component
    private RepositorySystem repositorySystem;

    @Component(hint = "default")
    private DependencyGraphBuilder dependencyGraphBuilder;

    //--------------------------------/
    //------- PUBLIC METHOD/S -------/
    //------------------------------/

    @Override
    public void execute() throws MojoExecutionException, MojoFailureException {

        if (skipDepClean) {
            getLog().info("Skipping DepClean plugin execution");
            return;
        }

        System.out.println("-------------------------------------------------------");
        getLog().info("Starting DepClean dependency analysis");

        File pomFile = new File(project.getBasedir().getAbsolutePath() + "/" + "pom.xml");

        String packaging = project.getPackaging();
        if (packaging.equals("pom")) {
            getLog().info("Skipping because packaging type " + packaging + ".");
            return;
        }

        String pathToPutDebloatedPom = project.getBasedir().getAbsolutePath() + "/" + "pom-debloated.xml";

        /* Build maven model to manipulate the pom */
        Model model;
        FileReader reader;
        MavenXpp3Reader mavenReader = new MavenXpp3Reader();
        try {
            reader = new FileReader(pomFile);
            model = mavenReader.read(reader);
            model.setPomFile(pomFile);
        } catch (Exception ex) {
            getLog().error("Unable to build the maven project.");
            return;
        }

        /* Copy direct dependencies locally */
        try {
            MavenInvoker.runCommand("mvn dependency:copy-dependencies");
        } catch (IOException e) {
            getLog().error("Unable resolve all the dependencies.");
            return;
        }

        /* Decompress dependencies */
        JarUtils.decompressJars(project.getBuild().getDirectory() + "/" + "dependency");

        /* Analyze dependencies usage status */
        ProjectDependencyAnalysis projectDependencyAnalysis;
        try {
            ProjectDependencyAnalyzer dependencyAnalyzer = new DefaultProjectDependencyAnalyzer();
            projectDependencyAnalysis = dependencyAnalyzer.analyze(project);
        } catch (ProjectDependencyAnalyzerException e) {
            getLog().error("Unable to analyze dependencies.");
            return;
        }

        Set usedUndeclaredArtifacts = projectDependencyAnalysis.getUsedUndeclaredArtifacts();
        Set usedDeclaredArtifacts = projectDependencyAnalysis.getUsedDeclaredArtifacts();
        Set unusedDeclaredArtifacts = projectDependencyAnalysis.getUnusedDeclaredArtifacts();

        Set unusedUndeclaredArtifacts = project.getArtifacts();
        unusedUndeclaredArtifacts.removeAll(usedDeclaredArtifacts);
        unusedUndeclaredArtifacts.removeAll(usedUndeclaredArtifacts);
        unusedUndeclaredArtifacts.removeAll(unusedDeclaredArtifacts);

        /* Use artifacts coordinates for the report instead of the Artifact object */
        Set usedDeclaredArtifactsCoordinates = new HashSet<>();
        usedDeclaredArtifacts.forEach(s -> usedDeclaredArtifactsCoordinates.add(s.getGroupId() + ":" + s.getArtifactId() + ":" + s.getVersion()));

        Set usedUndeclaredArtifactsCoordinates = new HashSet<>();
        usedUndeclaredArtifacts.forEach(s -> usedUndeclaredArtifactsCoordinates.add(s.getGroupId() + ":" + s.getArtifactId() + ":" + s.getVersion()));

        Set unusedDeclaredArtifactsCoordinates = new HashSet<>();
        unusedDeclaredArtifacts.forEach(s -> unusedDeclaredArtifactsCoordinates.add(s.getGroupId() + ":" + s.getArtifactId() + ":" + s.getVersion()));

        Set unusedUndeclaredArtifactsCoordinates = new HashSet<>();
        unusedUndeclaredArtifacts.forEach(s -> unusedUndeclaredArtifactsCoordinates.add(s.getGroupId() + ":" + s.getArtifactId() + ":" + s.getVersion()));

        /* Ignoring dependencies from analysis */
        if (ignoreDependencies != null) {
            for (String ignoredDependency : ignoreDependencies) {
                // if the ignored dependency is an unused declared dependency then add it to the set of used declared
                // and remove it from the set of unused declared
                for (Iterator i = unusedDeclaredArtifactsCoordinates.iterator(); i.hasNext(); ) {
                    String unusedDeclaredArtifact = i.next();
                    if (ignoredDependency.equals(unusedDeclaredArtifact)) {
                        usedDeclaredArtifactsCoordinates.add(unusedDeclaredArtifact);
                        i.remove();
                        break;
                    }
                }
                // if the ignored dependency is an unused undeclared dependency then add it to the set of used undeclared
                // and remove it from the set of unused undeclared
                for (Iterator j = unusedUndeclaredArtifactsCoordinates.iterator(); j.hasNext(); ) {
                    String unusedUndeclaredArtifact = j.next();
                    if (ignoredDependency.equals(unusedUndeclaredArtifact)) {
                        usedUndeclaredArtifactsCoordinates.add(unusedUndeclaredArtifact);
                        j.remove();
                        break;
                    }
                }
            }
        }

        /* Printing the results to the console */
        System.out.println(" D E P C L E A N   A N A L Y S I S   R E S U L T S");
        System.out.println("-------------------------------------------------------");

        System.out.println("Used direct dependencies" + " [" + usedDeclaredArtifactsCoordinates.size() + "]" + ": ");
        usedDeclaredArtifactsCoordinates.stream().forEach(s -> System.out.println("\t" + s));

        System.out.println("Used transitive dependencies" + " [" + usedUndeclaredArtifactsCoordinates.size() + "]" + ": ");
        usedUndeclaredArtifactsCoordinates.stream().forEach(s -> System.out.println("\t" + s));

        System.out.println("Potentially unused direct dependencies" + " [" + unusedDeclaredArtifactsCoordinates.size() + "]" + ": ");
        unusedDeclaredArtifactsCoordinates.stream().forEach(s -> System.out.println("\t" + s));

        System.out.println("Potentially unused transitive dependencies" + " [" + unusedUndeclaredArtifactsCoordinates.size() + "]" + ": ");
        unusedUndeclaredArtifactsCoordinates.stream().forEach(s -> System.out.println("\t" + s));

        if (!ignoreDependencies.isEmpty()) {
            System.out.println("-------------------------------------------------------");
            System.out.println("Dependencies ignored in the analysis by the user" + " [" + ignoreDependencies.size() + "]" + ": ");
            ignoreDependencies.stream().forEach(s -> System.out.println("\t" + s));
        }

        /* Fail the build if there are unused dependencies */
        if (failIfUnusedDependency && (unusedDeclaredArtifactsCoordinates.size() > 0 || unusedUndeclaredArtifactsCoordinates.size() > 0)) {
            throw new MojoExecutionException("Build failed due to unused dependencies in the dependency tree.");
        }


        /* Writing the debloated version of the pom */
        if (createPomDebloated) {
            getLog().info("Starting debloating POM");

            /* add used transitive as direct dependencies */
            try {
                if (!usedUndeclaredArtifacts.isEmpty()) {
                    getLog().info("Adding " + usedUndeclaredArtifacts.size() + " used transitive dependencies as direct dependencies.");
                    for (Artifact usedUndeclaredArtifact : usedUndeclaredArtifacts) {
                        model.addDependency(createDependency(usedUndeclaredArtifact));
                    }
                }
            } catch (Exception e) {
                throw new MojoExecutionException(e.getMessage(), e);
            }

            /* remove unused direct dependencies */
            try {
                if (!unusedDeclaredArtifacts.isEmpty()) {
                    getLog().info("Removing " + unusedDeclaredArtifacts.size() + " unused direct dependencies.");
                    for (Artifact unusedDeclaredArtifact : unusedDeclaredArtifacts) {
                        for (Dependency dependency : model.getDependencies()) {
                            if (dependency.getGroupId().equals(unusedDeclaredArtifact.getGroupId()) &&
                                    dependency.getArtifactId().equals(unusedDeclaredArtifact.getArtifactId())) {
                                model.removeDependency(dependency);
                                break;
                            }
                        }
                    }
                }
            } catch (Exception e) {
                throw new MojoExecutionException(e.getMessage(), e);
            }

            /* exclude unused transitive dependencies */
            try {
                if (!unusedUndeclaredArtifacts.isEmpty()) {
                    getLog().info("Excluding " + unusedUndeclaredArtifacts.size() + " unused transitive dependencies one-by-one.");
                    for (Dependency dependency : model.getDependencies()) {
                        for (Artifact artifact : unusedUndeclaredArtifacts) {
                            if (isChildren(artifact, dependency)) {
                                System.out.println("Excluding " + artifact.toString() + " from dependency " + dependency.toString());
                                Exclusion exclusion = new Exclusion();
                                exclusion.setGroupId(artifact.getGroupId());
                                exclusion.setArtifactId(artifact.getArtifactId());
                                dependency.addExclusion(exclusion);
                            }
                        }
                    }
                }
            } catch (Exception e) {
                throw new MojoExecutionException(e.getMessage(), e);
            }


            /* write the debloated pom file */
            try {
                Path path = Paths.get(pathToPutDebloatedPom);
                writePom(path, model);
            } catch (IOException e) {
                throw new MojoExecutionException(e.getMessage(), e);
            }

            getLog().info("POM debloated successfully");
            getLog().info("pom-debloated.xml file created in: " + pathToPutDebloatedPom);
        }
    }

    //--------------------------------/
    //------ PRIVATE METHOD/S -------/
    //------------------------------/

    /**
     * Determine if an artifact is a direct or transitive child of a dependency.
     *
     * @param artifact   The artifact.
     * @param dependency The dependency
     * @return true if the artifact is a child of a dependency in the dependency tree.
     * @throws DependencyGraphBuilderException If the graph cannot be constructed.
     */
    private boolean isChildren(Artifact artifact, Dependency dependency) throws DependencyGraphBuilderException {
        List dependencyNodes = getDependencyNodes();
        for (DependencyNode node : dependencyNodes) {
            Dependency dependencyNode = createDependency(node.getArtifact());
            if (dependency.getGroupId().equals(dependencyNode.getGroupId()) &&
                    dependency.getArtifactId().equals(dependencyNode.getArtifactId())) {
                // now we are in the target dependency
                for (DependencyNode child : node.getChildren()) {
                    if (child.getArtifact().equals(artifact)) {
                        // the dependency contains the artifact as a child node
                        return true;
                    }

                }
            }
        }
        return false;
    }

    /**
     * This method returns a list of dependency nodes from a graph of dependency tree.
     *
     * @return The nodes in the dependency graph.
     * @throws DependencyGraphBuilderException If the graph cannot be built.
     */
    private List getDependencyNodes() throws DependencyGraphBuilderException {
        ProjectBuildingRequest buildingRequest = new DefaultProjectBuildingRequest(session.getProjectBuildingRequest());
        buildingRequest.setProject(project);
        DependencyNode rootNode = dependencyGraphBuilder.buildDependencyGraph(buildingRequest, null);
        CollectingDependencyNodeVisitor visitor = new CollectingDependencyNodeVisitor();
        rootNode.accept(visitor);
        return visitor.getNodes();
    }

    /**
     * This method creates a {@link org.apache.maven.model.Dependency} object from a
     * Maven {@link org.apache.maven.artifact.Artifact}.
     *
     * @param artifact The artifact to create the dependency.
     * @return The Dependency object.
     */
    private Dependency createDependency(final Artifact artifact) {
        Dependency dependency = new Dependency();
        dependency.setGroupId(artifact.getGroupId());
        dependency.setArtifactId(artifact.getArtifactId());
        dependency.setVersion(artifact.getVersion());
        if (artifact.hasClassifier()) {
            dependency.setClassifier(artifact.getClassifier());
        }
        dependency.setOptional(artifact.isOptional());
        dependency.setScope(artifact.getScope());
        dependency.setType(artifact.getType());
        return dependency;
    }

    /**
     * Write pom file to the filesystem.
     *
     * @param pomFile The path to the pom.
     * @param model   The maven model to get the pom from.
     * @throws IOException In case of any IO issue.
     */
    private static void writePom(final Path pomFile, final Model model) throws IOException {
        MavenXpp3Writer writer = new MavenXpp3Writer();
        writer.write(Files.newBufferedWriter(pomFile), model);
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy