Skip to content

Commit

Permalink
feat(spoon): Add spoon based analyzer (#806)
Browse files Browse the repository at this point in the history
  • Loading branch information
MartinWitt committed Jul 9, 2023
1 parent 466d962 commit 24affc8
Show file tree
Hide file tree
Showing 49 changed files with 2,578 additions and 34 deletions.
1 change: 1 addition & 0 deletions code-transformation/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ dependencies {
implementation "com.contrastsecurity:java-sarif:+"
testImplementation group: 'org.assertj', name: 'assertj-core', version: '3.24.2'
implementation project(":commons")
implementation project(":spoon-analyzer")
implementation 'io.github.java-diff-utils:java-diff-utils:4.12'
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
package xyz.keksdose.spoon.code_solver.analyzer.spoon;

import io.github.martinwitt.laughing_train.domain.entity.AnalyzerResult;
import io.github.martinwitt.laughing_train.domain.value.Position;
import io.github.martinwitt.laughing_train.domain.value.RuleId;
import io.github.martinwitt.spoon_analyzer.BadSmell;
import io.github.martinwitt.spoon_analyzer.BadSmellVisitor;
import io.github.martinwitt.spoon_analyzer.badsmells.Index_off_replaceable_by_contains.IndexOfReplaceableByContains;
import io.github.martinwitt.spoon_analyzer.badsmells.access_static_via_instance.AccessStaticViaInstance;
import io.github.martinwitt.spoon_analyzer.badsmells.array_can_be_replaced_with_enum_values.ArrayCanBeReplacedWithEnumValues;
import io.github.martinwitt.spoon_analyzer.badsmells.charset_object_can_be_used.CharsetObjectCanBeUsed;
import io.github.martinwitt.spoon_analyzer.badsmells.innerclass_may_be_static.InnerClassMayBeStatic;
import io.github.martinwitt.spoon_analyzer.badsmells.non_protected_constructor_In_abstract_class.NonProtectedConstructorInAbstractClass;
import io.github.martinwitt.spoon_analyzer.badsmells.private_final_method.PrivateFinalMethod;
import io.github.martinwitt.spoon_analyzer.badsmells.size_replaceable_by_is_empty.SizeReplaceableByIsEmpty;
import io.github.martinwitt.spoon_analyzer.badsmells.unnecessary_implements.UnnecessaryImplements;
import io.github.martinwitt.spoon_analyzer.badsmells.unnecessary_tostring.UnnecessaryTostring;
import java.util.ArrayList;
import java.util.Optional;
import spoon.reflect.code.CtBinaryOperator;
import spoon.reflect.cu.SourcePosition;
import spoon.reflect.declaration.CtType;

class AnalyzerResultVisitor implements BadSmellVisitor<AnalyzerResult> {

private static final AnalyzerResultVisitor analyzerResultVisitor = new AnalyzerResultVisitor();

public static Optional<AnalyzerResult> toAnalyzerResult(BadSmell badSmell) {
return Optional.ofNullable(badSmell.accept(analyzerResultVisitor));
}

private AnalyzerResultVisitor() {}

@Override
public AnalyzerResult visit(IndexOfReplaceableByContains badSmell) {
return toSpoonAnalyzerResult(
badSmell,
badSmell.getIndexOfCall().getPosition(),
badSmell.getIndexOfCall()
.getParent(CtBinaryOperator.class)
.getOriginalSourceFragment()
.toString());
}

private String getAbsolutePath(BadSmell badSmell) {
return badSmell.getAffectedType().getPosition().getFile().getAbsolutePath();
}

private Position toPosition(SourcePosition position) {
int sourceStart = position.getSourceStart();
int sourceEnd = position.getSourceEnd();
int line = position.getLine();
int column = position.getColumn();
int endColumn = position.getEndColumn();
int endLine = position.getEndLine();
return new Position(line, endLine, column, endColumn, sourceStart, sourceEnd - sourceStart);
}

public AnalyzerResult toSpoonAnalyzerResult(BadSmell badSmell, SourcePosition position, String snippet) {
String absolutePath = getAbsolutePath(badSmell);
RuleId ruleId = new RuleId(badSmell.getName());
return new SpoonAnalyzerResult(
ruleId,
absolutePath,
toPosition(position),
badSmell.getDescription(),
badSmell.getDescription(),
snippet);
}

@Override
public AnalyzerResult visit(AccessStaticViaInstance badSmell) {
String snippet =
badSmell.getAffectedCtInvocation().getOriginalSourceFragment().toString();
return toSpoonAnalyzerResult(
badSmell, badSmell.getAffectedCtInvocation().getPosition(), snippet);
}

@Override
public AnalyzerResult visit(ArrayCanBeReplacedWithEnumValues badSmell) {
String snippet =
badSmell.getAffectedElement().getOriginalSourceFragment().toString();
return toSpoonAnalyzerResult(badSmell, badSmell.getAffectedElement().getPosition(), snippet);
}

@Override
public AnalyzerResult visit(CharsetObjectCanBeUsed badSmell) {
if (badSmell.getInvocation() != null) {
String snippet =
badSmell.getInvocation().getOriginalSourceFragment().toString();
return toSpoonAnalyzerResult(badSmell, badSmell.getInvocation().getPosition(), snippet);
} else {
String snippet = badSmell.getCtorCall().getOriginalSourceFragment().toString();
return toSpoonAnalyzerResult(badSmell, badSmell.getCtorCall().getPosition(), snippet);
}
}

@Override
public AnalyzerResult visit(InnerClassMayBeStatic badSmell) {
CtType<?> clone = badSmell.getAffectedType().clone();
clone.setTypeMembers(new ArrayList<>());
String snippet = clone.toString();
return toSpoonAnalyzerResult(badSmell, badSmell.getAffectedType().getPosition(), snippet);
}

@Override
public AnalyzerResult visit(NonProtectedConstructorInAbstractClass badSmell) {
String snippet = badSmell.getCtConstructor().getOriginalSourceFragment().toString();
return toSpoonAnalyzerResult(badSmell, badSmell.getCtConstructor().getPosition(), snippet);
}

@Override
public AnalyzerResult visit(PrivateFinalMethod badSmell) {
String snippet =
badSmell.getAffectedMethod().getOriginalSourceFragment().toString();
return toSpoonAnalyzerResult(badSmell, badSmell.getAffectedMethod().getPosition(), snippet);
}

@Override
public AnalyzerResult visit(SizeReplaceableByIsEmpty badSmell) {
String snippet =
badSmell.getSizeInvocation().getOriginalSourceFragment().toString();
return toSpoonAnalyzerResult(badSmell, badSmell.getSizeInvocation().getPosition(), snippet);
}

@Override
public AnalyzerResult visit(UnnecessaryImplements badSmell) {

CtType<?> clone = badSmell.getAffectedType().clone();
clone.setTypeMembers(new ArrayList<>());
String snippet = clone.toString();
return toSpoonAnalyzerResult(badSmell, badSmell.getAffectedType().getPosition(), snippet);
}

@Override
public AnalyzerResult visit(UnnecessaryTostring badSmell) {
String snippet =
badSmell.getNotNeededTostring().getOriginalSourceFragment().toString();
return toSpoonAnalyzerResult(badSmell, badSmell.getNotNeededTostring().getPosition(), snippet);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package xyz.keksdose.spoon.code_solver.analyzer.spoon;

import io.github.martinwitt.laughing_train.domain.entity.AnalyzerResult;
import io.github.martinwitt.laughing_train.domain.value.Position;
import io.github.martinwitt.laughing_train.domain.value.RuleId;

public record SpoonAnalyzerResult(
RuleId ruleID, String filePath, Position position, String message, String messageMarkdown, String snippet)
implements AnalyzerResult {

@Override
public String getAnalyzer() {
return "Spoon";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package xyz.keksdose.spoon.code_solver.analyzer.spoon;

import io.github.martinwitt.laughing_train.domain.entity.AnalyzerResult;
import io.github.martinwitt.spoon_analyzer.BadSmell;
import io.github.martinwitt.spoon_analyzer.SpoonAnalyzer;
import java.nio.file.Path;
import java.util.List;

public class SpoonBasedAnalyzer {

public List<AnalyzerResult> analyze(Path sourceRoot) {
SpoonAnalyzer analyzer = new SpoonAnalyzer();
List<BadSmell> analyze = analyzer.analyze(sourceRoot.toAbsolutePath().toString());
return analyze.stream()
.map(AnalyzerResultVisitor::toAnalyzerResult)
.filter(v -> v.isPresent())
.map(v -> v.get())
.toList();
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.github.martinwitt.laughing_train.data;
package io.github.martinwitt.laughing_train.data.request;

import io.github.martinwitt.laughing_train.data.Project;
import java.io.Serializable;

public sealed interface AnalyzerRequest extends Serializable {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package io.github.martinwitt.laughing_train.data.result;

import io.github.martinwitt.laughing_train.data.Project;
import io.github.martinwitt.laughing_train.domain.entity.AnalyzerResult;
import java.io.Serializable;
import java.util.List;

public interface CodeAnalyzerResult extends Serializable {

record Success(List<AnalyzerResult> results, Project project) implements CodeAnalyzerResult {}

record Failure(String message) implements CodeAnalyzerResult {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import io.github.martinwitt.laughing_train.Config;
import io.github.martinwitt.laughing_train.MarkdownPrinter;
import io.github.martinwitt.laughing_train.data.Project;
import io.github.martinwitt.laughing_train.domain.entity.AnalyzerResult;
import io.github.martinwitt.laughing_train.domain.value.RuleId;
import jakarta.enterprise.context.ApplicationScoped;
Expand All @@ -25,7 +24,7 @@ public class MiningPrinter {
@Inject
Config config;

public String printAllResults(List<AnalyzerResult> results, Project project) {
public String printAllResults(List<AnalyzerResult> results) {
StringBuilder sb = new StringBuilder();
List<RuleId> ruleIds = config.getRules().keySet().stream()
.map(QodanaRules::getRuleId)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,24 +1,27 @@
package io.github.martinwitt.laughing_train.mining;

import com.google.common.flogger.FluentLogger;
import io.github.martinwitt.laughing_train.data.AnalyzerRequest;
import io.github.martinwitt.laughing_train.data.ProjectRequest;
import io.github.martinwitt.laughing_train.data.ProjectResult;
import io.github.martinwitt.laughing_train.data.ProjectResult.Success;
import io.github.martinwitt.laughing_train.data.QodanaResult;
import io.github.martinwitt.laughing_train.data.request.AnalyzerRequest;
import io.github.martinwitt.laughing_train.data.result.CodeAnalyzerResult;
import io.github.martinwitt.laughing_train.domain.entity.AnalyzerResult;
import io.github.martinwitt.laughing_train.domain.entity.Project;
import io.github.martinwitt.laughing_train.persistence.repository.ProjectRepository;
import io.github.martinwitt.laughing_train.services.ProjectService;
import io.github.martinwitt.laughing_train.services.QodanaService;
import io.github.martinwitt.laughing_train.services.SpoonAnalyzerService;
import io.micrometer.core.instrument.MeterRegistry;
import io.quarkus.runtime.StartupEvent;
import io.vertx.core.Vertx;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.event.Observes;
import java.io.IOException;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.Queue;
import java.util.Random;
import java.util.concurrent.TimeUnit;
Expand All @@ -37,7 +40,7 @@ public class PeriodicMiner {
final ProjectRepository projectRepository;
final QodanaService qodanaService;
final ProjectService projectService;

final SpoonAnalyzerService spoonAnalyzerService;
MeterRegistry registry;

private final Random random = new Random();
Expand All @@ -50,14 +53,16 @@ public PeriodicMiner(
ProjectRepository projectRepository,
QodanaService qodanaService,
ProjectService projectService,
MiningPrinter miningPrinter) {
MiningPrinter miningPrinter,
SpoonAnalyzerService spoonAnalyzerService) {
this.registry = registry;
this.vertx = vertx;
this.searchProjectService = searchProjectService;
this.projectRepository = projectRepository;
this.qodanaService = qodanaService;
this.projectService = projectService;
this.miningPrinter = miningPrinter;
this.spoonAnalyzerService = spoonAnalyzerService;
}

private Project getRandomProject() throws IOException {
Expand Down Expand Up @@ -97,17 +102,32 @@ private void mineRandomRepo() {
}
logger.atInfo().log("Successfully checked out project %s", success.project());
var qodanaResult = analyzeProject(success);
var spoonResult = analyzeProjectWithSpoon(success);
List<AnalyzerResult> results = new ArrayList<>();
if (qodanaResult instanceof QodanaResult.Failure failure) {
logger.atWarning().log("Failed to analyze project %s", failure.message());
registry.counter("mining.qodana.error").increment();
tryDeleteProject(success);
return;
}
if (spoonResult instanceof CodeAnalyzerResult.Failure error) {
logger.atWarning().log("Failed to analyze project with spoon %s", error.message());
tryDeleteProject(success);
}
if (spoonResult instanceof CodeAnalyzerResult.Success successResult) {
results.addAll(successResult.results());
}
if (qodanaResult instanceof QodanaResult.Success successResult) {
logger.atInfo().log("Successfully analyzed project %s", success.project());
saveQodanaResults(successResult);
addOrUpdateCommitHash(success);
results.addAll(successResult.result());
}
if (results.isEmpty()) {
logger.atWarning().log("No results for project %s", success.project());
tryDeleteProject(success);
mineRandomRepo();
return;
}
saveResults(results, project);
addOrUpdateCommitHash(success);
tryDeleteProject(success);
}
} catch (Exception e) {
logger.atWarning().withCause(e).log("Failed to mine random repo");
Expand All @@ -119,6 +139,10 @@ private void mineRandomRepo() {
}
}

private CodeAnalyzerResult analyzeProjectWithSpoon(Success success) {
return spoonAnalyzerService.analyze(new AnalyzerRequest.WithProject(success.project()));
}

private boolean isAlreadyMined(ProjectResult.Success success, String commitHash) {
return projectRepository.findByProjectUrl(success.project().url()).stream()
.anyMatch(it -> !it.getCommitHashes().contains(commitHash));
Expand Down Expand Up @@ -157,28 +181,18 @@ private void addOrUpdateCommitHash(ProjectResult.Success projectResult) {
}
}

private void saveQodanaResults(QodanaResult.Success success) {
success.project().runInContext(() -> {
try {
List<AnalyzerResult> results = success.result();
registry.summary("mining.qodana", "result", Integer.toString(results.size()));
if (results.isEmpty()) {
logger.atInfo().log("No results for %s", success);
return Optional.empty();
}
String content = printFormattedResults(success, results);
var laughingRepo = getLaughingRepo();
updateOrCreateContent(laughingRepo, success.project().name(), content);
} catch (Exception e) {
logger.atSevere().withCause(e).log("Error while updating content");
}
return Optional.empty();
});
private void saveResults(List<AnalyzerResult> results, Project project) {
try {
String content = printFormattedResults(project, results);
var laughingRepo = getLaughingRepo();
updateOrCreateContent(laughingRepo, project.getProjectName(), content);
} catch (Exception e) {
logger.atSevere().withCause(e).log("Error while updating content");
}
}

private String printFormattedResults(QodanaResult.Success success, List<AnalyzerResult> results) {
return "# %s %n %s"
.formatted(success.project().name(), miningPrinter.printAllResults(results, success.project()));
private String printFormattedResults(Project project, List<AnalyzerResult> results) {
return "# %s %n %s".formatted(project.getProjectName(), miningPrinter.printAllResults(results));
}

private GHRepository getLaughingRepo() throws IOException {
Expand Down
Loading

0 comments on commit 24affc8

Please sign in to comment.