Skip to content

Commit

Permalink
update for overloaded methods
Browse files Browse the repository at this point in the history
Signed-off-by: see-quick <[email protected]>
  • Loading branch information
see-quick committed Nov 9, 2024
1 parent 66a4bb1 commit d19cd6b
Show file tree
Hide file tree
Showing 2 changed files with 133 additions and 198 deletions.
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
package org.pitest.mutationtest.build.intercept.annotations;

import org.objectweb.asm.Handle;
import org.objectweb.asm.tree.AbstractInsnNode;
import org.objectweb.asm.tree.AnnotationNode;
import org.pitest.bytecode.analysis.AnalysisFunctions;
import org.objectweb.asm.tree.InvokeDynamicInsnNode;
import org.pitest.bytecode.analysis.ClassTree;
import org.pitest.bytecode.analysis.MethodTree;
import org.pitest.classpath.ClassloaderByteArraySource;
import org.pitest.functional.FCollection;
import org.pitest.functional.prelude.Prelude;
import org.pitest.mutationtest.build.InterceptorType;
Expand All @@ -14,12 +15,13 @@

import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
import java.util.Queue;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class ExcludedAnnotationInterceptor implements MutationInterceptor {

Expand All @@ -44,115 +46,91 @@ public void begin(ClassTree clazz) {
if (!this.skipClass) {
// 1. Collect methods with avoided annotations or that override such methods
final List<MethodTree> avoidedMethods = clazz.methods().stream()
.filter(hasAvoidedAnnotationOrOverridesMethodWithAvoidedAnnotation(clazz))
.filter(hasAvoidedAnnotation())
.collect(Collectors.toList());

final Set<String> avoidedMethodNames = avoidedMethods.stream()
.map(method -> method.rawNode().name)
// Collect method names along with descriptors to handle overloaded methods
final Set<MethodSignature> avoidedMethodSignatures = avoidedMethods.stream()
.map(method -> new MethodSignature(method.rawNode().name, method.rawNode().desc))
.collect(Collectors.toSet());

// 2. Collect lambda methods with being inside avoided methods
final List<MethodTree> lambdaMethods = clazz.methods().stream()
.filter(MethodTree::isGeneratedLambdaMethod)
.filter(lambdaMethod -> {
String lambdaName = lambdaMethod.rawNode().name; // e.g., lambda$fooWithLambdas$0
String enclosingMethodName = extractEnclosingMethodName(lambdaName);
// Keep track of processed methods to avoid infinite loops
Set<MethodSignature> processedMethods = new HashSet<>(avoidedMethodSignatures);

return avoidedMethodNames.contains(enclosingMethodName);
})
.collect(Collectors.toList());

// 3. Merge the two lists into a single list and cast MethodTree to Predicate<MutationDetails>
final List<Predicate<MutationDetails>> mutationPredicates = Stream.concat(avoidedMethods.stream(), lambdaMethods.stream())
.map(AnalysisFunctions.matchMutationsInMethod())
.collect(Collectors.toList());
// 2. For each avoided method, collect lambda methods recursively
for (MethodTree avoidedMethod : avoidedMethods) {
collectLambdaMethods(avoidedMethod, clazz, avoidedMethodSignatures, processedMethods);
}

this.annotatedMethodMatcher = Prelude.or(mutationPredicates);
// 3. Create a predicate to match mutations in methods to avoid
this.annotatedMethodMatcher = mutation -> {
MethodSignature mutationSignature = new MethodSignature(
mutation.getMethod(), mutation.getId().getLocation().getMethodDesc());
return avoidedMethodSignatures.contains(mutationSignature);
};
}
}

/**
* TODO: maybe move to MethodTree class?? WDYT?
* Extracts the enclosing method name from a lambda method's name.
* Assumes lambda methods follow the naming convention: lambda$enclosingMethodName$number
* Recursively collects lambda methods defined within the given method.
*
* @param lambdaName The name of the lambda method (e.g., "lambda$fooWithLambdas$0")
* @return The name of the enclosing method (e.g., "fooWithLambdas")
* @param method The method to inspect for lambdas.
* @param clazz The class containing the methods.
* @param avoidedMethodSignatures The set of method signatures to avoid.
* @param processedMethods The set of already processed methods to prevent infinite loops.
*/
private String extractEnclosingMethodName(String lambdaName) {
int firstDollar = lambdaName.indexOf('$');
int secondDollar = lambdaName.indexOf('$', firstDollar + 1);

if (firstDollar != -1 && secondDollar != -1) {
return lambdaName.substring(firstDollar + 1, secondDollar);
private void collectLambdaMethods(MethodTree method, ClassTree clazz,
Set<MethodSignature> avoidedMethodSignatures,
Set<MethodSignature> processedMethods) {
Queue<MethodTree> methodsToProcess = new LinkedList<>();
methodsToProcess.add(method);

while (!methodsToProcess.isEmpty()) {
MethodTree currentMethod = methodsToProcess.poll();

for (AbstractInsnNode insn : currentMethod.rawNode().instructions) {
if (insn instanceof InvokeDynamicInsnNode) {
InvokeDynamicInsnNode indy = (InvokeDynamicInsnNode) insn;

for (Object bsmArg : indy.bsmArgs) {
if (bsmArg instanceof Handle) {
Handle handle = (Handle) bsmArg;
// Check if the method is in the same class and is a lambda method
if (handle.getOwner().equals(clazz.rawNode().name) && handle.getName().startsWith("lambda$")) {
MethodSignature lambdaMethodSignature = new MethodSignature(handle.getName(), handle.getDesc());
if (!avoidedMethodSignatures.contains(lambdaMethodSignature)
&& !processedMethods.contains(lambdaMethodSignature)) {
avoidedMethodSignatures.add(lambdaMethodSignature);
processedMethods.add(lambdaMethodSignature);
// Find the MethodTree for this lambda method
MethodTree lambdaMethod = findMethodTree(clazz, handle.getName(), handle.getDesc());
if (lambdaMethod != null) {
methodsToProcess.add(lambdaMethod);
}
}
}
}
}
}
}
}
return lambdaName;
}

/**
* Creates a predicate that checks if a method has an avoided annotation or overrides a method
* in its superclass hierarchy that has an avoided annotation.
*
* @param clazz The class tree of the current class.
* @return A predicate that returns true if the method should be avoided.
*/
private Predicate<MethodTree> hasAvoidedAnnotationOrOverridesMethodWithAvoidedAnnotation(ClassTree clazz) {
return methodTree ->
methodTree.annotations().stream().anyMatch(avoidedAnnotation())
|| isOverridingMethodWithAvoidedAnnotation(methodTree, clazz);
}

/**
* Checks if the given method overrides a method in its superclass hierarchy that has an avoided annotation.
*
* @param method The method to check.
* @param clazz The class tree of the current class.
* @return True if the method overrides an annotated method; false otherwise.
*/
private boolean isOverridingMethodWithAvoidedAnnotation(MethodTree method, ClassTree clazz) {
String methodName = method.rawNode().name;
String methodDesc = method.rawNode().desc;
return isMethodInSuperClassWithAvoidedAnnotation(methodName, methodDesc, clazz);
private MethodTree findMethodTree(ClassTree clazz, String methodName, String methodDesc) {
return clazz.methods().stream()
.filter(m -> m.rawNode().name.equals(methodName) && m.rawNode().desc.equals(methodDesc))
.findFirst()
.orElse(null);
}

/**
* Recursively checks if a method with the given name and descriptor exists in the superclass hierarchy
* and has an avoided annotation.
* Creates a predicate that checks if a method has an avoided annotation.
*
* @param methodName The name of the method to search for.
* @param methodDesc The descriptor of the method to search for.
* @param clazz The class tree of the current class or superclass.
* @return True if an annotated method is found in the superclass hierarchy; false otherwise.
* @return A predicate that returns true if the method should be avoided.
*/
private boolean isMethodInSuperClassWithAvoidedAnnotation(String methodName, String methodDesc, ClassTree clazz) {
String superClassName = clazz.rawNode().superName;
if (superClassName == null || superClassName.equals("java/lang/Object")) {
return false;
}

ClassloaderByteArraySource source = ClassloaderByteArraySource.fromContext();
Optional<byte[]> superClassBytes = source.getBytes(superClassName.replace('/', '.'));
if (!superClassBytes.isPresent()) {
return false;
}

ClassTree superClassTree = ClassTree.fromBytes(superClassBytes.get());

Optional<MethodTree> superMethod = superClassTree.methods().stream()
.filter(m -> m.rawNode().name.equals(methodName) && m.rawNode().desc.equals(methodDesc))
.findFirst();

if (superMethod.isPresent()) {
if (superMethod.get().annotations().stream().anyMatch(avoidedAnnotation())) {
return true;
} else {
// continue recursion to check superclass chain
return isMethodInSuperClassWithAvoidedAnnotation(methodName, methodDesc, superClassTree);
}
} else {
// method not found in this superclass, continue searching up the hierarchy
return false;
}
private Predicate<MethodTree> hasAvoidedAnnotation() {
return methodTree ->
methodTree.annotations().stream().anyMatch(avoidedAnnotation());
}

private Predicate<AnnotationNode> avoidedAnnotation() {
Expand Down Expand Up @@ -184,4 +162,34 @@ boolean shouldAvoid(String desc) {
return false;
}

/**
* Represents a method signature with its name and descriptor.
* Used to uniquely identify methods, especially overloaded ones.
*/
private static class MethodSignature {
private final String name;
private final String desc;

MethodSignature(String name, String desc) {
this.name = name;
this.desc = desc;
}

@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
MethodSignature that = (MethodSignature) obj;
return name.equals(that.name) && desc.equals(that.desc);
}

@Override
public int hashCode() {
return name.hashCode() * 31 + desc.hashCode();
}
}
}
Loading

0 comments on commit d19cd6b

Please sign in to comment.