diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b43c59cb..39fd34f6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -15,7 +15,7 @@ jobs: max-parallel: 1 matrix: os: [ ubuntu-latest, windows-latest ] - java: [ 11 ] + java: [ 17 ] runs-on: ${{ matrix.os }} name: Maven Build with Java ${{ matrix.java }} on ${{ matrix.os }} steps: @@ -49,13 +49,13 @@ jobs: - name: "Integration Tests" run: mvn failsafe:integration-test --errors --fail-at-end - # The following is only executed on Ubuntu on Java 11 + # The following is only executed on Ubuntu on Java 17 - name: "JaCoCo Coverage Report" - if: matrix.os == 'ubuntu-latest' && matrix.java == 11 && github.repository == 'castor-software/depclean' + if: matrix.os == 'ubuntu-latest' && matrix.java == 17 && github.repository == 'castor-software/depclean' run: mvn jacoco:report - name: "Codecov" - if: matrix.os == 'ubuntu-latest' && matrix.java == 11 && github.repository == 'castor-software/depclean' + if: matrix.os == 'ubuntu-latest' && matrix.java == 17 && github.repository == 'castor-software/depclean' uses: codecov/codecov-action@v1 with: token: ${{ secrets.CODECOV_TOKEN }} @@ -63,7 +63,7 @@ jobs: flags: unittests - name: "Cache SonarCloud" - if: matrix.os == 'ubuntu-latest' && matrix.java == 11 && github.repository == 'castor-software/depclean' + if: matrix.os == 'ubuntu-latest' && matrix.java == 17 && github.repository == 'castor-software/depclean' uses: actions/cache@v1 with: path: ~/.sonar/cache @@ -71,8 +71,8 @@ jobs: restore-keys: ${{ runner.os }}-sonar - name: "SonarCloud" - if: matrix.os == 'ubuntu-latest' && matrix.java == 11 && github.repository == 'castor-software/depclean' - run: mvn sonar:sonar -Dsonar.projectKey=castor-software_depclean -Dsonar.organization=castor-software -Dsonar.host.url=https://sonarcloud.io -Dsonar.login=${{ secrets.SONAR_TOKEN }} -Dsonar.java.source=11 -Dsonar.java.target=11 + if: matrix.os == 'ubuntu-latest' && matrix.java == 17 && github.repository == 'castor-software/depclean' + run: mvn sonar:sonar -Dsonar.projectKey=castor-software_depclean -Dsonar.organization=castor-software -Dsonar.host.url=https://sonarcloud.io -Dsonar.login=${{ secrets.SONAR_TOKEN }} -Dsonar.java.source=17 -Dsonar.java.target=17 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} diff --git a/depclean-core/pom.xml b/depclean-core/pom.xml index 31406d19..98e9559b 100644 --- a/depclean-core/pom.xml +++ b/depclean-core/pom.xml @@ -1,7 +1,7 @@ + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 @@ -113,6 +113,18 @@ 17.0.0 compile + + + com.google.guava + guava + 30.1-jre + + + + org.assertj + assertj-core + 3.22.0 + diff --git a/depclean-core/src/main/java/se/kth/depclean/core/AbstractDebloater.java b/depclean-core/src/main/java/se/kth/depclean/core/AbstractDebloater.java new file mode 100644 index 00000000..ea017a8d --- /dev/null +++ b/depclean-core/src/main/java/se/kth/depclean/core/AbstractDebloater.java @@ -0,0 +1,73 @@ +package se.kth.depclean.core; + +import java.io.IOException; +import java.util.List; +import java.util.stream.Collectors; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import se.kth.depclean.core.analysis.model.DebloatedDependency; +import se.kth.depclean.core.analysis.model.ProjectDependencyAnalysis; + +/** + * Analyses the analysis result and writes the debloated config file. + */ +@Slf4j +@AllArgsConstructor +public abstract class AbstractDebloater { + + protected final ProjectDependencyAnalysis analysis; + + /** + * Writes the debloated config file down. + */ + public void write() throws IOException { + log.info("Starting debloating file"); + logChanges(); + setDependencies(analysis.getDebloatedDependencies().stream() + .map(this::toMavenDependency) + .collect(Collectors.toList())); + + if (log.isDebugEnabled()) { + logDependencies(); + } + postProcessDependencies(); + writeFile(); + } + + protected abstract T toMavenDependency(DebloatedDependency debloatedDependency); + + protected abstract void setDependencies(List dependencies); + + protected abstract void writeFile() throws IOException; + + protected abstract void logDependencies(); + + /** + * In order to keep the version as variable (property) for dependencies that were declared as such, post-process + * dependencies to replace interpolated version with the initial one. + */ + protected abstract void postProcessDependencies(); + + private void logChanges() { + if (analysis.hasUsedTransitiveDependencies()) { + final int dependencyAmount = analysis.getUsedTransitiveDependencies().size(); + log.info("Adding {} used transitive {} as direct {}.", + dependencyAmount, getDependencyWording(dependencyAmount), getDependencyWording(dependencyAmount)); + } + + if (analysis.hasUnusedDirectDependencies()) { + final int dependencyAmount = analysis.getUnusedDirectDependencies().size(); + log.info("Removing {} unused direct {}.", dependencyAmount, getDependencyWording(dependencyAmount)); + } + + if (analysis.hasUnusedTransitiveDependencies()) { + final int dependencyAmount = analysis.getUnusedTransitiveDependencies().size(); + log.info( + "Excluding {} unused transitive {} one-by-one.", dependencyAmount, getDependencyWording(dependencyAmount)); + } + } + + private String getDependencyWording(int amount) { + return amount > 1 ? "dependencies" : "dependency"; + } +} diff --git a/depclean-core/src/main/java/se/kth/depclean/core/DepCleanManager.java b/depclean-core/src/main/java/se/kth/depclean/core/DepCleanManager.java new file mode 100644 index 00000000..5d576dbd --- /dev/null +++ b/depclean-core/src/main/java/se/kth/depclean/core/DepCleanManager.java @@ -0,0 +1,211 @@ +package se.kth.depclean.core; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.Charset; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; +import lombok.AllArgsConstructor; +import lombok.SneakyThrows; +import org.apache.commons.io.FileUtils; +import org.apache.maven.plugin.logging.Log; +import org.jetbrains.annotations.Nullable; +import se.kth.depclean.core.analysis.AnalysisFailureException; +import se.kth.depclean.core.analysis.DefaultProjectDependencyAnalyzer; +import se.kth.depclean.core.analysis.ProjectDependencyAnalyzerException; +import se.kth.depclean.core.analysis.graph.DependencyGraph; +import se.kth.depclean.core.analysis.model.ProjectDependencyAnalysis; +import se.kth.depclean.core.model.ClassName; +import se.kth.depclean.core.model.Dependency; +import se.kth.depclean.core.model.ProjectContext; +import se.kth.depclean.core.model.Scope; +import se.kth.depclean.core.wrapper.DependencyManagerWrapper; + +/** + * Runs the depclean process, regardless of a specific dependency manager. + */ +@AllArgsConstructor +public class DepCleanManager { + + private static final String SEPARATOR = "-------------------------------------------------------"; + + private final DependencyManagerWrapper dependencyManager; + private final boolean skipDepClean; + private final boolean ignoreTests; + private final Set ignoreScopes; + private final Set ignoreDependencies; + private final boolean failIfUnusedDirect; + private final boolean failIfUnusedTransitive; + private final boolean failIfUnusedInherited; + private final boolean createPomDebloated; + private final boolean createResultJson; + private final boolean createClassUsageCsv; + + /** + * Execute the depClean manager. + */ + @SneakyThrows + public void execute() throws AnalysisFailureException { + final long startTime = System.currentTimeMillis(); + + if (skipDepClean) { + getLog().info("Skipping DepClean plugin execution"); + return; + } + printString(SEPARATOR); + getLog().info("Starting DepClean dependency analysis"); + + if (dependencyManager.isMaven() && dependencyManager.isPackagingPom()) { + getLog().info("Skipping because packaging type pom."); + return; + } + + dependencyManager.copyAndExtractDependencies(); + + final ProjectDependencyAnalysis analysis = getAnalysis(); + if (analysis == null) { + return; + } + analysis.print(); + + /* Fail the build if there are unused direct dependencies */ + if (failIfUnusedDirect && analysis.hasUnusedDirectDependencies()) { + throw new AnalysisFailureException( + "Build failed due to unused direct dependencies in the dependency tree of the project."); + } + + /* Fail the build if there are unused transitive dependencies */ + if (failIfUnusedTransitive && analysis.hasUnusedTransitiveDependencies()) { + throw new AnalysisFailureException( + "Build failed due to unused transitive dependencies in the dependency tree of the project."); + } + + /* Fail the build if there are unused inherited dependencies */ + if (failIfUnusedInherited && analysis.hasUnusedInheritedDependencies()) { + throw new AnalysisFailureException( + "Build failed due to unused inherited dependencies in the dependency tree of the project."); + } + + /* Writing the debloated version of the pom */ + if (createPomDebloated) { + dependencyManager.getDebloater(analysis).write(); + } + + /* Writing the JSON file with the debloat results */ + if (createResultJson) { + createResultJson(analysis); + } + + final long stopTime = System.currentTimeMillis(); + getLog().info("Analysis done in " + getTime(stopTime - startTime)); + } + + private void createResultJson(ProjectDependencyAnalysis analysis) { + printString("Creating depclean-results.json, please wait..."); + final File jsonFile = new File(dependencyManager.getBuildDirectory() + File.separator + "depclean-results.json"); + final File treeFile = new File(dependencyManager.getBuildDirectory() + File.separator + "tree.txt"); + final File classUsageFile = new File(dependencyManager.getBuildDirectory() + File.separator + "class-usage.csv"); + try { + dependencyManager.generateDependencyTree(treeFile); + } catch (IOException | InterruptedException e) { + getLog().error("Unable to generate dependency tree."); + // Restore interrupted state... + Thread.currentThread().interrupt(); + return; + } + if (createClassUsageCsv) { + printString("Creating class-usage.csv, please wait..."); + try { + FileUtils.write(classUsageFile, "OriginClass,TargetClass,Dependency\n", Charset.defaultCharset()); + } catch (IOException e) { + getLog().error("Error writing the CSV header."); + } + } + String treeAsJson = dependencyManager.getTreeAsJson(treeFile, + analysis, + classUsageFile, + createClassUsageCsv + ); + + try { + FileUtils.write(jsonFile, treeAsJson, Charset.defaultCharset()); + } catch (IOException e) { + getLog().error("Unable to generate JSON file."); + } + if (jsonFile.exists()) { + getLog().info("depclean-results.json file created in: " + jsonFile.getAbsolutePath()); + } + if (classUsageFile.exists()) { + getLog().info("class-usage.csv file created in: " + classUsageFile.getAbsolutePath()); + } + } + + @Nullable + private ProjectDependencyAnalysis getAnalysis() { + /* Analyze dependencies usage status */ + final ProjectContext projectContext = buildProjectContext(); + final ProjectDependencyAnalysis analysis; + final DefaultProjectDependencyAnalyzer dependencyAnalyzer = new DefaultProjectDependencyAnalyzer(projectContext); + try { + analysis = dependencyAnalyzer.analyze(); + } catch (ProjectDependencyAnalyzerException e) { + getLog().error("Unable to analyze dependencies."); + return null; + } + return analysis; + } + + private ProjectContext buildProjectContext() { + if (ignoreTests) { + ignoreScopes.add("test"); + } + + final DependencyGraph dependencyGraph = dependencyManager.dependencyGraph(); + return new ProjectContext( + dependencyGraph, + dependencyManager.getOutputDirectory(), + dependencyManager.getTestOutputDirectory(), + ignoreScopes.stream().map(Scope::new).collect(Collectors.toSet()), + toDependency(dependencyGraph.allDependencies(), ignoreDependencies), + dependencyManager.collectUsedClassesFromProcessors().stream().map(ClassName::new).collect(Collectors.toSet()) + ); + } + + /** + * Returns a set of {@code DependencyCoordinate}s that match given string representations. + * + * @param allDependencies all known dependencies + * @param ignoreDependencies string representation of dependencies to return + * @return a set of {@code Dependency} that match given string representations + */ + private Set toDependency(Set allDependencies, Set ignoreDependencies) { + return ignoreDependencies.stream() + .map(dependency -> findDependency(allDependencies, dependency)) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + } + + private Dependency findDependency(Set allDependencies, String dependency) { + return allDependencies.stream() + .filter(dep -> dep.toString().toLowerCase().contains(dependency.toLowerCase())) + .findFirst() + .orElse(null); + } + + private String getTime(long millis) { + long minutes = TimeUnit.MILLISECONDS.toMinutes(millis); + long seconds = (TimeUnit.MILLISECONDS.toSeconds(millis) % 60); + + return String.format("%smin %ss", minutes, seconds); + } + + private void printString(final String string) { + System.out.println(string); //NOSONAR avoid a warning of non-used logger + } + + private Log getLog() { + return dependencyManager.getLog(); + } +} diff --git a/depclean-core/src/main/java/se/kth/depclean/core/analysis/ActualUsedClasses.java b/depclean-core/src/main/java/se/kth/depclean/core/analysis/ActualUsedClasses.java new file mode 100644 index 00000000..a12eee7c --- /dev/null +++ b/depclean-core/src/main/java/se/kth/depclean/core/analysis/ActualUsedClasses.java @@ -0,0 +1,40 @@ +package se.kth.depclean.core.analysis; + +import java.util.HashSet; +import java.util.Set; +import lombok.extern.slf4j.Slf4j; +import se.kth.depclean.core.model.ClassName; +import se.kth.depclean.core.model.ProjectContext; + +/** + * Contains the actual classes used in the project (i.e. in classes, processors, configurations, etc.) + */ +@Slf4j +public class ActualUsedClasses { + + final Set classes = new HashSet<>(); + private final ProjectContext context; + + public ActualUsedClasses(ProjectContext context) { + this.context = context; + } + + private void registerClass(ClassName className) { + + // Do not register class unknown to dependencies + if (context.hasNoDependencyOnClass(className)) { + return; + } + + log.trace("## Register class {}", className); + classes.add(className); + } + + public void registerClasses(Iterable classes) { + classes.forEach(this::registerClass); + } + + public Set getRegisteredClasses() { + return classes; + } +} diff --git a/depclean-core/src/main/java/se/kth/depclean/core/analysis/AnalysisFailureException.java b/depclean-core/src/main/java/se/kth/depclean/core/analysis/AnalysisFailureException.java new file mode 100644 index 00000000..2075e146 --- /dev/null +++ b/depclean-core/src/main/java/se/kth/depclean/core/analysis/AnalysisFailureException.java @@ -0,0 +1,16 @@ +package se.kth.depclean.core.analysis; + +/** + * Indicates the analysis should fail. + */ +public class AnalysisFailureException extends Exception { + + /** + * Create the failure. + * + * @param message the message to explain with the analysis failed + */ + public AnalysisFailureException(String message) { + super(message); + } +} diff --git a/depclean-core/src/main/java/se/kth/depclean/core/analysis/ArtifactTypes.java b/depclean-core/src/main/java/se/kth/depclean/core/analysis/ArtifactTypes.java deleted file mode 100644 index dbc24312..00000000 --- a/depclean-core/src/main/java/se/kth/depclean/core/analysis/ArtifactTypes.java +++ /dev/null @@ -1,24 +0,0 @@ -package se.kth.depclean.core.analysis; - -import java.util.Set; -import lombok.AllArgsConstructor; -import lombok.Data; - -/** - * POJO containing the types in an artifact. - */ -@Data -@AllArgsConstructor -public class ArtifactTypes { - - /** - * A HashSet to store the types. - */ - private Set allTypes; - - /** - * A HashSet to store the used types. - */ - private Set usedTypes; - -} diff --git a/depclean-core/src/main/java/se/kth/depclean/core/analysis/ClassAnalyzer.java b/depclean-core/src/main/java/se/kth/depclean/core/analysis/ClassAnalyzer.java index 9e770a52..a5b30cf5 100644 --- a/depclean-core/src/main/java/se/kth/depclean/core/analysis/ClassAnalyzer.java +++ b/depclean-core/src/main/java/se/kth/depclean/core/analysis/ClassAnalyzer.java @@ -24,7 +24,7 @@ import java.util.Set; /** - * Gets the set of classes contained in an artifact given either + * Gets the set of classes contained in a dependency given either * as a jar file or an exploded directory. */ public interface ClassAnalyzer { @@ -39,9 +39,9 @@ public interface ClassAnalyzer { // public methods --------------------------------------------------------- /** - * Analyze the classes of a given artifact. + * Analyze the classes of a given dependency. * - * @param url The artifact. + * @param url The dependency. * @return A set of classes. * @throws IOException In case of IO issues. */ diff --git a/depclean-core/src/main/java/se/kth/depclean/core/analysis/DefaultClassAnalyzer.java b/depclean-core/src/main/java/se/kth/depclean/core/analysis/DefaultClassAnalyzer.java index 1ecfcee6..0e5b9320 100644 --- a/depclean-core/src/main/java/se/kth/depclean/core/analysis/DefaultClassAnalyzer.java +++ b/depclean-core/src/main/java/se/kth/depclean/core/analysis/DefaultClassAnalyzer.java @@ -23,12 +23,10 @@ import java.net.URL; import java.util.Set; import java.util.zip.ZipException; -import org.codehaus.plexus.component.annotations.Component; /** * The default class analyzer. */ -@Component(role = ClassAnalyzer.class) public class DefaultClassAnalyzer implements ClassAnalyzer { /** diff --git a/depclean-core/src/main/java/se/kth/depclean/core/analysis/DefaultProjectDependencyAnalyzer.java b/depclean-core/src/main/java/se/kth/depclean/core/analysis/DefaultProjectDependencyAnalyzer.java index 21104c92..cf3e5fb7 100644 --- a/depclean-core/src/main/java/se/kth/depclean/core/analysis/DefaultProjectDependencyAnalyzer.java +++ b/depclean-core/src/main/java/se/kth/depclean/core/analysis/DefaultProjectDependencyAnalyzer.java @@ -17,301 +17,90 @@ package se.kth.depclean.core.analysis; -import java.io.File; import java.io.IOException; -import java.net.URL; -import java.util.Arrays; -import java.util.Enumeration; -import java.util.HashMap; +import java.nio.file.Path; import java.util.HashSet; -import java.util.LinkedHashMap; -import java.util.LinkedHashSet; -import java.util.Map; -import java.util.Optional; import java.util.Set; -import java.util.jar.JarEntry; -import java.util.jar.JarFile; +import java.util.stream.Collectors; import lombok.extern.slf4j.Slf4j; -import org.apache.maven.artifact.Artifact; -import org.apache.maven.project.MavenProject; -import org.codehaus.plexus.component.annotations.Component; -import org.codehaus.plexus.component.annotations.Requirement; -import org.codehaus.plexus.util.xml.Xpp3Dom; import se.kth.depclean.core.analysis.asm.ASMDependencyAnalyzer; import se.kth.depclean.core.analysis.graph.DefaultCallGraph; +import se.kth.depclean.core.analysis.model.ProjectDependencyAnalysis; +import se.kth.depclean.core.model.ClassName; +import se.kth.depclean.core.model.ProjectContext; /** * This is principal class that perform the dependency analysis in a Maven project. */ @Slf4j -@Component(role = ProjectDependencyAnalyzer.class) -public class DefaultProjectDependencyAnalyzer implements ProjectDependencyAnalyzer { +public class DefaultProjectDependencyAnalyzer { - @Requirement - private final ClassAnalyzer classAnalyzer = new DefaultClassAnalyzer(); - - @Requirement private final DependencyAnalyzer dependencyAnalyzer = new ASMDependencyAnalyzer(); - - /** - * If true, the project's classes in target/test-classes are not going to be analyzed. - */ - private final boolean isIgnoredTest; - - /** - * A map [artifact] -> [allTypes]. - */ - private Map> artifactClassesMap; - - /** - * A map [artifact] -> [usedTypes]. - */ - private final Map> artifactUsedClassesMap = new HashMap<>(); + private final ProjectContext projectContext; /** * Ctor. */ - public DefaultProjectDependencyAnalyzer(boolean isIgnoredTest) { - this.isIgnoredTest = isIgnoredTest; + public DefaultProjectDependencyAnalyzer(ProjectContext projectContext) { + this.projectContext = projectContext; } /** * Analyze the dependencies in a project. * - * @param project The Maven project to be analyzed. - * @return An object with the usedDeclaredArtifacts, usedUndeclaredArtifacts, and unusedDeclaredArtifacts. + * @return An object representing the analysis result. * @throws ProjectDependencyAnalyzerException if the analysis fails. - * @see ProjectDependencyAnalyzer#analyze(org.apache.invoke.project.MavenProject) */ - @Override - public ProjectDependencyAnalysis analyze(MavenProject project) throws ProjectDependencyAnalyzerException { + public ProjectDependencyAnalysis analyze() throws ProjectDependencyAnalyzerException { try { // a map of [dependency] -> [classes] - artifactClassesMap = buildArtifactClassMap(project); - - // direct dependencies of the project - Set declaredArtifacts = project.getDependencyArtifacts(); - - // transitive dependencies of the project - Set transitiveArtifacts = removeAll(project.getArtifacts(), declaredArtifacts); + final ActualUsedClasses actualUsedClasses = new ActualUsedClasses(projectContext); /* ******************** bytecode analysis ********************* */ // execute the analysis (note that the order of these operations matters!) - buildProjectDependencyClasses(project); - Set projectClasses = new HashSet<>(DefaultCallGraph.getProjectVertices()); - buildDependenciesDependencyClasses(project); + actualUsedClasses.registerClasses(getProjectDependencyClasses(projectContext.getOutputFolder())); + if (!projectContext.ignoreTests()) { + log.trace("Parsing test folder"); + actualUsedClasses.registerClasses(getProjectTestDependencyClasses(projectContext.getTestOutputFolder())); + } + actualUsedClasses.registerClasses(projectContext.getExtraClasses()); /* ******************** usage analysis ********************* */ // search for the dependencies used by the project - collectUsedArtifacts( - artifactClassesMap, - DefaultCallGraph.referencedClassMembers(projectClasses) - ); - Set usedArtifacts = collectUsedArtifactsFromProcessors(project, artifactClassesMap); + Set projectClasses = new HashSet<>(DefaultCallGraph.getProjectVertices()); + log.trace("# DefaultCallGraph.referencedClassMembers()"); + actualUsedClasses.registerClasses(getReferencedClassMembers(projectClasses)); /* ******************** results as statically used at the bytecode *********************** */ - - // for the used dependencies, get the ones that are declared - Set usedDeclaredArtifacts = new LinkedHashSet<>(declaredArtifacts); - usedDeclaredArtifacts.retainAll(usedArtifacts); - - // for the used dependencies, remove the ones that are declared - Set usedUndeclaredArtifacts = new LinkedHashSet<>(usedArtifacts); - usedUndeclaredArtifacts = removeAll(usedUndeclaredArtifacts, declaredArtifacts); - - // for the declared dependencies, get the ones that are not used - Set unusedDeclaredArtifacts = new LinkedHashSet<>(declaredArtifacts); - unusedDeclaredArtifacts = removeAll(unusedDeclaredArtifacts, usedArtifacts); - - return new ProjectDependencyAnalysis(usedDeclaredArtifacts, usedUndeclaredArtifacts, unusedDeclaredArtifacts); + return new ProjectDependencyAnalysisBuilder(projectContext, actualUsedClasses).analyse(); } catch (IOException exception) { throw new ProjectDependencyAnalyzerException("Cannot analyze dependencies", exception); } - - } - - /** - * Maven processors are defined like this. - *
{@code
-   *       
-   *         org.bsc.maven
-   *         maven-processor-plugin
-   *         
-   *           
-   *             process
-   *             [...]
-   *             
-   *               
-   *                 XXXProcessor
-   *               
-   *             
-   *           
-   *         
-   *       
-   * }
- * - * @param project the maven project - * @param artifactClassesMap previously built artifacts map - * @return all used artifacts so far - */ - private Set collectUsedArtifactsFromProcessors(MavenProject project, - Map> artifactClassesMap) { - final Xpp3Dom[] processors = Optional.ofNullable(project.getPlugin("org.bsc.maven:maven-processor-plugin")) - .map(plugin -> plugin.getExecutionsAsMap().get("process")) - .map(exec -> (Xpp3Dom) exec.getConfiguration()) - .map(config -> config.getChild("processors")) - .map(Xpp3Dom::getChildren) - .orElse(new Xpp3Dom[0]); - Arrays.stream(processors) - .forEach(processor -> findArtifactForClassName(artifactClassesMap, processor.getValue()) - .ifPresent(artifact -> artifactUsedClassesMap.putIfAbsent(artifact, new HashSet<>())) - ); - - return artifactUsedClassesMap.keySet(); - } - - /** - * Returns a map with the artifacts (dependencies) in a Maven project and their corresponding classes. - * - * @param project A Maven project. - * @return A map of artifact -> classes. - * @throws IOException If the class cannot be analyzed. - */ - public Map> buildArtifactClassMap(MavenProject project) throws IOException { - Map> artifactClassMap = new LinkedHashMap<>(); - Set dependencyArtifacts = project.getArtifacts(); - for (Artifact artifact : dependencyArtifacts) { - File file = artifact.getFile(); - if (file != null && file.getName().endsWith(".jar")) { - // optimized solution for the jar case - try (JarFile jarFile = new JarFile(file)) { - Enumeration jarEntries = jarFile.entries(); - Set classes = new HashSet<>(); - while (jarEntries.hasMoreElements()) { - String entry = jarEntries.nextElement().getName(); - if (entry.endsWith(".class")) { - String className = entry.replace('/', '.'); - className = className.substring(0, className.length() - ".class".length()); - classes.add(className); - } - } - artifactClassMap.put(artifact, classes); - } - } else if (file != null && file.isDirectory()) { - URL url = file.toURI().toURL(); - Set classes = classAnalyzer.analyze(url); - artifactClassMap.put(artifact, classes); - } - } - return artifactClassMap; } - private void buildProjectDependencyClasses(MavenProject project) throws IOException { + private Iterable getProjectDependencyClasses(Path outputFolder) throws IOException { // Analyze src classes in the project - String outputDirectory = project.getBuild().getOutputDirectory(); - collectDependencyClasses(outputDirectory); - // Analyze test classes in the project - if (!isIgnoredTest) { - String testOutputDirectory = project.getBuild().getTestOutputDirectory(); - collectDependencyClasses(testOutputDirectory); - } - } - - private void buildDependenciesDependencyClasses(MavenProject project) throws IOException { - String dependenciesDirectory = project.getBuild().getDirectory() + File.separator + "dependency"; - collectDependencyClasses(dependenciesDirectory); - } - - /** - * Determine the artifacts that are used. - * - * @param artifactClassMap A map of [artifact] -> [classes in the artifact]. - * @param referencedClasses A set of classes that are detected as used. - * @return The set of used artifacts. - */ - private Set collectUsedArtifacts( - Map> artifactClassMap, - Set referencedClasses) { - Set usedArtifacts = new HashSet<>(); - for (String clazz : referencedClasses) { - Optional artifact = findArtifactForClassName(artifactClassMap, clazz); - if (artifact.isPresent()) { - if (!artifactUsedClassesMap.containsKey(artifact.get())) { - artifactUsedClassesMap.put(artifact.get(), new HashSet<>()); - } - artifactUsedClassesMap.get(artifact.get()).add(clazz); - usedArtifacts.add(artifact.get()); - } - } - return usedArtifacts; + log.trace("# getProjectDependencyClasses()"); + return collectDependencyClasses(outputFolder); } - - private Optional findArtifactForClassName(Map> artifactClassMap, String className) { - for (Map.Entry> entry : artifactClassMap.entrySet()) { - if (entry.getValue().contains(className)) { - return Optional.of(entry.getKey()); - } - } - return Optional.empty(); - } - - /** - * This method defines a new way to remove the artifacts by using the conflict id. We don't care about the version - * here because there can be only 1 for a given artifact anyway. - * - * @param start initial set - * @param remove set to exclude - * @return set with remove excluded - */ - private Set removeAll(Set start, Set remove) { - Set results = new LinkedHashSet<>(start.size()); - for (Artifact artifact : start) { - boolean found = false; - for (Artifact artifact2 : remove) { - if (artifact.getDependencyConflictId().equals(artifact2.getDependencyConflictId())) { - found = true; - break; - } - } - if (!found) { - results.add(artifact); - } - } - return results; + private Iterable getProjectTestDependencyClasses(Path testOutputFolder) throws IOException { + // Analyze test classes in the project + log.trace("# getProjectTestDependencyClasses()"); + return collectDependencyClasses(testOutputFolder); } - private Set collectDependencyClasses(String path) throws IOException { - URL url = new File(path).toURI().toURL(); - return dependencyAnalyzer.analyze(url); + private Iterable collectDependencyClasses(Path path) throws IOException { + return dependencyAnalyzer.analyze(path.toUri().toURL()).stream() + .map(ClassName::new) + .collect(Collectors.toSet()); } - /** - * Computes a map of [artifact] -> [allTypes, usedTypes]. - * - * @return A map of [artifact] -> [allTypes, usedTypes] - */ - public Map getArtifactClassesMap() { - Map output = new HashMap<>(); - for (Map.Entry> entry : artifactClassesMap.entrySet()) { - Artifact key = entry.getKey(); - if (artifactUsedClassesMap.containsKey(key)) { - output.put(key.toString(), - new ArtifactTypes( - artifactClassesMap.get(key), // get all the types - artifactUsedClassesMap.get(key) // get used types - )); - } else { - output.put(key.toString(), - new ArtifactTypes( - artifactClassesMap.get(key), // get all the types - new HashSet<>() // get used types - )); - } - } - return output; + private Iterable getReferencedClassMembers(Set projectClasses) { + return DefaultCallGraph.referencedClassMembers(projectClasses).stream() + .map(ClassName::new) + .collect(Collectors.toSet()); } } - diff --git a/depclean-core/src/main/java/se/kth/depclean/core/analysis/DependencyTypes.java b/depclean-core/src/main/java/se/kth/depclean/core/analysis/DependencyTypes.java new file mode 100644 index 00000000..ac852082 --- /dev/null +++ b/depclean-core/src/main/java/se/kth/depclean/core/analysis/DependencyTypes.java @@ -0,0 +1,27 @@ +package se.kth.depclean.core.analysis; + +import java.util.Set; +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import se.kth.depclean.core.model.ClassName; + +/** + * POJO containing the types in a dependency. + */ +@Getter +@EqualsAndHashCode +@AllArgsConstructor +public class DependencyTypes { + + /** + * An iterable to store the types. + */ + private Set allTypes; + + /** + * An iterable to store the used types. + */ + private Set usedTypes; + +} diff --git a/depclean-core/src/main/java/se/kth/depclean/core/analysis/ProjectDependencyAnalysis.java b/depclean-core/src/main/java/se/kth/depclean/core/analysis/ProjectDependencyAnalysis.java deleted file mode 100644 index 4009ac13..00000000 --- a/depclean-core/src/main/java/se/kth/depclean/core/analysis/ProjectDependencyAnalysis.java +++ /dev/null @@ -1,170 +0,0 @@ -package se.kth.depclean.core.analysis; - -/* - * 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. - */ - -import java.util.Collections; -import java.util.HashSet; -import java.util.LinkedHashSet; -import java.util.Set; -import org.apache.maven.artifact.Artifact; - -/** - * Project dependencies analysis result. - */ -public class ProjectDependencyAnalysis { - - /** - * Store all the used declared artifacts (ie. used direct dependencies). - */ - private final Set usedDeclaredArtifacts; - - /** - * Store all the used undeclared artifacts (ie. used transitive dependencies). - */ - private final Set usedUndeclaredArtifacts; - - /** - * Store all the unused declared artifacts (ie. unused transitive dependencies). - */ - private final Set unusedDeclaredArtifacts; - - /** - * Ctor. - */ - public ProjectDependencyAnalysis( - Set usedDeclaredArtifacts, - Set usedUndeclaredArtifacts, - Set unusedDeclaredArtifacts) { - this.usedDeclaredArtifacts = safeCopy(usedDeclaredArtifacts); - this.usedUndeclaredArtifacts = safeCopy(usedUndeclaredArtifacts); - this.unusedDeclaredArtifacts = safeCopy(unusedDeclaredArtifacts); - } - - /** - * To prevent unnecessary and unexpected modification in the set. - * - * @param The required set. - * @return An unmodifiable set corresponding to the provided set. - */ - private Set safeCopy(Set set) { - return (set == null) ? Collections.emptySet() - : Collections.unmodifiableSet(new LinkedHashSet(set)); - } - - /** - * Filter out artifacts with scope other than compile from the set of unused declared artifacts. - * - * @return updated project dependency analysis - * @since 1.3 - */ - public ProjectDependencyAnalysis ignoreNonCompile() { - Set filteredUnusedDeclared = new HashSet<>(unusedDeclaredArtifacts); - filteredUnusedDeclared.removeIf(artifact -> !artifact.getScope().equals(Artifact.SCOPE_COMPILE)); - return new ProjectDependencyAnalysis(usedDeclaredArtifacts, usedUndeclaredArtifacts, filteredUnusedDeclared); - } - - /** - * Overrides the hash code value method of the object. - */ - @Override - public int hashCode() { - int hashCode = getUsedDeclaredArtifacts().hashCode(); - hashCode = (hashCode * 37) + getUsedUndeclaredArtifacts().hashCode(); - hashCode = (hashCode * 37) + getUnusedDeclaredArtifacts().hashCode(); - return hashCode; - } - - /** - * Used declared artifacts. - * - * @return {@link Artifact} - */ - public Set getUsedDeclaredArtifacts() { - return usedDeclaredArtifacts; - } - - // Object methods --------------------------------------------------------- - - /** - * Used but not declared artifacts. - * - * @return {@link Artifact} - */ - public Set getUsedUndeclaredArtifacts() { - return usedUndeclaredArtifacts; - } - - /** - * Unused but declared artifacts. - * - * @return {@link Artifact} - */ - public Set getUnusedDeclaredArtifacts() { - return unusedDeclaredArtifacts; - } - - /** - * Overrides the standard equals method of Object. - */ - @Override - public boolean equals(Object object) { - if (object instanceof ProjectDependencyAnalysis) { - ProjectDependencyAnalysis analysis = (ProjectDependencyAnalysis) object; - return getUsedDeclaredArtifacts().equals(analysis.getUsedDeclaredArtifacts()) - && getUsedUndeclaredArtifacts().equals(analysis.getUsedUndeclaredArtifacts()) - && getUnusedDeclaredArtifacts().equals(analysis.getUnusedDeclaredArtifacts()); - } - - return false; - } - - /** - * Overrides de toString standard method of class Object @see java.lang.Object#toString(). - */ - @Override - public String toString() { - StringBuilder buffer = new StringBuilder(); - - if (!getUsedDeclaredArtifacts().isEmpty()) { - buffer.append("usedDeclaredArtifacts=").append(getUsedDeclaredArtifacts()); - } - - if (!getUsedUndeclaredArtifacts().isEmpty()) { - if (buffer.length() > 0) { - buffer.append(","); - } - buffer.append("usedUndeclaredArtifacts=").append(getUsedUndeclaredArtifacts()); - } - - if (!getUnusedDeclaredArtifacts().isEmpty()) { - if (buffer.length() > 0) { - buffer.append(","); - } - buffer.append("unusedDeclaredArtifacts=").append(getUnusedDeclaredArtifacts()); - } - - buffer.insert(0, "["); - buffer.insert(0, getClass().getName()); - - buffer.append("]"); - - return buffer.toString(); - } -} diff --git a/depclean-core/src/main/java/se/kth/depclean/core/analysis/ProjectDependencyAnalysisBuilder.java b/depclean-core/src/main/java/se/kth/depclean/core/analysis/ProjectDependencyAnalysisBuilder.java new file mode 100644 index 00000000..309d2789 --- /dev/null +++ b/depclean-core/src/main/java/se/kth/depclean/core/analysis/ProjectDependencyAnalysisBuilder.java @@ -0,0 +1,151 @@ +package se.kth.depclean.core.analysis; + +import static com.google.common.collect.Sets.newHashSet; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import lombok.extern.slf4j.Slf4j; +import se.kth.depclean.core.analysis.model.ProjectDependencyAnalysis; +import se.kth.depclean.core.model.ClassName; +import se.kth.depclean.core.model.Dependency; +import se.kth.depclean.core.model.ProjectContext; + +/** + * Builds the analysis given the declared dependencies and the one actually used. + */ +@Slf4j +public class ProjectDependencyAnalysisBuilder { + + private final ProjectContext context; + private final ActualUsedClasses actualUsedClasses; + private final Set usedDependencies; + + ProjectDependencyAnalysisBuilder(ProjectContext context, + ActualUsedClasses actualUsedClasses) { + this.context = context; + this.actualUsedClasses = actualUsedClasses; + usedDependencies = actualUsedClasses.getRegisteredClasses().stream() + .flatMap(clazz -> context.getDependenciesForClass(clazz).stream()) + .collect(Collectors.toSet()); + } + + /** + * Analyse the dependencies to find out what is used and what is not. + * + * @return the analysis + */ + public ProjectDependencyAnalysis analyse() { + final Set usedDirectDependencies = + getUsedDirectDependencies(); + final Set usedTransitiveDependencies = + getUsedTransitiveDependencies(); + final Set usedInheritedDependencies = + getUsedInheritedDependencies(); + final Set unusedDirectDependencies = + getUnusedDirectDependencies(usedDirectDependencies); + final Set unusedTransitiveDependencies = + getUnusedTransitiveDependencies(usedTransitiveDependencies); + final Set unusedInheritedDependencies = + getUnusedInheritedDependencies(usedInheritedDependencies); + final Map dependencyClassesMap = + buildDependencyClassesMap(); + + context.getIgnoredDependencies().forEach(dependencyToIgnore -> { + ignoreDependency(usedDirectDependencies, unusedDirectDependencies, dependencyToIgnore); + ignoreDependency(usedTransitiveDependencies, unusedTransitiveDependencies, dependencyToIgnore); + ignoreDependency(usedInheritedDependencies, unusedInheritedDependencies, dependencyToIgnore); + }); + + return new ProjectDependencyAnalysis( + usedDirectDependencies, + usedTransitiveDependencies, + usedInheritedDependencies, + unusedDirectDependencies, + unusedTransitiveDependencies, + unusedInheritedDependencies, + context.getIgnoredDependencies(), + dependencyClassesMap, + context.getDependencyGraph()); + } + + private Map buildDependencyClassesMap() { + final Map output = new HashMap<>(); + final Collection allDependencies = newHashSet(context.getAllDependencies()); + for (Dependency dependency : allDependencies) { + final Set allClasses = context.getClassesForDependency(dependency); + final Set usedClasses = newHashSet(allClasses); + usedClasses.retainAll(actualUsedClasses.getRegisteredClasses()); + output.put(dependency, new DependencyTypes(allClasses, usedClasses)); + } + return output; + } + + private Set getUsedDirectDependencies() { + return usedDependencies.stream() + .filter(a -> context.getDependencyGraph().directDependencies().contains(a)) + .peek(dependency -> log.trace("## Used Direct dependency {}", dependency)) + .collect(Collectors.toSet()); + } + + private Set getUsedTransitiveDependencies() { + return usedDependencies.stream() + .filter(a -> context.getDependencyGraph().transitiveDependencies().contains(a)) + .peek(dependency -> log.trace("## Used Transitive dependency {}", dependency)) + .collect(Collectors.toSet()); + } + + private Set getUsedInheritedDependencies() { + return usedDependencies.stream() + .filter(a -> context.getDependencyGraph().inheritedDependencies().contains(a)) + .peek(dependency -> log.trace("## Used Transitive dependency {}", dependency)) + .collect(Collectors.toSet()); + } + + private Set getUnusedDirectDependencies(Set usedDirectDependencies) { + return getUnusedDependencies(context.getDependencyGraph().directDependencies(), usedDirectDependencies); + } + + private Set getUnusedTransitiveDependencies( + Set usedTransitiveDependencies) { + return getUnusedDependencies(context.getDependencyGraph().transitiveDependencies(), usedTransitiveDependencies); + } + + private Set getUnusedInheritedDependencies( + Set usedInheritedDependencies) { + return getUnusedDependencies(context.getDependencyGraph().inheritedDependencies(), usedInheritedDependencies); + } + + private Set getUnusedDependencies( + Set baseDependencies, Set usedDependencies) { + final Set unusedInheritedDependencies = newHashSet(baseDependencies); + unusedInheritedDependencies.removeAll(usedDependencies); + return unusedInheritedDependencies; + } + + /** + * If the dependencyToIgnore is an unused dependency, then add it to the set of usedDependencyCoordinates and remove + * it from the set of unusedDependencyCoordinates. + * + * @param usedDependencies The set of used artifacts where the dependency will be added. + * @param unusedDependencies The set of unused artifacts where the dependency will be removed. + * @param dependencyToIgnore The dependency to ignore. + */ + private void ignoreDependency( + Set usedDependencies, + Set unusedDependencies, + Dependency dependencyToIgnore) { + + for (Iterator i = unusedDependencies.iterator(); i.hasNext(); ) { + Dependency unusedDependency = i.next(); + if (dependencyToIgnore.equals(unusedDependency)) { + usedDependencies.add(unusedDependency); + i.remove(); + break; + } + } + } +} diff --git a/depclean-core/src/main/java/se/kth/depclean/core/analysis/ProjectDependencyAnalyzer.java b/depclean-core/src/main/java/se/kth/depclean/core/analysis/ProjectDependencyAnalyzer.java deleted file mode 100644 index 5e33d626..00000000 --- a/depclean-core/src/main/java/se/kth/depclean/core/analysis/ProjectDependencyAnalyzer.java +++ /dev/null @@ -1,35 +0,0 @@ -package se.kth.depclean.core.analysis; - -/* - * 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. - */ - -import org.apache.maven.project.MavenProject; - -/** - * Analyze a project's declared dependencies and effective classes. - */ -public interface ProjectDependencyAnalyzer { - // fields ----------------------------------------------------------------- - - String ROLE = ProjectDependencyAnalyzer.class.getName(); - - // public methods --------------------------------------------------------- - - ProjectDependencyAnalysis analyze(MavenProject project) throws ProjectDependencyAnalyzerException; -} diff --git a/depclean-core/src/main/java/se/kth/depclean/core/analysis/asm/ASMDependencyAnalyzer.java b/depclean-core/src/main/java/se/kth/depclean/core/analysis/asm/ASMDependencyAnalyzer.java index e7c9e169..eb91807f 100644 --- a/depclean-core/src/main/java/se/kth/depclean/core/analysis/asm/ASMDependencyAnalyzer.java +++ b/depclean-core/src/main/java/se/kth/depclean/core/analysis/asm/ASMDependencyAnalyzer.java @@ -22,7 +22,6 @@ import java.io.IOException; import java.net.URL; import java.util.Set; -import org.codehaus.plexus.component.annotations.Component; import se.kth.depclean.core.analysis.ClassFileVisitorUtils; import se.kth.depclean.core.analysis.DependencyAnalyzer; import se.kth.depclean.core.analysis.graph.ClassMembersVisitorCounter; @@ -30,7 +29,6 @@ /** * Dependency analyzer. */ -@Component(role = DependencyAnalyzer.class) public class ASMDependencyAnalyzer implements DependencyAnalyzer { /** diff --git a/depclean-core/src/main/java/se/kth/depclean/core/analysis/graph/DependencyGraph.java b/depclean-core/src/main/java/se/kth/depclean/core/analysis/graph/DependencyGraph.java new file mode 100644 index 00000000..1378ca98 --- /dev/null +++ b/depclean-core/src/main/java/se/kth/depclean/core/analysis/graph/DependencyGraph.java @@ -0,0 +1,22 @@ +package se.kth.depclean.core.analysis.graph; + +import java.util.Set; +import se.kth.depclean.core.model.Dependency; + +/** + * Should build a graph of dependencies, so that it can be requested for any representation of this graph. + */ +public interface DependencyGraph { + + Dependency projectCoordinates(); + + Set directDependencies(); + + Set inheritedDependencies(); + + Set transitiveDependencies(); + + Set allDependencies(); + + Set getDependenciesForParent(Dependency parent); +} diff --git a/depclean-core/src/main/java/se/kth/depclean/core/analysis/model/DebloatedDependency.java b/depclean-core/src/main/java/se/kth/depclean/core/analysis/model/DebloatedDependency.java new file mode 100644 index 00000000..adae6fc0 --- /dev/null +++ b/depclean-core/src/main/java/se/kth/depclean/core/analysis/model/DebloatedDependency.java @@ -0,0 +1,19 @@ +package se.kth.depclean.core.analysis.model; + +import java.util.Set; +import lombok.Getter; +import se.kth.depclean.core.model.Dependency; + +/** + * A debloated dependency. + */ +@Getter +public class DebloatedDependency extends Dependency { + + private final Set exclusions; + + public DebloatedDependency(Dependency dependency, Set exclusions) { + super(dependency); + this.exclusions = exclusions; + } +} diff --git a/depclean-core/src/main/java/se/kth/depclean/core/analysis/model/DependencyAnalysisInfo.java b/depclean-core/src/main/java/se/kth/depclean/core/analysis/model/DependencyAnalysisInfo.java new file mode 100644 index 00000000..baace839 --- /dev/null +++ b/depclean-core/src/main/java/se/kth/depclean/core/analysis/model/DependencyAnalysisInfo.java @@ -0,0 +1,22 @@ +package se.kth.depclean.core.analysis.model; + +import java.util.TreeSet; +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.ToString; + +/** + * The result of a dependency analysis. + */ +@Getter +@ToString +@EqualsAndHashCode +@AllArgsConstructor +public class DependencyAnalysisInfo { + private final String status; + private final String type; + private final Long size; + private final TreeSet allTypes; + private final TreeSet usedTypes; +} diff --git a/depclean-core/src/main/java/se/kth/depclean/core/analysis/model/ProjectDependencyAnalysis.java b/depclean-core/src/main/java/se/kth/depclean/core/analysis/model/ProjectDependencyAnalysis.java new file mode 100644 index 00000000..bb1a8686 --- /dev/null +++ b/depclean-core/src/main/java/se/kth/depclean/core/analysis/model/ProjectDependencyAnalysis.java @@ -0,0 +1,228 @@ +package se.kth.depclean.core.analysis.model; + +/* + * 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. + */ + +import static com.google.common.collect.ImmutableSet.copyOf; +import static com.google.common.collect.ImmutableSet.toImmutableSet; +import static java.util.stream.Collectors.toCollection; + +import java.util.Comparator; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; +import java.util.stream.Collectors; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import se.kth.depclean.core.analysis.DependencyTypes; +import se.kth.depclean.core.analysis.graph.DependencyGraph; +import se.kth.depclean.core.model.ClassName; +import se.kth.depclean.core.model.Dependency; + +/** + * Project dependencies analysis result. + */ +@Getter +@EqualsAndHashCode +public class ProjectDependencyAnalysis { + private static final String SEPARATOR = "-------------------------------------------------------"; + + private final Set usedDirectDependencies; + private final Set usedTransitiveDependencies; + private final Set usedInheritedDependencies; + private final Set unusedDirectDependencies; + private final Set unusedTransitiveDependencies; + private final Set unusedInheritedDependencies; + private final Set ignoredDependencies; + private final Map dependencyClassesMap; + private final DependencyGraph dependencyGraph; + + /** + * The analysis result. + * + * @param usedDirectDependencies used direct dependencies + * @param usedTransitiveDependencies used transitive dependencies + * @param usedInheritedDependencies used inherited dependencies + * @param unusedDirectDependencies unused direct dependencies + * @param unusedTransitiveDependencies unused transitive dependencies + * @param unusedInheritedDependencies unused inherited dependencies + * @param ignoredDependencies ignored dependencies + * @param dependencyClassesMap the whole dependencies and their relates classes and used classes + */ + public ProjectDependencyAnalysis( + Set usedDirectDependencies, + Set usedTransitiveDependencies, + Set usedInheritedDependencies, + Set unusedDirectDependencies, + Set unusedTransitiveDependencies, + Set unusedInheritedDependencies, + Set ignoredDependencies, + Map dependencyClassesMap, + DependencyGraph dependencyGraph) { + this.usedDirectDependencies = copyOf(usedDirectDependencies); + this.usedTransitiveDependencies = copyOf(usedTransitiveDependencies); + this.usedInheritedDependencies = copyOf(usedInheritedDependencies); + this.unusedDirectDependencies = copyOf(unusedDirectDependencies); + this.unusedTransitiveDependencies = copyOf(unusedTransitiveDependencies); + this.unusedInheritedDependencies = copyOf(unusedInheritedDependencies); + this.ignoredDependencies = copyOf(ignoredDependencies); + this.dependencyClassesMap = dependencyClassesMap; + this.dependencyGraph = dependencyGraph; + } + + public boolean hasUsedTransitiveDependencies() { + return !usedTransitiveDependencies.isEmpty(); + } + + public boolean hasUnusedDirectDependencies() { + return !unusedDirectDependencies.isEmpty(); + } + + public boolean hasUnusedTransitiveDependencies() { + return !unusedTransitiveDependencies.isEmpty(); + } + + public boolean hasUnusedInheritedDependencies() { + return !unusedInheritedDependencies.isEmpty(); + } + + /** + * Displays the analysis result. + */ + public void print() { + printString(SEPARATOR); + printString(" D E P C L E A N A N A L Y S I S R E S U L T S"); + printString(SEPARATOR); + printInfoOfDependencies("Used direct dependencies", getUsedDirectDependencies()); + printInfoOfDependencies("Used inherited dependencies", getUsedInheritedDependencies()); + printInfoOfDependencies("Used transitive dependencies", getUsedTransitiveDependencies()); + printInfoOfDependencies("Potentially unused direct dependencies", getUnusedDirectDependencies()); + printInfoOfDependencies("Potentially unused inherited dependencies", getUnusedInheritedDependencies()); + printInfoOfDependencies("Potentially unused transitive dependencies", getUnusedTransitiveDependencies()); + + if (!ignoredDependencies.isEmpty()) { + printString(SEPARATOR); + printString( + "Dependencies ignored in the analysis by the user" + + " [" + ignoredDependencies.size() + "]" + ":" + " "); + ignoredDependencies.forEach(s -> printString("\t" + s)); + } + } + + /** + * Calculates information about the dependency once analysed. + * + * @param coordinate the dependency coordinate (groupId:dependencyId:version) + * @return the information about the dependency + */ + public DependencyAnalysisInfo getDependencyInfo(String coordinate) { + final Dependency dependency = findByCoordinates(coordinate); + return new DependencyAnalysisInfo( + getStatus(dependency), + getType(dependency), + dependency.getSize(), + toValue(dependencyClassesMap.get(dependency).getAllTypes()), + toValue(dependencyClassesMap.get(dependency).getUsedTypes()) + ); + } + + /** + * The processed dependencies. + * + * @return the debloated dependencies + */ + public Set getDebloatedDependencies() { + final Set dependencies = new HashSet<>(getUsedDirectDependencies()); + dependencies.addAll(getUsedTransitiveDependencies()); + return dependencies.stream() + .map(this::toDebloatedDependency) + .collect(toImmutableSet()); + } + + private Dependency findByCoordinates(String coordinate) { + return dependencyClassesMap.keySet().stream() + .filter(dc -> dc.toString().contains(coordinate)) + .findFirst() + .orElseThrow(() -> new RuntimeException("Unable to find " + coordinate + " in dependencies")); + } + + private TreeSet toValue(Set types) { + return types.stream() + .map(ClassName::getValue) + .collect(toCollection(TreeSet::new)); + } + + private String getStatus(Dependency coordinates) { + return (usedDirectDependencies.contains(coordinates) || usedInheritedDependencies + .contains(coordinates) || usedTransitiveDependencies.contains(coordinates)) + ? "used" : + (unusedDirectDependencies.contains(coordinates) || unusedInheritedDependencies + .contains(coordinates) || unusedTransitiveDependencies.contains(coordinates)) + ? "bloated" : "unknown"; + } + + private String getType(Dependency coordinates) { + return (usedDirectDependencies.contains(coordinates) || unusedDirectDependencies + .contains(coordinates)) ? "direct" : + (usedInheritedDependencies.contains(coordinates) || unusedInheritedDependencies + .contains(coordinates)) ? "inherited" : + (usedTransitiveDependencies.contains(coordinates) || unusedTransitiveDependencies + .contains(coordinates)) ? "transitive" : "unknown"; + } + + private void printString(final String string) { + System.out.println(string); //NOSONAR avoid a warning of non-used logger + } + + /** + * Util function to print the information of the analyzed artifacts. + * + * @param info The usage status (used or unused) and type (direct, transitive, inherited) of artifacts. + * @param dependencies The GAV of the artifact. + */ + private void printInfoOfDependencies(final String info, final Set dependencies) { + printString(info.toUpperCase() + " [" + dependencies.size() + "]" + ": "); + printDependencies(dependencies); + } + + /** + * Print the status of the dependencies to the standard output. The format is: "[coordinates][scope] [(size)]" + * + * @param dependencies The set dependencies to print. + */ + private void printDependencies(final Set dependencies) { + dependencies + .stream() + .sorted(Comparator.comparing(Dependency::getSize)) + .collect(Collectors.toCollection(LinkedList::new)) + .descendingIterator() + .forEachRemaining(s -> printString("\t" + s.printWithSize())); + } + + private DebloatedDependency toDebloatedDependency(Dependency dependency) { + final Set dependenciesForParent = dependencyGraph.getDependenciesForParent(dependency); + final Set dependenciesToExclude = dependenciesForParent.stream() + .filter(dep -> getUnusedTransitiveDependencies().contains(dep)) + .collect(Collectors.toSet()); + + return new DebloatedDependency(dependency, copyOf(dependenciesToExclude)); + } +} diff --git a/depclean-core/src/main/java/se/kth/depclean/core/model/ClassName.java b/depclean-core/src/main/java/se/kth/depclean/core/model/ClassName.java new file mode 100644 index 00000000..7f57301f --- /dev/null +++ b/depclean-core/src/main/java/se/kth/depclean/core/model/ClassName.java @@ -0,0 +1,37 @@ +package se.kth.depclean.core.model; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import org.jetbrains.annotations.NotNull; + +/** + * Represents a class to be analysed. + */ +@Getter +@EqualsAndHashCode +public class ClassName implements Comparable { + private final String value; + + /** + * Creates a class representation by its name, and rename it in a defined format. + * + * @param name the class name + */ + public ClassName(String name) { + String className = name.replace('/', '.'); + if (className.endsWith(".class")) { + className = className.substring(0, className.length() - ".class".length()); + } + this.value = className; + } + + @Override + public String toString() { + return value; + } + + @Override + public int compareTo(@NotNull ClassName cn) { + return value.compareTo(cn.value); + } +} diff --git a/depclean-core/src/main/java/se/kth/depclean/core/model/Dependency.java b/depclean-core/src/main/java/se/kth/depclean/core/model/Dependency.java new file mode 100644 index 00000000..47adba9b --- /dev/null +++ b/depclean-core/src/main/java/se/kth/depclean/core/model/Dependency.java @@ -0,0 +1,120 @@ +package se.kth.depclean.core.model; + +import static com.google.common.collect.ImmutableSet.copyOf; + +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.Set; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.io.FileUtils; +import se.kth.depclean.core.analysis.ClassAnalyzer; +import se.kth.depclean.core.analysis.DefaultClassAnalyzer; + +/** + * Identifies a dependency to analyse. + */ +@Slf4j +@Getter +@EqualsAndHashCode(exclude = "file") +public class Dependency { + + private final String groupId; + private final String dependencyId; + private final String version; + private final String scope; + private final File file; + private final Long size; + + private final Iterable relatedClasses; + + /** + * Creates a dependency. + * + * @param groupId groupId + * @param dependencyId dependencyId + * @param version version + * @param scope scope + * @param file the related dependency file (a jar in most cases) + */ + public Dependency(String groupId, String dependencyId, String version, String scope, File file) { + this.groupId = groupId; + this.dependencyId = dependencyId; + this.version = version; + this.scope = scope; + this.file = file; + this.relatedClasses = findRelatedClasses(); + this.size = calculateSize(); + } + + /** + * Creates a dependency for the current project. + * + * @param groupId groupId + * @param dependencyId dependencyId + * @param version version + * @param file the related dependency file (a jar in most cases) + */ + public Dependency(String groupId, String dependencyId, String version, File file) { + this(groupId, dependencyId, version, null, file); + } + + @SuppressWarnings("CopyConstructorMissesField") + protected Dependency(Dependency dependency) { + this(dependency.getGroupId(), dependency.getDependencyId(), dependency.getVersion(), + dependency.getScope(), dependency.getFile()); + } + + @Override + public String toString() { + return String.format("%s:%s:%s:%s", groupId, dependencyId, version, scope); + } + + public String printWithSize() { + return String.format("%s (%s)", this, FileUtils.byteCountToDisplaySize(getSize())); + } + + private Iterable findRelatedClasses() { + final Set relatedClasses = new HashSet<>(); + if (file != null && file.getName().endsWith(".jar")) { + // optimized solution for the jar case + try (JarFile jarFile = new JarFile(file)) { + Enumeration jarEntries = jarFile.entries(); + while (jarEntries.hasMoreElements()) { + String entry = jarEntries.nextElement().getName(); + if (entry.endsWith(".class")) { + relatedClasses.add(new ClassName(entry)); + } + } + } catch (IOException e) { + log.error(e.getMessage(), e); + } + } else if (file != null && file.isDirectory()) { + try { + URL url = file.toURI().toURL(); + ClassAnalyzer classAnalyzer = new DefaultClassAnalyzer(); + Set classes = classAnalyzer.analyze(url); + classes.forEach(c -> relatedClasses.add(new ClassName(c))); + } catch (IOException e) { + log.error(e.getMessage(), e); + } + } + + return copyOf(relatedClasses); + } + + private Long calculateSize() { + try { + return FileUtils.sizeOf(file); + } catch (IllegalArgumentException | NullPointerException e) { + // File does not exist + return 0L; + } + } +} diff --git a/depclean-core/src/main/java/se/kth/depclean/core/model/ProjectContext.java b/depclean-core/src/main/java/se/kth/depclean/core/model/ProjectContext.java new file mode 100644 index 00000000..2ba450a3 --- /dev/null +++ b/depclean-core/src/main/java/se/kth/depclean/core/model/ProjectContext.java @@ -0,0 +1,116 @@ +package se.kth.depclean.core.model; + +import static com.google.common.collect.ImmutableSet.copyOf; + +import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.Iterables; +import com.google.common.collect.Multimap; +import com.google.common.collect.Multimaps; +import java.nio.file.Path; +import java.util.HashSet; +import java.util.Set; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; +import se.kth.depclean.core.analysis.graph.DependencyGraph; + +/** + * Contains all information about the project's context, without any reference + * to a given framework (Maven, Gradle, etc.). + */ +@Slf4j +@ToString +@EqualsAndHashCode +public final class ProjectContext { + + private final Multimap classesPerDependency = ArrayListMultimap.create(); + private final Multimap dependenciesPerClass = ArrayListMultimap.create(); + + @Getter + private final Path outputFolder; + @Getter + private final Path testOutputFolder; + + @Getter + private final Set ignoredScopes; + @Getter + private final Set ignoredDependencies; + + @Getter + private final Set extraClasses; + @Getter + private final DependencyGraph dependencyGraph; + + /** + * Creates a new project context. + * + * @param dependencyGraph the dependencyGraph + * @param outputFolder where the project's classes are compiled + * @param testOutputFolder where the project's test classes are compiled + * @param ignoredScopes the scopes to ignore + * @param ignoredDependencies the dependencies to ignore (i.e. considered as 'used') + * @param extraClasses some classes we want to tell the analyser to consider used + * (like maven processors for instance) + */ + public ProjectContext(DependencyGraph dependencyGraph, + Path outputFolder, Path testOutputFolder, + Set ignoredScopes, + Set ignoredDependencies, + Set extraClasses) { + this.dependencyGraph = dependencyGraph; + this.outputFolder = outputFolder; + this.testOutputFolder = testOutputFolder; + this.ignoredScopes = ignoredScopes; + this.ignoredDependencies = ignoredDependencies; + this.extraClasses = extraClasses; + + ignoredScopes.forEach(scope -> log.info("Ignoring scope {}", scope)); + + populateDependenciesAndClassesMap(dependencyGraph.directDependencies()); + populateDependenciesAndClassesMap(dependencyGraph.inheritedDependencies()); + populateDependenciesAndClassesMap(dependencyGraph.transitiveDependencies()); + + Multimaps.invertFrom(classesPerDependency, dependenciesPerClass); + } + + public Set getClassesForDependency(Dependency dependency) { + return copyOf(classesPerDependency.get(dependency)); + } + + public Set getDependenciesForClass(ClassName className) { + return copyOf(dependenciesPerClass.get(className)); + } + + public boolean hasNoDependencyOnClass(ClassName className) { + return Iterables.isEmpty(getDependenciesForClass(className)); + } + + /** + * Get all known dependencies. + * + * @return all known dependencies + */ + public Set getAllDependencies() { + final Set dependencies = new HashSet<>(dependencyGraph.allDependencies()); + dependencies.add(dependencyGraph.projectCoordinates()); + return copyOf(dependencies); + } + + public boolean ignoreTests() { + return ignoredScopes.contains(new Scope("test")); + } + + private void populateDependenciesAndClassesMap(Set dependencies) { + dependencies.stream() + .filter(this::filterScopesIfNeeded) + .forEach(dc -> classesPerDependency.putAll(dc, dc.getRelatedClasses())); + } + + private boolean filterScopesIfNeeded(Dependency dc) { + final String declaredScope = dc.getScope(); + return ignoredScopes.stream() + .map(Scope::getValue) + .noneMatch(declaredScope::equalsIgnoreCase); + } +} diff --git a/depclean-core/src/main/java/se/kth/depclean/core/model/Scope.java b/depclean-core/src/main/java/se/kth/depclean/core/model/Scope.java new file mode 100644 index 00000000..16360acc --- /dev/null +++ b/depclean-core/src/main/java/se/kth/depclean/core/model/Scope.java @@ -0,0 +1,17 @@ +package se.kth.depclean.core.model; + +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.ToString; + +/** + * Represents a dependency scope. + */ +@Getter +@ToString +@EqualsAndHashCode +@AllArgsConstructor +public class Scope { + private final String value; +} diff --git a/depclean-core/src/main/java/se/kth/depclean/core/wrapper/DependencyManagerWrapper.java b/depclean-core/src/main/java/se/kth/depclean/core/wrapper/DependencyManagerWrapper.java new file mode 100644 index 00000000..2875d92d --- /dev/null +++ b/depclean-core/src/main/java/se/kth/depclean/core/wrapper/DependencyManagerWrapper.java @@ -0,0 +1,106 @@ +package se.kth.depclean.core.wrapper; + +import java.io.File; +import java.io.IOException; +import java.io.Serializable; +import java.nio.file.Path; +import java.util.Set; +import org.apache.maven.plugin.logging.Log; +import se.kth.depclean.core.AbstractDebloater; +import se.kth.depclean.core.analysis.graph.DependencyGraph; +import se.kth.depclean.core.analysis.model.ProjectDependencyAnalysis; + +/** + * Tells a dependency manager (i.e. Maven, gradle, ...) what to expose so the process can be managed from the core + * rather than from the dependency manager plugin + */ +public interface DependencyManagerWrapper { + + /** + * The dependency manager logger. + * + * @return The dependency manager logger + */ + Log getLog(); + + /** + * Whether this is a Maven project. + * + * @return {@code true} if the is a Maven project + */ + boolean isMaven(); + + /** + * Whether the project is of 'pom' kind. + * + * @return {@code true} if the project is a pom project + */ + boolean isPackagingPom(); + + /** + * Copies the dependencies to a folder, to use them later. + */ + void copyAndExtractDependencies(); + + /** + * A representation of the dependency manager's dependency graph. + * + * @return the graph + */ + DependencyGraph dependencyGraph(); + + /** + * Where the sources are compiled to. + * + * @return the path to the compiled sources folder + */ + Path getOutputDirectory(); + + /** + * Where the tests sources are compiled to. + * + * @return the path to the compiled test sources folder + */ + Path getTestOutputDirectory(); + + /** + * Find classes used in processors. + * + * @return the processors + */ + Set collectUsedClassesFromProcessors(); + + /** + * The instance that will debloat the config file. + * + * @param analysis the depclean analysis + * @return the debloater + */ + AbstractDebloater getDebloater(ProjectDependencyAnalysis analysis); + + /** + * The build directory path. + * + * @return the build directory path + */ + String getBuildDirectory(); + + /** + * Generates the dependency tree. + * + * @param treeFile the file to store the result to + */ + void generateDependencyTree(File treeFile) throws IOException, InterruptedException; + + /** + * Gets the JSON representation of the dependency tree. + * + * @param treeFile the file containing the tree + * @param analysis the depclean analysis result + * @param classUsageFile the class usage file + * @param createClassUsageCsv whether to write the class usage down + * @return the JSON tree + */ + String getTreeAsJson( + File treeFile, ProjectDependencyAnalysis analysis, File classUsageFile, boolean createClassUsageCsv); +} \ No newline at end of file diff --git a/depclean-core/src/test/java/se/kth/depclean/core/analysis/ActualUsedClassesTest.java b/depclean-core/src/test/java/se/kth/depclean/core/analysis/ActualUsedClassesTest.java new file mode 100644 index 00000000..5a647abb --- /dev/null +++ b/depclean-core/src/test/java/se/kth/depclean/core/analysis/ActualUsedClassesTest.java @@ -0,0 +1,29 @@ +package se.kth.depclean.core.analysis; + +import static org.assertj.core.api.Assertions.assertThat; +import static com.google.common.collect.ImmutableSet.of; + +import org.junit.jupiter.api.Test; +import se.kth.depclean.core.model.ProjectContext; + +class ActualUsedClassesTest implements ProjectContextCreator { + + @Test + void shouldRegisterClasses() { + final ProjectContext context = createContext(); + final ActualUsedClasses actualUsedClasses = new ActualUsedClasses(context); + actualUsedClasses.registerClasses(of(COMMONS_IO_CLASS)); + + assertThat(actualUsedClasses.getRegisteredClasses()).containsExactly(COMMONS_IO_CLASS); + } + + @Test + void shouldNotRegisterUnknownClasses() { + final ProjectContext context = createContext(); + final ActualUsedClasses actualUsedClasses = new ActualUsedClasses(context); + actualUsedClasses.registerClasses(of(UNKNOWN_CLASS)); + + assertThat(actualUsedClasses.getRegisteredClasses()).isEmpty(); + } + +} \ No newline at end of file diff --git a/depclean-core/src/test/java/se/kth/depclean/core/analysis/ProjectContextCreator.java b/depclean-core/src/test/java/se/kth/depclean/core/analysis/ProjectContextCreator.java new file mode 100644 index 00000000..036a4855 --- /dev/null +++ b/depclean-core/src/test/java/se/kth/depclean/core/analysis/ProjectContextCreator.java @@ -0,0 +1,145 @@ +package se.kth.depclean.core.analysis; + +import static com.google.common.collect.ImmutableSet.copyOf; +import static com.google.common.collect.ImmutableSet.of; +import static org.assertj.core.util.Sets.newHashSet; + +import java.io.File; +import java.nio.file.Paths; +import java.util.Collections; +import java.util.Set; +import lombok.AllArgsConstructor; +import se.kth.depclean.core.analysis.graph.DependencyGraph; +import se.kth.depclean.core.model.ClassName; +import se.kth.depclean.core.model.Dependency; +import se.kth.depclean.core.model.ProjectContext; +import se.kth.depclean.core.model.Scope; + +public interface ProjectContextCreator { + + ClassName COMMONS_IO_CLASS = new ClassName("org.apache.commons.io.IOUtils"); + ClassName COMMONS_LANG_CLASS = new ClassName("org.apache.commons.lang.ArrayUtils"); + ClassName COMMONS_LOGGING_CLASS = new ClassName("org.apache.commons.logging.Log"); + ClassName JUNIT_CLASS = new ClassName("org.junit.jupiter.engine.JupiterTestEngine"); + ClassName UNKNOWN_CLASS = new ClassName("com.unknown.Unknown"); + Dependency COMMONS_IO_DEPENDENCY = createDependency("commons-io"); + Dependency COMMONS_LANG_DEPENDENCY = createDependency("commons-lang"); + Dependency COMMONS_LOGGING_DEPENDENCY = createDependency("commons-logging-api"); + Dependency JUNIT_DEPENDENCY = createTestDependency("junit-jupiter"); + Dependency UNKNOWN_DEPENDENCY = createDependency("unknown"); + + default ProjectContext createContext() { + return new ProjectContext( + new TestDependencyGraph( + createDependency("ExampleClass"), + of(COMMONS_IO_DEPENDENCY, JUNIT_DEPENDENCY), + of(COMMONS_LANG_DEPENDENCY), + of(COMMONS_LOGGING_DEPENDENCY) + ), + Paths.get("main/resources"), + Paths.get("test/resources"), + Collections.emptySet(), + Collections.emptySet(), + Collections.emptySet() + ); + } + + default ProjectContext createContextIgnoringTests() { + return new ProjectContext( + new TestDependencyGraph( + createDependency("ExampleClass"), + of(COMMONS_IO_DEPENDENCY, JUNIT_DEPENDENCY), + of(COMMONS_LANG_DEPENDENCY), + of(COMMONS_LOGGING_DEPENDENCY) + ), + Paths.get("main/resources"), + Paths.get("test/resources"), + of(new Scope("test")), + Collections.emptySet(), + Collections.emptySet() + ); + } + + default ProjectContext createContextIgnoringDependency() { + return new ProjectContext( + new TestDependencyGraph( + createDependency("ExampleClass"), + of(COMMONS_IO_DEPENDENCY), + of(COMMONS_LANG_DEPENDENCY), + of(COMMONS_LOGGING_DEPENDENCY) + ), + Paths.get("main/resources"), + Paths.get("test/resources"), + of(new Scope("test")), + of(COMMONS_IO_DEPENDENCY), + Collections.emptySet() + ); + } + + static Dependency createDependency(String name) { + final File jarFile = new File("src/test/resources/analysisResources/" + name + ".jar"); + return new Dependency( + "se.kth.depclean.core.analysis", + name, + "1.0.0", + "compile", + jarFile + ); + } + + static Dependency createTestDependency(String name) { + final File jarFile = new File("src/test/resources/analysisResources/" + name + ".jar"); + return new Dependency( + "se.kth.depclean.core.analysis", + name, + "1.0.0", + "test", + jarFile + ); + } + + @AllArgsConstructor + class TestDependencyGraph implements DependencyGraph { + + private final Dependency projectCoordinates; + private final Set directDependencies; + private final Set inheritedDependencies; + private final Set transitiveDependencies; + + @Override + public Dependency projectCoordinates() { + return projectCoordinates; + } + + @Override + public Set directDependencies() { + return directDependencies; + } + + @Override + public Set inheritedDependencies() { + return inheritedDependencies; + } + + @Override + public Set transitiveDependencies() { + return transitiveDependencies; + } + + @Override + public Set allDependencies() { + final Set dependencies = newHashSet(directDependencies); + dependencies.addAll(inheritedDependencies); + dependencies.addAll(transitiveDependencies); + return copyOf(dependencies); + } + + @Override + public Set getDependenciesForParent(Dependency parent) { + if (parent.equals(COMMONS_LANG_DEPENDENCY) || parent.equals(COMMONS_IO_DEPENDENCY)) { + return of(COMMONS_LOGGING_DEPENDENCY); + } + return of(); + } + } +} diff --git a/depclean-core/src/test/java/se/kth/depclean/core/analysis/ProjectDependencyAnalysisBuilderTest.java b/depclean-core/src/test/java/se/kth/depclean/core/analysis/ProjectDependencyAnalysisBuilderTest.java new file mode 100644 index 00000000..6645060e --- /dev/null +++ b/depclean-core/src/test/java/se/kth/depclean/core/analysis/ProjectDependencyAnalysisBuilderTest.java @@ -0,0 +1,145 @@ +package se.kth.depclean.core.analysis; + +import static com.google.common.collect.ImmutableSet.of; +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; +import se.kth.depclean.core.model.ProjectContext; + +class ProjectDependencyAnalysisBuilderTest implements ProjectContextCreator { + + @Test + void shouldFindOneUsedDirectDependency() { + final ProjectContext context = createContext(); + final ActualUsedClasses actualUsedClasses = new ActualUsedClasses(context); + actualUsedClasses.registerClasses(of(COMMONS_IO_CLASS)); + final ProjectDependencyAnalysisBuilder analysisBuilder = + new ProjectDependencyAnalysisBuilder(context, actualUsedClasses); + + assertThat(analysisBuilder.analyse().getUsedDirectDependencies()) + .containsExactlyInAnyOrder(COMMONS_IO_DEPENDENCY); + } + + @Test + void shouldFindUsedInheritedDependencies() { + final ProjectContext context = createContext(); + final ActualUsedClasses actualUsedClasses = new ActualUsedClasses(context); + actualUsedClasses.registerClasses(of(COMMONS_LANG_CLASS)); + final ProjectDependencyAnalysisBuilder analysisBuilder = + new ProjectDependencyAnalysisBuilder(context, actualUsedClasses); + + assertThat(analysisBuilder.analyse().getUsedInheritedDependencies()) + .containsExactlyInAnyOrder(COMMONS_LANG_DEPENDENCY); + } + + @Test + void shouldFindUsedTransitiveDependencies() { + final ProjectContext context = createContext(); + final ActualUsedClasses actualUsedClasses = new ActualUsedClasses(context); + actualUsedClasses.registerClasses(of(COMMONS_LOGGING_CLASS)); + final ProjectDependencyAnalysisBuilder analysisBuilder = + new ProjectDependencyAnalysisBuilder(context, actualUsedClasses); + + assertThat(analysisBuilder.analyse().getUsedTransitiveDependencies()) + .containsExactlyInAnyOrder(COMMONS_LOGGING_DEPENDENCY); + } + + @Test + void shouldFindUnusedDirectDependencies() { + final ProjectContext context = createContext(); + final ActualUsedClasses actualUsedClasses = new ActualUsedClasses(context); + final ProjectDependencyAnalysisBuilder analysisBuilder = + new ProjectDependencyAnalysisBuilder(context, actualUsedClasses); + + assertThat(analysisBuilder.analyse().getUnusedDirectDependencies()) + .containsExactlyInAnyOrder(COMMONS_IO_DEPENDENCY, JUNIT_DEPENDENCY); + } + + @Test + void shouldFindUnusedTransitiveDependencies() { + final ProjectContext context = createContext(); + final ActualUsedClasses actualUsedClasses = new ActualUsedClasses(context); + final ProjectDependencyAnalysisBuilder analysisBuilder = + new ProjectDependencyAnalysisBuilder(context, actualUsedClasses); + + assertThat(analysisBuilder.analyse().getUnusedTransitiveDependencies()) + .containsExactlyInAnyOrder(COMMONS_LOGGING_DEPENDENCY); + } + + @Test + void shouldFindUnusedInheritedDependencies() { + final ProjectContext context = createContext(); + final ActualUsedClasses actualUsedClasses = new ActualUsedClasses(context); + final ProjectDependencyAnalysisBuilder analysisBuilder = + new ProjectDependencyAnalysisBuilder(context, actualUsedClasses); + + assertThat(analysisBuilder.analyse().getUnusedInheritedDependencies()) + .containsExactlyInAnyOrder(COMMONS_LANG_DEPENDENCY); + } + + @Test + void shouldIgnoreDependency() { + final ProjectContext context = createContextIgnoringDependency(); + final ActualUsedClasses actualUsedClasses = new ActualUsedClasses(context); + final ProjectDependencyAnalysisBuilder analysisBuilder = + new ProjectDependencyAnalysisBuilder(context, actualUsedClasses); + + assertThat(analysisBuilder.analyse().getUsedDirectDependencies()) + .containsExactlyInAnyOrder(COMMONS_IO_DEPENDENCY); + assertThat(analysisBuilder.analyse().getUnusedDirectDependencies()) + .doesNotContain(COMMONS_IO_DEPENDENCY); + } + + @Test + void shouldHaveRightStatus() { + final ProjectContext context = createContextIgnoringDependency(); + final ActualUsedClasses actualUsedClasses = new ActualUsedClasses(context); + actualUsedClasses.registerClasses(of(COMMONS_IO_CLASS)); + final ProjectDependencyAnalysisBuilder analysisBuilder = + new ProjectDependencyAnalysisBuilder(context, actualUsedClasses); + + assertThat(analysisBuilder.analyse().getDependencyInfo(COMMONS_IO_DEPENDENCY.toString()).getStatus()) + .isEqualTo("used"); + assertThat(analysisBuilder.analyse().getDependencyInfo(COMMONS_LANG_DEPENDENCY.toString()).getStatus()) + .isEqualTo("bloated"); + } + + @Test + void shouldHaveRightType() { + final ProjectContext context = createContextIgnoringDependency(); + final ActualUsedClasses actualUsedClasses = new ActualUsedClasses(context); + actualUsedClasses.registerClasses(of(COMMONS_IO_CLASS)); + final ProjectDependencyAnalysisBuilder analysisBuilder = + new ProjectDependencyAnalysisBuilder(context, actualUsedClasses); + + assertThat(analysisBuilder.analyse().getDependencyInfo(COMMONS_IO_DEPENDENCY.toString()).getType()) + .isEqualTo("direct"); + assertThat(analysisBuilder.analyse().getDependencyInfo(COMMONS_LANG_DEPENDENCY.toString()).getType()) + .isEqualTo("inherited"); + assertThat(analysisBuilder.analyse().getDependencyInfo(COMMONS_LOGGING_DEPENDENCY.toString()).getType()) + .isEqualTo("transitive"); + } + + @Test + void shouldBuildDependencyClassesMap() { + final ProjectContext context = createContext(); + final ActualUsedClasses actualUsedClasses = new ActualUsedClasses(context); + actualUsedClasses.registerClasses(of(COMMONS_IO_CLASS)); + final ProjectDependencyAnalysisBuilder analysisBuilder = + new ProjectDependencyAnalysisBuilder(context, actualUsedClasses); + + assertThat(analysisBuilder.analyse().getDependencyClassesMap()) + .hasEntrySatisfying(COMMONS_IO_DEPENDENCY, dependencyTypes -> { + assertThat(dependencyTypes.getAllTypes()).hasSize(123); + assertThat(dependencyTypes.getUsedTypes()).hasSize(1); + }) + .hasEntrySatisfying(COMMONS_LANG_DEPENDENCY, dependencyTypes -> { + assertThat(dependencyTypes.getAllTypes()).hasSize(127); + assertThat(dependencyTypes.getUsedTypes()).hasSize(0); + }) + .hasEntrySatisfying(COMMONS_LOGGING_DEPENDENCY, dependencyTypes -> { + assertThat(dependencyTypes.getAllTypes()).hasSize(20); + assertThat(dependencyTypes.getUsedTypes()).hasSize(0); + }); + } +} \ No newline at end of file diff --git a/depclean-core/src/test/java/se/kth/depclean/core/analysis/model/ProjectDependencyAnalysisTest.java b/depclean-core/src/test/java/se/kth/depclean/core/analysis/model/ProjectDependencyAnalysisTest.java new file mode 100644 index 00000000..3a2930be --- /dev/null +++ b/depclean-core/src/test/java/se/kth/depclean/core/analysis/model/ProjectDependencyAnalysisTest.java @@ -0,0 +1,40 @@ +package se.kth.depclean.core.analysis.model; + +import static com.google.common.collect.ImmutableSet.of; +import static org.assertj.core.api.Assertions.assertThat; +import se.kth.depclean.core.analysis.ProjectContextCreator; +import static se.kth.depclean.core.analysis.ProjectContextCreator.createDependency; + +import com.google.common.collect.ImmutableMap; +import org.junit.jupiter.api.Test; +import se.kth.depclean.core.analysis.model.DebloatedDependency; +import se.kth.depclean.core.analysis.model.ProjectDependencyAnalysis; + +class ProjectDependencyAnalysisTest implements ProjectContextCreator { + + @Test + void shouldBuildResultingDependencyGraph() { + final ProjectDependencyAnalysis analysis = new ProjectDependencyAnalysis( + of(COMMONS_IO_DEPENDENCY), + of(JUNIT_DEPENDENCY), + of(), + of(COMMONS_LANG_DEPENDENCY), + of(COMMONS_LOGGING_DEPENDENCY), + of(), + of(), + ImmutableMap.of(), + new TestDependencyGraph( + createDependency("ExampleClass"), + of(COMMONS_IO_DEPENDENCY), + of(COMMONS_LANG_DEPENDENCY), + of(JUNIT_DEPENDENCY, COMMONS_LOGGING_DEPENDENCY) + ) + ); + + assertThat(analysis.getDebloatedDependencies()) + .containsExactlyInAnyOrder( + new DebloatedDependency(COMMONS_IO_DEPENDENCY, of(COMMONS_LOGGING_DEPENDENCY)), + new DebloatedDependency(JUNIT_DEPENDENCY, of()) + ); + } +} \ No newline at end of file diff --git a/depclean-core/src/test/java/se/kth/depclean/core/model/ClassNameTest.java b/depclean-core/src/test/java/se/kth/depclean/core/model/ClassNameTest.java new file mode 100644 index 00000000..61d57ab4 --- /dev/null +++ b/depclean-core/src/test/java/se/kth/depclean/core/model/ClassNameTest.java @@ -0,0 +1,18 @@ +package se.kth.depclean.core.model; + +import static org.assertj.core.api.Assertions.assertThat; +import org.junit.jupiter.api.Test; +import se.kth.depclean.core.model.ClassName; + +class ClassNameTest { + + public static final String CLASS_NAME_RESULT = "org.my.Class"; + + @Test + void shouldNormalizeName() { + assertThat(new ClassName("org/my/Class").getValue()).isEqualTo(CLASS_NAME_RESULT); + assertThat(new ClassName("org/my/Class.class").getValue()).isEqualTo(CLASS_NAME_RESULT); + assertThat(new ClassName("org.my.Class").getValue()).isEqualTo(CLASS_NAME_RESULT); + assertThat(new ClassName("org.my.Class.class").getValue()).isEqualTo(CLASS_NAME_RESULT); + } +} \ No newline at end of file diff --git a/depclean-core/src/test/java/se/kth/depclean/core/model/DependencyTest.java b/depclean-core/src/test/java/se/kth/depclean/core/model/DependencyTest.java new file mode 100644 index 00000000..2cca4d1b --- /dev/null +++ b/depclean-core/src/test/java/se/kth/depclean/core/model/DependencyTest.java @@ -0,0 +1,16 @@ +package se.kth.depclean.core.model; + +import static org.assertj.core.api.Assertions.assertThat; +import org.junit.jupiter.api.Test; +import se.kth.depclean.core.analysis.ProjectContextCreator; +import se.kth.depclean.core.model.Dependency; + +class DependencyTest { + + @Test + void shouldGetRelatedClasses() { + final Dependency dependency = ProjectContextCreator.COMMONS_IO_DEPENDENCY; + + assertThat(dependency.getRelatedClasses()).hasSize(123); + } +} diff --git a/depclean-core/src/test/java/se/kth/depclean/core/model/ProjectContextTest.java b/depclean-core/src/test/java/se/kth/depclean/core/model/ProjectContextTest.java new file mode 100644 index 00000000..4e0eeb1c --- /dev/null +++ b/depclean-core/src/test/java/se/kth/depclean/core/model/ProjectContextTest.java @@ -0,0 +1,36 @@ +package se.kth.depclean.core.model; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; +import se.kth.depclean.core.analysis.ProjectContextCreator; +import se.kth.depclean.core.model.ProjectContext; + +class ProjectContextTest implements ProjectContextCreator { + + @Test + void shouldContainDependenciesWithClasses() { + final ProjectContext context = createContext(); + assertThat(context.getClassesForDependency(COMMONS_IO_DEPENDENCY)).hasSize(123); + assertThat(context.getClassesForDependency(COMMONS_LANG_DEPENDENCY)).hasSize(127); + assertThat(context.getClassesForDependency(COMMONS_LOGGING_DEPENDENCY)).hasSize(20); + assertThat(context.getClassesForDependency(JUNIT_DEPENDENCY)).hasSize(113); + assertThat(context.getClassesForDependency(UNKNOWN_DEPENDENCY)).isEmpty(); + } + + @Test + void shouldContainClassWithDependencies() { + final ProjectContext context = createContext(); + assertThat(context.getDependenciesForClass(COMMONS_IO_CLASS)).hasSize(1); + assertThat(context.getDependenciesForClass(JUNIT_CLASS)).hasSize(1); + assertThat(context.getDependenciesForClass(UNKNOWN_CLASS)).isEmpty(); + } + + @Test + void shouldIgnoreTestDependencies() { + final ProjectContext context = createContextIgnoringTests(); + assertThat(context.getDependenciesForClass(COMMONS_IO_CLASS)).hasSize(1); + assertThat(context.getDependenciesForClass(JUNIT_CLASS)).isEmpty(); + assertThat(context.getDependenciesForClass(UNKNOWN_CLASS)).isEmpty(); + } +} diff --git a/depclean-core/src/test/resources/analysisResources/commons-io.jar b/depclean-core/src/test/resources/analysisResources/commons-io.jar new file mode 100644 index 00000000..107b061f Binary files /dev/null and b/depclean-core/src/test/resources/analysisResources/commons-io.jar differ diff --git a/depclean-core/src/test/resources/analysisResources/commons-lang.jar b/depclean-core/src/test/resources/analysisResources/commons-lang.jar new file mode 100644 index 00000000..532939ec Binary files /dev/null and b/depclean-core/src/test/resources/analysisResources/commons-lang.jar differ diff --git a/depclean-core/src/test/resources/analysisResources/commons-logging-api.jar b/depclean-core/src/test/resources/analysisResources/commons-logging-api.jar new file mode 100644 index 00000000..d1abcbb4 Binary files /dev/null and b/depclean-core/src/test/resources/analysisResources/commons-logging-api.jar differ diff --git a/depclean-core/src/test/resources/analysisResources/junit-jupiter.jar b/depclean-core/src/test/resources/analysisResources/junit-jupiter.jar new file mode 100644 index 00000000..024a98f3 Binary files /dev/null and b/depclean-core/src/test/resources/analysisResources/junit-jupiter.jar differ diff --git a/depclean-gradle-plugin/build.gradle b/depclean-gradle-plugin/build.gradle index fea0608a..df113766 100644 --- a/depclean-gradle-plugin/build.gradle +++ b/depclean-gradle-plugin/build.gradle @@ -5,6 +5,12 @@ plugins { id 'maven-publish' } +java { + toolchain { + languageVersion.set(JavaLanguageVersion.of(11)) + } +} + group 'se.kth.castor' version '1.0-SNAPSHOT' diff --git a/depclean-gradle-plugin/gradlew b/depclean-gradle-plugin/gradlew old mode 100644 new mode 100755 diff --git a/depclean-gradle-plugin/src/main/java/se/kth/depclean/analysis/DefaultGradleProjectDependencyAnalyzer.java b/depclean-gradle-plugin/src/main/java/se/kth/depclean/analysis/DefaultGradleProjectDependencyAnalyzer.java index 80ca972a..85c1671e 100644 --- a/depclean-gradle-plugin/src/main/java/se/kth/depclean/analysis/DefaultGradleProjectDependencyAnalyzer.java +++ b/depclean-gradle-plugin/src/main/java/se/kth/depclean/analysis/DefaultGradleProjectDependencyAnalyzer.java @@ -22,14 +22,15 @@ import org.gradle.api.artifacts.ConfigurationContainer; import org.gradle.api.artifacts.ResolvedArtifact; import org.gradle.api.artifacts.ResolvedDependency; -import se.kth.depclean.core.analysis.ArtifactTypes; -import se.kth.depclean.utils.DependencyUtils; +import se.kth.depclean.core.analysis.DependencyTypes; import se.kth.depclean.core.analysis.ClassAnalyzer; import se.kth.depclean.core.analysis.DefaultClassAnalyzer; import se.kth.depclean.core.analysis.DependencyAnalyzer; import se.kth.depclean.core.analysis.ProjectDependencyAnalyzerException; import se.kth.depclean.core.analysis.asm.ASMDependencyAnalyzer; import se.kth.depclean.core.analysis.graph.DefaultCallGraph; +import se.kth.depclean.core.model.ClassName; +import se.kth.depclean.utils.DependencyUtils; /** * This is principal class that perform the dependency analysis in a Gradle project. @@ -312,28 +313,51 @@ private Set removeAll( } /** - * Computes a map of [artifact] -> [allTypes, usedTypes]. + * Computes a map of [dependency] -> [allTypes, usedTypes]. * - * @return A map of [artifact] -> [allTypes, usedTypes] + * @return A map of [dependency] -> [allTypes, usedTypes] */ - public Map getArtifactClassesMap() { - Map output = new HashMap<>(); + public Map getDependenciesClassesMap() { + // the output + Map dependenciesClassMap = new HashMap<>(); + // iterate through all the resolved artifacts for (Map.Entry> entry : artifactClassesMap.entrySet()) { - ResolvedArtifact key = entry.getKey(); - if (artifactUsedClassesMap.containsKey(key)) { - output.put(key.getModuleVersion().toString(), - new ArtifactTypes( - artifactClassesMap.get(key), // get all the types - artifactUsedClassesMap.get(key) // get used types + ResolvedArtifact resolvedArtifact = entry.getKey(); + // all the types in all artifacts + Set typesSet = artifactClassesMap.get(resolvedArtifact); + if (typesSet == null) { + typesSet = new HashSet<>(); + } + Set allClassNameSet = new HashSet<>(); + for (String type : typesSet) { + allClassNameSet.add(new ClassName(type)); + } + // all the types in used artifacts + Set usedTypesSet = artifactUsedClassesMap.get(resolvedArtifact); + if (usedTypesSet == null) { + usedTypesSet = new HashSet<>(); + } + Set usedClassNameSet = new HashSet<>(); + for (String type : usedTypesSet) { + usedClassNameSet.add(new ClassName(type)); + } + + if (artifactUsedClassesMap.containsKey(resolvedArtifact)) { + dependenciesClassMap + .put(resolvedArtifact.getModuleVersion().toString(), + new DependencyTypes( + allClassNameSet, // get all the typesSet + usedClassNameSet // get used typesSet )); } else { - output.put(key.getModuleVersion().toString(), - new ArtifactTypes( - artifactClassesMap.get(key), // get all the types - new HashSet<>() // get used types + dependenciesClassMap + .put(resolvedArtifact.getModuleVersion().toString(), + new DependencyTypes( + allClassNameSet, // get all the typesSet + new HashSet<>() // get used typesSet )); } } - return output; + return dependenciesClassMap; } } diff --git a/depclean-gradle-plugin/src/main/java/se/kth/depclean/utils/json/writeJsonResult.java b/depclean-gradle-plugin/src/main/java/se/kth/depclean/utils/json/writeJsonResult.java index d8e463a2..6b0b05aa 100644 --- a/depclean-gradle-plugin/src/main/java/se/kth/depclean/utils/json/writeJsonResult.java +++ b/depclean-gradle-plugin/src/main/java/se/kth/depclean/utils/json/writeJsonResult.java @@ -15,6 +15,7 @@ import lombok.extern.slf4j.Slf4j; import se.kth.depclean.analysis.DefaultGradleProjectDependencyAnalyzer; import se.kth.depclean.core.analysis.graph.DefaultCallGraph; +import se.kth.depclean.core.model.ClassName; /** * Uses the DepClean analysis results and the declared dependencies of the project @@ -215,20 +216,21 @@ private String getType(String coordinates) { private void writeUsageRatio(String dependencyId, JsonWriter localWriter) throws IOException { localWriter.name("usageRatio") - .value(dependencyAnalyzer.getArtifactClassesMap().containsKey(dependencyId) - ? dependencyAnalyzer.getArtifactClassesMap().get(dependencyId).getAllTypes().isEmpty() + .value(dependencyAnalyzer.getDependenciesClassesMap().containsKey(dependencyId) + ? dependencyAnalyzer.getDependenciesClassesMap().get(dependencyId).getAllTypes().isEmpty() ? 0 : // handle division by zero - ((double) dependencyAnalyzer.getArtifactClassesMap().get(dependencyId).getUsedTypes().size() - / dependencyAnalyzer.getArtifactClassesMap().get(dependencyId).getAllTypes().size()) : -1) + ((double) dependencyAnalyzer.getDependenciesClassesMap().get(dependencyId).getUsedTypes().size() + / dependencyAnalyzer.getDependenciesClassesMap().get(dependencyId).getAllTypes().size()) : -1) .name("children(s)") .beginArray(); } private void writeUsedTypes(String dependencyId, JsonWriter localWriter) throws IOException { JsonWriter usedTypes = localWriter.name("usedTypes").beginArray(); - if (dependencyAnalyzer.getArtifactClassesMap().containsKey(dependencyId)) { - for (String usedType : dependencyAnalyzer.getArtifactClassesMap().get(dependencyId).getUsedTypes()) { - usedTypes.value(usedType); + if (dependencyAnalyzer.getDependenciesClassesMap().containsKey(dependencyId)) { + for (ClassName usedType : dependencyAnalyzer.getDependenciesClassesMap().get(dependencyId).getUsedTypes()) { + System.out.println("Used type: " + usedType.toString()); + usedTypes.value(usedType.getValue()); } } usedTypes.endArray(); @@ -236,9 +238,9 @@ private void writeUsedTypes(String dependencyId, JsonWriter localWriter) throws private void writeAllTypes(String dependencyId, JsonWriter localWriter) throws IOException { JsonWriter allTypes = localWriter.name("allTypes").beginArray(); - if (dependencyAnalyzer.getArtifactClassesMap().containsKey(dependencyId)) { - for (String allType : dependencyAnalyzer.getArtifactClassesMap().get(dependencyId).getAllTypes()) { - allTypes.value(allType); + if (dependencyAnalyzer.getDependenciesClassesMap().containsKey(dependencyId)) { + for (ClassName allType : dependencyAnalyzer.getDependenciesClassesMap().get(dependencyId).getAllTypes()) { + allTypes.value(allType.getValue()); } } allTypes.endArray(); @@ -250,8 +252,8 @@ private void writeClassUsageCsv(String dependencyId) throws IOException { String key = usagePerClassMap.getKey(); Set value = usagePerClassMap.getValue(); for (String s : value) { - if (dependencyAnalyzer.getArtifactClassesMap().containsKey(dependencyId) && dependencyAnalyzer - .getArtifactClassesMap().get(dependencyId).getAllTypes().contains(s)) { + if (dependencyAnalyzer.getDependenciesClassesMap().containsKey(dependencyId) && dependencyAnalyzer + .getDependenciesClassesMap().get(dependencyId).getAllTypes().contains(s)) { String triplet = key + "," + s + "," + dependencyId + "\n"; FileUtils.write(classUsageFile, triplet, Charset.defaultCharset(), true); } diff --git a/depclean-gradle-plugin/src/test/groovy/se/kth/depclean/DepCleanGradleFT.groovy b/depclean-gradle-plugin/src/test/groovy/se/kth/depclean/DepCleanGradleFT.groovy index dee3631b..307b34a7 100644 --- a/depclean-gradle-plugin/src/test/groovy/se/kth/depclean/DepCleanGradleFT.groovy +++ b/depclean-gradle-plugin/src/test/groovy/se/kth/depclean/DepCleanGradleFT.groovy @@ -17,7 +17,7 @@ class DepCleanGradleFT extends Specification { File emptyProjectFile = new File("src/test/resources-fts/empty_project") @Test - @DisplayName("Test that depclean gradle plugin runs on an empty project.") + @DisplayName("Test that the DepClean gradle plugin runs on an empty project.") def "pluginRunsOnEmptyProject"() { given: def project = ProjectBuilder.builder().withProjectDir(emptyProjectFile).build() @@ -88,7 +88,7 @@ class DepCleanGradleFT extends Specification { File debloatedDependenciesIsCorrect = new File(projectPath3) File generatedDebloatedDependenciesDotGradle = new File(projectPath3 + "/debloated-dependencies.gradle"); @Test - @DisplayName("Test that the depclean creates a proper debloated-dependencies.gradle file.") + @DisplayName("Test that the DepClean creates a proper debloated-dependencies.gradle file.") def "debloated_dependencies.gradle_is_correct"() { given: def project = ProjectBuilder.builder().withProjectDir(debloatedDependenciesIsCorrect).build() diff --git a/depclean-maven-plugin/pom.xml b/depclean-maven-plugin/pom.xml index 867f12a2..ae901402 100644 --- a/depclean-maven-plugin/pom.xml +++ b/depclean-maven-plugin/pom.xml @@ -1,5 +1,5 @@ + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> 4.0.0 @@ -16,6 +16,7 @@ depclean-maven-plugin + 17 target/site/jacoco/jacoco.xml ../checkstyle.xml false diff --git a/depclean-maven-plugin/src/main/java/se/kth/depclean/DepCleanMojo.java b/depclean-maven-plugin/src/main/java/se/kth/depclean/DepCleanMojo.java index e8794fa5..0c527810 100644 --- a/depclean-maven-plugin/src/main/java/se/kth/depclean/DepCleanMojo.java +++ b/depclean-maven-plugin/src/main/java/se/kth/depclean/DepCleanMojo.java @@ -17,33 +17,10 @@ package se.kth.depclean; -import fr.dutra.tools.maven.deptree.core.ParseException; -import java.io.File; -import java.io.FileReader; -import java.io.IOException; -import java.nio.charset.Charset; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.Comparator; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; import java.util.Set; -import java.util.stream.Collectors; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.io.FileUtils; -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.plugins.annotations.Component; @@ -51,21 +28,11 @@ 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.ProjectBuildingRequest; 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.ProjectDependencyAnalyzerException; -import se.kth.depclean.util.ChangeDependencyResultUtils; -import se.kth.depclean.util.JarUtils; -import se.kth.depclean.util.MavenInvoker; -import se.kth.depclean.util.ResultsUtils; -import se.kth.depclean.util.json.ParsedDependencies; +import se.kth.depclean.core.DepCleanManager; +import se.kth.depclean.core.analysis.AnalysisFailureException; +import se.kth.depclean.wrapper.MavenDependencyManager; /** * This Maven mojo is the main class of DepClean. DepClean is built on top of the maven-dependency-analyzer component. @@ -80,19 +47,6 @@ @Slf4j public class DepCleanMojo extends AbstractMojo { - private static final String SEPARATOR = "-------------------------------------------------------"; - public static final String DIRECTORY_TO_COPY_DEPENDENCIES = "dependency"; - - /** - * A map [Module coordinates] -> [Depclean result]. - */ - private static final Map ModuleResult = new HashMap<>(); - - /** - * A set to store module id. - */ - private static final Set ModuleDependency = new HashSet<>(); - /** * The Maven project to analyze. */ @@ -182,614 +136,30 @@ public class DepCleanMojo extends AbstractMojo { @Component(hint = "default") private DependencyGraphBuilder dependencyGraphBuilder; - - /** - * 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); - } - - /** - * Print the status of the dependencies to the standard output. The format is: "[coordinates][scope] [(size)]" - * - * @param sizeOfDependencies A map with the size of the dependencies. - * @param dependencies The set dependencies to print. - */ - private void printDependencies(final Map sizeOfDependencies, final Set dependencies) { - dependencies - .stream() - .sorted(Comparator.comparing(o -> getSizeOfDependency(sizeOfDependencies, o))) - .collect(Collectors.toCollection(LinkedList::new)) - .descendingIterator() - .forEachRemaining(s -> printString("\t" + s + " (" + getSize(s, sizeOfDependencies) + ")")); - } - - /** - * Util function to print the information of the analyzed artifacts. - * - * @param info The usage status (used or unused) and type (direct, transitive, inherited) of artifacts. - * @param sizeOfDependencies The size of the JAR file of the artifact. - * @param dependencies The GAV of the artifact. - */ - private void printInfoOfDependencies(final String info, final Map sizeOfDependencies, - final Set dependencies) { - printString(info.toUpperCase() + " [" + dependencies.size() + "]" + ": "); - printDependencies(sizeOfDependencies, dependencies); - } - - /** - * Utility method to obtain the size of a dependency from a map of dependency -> size. If the size of the dependency - * cannot be obtained form the map (no key with the name of the dependency exists), then it returns 0. - * - * @param sizeOfDependencies A map of dependency -> size. - * @param dependency The coordinates of a dependency. - * @return The size of the dependency if its name is a key in the map, otherwise it returns 0. - */ - private Long getSizeOfDependency(final Map sizeOfDependencies, final String dependency) { - Long size = sizeOfDependencies - .get(dependency.split(":")[1] + "-" + dependency.split(":")[2] + ".jar"); - if (size != null) { - return size; - } else { - // The name of the dependency does not match with the name of the download jar, so we keep assume the size - // cannot be obtained and return 0. - return 0L; - } - } - - /** - * Get the size of the dependency in human readable format. - * - * @param dependency The dependency. - * @param sizeOfDependencies A map with the size of the dependencies, keys are stored as the downloaded jar file i.e., - * [artifactId]-[version].jar - * @return The human readable representation of the dependency size. - */ - private String getSize(final String dependency, final Map sizeOfDependencies) { - String dep = dependency.split(":")[1] + "-" + dependency.split(":")[2] + ".jar"; - if (sizeOfDependencies.containsKey(dep)) { - return FileUtils.byteCountToDisplaySize(sizeOfDependencies.get(dep)); - } else { - // The size cannot be obtained. - return "size unknown"; - } - } - - /** - * Exclude artifacts with specific scopes from the analysis. - * - * @param artifacts The set of artifacts to analyze. - * @return The set of artifacts for which the scope has not been excluded. - */ - private Set excludeScope(final Set artifacts) { - Set nonExcludedArtifacts = new HashSet<>(); - for (Artifact artifact : artifacts) { - if (!ignoreScopes.contains(artifact.getScope())) { - nonExcludedArtifacts.add(artifact); - } - } - return nonExcludedArtifacts; - } - - /** - * 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(final Artifact artifact, final 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; - } - - /** - * 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; - } - - private void printString(final String string) { - System.out.println(string); //NOSONAR avoid a warning of non-used logger - } - @SneakyThrows @Override - public final void execute() throws MojoExecutionException { - if (skipDepClean) { - getLog().info("Skipping DepClean plugin execution"); - return; - } - - printString(SEPARATOR); - getLog().info("Starting DepClean dependency analysis"); - - File pomFile = new File(project.getBasedir().getAbsolutePath() + File.separator + "pom.xml"); - - String packaging = project.getPackaging(); - if (packaging.equals("pom")) { - getLog().info("Skipping because packaging type " + packaging + "."); - return; - } - - /* 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 */ + public final void execute() { try { - MavenInvoker.runCommand("mvn dependency:copy-dependencies -DoutputDirectory=" - + project.getBuild().getDirectory() + File.separator + DIRECTORY_TO_COPY_DEPENDENCIES); - } catch (IOException | InterruptedException e) { - getLog().error("Unable to resolve all the dependencies."); - Thread.currentThread().interrupt(); - return; - } - - // TODO remove this workaround later - if (new File(project.getBuild().getDirectory() + File.separator + "libs").exists()) { - try { - FileUtils.copyDirectory(new File(project.getBuild().getDirectory() + File.separator + "libs"), - new File(project.getBuild().getDirectory() + File.separator + DIRECTORY_TO_COPY_DEPENDENCIES) - ); - } catch (IOException | NullPointerException e) { - getLog().error("Error copying directory libs to dependency"); - } - } - - /* Get the size of all the dependencies */ - Map sizeOfDependencies = new HashMap<>(); - // First, add the size of the project, as the sum of all the files in target/classes - String projectJar = project.getArtifactId() + "-" + project.getVersion() + ".jar"; - long projectSize = FileUtils.sizeOf(new File(project.getBuild().getOutputDirectory())); - sizeOfDependencies.put(projectJar, projectSize); - if (Files.exists(Paths.get( - project.getBuild().getDirectory() + File.separator + DIRECTORY_TO_COPY_DEPENDENCIES))) { - Iterator iterator = FileUtils.iterateFiles( - new File( - project.getBuild().getDirectory() + File.separator - + DIRECTORY_TO_COPY_DEPENDENCIES), new String[] {"jar"}, true); - while (iterator.hasNext()) { - File file = iterator.next(); - sizeOfDependencies.put(file.getName(), FileUtils.sizeOf(file)); - } - } else { - log.warn("Dependencies were not copied locally"); - } - - /* Decompress dependencies */ - String dependencyDirectoryName = - project.getBuild().getDirectory() + "/" + DIRECTORY_TO_COPY_DEPENDENCIES; - File dependencyDirectory = new File(dependencyDirectoryName); - if (dependencyDirectory.exists()) { - JarUtils.decompress(dependencyDirectoryName); - } - - /* Analyze dependencies usage status */ - ProjectDependencyAnalysis projectDependencyAnalysis; - DefaultProjectDependencyAnalyzer dependencyAnalyzer = new DefaultProjectDependencyAnalyzer(ignoreTests); - try { - projectDependencyAnalysis = dependencyAnalyzer.analyze(project); - } catch (ProjectDependencyAnalyzerException e) { - getLog().error("Unable to analyze dependencies."); - return; - } - - // Getting coordinates of all artifacts without version. - Set allDependenciesCoordinates = new HashSet<>(); - Set allArtifacts = project.getArtifacts(); - for (Artifact artifact : allArtifacts) { - String coordinate = artifact.getGroupId() + ":" - + artifact.getArtifactId() + ":" - + artifact.getVersion(); - allDependenciesCoordinates.add(coordinate); - } - - Set usedTransitiveArtifacts = projectDependencyAnalysis.getUsedUndeclaredArtifacts(); - Set usedDirectArtifacts = projectDependencyAnalysis.getUsedDeclaredArtifacts(); - Set unusedDirectArtifacts = projectDependencyAnalysis.getUnusedDeclaredArtifacts(); - Set unusedTransitiveArtifacts = new HashSet<>(allArtifacts); - - unusedTransitiveArtifacts.removeAll(usedDirectArtifacts); - unusedTransitiveArtifacts.removeAll(usedTransitiveArtifacts); - unusedTransitiveArtifacts.removeAll(unusedDirectArtifacts); - - /* Exclude dependencies with specific scopes from the DepClean analysis */ - if (!ignoreScopes.isEmpty()) { - printString("Ignoring dependencies with scope(s): " + ignoreScopes); - if (!ignoreScopes.isEmpty()) { - usedTransitiveArtifacts = excludeScope(usedTransitiveArtifacts); - usedDirectArtifacts = excludeScope(usedDirectArtifacts); - unusedDirectArtifacts = excludeScope(unusedDirectArtifacts); - unusedTransitiveArtifacts = excludeScope(unusedTransitiveArtifacts); - } - } - - /* Use artifacts coordinates for the report instead of the Artifact object */ - - // List of dependencies declared in the POM - List dependencies = model.getDependencies(); - Set declaredArtifactsGroupArtifactIds = new HashSet<>(); - for (Dependency dep : dependencies) { - declaredArtifactsGroupArtifactIds.add(dep.getGroupId() + ":" + dep.getArtifactId()); - } - - // --- used dependencies - Set usedDirectArtifactsCoordinates = new HashSet<>(); - Set usedInheritedArtifactsCoordinates = new HashSet<>(); - Set usedTransitiveArtifactsCoordinates = new HashSet<>(); - - for (Artifact artifact : usedDirectArtifacts) { - String artifactGroupArtifactId = artifact.getGroupId() + ":" + artifact.getArtifactId(); - String artifactGroupArtifactIds = - artifactGroupArtifactId + ":" + artifact.toString().split(":")[3] + ":" + artifact.toString() - .split(":")[4]; - if (declaredArtifactsGroupArtifactIds.contains(artifactGroupArtifactId)) { - // the artifact is declared in the pom - usedDirectArtifactsCoordinates.add(artifactGroupArtifactIds); - } else { - // the artifact is inherited - usedInheritedArtifactsCoordinates.add(artifactGroupArtifactIds); - } - } - - // TODO Fix: The used transitive dependencies induced by inherited dependencies should be considered - // as used inherited - for (Artifact artifact : usedTransitiveArtifacts) { - String artifactGroupArtifactId = artifact.getGroupId() + ":" + artifact.getArtifactId(); - String artifactGroupArtifactIds = - artifactGroupArtifactId + ":" + artifact.toString().split(":")[3] + ":" + artifact.toString() - .split(":")[4]; - usedTransitiveArtifactsCoordinates.add(artifactGroupArtifactIds); - } - - // --- unused dependencies - Set unusedDirectArtifactsCoordinates = new HashSet<>(); - Set unusedInheritedArtifactsCoordinates = new HashSet<>(); - Set unusedTransitiveArtifactsCoordinates = new HashSet<>(); - - for (Artifact artifact : unusedDirectArtifacts) { - String artifactGroupArtifactId = artifact.getGroupId() + ":" + artifact.getArtifactId(); - String artifactGroupArtifactIds = - artifactGroupArtifactId + ":" + artifact.toString().split(":")[3] + ":" + artifact.toString() - .split(":")[4]; - if (declaredArtifactsGroupArtifactIds.contains(artifactGroupArtifactId)) { - // the artifact is declared in the pom - unusedDirectArtifactsCoordinates.add(artifactGroupArtifactIds); - } else { - // the artifact is inherited - unusedInheritedArtifactsCoordinates.add(artifactGroupArtifactIds); - } - } - - // TODO Fix: The unused transitive dependencies induced by inherited dependencies should be considered as - // unused inherited - for (Artifact artifact : unusedTransitiveArtifacts) { - String artifactGroupArtifactId = artifact.getGroupId() + ":" + artifact.getArtifactId(); - String artifactGroupArtifactIds = - artifactGroupArtifactId + ":" + artifact.toString().split(":")[3] + ":" + artifact.toString() - .split(":")[4]; - unusedTransitiveArtifactsCoordinates.add(artifactGroupArtifactIds); - } - - /* Ignoring dependencies from the analysis */ - if (ignoreDependencies != null) { - for (String dependencyToIgnore : ignoreDependencies) { - // if the ignored dependency is an unused direct dependency then add it to the set of used direct - // and remove it from the set of unused direct - ignoreDependency(usedDirectArtifactsCoordinates, unusedDirectArtifactsCoordinates, dependencyToIgnore); - // if the ignored dependency is an unused inherited dependency then add it to the set of used inherited - // and remove it from the set of unused inherited - ignoreDependency(usedInheritedArtifactsCoordinates, unusedInheritedArtifactsCoordinates, dependencyToIgnore); - // if the ignored dependency is an unused transitive dependency then add it to the set of used transitive - // and remove it from the set of unused transitive - ignoreDependency(usedTransitiveArtifactsCoordinates, unusedTransitiveArtifactsCoordinates, dependencyToIgnore); - } - } - - // Adding module coordinates as a dependency. - String moduleId = project.getGroupId() + ":" + project.getArtifactId() + ":" + project.getVersion(); - - // Collecting the result. - ResultsUtils resultInfo = new ResultsUtils( - unusedDirectArtifactsCoordinates, - unusedInheritedArtifactsCoordinates, - unusedTransitiveArtifactsCoordinates); - // Mapping the result with module for further usage. - ModuleResult.put(moduleId, resultInfo); - - /* Printing the results to the terminal */ - printString(SEPARATOR); - printString(" D E P C L E A N A N A L Y S I S R E S U L T S"); - printString(SEPARATOR); - printInfoOfDependencies("Used direct dependencies", sizeOfDependencies, usedDirectArtifactsCoordinates); - printInfoOfDependencies("Used inherited dependencies", sizeOfDependencies, usedInheritedArtifactsCoordinates); - printInfoOfDependencies("Used transitive dependencies", sizeOfDependencies, usedTransitiveArtifactsCoordinates); - printInfoOfDependencies("Potentially unused direct dependencies", sizeOfDependencies, - unusedDirectArtifactsCoordinates); - printInfoOfDependencies("Potentially unused inherited dependencies", sizeOfDependencies, - unusedInheritedArtifactsCoordinates); - printInfoOfDependencies("Potentially unused transitive dependencies", sizeOfDependencies, - unusedTransitiveArtifactsCoordinates); - - if (!ignoreDependencies.isEmpty()) { - printString(SEPARATOR); - printString( - "Dependencies ignored in the analysis by the user" - + " [" + ignoreDependencies.size() + "]" + ":" + " "); - ignoreDependencies.stream().forEach(s -> printString("\t" + s)); - } - - // Getting those dependencies from previous modules whose status might have been changed now. - Set dependenciesResultChange = new HashSet<>(); - for (String module : ModuleDependency) { - /* If the module is used as a dependency in the project, - then it will be present in allDependenciesCoordinates. */ - if (allDependenciesCoordinates.contains(module)) { - // Getting the result of specified module. - ResultsUtils result = ModuleResult.get(module); - /* Build will only fail when status of any dependencies has been changed - from unused to used, so getting all the unused dependencies from the - previous modules and comparing that with all the used transitive - dependencies of the current module. */ - Set allUnusedDependency = result.getAllUnusedDependenciesCoordinates(); - for (String usedDependency : usedTransitiveArtifactsCoordinates) { - if (allUnusedDependency.contains(usedDependency)) { - // This dependency status need to be changed. - dependenciesResultChange.add( - new ChangeDependencyResultUtils(usedDependency, - module, - result.getType(usedDependency))); - } - } - } - } - - // Adding the module whose result has been collected. (Alert: This position is specific for adding it) - ModuleDependency.add(moduleId); - - // Printing those dependencies to the terminal whose status needs to be changed. - if (!dependenciesResultChange.isEmpty()) { - printString("\n" + SEPARATOR); - getLog().info("DEPENDENT MODULES FOUND"); - printString("Due to dependent modules, the debloated result of some dependencies" - + " from previous modules has been changed now."); - printString("The dependency-module details of such dependencies with the" - + " new results are as follows :\n"); - int serialNumber = 0; - for (ChangeDependencyResultUtils result : dependenciesResultChange) { - printString("\t" + ++serialNumber + ") ModuleCoordinates : " + result.getModule()); - printString("\t DependencyCoordinates : " + result.getDependencyCoordinate()); - printString("\t OldType : " + result.getType()); - printString("\t NewType : " + result.getNewType()); - printString(""); - } - printString(SEPARATOR); - } - - /* Fail the build if there are unused direct dependencies */ - if (failIfUnusedDirect && !unusedDirectArtifactsCoordinates.isEmpty()) { - throw new MojoExecutionException( - "Build failed due to unused direct dependencies in the dependency tree of the project."); - } - - /* Fail the build if there are unused direct dependencies */ - if (failIfUnusedTransitive && !unusedTransitiveArtifactsCoordinates.isEmpty()) { - throw new MojoExecutionException( - "Build failed due to unused transitive dependencies in the dependency tree of the project."); - } - - /* Fail the build if there are unused direct dependencies */ - if (failIfUnusedInherited && !unusedInheritedArtifactsCoordinates.isEmpty()) { - throw new MojoExecutionException( - "Build failed due to unused inherited dependencies in the dependency tree of the project."); - } - - /* Writing the debloated version of the pom */ - if (createPomDebloated) { - getLog().info("Starting debloating POM"); - - /* Add used transitive as direct dependencies */ - try { - if (!usedTransitiveArtifacts.isEmpty()) { - getLog() - .info("Adding " + unusedTransitiveArtifactsCoordinates.size() - + " used transitive dependencies as direct dependencies."); - for (Artifact usedUndeclaredArtifact : usedTransitiveArtifacts) { - model.addDependency(createDependency(usedUndeclaredArtifact)); - } - } - } catch (Exception e) { - throw new MojoExecutionException(e.getMessage(), e); - } - - /* Remove unused direct dependencies */ - try { - if (!unusedDirectArtifacts.isEmpty()) { - getLog().info("Removing " + unusedDirectArtifactsCoordinates.size() - + " unused direct dependencies."); - for (Artifact unusedDeclaredArtifact : unusedDirectArtifacts) { - 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 (!unusedTransitiveArtifacts.isEmpty()) { - getLog().info( - "Excluding " + unusedTransitiveArtifacts.size() - + " unused transitive dependencies one-by-one."); - for (Dependency dependency : model.getDependencies()) { - for (Artifact artifact : unusedTransitiveArtifacts) { - if (isChildren(artifact, dependency)) { - getLog().info("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 */ - String pathToDebloatedPom = - project.getBasedir().getAbsolutePath() + File.separator + "pom-debloated.xml"; - try { - Path path = Paths.get(pathToDebloatedPom); - 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: " + pathToDebloatedPom); - } - - /* Writing the JSON file with the debloat results */ - if (createResultJson) { - printString("Creating depclean-results.json, please wait..."); - final File jsonFile = new File(project.getBuild().getDirectory() + File.separator + "depclean-results.json"); - final File treeFile = new File(project.getBuild().getDirectory() + File.separator + "tree.txt"); - final File classUsageFile = new File(project.getBuild().getDirectory() + File.separator + "class-usage.csv"); - try { - MavenInvoker.runCommand("mvn dependency:tree -DoutputFile=" + treeFile + " -Dverbose=true"); - } catch (IOException | InterruptedException e) { - getLog().error("Unable to generate dependency tree."); - // Restore interrupted state... - Thread.currentThread().interrupt(); - return; - } - if (createClassUsageCsv) { - printString("Creating class-usage.csv, please wait..."); - try { - FileUtils.write(classUsageFile, "OriginClass,TargetClass,Dependency\n", Charset.defaultCharset()); - } catch (IOException e) { - getLog().error("Error writing the CSV header."); - } - } - ParsedDependencies parsedDependencies = new ParsedDependencies( - treeFile, - sizeOfDependencies, - dependencyAnalyzer, - usedDirectArtifactsCoordinates, - usedInheritedArtifactsCoordinates, - usedTransitiveArtifactsCoordinates, - unusedDirectArtifactsCoordinates, - unusedInheritedArtifactsCoordinates, - unusedTransitiveArtifactsCoordinates, - classUsageFile, + new DepCleanManager( + new MavenDependencyManager( + getLog(), + project, + session, + dependencyGraphBuilder + ), + skipDepClean, + ignoreTests, + ignoreScopes, + ignoreDependencies, + failIfUnusedDirect, + failIfUnusedTransitive, + failIfUnusedInherited, + createPomDebloated, + createResultJson, createClassUsageCsv - ); - try { - FileUtils.write(jsonFile, parsedDependencies.parseTreeToJson(), Charset.defaultCharset()); - } catch (ParseException | IOException e) { - getLog().error("Unable to generate JSON file."); - } - if (jsonFile.exists()) { - getLog().info("depclean-results.json file created in: " + jsonFile.getAbsolutePath()); - } - if (classUsageFile.exists()) { - getLog().info("class-usage.csv file created in: " + classUsageFile.getAbsolutePath()); - } - } - } - - - /** - * If the dependencyToIgnore is an unused dependency, then add it to the set of usedArtifactsCoordinates and remove it - * from the set of unusedArtifactsCoordinates. - * - * @param usedArtifactsCoordinates The set of used artifacts where the dependency will be added. - * @param unusedArtifactsCoordinates The set of unused artifacts where the dependency will be removed. - * @param dependencyToIgnore The dependency to ignore. - */ - private void ignoreDependency( - Set usedArtifactsCoordinates, - Set unusedArtifactsCoordinates, - String dependencyToIgnore) { - for (Iterator i = unusedArtifactsCoordinates.iterator(); i.hasNext(); ) { - String unusedDirectArtifact = i.next(); - if (dependencyToIgnore.equals(unusedDirectArtifact)) { - usedArtifactsCoordinates.add(unusedDirectArtifact); - i.remove(); - break; - } + ).execute(); + } catch (AnalysisFailureException e) { + throw new MojoExecutionException(e.getMessage(), e); } } } diff --git a/depclean-maven-plugin/src/main/java/se/kth/depclean/graph/MavenDependencyGraph.java b/depclean-maven-plugin/src/main/java/se/kth/depclean/graph/MavenDependencyGraph.java new file mode 100644 index 00000000..ef0f52ea --- /dev/null +++ b/depclean-maven-plugin/src/main/java/se/kth/depclean/graph/MavenDependencyGraph.java @@ -0,0 +1,160 @@ +package se.kth.depclean.graph; + +import static com.google.common.collect.ImmutableSet.copyOf; +import static com.google.common.collect.ImmutableSet.toImmutableSet; +import static com.google.common.collect.Sets.newHashSet; + +import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.Multimap; +import java.io.File; +import java.util.Set; +import java.util.stream.Collectors; +import lombok.extern.slf4j.Slf4j; +import org.apache.maven.artifact.Artifact; +import org.apache.maven.model.Model; +import org.apache.maven.project.MavenProject; +import org.apache.maven.shared.dependency.graph.DependencyNode; +import org.jetbrains.annotations.NotNull; +import se.kth.depclean.core.analysis.graph.DependencyGraph; +import se.kth.depclean.core.model.Dependency; + +/** + * A dependency graph for maven reactor. + */ +@Slf4j +public class MavenDependencyGraph implements DependencyGraph { + + private final Set allDependencies; + + private final MavenProject project; + private final DependencyNode rootNode; + private final Set directDependencies; + private final Set inheritedDependencies; + private final Set transitiveDependencies; + private final Multimap dependenciesPerDependency + = ArrayListMultimap.create(); + + /** + * Create a maven dependency graph. + * + * @param project the maven project + * @param rootNode the graph's root node + */ + public MavenDependencyGraph(MavenProject project, Model model, DependencyNode rootNode) { + this.project = project; + this.rootNode = rootNode; + + this.allDependencies = project.getArtifacts().stream() + .map(this::toDepCleanDependency) + .collect(toImmutableSet()); + // The model gets only the direct dependencies (not the inherited ones) + this.directDependencies = model.getDependencies().stream() + .map(this::toDepCleanDependency) + .collect(toImmutableSet()); + // The project gets all the direct dependencies (with the inherited ones) + //noinspection deprecation + this.inheritedDependencies = inheritedDependencies(project.getDependencyArtifacts()); + this.transitiveDependencies = transitiveDependencies(allDependencies); + + buildDependencyDependencies(rootNode); + + if (log.isDebugEnabled()) { + this.allDependencies.forEach(dep -> { + log.debug("Found dependency {}", dep); + if (dependenciesPerDependency.get(dep) != null) { + dependenciesPerDependency.get(dep).forEach(transDep -> log.debug("# {}", transDep)); + } + }); + } + } + + @Override + public Dependency projectCoordinates() { + log.info("project's jar {}", project.getBuild().getDirectory() + "/" + project.getBuild().getFinalName()); + return new Dependency( + rootNode.getArtifact().getGroupId(), + rootNode.getArtifact().getArtifactId(), + rootNode.getArtifact().getVersion(), + new File(project.getBuild().getDirectory() + "/" + project.getBuild().getFinalName() + ".jar") + ); + } + + @Override + public Set allDependencies() { + return allDependencies; + } + + @Override + public Set getDependenciesForParent(Dependency parent) { + return copyOf(dependenciesPerDependency.get(parent)); + } + + @Override + public Set directDependencies() { + return directDependencies; + } + + @Override + public Set inheritedDependencies() { + return inheritedDependencies; + } + + @NotNull + private Set inheritedDependencies(Set dependencyArtifacts) { + final Set visibleDependencies = dependencyArtifacts.stream() + .map(this::toDepCleanDependency) + .collect(Collectors.toSet()); + visibleDependencies.removeAll(this.directDependencies); + return copyOf(visibleDependencies); + } + + @Override + public Set transitiveDependencies() { + return transitiveDependencies; + } + + @NotNull + private Set transitiveDependencies(Set allArtifactsFound) { + final Set transitiveDependencies = newHashSet(allArtifactsFound); + transitiveDependencies.removeAll(this.directDependencies); + transitiveDependencies.removeAll(this.inheritedDependencies); + return copyOf(transitiveDependencies); + } + + private void buildDependencyDependencies(DependencyNode parentNode) { + for (DependencyNode child : parentNode.getChildren()) { + if (!child.getChildren().isEmpty()) { + child.getChildren().forEach(c -> { + dependenciesPerDependency.put(toDepCleanDependency(child), toDepCleanDependency(c)); + buildDependencyDependencies(c); + }); + } + } + } + + private Dependency toDepCleanDependency(Artifact artifact) { + return new Dependency( + artifact.getGroupId(), + artifact.getArtifactId(), + artifact.getVersion(), + artifact.getScope(), + artifact.getFile()); + } + + private Dependency toDepCleanDependency(DependencyNode node) { + return toDepCleanDependency(node.getArtifact()); + } + + private Dependency toDepCleanDependency(org.apache.maven.model.Dependency dependency) { + //noinspection OptionalGetWithoutIsPresent + return allDependencies.stream() + .filter(artifact -> matches(artifact, dependency)) + .findFirst() + .get(); + } + + private boolean matches(Dependency dependencyCoordinate, org.apache.maven.model.Dependency dependency) { + return dependencyCoordinate.getGroupId().equalsIgnoreCase(dependency.getGroupId()) + && dependencyCoordinate.getDependencyId().equalsIgnoreCase(dependency.getArtifactId()); + } +} diff --git a/depclean-maven-plugin/src/main/java/se/kth/depclean/util/ChangeDependencyResultUtils.java b/depclean-maven-plugin/src/main/java/se/kth/depclean/util/ChangeDependencyResultUtils.java deleted file mode 100644 index e669e6df..00000000 --- a/depclean-maven-plugin/src/main/java/se/kth/depclean/util/ChangeDependencyResultUtils.java +++ /dev/null @@ -1,60 +0,0 @@ -package se.kth.depclean.util; - -import java.util.Objects; - -/** - * This will help in changing the status of the dependencies involved in - * dependent modules of a multi-module java project. - */ -public class ChangeDependencyResultUtils { - - private final String dependencyCoordinate; - private final String module; - private final String type; - - /** - * Ctor. - * - * @param dependencyCoordinate Target dependency. - * @param module Target module. - * @param type Debloat status. - */ - public ChangeDependencyResultUtils(final String dependencyCoordinate, - final String module, - final String type) { - this.dependencyCoordinate = dependencyCoordinate; - this.module = module; - this.type = type; - } - - // Getters ------------------------------------------------------------- - public String getDependencyCoordinate() { - return dependencyCoordinate; - } - - public String getModule() { - return module; - } - - public String getType() { - return type; - } - - /** - * Return the new type (status) of the dependency. - * - * @return New type - */ - public String getNewType() { - // Changing the status of debloat. - String newType; - if (Objects.equals(type, "unusedDirect")) { - newType = "usedDirect"; - } else if (Objects.equals(type, "unusedTransitive")) { - newType = "usedTransitive"; - } else { - newType = "usedInherited"; - } - return newType; - } -} diff --git a/depclean-maven-plugin/src/main/java/se/kth/depclean/util/MavenDebloater.java b/depclean-maven-plugin/src/main/java/se/kth/depclean/util/MavenDebloater.java new file mode 100644 index 00000000..c9dc78f8 --- /dev/null +++ b/depclean-maven-plugin/src/main/java/se/kth/depclean/util/MavenDebloater.java @@ -0,0 +1,158 @@ +package se.kth.depclean.util; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import lombok.extern.slf4j.Slf4j; +import org.apache.maven.artifact.Artifact; +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.MavenXpp3Writer; +import org.apache.maven.project.MavenProject; +import se.kth.depclean.core.AbstractDebloater; +import se.kth.depclean.core.analysis.model.DebloatedDependency; +import se.kth.depclean.core.analysis.model.ProjectDependencyAnalysis; + +/** + * Writes a debloated pom is needed. + */ +@Slf4j +public class MavenDebloater extends AbstractDebloater { + + private final MavenProject project; + private final Model model; + private final List initialDependencies; + + /** + * Creates the debloater. + * + * @param analysis the depclean analysis result + * @param project the maven project + * @param model the maven model + */ + public MavenDebloater(ProjectDependencyAnalysis analysis, MavenProject project, Model model) { + super(analysis); + this.project = project; + this.model = model; + this.initialDependencies = model.getDependencies(); + } + + @Override + protected void logDependencies() { + model.getDependencies().forEach(dep -> { + log.debug("Debloated dependency {}", dep); + dep.getExclusions().forEach(excl -> log.debug("- Excluding {}:{}", + excl.getGroupId(), excl.getArtifactId())); + }); + } + + @Override + protected Dependency toMavenDependency(DebloatedDependency debloatedDependency) { + final Dependency dependency = createDependency(debloatedDependency); + debloatedDependency.getExclusions().forEach(depToExclude -> exclude(dependency, depToExclude)); + return dependency; + } + + @Override + protected void setDependencies(List dependencies) { + model.setDependencies(dependencies); + } + + private void exclude(Dependency dependency, se.kth.depclean.core.model.Dependency dependencyToExclude) { + Exclusion exclusion = new Exclusion(); + exclusion.setGroupId(dependencyToExclude.getGroupId()); + exclusion.setArtifactId(dependencyToExclude.getDependencyId()); + dependency.addExclusion(exclusion); + } + + @Override + protected void postProcessDependencies() { + model.getDependencies().forEach(dep -> { + for (Dependency initialDependency : initialDependencies) { + if (hasVersionAsProperty(initialDependency) && matches(dep, initialDependency)) { + dep.setVersion(initialDependency.getVersion()); + } + } + }); + } + + @Override + protected void writeFile() throws IOException { + String pathToDebloatedPom = + project.getBasedir().getAbsolutePath() + File.separator + "pom-debloated.xml"; + Path path = Paths.get(pathToDebloatedPom); + writePom(path); + log.info("POM debloated successfully"); + log.info("pom-debloated.xml file created in: " + pathToDebloatedPom); + } + + private boolean hasVersionAsProperty(Dependency initialDependency) { + return initialDependency.getVersion().startsWith("$"); + } + + /** + * Write pom file to the filesystem. + * + * @param pomFile The path to the pom. + * @throws IOException In case of any IO issue. + */ + private void writePom(final Path pomFile) throws IOException { + MavenXpp3Writer writer = new MavenXpp3Writer(); + writer.write(Files.newBufferedWriter(pomFile), model); + } + + private Artifact findArtifact(se.kth.depclean.core.model.Dependency dependency) { + return project.getArtifacts().stream() + .filter(artifact -> matches(artifact, dependency)) + .findFirst() + .orElseThrow(() -> new RuntimeException("Unable to find " + dependency + " in dependencies")); + } + + private boolean matches(Artifact artifact, se.kth.depclean.core.model.Dependency coordinate) { + return coordinate.toString().toLowerCase().contains( + String.format("%s:%s:%s", artifact.getGroupId(), artifact.getArtifactId(), artifact.getVersion()) + .toLowerCase()); + } + + private boolean matches(Dependency dep, Dependency initialDependency) { + return initialDependency.getGroupId().equals(dep.getGroupId()) + && initialDependency.getArtifactId().equals(dep.getArtifactId()); + } + + /** + * This method creates a Maven {@link Dependency} object from a depclean {@link + * se.kth.depclean.core.model.Dependency}. + * + * @param dependency The depclean dependency to create the maven dependency. + * @return The Dependency object. + */ + private Dependency createDependency( + final se.kth.depclean.core.model.Dependency dependency) { + return createDependency(findArtifact(dependency)); + } + + /** + * This method creates a {@link 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; + } +} diff --git a/depclean-maven-plugin/src/main/java/se/kth/depclean/util/ResultsUtils.java b/depclean-maven-plugin/src/main/java/se/kth/depclean/util/ResultsUtils.java deleted file mode 100644 index 27880c7f..00000000 --- a/depclean-maven-plugin/src/main/java/se/kth/depclean/util/ResultsUtils.java +++ /dev/null @@ -1,58 +0,0 @@ -package se.kth.depclean.util; - -import java.util.HashSet; -import java.util.Set; - -/** - * Collects the data from the report generated by depclean on any module of the project. - */ -public class ResultsUtils { - - private final Set unusedDirectArtifactsCoordinates; - private final Set unusedInheritedArtifactsCoordinates; - private final Set unusedTransitiveArtifactsCoordinates; - private final Set allUnusedDependenciesCoordinates = new HashSet<>(); - - /** - * Ctor. - */ - public ResultsUtils(final Set unusedDirectArtifactsCoordinates, - final Set unusedInheritedArtifactsCoordinates, - final Set unusedTransitiveArtifactsCoordinates) { - this.unusedDirectArtifactsCoordinates = unusedDirectArtifactsCoordinates; - this.unusedInheritedArtifactsCoordinates = unusedInheritedArtifactsCoordinates; - this.unusedTransitiveArtifactsCoordinates = unusedTransitiveArtifactsCoordinates; - } - - // Getters ------------------------------------------------------------------------------ - - /** - * Collect all the unused dependencies from the provided result. - * - * @return A set of all unused dependencies - */ - public Set getAllUnusedDependenciesCoordinates() { - /* Collecting only the unused dependencies, cause in multi-module analysis, build - will only fail when any unused dependencies iof one module is used by another. */ - allUnusedDependenciesCoordinates.addAll(unusedDirectArtifactsCoordinates); - allUnusedDependenciesCoordinates.addAll(unusedInheritedArtifactsCoordinates); - allUnusedDependenciesCoordinates.addAll(unusedTransitiveArtifactsCoordinates); - return allUnusedDependenciesCoordinates; - } - - /** - * To get the type (status) of a dependency. - * - * @param coordinates The dependency. - * @return Type (status) - */ - public String getType(final String coordinates) { - if (unusedDirectArtifactsCoordinates.contains(coordinates)) { - return "unusedDirect"; - } else if (unusedTransitiveArtifactsCoordinates.contains(coordinates)) { - return "unusedTransitive"; - } else { - return "unusedInherited"; - } - } -} diff --git a/depclean-maven-plugin/src/main/java/se/kth/depclean/util/json/NodeAdapter.java b/depclean-maven-plugin/src/main/java/se/kth/depclean/util/json/NodeAdapter.java index 1f40e166..263907ea 100644 --- a/depclean-maven-plugin/src/main/java/se/kth/depclean/util/json/NodeAdapter.java +++ b/depclean-maven-plugin/src/main/java/se/kth/depclean/util/json/NodeAdapter.java @@ -11,9 +11,9 @@ import java.util.Set; import lombok.AllArgsConstructor; import org.apache.commons.io.FileUtils; -import org.jetbrains.annotations.NotNull; -import se.kth.depclean.core.analysis.DefaultProjectDependencyAnalyzer; import se.kth.depclean.core.analysis.graph.DefaultCallGraph; +import se.kth.depclean.core.analysis.model.DependencyAnalysisInfo; +import se.kth.depclean.core.analysis.model.ProjectDependencyAnalysis; /** * Custom Gson type adapter to write a JSON file with information of the dependencies. @@ -21,22 +21,15 @@ @AllArgsConstructor public class NodeAdapter extends TypeAdapter { - private final Set usedDirectArtifactsCoordinates; - private final Set usedInheritedArtifactsCoordinates; - private final Set usedTransitiveArtifactsCoordinates; - private final Set unusedDirectArtifactsCoordinates; - private final Set unusedInheritedArtifactsCoordinates; - private final Set unusedTransitiveArtifactsCoordinates; - private final Map sizeOfDependencies; - private final DefaultProjectDependencyAnalyzer dependencyAnalyzer; + private final ProjectDependencyAnalysis analysis; private final File classUsageFile; private final boolean createClassUsageCsv; @Override public void write(JsonWriter jsonWriter, Node node) throws IOException { String ga = node.getGroupId() + ":" + node.getArtifactId(); + String gav = ga + ":" + node.getVersion(); String vs = node.getVersion() + ":" + node.getScope(); - String coordinates = ga + ":" + vs; String canonical = ga + ":" + node.getPackaging() + ":" + vs; String dependencyJar = node.getArtifactId() + "-" + node.getVersion() + ".jar"; @@ -44,12 +37,14 @@ public void write(JsonWriter jsonWriter, Node node) throws IOException { writeClassUsageCsv(canonical); } + final DependencyAnalysisInfo dependencyInfo = analysis.getDependencyInfo(gav); + JsonWriter localWriter = jsonWriter.beginObject() .name("id") .value(canonical) .name("coordinates") - .value(node.getGroupId() + ":" + node.getArtifactId() + ":" + node.getVersion()) + .value(gav) .name("groupId") .value(node.getGroupId()) @@ -73,20 +68,20 @@ public void write(JsonWriter jsonWriter, Node node) throws IOException { .value(node.getClassifier()) .name("size") - .value(sizeOfDependencies.get(dependencyJar)) + .value(dependencyInfo.getSize()) .name("type") - .value(getType(coordinates)) + .value(dependencyInfo.getType()) .name("status") - .value(getStatus(coordinates)) + .value(dependencyInfo.getStatus()) .name("parent") .value(getParent(node)); - writeAllTypes(canonical, localWriter); - writeUsedTypes(canonical, localWriter); - writeUsageRatio(canonical, localWriter); + writeAllTypes(dependencyInfo, localWriter); + writeUsedTypes(dependencyInfo, localWriter); + writeUsageRatio(dependencyInfo, localWriter); for (Node c : node.getChildNodes()) { this.write(jsonWriter, c); @@ -99,53 +94,25 @@ private String getParent(Node node) { return node.getParent() != null ? node.getParent().getArtifactCanonicalForm() : "unknown"; } - @NotNull - private String getStatus(String coordinates) { - return (usedDirectArtifactsCoordinates.contains(coordinates) || usedInheritedArtifactsCoordinates - .contains(coordinates) || usedTransitiveArtifactsCoordinates.contains(coordinates)) - ? "used" : - (unusedDirectArtifactsCoordinates.contains(coordinates) || unusedInheritedArtifactsCoordinates - .contains(coordinates) || unusedTransitiveArtifactsCoordinates.contains(coordinates)) - ? "bloated" : "unknown"; - } - - @NotNull - private String getType(String coordinates) { - return (usedDirectArtifactsCoordinates.contains(coordinates) || unusedDirectArtifactsCoordinates - .contains(coordinates)) ? "direct" : - (usedInheritedArtifactsCoordinates.contains(coordinates) || unusedInheritedArtifactsCoordinates - .contains(coordinates)) ? "inherited" : - (usedTransitiveArtifactsCoordinates.contains(coordinates) || unusedTransitiveArtifactsCoordinates - .contains(coordinates)) ? "transitive" : "unknown"; - } - - private void writeUsageRatio(String canonical, JsonWriter localWriter) throws IOException { + private void writeUsageRatio(DependencyAnalysisInfo info, JsonWriter localWriter) throws IOException { localWriter.name("usageRatio") - .value(dependencyAnalyzer.getArtifactClassesMap().containsKey(canonical) - ? dependencyAnalyzer.getArtifactClassesMap().get(canonical).getAllTypes().isEmpty() - ? 0 : // handle division by zero - ((double) dependencyAnalyzer.getArtifactClassesMap().get(canonical).getUsedTypes().size() - / dependencyAnalyzer.getArtifactClassesMap().get(canonical).getAllTypes().size()) : -1) + .value(info.getAllTypes().isEmpty() ? 0 : ((double) info.getUsedTypes().size() / info.getAllTypes().size())) .name("children") .beginArray(); } - private void writeUsedTypes(String canonical, JsonWriter localWriter) throws IOException { + private void writeUsedTypes(DependencyAnalysisInfo info, JsonWriter localWriter) throws IOException { JsonWriter usedTypes = localWriter.name("usedTypes").beginArray(); - if (dependencyAnalyzer.getArtifactClassesMap().containsKey(canonical)) { - for (String usedType : dependencyAnalyzer.getArtifactClassesMap().get(canonical).getUsedTypes()) { - usedTypes.value(usedType); - } + for (String usedType : info.getUsedTypes()) { + usedTypes.value(usedType); } usedTypes.endArray(); } - private void writeAllTypes(String canonical, JsonWriter localWriter) throws IOException { + private void writeAllTypes(DependencyAnalysisInfo info, JsonWriter localWriter) throws IOException { JsonWriter allTypes = localWriter.name("allTypes").beginArray(); - if (dependencyAnalyzer.getArtifactClassesMap().containsKey(canonical)) { - for (String allType : dependencyAnalyzer.getArtifactClassesMap().get(canonical).getAllTypes()) { - allTypes.value(allType); - } + for (String allType : info.getAllTypes()) { + allTypes.value(allType); } allTypes.endArray(); } @@ -156,11 +123,8 @@ private void writeClassUsageCsv(String canonical) throws IOException { String key = usagePerClassMap.getKey(); Set value = usagePerClassMap.getValue(); for (String s : value) { - if (dependencyAnalyzer.getArtifactClassesMap().containsKey(canonical) && dependencyAnalyzer - .getArtifactClassesMap().get(canonical).getAllTypes().contains(s)) { - String triplet = key + "," + s + "," + canonical + "\n"; - FileUtils.write(classUsageFile, triplet, Charset.defaultCharset(), true); - } + String triplet = key + "," + s + "," + canonical + "\n"; + FileUtils.write(classUsageFile, triplet, Charset.defaultCharset(), true); } } } diff --git a/depclean-maven-plugin/src/main/java/se/kth/depclean/util/json/ParsedDependencies.java b/depclean-maven-plugin/src/main/java/se/kth/depclean/util/json/ParsedDependencies.java index 3f634f3b..93d78ad6 100644 --- a/depclean-maven-plugin/src/main/java/se/kth/depclean/util/json/ParsedDependencies.java +++ b/depclean-maven-plugin/src/main/java/se/kth/depclean/util/json/ParsedDependencies.java @@ -13,29 +13,18 @@ import java.io.InputStreamReader; import java.io.Reader; import java.nio.charset.StandardCharsets; -import java.util.Map; -import java.util.Set; import lombok.AllArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import se.kth.depclean.core.analysis.DefaultProjectDependencyAnalyzer; +import se.kth.depclean.core.analysis.model.ProjectDependencyAnalysis; /** * Uses the DepClean analysis results and the dependency tree of the project to produce a JSON file. This file represent * the structure of the dependency tree enriched with metadata of the usage or not of each dependency. */ -@Slf4j @AllArgsConstructor public class ParsedDependencies { private final File treeFile; - private final Map sizeOfDependencies; - private final DefaultProjectDependencyAnalyzer dependencyAnalyzer; - private final Set usedDirectArtifactsCoordinates; - private final Set usedInheritedArtifactsCoordinates; - private final Set usedTransitiveArtifactsCoordinates; - private final Set unusedDirectArtifactsCoordinates; - private final Set unusedInheritedArtifactsCoordinates; - private final Set unusedTransitiveArtifactsCoordinates; + private final ProjectDependencyAnalysis analysis; private final File classUsageFile; private final boolean createClassUsageCsv; @@ -54,14 +43,7 @@ public String parseTreeToJson() throws ParseException, IOException { Parser parser = type.newParser(); Node tree = parser.parse(r); NodeAdapter nodeAdapter = new NodeAdapter( - usedDirectArtifactsCoordinates, - usedInheritedArtifactsCoordinates, - usedTransitiveArtifactsCoordinates, - unusedDirectArtifactsCoordinates, - unusedInheritedArtifactsCoordinates, - unusedTransitiveArtifactsCoordinates, - sizeOfDependencies, - dependencyAnalyzer, + analysis, classUsageFile, createClassUsageCsv ); diff --git a/depclean-maven-plugin/src/main/java/se/kth/depclean/wrapper/MavenDependencyManager.java b/depclean-maven-plugin/src/main/java/se/kth/depclean/wrapper/MavenDependencyManager.java new file mode 100644 index 00000000..b0726521 --- /dev/null +++ b/depclean-maven-plugin/src/main/java/se/kth/depclean/wrapper/MavenDependencyManager.java @@ -0,0 +1,217 @@ +package se.kth.depclean.wrapper; + +import com.google.common.collect.ImmutableSet; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.io.Serializable; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; +import lombok.AllArgsConstructor; +import lombok.SneakyThrows; +import org.apache.commons.io.FileUtils; +import org.apache.maven.execution.MavenSession; +import org.apache.maven.model.Model; +import org.apache.maven.model.io.xpp3.MavenXpp3Reader; +import org.apache.maven.plugin.logging.Log; +import org.apache.maven.project.DefaultProjectBuildingRequest; +import org.apache.maven.project.MavenProject; +import org.apache.maven.project.ProjectBuildingRequest; +import org.apache.maven.shared.dependency.graph.DependencyGraphBuilder; +import org.apache.maven.shared.dependency.graph.DependencyNode; +import org.codehaus.plexus.util.xml.Xpp3Dom; +import se.kth.depclean.core.AbstractDebloater; +import se.kth.depclean.core.analysis.graph.DependencyGraph; +import se.kth.depclean.core.analysis.model.ProjectDependencyAnalysis; +import se.kth.depclean.core.wrapper.DependencyManagerWrapper; +import se.kth.depclean.graph.MavenDependencyGraph; +import se.kth.depclean.util.JarUtils; +import se.kth.depclean.util.MavenDebloater; +import se.kth.depclean.util.MavenInvoker; +import se.kth.depclean.util.json.ParsedDependencies; + +/** + * Maven's implementation of the dependency manager wrapper. + */ +@AllArgsConstructor +public class MavenDependencyManager implements DependencyManagerWrapper { + + private static final String DIRECTORY_TO_COPY_DEPENDENCIES = "dependency"; + + private final Log logger; + private final MavenProject project; + private final MavenSession session; + private final DependencyGraphBuilder dependencyGraphBuilder; + private final Model model; + + /** + * Creates the manager. + * + * @param logger the logger + * @param project the maven project + * @param session the maven session + * @param dependencyGraphBuilder a tool to build the dependency graph + */ + public MavenDependencyManager(Log logger, MavenProject project, MavenSession session, + DependencyGraphBuilder dependencyGraphBuilder) { + this.logger = logger; + this.project = project; + this.session = session; + this.dependencyGraphBuilder = dependencyGraphBuilder; + + this.model = buildModel(project); + } + + @Override + public Log getLog() { + return logger; + } + + @Override + public boolean isMaven() { + return true; + } + + @Override + public boolean isPackagingPom() { + return project.getPackaging().equals("pom"); + } + + @Override + public void copyAndExtractDependencies() { + /* Copy direct dependencies locally */ + try { + MavenInvoker.runCommand("mvn dependency:copy-dependencies -DoutputDirectory=" + + project.getBuild().getDirectory() + File.separator + DIRECTORY_TO_COPY_DEPENDENCIES); + } catch (IOException | InterruptedException e) { + getLog().error("Unable to resolve all the dependencies."); + Thread.currentThread().interrupt(); + throw new RuntimeException(e); + } + + // TODO remove this workaround later + if (new File(project.getBuild().getDirectory() + File.separator + "libs").exists()) { + try { + FileUtils.copyDirectory(new File(project.getBuild().getDirectory() + File.separator + "libs"), + new File(project.getBuild().getDirectory() + File.separator + DIRECTORY_TO_COPY_DEPENDENCIES) + ); + } catch (IOException | NullPointerException e) { + getLog().error("Error copying directory libs to dependency"); + throw new RuntimeException(e); + } + } + + /* Decompress dependencies */ + String dependencyDirectoryName = + project.getBuild().getDirectory() + "/" + DIRECTORY_TO_COPY_DEPENDENCIES; + File dependencyDirectory = new File(dependencyDirectoryName); + if (dependencyDirectory.exists()) { + JarUtils.decompress(dependencyDirectoryName); + } + } + + @Override + @SneakyThrows + public DependencyGraph dependencyGraph() { + ProjectBuildingRequest buildingRequest = new DefaultProjectBuildingRequest( + session.getProjectBuildingRequest()); + buildingRequest.setProject(project); + DependencyNode rootNode = dependencyGraphBuilder.buildDependencyGraph(buildingRequest, null); + return new MavenDependencyGraph(project, model, rootNode); + } + + @Override + public Path getOutputDirectory() { + return Paths.get(project.getBuild().getOutputDirectory()); + } + + @Override + public Path getTestOutputDirectory() { + return Paths.get(project.getBuild().getTestOutputDirectory()); + } + + private Model buildModel(MavenProject project) { + File pomFile = new File(project.getBasedir().getAbsolutePath() + File.separator + "pom.xml"); + + /* Build Maven model to manipulate the pom */ + final 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."); + throw new RuntimeException(ex); + } + return model; + } + + /** + * Maven processors are defined like this. + *
{@code
+   *       
+   *         org.bsc.maven
+   *         maven-processor-plugin
+   *         
+   *           
+   *             process
+   *             [...]
+   *             
+   *               
+   *                 XXXProcessor
+   *               
+   *             
+   *           
+   *         
+   *       
+   * }
+ */ + @Override + public Set collectUsedClassesFromProcessors() { + getLog().debug("# collectUsedClassesFromProcessors()"); + return Optional.ofNullable(project.getPlugin("org.bsc.maven:maven-processor-plugin")) + .map(plugin -> plugin.getExecutionsAsMap().get("process")) + .map(exec -> (Xpp3Dom) exec.getConfiguration()) + .map(config -> config.getChild("processors")) + .map(Xpp3Dom::getChildren) + .map(arr -> Arrays.stream(arr).map(Xpp3Dom::getValue).collect(Collectors.toSet())) + .orElse(ImmutableSet.of()); + } + + @Override + public AbstractDebloater getDebloater(ProjectDependencyAnalysis analysis) { + return new MavenDebloater( + analysis, + project, + model + ); + } + + @Override + public String getBuildDirectory() { + return project.getBuild().getDirectory(); + } + + @Override + public void generateDependencyTree(File treeFile) throws IOException, InterruptedException { + MavenInvoker.runCommand("mvn dependency:tree -DoutputFile=" + treeFile + " -Dverbose=true"); + } + + @SneakyThrows + @Override + public String getTreeAsJson( + File treeFile, ProjectDependencyAnalysis analysis, File classUsageFile, boolean createClassUsageCsv) { + return new ParsedDependencies( + treeFile, + analysis, + classUsageFile, + createClassUsageCsv + ).parseTreeToJson(); + } +} diff --git a/depclean-maven-plugin/src/test/java/se/kth/depclean/DepCleanMojoIT.java b/depclean-maven-plugin/src/test/java/se/kth/depclean/DepCleanMojoIT.java index 612a3e65..ceffba84 100644 --- a/depclean-maven-plugin/src/test/java/se/kth/depclean/DepCleanMojoIT.java +++ b/depclean-maven-plugin/src/test/java/se/kth/depclean/DepCleanMojoIT.java @@ -121,10 +121,10 @@ void debloated_pom_is_correct(MavenExecutionResult result) { assertThat(result).isSuccessful() .out() .plain().contains( - "[INFO] Starting debloating POM", - "[INFO] Adding 1 used transitive dependencies as direct dependencies.", - "[INFO] Removing 1 unused direct dependencies.", - "[INFO] Excluding 1 unused transitive dependencies one-by-one.", + "[INFO] Starting debloating file", + "[INFO] Adding 1 used transitive dependency as direct dependency.", + "[INFO] Removing 1 unused direct dependency.", + "[INFO] Excluding 1 unused transitive dependency one-by-one.", "[INFO] POM debloated successfully", "[INFO] pom-debloated.xml file created in: " + generated_pom_debloated.getAbsolutePath()); Assertions.assertTrue(generated_pom_debloated.exists()); diff --git a/depclean-maven-plugin/src/test/java/se/kth/depclean/util/MavenInvokerTest.java b/depclean-maven-plugin/src/test/java/se/kth/depclean/util/MavenInvokerTest.java index 252d4bc4..1b586f64 100644 --- a/depclean-maven-plugin/src/test/java/se/kth/depclean/util/MavenInvokerTest.java +++ b/depclean-maven-plugin/src/test/java/se/kth/depclean/util/MavenInvokerTest.java @@ -27,15 +27,15 @@ class MavenInvokerTest { @Test @DisplayName("Test that the Maven dependency tree, then the dependency tree is obtained") void testRunCommandToGetDependencyTree() throws IOException, InterruptedException { - MavenInvoker.runCommand("mvn dependency:tree -DoutputFile=" + producedTree + " -Dverbose=true"); + MavenInvoker.runCommand("mvn dependency:tree -DoutputFile=" + producedTree); assertTrue(producedTree.exists()); assertThat(producedTree).hasSameTextualContentAs(expectedTree); } - @AfterAll - public static void tearDown() throws IOException { - if (producedTree.exists()) { - FileUtils.forceDelete(producedTree); - } - } + //@AfterAll + //public static void tearDown() throws IOException { + // if (producedTree.exists()) { + // FileUtils.forceDelete(producedTree); + // } + //} } \ No newline at end of file diff --git a/depclean-maven-plugin/src/test/resources/DepCleanMojoResources/depclean-results.json b/depclean-maven-plugin/src/test/resources/DepCleanMojoResources/depclean-results.json index 4b0a2d27..7682a8eb 100644 --- a/depclean-maven-plugin/src/test/resources/DepCleanMojoResources/depclean-results.json +++ b/depclean-maven-plugin/src/test/resources/DepCleanMojoResources/depclean-results.json @@ -6,13 +6,13 @@ "version": "1.0.0-SNAPSHOT", "packaging": "jar", "omitted": false, - "size": 1284, + "size": 2859, "type": "unknown", "status": "unknown", "parent": "unknown", "allTypes": [], "usedTypes": [], - "usageRatio": -1.0, + "usageRatio": 0.0, "children": [ { "id": "com.jcabi:jcabi-manifests:jar:1.1:compile", @@ -28,13 +28,13 @@ "status": "used", "parent": "org.foo.bar:foobar:jar:1.0.0-SNAPSHOT", "allTypes": [ - "com.jcabi.manifests.Mfs", - "com.jcabi.manifests.MfMap", - "com.jcabi.manifests.StreamsMfs", + "com.jcabi.manifests.ClasspathMfs", "com.jcabi.manifests.FilesMfs", "com.jcabi.manifests.Manifests", + "com.jcabi.manifests.MfMap", + "com.jcabi.manifests.Mfs", "com.jcabi.manifests.ServletMfs", - "com.jcabi.manifests.ClasspathMfs" + "com.jcabi.manifests.StreamsMfs" ], "usedTypes": [ "com.jcabi.manifests.Manifests" @@ -56,188 +56,188 @@ "status": "bloated", "parent": "org.foo.bar:foobar:jar:1.0.0-SNAPSHOT", "allTypes": [ - "org.apache.commons.io.LineIterator", - "org.apache.commons.io.input.ObservableInputStream", - "org.apache.commons.io.comparator.AbstractFileComparator", - "org.apache.commons.io.output.FilterCollectionWriter", - "org.apache.commons.io.input.ReaderInputStream", - "org.apache.commons.io.output.NullPrintStream", - "org.apache.commons.io.input.XmlStreamReaderException", - "org.apache.commons.io.FileCleaningTracker$Tracker", - "org.apache.commons.io.EndianUtils", - "org.apache.commons.io.serialization.RegexpClassNameMatcher", - "org.apache.commons.io.output.FileWriterWithEncoding", - "org.apache.commons.io.output.AbstractByteArrayOutputStream$InputStreamConstructor", - "org.apache.commons.io.input.CloseShieldInputStream", - "org.apache.commons.io.output.AppendableWriter", - "org.apache.commons.io.filefilter.EmptyFileFilter", - "org.apache.commons.io.input.XmlStreamReader", - "org.apache.commons.io.file.CopyDirectoryVisitor", - "org.apache.commons.io.filefilter.NotFileFilter", - "org.apache.commons.io.filefilter.TrueFileFilter", - "org.apache.commons.io.input.ReversedLinesFileReader$FilePart", - "org.apache.commons.io.input.ObservableInputStream$Observer", - "org.apache.commons.io.filefilter.AgeFileFilter", - "org.apache.commons.io.serialization.ValidatingObjectInputStream", + "org.apache.commons.io.ByteOrderMark", + "org.apache.commons.io.ByteOrderParser", + "org.apache.commons.io.Charsets", "org.apache.commons.io.CopyUtils", - "org.apache.commons.io.IOExceptionWithCause", "org.apache.commons.io.DirectoryWalker", - "org.apache.commons.io.ByteOrderParser", - "org.apache.commons.io.output.ByteArrayOutputStream", + "org.apache.commons.io.DirectoryWalker$CancelException", + "org.apache.commons.io.EndianUtils", + "org.apache.commons.io.FileCleaner", + "org.apache.commons.io.FileCleaningTracker", + "org.apache.commons.io.FileCleaningTracker$Reaper", + "org.apache.commons.io.FileCleaningTracker$Tracker", + "org.apache.commons.io.FileDeleteStrategy", + "org.apache.commons.io.FileDeleteStrategy$ForceFileDeleteStrategy", + "org.apache.commons.io.FileExistsException", + "org.apache.commons.io.FileSystem", + "org.apache.commons.io.FileSystemUtils", + "org.apache.commons.io.FileUtils", + "org.apache.commons.io.FilenameUtils", "org.apache.commons.io.HexDump", - "org.apache.commons.io.filefilter.ConditionalFileFilter", - "org.apache.commons.io.input.ProxyReader", - "org.apache.commons.io.comparator.ExtensionFileComparator", - "org.apache.commons.io.input.buffer.CircularBufferInputStream", - "org.apache.commons.io.serialization.ClassNameMatcher", - "org.apache.commons.io.output.ProxyCollectionWriter", - "org.apache.commons.io.filefilter.DirectoryFileFilter", - "org.apache.commons.io.input.BrokenReader", + "org.apache.commons.io.IOCase", + "org.apache.commons.io.IOExceptionList", + "org.apache.commons.io.IOExceptionWithCause", + "org.apache.commons.io.IOIndexedException", "org.apache.commons.io.IOUtils", - "org.apache.commons.io.file.Counters$Counter", - "org.apache.commons.io.filefilter.SuffixFileFilter", - "org.apache.commons.io.output.ProxyWriter", - "org.apache.commons.io.file.StandardDeleteOption", - "org.apache.commons.io.input.NullInputStream", - "org.apache.commons.io.comparator.LastModifiedFileComparator", + "org.apache.commons.io.LineIterator", "org.apache.commons.io.TaggedIOException", - "org.apache.commons.io.file.Counters$LongPathCounters", - "org.apache.commons.io.FileSystem", - "org.apache.commons.io.input.buffer.PeekableInputStream", - "org.apache.commons.io.file.Counters$LongCounter", - "org.apache.commons.io.FileCleaningTracker", - "org.apache.commons.io.input.UnixLineEndingInputStream", - "org.apache.commons.io.filefilter.DelegateFileFilter", - "org.apache.commons.io.input.Tailer", - "org.apache.commons.io.filefilter.AndFileFilter", - "org.apache.commons.io.input.TeeInputStream", - "org.apache.commons.io.input.BoundedReader", - "org.apache.commons.io.input.WindowsLineEndingInputStream", - "org.apache.commons.io.output.ChunkedOutputStream", - "org.apache.commons.io.file.SimplePathVisitor", - "org.apache.commons.io.input.CharSequenceReader", - "org.apache.commons.io.input.MessageDigestCalculatingInputStream", - "org.apache.commons.io.output.UnsynchronizedByteArrayOutputStream", + "org.apache.commons.io.ThreadMonitor", + "org.apache.commons.io.comparator.AbstractFileComparator", + "org.apache.commons.io.comparator.CompositeFileComparator", "org.apache.commons.io.comparator.DefaultFileComparator", - "org.apache.commons.io.filefilter.NameFileFilter", - "org.apache.commons.io.output.StringBuilderWriter", - "org.apache.commons.io.filefilter.CanExecuteFileFilter", - "org.apache.commons.io.output.TaggedWriter", - "org.apache.commons.io.serialization.FullClassNameMatcher", + "org.apache.commons.io.comparator.DirectoryFileComparator", + "org.apache.commons.io.comparator.ExtensionFileComparator", + "org.apache.commons.io.comparator.LastModifiedFileComparator", + "org.apache.commons.io.comparator.NameFileComparator", + "org.apache.commons.io.comparator.PathFileComparator", + "org.apache.commons.io.comparator.ReverseFileComparator", + "org.apache.commons.io.comparator.SizeFileComparator", + "org.apache.commons.io.file.AccumulatorPathVisitor", + "org.apache.commons.io.file.CleaningPathVisitor", + "org.apache.commons.io.file.CopyDirectoryVisitor", + "org.apache.commons.io.file.Counters", "org.apache.commons.io.file.Counters$1", + "org.apache.commons.io.file.Counters$AbstractPathCounters", + "org.apache.commons.io.file.Counters$BigIntegerCounter", + "org.apache.commons.io.file.Counters$BigIntegerPathCounters", + "org.apache.commons.io.file.Counters$Counter", + "org.apache.commons.io.file.Counters$LongCounter", + "org.apache.commons.io.file.Counters$LongPathCounters", + "org.apache.commons.io.file.Counters$PathCounters", + "org.apache.commons.io.file.CountingPathVisitor", "org.apache.commons.io.file.DeleteOption", - "org.apache.commons.io.output.TeeOutputStream", - "org.apache.commons.io.file.AccumulatorPathVisitor", - "org.apache.commons.io.Charsets", - "org.apache.commons.io.output.XmlStreamWriter", - "org.apache.commons.io.output.CountingOutputStream", + "org.apache.commons.io.file.DeletingPathVisitor", + "org.apache.commons.io.file.PathUtils", "org.apache.commons.io.file.PathUtils$1", - "org.apache.commons.io.input.InfiniteCircularInputStream", - "org.apache.commons.io.monitor.FileAlterationListener", - "org.apache.commons.io.output.DeferredFileOutputStream", - "org.apache.commons.io.comparator.CompositeFileComparator", - "org.apache.commons.io.output.ChunkedWriter", - "org.apache.commons.io.FileUtils", - "org.apache.commons.io.output.BrokenOutputStream", - "org.apache.commons.io.input.SwappedDataInputStream", - "org.apache.commons.io.output.TaggedOutputStream", - "org.apache.commons.io.comparator.SizeFileComparator", "org.apache.commons.io.file.PathUtils$RelativeSortedPaths", - "org.apache.commons.io.input.RandomAccessFileInputStream", - "org.apache.commons.io.input.NullReader", - "org.apache.commons.io.filefilter.WildcardFilter", - "org.apache.commons.io.output.NullOutputStream", - "org.apache.commons.io.output.LockableFileWriter", - "org.apache.commons.io.filefilter.WildcardFileFilter", - "org.apache.commons.io.comparator.DirectoryFileComparator", - "org.apache.commons.io.output.WriterOutputStream", + "org.apache.commons.io.file.SimplePathVisitor", + "org.apache.commons.io.file.StandardDeleteOption", + "org.apache.commons.io.filefilter.AbstractFileFilter", + "org.apache.commons.io.filefilter.AgeFileFilter", + "org.apache.commons.io.filefilter.AndFileFilter", + "org.apache.commons.io.filefilter.CanExecuteFileFilter", + "org.apache.commons.io.filefilter.CanReadFileFilter", + "org.apache.commons.io.filefilter.CanWriteFileFilter", + "org.apache.commons.io.filefilter.ConditionalFileFilter", + "org.apache.commons.io.filefilter.DelegateFileFilter", + "org.apache.commons.io.filefilter.DirectoryFileFilter", + "org.apache.commons.io.filefilter.EmptyFileFilter", + "org.apache.commons.io.filefilter.FalseFileFilter", + "org.apache.commons.io.filefilter.FileFileFilter", + "org.apache.commons.io.filefilter.FileFilterUtils", + "org.apache.commons.io.filefilter.HiddenFileFilter", "org.apache.commons.io.filefilter.IOFileFilter", - "org.apache.commons.io.input.CloseShieldReader", "org.apache.commons.io.filefilter.MagicNumberFileFilter", - "org.apache.commons.io.input.BoundedInputStream", - "org.apache.commons.io.input.TaggedReader", - "org.apache.commons.io.output.NullWriter", - "org.apache.commons.io.monitor.FileAlterationObserver", - "org.apache.commons.io.input.CharacterSetFilterReader", - "org.apache.commons.io.file.Counters$BigIntegerPathCounters", - "org.apache.commons.io.filefilter.FileFilterUtils", + "org.apache.commons.io.filefilter.NameFileFilter", + "org.apache.commons.io.filefilter.NotFileFilter", + "org.apache.commons.io.filefilter.OrFileFilter", + "org.apache.commons.io.filefilter.PrefixFileFilter", + "org.apache.commons.io.filefilter.RegexFileFilter", + "org.apache.commons.io.filefilter.SizeFileFilter", + "org.apache.commons.io.filefilter.SuffixFileFilter", + "org.apache.commons.io.filefilter.TrueFileFilter", + "org.apache.commons.io.filefilter.WildcardFileFilter", + "org.apache.commons.io.filefilter.WildcardFilter", + "org.apache.commons.io.function.IOConsumer", + "org.apache.commons.io.function.IOFunction", + "org.apache.commons.io.function.IOSupplier", "org.apache.commons.io.input.AbstractCharacterFilterReader", - "org.apache.commons.io.input.CharacterFilterReader", - "org.apache.commons.io.file.Counters$AbstractPathCounters", - "org.apache.commons.io.output.CloseShieldWriter", + "org.apache.commons.io.input.AutoCloseInputStream", "org.apache.commons.io.input.BOMInputStream", - "org.apache.commons.io.file.Counters$BigIntegerCounter", - "org.apache.commons.io.input.TaggedInputStream", - "org.apache.commons.io.output.AbstractByteArrayOutputStream", - "org.apache.commons.io.input.ReversedLinesFileReader$1", - "org.apache.commons.io.IOIndexedException", - "org.apache.commons.io.filefilter.AbstractFileFilter", - "org.apache.commons.io.ByteOrderMark", - "org.apache.commons.io.comparator.ReverseFileComparator", - "org.apache.commons.io.filefilter.FileFileFilter", - "org.apache.commons.io.input.DemuxInputStream", - "org.apache.commons.io.output.ProxyOutputStream", - "org.apache.commons.io.function.IOSupplier", - "org.apache.commons.io.filefilter.HiddenFileFilter", - "org.apache.commons.io.file.CountingPathVisitor", - "org.apache.commons.io.input.buffer.CircularByteBuffer", - "org.apache.commons.io.FileExistsException", + "org.apache.commons.io.input.BoundedInputStream", + "org.apache.commons.io.input.BoundedReader", "org.apache.commons.io.input.BrokenInputStream", - "org.apache.commons.io.IOExceptionList", - "org.apache.commons.io.filefilter.FalseFileFilter", - "org.apache.commons.io.function.IOConsumer", + "org.apache.commons.io.input.BrokenReader", + "org.apache.commons.io.input.CharSequenceInputStream", + "org.apache.commons.io.input.CharSequenceReader", + "org.apache.commons.io.input.CharacterFilterReader", + "org.apache.commons.io.input.CharacterSetFilterReader", + "org.apache.commons.io.input.CircularInputStream", + "org.apache.commons.io.input.ClassLoaderObjectInputStream", + "org.apache.commons.io.input.CloseShieldInputStream", + "org.apache.commons.io.input.CloseShieldReader", + "org.apache.commons.io.input.ClosedInputStream", + "org.apache.commons.io.input.ClosedReader", + "org.apache.commons.io.input.CountingInputStream", + "org.apache.commons.io.input.DemuxInputStream", + "org.apache.commons.io.input.InfiniteCircularInputStream", + "org.apache.commons.io.input.MarkShieldInputStream", + "org.apache.commons.io.input.MessageDigestCalculatingInputStream", + "org.apache.commons.io.input.MessageDigestCalculatingInputStream$MessageDigestMaintainingObserver", + "org.apache.commons.io.input.NullInputStream", + "org.apache.commons.io.input.NullReader", + "org.apache.commons.io.input.ObservableInputStream", + "org.apache.commons.io.input.ObservableInputStream$Observer", + "org.apache.commons.io.input.ProxyInputStream", + "org.apache.commons.io.input.ProxyReader", + "org.apache.commons.io.input.RandomAccessFileInputStream", + "org.apache.commons.io.input.ReaderInputStream", + "org.apache.commons.io.input.ReversedLinesFileReader", + "org.apache.commons.io.input.ReversedLinesFileReader$1", + "org.apache.commons.io.input.ReversedLinesFileReader$FilePart", + "org.apache.commons.io.input.SequenceReader", + "org.apache.commons.io.input.SwappedDataInputStream", + "org.apache.commons.io.input.TaggedInputStream", + "org.apache.commons.io.input.TaggedReader", + "org.apache.commons.io.input.Tailer", "org.apache.commons.io.input.TailerListener", - "org.apache.commons.io.file.DeletingPathVisitor", - "org.apache.commons.io.filefilter.CanReadFileFilter", - "org.apache.commons.io.output.DemuxOutputStream", - "org.apache.commons.io.FilenameUtils", - "org.apache.commons.io.DirectoryWalker$CancelException", + "org.apache.commons.io.input.TailerListenerAdapter", + "org.apache.commons.io.input.TeeInputStream", + "org.apache.commons.io.input.TeeReader", + "org.apache.commons.io.input.UnixLineEndingInputStream", + "org.apache.commons.io.input.UnsynchronizedByteArrayInputStream", + "org.apache.commons.io.input.WindowsLineEndingInputStream", + "org.apache.commons.io.input.XmlStreamReader", + "org.apache.commons.io.input.XmlStreamReaderException", + "org.apache.commons.io.input.buffer.CircularBufferInputStream", + "org.apache.commons.io.input.buffer.CircularByteBuffer", + "org.apache.commons.io.input.buffer.PeekableInputStream", + "org.apache.commons.io.monitor.FileAlterationListener", + "org.apache.commons.io.monitor.FileAlterationListenerAdaptor", + "org.apache.commons.io.monitor.FileAlterationMonitor", + "org.apache.commons.io.monitor.FileAlterationObserver", "org.apache.commons.io.monitor.FileEntry", - "org.apache.commons.io.IOCase", - "org.apache.commons.io.FileDeleteStrategy", - "org.apache.commons.io.file.CleaningPathVisitor", - "org.apache.commons.io.FileSystemUtils", + "org.apache.commons.io.output.AbstractByteArrayOutputStream", + "org.apache.commons.io.output.AbstractByteArrayOutputStream$InputStreamConstructor", "org.apache.commons.io.output.AppendableOutputStream", - "org.apache.commons.io.function.IOFunction", + "org.apache.commons.io.output.AppendableWriter", + "org.apache.commons.io.output.BrokenOutputStream", "org.apache.commons.io.output.BrokenWriter", - "org.apache.commons.io.comparator.PathFileComparator", - "org.apache.commons.io.filefilter.SizeFileFilter", - "org.apache.commons.io.input.ReversedLinesFileReader", - "org.apache.commons.io.output.TeeWriter", - "org.apache.commons.io.monitor.FileAlterationListenerAdaptor", - "org.apache.commons.io.filefilter.OrFileFilter", - "org.apache.commons.io.input.TeeReader", - "org.apache.commons.io.input.MessageDigestCalculatingInputStream$MessageDigestMaintainingObserver", - "org.apache.commons.io.input.TailerListenerAdapter", - "org.apache.commons.io.input.MarkShieldInputStream", - "org.apache.commons.io.filefilter.RegexFileFilter", - "org.apache.commons.io.file.Counters", + "org.apache.commons.io.output.ByteArrayOutputStream", + "org.apache.commons.io.output.ChunkedOutputStream", + "org.apache.commons.io.output.ChunkedWriter", "org.apache.commons.io.output.CloseShieldOutputStream", + "org.apache.commons.io.output.CloseShieldWriter", "org.apache.commons.io.output.ClosedOutputStream", "org.apache.commons.io.output.ClosedWriter", - "org.apache.commons.io.input.ClosedInputStream", - "org.apache.commons.io.input.CountingInputStream", - "org.apache.commons.io.input.UnsynchronizedByteArrayInputStream", - "org.apache.commons.io.ThreadMonitor", - "org.apache.commons.io.monitor.FileAlterationMonitor", - "org.apache.commons.io.FileCleaner", - "org.apache.commons.io.serialization.WildcardClassNameMatcher", - "org.apache.commons.io.input.CharSequenceInputStream", - "org.apache.commons.io.filefilter.PrefixFileFilter", - "org.apache.commons.io.comparator.NameFileComparator", - "org.apache.commons.io.FileCleaningTracker$Reaper", - "org.apache.commons.io.file.Counters$PathCounters", + "org.apache.commons.io.output.CountingOutputStream", + "org.apache.commons.io.output.DeferredFileOutputStream", + "org.apache.commons.io.output.DemuxOutputStream", + "org.apache.commons.io.output.FileWriterWithEncoding", + "org.apache.commons.io.output.FilterCollectionWriter", + "org.apache.commons.io.output.LockableFileWriter", "org.apache.commons.io.output.NullAppendable", - "org.apache.commons.io.FileDeleteStrategy$ForceFileDeleteStrategy", - "org.apache.commons.io.input.AutoCloseInputStream", - "org.apache.commons.io.file.PathUtils", - "org.apache.commons.io.input.ProxyInputStream", + "org.apache.commons.io.output.NullOutputStream", + "org.apache.commons.io.output.NullPrintStream", + "org.apache.commons.io.output.NullWriter", + "org.apache.commons.io.output.ProxyCollectionWriter", + "org.apache.commons.io.output.ProxyOutputStream", + "org.apache.commons.io.output.ProxyWriter", + "org.apache.commons.io.output.StringBuilderWriter", + "org.apache.commons.io.output.TaggedOutputStream", + "org.apache.commons.io.output.TaggedWriter", + "org.apache.commons.io.output.TeeOutputStream", + "org.apache.commons.io.output.TeeWriter", "org.apache.commons.io.output.ThresholdingOutputStream", - "org.apache.commons.io.input.ClassLoaderObjectInputStream", - "org.apache.commons.io.input.ClosedReader", - "org.apache.commons.io.input.SequenceReader", - "org.apache.commons.io.filefilter.CanWriteFileFilter", - "org.apache.commons.io.input.CircularInputStream" + "org.apache.commons.io.output.UnsynchronizedByteArrayOutputStream", + "org.apache.commons.io.output.WriterOutputStream", + "org.apache.commons.io.output.XmlStreamWriter", + "org.apache.commons.io.serialization.ClassNameMatcher", + "org.apache.commons.io.serialization.FullClassNameMatcher", + "org.apache.commons.io.serialization.RegexpClassNameMatcher", + "org.apache.commons.io.serialization.ValidatingObjectInputStream", + "org.apache.commons.io.serialization.WildcardClassNameMatcher" ], "usedTypes": [], "usageRatio": 0.0, @@ -257,112 +257,112 @@ "status": "used", "parent": "org.foo.bar:foobar:jar:1.0.0-SNAPSHOT", "allTypes": [ + "org.apache.commons.codec.BinaryDecoder", + "org.apache.commons.codec.BinaryEncoder", + "org.apache.commons.codec.CharEncoding", + "org.apache.commons.codec.Charsets", + "org.apache.commons.codec.CodecPolicy", + "org.apache.commons.codec.Decoder", + "org.apache.commons.codec.DecoderException", + "org.apache.commons.codec.Encoder", "org.apache.commons.codec.EncoderException", - "org.apache.commons.codec.language.DoubleMetaphone$DoubleMetaphoneResult", - "org.apache.commons.codec.language.ColognePhonetic$CologneBuffer", - "org.apache.commons.codec.language.SoundexUtils", - "org.apache.commons.codec.net.Utils", - "org.apache.commons.codec.digest.MurmurHash2", - "org.apache.commons.codec.digest.MurmurHash3", + "org.apache.commons.codec.Resources", + "org.apache.commons.codec.StringDecoder", + "org.apache.commons.codec.StringEncoder", + "org.apache.commons.codec.StringEncoderComparator", + "org.apache.commons.codec.binary.Base16", + "org.apache.commons.codec.binary.Base16InputStream", + "org.apache.commons.codec.binary.Base16OutputStream", + "org.apache.commons.codec.binary.Base32", "org.apache.commons.codec.binary.Base32InputStream", - "org.apache.commons.codec.language.ColognePhonetic$CologneOutputBuffer", + "org.apache.commons.codec.binary.Base32OutputStream", "org.apache.commons.codec.binary.Base64", - "org.apache.commons.codec.language.Caverphone1", - "org.apache.commons.codec.language.Caverphone2", - "org.apache.commons.codec.language.bm.Languages$2", - "org.apache.commons.codec.language.Caverphone", - "org.apache.commons.codec.digest.MurmurHash3$IncrementalHash32", - "org.apache.commons.codec.Charsets", - "org.apache.commons.codec.language.bm.Languages$1", + "org.apache.commons.codec.binary.Base64InputStream", + "org.apache.commons.codec.binary.Base64OutputStream", "org.apache.commons.codec.binary.BaseNCodec", - "org.apache.commons.codec.digest.HmacUtils", - "org.apache.commons.codec.language.bm.PhoneticEngine$RulesApplication", - "org.apache.commons.codec.DecoderException", - "org.apache.commons.codec.digest.Crypt", + "org.apache.commons.codec.binary.BaseNCodec$Context", + "org.apache.commons.codec.binary.BaseNCodecInputStream", + "org.apache.commons.codec.binary.BaseNCodecOutputStream", "org.apache.commons.codec.binary.BinaryCodec", - "org.apache.commons.codec.language.bm.Rule$PhonemeExpr", - "org.apache.commons.codec.net.QuotedPrintableCodec", + "org.apache.commons.codec.binary.CharSequenceUtils", + "org.apache.commons.codec.binary.Hex", + "org.apache.commons.codec.binary.StringUtils", "org.apache.commons.codec.cli.Digest", - "org.apache.commons.codec.language.DaitchMokotoffSoundex$1", - "org.apache.commons.codec.digest.Sha2Crypt", - "org.apache.commons.codec.net.RFC1522Codec", + "org.apache.commons.codec.digest.B64", + "org.apache.commons.codec.digest.Crypt", + "org.apache.commons.codec.digest.DigestUtils", + "org.apache.commons.codec.digest.HmacAlgorithms", + "org.apache.commons.codec.digest.HmacUtils", "org.apache.commons.codec.digest.Md5Crypt", - "org.apache.commons.codec.language.DaitchMokotoffSoundex$Rule", - "org.apache.commons.codec.language.Nysiis", - "org.apache.commons.codec.net.QCodec", - "org.apache.commons.codec.CodecPolicy", - "org.apache.commons.codec.language.bm.Languages$LanguageSet", - "org.apache.commons.codec.language.bm.PhoneticEngine$PhonemeBuilder", - "org.apache.commons.codec.language.bm.Languages", + "org.apache.commons.codec.digest.MessageDigestAlgorithms", + "org.apache.commons.codec.digest.MurmurHash2", + "org.apache.commons.codec.digest.MurmurHash3", + "org.apache.commons.codec.digest.MurmurHash3$IncrementalHash32", + "org.apache.commons.codec.digest.MurmurHash3$IncrementalHash32x86", + "org.apache.commons.codec.digest.PureJavaCrc32", "org.apache.commons.codec.digest.PureJavaCrc32C", - "org.apache.commons.codec.language.Metaphone", + "org.apache.commons.codec.digest.Sha2Crypt", + "org.apache.commons.codec.digest.UnixCrypt", + "org.apache.commons.codec.digest.XXHash32", + "org.apache.commons.codec.language.AbstractCaverphone", + "org.apache.commons.codec.language.Caverphone", + "org.apache.commons.codec.language.Caverphone1", + "org.apache.commons.codec.language.Caverphone2", + "org.apache.commons.codec.language.ColognePhonetic", + "org.apache.commons.codec.language.ColognePhonetic$CologneBuffer", + "org.apache.commons.codec.language.ColognePhonetic$CologneInputBuffer", + "org.apache.commons.codec.language.ColognePhonetic$CologneOutputBuffer", + "org.apache.commons.codec.language.DaitchMokotoffSoundex", + "org.apache.commons.codec.language.DaitchMokotoffSoundex$1", "org.apache.commons.codec.language.DaitchMokotoffSoundex$Branch", - "org.apache.commons.codec.digest.HmacAlgorithms", + "org.apache.commons.codec.language.DaitchMokotoffSoundex$Rule", + "org.apache.commons.codec.language.DoubleMetaphone", + "org.apache.commons.codec.language.DoubleMetaphone$DoubleMetaphoneResult", + "org.apache.commons.codec.language.MatchRatingApproachEncoder", + "org.apache.commons.codec.language.Metaphone", + "org.apache.commons.codec.language.Nysiis", + "org.apache.commons.codec.language.RefinedSoundex", "org.apache.commons.codec.language.Soundex", - "org.apache.commons.codec.digest.PureJavaCrc32", - "org.apache.commons.codec.language.bm.Rule$RPattern", + "org.apache.commons.codec.language.SoundexUtils", "org.apache.commons.codec.language.bm.BeiderMorseEncoder", - "org.apache.commons.codec.language.ColognePhonetic$CologneInputBuffer", - "org.apache.commons.codec.language.DoubleMetaphone", - "org.apache.commons.codec.binary.Base16OutputStream", - "org.apache.commons.codec.language.bm.Rule$PhonemeList", - "org.apache.commons.codec.language.bm.Rule", - "org.apache.commons.codec.StringEncoderComparator", - "org.apache.commons.codec.language.bm.PhoneticEngine", - "org.apache.commons.codec.language.bm.PhoneticEngine$1", - "org.apache.commons.codec.digest.UnixCrypt", - "org.apache.commons.codec.binary.BaseNCodecOutputStream", - "org.apache.commons.codec.language.bm.Rule$Phoneme", - "org.apache.commons.codec.language.bm.ResourceConstants", "org.apache.commons.codec.language.bm.Lang", - "org.apache.commons.codec.net.BCodec", - "org.apache.commons.codec.binary.Base32OutputStream", - "org.apache.commons.codec.Encoder", + "org.apache.commons.codec.language.bm.Lang$1", "org.apache.commons.codec.language.bm.Lang$LangRule", - "org.apache.commons.codec.language.AbstractCaverphone", - "org.apache.commons.codec.BinaryDecoder", - "org.apache.commons.codec.binary.Base32", - "org.apache.commons.codec.binary.BaseNCodec$Context", - "org.apache.commons.codec.binary.Base16InputStream", - "org.apache.commons.codec.language.DaitchMokotoffSoundex", - "org.apache.commons.codec.net.PercentCodec", - "org.apache.commons.codec.binary.Base64InputStream", + "org.apache.commons.codec.language.bm.Languages", + "org.apache.commons.codec.language.bm.Languages$1", + "org.apache.commons.codec.language.bm.Languages$2", + "org.apache.commons.codec.language.bm.Languages$LanguageSet", + "org.apache.commons.codec.language.bm.Languages$SomeLanguages", "org.apache.commons.codec.language.bm.NameType", - "org.apache.commons.codec.BinaryEncoder", - "org.apache.commons.codec.Resources", - "org.apache.commons.codec.binary.Hex", - "org.apache.commons.codec.binary.CharSequenceUtils", - "org.apache.commons.codec.StringDecoder", + "org.apache.commons.codec.language.bm.PhoneticEngine", + "org.apache.commons.codec.language.bm.PhoneticEngine$1", + "org.apache.commons.codec.language.bm.PhoneticEngine$PhonemeBuilder", + "org.apache.commons.codec.language.bm.PhoneticEngine$RulesApplication", + "org.apache.commons.codec.language.bm.ResourceConstants", + "org.apache.commons.codec.language.bm.Rule", + "org.apache.commons.codec.language.bm.Rule$1", "org.apache.commons.codec.language.bm.Rule$10", - "org.apache.commons.codec.Decoder", - "org.apache.commons.codec.digest.XXHash32", - "org.apache.commons.codec.language.bm.Lang$1", - "org.apache.commons.codec.binary.Base64OutputStream", - "org.apache.commons.codec.language.RefinedSoundex", - "org.apache.commons.codec.language.MatchRatingApproachEncoder", - "org.apache.commons.codec.language.bm.Rule$Phoneme$1", - "org.apache.commons.codec.StringEncoder", - "org.apache.commons.codec.binary.Base16", - "org.apache.commons.codec.language.bm.Rule$8", - "org.apache.commons.codec.digest.MurmurHash3$IncrementalHash32x86", - "org.apache.commons.codec.language.bm.Rule$7", - "org.apache.commons.codec.language.bm.Rule$9", - "org.apache.commons.codec.language.bm.Rule$4", - "org.apache.commons.codec.digest.B64", + "org.apache.commons.codec.language.bm.Rule$2", "org.apache.commons.codec.language.bm.Rule$3", - "org.apache.commons.codec.binary.BaseNCodecInputStream", - "org.apache.commons.codec.digest.MessageDigestAlgorithms", - "org.apache.commons.codec.language.bm.Rule$6", - "org.apache.commons.codec.CharEncoding", + "org.apache.commons.codec.language.bm.Rule$4", "org.apache.commons.codec.language.bm.Rule$5", - "org.apache.commons.codec.language.bm.Rule$2", - "org.apache.commons.codec.net.URLCodec", - "org.apache.commons.codec.language.bm.Rule$1", - "org.apache.commons.codec.binary.StringUtils", - "org.apache.commons.codec.digest.DigestUtils", - "org.apache.commons.codec.language.ColognePhonetic", + "org.apache.commons.codec.language.bm.Rule$6", + "org.apache.commons.codec.language.bm.Rule$7", + "org.apache.commons.codec.language.bm.Rule$8", + "org.apache.commons.codec.language.bm.Rule$9", + "org.apache.commons.codec.language.bm.Rule$Phoneme", + "org.apache.commons.codec.language.bm.Rule$Phoneme$1", + "org.apache.commons.codec.language.bm.Rule$PhonemeExpr", + "org.apache.commons.codec.language.bm.Rule$PhonemeList", + "org.apache.commons.codec.language.bm.Rule$RPattern", "org.apache.commons.codec.language.bm.RuleType", - "org.apache.commons.codec.language.bm.Languages$SomeLanguages" + "org.apache.commons.codec.net.BCodec", + "org.apache.commons.codec.net.PercentCodec", + "org.apache.commons.codec.net.QCodec", + "org.apache.commons.codec.net.QuotedPrintableCodec", + "org.apache.commons.codec.net.RFC1522Codec", + "org.apache.commons.codec.net.URLCodec", + "org.apache.commons.codec.net.Utils" ], "usedTypes": [ "org.apache.commons.codec.Charsets" diff --git a/depclean-maven-plugin/src/test/resources/DepCleanMojoResources/pom-debloated.xml b/depclean-maven-plugin/src/test/resources/DepCleanMojoResources/pom-debloated.xml index 775c20a1..db33fc1a 100644 --- a/depclean-maven-plugin/src/test/resources/DepCleanMojoResources/pom-debloated.xml +++ b/depclean-maven-plugin/src/test/resources/DepCleanMojoResources/pom-debloated.xml @@ -14,12 +14,6 @@ UTF-8
- - org.mapstruct - mapstruct-processor - ${mapstruct.version} - provided - com.fasterxml.jackson.core jackson-core @@ -27,6 +21,13 @@ compile false + + org.mapstruct + mapstruct-processor + ${mapstruct.version} + provided + false + diff --git a/depclean-maven-plugin/src/test/resources/MavenInvokerResources/basic_spring_maven_project/tree_expected.txt b/depclean-maven-plugin/src/test/resources/MavenInvokerResources/basic_spring_maven_project/tree_expected.txt index 98aad83f..44867c28 100644 --- a/depclean-maven-plugin/src/test/resources/MavenInvokerResources/basic_spring_maven_project/tree_expected.txt +++ b/depclean-maven-plugin/src/test/resources/MavenInvokerResources/basic_spring_maven_project/tree_expected.txt @@ -3,161 +3,77 @@ se.kth.castor:depclean-maven-plugin:maven-plugin:2.0.2-SNAPSHOT | +- org.ow2.asm:asm:jar:9.1:compile | +- org.codehaus.plexus:plexus-component-annotations:jar:2.0.0:compile | +- org.codehaus.plexus:plexus-utils:jar:3.3.0:compile -| +- (org.apache.maven:maven-core:jar:3.6.2:compile - omitted for conflict with 3.6.0) | +- org.jgrapht:jgrapht-core:jar:1.3.0:compile | | \- org.jheaps:jheaps:jar:0.9:compile | +- org.jetbrains:annotations:jar:17.0.0:compile -| +- (org.slf4j:slf4j-api:jar:2.0.0-alpha1:compile - omitted for duplicate) -| \- (org.slf4j:slf4j-log4j12:jar:1.8.0-beta4:compile - omitted for duplicate) +| \- com.google.guava:guava:jar:30.1-jre:compile +| +- com.google.guava:failureaccess:jar:1.0.1:compile +| +- com.google.guava:listenablefuture:jar:9999.0-empty-to-avoid-conflict-with-guava:compile +| +- com.google.code.findbugs:jsr305:jar:3.0.2:compile +| +- org.checkerframework:checker-qual:jar:3.5.0:compile +| +- com.google.errorprone:error_prone_annotations:jar:2.3.4:compile +| \- com.google.j2objc:j2objc-annotations:jar:1.3:compile +- org.apache.maven:maven-core:jar:3.6.0:compile | +- org.apache.maven:maven-model:jar:3.6.0:compile -| | \- (org.codehaus.plexus:plexus-utils:jar:3.1.0:compile - omitted for conflict with 3.3.0) | +- org.apache.maven:maven-settings:jar:3.6.0:compile -| | \- (org.codehaus.plexus:plexus-utils:jar:3.1.0:compile - omitted for conflict with 3.3.0) | +- org.apache.maven:maven-settings-builder:jar:3.6.0:compile -| | +- (org.apache.maven:maven-builder-support:jar:3.6.0:compile - omitted for duplicate) -| | +- (org.codehaus.plexus:plexus-utils:jar:3.1.0:compile - omitted for conflict with 3.3.0) -| | +- (org.codehaus.plexus:plexus-interpolation:jar:1.25:compile - omitted for conflict with 1.1) -| | +- (org.codehaus.plexus:plexus-component-annotations:jar:1.7.1:compile - omitted for conflict with 2.0.0) -| | +- (org.apache.maven:maven-settings:jar:3.6.0:compile - omitted for duplicate) | | \- org.sonatype.plexus:plexus-sec-dispatcher:jar:1.4:compile -| | +- (org.codehaus.plexus:plexus-utils:jar:1.5.5:compile - omitted for conflict with 3.3.0) | | \- org.sonatype.plexus:plexus-cipher:jar:1.4:compile | +- org.apache.maven:maven-builder-support:jar:3.6.0:compile | +- org.apache.maven:maven-repository-metadata:jar:3.6.0:compile -| | \- (org.codehaus.plexus:plexus-utils:jar:3.1.0:compile - omitted for conflict with 3.3.0) | +- org.apache.maven:maven-artifact:jar:3.6.0:compile -| | +- (org.codehaus.plexus:plexus-utils:jar:3.1.0:compile - omitted for conflict with 3.3.0) -| | \- (org.apache.commons:commons-lang3:jar:3.8.1:compile - omitted for duplicate) -| +- (org.apache.maven:maven-plugin-api:jar:3.6.0:compile - omitted for duplicate) | +- org.apache.maven:maven-model-builder:jar:3.6.0:compile -| | +- (org.codehaus.plexus:plexus-utils:jar:3.1.0:compile - omitted for conflict with 3.3.0) -| | +- (org.codehaus.plexus:plexus-interpolation:jar:1.25:compile - omitted for duplicate) -| | +- (org.codehaus.plexus:plexus-component-annotations:jar:1.7.1:compile - omitted for conflict with 2.0.0) -| | +- (org.apache.maven:maven-model:jar:3.6.0:compile - omitted for duplicate) -| | +- (org.apache.maven:maven-artifact:jar:3.6.0:compile - omitted for duplicate) -| | \- (org.apache.maven:maven-builder-support:jar:3.6.0:compile - omitted for duplicate) | +- org.apache.maven:maven-resolver-provider:jar:3.6.0:compile -| | +- (org.apache.maven:maven-model:jar:3.6.0:compile - omitted for duplicate) -| | +- (org.apache.maven:maven-model-builder:jar:3.6.0:compile - omitted for duplicate) -| | +- (org.apache.maven:maven-repository-metadata:jar:3.6.0:compile - omitted for duplicate) -| | +- (org.apache.maven.resolver:maven-resolver-api:jar:1.3.1:compile - omitted for duplicate) -| | +- (org.apache.maven.resolver:maven-resolver-spi:jar:1.3.1:compile - omitted for duplicate) -| | +- (org.apache.maven.resolver:maven-resolver-util:jar:1.3.1:compile - omitted for duplicate) -| | +- (org.apache.maven.resolver:maven-resolver-impl:jar:1.3.1:compile - omitted for duplicate) -| | +- (org.codehaus.plexus:plexus-utils:jar:3.1.0:compile - omitted for conflict with 3.3.0) -| | +- (javax.inject:javax.inject:jar:1:compile - omitted for duplicate) -| | \- (org.slf4j:slf4j-api:jar:1.7.25:compile - omitted for conflict with 2.0.0-alpha1) | +- org.apache.maven.resolver:maven-resolver-impl:jar:1.3.1:compile -| | +- (org.apache.maven.resolver:maven-resolver-api:jar:1.3.1:compile - omitted for duplicate) -| | +- (org.apache.maven.resolver:maven-resolver-spi:jar:1.3.1:compile - omitted for duplicate) -| | +- (org.apache.maven.resolver:maven-resolver-util:jar:1.3.1:compile - omitted for duplicate) -| | \- (org.slf4j:slf4j-api:jar:1.7.25:compile - omitted for conflict with 2.0.0-alpha1) | +- org.apache.maven.resolver:maven-resolver-api:jar:1.3.1:compile | +- org.apache.maven.resolver:maven-resolver-spi:jar:1.3.1:compile -| | \- (org.apache.maven.resolver:maven-resolver-api:jar:1.3.1:compile - omitted for duplicate) | +- org.apache.maven.resolver:maven-resolver-util:jar:1.3.1:compile -| | \- (org.apache.maven.resolver:maven-resolver-api:jar:1.3.1:compile - omitted for duplicate) | +- org.apache.maven.shared:maven-shared-utils:jar:3.2.1:compile -| | \- (commons-io:commons-io:jar:2.5:compile - omitted for duplicate) | +- org.eclipse.sisu:org.eclipse.sisu.plexus:jar:0.3.3:compile -| | +- javax.enterprise:cdi-api:jar:1.0:compile -| | | +- javax.annotation:jsr250-api:jar:1.0:compile -| | | \- (javax.inject:javax.inject:jar:1:compile - omitted for duplicate) -| | +- (org.eclipse.sisu:org.eclipse.sisu.inject:jar:0.3.3:compile - omitted for duplicate) -| | +- (org.codehaus.plexus:plexus-component-annotations:jar:1.5.5:compile - omitted for conflict with 2.0.0) -| | +- (org.codehaus.plexus:plexus-classworlds:jar:2.5.2:compile - omitted for duplicate) -| | \- (org.codehaus.plexus:plexus-utils:jar:3.0.17:compile - omitted for conflict with 3.3.0) +| | \- javax.enterprise:cdi-api:jar:1.0:compile +| | \- javax.annotation:jsr250-api:jar:1.0:compile | +- org.eclipse.sisu:org.eclipse.sisu.inject:jar:0.3.3:compile | +- com.google.inject:guice:jar:no_aop:4.2.1:compile -| | +- (javax.inject:javax.inject:jar:1:compile - omitted for duplicate) -| | +- aopalliance:aopalliance:jar:1.0:compile -| | \- com.google.guava:guava:jar:25.1-android:compile -| | +- com.google.code.findbugs:jsr305:jar:3.0.2:compile -| | +- org.checkerframework:checker-compat-qual:jar:2.0.0:compile -| | +- com.google.errorprone:error_prone_annotations:jar:2.1.3:compile -| | +- com.google.j2objc:j2objc-annotations:jar:1.1:compile -| | \- org.codehaus.mojo:animal-sniffer-annotations:jar:1.14:compile +| | \- aopalliance:aopalliance:jar:1.0:compile | +- javax.inject:javax.inject:jar:1:compile -| +- (org.codehaus.plexus:plexus-utils:jar:3.1.0:compile - omitted for conflict with 3.3.0) | +- org.codehaus.plexus:plexus-classworlds:jar:2.5.2:compile -| +- (org.codehaus.plexus:plexus-component-annotations:jar:1.7.1:compile - omitted for conflict with 2.0.0) | \- org.apache.commons:commons-lang3:jar:3.8.1:compile +- org.apache.maven:maven-plugin-api:jar:3.6.0:compile -| +- (org.apache.maven:maven-model:jar:3.6.0:compile - omitted for duplicate) -| +- (org.apache.maven:maven-artifact:jar:3.6.0:compile - omitted for duplicate) -| +- (org.eclipse.sisu:org.eclipse.sisu.plexus:jar:0.3.3:compile - omitted for duplicate) -| +- (org.codehaus.plexus:plexus-utils:jar:3.1.0:compile - omitted for conflict with 3.3.0) -| \- (org.codehaus.plexus:plexus-classworlds:jar:2.5.2:compile - omitted for duplicate) +- org.apache.maven:maven-project:jar:3.0-alpha-2:compile -| +- (org.apache.maven:maven-model:jar:3.0-alpha-2:compile - omitted for conflict with 3.6.0) -| +- (org.codehaus.plexus:plexus-utils:jar:1.5.6:compile - omitted for conflict with 3.3.0) | +- org.codehaus.plexus:plexus-interpolation:jar:1.1:compile | +- org.apache.maven:maven-compat:jar:3.0-alpha-2:compile -| | +- (org.apache.maven:maven-model:jar:3.0-alpha-2:compile - omitted for conflict with 3.6.0) -| | +- (org.codehaus.plexus:plexus-container-default:jar:1.0-beta-3.0.5:compile - omitted for duplicate) -| | +- (org.codehaus.plexus:plexus-component-annotations:jar:1.0-beta-3.0.5:compile - omitted for conflict with 2.0.0) | | \- org.apache.maven.wagon:wagon-provider-api:jar:1.0-beta-4:compile -| | \- (org.codehaus.plexus:plexus-utils:jar:1.4.2:compile - omitted for conflict with 3.3.0) | +- org.codehaus.plexus:plexus-container-default:jar:1.0-beta-3.0.5:compile -| | +- (org.codehaus.plexus:plexus-utils:jar:1.4.5:compile - omitted for conflict with 3.3.0) -| | +- (org.codehaus.plexus:plexus-classworlds:jar:1.4:compile - omitted for conflict with 2.5.2) | | +- org.apache.xbean:xbean-reflect:jar:3.4:compile -| | | +- (log4j:log4j:jar:1.2.12:compile - omitted for conflict with 1.2.17) | | | \- commons-logging:commons-logging-api:jar:1.1:compile -| | +- com.google.code.google-collections:google-collect:jar:snapshot-20080530:compile -| | \- junit:junit:jar:4.13:compile +| | \- com.google.code.google-collections:google-collect:jar:snapshot-20080530:compile | +- org.codehaus.woodstox:wstx-asl:jar:3.2.6:compile | | \- stax:stax-api:jar:1.0.1:compile | +- org.sonatype.spice:model-builder:jar:1.3:compile -| | +- (org.codehaus.woodstox:wstx-asl:jar:3.2.6:compile - omitted for duplicate) -| | \- (stax:stax-api:jar:1.0.1:compile - omitted for duplicate) | \- org.apache.maven:maven-project-builder:jar:3.0-alpha-2:compile -| \- (org.sonatype.spice:model-builder:jar:1.3:compile - omitted for duplicate) +- org.apache.maven.plugin-tools:maven-plugin-annotations:jar:3.6.0:compile -| \- (org.apache.maven:maven-artifact:jar:3.0:compile - omitted for conflict with 3.6.0) +- org.apache.maven.shared:maven-dependency-tree:jar:3.0.1:compile -| +- (org.codehaus.plexus:plexus-component-annotations:jar:1.6:compile - omitted for conflict with 2.0.0) | \- org.eclipse.aether:aether-util:jar:0.9.0.M2:compile +- commons-io:commons-io:jar:2.5:compile +- com.google.code.gson:gson:jar:2.8.6:compile +- org.whitesource:maven-dependency-tree-parser:jar:1.0.6:compile -| +- commons-lang:commons-lang:jar:2.4:compile -| \- (commons-io:commons-io:jar:2.0.1:compile - omitted for conflict with 2.5) +| \- commons-lang:commons-lang:jar:2.4:compile +- org.apache.maven.plugin-testing:maven-plugin-testing-tools:jar:3.3.0:compile | \- org.apache.maven.shared:maven-invoker:jar:2.0.11:compile -| \- (org.codehaus.plexus:plexus-utils:jar:1.5.6:compile - omitted for conflict with 3.3.0) +- com.soebes.itf.jupiter.extension:itf-extension-maven:jar:0.9.0:test -| +- (org.apache.maven:maven-model:jar:3.1.0:test - omitted for conflict with 3.6.0) | +- org.apiguardian:apiguardian-api:jar:1.1.0:test | \- org.junit.platform:junit-platform-commons:jar:1.6.2:test -| \- (org.apiguardian:apiguardian-api:jar:1.1.0:test - omitted for duplicate) +- com.soebes.itf.jupiter.extension:itf-assertj:jar:0.9.0:test -| +- (com.soebes.itf.jupiter.extension:itf-extension-maven:jar:0.9.0:test - omitted for duplicate) -| \- (org.assertj:assertj-core:jar:3.16.1:test - omitted for conflict with 3.19.0) +- com.soebes.itf.jupiter.extension:itf-jupiter-extension:jar:0.9.0:test -| +- (com.soebes.itf.jupiter.extension:itf-extension-maven:jar:0.9.0:test - omitted for duplicate) -| +- (org.junit.jupiter:junit-jupiter-api:jar:5.6.2:test - omitted for duplicate) -| +- (org.apache.maven:maven-model:jar:3.1.0:test - omitted for conflict with 3.6.0) -| \- (commons-io:commons-io:jar:2.6:test - omitted for conflict with 2.5) +- org.assertj:assertj-core:jar:3.19.0:test -+- org.projectlombok:lombok:jar:1.18.16:provided ++- org.projectlombok:lombok:jar:1.18.22:provided +- org.slf4j:slf4j-api:jar:2.0.0-alpha1:compile +- org.slf4j:slf4j-log4j12:jar:1.8.0-beta4:compile -| +- (org.slf4j:slf4j-api:jar:1.8.0-beta4:compile - omitted for conflict with 2.0.0-alpha1) | \- log4j:log4j:jar:1.2.17:compile +- org.junit.jupiter:junit-jupiter-api:jar:5.6.2:test -| +- (org.apiguardian:apiguardian-api:jar:1.1.0:test - omitted for duplicate) -| +- org.opentest4j:opentest4j:jar:1.2.0:test -| \- (org.junit.platform:junit-platform-commons:jar:1.6.2:test - omitted for duplicate) +| \- org.opentest4j:opentest4j:jar:1.2.0:test +- org.junit.jupiter:junit-jupiter-engine:jar:5.6.2:test -| +- (org.apiguardian:apiguardian-api:jar:1.1.0:test - omitted for duplicate) -| +- org.junit.platform:junit-platform-engine:jar:1.6.2:test -| | +- (org.apiguardian:apiguardian-api:jar:1.1.0:test - omitted for duplicate) -| | +- (org.opentest4j:opentest4j:jar:1.2.0:test - omitted for duplicate) -| | \- (org.junit.platform:junit-platform-commons:jar:1.6.2:test - omitted for duplicate) -| \- (org.junit.jupiter:junit-jupiter-api:jar:5.6.2:test - omitted for duplicate) +| \- org.junit.platform:junit-platform-engine:jar:1.6.2:test \- org.junit.vintage:junit-vintage-engine:jar:5.6.2:test - +- (org.apiguardian:apiguardian-api:jar:1.1.0:test - omitted for duplicate) - +- (org.junit.platform:junit-platform-engine:jar:1.6.2:test - omitted for duplicate) - \- (junit:junit:jar:4.13:compile - scope updated from test; omitted for duplicate) + \- junit:junit:jar:4.13:compile + \- org.hamcrest:hamcrest-core:jar:1.3:compile diff --git a/depclean-maven-plugin/src/test/resources/MavenInvokerResources/basic_spring_maven_project/tree_produced.txt b/depclean-maven-plugin/src/test/resources/MavenInvokerResources/basic_spring_maven_project/tree_produced.txt new file mode 100644 index 00000000..44867c28 --- /dev/null +++ b/depclean-maven-plugin/src/test/resources/MavenInvokerResources/basic_spring_maven_project/tree_produced.txt @@ -0,0 +1,79 @@ +se.kth.castor:depclean-maven-plugin:maven-plugin:2.0.2-SNAPSHOT ++- se.kth.castor:depclean-core:jar:2.0.2-SNAPSHOT:compile +| +- org.ow2.asm:asm:jar:9.1:compile +| +- org.codehaus.plexus:plexus-component-annotations:jar:2.0.0:compile +| +- org.codehaus.plexus:plexus-utils:jar:3.3.0:compile +| +- org.jgrapht:jgrapht-core:jar:1.3.0:compile +| | \- org.jheaps:jheaps:jar:0.9:compile +| +- org.jetbrains:annotations:jar:17.0.0:compile +| \- com.google.guava:guava:jar:30.1-jre:compile +| +- com.google.guava:failureaccess:jar:1.0.1:compile +| +- com.google.guava:listenablefuture:jar:9999.0-empty-to-avoid-conflict-with-guava:compile +| +- com.google.code.findbugs:jsr305:jar:3.0.2:compile +| +- org.checkerframework:checker-qual:jar:3.5.0:compile +| +- com.google.errorprone:error_prone_annotations:jar:2.3.4:compile +| \- com.google.j2objc:j2objc-annotations:jar:1.3:compile ++- org.apache.maven:maven-core:jar:3.6.0:compile +| +- org.apache.maven:maven-model:jar:3.6.0:compile +| +- org.apache.maven:maven-settings:jar:3.6.0:compile +| +- org.apache.maven:maven-settings-builder:jar:3.6.0:compile +| | \- org.sonatype.plexus:plexus-sec-dispatcher:jar:1.4:compile +| | \- org.sonatype.plexus:plexus-cipher:jar:1.4:compile +| +- org.apache.maven:maven-builder-support:jar:3.6.0:compile +| +- org.apache.maven:maven-repository-metadata:jar:3.6.0:compile +| +- org.apache.maven:maven-artifact:jar:3.6.0:compile +| +- org.apache.maven:maven-model-builder:jar:3.6.0:compile +| +- org.apache.maven:maven-resolver-provider:jar:3.6.0:compile +| +- org.apache.maven.resolver:maven-resolver-impl:jar:1.3.1:compile +| +- org.apache.maven.resolver:maven-resolver-api:jar:1.3.1:compile +| +- org.apache.maven.resolver:maven-resolver-spi:jar:1.3.1:compile +| +- org.apache.maven.resolver:maven-resolver-util:jar:1.3.1:compile +| +- org.apache.maven.shared:maven-shared-utils:jar:3.2.1:compile +| +- org.eclipse.sisu:org.eclipse.sisu.plexus:jar:0.3.3:compile +| | \- javax.enterprise:cdi-api:jar:1.0:compile +| | \- javax.annotation:jsr250-api:jar:1.0:compile +| +- org.eclipse.sisu:org.eclipse.sisu.inject:jar:0.3.3:compile +| +- com.google.inject:guice:jar:no_aop:4.2.1:compile +| | \- aopalliance:aopalliance:jar:1.0:compile +| +- javax.inject:javax.inject:jar:1:compile +| +- org.codehaus.plexus:plexus-classworlds:jar:2.5.2:compile +| \- org.apache.commons:commons-lang3:jar:3.8.1:compile ++- org.apache.maven:maven-plugin-api:jar:3.6.0:compile ++- org.apache.maven:maven-project:jar:3.0-alpha-2:compile +| +- org.codehaus.plexus:plexus-interpolation:jar:1.1:compile +| +- org.apache.maven:maven-compat:jar:3.0-alpha-2:compile +| | \- org.apache.maven.wagon:wagon-provider-api:jar:1.0-beta-4:compile +| +- org.codehaus.plexus:plexus-container-default:jar:1.0-beta-3.0.5:compile +| | +- org.apache.xbean:xbean-reflect:jar:3.4:compile +| | | \- commons-logging:commons-logging-api:jar:1.1:compile +| | \- com.google.code.google-collections:google-collect:jar:snapshot-20080530:compile +| +- org.codehaus.woodstox:wstx-asl:jar:3.2.6:compile +| | \- stax:stax-api:jar:1.0.1:compile +| +- org.sonatype.spice:model-builder:jar:1.3:compile +| \- org.apache.maven:maven-project-builder:jar:3.0-alpha-2:compile ++- org.apache.maven.plugin-tools:maven-plugin-annotations:jar:3.6.0:compile ++- org.apache.maven.shared:maven-dependency-tree:jar:3.0.1:compile +| \- org.eclipse.aether:aether-util:jar:0.9.0.M2:compile ++- commons-io:commons-io:jar:2.5:compile ++- com.google.code.gson:gson:jar:2.8.6:compile ++- org.whitesource:maven-dependency-tree-parser:jar:1.0.6:compile +| \- commons-lang:commons-lang:jar:2.4:compile ++- org.apache.maven.plugin-testing:maven-plugin-testing-tools:jar:3.3.0:compile +| \- org.apache.maven.shared:maven-invoker:jar:2.0.11:compile ++- com.soebes.itf.jupiter.extension:itf-extension-maven:jar:0.9.0:test +| +- org.apiguardian:apiguardian-api:jar:1.1.0:test +| \- org.junit.platform:junit-platform-commons:jar:1.6.2:test ++- com.soebes.itf.jupiter.extension:itf-assertj:jar:0.9.0:test ++- com.soebes.itf.jupiter.extension:itf-jupiter-extension:jar:0.9.0:test ++- org.assertj:assertj-core:jar:3.19.0:test ++- org.projectlombok:lombok:jar:1.18.22:provided ++- org.slf4j:slf4j-api:jar:2.0.0-alpha1:compile ++- org.slf4j:slf4j-log4j12:jar:1.8.0-beta4:compile +| \- log4j:log4j:jar:1.2.17:compile ++- org.junit.jupiter:junit-jupiter-api:jar:5.6.2:test +| \- org.opentest4j:opentest4j:jar:1.2.0:test ++- org.junit.jupiter:junit-jupiter-engine:jar:5.6.2:test +| \- org.junit.platform:junit-platform-engine:jar:1.6.2:test +\- org.junit.vintage:junit-vintage-engine:jar:5.6.2:test + \- junit:junit:jar:4.13:compile + \- org.hamcrest:hamcrest-core:jar:1.3:compile diff --git a/pom.xml b/pom.xml index aa974b10..edac0da8 100644 --- a/pom.xml +++ b/pom.xml @@ -26,7 +26,7 @@ 3.0.0-M3 3.8.1 - 1.18.16 + 1.18.22 2.0.0-alpha1 1.8.0-beta4 5.6.2