From d19cd6b5340b56eafca3ba774ca8f6b204e00b21 Mon Sep 17 00:00:00 2001 From: see-quick Date: Sat, 9 Nov 2024 01:02:47 +0100 Subject: [PATCH] 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"); }; } } - -