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(matcher): Add new matchers for spoon elements #804

Merged
merged 3 commits into from
Jul 8, 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
4 changes: 4 additions & 0 deletions matcher/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@

plugins {
id 'xyz.keksdose.spoon.code_solver.java-common-conventions'
}
Original file line number Diff line number Diff line change
@@ -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<CtConstructorCall<?>> {

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<Pair<CtTypeReference<?>, 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()));
}
}
Original file line number Diff line number Diff line change
@@ -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<CtInvocation<?>> {
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<Pair<CtTypeReference<?>, 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()));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package io.github.martinwitt.laughing_train.spoonutils.matcher;

/**
* A functional interface for matching elements of a certain type.
*
* @param <T> the type of elements to match
*/
@FunctionalInterface
public interface Matcher<T> {

/**
* 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);
}
Original file line number Diff line number Diff line change
@@ -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<CtModifiable> 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<CtModifiable> 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<CtType<?>> 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<CtExpression<?>> 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<CtModifiable> 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 <T> the type of elements to match
* @return a matcher that matches elements that match all of the given matchers
*/
@SafeVarargs
public static <T> Matcher<T> allOf(Matcher<T>... matchers) {
return v -> {
for (Matcher<T> matcher : matchers) {
if (!matcher.matches(v)) {
return false;
}
}
return true;
};
}

private Matchers() {
throw new AssertionError("Utility class should not be instantiated");
}
}
2 changes: 1 addition & 1 deletion settings.gradle
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
rootProject.name = 'laughing-train-project'
include(':code-transformation',":commons", ":github-bot", ":application")
include(':code-transformation',":commons", ":github-bot", ":application", ":matcher")