Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Refactoring] Code clarifications, Unit Tests, Responsibilities #109

Merged
merged 10 commits into from
Mar 29, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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