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 extends Serializable> 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
+ * [...]
+ *
+ *
+ *