Skip to content

Commit

Permalink
Move logic in the core, so it's independent from a specific dependenc…
Browse files Browse the repository at this point in the history
…y manager
  • Loading branch information
Alexandre Fillatre committed Mar 28, 2022
1 parent 16bca3a commit af5fca6
Show file tree
Hide file tree
Showing 33 changed files with 875 additions and 532 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package se.kth.depclean.core;

import java.io.IOException;
import java.util.List;
import java.util.stream.Collectors;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import se.kth.depclean.core.analysis.model.DebloatedDependency;
import se.kth.depclean.core.analysis.model.ProjectDependencyAnalysis;

/**
* Analyses the analysis result and writes the debloated config file.
*/
@Slf4j
@AllArgsConstructor
public abstract class AbstractDebloater<T> {

protected final ProjectDependencyAnalysis analysis;

/**
* Writes the debloated config file down.
*/
public void write() throws IOException {
log.info("Starting debloating file");
logChanges();
setDependencies(analysis.getDebloatedDependencies().stream()
.map(this::toMavenDependency)
.collect(Collectors.toList()));

if (log.isDebugEnabled()) {
logDependencies();
}
postProcessDependencies();
writeFile();
}

protected abstract T toMavenDependency(DebloatedDependency debloatedDependency);

protected abstract void setDependencies(List<T> dependencies);

protected abstract void writeFile() throws IOException;

protected abstract void logDependencies();

/**
* In order to keep the version as variable (property) for dependencies that were declared as such, post-process
* dependencies to replace interpolated version with the initial one.
*/
protected abstract void postProcessDependencies();

private void logChanges() {
if (analysis.hasUsedTransitiveDependencies()) {
final int dependencyAmount = analysis.getUsedTransitiveDependencies().size();
log.info("Adding {} used transitive {} as direct {}.",
dependencyAmount, getDependencyWording(dependencyAmount), getDependencyWording(dependencyAmount));
}

if (analysis.hasUnusedDirectDependencies()) {
final int dependencyAmount = analysis.getUnusedDirectDependencies().size();
log.info("Removing {} unused direct {}.", dependencyAmount, getDependencyWording(dependencyAmount));
}

if (analysis.hasUnusedTransitiveDependencies()) {
final int dependencyAmount = analysis.getUnusedTransitiveDependencies().size();
log.info(
"Excluding {} unused transitive {} one-by-one.", dependencyAmount, getDependencyWording(dependencyAmount));
}
}

private String getDependencyWording(int amount) {
return amount > 1 ? "dependencies" : "dependency";
}
}
211 changes: 211 additions & 0 deletions depclean-core/src/main/java/se/kth/depclean/core/DepCleanManager.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
package se.kth.depclean.core;

import java.io.File;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import lombok.AllArgsConstructor;
import lombok.SneakyThrows;
import org.apache.commons.io.FileUtils;
import org.apache.maven.plugin.logging.Log;
import org.jetbrains.annotations.Nullable;
import se.kth.depclean.core.analysis.AnalysisFailureException;
import se.kth.depclean.core.analysis.DefaultProjectDependencyAnalyzer;
import se.kth.depclean.core.analysis.ProjectDependencyAnalyzerException;
import se.kth.depclean.core.analysis.graph.DependencyGraph;
import se.kth.depclean.core.analysis.model.ProjectDependencyAnalysis;
import se.kth.depclean.core.model.ClassName;
import se.kth.depclean.core.model.Dependency;
import se.kth.depclean.core.model.ProjectContext;
import se.kth.depclean.core.model.Scope;
import se.kth.depclean.core.wrapper.DependencyManagerWrapper;

/**
* Runs the depclean process, regardless of a specific dependency manager.
*/
@AllArgsConstructor
public class DepCleanManager {

private static final String SEPARATOR = "-------------------------------------------------------";

private final DependencyManagerWrapper dependencyManager;
private final boolean skipDepClean;
private final boolean ignoreTests;
private final Set<String> ignoreScopes;
private final Set<String> ignoreDependencies;
private final boolean failIfUnusedDirect;
private final boolean failIfUnusedTransitive;
private final boolean failIfUnusedInherited;
private final boolean createPomDebloated;
private final boolean createResultJson;
private final boolean createClassUsageCsv;

/**
* Execute the depClean manager.
*/
@SneakyThrows
public void execute() throws AnalysisFailureException {
final long startTime = System.currentTimeMillis();

if (skipDepClean) {
getLog().info("Skipping DepClean plugin execution");
return;
}
printString(SEPARATOR);
getLog().info("Starting DepClean dependency analysis");

if (dependencyManager.isMaven() && dependencyManager.isPackagingPom()) {
getLog().info("Skipping because packaging type pom.");
return;
}

dependencyManager.copyAndExtractDependencies();

final ProjectDependencyAnalysis analysis = getAnalysis();
if (analysis == null) {
return;
}
analysis.print();

/* Fail the build if there are unused direct dependencies */
if (failIfUnusedDirect && analysis.hasUnusedDirectDependencies()) {
throw new AnalysisFailureException(
"Build failed due to unused direct dependencies in the dependency tree of the project.");
}

/* Fail the build if there are unused transitive dependencies */
if (failIfUnusedTransitive && analysis.hasUnusedTransitiveDependencies()) {
throw new AnalysisFailureException(
"Build failed due to unused transitive dependencies in the dependency tree of the project.");
}

/* Fail the build if there are unused inherited dependencies */
if (failIfUnusedInherited && analysis.hasUnusedInheritedDependencies()) {
throw new AnalysisFailureException(
"Build failed due to unused inherited dependencies in the dependency tree of the project.");
}

/* Writing the debloated version of the pom */
if (createPomDebloated) {
dependencyManager.getDebloater(analysis).write();
}

/* Writing the JSON file with the debloat results */
if (createResultJson) {
createResultJson(analysis);
}

final long stopTime = System.currentTimeMillis();
getLog().info("Analysis done in " + getTime(stopTime - startTime));
}

private void createResultJson(ProjectDependencyAnalysis analysis) {
printString("Creating depclean-results.json, please wait...");
final File jsonFile = new File(dependencyManager.getBuildDirectory() + File.separator + "depclean-results.json");
final File treeFile = new File(dependencyManager.getBuildDirectory() + File.separator + "tree.txt");
final File classUsageFile = new File(dependencyManager.getBuildDirectory() + File.separator + "class-usage.csv");
try {
dependencyManager.generateDependencyTree(treeFile);
} catch (IOException | InterruptedException e) {
getLog().error("Unable to generate dependency tree.");
// Restore interrupted state...
Thread.currentThread().interrupt();
return;
}
if (createClassUsageCsv) {
printString("Creating class-usage.csv, please wait...");
try {
FileUtils.write(classUsageFile, "OriginClass,TargetClass,Dependency\n", Charset.defaultCharset());
} catch (IOException e) {
getLog().error("Error writing the CSV header.");
}
}
String treeAsJson = dependencyManager.getTreeAsJson(treeFile,
analysis,
classUsageFile,
createClassUsageCsv
);

try {
FileUtils.write(jsonFile, treeAsJson, Charset.defaultCharset());
} catch (IOException e) {
getLog().error("Unable to generate JSON file.");
}
if (jsonFile.exists()) {
getLog().info("depclean-results.json file created in: " + jsonFile.getAbsolutePath());
}
if (classUsageFile.exists()) {
getLog().info("class-usage.csv file created in: " + classUsageFile.getAbsolutePath());
}
}

@Nullable
private ProjectDependencyAnalysis getAnalysis() {
/* Analyze dependencies usage status */
final ProjectContext projectContext = buildProjectContext();
final ProjectDependencyAnalysis analysis;
final DefaultProjectDependencyAnalyzer dependencyAnalyzer = new DefaultProjectDependencyAnalyzer(projectContext);
try {
analysis = dependencyAnalyzer.analyze();
} catch (ProjectDependencyAnalyzerException e) {
getLog().error("Unable to analyze dependencies.");
return null;
}
return analysis;
}

private ProjectContext buildProjectContext() {
if (ignoreTests) {
ignoreScopes.add("test");
}

final DependencyGraph dependencyGraph = dependencyManager.dependencyGraph();
return new ProjectContext(
dependencyGraph,
dependencyManager.getOutputDirectory(),
dependencyManager.getTestOutputDirectory(),
ignoreScopes.stream().map(Scope::new).collect(Collectors.toSet()),
toDependency(dependencyGraph.allDependencies(), ignoreDependencies),
dependencyManager.collectUsedClassesFromProcessors().stream().map(ClassName::new).collect(Collectors.toSet())
);
}

/**
* Returns a set of {@code DependencyCoordinate}s that match given string representations.
*
* @param allDependencies all known dependencies
* @param ignoreDependencies string representation of dependencies to return
* @return a set of {@code Dependency} that match given string representations
*/
private Set<Dependency> toDependency(Set<Dependency> allDependencies, Set<String> ignoreDependencies) {
return ignoreDependencies.stream()
.map(dependency -> findDependency(allDependencies, dependency))
.filter(Objects::nonNull)
.collect(Collectors.toSet());
}

private Dependency findDependency(Set<Dependency> allDependencies, String dependency) {
return allDependencies.stream()
.filter(dep -> dep.toString().toLowerCase().contains(dependency.toLowerCase()))
.findFirst()
.orElse(null);
}

private String getTime(long millis) {
long minutes = TimeUnit.MILLISECONDS.toMinutes(millis);
long seconds = (TimeUnit.MILLISECONDS.toSeconds(millis) % 60);

return String.format("%smin %ss", minutes, seconds);
}

private void printString(final String string) {
System.out.println(string); //NOSONAR avoid a warning of non-used logger
}

private Log getLog() {
return dependencyManager.getLog();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
import java.util.HashSet;
import java.util.Set;
import lombok.extern.slf4j.Slf4j;
import se.kth.depclean.core.analysis.model.ClassName;
import se.kth.depclean.core.analysis.model.ProjectContext;
import se.kth.depclean.core.model.ClassName;
import se.kth.depclean.core.model.ProjectContext;

/**
* Contains the actual classes used in the project (i.e. in classes, processors, configurations, etc.)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package se.kth.depclean.core.analysis;

/**
* Indicates the analysis should fail.
*/
public class AnalysisFailureException extends Exception {

/**
* Create the failure.
*
* @param message the message to explain with the analysis failed
*/
public AnalysisFailureException(String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,9 @@
import lombok.extern.slf4j.Slf4j;
import se.kth.depclean.core.analysis.asm.ASMDependencyAnalyzer;
import se.kth.depclean.core.analysis.graph.DefaultCallGraph;
import se.kth.depclean.core.analysis.model.ClassName;
import se.kth.depclean.core.analysis.model.ProjectContext;
import se.kth.depclean.core.analysis.model.ProjectDependencyAnalysis;
import se.kth.depclean.core.model.ClassName;
import se.kth.depclean.core.model.ProjectContext;

/**
* This is principal class that perform the dependency analysis in a Maven project.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import lombok.AllArgsConstructor;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import se.kth.depclean.core.analysis.model.ClassName;
import se.kth.depclean.core.model.ClassName;

/**
* POJO containing the types in a dependency.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@
import java.util.Set;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import se.kth.depclean.core.analysis.model.ClassName;
import se.kth.depclean.core.analysis.model.Dependency;
import se.kth.depclean.core.analysis.model.ProjectContext;
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.
Expand Down Expand Up @@ -86,21 +87,21 @@ private Map<Dependency, DependencyTypes> buildDependencyClassesMap() {
private Set<Dependency> getUsedDirectDependencies() {
return usedDependencies.stream()
.filter(a -> context.getDependencyGraph().directDependencies().contains(a))
.peek(DependencyCoordinate -> log.trace("## Used Direct dependency {}", DependencyCoordinate))
.peek(dependency -> log.trace("## Used Direct dependency {}", dependency))
.collect(Collectors.toSet());
}

private Set<Dependency> getUsedTransitiveDependencies() {
return usedDependencies.stream()
.filter(a -> context.getDependencyGraph().transitiveDependencies().contains(a))
.peek(DependencyCoordinate -> log.trace("## Used Transitive dependency {}", DependencyCoordinate))
.peek(dependency -> log.trace("## Used Transitive dependency {}", dependency))
.collect(Collectors.toSet());
}

private Set<Dependency> getUsedInheritedDependencies() {
return usedDependencies.stream()
.filter(a -> context.getDependencyGraph().inheritedDependencies().contains(a))
.peek(DependencyCoordinate -> log.trace("## Used Transitive dependency {}", DependencyCoordinate))
.peek(dependency -> log.trace("## Used Transitive dependency {}", dependency))
.collect(Collectors.toSet());
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package se.kth.depclean.core.analysis.graph;

import java.util.Set;
import se.kth.depclean.core.analysis.model.Dependency;
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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import java.util.Set;
import lombok.Getter;
import se.kth.depclean.core.model.Dependency;

/**
* A debloated dependency.
Expand Down
Loading

0 comments on commit af5fca6

Please sign in to comment.