diff --git a/README.md b/README.md index 1acf0ca9..efd8789d 100644 --- a/README.md +++ b/README.md @@ -89,17 +89,18 @@ If all the tests pass, and the project builds correctly after these changes, the The Maven plugin can be configured with the following additional parameters. -| Name | Type | Description | -|:----------|:-------------:| :-------------| -| `` | `Set` | Add a list of dependencies, identified by their coordinates, to be ignored by DepClean during the analysis and considered as used dependencies. Useful to override incomplete result caused by bytecode-level analysis. **Dependency format is:** `groupId:artifactId:version:scope`.| -| `` | `Set` | Add a list of scopes, to be ignored by DepClean during the analysis. Useful to not analyze dependencies with scopes that are not needed at runtime. **Valid scopes are:** `compile`, `provided`, `test`, `runtime`, `system`, `import`. An Empty string indicates no scopes (default).| -| `` | `boolean` | If this is true, DepClean will not analyze the test classes in the project, and, therefore, the dependencies that are only used for testing will be considered unused. This parameter is useful to detect dependencies that have `compile` scope but are only used for testing. **Default value is:** `false`.| -| `` | `boolean` | If this is true, DepClean creates a debloated version of the pom without unused dependencies called `debloated-pom.xml`, in the root of the project. **Default value is:** `false`.| -| `` | `boolean` | If this is true, DepClean creates a JSON file of the dependency tree along with metadata of each dependency. The file is called `depclean-results.json`, and is located in the root of the project. **Default value is:** `false`.| -| `` | `boolean` | If this is true, and DepClean reported any unused direct dependency in the dependency tree, the build fails immediately after running DepClean. **Default value is:** `false`.| -| `` | `boolean` | If this is true, and DepClean reported any unused transitive dependency in the dependency tree, the build fails immediately after running DepClean. **Default value is:** `false`.| -| `` | `boolean` | If this is true, and DepClean reported any unused inherited dependency in the dependency tree, the build fails immediately after running DepClean. **Default value is:** `false`.| -| `` | `boolean` | Skip plugin execution completely. **Default value is:** `false`.| +| Name | Type | Description | +|:---------------------------|:-------------:|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `` | `Set` | Add a list of dependencies, identified by their coordinates, to be ignored by DepClean during the analysis and considered as used dependencies. Useful to override incomplete result caused by bytecode-level analysis. **Dependency format is:** `groupId:artifactId:version:scope`. | +| `` | `Set` | Add a list of scopes, to be ignored by DepClean during the analysis. Useful to not analyze dependencies with scopes that are not needed at runtime. **Valid scopes are:** `compile`, `provided`, `test`, `runtime`, `system`, `import`. An Empty string indicates no scopes (default). | +| `` | `boolean` | If this is true, DepClean will not analyze the test classes in the project, and, therefore, the dependencies that are only used for testing will be considered unused. This parameter is useful to detect dependencies that have `compile` scope but are only used for testing. **Default value is:** `false`. | +| `` | `boolean` | If this is true, DepClean creates a debloated version of the pom without unused dependencies called `debloated-pom.xml`, in the root of the project. **Default value is:** `false`. | +| `` | `boolean` | If this is true, DepClean creates a JSON file of the dependency tree along with metadata of each dependency. The file is called `depclean-results.json`, and is located in the `target` directory of the project. **Default value is:** `false`. | +| `` | `boolean` | If this is true, DepClean creates a CSV file with the static call graph of the API members used in the project. The file is called `depclean-callgraph.csv`, and is located in the `target` directory of the project. **Default value is:** `false`. | +| `` | `boolean` | If this is true, and DepClean reported any unused direct dependency in the dependency tree, the build fails immediately after running DepClean. **Default value is:** `false`. | +| `` | `boolean` | If this is true, and DepClean reported any unused transitive dependency in the dependency tree, the build fails immediately after running DepClean. **Default value is:** `false`. | +| `` | `boolean` | If this is true, and DepClean reported any unused inherited dependency in the dependency tree, the build fails immediately after running DepClean. **Default value is:** `false`. | +| `` | `boolean` | Skip plugin execution completely. **Default value is:** `false`. | For example, to fail the build in the presence of unused direct dependencies and ignore all the scopes except the `compile` scope, use the following plugin configuration. diff --git a/checkstyle.xml b/checkstyle.xml index 93d85806..396e35eb 100644 --- a/checkstyle.xml +++ b/checkstyle.xml @@ -34,7 +34,7 @@ - + diff --git a/depclean-core/pom.xml b/depclean-core/pom.xml index 8321cdd2..fd91596b 100644 --- a/depclean-core/pom.xml +++ b/depclean-core/pom.xml @@ -72,10 +72,24 @@ + + org.apache.maven.plugins + maven-compiler-plugin + + 11 + 11 + + + + + com.thoughtworks.qdox + qdox + 2.0.1 + org.ow2.asm 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 index 5d576dbd..a63cacb4 100644 --- a/depclean-core/src/main/java/se/kth/depclean/core/DepCleanManager.java +++ b/depclean-core/src/main/java/se/kth/depclean/core/DepCleanManager.java @@ -3,6 +3,7 @@ import java.io.File; import java.io.IOException; import java.nio.charset.Charset; +import java.util.HashSet; import java.util.Objects; import java.util.Set; import java.util.concurrent.TimeUnit; @@ -41,7 +42,7 @@ public class DepCleanManager { private final boolean failIfUnusedInherited; private final boolean createPomDebloated; private final boolean createResultJson; - private final boolean createClassUsageCsv; + private final boolean createCallGraphCsv; /** * Execute the depClean manager. @@ -58,7 +59,7 @@ public void execute() throws AnalysisFailureException { getLog().info("Starting DepClean dependency analysis"); if (dependencyManager.isMaven() && dependencyManager.isPackagingPom()) { - getLog().info("Skipping because packaging type pom."); + getLog().info("Skipping because packaging type is pom."); return; } @@ -93,7 +94,7 @@ public void execute() throws AnalysisFailureException { dependencyManager.getDebloater(analysis).write(); } - /* Writing the JSON file with the debloat results */ + /* Writing the JSON file with the depclean results */ if (createResultJson) { createResultJson(analysis); } @@ -106,7 +107,7 @@ 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"); + final File csvFile = new File(dependencyManager.getBuildDirectory() + File.separator + "depclean-callgraph.csv"); try { dependencyManager.generateDependencyTree(treeFile); } catch (IOException | InterruptedException e) { @@ -115,30 +116,31 @@ private void createResultJson(ProjectDependencyAnalysis analysis) { Thread.currentThread().interrupt(); return; } - if (createClassUsageCsv) { - printString("Creating class-usage.csv, please wait..."); + if (createCallGraphCsv) { + printString("Creating " + csvFile.getName() + ", please wait..."); try { - FileUtils.write(classUsageFile, "OriginClass,TargetClass,Dependency\n", Charset.defaultCharset()); + FileUtils.write(csvFile, "OriginClass,TargetClass,OriginDependency,TargetDependency\n", Charset.defaultCharset()); } catch (IOException e) { getLog().error("Error writing the CSV header."); } } - String treeAsJson = dependencyManager.getTreeAsJson(treeFile, + String treeAsJson = dependencyManager.getTreeAsJson( + treeFile, analysis, - classUsageFile, - createClassUsageCsv + csvFile, + createCallGraphCsv ); try { FileUtils.write(jsonFile, treeAsJson, Charset.defaultCharset()); } catch (IOException e) { - getLog().error("Unable to generate JSON file."); + getLog().error("Unable to generate " + jsonFile.getName() + " file."); } if (jsonFile.exists()) { - getLog().info("depclean-results.json file created in: " + jsonFile.getAbsolutePath()); + getLog().info(jsonFile.getName() + " file created in: " + jsonFile.getAbsolutePath()); } - if (classUsageFile.exists()) { - getLog().info("class-usage.csv file created in: " + classUsageFile.getAbsolutePath()); + if (csvFile.exists()) { + getLog().info(csvFile.getName() + " file created in: " + csvFile.getAbsolutePath()); } } @@ -162,21 +164,42 @@ private ProjectContext buildProjectContext() { ignoreScopes.add("test"); } + // Consider are used all the classes declared in Maven processors + Set allUsedClasses = new HashSet<>(); + Set usedClassesFromProcessors = dependencyManager + .collectUsedClassesFromProcessors().stream() + .map(ClassName::new) + .collect(Collectors.toSet()); + + // Consider as used all the classes located in the imports of the source code + Set usedClassesFromSource = dependencyManager.collectUsedClassesFromSource( + dependencyManager.getSourceDirectory(), + dependencyManager.getTestDirectory()) + .stream() + .map(ClassName::new) + .collect(Collectors.toSet()); + + allUsedClasses.addAll(usedClassesFromProcessors); + allUsedClasses.addAll(usedClassesFromSource); + final DependencyGraph dependencyGraph = dependencyManager.dependencyGraph(); return new ProjectContext( dependencyGraph, dependencyManager.getOutputDirectory(), dependencyManager.getTestOutputDirectory(), + dependencyManager.getSourceDirectory(), + dependencyManager.getTestDirectory(), + dependencyManager.getDependenciesDirectory(), ignoreScopes.stream().map(Scope::new).collect(Collectors.toSet()), toDependency(dependencyGraph.allDependencies(), ignoreDependencies), - dependencyManager.collectUsedClassesFromProcessors().stream().map(ClassName::new).collect(Collectors.toSet()) + allUsedClasses ); } /** * Returns a set of {@code DependencyCoordinate}s that match given string representations. * - * @param allDependencies all known dependencies + * @param allDependencies all known dependencies * @param ignoreDependencies string representation of dependencies to return * @return a set of {@code Dependency} that match given string representations */ @@ -197,7 +220,6 @@ private Dependency findDependency(Set allDependencies, String depend 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); } 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 index a12eee7c..89a12b0a 100644 --- 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 @@ -12,20 +12,18 @@ @Slf4j public class ActualUsedClasses { - final Set classes = new HashSet<>(); private final ProjectContext context; + private final Set classes = new HashSet<>(); 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); } diff --git a/depclean-core/src/main/java/se/kth/depclean/core/analysis/ClassFileVisitorUtils.java b/depclean-core/src/main/java/se/kth/depclean/core/analysis/ClassFileVisitorUtils.java index bd717d60..2e3b789d 100644 --- a/depclean-core/src/main/java/se/kth/depclean/core/analysis/ClassFileVisitorUtils.java +++ b/depclean-core/src/main/java/se/kth/depclean/core/analysis/ClassFileVisitorUtils.java @@ -26,6 +26,8 @@ import java.net.URI; import java.net.URISyntaxException; import java.net.URL; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.jar.JarEntry; import java.util.jar.JarInputStream; import org.codehaus.plexus.util.DirectoryScanner; @@ -50,10 +52,8 @@ private ClassFileVisitorUtils() { * * @param url The jar or directory * @param visitor A {@link ClassFileVisitor}. - * @throws IOException In case of any I/O problems. */ - public static void accept(URL url, ClassFileVisitor visitor) - throws IOException { + public static void accept(URL url, ClassFileVisitor visitor) { if (url.getPath().endsWith(".jar")) { acceptJar(url, visitor); } else { @@ -80,12 +80,10 @@ public static void accept(URL url, ClassFileVisitor visitor) * * @param url URL of jar * @param visitor A {@link ClassFileVisitor}. - * @throws IOException In case of IO issues. */ - private static void acceptJar(URL url, ClassFileVisitor visitor) - throws IOException { + private static void acceptJar(URL url, ClassFileVisitor visitor) { try (JarInputStream in = new JarInputStream(url.openStream())) { - JarEntry entry = null; + JarEntry entry; while ((entry = in.getNextJarEntry()) != null) { //NOSONAR String name = entry.getName(); // ignore files like package-info.class and module-info.class @@ -93,6 +91,8 @@ private static void acceptJar(URL url, ClassFileVisitor visitor) visitClass(name, in, visitor); } } + } catch (IOException e) { + e.printStackTrace(); } } @@ -101,10 +101,8 @@ private static void acceptJar(URL url, ClassFileVisitor visitor) * * @param directory Directory or File to be analyzed. * @param visitor A {@link ClassFileVisitor}. - * @throws IOException In case of IO issues. */ - private static void acceptDirectory(File directory, ClassFileVisitor visitor) - throws IOException { + private static void acceptDirectory(File directory, ClassFileVisitor visitor) { if (!directory.isDirectory()) { throw new IllegalArgumentException("File is not a directory"); } @@ -118,21 +116,40 @@ private static void acceptDirectory(File directory, ClassFileVisitor visitor) File file = new File(directory, path); try (FileInputStream in = new FileInputStream(file)) { visitClass(path, in, visitor); + } catch (IOException e) { + e.printStackTrace(); } } } + /** + * Removes the root folder of the dependency from the path. + * + * @param path the dependency folder + * @return path without the dependency folder + */ + public static String getChild(String path) { + Path tmp = Paths.get(path); + if (tmp.getNameCount() > 1) { + return tmp.subpath(1, tmp.getNameCount()).toString(); + } else { + // impossible to extract child's path + return path; + } + } + /** * Visits the classes. * - * @param path Path of the class. - * @param in read the input bytes. + * @param path Path of the class. + * @param in read the input bytes. * @param visitor A {@link ClassFileVisitor}. */ private static void visitClass(String path, InputStream in, ClassFileVisitor visitor) { if (!path.endsWith(CLASS)) { throw new IllegalArgumentException("Path is not a class"); } + path = getChild(path); String className = path.substring(0, path.length() - CLASS.length()); className = className.replace('/', '.'); visitor.visitClass(className, in); 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 0e5b9320..ecbb6a20 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 @@ -41,12 +41,8 @@ public Set analyze(URL url) throws IOException { CollectorClassFileVisitor visitor = new CollectorClassFileVisitor(); try { ClassFileVisitorUtils.accept(url, visitor); - } catch (ZipException e) { - // since the current ZipException gives no indication what jar file is corrupted - // we prefer to wrap another ZipException for better error visibility - ZipException ze = new ZipException("Cannot process Jar entry on URL: " + url + " due to " + e.getMessage()); - ze.initCause(e); - throw ze; + } catch (Exception e) { + e.printStackTrace(); } return visitor.getClasses(); } 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 cf3e5fb7..a1de64f7 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 @@ -58,23 +58,27 @@ public ProjectDependencyAnalysis analyze() throws ProjectDependencyAnalyzerExcep /* ******************** bytecode analysis ********************* */ - // execute the analysis (note that the order of these operations matters!) + // analyze project's class files actualUsedClasses.registerClasses(getProjectDependencyClasses(projectContext.getOutputFolder())); + // analyze project's tests class files if (!projectContext.ignoreTests()) { log.trace("Parsing test folder"); actualUsedClasses.registerClasses(getProjectTestDependencyClasses(projectContext.getTestOutputFolder())); } + // the set of compiled classes and tests in the project + Set projectClasses = new HashSet<>(DefaultCallGraph.getProjectVertices()); + // analyze dependencies' class files + actualUsedClasses.registerClasses(getProjectDependencyClasses(projectContext.getDependenciesFolder())); + // analyze extra classes (collected through static analysis of source code) actualUsedClasses.registerClasses(projectContext.getExtraClasses()); /* ******************** usage analysis ********************* */ - - // search for the dependencies used by the project - Set projectClasses = new HashSet<>(DefaultCallGraph.getProjectVertices()); - log.trace("# DefaultCallGraph.referencedClassMembers()"); actualUsedClasses.registerClasses(getReferencedClassMembers(projectClasses)); /* ******************** results as statically used at the bytecode *********************** */ return new ProjectDependencyAnalysisBuilder(projectContext, actualUsedClasses).analyse(); + + } catch (IOException exception) { throw new ProjectDependencyAnalyzerException("Cannot analyze dependencies", exception); } @@ -93,13 +97,17 @@ private Iterable getProjectTestDependencyClasses(Path testOutputFolde } private Iterable collectDependencyClasses(Path path) throws IOException { - return dependencyAnalyzer.analyze(path.toUri().toURL()).stream() + return dependencyAnalyzer + .analyze(path.toUri().toURL()) + .stream() .map(ClassName::new) .collect(Collectors.toSet()); } private Iterable getReferencedClassMembers(Set projectClasses) { - return DefaultCallGraph.referencedClassMembers(projectClasses).stream() + return DefaultCallGraph + .referencedClassMembers(projectClasses) + .stream() .map(ClassName::new) .collect(Collectors.toSet()); } diff --git a/depclean-core/src/main/java/se/kth/depclean/core/analysis/DependencyAnalyzer.java b/depclean-core/src/main/java/se/kth/depclean/core/analysis/DependencyAnalyzer.java index 6baead12..c3defa9a 100644 --- a/depclean-core/src/main/java/se/kth/depclean/core/analysis/DependencyAnalyzer.java +++ b/depclean-core/src/main/java/se/kth/depclean/core/analysis/DependencyAnalyzer.java @@ -28,6 +28,6 @@ */ public interface DependencyAnalyzer { - Set analyze(URL url) throws IOException; + Set analyze(URL url); } 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 index 309d2789..3a4a4af1 100644 --- 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 @@ -24,8 +24,7 @@ public class ProjectDependencyAnalysisBuilder { private final ActualUsedClasses actualUsedClasses; private final Set usedDependencies; - ProjectDependencyAnalysisBuilder(ProjectContext context, - ActualUsedClasses actualUsedClasses) { + ProjectDependencyAnalysisBuilder(ProjectContext context, ActualUsedClasses actualUsedClasses) { this.context = context; this.actualUsedClasses = actualUsedClasses; usedDependencies = actualUsedClasses.getRegisteredClasses().stream() @@ -39,20 +38,13 @@ public class ProjectDependencyAnalysisBuilder { * @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(); + 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); @@ -127,12 +119,12 @@ private Set getUnusedDependencies( } /** - * If the dependencyToIgnore is an unused dependency, then add it to the set of usedDependencyCoordinates and remove - * it from the set of unusedDependencyCoordinates. + * 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. + * @param dependencyToIgnore The dependency to ignore. */ private void ignoreDependency( Set usedDependencies, 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 eb91807f..dc9b164d 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 @@ -19,7 +19,6 @@ package se.kth.depclean.core.analysis.asm; -import java.io.IOException; import java.net.URL; import java.util.Set; import se.kth.depclean.core.analysis.ClassFileVisitorUtils; @@ -32,12 +31,12 @@ public class ASMDependencyAnalyzer implements DependencyAnalyzer { /** - * Analyze the . Updates the {@link ClassMembersVisitorCounter} class counters. + * Analyze the Updates the {@link ClassMembersVisitorCounter} class counters. * * @see org.apache.invoke.shared.dependency.analyzer.DependencyAnalyzer#analyze(java.net.URL) */ @Override - public Set analyze(URL url) throws IOException { + public Set analyze(URL url) { ClassMembersVisitorCounter.resetClassCounters(); DependencyClassFileVisitor visitor = new DependencyClassFileVisitor(); ClassFileVisitorUtils.accept(url, visitor); diff --git a/depclean-core/src/main/java/se/kth/depclean/core/analysis/asm/ConstantPoolParser.java b/depclean-core/src/main/java/se/kth/depclean/core/analysis/asm/ConstantPoolParser.java index ce505784..fab0e670 100644 --- a/depclean-core/src/main/java/se/kth/depclean/core/analysis/asm/ConstantPoolParser.java +++ b/depclean-core/src/main/java/se/kth/depclean/core/analysis/asm/ConstantPoolParser.java @@ -90,8 +90,7 @@ static Set getConstantPoolClassReferences(byte[] b) { } static Set parseConstantPoolClassReferences(ByteBuffer buf) { - if (buf.order(ByteOrder.BIG_ENDIAN) - .getInt() != HEAD) { + if (buf.order(ByteOrder.BIG_ENDIAN).getInt() != HEAD) { return Collections.emptySet(); } buf.getChar(); diff --git a/depclean-core/src/main/java/se/kth/depclean/core/analysis/asm/DefaultAnnotationVisitor.java b/depclean-core/src/main/java/se/kth/depclean/core/analysis/asm/DefaultAnnotationVisitor.java index ea818abf..f53e63a4 100644 --- a/depclean-core/src/main/java/se/kth/depclean/core/analysis/asm/DefaultAnnotationVisitor.java +++ b/depclean-core/src/main/java/se/kth/depclean/core/analysis/asm/DefaultAnnotationVisitor.java @@ -51,7 +51,6 @@ public void visitEnum(final String name, final String desc, final String value) @Override public AnnotationVisitor visitAnnotation(final String name, final String desc) { resultCollector.addDesc(desc); - return this; } diff --git a/depclean-core/src/main/java/se/kth/depclean/core/analysis/asm/DefaultClassVisitor.java b/depclean-core/src/main/java/se/kth/depclean/core/analysis/asm/DefaultClassVisitor.java index cef600bb..ee112a42 100644 --- a/depclean-core/src/main/java/se/kth/depclean/core/analysis/asm/DefaultClassVisitor.java +++ b/depclean-core/src/main/java/se/kth/depclean/core/analysis/asm/DefaultClassVisitor.java @@ -60,7 +60,7 @@ public DefaultClassVisitor(SignatureVisitor signatureVisitor, @Override public void visit(final int version, final int access, final String name, final String signature, final String superName, final String[] interfaces) { - // System.out.println("Visiting class: " + name); + //System.out.println("Visiting class: " + name); ClassMembersVisitorCounter.addVisitedClass(); if (signature == null) { resultCollector.addName(superName); diff --git a/depclean-core/src/main/java/se/kth/depclean/core/analysis/asm/DependencyClassFileVisitor.java b/depclean-core/src/main/java/se/kth/depclean/core/analysis/asm/DependencyClassFileVisitor.java index c61ca234..dfbf8269 100644 --- a/depclean-core/src/main/java/se/kth/depclean/core/analysis/asm/DependencyClassFileVisitor.java +++ b/depclean-core/src/main/java/se/kth/depclean/core/analysis/asm/DependencyClassFileVisitor.java @@ -73,7 +73,6 @@ public void visitClass(String className, InputStream in) { signatureVisitor, resultCollector ); - DefaultClassVisitor defaultClassVisitor = new DefaultClassVisitor( signatureVisitor, annotationVisitor, @@ -85,6 +84,7 @@ public void visitClass(String className, InputStream in) { reader.accept(defaultClassVisitor, 0); // inset edge in the graph based on the bytecode analysis + //System.out.println("Edge " + className + " -> " + resultCollector.getDependencies()); DefaultCallGraph.addEdge(className, resultCollector.getDependencies()); } catch (IndexOutOfBoundsException | IOException e) { diff --git a/depclean-core/src/main/java/se/kth/depclean/core/analysis/graph/DefaultCallGraph.java b/depclean-core/src/main/java/se/kth/depclean/core/analysis/graph/DefaultCallGraph.java index ffa35857..b58321b3 100644 --- a/depclean-core/src/main/java/se/kth/depclean/core/analysis/graph/DefaultCallGraph.java +++ b/depclean-core/src/main/java/se/kth/depclean/core/analysis/graph/DefaultCallGraph.java @@ -28,13 +28,11 @@ import org.jgrapht.traverse.DepthFirstIterator; /** - * A directed graph G = (V, E) where V is a set of classes and E is a set of edges - * representing class member calls between the classes in V. + * A directed graph G = (V, E) where V is a set of classes and E is a set of edges. Edges represent class member calls between the classes in V. */ public class DefaultCallGraph { - private static final AbstractBaseGraph directedGraph = - new DefaultDirectedGraph<>(DefaultEdge.class); + private static final AbstractBaseGraph directedGraph = new DefaultDirectedGraph<>(DefaultEdge.class); private static final Set projectVertices = new HashSet<>(); private static final Map> usagesPerClass = new HashMap<>(); @@ -59,19 +57,18 @@ public static void addEdge(String clazz, Set referencedClassMembers) { } /** - * Traverses the call graph to obtain a set of all the reachable classes from a set of classes (ie., vertices in the - * graph). + * Traverses the call graph to obtain a set of all the reachable classes from a set of classes. Classes are vertices in the graph. * * @param projectClasses The classes in the Maven project. * @return All the referenced classes. */ public static Set referencedClassMembers(Set projectClasses) { - // System.out.println("project classes: " + projectClasses); + //System.out.println("project classes: " + projectClasses); Set allReferencedClassMembers = new HashSet<>(); for (String projectClass : projectClasses) { allReferencedClassMembers.addAll(traverse(projectClass)); } - // System.out.println("All referenced class members: " + allReferencedClassMembers); + //System.out.println("All referenced class members: " + allReferencedClassMembers); return allReferencedClassMembers; } @@ -91,7 +88,7 @@ private static Set traverse(String start) { } private static void addReferencedClassMember(String clazz, String referencedClassMember) { - // System.out.println("\t" + clazz + " -> " + referencedClassMember); + //System.out.println("\t" + clazz + " -> " + referencedClassMember); Set s = usagesPerClass.computeIfAbsent(clazz, k -> new HashSet<>()); s.add(referencedClassMember); } @@ -113,8 +110,8 @@ public static void cleanDirectedGraph() { directedGraph.edgeSet().clear(); } - public Map> getUsagesPerClass() { + public static Map> getUsagesPerClass() { return usagesPerClass; } -} +} \ No newline at end of file diff --git a/depclean-core/src/main/java/se/kth/depclean/core/analysis/src/ImportsAnalyzer.java b/depclean-core/src/main/java/se/kth/depclean/core/analysis/src/ImportsAnalyzer.java new file mode 100644 index 00000000..0f8f3f73 --- /dev/null +++ b/depclean-core/src/main/java/se/kth/depclean/core/analysis/src/ImportsAnalyzer.java @@ -0,0 +1,57 @@ +package se.kth.depclean.core.analysis.src; + +import com.thoughtworks.qdox.JavaProjectBuilder; +import com.thoughtworks.qdox.model.JavaClass; +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.io.FileUtils; + +/** + * All the classes imported in the source code of the project. + */ +@Data +@AllArgsConstructor +@Slf4j +public class ImportsAnalyzer { + + /** + * A directory with Java source files. + */ + private Path directoryPath; + + /** + * Collects the set of all imported classes in all the Java source files in a directory. + * + * @return The set of all the imports. + */ + public Set collectImportedClassesFromSource() { + Set importsSet = new HashSet<>(); + JavaProjectBuilder builder = new JavaProjectBuilder(); + String[] extensions = new String[]{"java"}; + File directory = new File(directoryPath.toUri()); + if (directory.canRead() && directory.isDirectory()) { + List files = (List) FileUtils.listFiles(directoryPath.toFile(), extensions, true); + for (File file : files) { + try { + builder.addSource(file); + } catch (IOException | RuntimeException e) { + log.info("Cannot analyze imports in file: " + file.getAbsolutePath()); + } + } + Collection javaClasses = builder.getClasses(); + for (JavaClass javaClass : javaClasses) { + List imports = javaClass.getSource().getImports(); + importsSet.addAll(imports); + } + } + return importsSet; + } +} 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 index 47adba9b..92aecbfe 100644 --- 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 @@ -105,7 +105,6 @@ private Iterable findRelatedClasses() { log.error(e.getMessage(), e); } } - return copyOf(relatedClasses); } 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 index 2ba450a3..eb40e6ee 100644 --- 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 @@ -16,8 +16,7 @@ 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.). + * Contains all information about the project's context. It doesn't have any reference to a given framework (Maven, Gradle, etc.). */ @Slf4j @ToString @@ -31,12 +30,18 @@ public final class ProjectContext { private final Path outputFolder; @Getter private final Path testOutputFolder; + @Getter + private final Path sourceFolder; + @Getter + private final Path testFolder; + @Getter + private final Path dependenciesFolder; + @Getter private final Set ignoredScopes; @Getter private final Set ignoredDependencies; - @Getter private final Set extraClasses; @Getter @@ -45,22 +50,27 @@ public final class ProjectContext { /** * 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 dependencyGraph the dependencyGraph + * @param outputFolder where the project's classes are compiled + * @param testOutputFolder where the project's test classes are compiled + * @param sourceFolder where the project's source code are located + * @param tesSourceFolder where the project's test sources are located + * @param dependenciesFolder where the dependency classes are located + * @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) + * @param extraClasses some classes we want to tell the analyser to consider used */ public ProjectContext(DependencyGraph dependencyGraph, - Path outputFolder, Path testOutputFolder, - Set ignoredScopes, - Set ignoredDependencies, - Set extraClasses) { + Path outputFolder, Path testOutputFolder, + Path sourceFolder, Path tesSourceFolder, Path dependenciesFolder, Set ignoredScopes, + Set ignoredDependencies, + Set extraClasses) { this.dependencyGraph = dependencyGraph; this.outputFolder = outputFolder; this.testOutputFolder = testOutputFolder; + this.sourceFolder = sourceFolder; + this.testFolder = tesSourceFolder; + this.dependenciesFolder = dependenciesFolder; this.ignoredScopes = ignoredScopes; this.ignoredDependencies = ignoredDependencies; this.extraClasses = extraClasses; 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 index 2875d92d..13a8f92e 100644 --- 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 @@ -11,8 +11,8 @@ 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 + * 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 { @@ -70,6 +70,20 @@ public interface DependencyManagerWrapper { */ Set collectUsedClassesFromProcessors(); + /** + * Where the compiled dependencies are located. + * + * @return the path to the compiled dependencies. + */ + Path getDependenciesDirectory(); + + /** + * Find classes used in sources. + * + * @return the classes used. + */ + Set collectUsedClassesFromSource(Path sourceDirectory, Path testDirectory); + /** * The instance that will debloat the config file. * @@ -78,6 +92,20 @@ public interface DependencyManagerWrapper { */ AbstractDebloater getDebloater(ProjectDependencyAnalysis analysis); + /** + * Where the sources are. Default is src/main/java. + * + * @return the graph + */ + Path getSourceDirectory(); + + /** + * Where the tests sources are. Default is src/main/java. + * + * @return the graph + */ + Path getTestDirectory(); + /** * The build directory path. * @@ -95,12 +123,11 @@ public interface DependencyManagerWrapper { /** * 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 + * @param treeFile the file containing the tree + * @param analysis the depclean analysis result + * @param classUsageFile the class usage file + * @param createCallGraphCsv whether to write the call graph of usages down * @return the JSON tree */ - String getTreeAsJson( - File treeFile, ProjectDependencyAnalysis analysis, File classUsageFile, boolean createClassUsageCsv); + String getTreeAsJson(File treeFile, ProjectDependencyAnalysis analysis, File classUsageFile, boolean createCallGraphCsv); } \ No newline at end of file diff --git a/depclean-core/src/test/java/se/kth/depclean/core/analysis/ClassFileVisitorUtilsTest.java b/depclean-core/src/test/java/se/kth/depclean/core/analysis/ClassFileVisitorUtilsTest.java index 59b66e42..2b2b188f 100644 --- a/depclean-core/src/test/java/se/kth/depclean/core/analysis/ClassFileVisitorUtilsTest.java +++ b/depclean-core/src/test/java/se/kth/depclean/core/analysis/ClassFileVisitorUtilsTest.java @@ -1,8 +1,9 @@ package se.kth.depclean.core.analysis; -import static org.junit.jupiter.api.Assertions.*; - +import java.nio.file.Path; +import java.nio.file.Paths; import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -17,7 +18,10 @@ void tearDown() { } @Test - void dummy() { - assertTrue(true); + void testGetChild() { + Path parent = Paths.get("Users", "Documents", "SVG"); + Path child = Paths.get("Documents", "SVG"); + Assertions.assertEquals(child.toString(), ClassFileVisitorUtils.getChild(parent.toString())); } + } \ 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 index 036a4855..10c5bda8 100644 --- 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 @@ -38,6 +38,9 @@ default ProjectContext createContext() { ), Paths.get("main/resources"), Paths.get("test/resources"), + Paths.get("/main"), + Paths.get("/test"), + Paths.get(""), Collections.emptySet(), Collections.emptySet(), Collections.emptySet() @@ -54,6 +57,9 @@ default ProjectContext createContextIgnoringTests() { ), Paths.get("main/resources"), Paths.get("test/resources"), + Paths.get("/main"), + Paths.get("/test"), + Paths.get(""), of(new Scope("test")), Collections.emptySet(), Collections.emptySet() @@ -70,6 +76,9 @@ default ProjectContext createContextIgnoringDependency() { ), Paths.get("main/resources"), Paths.get("test/resources"), + Paths.get("/main"), + Paths.get("/test"), + Paths.get(""), of(new Scope("test")), of(COMMONS_IO_DEPENDENCY), Collections.emptySet() diff --git a/depclean-core/src/test/java/se/kth/depclean/core/analysis/graph/DefaultCallGraphTest.java b/depclean-core/src/test/java/se/kth/depclean/core/analysis/graph/DefaultCallGraphTest.java new file mode 100644 index 00000000..a55c6980 --- /dev/null +++ b/depclean-core/src/test/java/se/kth/depclean/core/analysis/graph/DefaultCallGraphTest.java @@ -0,0 +1,46 @@ +package se.kth.depclean.core.analysis.graph; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class DefaultCallGraphTest { + + @BeforeEach + void setUp() { + DefaultCallGraph.addEdge("A", new HashSet<>(Arrays.asList("B", "C", "D"))); + DefaultCallGraph.addEdge("D", new HashSet<>(Arrays.asList("E", "F"))); + DefaultCallGraph.addEdge("F", new HashSet<>(Arrays.asList("G", "H"))); + DefaultCallGraph.addEdge("I", new HashSet<>(Arrays.asList("J"))); + } + + @Test + void referencedClassMembers() { + HashSet projectClasses = new HashSet<>(Arrays.asList("A")); + Set referenced = DefaultCallGraph.referencedClassMembers(projectClasses); + // note that there is no path from A to J + Assertions.assertEquals(new HashSet<>(Arrays.asList("A", "B", "C", "D", "E", "F", "G", "H")), referenced); + } + + @Test + void getProjectVertices() { + Set projectVertices = DefaultCallGraph.getProjectVertices(); + Assertions.assertEquals(new HashSet<>(Arrays.asList("A", "D", "F", "I")), projectVertices); + } + + @Test + void getUsagesPerClass() { + Map> usagesPerClass = DefaultCallGraph.getUsagesPerClass(); + Map> usagesExpected = new HashMap<>(); + usagesExpected.put("A", new HashSet<>(Arrays.asList("B", "C", "D"))); + usagesExpected.put("D", new HashSet<>(Arrays.asList("E", "F"))); + usagesExpected.put("F", new HashSet<>(Arrays.asList("G", "H"))); + usagesExpected.put("I", new HashSet<>(Arrays.asList("J"))); + Assertions.assertEquals(usagesExpected, usagesPerClass); + } +} \ No newline at end of file diff --git a/depclean-core/src/test/java/se/kth/depclean/core/analysis/src/ImportsAnalyzerTest.java b/depclean-core/src/test/java/se/kth/depclean/core/analysis/src/ImportsAnalyzerTest.java new file mode 100644 index 00000000..93f598d7 --- /dev/null +++ b/depclean-core/src/test/java/se/kth/depclean/core/analysis/src/ImportsAnalyzerTest.java @@ -0,0 +1,37 @@ +package se.kth.depclean.core.analysis.src; + +import java.io.File; +import java.nio.file.Path; +import java.util.HashSet; +import java.util.Set; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class ImportsAnalyzerTest { + + Path src = new File("src/test/resources/basic_spring_maven_project/src/main/java").toPath(); + Path srcTest = new File("src/test/resources/basic_spring_maven_project/src/test/java").toPath(); + + @Test + @DisplayName("All the imports in the source files are collected") + void collectImportedClassesFromSource() { + ImportsAnalyzer importsAnalyzer = new ImportsAnalyzer(src); + Set imports = importsAnalyzer.collectImportedClassesFromSource(); + Set output = new HashSet<>(); + output.add("org.springframework.boot.SpringApplication"); + output.add("org.springframework.boot.autoconfigure.SpringBootApplication"); + Assertions.assertEquals(imports, output); + } + + @Test + @DisplayName("All the imports in the test files are collected") + void collectImportedClassesFromTests() { + ImportsAnalyzer importsAnalyzer = new ImportsAnalyzer(srcTest); + Set imports = importsAnalyzer.collectImportedClassesFromSource(); + Set output = new HashSet<>(); + output.add("org.junit.jupiter.api.Test"); + output.add("org.springframework.boot.test.context.SpringBootTest"); + Assertions.assertEquals(imports, output); + } +} \ No newline at end of file diff --git a/depclean-core/src/test/resources/basic_spring_maven_project/pom.xml b/depclean-core/src/test/resources/basic_spring_maven_project/pom.xml new file mode 100644 index 00000000..0d1d9a07 --- /dev/null +++ b/depclean-core/src/test/resources/basic_spring_maven_project/pom.xml @@ -0,0 +1,62 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 2.4.4 + + + com.example + demo + 0.0.1-SNAPSHOT + demo + Demo project for Spring Boot + + 11 + + + + org.springframework.boot + spring-boot-starter-thymeleaf + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + se.kth.castor + depclean-maven-plugin + 2.0.2 + + + + depclean + + + + true + + + + + + + + diff --git a/depclean-core/src/test/resources/basic_spring_maven_project/src/main/java/com/example/demo/DemoApplication.java b/depclean-core/src/test/resources/basic_spring_maven_project/src/main/java/com/example/demo/DemoApplication.java new file mode 100644 index 00000000..4670dd00 --- /dev/null +++ b/depclean-core/src/test/resources/basic_spring_maven_project/src/main/java/com/example/demo/DemoApplication.java @@ -0,0 +1,13 @@ +package com.example.demo; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class DemoApplication { + + public static void main(String[] args) { + SpringApplication.run(DemoApplication.class, args); + } + +} diff --git a/depclean-core/src/test/resources/basic_spring_maven_project/src/main/resources/application.properties b/depclean-core/src/test/resources/basic_spring_maven_project/src/main/resources/application.properties new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/depclean-core/src/test/resources/basic_spring_maven_project/src/main/resources/application.properties @@ -0,0 +1 @@ + diff --git a/depclean-core/src/test/resources/basic_spring_maven_project/src/test/java/com/example/demo/DemoApplicationTests.java b/depclean-core/src/test/resources/basic_spring_maven_project/src/test/java/com/example/demo/DemoApplicationTests.java new file mode 100644 index 00000000..e5f53d67 --- /dev/null +++ b/depclean-core/src/test/resources/basic_spring_maven_project/src/test/java/com/example/demo/DemoApplicationTests.java @@ -0,0 +1,13 @@ +package com.example.demo; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class DemoApplicationTests { + + @Test + void contextLoads() { + } + +} 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 0c527810..2d815bd8 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 @@ -76,10 +76,10 @@ public class DepCleanMojo extends AbstractMojo { /** * If this is true, DepClean creates a CSV file with the result of the analysis with the columns: - * OriginClass,TargetClass,Dependency. The file is called "class-usage.csv" and it is located in /target. + * OriginClass,TargetClass,OriginDependency,TargetDependency. The file is called "depclean-callgraph.csv" and it is located in /target. */ - @Parameter(property = "createClassUsageCsv", defaultValue = "false") - private boolean createClassUsageCsv; + @Parameter(property = "createCallGraphCsv", defaultValue = "false") + private boolean createCallGraphCsv; /** * Add a list of dependencies, identified by their coordinates, to be ignored by DepClean during the analysis and @@ -156,7 +156,7 @@ public final void execute() { failIfUnusedInherited, createPomDebloated, createResultJson, - createClassUsageCsv + createCallGraphCsv ).execute(); } catch (AnalysisFailureException e) { throw new MojoExecutionException(e.getMessage(), e); diff --git a/depclean-maven-plugin/src/main/java/se/kth/depclean/util/JarUtils.java b/depclean-maven-plugin/src/main/java/se/kth/depclean/util/JarUtils.java index e7a49796..0291c351 100644 --- a/depclean-maven-plugin/src/main/java/se/kth/depclean/util/JarUtils.java +++ b/depclean-maven-plugin/src/main/java/se/kth/depclean/util/JarUtils.java @@ -107,6 +107,4 @@ private static void decompressDependencyFiles(String zipFile) throws IOException } } } - - } 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 263907ea..21c24d70 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 @@ -22,8 +22,8 @@ public class NodeAdapter extends TypeAdapter { private final ProjectDependencyAnalysis analysis; - private final File classUsageFile; - private final boolean createClassUsageCsv; + private final File callGraphFile; + private final boolean createCallGraphCsv; @Override public void write(JsonWriter jsonWriter, Node node) throws IOException { @@ -31,10 +31,9 @@ public void write(JsonWriter jsonWriter, Node node) throws IOException { String gav = ga + ":" + node.getVersion(); String vs = node.getVersion() + ":" + node.getScope(); String canonical = ga + ":" + node.getPackaging() + ":" + vs; - String dependencyJar = node.getArtifactId() + "-" + node.getVersion() + ".jar"; - if (createClassUsageCsv) { - writeClassUsageCsv(canonical); + if (createCallGraphCsv) { + writeCallGraphCsv(canonical); } final DependencyAnalysisInfo dependencyInfo = analysis.getDependencyInfo(gav); @@ -117,14 +116,14 @@ private void writeAllTypes(DependencyAnalysisInfo info, JsonWriter localWriter) allTypes.endArray(); } - private void writeClassUsageCsv(String canonical) throws IOException { + private void writeCallGraphCsv(String canonical) throws IOException { DefaultCallGraph defaultCallGraph = new DefaultCallGraph(); for (Map.Entry> usagePerClassMap : defaultCallGraph.getUsagesPerClass().entrySet()) { String key = usagePerClassMap.getKey(); Set value = usagePerClassMap.getValue(); for (String s : value) { String triplet = key + "," + s + "," + canonical + "\n"; - FileUtils.write(classUsageFile, triplet, Charset.defaultCharset(), true); + FileUtils.write(callGraphFile, 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 93d78ad6..a4bccad2 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 @@ -26,7 +26,7 @@ public class ParsedDependencies { private final File treeFile; private final ProjectDependencyAnalysis analysis; private final File classUsageFile; - private final boolean createClassUsageCsv; + private final boolean createCallgraphCsv; /** * Creates string with the JSON representation of the enriched dependency tree of the Maven project. @@ -45,7 +45,7 @@ public String parseTreeToJson() throws ParseException, IOException { NodeAdapter nodeAdapter = new NodeAdapter( analysis, classUsageFile, - createClassUsageCsv + createCallgraphCsv ); GsonBuilder gsonBuilder = new GsonBuilder() .setPrettyPrinting() 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 index a7203f52..05f964cf 100644 --- 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 @@ -8,6 +8,7 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.util.Arrays; +import java.util.HashSet; import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; @@ -27,6 +28,7 @@ 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.analysis.src.ImportsAnalyzer; import se.kth.depclean.core.wrapper.DependencyManagerWrapper; import se.kth.depclean.graph.MavenDependencyGraph; import se.kth.depclean.util.JarUtils; @@ -51,18 +53,17 @@ public class MavenDependencyManager implements DependencyManagerWrapper { /** * Creates the manager. * - * @param logger the logger - * @param project the maven project - * @param session the maven session + * @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) { + DependencyGraphBuilder dependencyGraphBuilder) { this.logger = logger; this.project = project; this.session = session; this.dependencyGraphBuilder = dependencyGraphBuilder; - this.model = buildModel(project); } @@ -86,8 +87,7 @@ public void copyAndExtractDependencies() { /* Copy direct dependencies locally */ try { MavenInvoker.runCommand("mvn dependency:copy-dependencies -DoutputDirectory=" - + project.getBuild().getDirectory() + File.separator + DIRECTORY_TO_COPY_DEPENDENCIES, - null); + + project.getBuild().getDirectory() + File.separator + DIRECTORY_TO_COPY_DEPENDENCIES, null); } catch (IOException | InterruptedException e) { getLog().error("Unable to resolve all the dependencies."); Thread.currentThread().interrupt(); @@ -185,6 +185,24 @@ public Set collectUsedClassesFromProcessors() { .orElse(ImmutableSet.of()); } + @Override + public Path getDependenciesDirectory() { + String dependencyDirectoryName = project.getBuild().getDirectory() + "/" + DIRECTORY_TO_COPY_DEPENDENCIES; + return new File(dependencyDirectoryName).toPath(); + } + + @Override + public Set collectUsedClassesFromSource(Path sourceDirectory, Path testSourceDirectory) { + Set allImports = new HashSet<>(); + ImportsAnalyzer importsInSourceFolder = new ImportsAnalyzer(sourceDirectory); + ImportsAnalyzer importsInTestsFolder = new ImportsAnalyzer(testSourceDirectory); + Set importsInSourceFolderSet = importsInSourceFolder.collectImportedClassesFromSource(); + Set importsInTestsFolderSet = importsInTestsFolder.collectImportedClassesFromSource(); + allImports.addAll(importsInSourceFolderSet); + allImports.addAll(importsInTestsFolderSet); + return allImports; + } + @Override public AbstractDebloater getDebloater(ProjectDependencyAnalysis analysis) { return new MavenDebloater( @@ -199,6 +217,16 @@ public String getBuildDirectory() { return project.getBuild().getDirectory(); } + @Override + public Path getSourceDirectory() { + return new File(project.getBuild().getSourceDirectory()).toPath(); + } + + @Override + public Path getTestDirectory() { + return new File(project.getBuild().getTestSourceDirectory()).toPath(); + } + @Override public void generateDependencyTree(File treeFile) throws IOException, InterruptedException { MavenInvoker.runCommand("mvn dependency:tree -DoutputFile=" + treeFile + " -Dverbose=true", null); @@ -207,12 +235,12 @@ public void generateDependencyTree(File treeFile) throws IOException, Interrupte @SneakyThrows @Override public String getTreeAsJson( - File treeFile, ProjectDependencyAnalysis analysis, File classUsageFile, boolean createClassUsageCsv) { + File treeFile, ProjectDependencyAnalysis analysis, File classUsageFile, boolean createCallGraphCsv) { return new ParsedDependencies( treeFile, analysis, classUsageFile, - createClassUsageCsv + createCallGraphCsv ).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 116796b2..496f2e42 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 @@ -56,7 +56,6 @@ void all_dependencies_unused(MavenExecutionResult result) { ); } - @MavenTest @DisplayName("Test that DepClean identifies all dependencies as used") void all_dependencies_used(MavenExecutionResult result) { @@ -65,10 +64,12 @@ void all_dependencies_used(MavenExecutionResult result) { "-------------------------------------------------------", " D E P C L E A N A N A L Y S I S R E S U L T S", "-------------------------------------------------------", - "USED DIRECT DEPENDENCIES [3]: ", + "USED DIRECT DEPENDENCIES [5]: ", + " org.projectlombok:lombok:1.18.22:compile (1 MB)", " org.apache.commons:commons-lang3:3.12.0:compile (573 KB)", " commons-codec:commons-codec:1.15:compile (345 KB)", " commons-io:commons-io:2.11.0:compile (319 KB)", + " org.kohsuke.metainf-services:metainf-services:1.8:compile (7 KB)", "USED INHERITED DEPENDENCIES [0]: ", "USED TRANSITIVE DEPENDENCIES [0]: ", "POTENTIALLY UNUSED DIRECT DEPENDENCIES [0]: ", @@ -77,6 +78,24 @@ void all_dependencies_used(MavenExecutionResult result) { ); } + @MavenTest + @DisplayName("Test that dependencies used indirectly (org.tukaani:xz is used indirectly)") + void used_indirectly(MavenExecutionResult result) { + assertThat(result).isSuccessful().out() + .plain().contains( + "-------------------------------------------------------", + " D E P C L E A N A N A L Y S I S R E S U L T S", + "-------------------------------------------------------", + "USED DIRECT DEPENDENCIES [2]: ", + " org.apache.commons:commons-compress:1.21:compile (994 KB)", + " org.tukaani:xz:1.9:compile (113 KB)", + "USED INHERITED DEPENDENCIES [0]: ", + "USED TRANSITIVE DEPENDENCIES [0]: ", + "POTENTIALLY UNUSED DIRECT DEPENDENCIES [0]: ", + "POTENTIALLY UNUSED INHERITED DEPENDENCIES [0]: ", + "POTENTIALLY UNUSED TRANSITIVE DEPENDENCIES [0]: " + ); + } @MavenTest @DisplayName("Test that DepClean runs in a Maven project with processors") diff --git a/depclean-maven-plugin/src/test/resources-its/se/kth/depclean/DepCleanMojoIT/all_dependencies_used/pom.xml b/depclean-maven-plugin/src/test/resources-its/se/kth/depclean/DepCleanMojoIT/all_dependencies_used/pom.xml index 73234ef3..4c9761fc 100644 --- a/depclean-maven-plugin/src/test/resources-its/se/kth/depclean/DepCleanMojoIT/all_dependencies_used/pom.xml +++ b/depclean-maven-plugin/src/test/resources-its/se/kth/depclean/DepCleanMojoIT/all_dependencies_used/pom.xml @@ -29,6 +29,12 @@ commons-lang3 3.12.0 + + org.kohsuke.metainf-services + metainf-services + 1.8 + compile + commons-codec commons-codec @@ -39,6 +45,12 @@ commons-io 2.11.0 + + org.projectlombok + lombok + RELEASE + compile + diff --git a/depclean-maven-plugin/src/test/resources-its/se/kth/depclean/DepCleanMojoIT/all_dependencies_used/src/main/java/DataClass.java b/depclean-maven-plugin/src/test/resources-its/se/kth/depclean/DepCleanMojoIT/all_dependencies_used/src/main/java/DataClass.java new file mode 100644 index 00000000..161551ea --- /dev/null +++ b/depclean-maven-plugin/src/test/resources-its/se/kth/depclean/DepCleanMojoIT/all_dependencies_used/src/main/java/DataClass.java @@ -0,0 +1,6 @@ +import lombok.Data; + +@Data +public class DataClass { + +} diff --git a/depclean-maven-plugin/src/test/resources-its/se/kth/depclean/DepCleanMojoIT/all_dependencies_used/src/test/java/KhsukeTest.java b/depclean-maven-plugin/src/test/resources-its/se/kth/depclean/DepCleanMojoIT/all_dependencies_used/src/test/java/KhsukeTest.java new file mode 100644 index 00000000..ef8f0c2f --- /dev/null +++ b/depclean-maven-plugin/src/test/resources-its/se/kth/depclean/DepCleanMojoIT/all_dependencies_used/src/test/java/KhsukeTest.java @@ -0,0 +1,11 @@ +import java.io.IOException; +import org.kohsuke.MetaInfServices; + +@MetaInfServices +public class KhsukeTest implements SomeContract { + + public static void useMain() throws IOException { + System.out.println("Use annotation"); + } + +} diff --git a/depclean-maven-plugin/src/test/resources-its/se/kth/depclean/DepCleanMojoIT/all_dependencies_used/src/test/java/SomeContract.java b/depclean-maven-plugin/src/test/resources-its/se/kth/depclean/DepCleanMojoIT/all_dependencies_used/src/test/java/SomeContract.java new file mode 100644 index 00000000..0e3cc9bd --- /dev/null +++ b/depclean-maven-plugin/src/test/resources-its/se/kth/depclean/DepCleanMojoIT/all_dependencies_used/src/test/java/SomeContract.java @@ -0,0 +1,3 @@ +public interface SomeContract { + +} diff --git a/depclean-maven-plugin/src/test/resources-its/se/kth/depclean/DepCleanMojoIT/used_indirectly/pom.xml b/depclean-maven-plugin/src/test/resources-its/se/kth/depclean/DepCleanMojoIT/used_indirectly/pom.xml new file mode 100644 index 00000000..daf66f1e --- /dev/null +++ b/depclean-maven-plugin/src/test/resources-its/se/kth/depclean/DepCleanMojoIT/used_indirectly/pom.xml @@ -0,0 +1,62 @@ + + + 4.0.0 + + org.foo.bar + foobar + 1.0.0-SNAPSHOT + jar + foobar + + + UTF-8 + UTF-8 + 11 + 11 + + + + + org.tukaani + xz + 1.9 + + + + org.apache.commons + commons-compress + 1.21 + + + + + + com.soebes.itf.jupiter.extension + itf-failure-plugin + 0.9.0 + + + first_very_simple + initialize + + failure + + + + + + se.kth.castor + depclean-maven-plugin + 2.0.2 + + + + depclean + + + + + + + diff --git a/depclean-maven-plugin/src/test/resources-its/se/kth/depclean/DepCleanMojoIT/used_indirectly/src/main/java/Main.java b/depclean-maven-plugin/src/test/resources-its/se/kth/depclean/DepCleanMojoIT/used_indirectly/src/main/java/Main.java new file mode 100644 index 00000000..9b1d8a1a --- /dev/null +++ b/depclean-maven-plugin/src/test/resources-its/se/kth/depclean/DepCleanMojoIT/used_indirectly/src/main/java/Main.java @@ -0,0 +1,17 @@ +// import com.fasterxml.jackson.databind.ObjectMapper; + + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import org.apache.commons.compress.compressors.lzma.LZMACompressorOutputStream; + +public class Main { + + public static void main(String[] args) throws IOException { + OutputStream out = new ByteArrayOutputStream(); + OutputStream tmp = new LZMACompressorOutputStream(out); + System.out.println(tmp); + } + +} \ No newline at end of file diff --git a/depclean-maven-plugin/src/test/resources-its/se/kth/depclean/DepCleanMojoIT/used_indirectly/src/test/java/EmptyTestClass.java b/depclean-maven-plugin/src/test/resources-its/se/kth/depclean/DepCleanMojoIT/used_indirectly/src/test/java/EmptyTestClass.java new file mode 100644 index 00000000..b030b044 --- /dev/null +++ b/depclean-maven-plugin/src/test/resources-its/se/kth/depclean/DepCleanMojoIT/used_indirectly/src/test/java/EmptyTestClass.java @@ -0,0 +1,6 @@ + +public class EmptyTestClass { + + + +} 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 7682a8eb..242a4c66 100644 --- a/depclean-maven-plugin/src/test/resources/DepCleanMojoResources/depclean-results.json +++ b/depclean-maven-plugin/src/test/resources/DepCleanMojoResources/depclean-results.json @@ -6,7 +6,7 @@ "version": "1.0.0-SNAPSHOT", "packaging": "jar", "omitted": false, - "size": 2859, + "size": 2861, "type": "unknown", "status": "unknown", "parent": "unknown", @@ -37,9 +37,15 @@ "com.jcabi.manifests.StreamsMfs" ], "usedTypes": [ - "com.jcabi.manifests.Manifests" + "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.StreamsMfs" ], - "usageRatio": 0.14285714285714285, + "usageRatio": 1.0, "children": [] }, {