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

feat(spoon): Add spoon based analyzer #806

Merged
merged 2 commits into from
Jul 9, 2023
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
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