From 1af21b67f58ec8f6ba8ca28cc541dcc9c5d244c6 Mon Sep 17 00:00:00 2001 From: see-quick Date: Wed, 6 Nov 2024 12:41:31 +0100 Subject: [PATCH 1/6] Mutation coverage fix within lambdas Signed-off-by: see-quick --- .../ExcludedAnnotationInterceptor.java | 7 ++- .../ExcludedAnnotationInterceptorTest.java | 58 ++++++++++++++++++- 2 files changed, 61 insertions(+), 4 deletions(-) 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..09bf358f5 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 @@ -24,7 +24,6 @@ public class ExcludedAnnotationInterceptor implements MutationInterceptor { private boolean skipClass; private Predicate annotatedMethodMatcher; - ExcludedAnnotationInterceptor(List configuredAnnotations) { this.configuredAnnotations = configuredAnnotations; } @@ -43,13 +42,15 @@ public void begin(ClassTree clazz) { .filter(hasAvoidedAnnotation()) .map(AnalysisFunctions.matchMutationsInMethod()) .collect(Collectors.toList()); + this.annotatedMethodMatcher = Prelude.or(methods); } } private Predicate hasAvoidedAnnotation() { - return a -> a.annotations().stream() - .anyMatch(avoidedAnnotation()); + return methodTree -> + // count also lambda generated methods + methodTree.isGeneratedLambdaMethod() || methodTree.annotations().stream().anyMatch(avoidedAnnotation()); } private Predicate avoidedAnnotation() { 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..1dbdbdd11 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,22 @@ public void shouldFilterMethodsWithGeneratedAnnotation() { assertThat(actual.iterator().next().getId().getLocation().getMethodName()).isEqualTo("bar"); } + @Test + public void shouldFilterMethodsWithGeneratedAnnotationAndLambdasInside() { + final List mutations = this.mutator.findMutations(ClassName.fromClass(ClassAnnotatedWithGeneratedWithLambdas.class)); + final Collection actual = runWithTestee(mutations, ClassAnnotatedWithGeneratedWithLambdas.class); + assertThat(actual).hasSize(1); + assertThat(actual.iterator().next().getId().getLocation().getMethodName()).isEqualTo("bar"); + } + + @Test + public void shouldHandleOverloadedMethods() { + final List mutations = this.mutator.findMutations(ClassName.fromClass(OverloadedMethods.class)); + final Collection actual = runWithTestee(mutations, OverloadedMethods.class); + // Assume only one overloaded version is annotated + assertThat(actual).hasSize(2); // Assuming three methods: two overloaded (one annotated) and one regular + } + private Collection runWithTestee( Collection input, Class clazz) { this.testee.begin(treeFor(clazz)); @@ -82,7 +98,6 @@ ClassTree treeFor(Class clazz) { return ClassTree.fromBytes(source.getBytes(clazz.getName()).get()); } - } class UnAnnotated { @@ -120,4 +135,45 @@ public void bar() { } +class OverloadedMethods { + public void foo(int x) { + System.out.println("mutate me"); + } + + @TestGeneratedAnnotation + public void foo(String x) { + System.out.println("don't mutate me"); + } + + public void bar() { + System.out.println("mutate me"); + } +} + + +class ClassAnnotatedWithGeneratedWithLambdas { + + @TestGeneratedAnnotation + public void foo() { + System.out.println("don't mutate me"); + } + + public void bar() { + System.out.println("mutate me"); + } + + @TestGeneratedAnnotation + public void fooWithLambdas() { + System.out.println("don't mutate me"); + + Runnable runnable = () -> { + System.out.println("don't mutate me also in lambdas"); + + Runnable anotherOne = () -> { + System.out.println("don't mutate me also recursive lambdas"); + }; + }; + } +} + From 2abe4fc7182ae45768ec9b4029829a41a3e0261e Mon Sep 17 00:00:00 2001 From: see-quick Date: Wed, 6 Nov 2024 12:42:24 +0100 Subject: [PATCH 2/6] change a bit Signed-off-by: see-quick --- .../intercept/annotations/ExcludedAnnotationInterceptor.java | 1 - 1 file changed, 1 deletion(-) 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 09bf358f5..2d33ea16e 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 @@ -42,7 +42,6 @@ public void begin(ClassTree clazz) { .filter(hasAvoidedAnnotation()) .map(AnalysisFunctions.matchMutationsInMethod()) .collect(Collectors.toList()); - this.annotatedMethodMatcher = Prelude.or(methods); } } From 726c2cfbb96650890017535a3f5b94237cb4eef7 Mon Sep 17 00:00:00 2001 From: see-quick Date: Wed, 6 Nov 2024 15:52:24 +0100 Subject: [PATCH 3/6] update Signed-off-by: see-quick --- .../ExcludedAnnotationInterceptor.java | 48 +++++++++++++++++-- .../ExcludedAnnotationInterceptorTest.java | 28 ++++++----- 2 files changed, 61 insertions(+), 15 deletions(-) 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 2d33ea16e..2992cba58 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 @@ -14,8 +14,10 @@ import java.util.Collection; import java.util.Collections; import java.util.List; +import java.util.Set; import java.util.function.Predicate; import java.util.stream.Collectors; +import java.util.stream.Stream; public class ExcludedAnnotationInterceptor implements MutationInterceptor { @@ -38,18 +40,56 @@ 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 + final List avoidedMethods = clazz.methods().stream() .filter(hasAvoidedAnnotation()) + .collect(Collectors.toList()); + + final Set avoidedMethodNames = avoidedMethods.stream() + .map(method -> method.rawNode().name) + .collect(Collectors.toSet()); + + // 2. Collect lambda methods with being inside avoided methods + final List lambdaMethods = clazz.methods().stream() + .filter(MethodTree::isGeneratedLambdaMethod) + .filter(lambdaMethod -> { + String lambdaName = lambdaMethod.rawNode().name; // e.g., lambda$fooWithLambdas$0 + String enclosingMethodName = extractEnclosingMethodName(lambdaName); + + return avoidedMethodNames.contains(enclosingMethodName); + }) + .collect(Collectors.toList()); + + // 3. Merge the two lists into a single list and cast MethodTree to Predicate + final List> mutationPredicates = Stream.concat(avoidedMethods.stream(), lambdaMethods.stream()) .map(AnalysisFunctions.matchMutationsInMethod()) .collect(Collectors.toList()); - this.annotatedMethodMatcher = Prelude.or(methods); + + this.annotatedMethodMatcher = Prelude.or(mutationPredicates); + } + } + + /** + * 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 + * + * @param lambdaName The name of the lambda method (e.g., "lambda$fooWithLambdas$0") + * @return The name of the enclosing method (e.g., "fooWithLambdas") + */ + 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); } + return lambdaName; } private Predicate hasAvoidedAnnotation() { return methodTree -> - // count also lambda generated methods - methodTree.isGeneratedLambdaMethod() || methodTree.annotations().stream().anyMatch(avoidedAnnotation()); + methodTree.annotations().stream().anyMatch(avoidedAnnotation()); } private Predicate avoidedAnnotation() { 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 1dbdbdd11..3d449fecb 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 @@ -71,8 +71,11 @@ public void shouldFilterMethodsWithGeneratedAnnotation() { public void shouldFilterMethodsWithGeneratedAnnotationAndLambdasInside() { final List mutations = this.mutator.findMutations(ClassName.fromClass(ClassAnnotatedWithGeneratedWithLambdas.class)); final Collection actual = runWithTestee(mutations, ClassAnnotatedWithGeneratedWithLambdas.class); - assertThat(actual).hasSize(1); - assertThat(actual.iterator().next().getId().getLocation().getMethodName()).isEqualTo("bar"); + assertThat(actual).hasSize(3); + + for (MutationDetails mutationDetails : actual) { + assertThat(mutationDetails.getId().getLocation().getMethodName()).isIn("barWithLambdas", "lambda$barWithLambdas$2", "lambda$barWithLambdas$3"); + } } @Test @@ -153,15 +156,6 @@ public void bar() { class ClassAnnotatedWithGeneratedWithLambdas { - @TestGeneratedAnnotation - public void foo() { - System.out.println("don't mutate me"); - } - - public void bar() { - System.out.println("mutate me"); - } - @TestGeneratedAnnotation public void fooWithLambdas() { System.out.println("don't mutate me"); @@ -174,6 +168,18 @@ public void fooWithLambdas() { }; }; } + + public void barWithLambdas() { + System.out.println("mutate me"); + + Runnable runnable = () -> { + System.out.println("mutate me also in lambdas"); + + Runnable anotherOne = () -> { + System.out.println("mutate me also recursive lambdas"); + }; + }; + } } From 66a4bb11ad4882f9f10dd14151acfa932d6f8766 Mon Sep 17 00:00:00 2001 From: see-quick Date: Fri, 8 Nov 2024 10:31:21 +0100 Subject: [PATCH 4/6] Add more test cases + adjust logic to also catch overriden methods Signed-off-by: see-quick --- .../ExcludedAnnotationInterceptor.java | 71 +++++++++- .../ExcludedAnnotationInterceptorTest.java | 128 ++++++++++++++++++ 2 files changed, 195 insertions(+), 4 deletions(-) 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 2992cba58..fa246fc03 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 @@ -4,6 +4,7 @@ import org.pitest.bytecode.analysis.AnalysisFunctions; 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; @@ -14,6 +15,7 @@ import java.util.Collection; import java.util.Collections; import java.util.List; +import java.util.Optional; import java.util.Set; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -40,9 +42,9 @@ public void begin(ClassTree clazz) { this.skipClass = clazz.annotations().stream() .anyMatch(avoidedAnnotation()); if (!this.skipClass) { - // 1. Collect methods with avoided annotations + // 1. Collect methods with avoided annotations or that override such methods final List avoidedMethods = clazz.methods().stream() - .filter(hasAvoidedAnnotation()) + .filter(hasAvoidedAnnotationOrOverridesMethodWithAvoidedAnnotation(clazz)) .collect(Collectors.toList()); final Set avoidedMethodNames = avoidedMethods.stream() @@ -87,9 +89,70 @@ private String extractEnclosingMethodName(String lambdaName) { return lambdaName; } - private Predicate hasAvoidedAnnotation() { + /** + * 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 hasAvoidedAnnotationOrOverridesMethodWithAvoidedAnnotation(ClassTree clazz) { return methodTree -> - methodTree.annotations().stream().anyMatch(avoidedAnnotation()); + 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); + } + + /** + * Recursively checks if a method with the given name and descriptor exists in the superclass hierarchy + * and 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. + */ + 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 superClassBytes = source.getBytes(superClassName.replace('/', '.')); + if (!superClassBytes.isPresent()) { + return false; + } + + ClassTree superClassTree = ClassTree.fromBytes(superClassBytes.get()); + + Optional 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 avoidedAnnotation() { 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 3d449fecb..a3fefc22a 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,62 @@ 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 shouldNotFilterMutationsInLambdaWithinUnannotatedOverriddenMethod() { + final Collection mutations = mutator.findMutations(ClassName.fromClass(SubclassWithLambdaInOverriddenUnannotatedMethod.class)); + final Collection actual = runWithTestee(mutations, SubclassWithLambdaInOverriddenUnannotatedMethod.class); + assertThat(actual).containsExactlyElementsOf(mutations); + } + + @Test + public void shouldFilterMutationsInLambdaWithinAnnotatedOverriddenMethod() { + final Collection mutations = mutator.findMutations(ClassName.fromClass(SubclassWithLambdaInOverriddenAnnotatedMethod.class)); + final Collection actual = runWithTestee(mutations, SubclassWithLambdaInOverriddenAnnotatedMethod.class); + assertThat(actual).isEmpty(); + } + + @Test + public void shouldNotFilterMutationsInNestedLambdaWithinUnannotatedOverriddenMethod() { + final Collection mutations = mutator.findMutations(ClassName.fromClass(SubclassWithNestedLambdaInOverriddenUnannotatedMethod.class)); + final Collection actual = runWithTestee(mutations, SubclassWithNestedLambdaInOverriddenUnannotatedMethod.class); + assertThat(actual).containsExactlyElementsOf(mutations); + } + + @Test + public void shouldFilterMutationsInNestedLambdaWithinAnnotatedOverriddenMethod() { + final Collection mutations = mutator.findMutations(ClassName.fromClass(SubclassWithNestedLambdaInOverriddenAnnotatedMethod.class)); + final Collection actual = runWithTestee(mutations, SubclassWithNestedLambdaInOverriddenAnnotatedMethod.class); + assertThat(actual).isEmpty(); + } + @Test public void shouldFilterMethodsWithGeneratedAnnotationAndLambdasInside() { final List mutations = this.mutator.findMutations(ClassName.fromClass(ClassAnnotatedWithGeneratedWithLambdas.class)); @@ -138,6 +194,78 @@ 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 BaseUnannotatedClass { + public void overriddenMethod() { + System.out.println("Base unannotated method."); + } +} + +class SubclassWithLambdaInOverriddenUnannotatedMethod extends BaseUnannotatedClass { + @Override + public void overriddenMethod() { + Runnable runnable = () -> System.out.println("Lambda inside overridden unannotated method."); + } +} + +class BaseAnnotatedClass { + @TestGeneratedAnnotation + public void overriddenMethod() { + System.out.println("Base annotated method."); + } +} + +class SubclassWithLambdaInOverriddenAnnotatedMethod extends BaseAnnotatedClass { + @Override + public void overriddenMethod() { + Runnable runnable = () -> System.out.println("Lambda inside overridden annotated method."); + } +} + +class SubclassWithNestedLambdaInOverriddenUnannotatedMethod extends BaseUnannotatedClass { + @Override + public void overriddenMethod() { + Runnable outerLambda = () -> { + Runnable innerLambda = () -> System.out.println("Nested lambda inside overridden unannotated method."); + }; + } +} + + +class SubclassWithNestedLambdaInOverriddenAnnotatedMethod extends BaseAnnotatedClass { + @Override + public void overriddenMethod() { + Runnable outerLambda = () -> { + Runnable innerLambda = () -> System.out.println("Nested lambda inside overridden annotated method."); + }; + } +} + class OverloadedMethods { public void foo(int x) { System.out.println("mutate me"); From d19cd6b5340b56eafca3ba774ca8f6b204e00b21 Mon Sep 17 00:00:00 2001 From: see-quick Date: Sat, 9 Nov 2024 01:02:47 +0100 Subject: [PATCH 5/6] update for overloaded methods Signed-off-by: see-quick --- .../ExcludedAnnotationInterceptor.java | 194 +++++++++--------- .../ExcludedAnnotationInterceptorTest.java | 137 +++---------- 2 files changed, 133 insertions(+), 198 deletions(-) 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 fa246fc03..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,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; @@ -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 { @@ -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 avoidedMethods = clazz.methods().stream() - .filter(hasAvoidedAnnotationOrOverridesMethodWithAvoidedAnnotation(clazz)) + .filter(hasAvoidedAnnotation()) .collect(Collectors.toList()); - final Set avoidedMethodNames = avoidedMethods.stream() - .map(method -> method.rawNode().name) + // 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()); - // 2. Collect lambda methods with being inside avoided methods - final List 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 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 - final List> 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 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); + } + } + } + } + } + } + } } - 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 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 superClassBytes = source.getBytes(superClassName.replace('/', '.')); - if (!superClassBytes.isPresent()) { - return false; - } - - ClassTree superClassTree = ClassTree.fromBytes(superClassBytes.get()); - - Optional 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 hasAvoidedAnnotation() { + return methodTree -> + methodTree.annotations().stream().anyMatch(avoidedAnnotation()); } private Predicate avoidedAnnotation() { @@ -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(); + } + } } 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 a3fefc22a..ee0e26125 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 @@ -96,50 +96,35 @@ public void shouldFilterMutationsInLambdaWithinAnnotatedMethod() { } @Test - public void shouldNotFilterMutationsInLambdaWithinUnannotatedOverriddenMethod() { - final Collection mutations = mutator.findMutations(ClassName.fromClass(SubclassWithLambdaInOverriddenUnannotatedMethod.class)); - final Collection actual = runWithTestee(mutations, SubclassWithLambdaInOverriddenUnannotatedMethod.class); - assertThat(actual).containsExactlyElementsOf(mutations); - } + public void shouldHandleOverloadedMethodsWithLambdas() { + final List mutations = this.mutator.findMutations(ClassName.fromClass(OverloadedMethods.class)); + final Collection actual = runWithTestee(mutations, OverloadedMethods.class); - @Test - public void shouldFilterMutationsInLambdaWithinAnnotatedOverriddenMethod() { - final Collection mutations = mutator.findMutations(ClassName.fromClass(SubclassWithLambdaInOverriddenAnnotatedMethod.class)); - final Collection actual = runWithTestee(mutations, SubclassWithLambdaInOverriddenAnnotatedMethod.class); - assertThat(actual).isEmpty(); + // 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 shouldNotFilterMutationsInNestedLambdaWithinUnannotatedOverriddenMethod() { - final Collection mutations = mutator.findMutations(ClassName.fromClass(SubclassWithNestedLambdaInOverriddenUnannotatedMethod.class)); - final Collection actual = runWithTestee(mutations, SubclassWithNestedLambdaInOverriddenUnannotatedMethod.class); - assertThat(actual).containsExactlyElementsOf(mutations); - } + public void shouldNotFilterMutationsInNestedLambdaWithinUnannotatedOverloadedMethod() { + final Collection mutations = mutator.findMutations(ClassName.fromClass(NestedLambdaInOverloadedMethods.class)); + final Collection actual = runWithTestee(mutations, NestedLambdaInOverloadedMethods.class); - @Test - public void shouldFilterMutationsInNestedLambdaWithinAnnotatedOverriddenMethod() { - final Collection mutations = mutator.findMutations(ClassName.fromClass(SubclassWithNestedLambdaInOverriddenAnnotatedMethod.class)); - final Collection actual = runWithTestee(mutations, SubclassWithNestedLambdaInOverriddenAnnotatedMethod.class); - assertThat(actual).isEmpty(); + // 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 -> mutation.getId().getLocation().getMethodName().startsWith("lambda$baz$")); } @Test - public void shouldFilterMethodsWithGeneratedAnnotationAndLambdasInside() { - final List mutations = this.mutator.findMutations(ClassName.fromClass(ClassAnnotatedWithGeneratedWithLambdas.class)); - final Collection actual = runWithTestee(mutations, ClassAnnotatedWithGeneratedWithLambdas.class); - assertThat(actual).hasSize(3); - - for (MutationDetails mutationDetails : actual) { - assertThat(mutationDetails.getId().getLocation().getMethodName()).isIn("barWithLambdas", "lambda$barWithLambdas$2", "lambda$barWithLambdas$3"); - } - } + public void shouldFilterMutationsInNestedLambdaWithinAnnotatedOverloadedMethod() { + final Collection mutations = mutator.findMutations(ClassName.fromClass(NestedLambdaInOverloadedMethods.class)); + final Collection actual = runWithTestee(mutations, NestedLambdaInOverloadedMethods.class); - @Test - public void shouldHandleOverloadedMethods() { - final List mutations = this.mutator.findMutations(ClassName.fromClass(OverloadedMethods.class)); - final Collection actual = runWithTestee(mutations, OverloadedMethods.class); - // Assume only one overloaded version is annotated - assertThat(actual).hasSize(2); // Assuming three methods: two overloaded (one annotated) and one regular + // 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( @@ -220,60 +205,16 @@ public void methodWithLambda() { } } -class BaseUnannotatedClass { - public void overriddenMethod() { - System.out.println("Base unannotated method."); - } -} - -class SubclassWithLambdaInOverriddenUnannotatedMethod extends BaseUnannotatedClass { - @Override - public void overriddenMethod() { - Runnable runnable = () -> System.out.println("Lambda inside overridden unannotated method."); - } -} - -class BaseAnnotatedClass { - @TestGeneratedAnnotation - public void overriddenMethod() { - System.out.println("Base annotated method."); - } -} - -class SubclassWithLambdaInOverriddenAnnotatedMethod extends BaseAnnotatedClass { - @Override - public void overriddenMethod() { - Runnable runnable = () -> System.out.println("Lambda inside overridden annotated method."); - } -} - -class SubclassWithNestedLambdaInOverriddenUnannotatedMethod extends BaseUnannotatedClass { - @Override - public void overriddenMethod() { - Runnable outerLambda = () -> { - Runnable innerLambda = () -> System.out.println("Nested lambda inside overridden unannotated method."); - }; - } -} - - -class SubclassWithNestedLambdaInOverriddenAnnotatedMethod extends BaseAnnotatedClass { - @Override - public void overriddenMethod() { - Runnable outerLambda = () -> { - Runnable innerLambda = () -> System.out.println("Nested lambda inside overridden 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() { @@ -281,33 +222,19 @@ public void bar() { } } - -class ClassAnnotatedWithGeneratedWithLambdas { - - @TestGeneratedAnnotation - public void fooWithLambdas() { - System.out.println("don't mutate me"); - - Runnable runnable = () -> { - System.out.println("don't mutate me also in lambdas"); - - Runnable anotherOne = () -> { - System.out.println("don't mutate me also recursive lambdas"); - }; +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"); }; } - public void barWithLambdas() { - System.out.println("mutate me"); - - Runnable runnable = () -> { - System.out.println("mutate me also in lambdas"); - - Runnable anotherOne = () -> { - System.out.println("mutate me also recursive lambdas"); - }; + @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"); }; } } - - From bbb2a2040837d5a89a36e8b706eb13aa2b418547 Mon Sep 17 00:00:00 2001 From: see-quick Date: Sat, 9 Nov 2024 13:17:35 +0100 Subject: [PATCH 6/6] fix tests Signed-off-by: see-quick --- .../annotations/ExcludedAnnotationInterceptorTest.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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 ee0e26125..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 @@ -115,7 +115,10 @@ public void shouldNotFilterMutationsInNestedLambdaWithinUnannotatedOverloadedMet // 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 -> mutation.getId().getLocation().getMethodName().startsWith("lambda$baz$")); + assertThat(actual).anyMatch(mutation -> { + String methodName = mutation.getId().getLocation().getMethodName(); + return methodName.startsWith("lambda$baz$") || methodName.startsWith("lambda$null$"); + }); } @Test