-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: ✨ Add pattern based spoon analyzer (#719)
- Loading branch information
1 parent
969160b
commit 66b9a08
Showing
12 changed files
with
651 additions
and
1 deletion.
There are no files selected for viewing
45 changes: 45 additions & 0 deletions
45
...rc/main/java/xyz/keksdose/spoon/code_solver/analyzer/spoon/AbstractSpoonRuleAnalyzer.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()); | ||
} | ||
} |
35 changes: 35 additions & 0 deletions
35
...sformation/src/main/java/xyz/keksdose/spoon/code_solver/analyzer/spoon/SpoonAnalyzer.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()); | ||
} | ||
} |
84 changes: 84 additions & 0 deletions
84
...tion/src/main/java/xyz/keksdose/spoon/code_solver/analyzer/spoon/SpoonAnalyzerResult.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
47 changes: 47 additions & 0 deletions
47
...ation/src/main/java/xyz/keksdose/spoon/code_solver/analyzer/spoon/SpoonAnalyzerRules.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} | ||
} |
31 changes: 31 additions & 0 deletions
31
...formation/src/main/java/xyz/keksdose/spoon/code_solver/analyzer/spoon/TemplateHelper.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} | ||
} |
92 changes: 92 additions & 0 deletions
92
...ain/java/xyz/keksdose/spoon/code_solver/analyzer/spoon/rules/UnnecessaryToStringCall.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'"); | ||
} | ||
} |
26 changes: 26 additions & 0 deletions
26
code-transformation/src/main/resources/patternDB/UnnecessaryToStringCall
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} | ||
} | ||
|
Oops, something went wrong.