diff --git a/matcher/build.gradle b/matcher/build.gradle new file mode 100644 index 000000000..241b0df24 --- /dev/null +++ b/matcher/build.gradle @@ -0,0 +1,4 @@ + +plugins { + id 'xyz.keksdose.spoon.code_solver.java-common-conventions' +} \ No newline at end of file diff --git a/matcher/src/main/java/io/github/martinwitt/laughing_train/spoonutils/ConstructorMatcher.java b/matcher/src/main/java/io/github/martinwitt/laughing_train/spoonutils/ConstructorMatcher.java new file mode 100644 index 000000000..a9c1be744 --- /dev/null +++ b/matcher/src/main/java/io/github/martinwitt/laughing_train/spoonutils/ConstructorMatcher.java @@ -0,0 +1,60 @@ +package io.github.martinwitt.laughing_train.spoonutils; + +import java.util.ArrayList; +import java.util.List; +import org.apache.commons.lang3.tuple.Pair; +import spoon.reflect.code.CtConstructorCall; +import spoon.reflect.code.CtExpression; +import spoon.reflect.reference.CtTypeReference; +import spoon.reflect.visitor.Filter; + +/** + * A filter for matching constructor calls with a specific target type and argument types. + */ +public class ConstructorMatcher implements Filter> { + + private final String fqTargetType; + private final String[] argsFQN; + + /** + * Creates a new constructor matcher with the given target type and argument types. + * + * @param fqTargetType the fully-qualified name of the target type + * @param argsFQN the fully-qualified names of the argument types + */ + public ConstructorMatcher(String fqTargetType, String... argsFQN) { + this.fqTargetType = fqTargetType; + this.argsFQN = argsFQN; + } + + /** + * Determines whether the given constructor call matches the target type and argument types. + * + * @param element the constructor call to match + * @return true if the constructor call matches the target type and argument types, false otherwise + */ + @Override + public boolean matches(CtConstructorCall element) { + if (element == null) { + return false; + } + + if (!element.getType().getQualifiedName().equals(fqTargetType)) { + return false; + } + if (argsFQN == null || argsFQN.length == 0) { + return true; + } + if (element.getArguments().size() != argsFQN.length) { + return false; + } + + List, CtExpression>> zipped = new ArrayList<>(); + for (int i = 0; i < argsFQN.length && i < element.getArguments().size(); i++) { + zipped.add(Pair.of( + element.getFactory().createReference(argsFQN[i]), + element.getArguments().get(i))); + } + return zipped.stream().allMatch(pair -> pair.getRight().getType().isSubtypeOf(pair.getLeft())); + } +} diff --git a/matcher/src/main/java/io/github/martinwitt/laughing_train/spoonutils/InvocationMatcher.java b/matcher/src/main/java/io/github/martinwitt/laughing_train/spoonutils/InvocationMatcher.java new file mode 100644 index 000000000..e5f81a81b --- /dev/null +++ b/matcher/src/main/java/io/github/martinwitt/laughing_train/spoonutils/InvocationMatcher.java @@ -0,0 +1,82 @@ +package io.github.martinwitt.laughing_train.spoonutils; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import org.apache.commons.lang3.tuple.Pair; +import spoon.reflect.code.CtExpression; +import spoon.reflect.code.CtInvocation; +import spoon.reflect.code.CtTypeAccess; +import spoon.reflect.reference.CtTypeReference; +import spoon.reflect.visitor.Filter; + +/** + * A matcher that checks if a given {@link CtInvocation} object matches a specified target type, method name, and argument types. + */ +public class InvocationMatcher implements Filter> { + private final String fqTargetType; + private final String methodName; + private final String[] argsFQN; + + /** + * Creates a new {@link InvocationMatcher} object with the specified target type, method name, and argument types. + * @param fqTargetType the fully qualified name of the target type + * @param methodName the name of the method + * @param argsFQN the fully qualified names of the argument types + */ + public InvocationMatcher(String fqTargetType, String methodName, String... argsFQN) { + this.fqTargetType = fqTargetType; + this.methodName = methodName; + this.argsFQN = argsFQN; + } + + /** + * Checks if the specified {@link CtInvocation} object matches the target type, method name, and argument types of this {@link InvocationMatcher}. + * @param element the {@link CtInvocation} object to check + * @return true if the invocation matches, false otherwise + */ + public boolean matches(CtInvocation element) { + if (element == null) { + return false; + } + + // Check if the target type matches + CtExpression target = element.getTarget(); + if (target == null || target.getType() == null) { + return false; + } + if (target instanceof CtTypeAccess access) { + if (!access.getAccessedType().getQualifiedName().equals(fqTargetType)) { + return false; + } + } else { + if (!target.getType().getQualifiedName().equals(fqTargetType)) { + return false; + } + } + + // Check if the method name matches + if (Optional.ofNullable(element.getExecutable()) + .map(v -> v.getExecutableDeclaration()) + .filter(v -> v.getSimpleName().equals(methodName)) + .isEmpty()) { + return false; + } + + // Check if the argument types match + if (argsFQN == null || argsFQN.length == 0) { + return true; + } + if (element.getArguments().size() != argsFQN.length) { + return false; + } + + List, CtExpression>> zipped = new ArrayList<>(); + for (int i = 0; i < argsFQN.length && i < element.getArguments().size(); i++) { + zipped.add(Pair.of( + element.getFactory().createReference(argsFQN[i]), + element.getArguments().get(i))); + } + return zipped.stream().allMatch(pair -> pair.getRight().getType().isSubtypeOf(pair.getLeft())); + } +} diff --git a/matcher/src/main/java/io/github/martinwitt/laughing_train/spoonutils/matcher/Matcher.java b/matcher/src/main/java/io/github/martinwitt/laughing_train/spoonutils/matcher/Matcher.java new file mode 100644 index 000000000..e21fe1015 --- /dev/null +++ b/matcher/src/main/java/io/github/martinwitt/laughing_train/spoonutils/matcher/Matcher.java @@ -0,0 +1,18 @@ +package io.github.martinwitt.laughing_train.spoonutils.matcher; + +/** + * A functional interface for matching elements of a certain type. + * + * @param the type of elements to match + */ +@FunctionalInterface +public interface Matcher { + + /** + * Determines whether the given element matches a certain criteria. + * + * @param element the element to match + * @return true if the element matches the criteria, false otherwise + */ + boolean matches(T element); +} diff --git a/matcher/src/main/java/io/github/martinwitt/laughing_train/spoonutils/matcher/Matchers.java b/matcher/src/main/java/io/github/martinwitt/laughing_train/spoonutils/matcher/Matchers.java new file mode 100644 index 000000000..148d389b0 --- /dev/null +++ b/matcher/src/main/java/io/github/martinwitt/laughing_train/spoonutils/matcher/Matchers.java @@ -0,0 +1,84 @@ +package io.github.martinwitt.laughing_train.spoonutils.matcher; + +import spoon.reflect.code.CtExpression; +import spoon.reflect.code.CtLiteral; +import spoon.reflect.declaration.CtModifiable; +import spoon.reflect.declaration.CtType; +import spoon.reflect.declaration.ModifierKind; + +/** + * A utility class for creating matchers for Spoon elements. + */ +public final class Matchers { + + /** + * Returns a matcher that matches elements that are public. + * + * @return a matcher that matches elements that are public + */ + public static Matcher isPublic() { + return v -> v.getModifiers().contains(ModifierKind.PUBLIC); + } + + /** + * Returns a matcher that matches elements that are private. + * + * @return a matcher that matches elements that are private + */ + public static Matcher isPrivate() { + return v -> v.getModifiers().contains(ModifierKind.PRIVATE); + } + + /** + * Returns a matcher that matches elements that are enums. + * + * @return a matcher that matches elements that are enums + */ + public static Matcher> isEnum() { + return v -> v.isEnum(); + } + + /** + * Returns a matcher that matches elements that are integer literals with the given value. + * + * @param literal the value of the integer literal to match + * @return a matcher that matches elements that are integer literals with the given value + */ + public static Matcher> isLiteral(int literal) { + return v -> v instanceof CtLiteral + && ((CtLiteral) v).getValue() instanceof Integer value + && value.equals(literal); + } + + /** + * Returns a matcher that matches elements that are final. + * + * @return a matcher that matches elements that are final + */ + public static Matcher isFinal() { + return v -> v.getModifiers().contains(ModifierKind.FINAL); + } + + /** + * Returns a matcher that matches elements that match all of the given matchers. + * + * @param matchers the matchers to match + * @param the type of elements to match + * @return a matcher that matches elements that match all of the given matchers + */ + @SafeVarargs + public static Matcher allOf(Matcher... matchers) { + return v -> { + for (Matcher matcher : matchers) { + if (!matcher.matches(v)) { + return false; + } + } + return true; + }; + } + + private Matchers() { + throw new AssertionError("Utility class should not be instantiated"); + } +} diff --git a/settings.gradle b/settings.gradle index b2216aea9..6569ddd21 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,2 +1,2 @@ rootProject.name = 'laughing-train-project' -include(':code-transformation',":commons", ":github-bot", ":application") \ No newline at end of file +include(':code-transformation',":commons", ":github-bot", ":application", ":matcher") \ No newline at end of file