Skip to content

Commit

Permalink
feat: ✨ Add pattern based spoon analyzer (#719)
Browse files Browse the repository at this point in the history
  • Loading branch information
MartinWitt authored Jun 9, 2023
1 parent 969160b commit 66b9a08
Show file tree
Hide file tree
Showing 12 changed files with 651 additions and 1 deletion.
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package xyz.keksdose.spoon.code_solver.analyzer.spoon;

import io.github.martinwitt.laughing_train.domain.entity.AnalyzerResult;
import java.nio.file.Path;
import java.util.List;
import spoon.reflect.declaration.CtType;
import spoon.reflect.declaration.CtTypeParameter;
import xyz.keksdose.spoon.code_solver.history.ChangeListener;
import xyz.keksdose.spoon.code_solver.transformations.BadSmell;

public interface AbstractSpoonRuleAnalyzer {

abstract List<SpoonAnalyzerResult> analyze(CtType<?> type);

/**
* Applies the refactoring to the given {@link CtType}.
* @param listener The listener which is used to report the changes.
* @param compilationUnit The type which contains the reported bad smell.
* @param result the result of an analysis run.
*/
abstract void refactor(ChangeListener listener, CtType<?> type, AnalyzerResult result);

/**
* Returns a list of all {@link BadSmell}s which are refactored by this refactoring.
* @return A list of all {@link BadSmell}s which are refactored by this refactoring. Never null.
*/
public abstract List<BadSmell> getHandledBadSmells();

/**
* Checks if the given {@link CtType} is the type which contains the reported bad smell.
* @param type The type which should be checked.
* @param resultPath The path of the file which contains the reported bad smell.
* @return True if the given {@link CtType} is the type which contains the reported bad smell.
*/
default boolean isSameType(CtType<?> type, Path resultPath) {
return type.getPosition().isValidPosition()
&& !(type instanceof CtTypeParameter)
&& type.getPosition()
.getCompilationUnit()
.getFile()
.toPath()
.toString()
.endsWith(resultPath.normalize().toString());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package xyz.keksdose.spoon.code_solver.analyzer.spoon;

import io.github.martinwitt.laughing_train.domain.entity.AnalyzerResult;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import spoon.Launcher;
import spoon.reflect.CtModel;
import spoon.reflect.declaration.CtType;

public class SpoonAnalyzer {

public List<AnalyzerResult> analyze(Path path) {
CtModel model = buildModel(path);
return model.getAllTypes().stream()
.flatMap(v -> analyzeType(v).stream())
.collect(Collectors.toList());
}

private CtModel buildModel(Path path) {
Launcher launcher = new Launcher();
launcher.addInputResource(path.toString());
launcher.getEnvironment().setAutoImports(true);
launcher.getEnvironment().setNoClasspath(true);
launcher.getEnvironment().setIgnoreDuplicateDeclarations(true);
launcher.getEnvironment().setShouldCompile(false);
return launcher.buildModel();
}

private List<SpoonAnalyzerResult> analyzeType(CtType<?> type) {
List<AbstractSpoonRuleAnalyzer> results = Arrays.asList(SpoonAnalyzerRules.values());
return results.stream().flatMap(v -> v.analyze(type).stream()).collect(Collectors.toList());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
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 java.util.Objects;

public class SpoonAnalyzerResult implements AnalyzerResult {

private static final String ANALYZER = "SpoonAnalyzer";
private final RuleId ruleId;
private final String filePath;
private final Position position;
private final String message;
private final String messageMarkdown;
private final String snippet;

public SpoonAnalyzerResult(
RuleId ruleId, String filePath, Position position, String message, String messageMarkdown, String snippet) {
this.ruleId = ruleId;
this.filePath = filePath;
this.position = position;
this.message = message;
this.messageMarkdown = messageMarkdown;
this.snippet = snippet;
}

@Override
public String getAnalyzer() {
return ANALYZER;
}

@Override
public RuleId ruleID() {
return ruleId;
}

@Override
public String filePath() {
return filePath;
}

@Override
public Position position() {
return position;
}

@Override
public String message() {
return message;
}

@Override
public String messageMarkdown() {
return messageMarkdown;
}

@Override
public String snippet() {
return snippet;
}

@Override
public int hashCode() {
return Objects.hash(ruleId, filePath, position, message, messageMarkdown, snippet);
}

@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof SpoonAnalyzerResult)) {
return false;
}
SpoonAnalyzerResult other = (SpoonAnalyzerResult) obj;
return Objects.equals(ruleId, other.ruleId)
&& Objects.equals(filePath, other.filePath)
&& Objects.equals(position, other.position)
&& Objects.equals(message, other.message)
&& Objects.equals(messageMarkdown, other.messageMarkdown)
&& Objects.equals(snippet, other.snippet);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
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.RuleId;
import java.util.List;
import spoon.reflect.declaration.CtType;
import xyz.keksdose.spoon.code_solver.analyzer.AnalyzerRule;
import xyz.keksdose.spoon.code_solver.analyzer.spoon.rules.UnnecessaryToStringCall;
import xyz.keksdose.spoon.code_solver.history.ChangeListener;
import xyz.keksdose.spoon.code_solver.transformations.BadSmell;

public enum SpoonAnalyzerRules implements AbstractSpoonRuleAnalyzer, AnalyzerRule {
UNNECESSARY_TOSTRING_CALL("UnnecessaryTostringCall", new UnnecessaryToStringCall());

private final RuleId ruleId;
private final AbstractSpoonRuleAnalyzer analyzer;

SpoonAnalyzerRules(String ruleId, AbstractSpoonRuleAnalyzer analyzer) {
this.ruleId = new RuleId(ruleId);
this.analyzer = analyzer;
}

@Override
public RuleId getRuleId() {
return ruleId;
}

List<BadSmell> getDescription() {
return analyzer.getHandledBadSmells();
}

@Override
public List<SpoonAnalyzerResult> analyze(CtType<?> type) {

return analyzer.analyze(type);
}

@Override
public void refactor(ChangeListener listener, CtType<?> type, AnalyzerResult result) {
analyzer.refactor(listener, type, result);
}

@Override
public List<BadSmell> getHandledBadSmells() {
return analyzer.getHandledBadSmells();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package xyz.keksdose.spoon.code_solver.analyzer.spoon;

import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import spoon.Launcher;
import spoon.reflect.declaration.CtType;
import spoon.support.compiler.VirtualFile;

public class TemplateHelper {

private TemplateHelper() {}

public static CtType<?> fromResource(String resource) {
ClassLoader classLoader = TemplateHelper.class.getClassLoader();
URL resourceUrl = classLoader.getResource(resource);
if (resourceUrl == null) {
throw new IllegalArgumentException("Resource not found: " + resource);
}
try {
String content = Files.readString(Path.of(resourceUrl.toURI()));
Launcher launcher = new Launcher();
VirtualFile file = new VirtualFile(content, "Test");
launcher.addInputResource(file);
var model = launcher.buildModel();
return model.getAllTypes().iterator().next();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package xyz.keksdose.spoon.code_solver.analyzer.spoon.rules;

import static xyz.keksdose.spoon.code_solver.history.MarkdownString.fromMarkdown;

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 java.util.ArrayList;
import java.util.List;
import spoon.reflect.code.CtReturn;
import spoon.reflect.declaration.CtElement;
import spoon.reflect.declaration.CtMethod;
import spoon.reflect.declaration.CtType;
import spoon.reflect.visitor.filter.TypeFilter;
import spoon.template.TemplateMatcher;
import xyz.keksdose.spoon.code_solver.analyzer.spoon.AbstractSpoonRuleAnalyzer;
import xyz.keksdose.spoon.code_solver.analyzer.spoon.SpoonAnalyzerResult;
import xyz.keksdose.spoon.code_solver.analyzer.spoon.TemplateHelper;
import xyz.keksdose.spoon.code_solver.history.ChangeListener;
import xyz.keksdose.spoon.code_solver.history.MarkdownString;
import xyz.keksdose.spoon.code_solver.transformations.BadSmell;

public class UnnecessaryToStringCall implements AbstractSpoonRuleAnalyzer {

private static final List<TemplateMatcher> templates = createPattern();
private static final BadSmell UNNECESSARY_TO_STRING_CALL = new BadSmell() {
@Override
public MarkdownString getName() {
return MarkdownString.fromRaw("UnnecessaryToStringCall");
}

@Override
public MarkdownString getDescription() {
return fromMarkdown(
"The `toString()` method is not needed in cases the underlying method handles the conversion. Also calling toString() on a String is redundant. Removing them simplifies the code.");
}
};

@Override
public List<BadSmell> getHandledBadSmells() {
throw new UnsupportedOperationException("Unimplemented method 'getHandledBadSmells'");
}

@Override
public List<SpoonAnalyzerResult> analyze(CtType<?> type) {
List<SpoonAnalyzerResult> results = new ArrayList<>();
List<CtElement> matches = new ArrayList<>();
for (TemplateMatcher templateMatcher : templates) {
templateMatcher.find(type).forEach(matches::add);
}
for (CtElement ctElement : matches) {
String filePath = ctElement.getPosition().getFile().getAbsolutePath();
Position position = createPosition(ctElement);
String snippet = ctElement.toString();
RuleId ruleId = new RuleId(UNNECESSARY_TO_STRING_CALL.getName().asText());
String message = UNNECESSARY_TO_STRING_CALL.getDescription().asText();
String messageMarkdown = UNNECESSARY_TO_STRING_CALL.getDescription().asMarkdown();
results.add(new SpoonAnalyzerResult(ruleId, filePath, position, message, messageMarkdown, snippet));
}
return results;
}

private Position createPosition(CtElement ctElement) {
int startLine = ctElement.getPosition().getLine();
int endLine = ctElement.getPosition().getEndLine();
int startColumn = ctElement.getPosition().getColumn();
int endColumn = ctElement.getPosition().getEndColumn();
int startOffset = ctElement.getPosition().getSourceStart();
int length = ctElement.getPosition().getSourceEnd() - startOffset;
Position position = new Position(startLine, endLine, startColumn, endColumn, startOffset, length);
return position;
}

private static List<TemplateMatcher> createPattern() {
List<TemplateMatcher> templates = new ArrayList<>();
var templateType = TemplateHelper.fromResource("patternDB/UnnecessaryToStringCall");
for (CtMethod<?> method : templateType.getMethods()) {
if (method.getSimpleName().startsWith("matcher")) {
var root = method.getElements(new TypeFilter<>(CtReturn.class))
.get(0)
.getReturnedExpression();
templates.add(new TemplateMatcher(root));
}
}
return templates;
}

@Override
public void refactor(ChangeListener listener, CtType<?> type, AnalyzerResult result) {
throw new UnsupportedOperationException("Unimplemented method 'refactor'");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package patternDB;

import java.util.Collection;

import spoon.template.Parameter;
import spoon.template.TemplateParameter;

public class UnnecessaryToStringCall {
// Step 1:
public TemplateParameter<Collection<?>> _col_;

public TemplateParameter<String> _str_;

@Parameter
String foo;


public String matcher1() {
return _str_.S().toString();
}

public String matcher2() {
return _str_.S() + toString();
}
}

Loading

0 comments on commit 66b9a08

Please sign in to comment.