diff --git a/pitest-entry/src/main/java/org/pitest/mutationtest/build/intercept/annotations/ExcludedAnnotationInterceptor.java b/pitest-entry/src/main/java/org/pitest/mutationtest/build/intercept/annotations/ExcludedAnnotationInterceptor.java index cffeb1c1a..7046427d6 100644 --- a/pitest-entry/src/main/java/org/pitest/mutationtest/build/intercept/annotations/ExcludedAnnotationInterceptor.java +++ b/pitest-entry/src/main/java/org/pitest/mutationtest/build/intercept/annotations/ExcludedAnnotationInterceptor.java @@ -1,7 +1,9 @@ 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.functional.FCollection; @@ -13,7 +15,11 @@ import java.util.Collection; import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedList; import java.util.List; +import java.util.Queue; +import java.util.Set; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -24,7 +30,6 @@ public class ExcludedAnnotationInterceptor implements MutationInterceptor { private boolean skipClass; private Predicate annotatedMethodMatcher; - ExcludedAnnotationInterceptor(List configuredAnnotations) { this.configuredAnnotations = configuredAnnotations; } @@ -39,17 +44,93 @@ public void begin(ClassTree clazz) { this.skipClass = clazz.annotations().stream() .anyMatch(avoidedAnnotation()); if (!this.skipClass) { - final List> methods = clazz.methods().stream() + // 1. Collect methods with avoided annotations or that override such methods + final List avoidedMethods = clazz.methods().stream() .filter(hasAvoidedAnnotation()) - .map(AnalysisFunctions.matchMutationsInMethod()) .collect(Collectors.toList()); - this.annotatedMethodMatcher = Prelude.or(methods); + + // Collect method names along with descriptors to handle overloaded methods + final Set avoidedMethodSignatures = avoidedMethods.stream() + .map(method -> new MethodSignature(method.rawNode().name, method.rawNode().desc)) + .collect(Collectors.toSet()); + + // Keep track of processed methods to avoid infinite loops + Set processedMethods = new HashSet<>(avoidedMethodSignatures); + + // 2. For each avoided method, collect lambda methods recursively + for (MethodTree avoidedMethod : avoidedMethods) { + collectLambdaMethods(avoidedMethod, clazz, avoidedMethodSignatures, processedMethods); + } + + // 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); + }; } } + /** + * Recursively collects lambda methods defined within the given method. + * + * @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 void collectLambdaMethods(MethodTree method, ClassTree clazz, + Set avoidedMethodSignatures, + Set processedMethods) { + Queue 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); + } + } + } + } + } + } + } + } + } + + 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); + } + + /** + * Creates a predicate that checks if a method has an avoided annotation. + * + * @return A predicate that returns true if the method should be avoided. + */ private Predicate hasAvoidedAnnotation() { - return a -> a.annotations().stream() - .anyMatch(avoidedAnnotation()); + return methodTree -> + methodTree.annotations().stream().anyMatch(avoidedAnnotation()); } private Predicate avoidedAnnotation() { @@ -81,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(); + } + } } diff --git a/pitest-entry/src/test/java/org/pitest/mutationtest/build/intercept/annotations/ExcludedAnnotationInterceptorTest.java b/pitest-entry/src/test/java/org/pitest/mutationtest/build/intercept/annotations/ExcludedAnnotationInterceptorTest.java index 8faa0ebc9..0879274ef 100644 --- a/pitest-entry/src/test/java/org/pitest/mutationtest/build/intercept/annotations/ExcludedAnnotationInterceptorTest.java +++ b/pitest-entry/src/test/java/org/pitest/mutationtest/build/intercept/annotations/ExcludedAnnotationInterceptorTest.java @@ -67,6 +67,69 @@ public void shouldFilterMethodsWithGeneratedAnnotation() { assertThat(actual.iterator().next().getId().getLocation().getMethodName()).isEqualTo("bar"); } + @Test + public void shouldNotFilterMutationsInUnannotatedMethod() { + final Collection mutations = mutator.findMutations(ClassName.fromClass(UnannotatedMethodClass.class)); + final Collection actual = runWithTestee(mutations, UnannotatedMethodClass.class); + assertThat(actual).containsExactlyElementsOf(mutations); + } + + @Test + public void shouldFilterMutationsInAnnotatedMethod() { + final Collection mutations = mutator.findMutations(ClassName.fromClass(AnnotatedMethodClass.class)); + final Collection actual = runWithTestee(mutations, AnnotatedMethodClass.class); + assertThat(actual).isEmpty(); + } + + @Test + public void shouldNotFilterMutationsInLambdaWithinUnannotatedMethod() { + final Collection mutations = mutator.findMutations(ClassName.fromClass(LambdaInUnannotatedMethodClass.class)); + final Collection actual = runWithTestee(mutations, LambdaInUnannotatedMethodClass.class); + assertThat(actual).containsExactlyElementsOf(mutations); + } + + @Test + public void shouldFilterMutationsInLambdaWithinAnnotatedMethod() { + final Collection mutations = mutator.findMutations(ClassName.fromClass(LambdaInAnnotatedMethodClass.class)); + final Collection actual = runWithTestee(mutations, LambdaInAnnotatedMethodClass.class); + assertThat(actual).isEmpty(); + } + + @Test + public void shouldHandleOverloadedMethodsWithLambdas() { + final List mutations = this.mutator.findMutations(ClassName.fromClass(OverloadedMethods.class)); + final Collection actual = runWithTestee(mutations, OverloadedMethods.class); + + // Expect mutations from unannotated methods and their lambdas + assertThat(actual).hasSize(3); // bar, foo(int x), and its lambda + for (MutationDetails mutationDetails : actual) { + assertThat(mutationDetails.getId().getLocation().getMethodName()) + .isIn("bar", "foo", "lambda$foo$0"); + } + } + + @Test + public void shouldNotFilterMutationsInNestedLambdaWithinUnannotatedOverloadedMethod() { + final Collection mutations = mutator.findMutations(ClassName.fromClass(NestedLambdaInOverloadedMethods.class)); + final Collection actual = runWithTestee(mutations, NestedLambdaInOverloadedMethods.class); + + // Should include mutations from the unannotated method and its nested lambdas + assertThat(actual).anyMatch(mutation -> mutation.getId().getLocation().getMethodName().equals("baz")); + assertThat(actual).anyMatch(mutation -> { + String methodName = mutation.getId().getLocation().getMethodName(); + return methodName.startsWith("lambda$baz$") || methodName.startsWith("lambda$null$"); + }); + } + + @Test + public void shouldFilterMutationsInNestedLambdaWithinAnnotatedOverloadedMethod() { + final Collection mutations = mutator.findMutations(ClassName.fromClass(NestedLambdaInOverloadedMethods.class)); + final Collection actual = runWithTestee(mutations, NestedLambdaInOverloadedMethods.class); + + // Should not include mutations from the annotated method and its nested lambdas + assertThat(actual).noneMatch(mutation -> mutation.getId().getLocation().getMethodDesc().equals("(Ljava/lang/String;)V")); + } + private Collection runWithTestee( Collection input, Class clazz) { this.testee.begin(treeFor(clazz)); @@ -82,7 +145,6 @@ ClassTree treeFor(Class clazz) { return ClassTree.fromBytes(source.getBytes(clazz.getName()).get()); } - } class UnAnnotated { @@ -120,4 +182,62 @@ public void bar() { } +class UnannotatedMethodClass { + public void unannotatedMethod() { + System.out.println("This method is not annotated."); + } +} +class AnnotatedMethodClass { + @TestGeneratedAnnotation + public void annotatedMethod() { + System.out.println("This method is annotated."); + } +} + +class LambdaInUnannotatedMethodClass { + public void methodWithLambda() { + Runnable runnable = () -> System.out.println("Lambda inside unannotated method."); + } +} + +class LambdaInAnnotatedMethodClass { + @TestGeneratedAnnotation + public void methodWithLambda() { + Runnable runnable = () -> System.out.println("Lambda inside annotated method."); + } +} + +class OverloadedMethods { + public void foo(int x) { + System.out.println("mutate me"); + Runnable r = () -> System.out.println("Lambda in unannotated overloaded method with int"); + } + + @TestGeneratedAnnotation + public void foo(String x) { + System.out.println("don't mutate me"); + Runnable r = () -> System.out.println("Lambda in annotated overloaded method with String"); + } + + public void bar() { + System.out.println("mutate me"); + } +} + +class NestedLambdaInOverloadedMethods { + public void baz(int x) { + System.out.println("mutate me"); + Runnable outerLambda = () -> { + Runnable innerLambda = () -> System.out.println("Nested lambda in unannotated overloaded method with int"); + }; + } + + @TestGeneratedAnnotation + public void baz(String x) { + System.out.println("don't mutate me"); + Runnable outerLambda = () -> { + Runnable innerLambda = () -> System.out.println("Nested lambda in annotated overloaded method with String"); + }; + } +}