Skip to content

Commit

Permalink
[Refactoring] Code clarifications, Unit Tests, Responsibilities (#109)
Browse files Browse the repository at this point in the history
* [Refactoring] Code clarifications, Unit Tests, Responsibilities

* More cleanup

* Remove maven dependencies from core

* Cleanup and use debloated poms

* De-duplicate dependencies before generating debloated pom

* More refactoring into separate classes.

* Move logic in the core, so it's independent from a specific dependency manager

* Fix Gradle plugin

* Try updating JDK to 17

* Change gradle build in GitHub actions to JDK 11

Co-authored-by: César Soto Valero <cesarsotovalero@gmail.com>
  • Loading branch information
Alexandre FILLATRE and cesarsotovalero authored Mar 29, 2022
1 parent 662d116 commit b970294
Show file tree
Hide file tree
Showing 56 changed files with 2,710 additions and 1,746 deletions.
14 changes: 7 additions & 7 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
max-parallel: 1
matrix:
os: [ ubuntu-latest, windows-latest ]
java: [ 11 ]
java: [ 17 ]
runs-on: ${{ matrix.os }}
name: Maven Build with Java ${{ matrix.java }} on ${{ matrix.os }}
steps:
Expand Down Expand Up @@ -49,30 +49,30 @@ jobs:
- name: "Integration Tests"
run: mvn failsafe:integration-test --errors --fail-at-end

# The following is only executed on Ubuntu on Java 11
# The following is only executed on Ubuntu on Java 17
- name: "JaCoCo Coverage Report"
if: matrix.os == 'ubuntu-latest' && matrix.java == 11 && github.repository == 'castor-software/depclean'
if: matrix.os == 'ubuntu-latest' && matrix.java == 17 && github.repository == 'castor-software/depclean'
run: mvn jacoco:report

- name: "Codecov"
if: matrix.os == 'ubuntu-latest' && matrix.java == 11 && github.repository == 'castor-software/depclean'
if: matrix.os == 'ubuntu-latest' && matrix.java == 17 && github.repository == 'castor-software/depclean'
uses: codecov/codecov-action@v1
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./depclean-maven-plugin/target/site/jacoco/jacoco.xml,./depclean-core/target/site/jacoco/jacoco.xml
flags: unittests

- name: "Cache SonarCloud"
if: matrix.os == 'ubuntu-latest' && matrix.java == 11 && github.repository == 'castor-software/depclean'
if: matrix.os == 'ubuntu-latest' && matrix.java == 17 && github.repository == 'castor-software/depclean'
uses: actions/cache@v1
with:
path: ~/.sonar/cache
key: ${{ runner.os }}-sonar
restore-keys: ${{ runner.os }}-sonar

- name: "SonarCloud"
if: matrix.os == 'ubuntu-latest' && matrix.java == 11 && github.repository == 'castor-software/depclean'
run: mvn sonar:sonar -Dsonar.projectKey=castor-software_depclean -Dsonar.organization=castor-software -Dsonar.host.url=https://sonarcloud.io -Dsonar.login=${{ secrets.SONAR_TOKEN }} -Dsonar.java.source=11 -Dsonar.java.target=11
if: matrix.os == 'ubuntu-latest' && matrix.java == 17 && github.repository == 'castor-software/depclean'
run: mvn sonar:sonar -Dsonar.projectKey=castor-software_depclean -Dsonar.organization=castor-software -Dsonar.host.url=https://sonarcloud.io -Dsonar.login=${{ secrets.SONAR_TOKEN }} -Dsonar.java.source=17 -Dsonar.java.target=17
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
Expand Down
14 changes: 13 additions & 1 deletion depclean-core/pom.xml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<!-- Parent pom -->
Expand Down Expand Up @@ -113,6 +113,18 @@
<version>17.0.0</version>
<scope>compile</scope>
</dependency>
<!-- Utils -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>30.1-jre</version>
</dependency>
<!-- Tests -->
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.22.0</version>
</dependency>
</dependencies>

</project>
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();
}
}
Loading

0 comments on commit b970294

Please sign in to comment.