From 45d9f34ef657ec9d71dcc0d1622cd5444b8a2279 Mon Sep 17 00:00:00 2001 From: Henry Coles Date: Thu, 29 Aug 2024 08:28:21 +0100 Subject: [PATCH] identify fields with a prescan --- .../StaticInitializerInterceptor.java | 54 +++++++++++++------ .../main/java/org/pitest/sequence/Slot.java | 5 ++ .../delayedexecution/EnumListOfSuppliers.java | 21 ++++++++ .../StaticListOfFunctions.java | 19 +++++++ .../StaticInitializerInterceptorTest.java | 24 +++++++++ .../org/pitest/bytecode/SignatureParser.java | 31 +++++++++++ .../pitest/bytecode/SignatureParserTest.java | 25 +++++++++ 7 files changed, 163 insertions(+), 16 deletions(-) create mode 100644 pitest-entry/src/test/java/com/example/staticinitializers/delayedexecution/EnumListOfSuppliers.java create mode 100644 pitest-entry/src/test/java/com/example/staticinitializers/delayedexecution/StaticListOfFunctions.java create mode 100644 pitest/src/main/java/org/pitest/bytecode/SignatureParser.java create mode 100644 pitest/src/test/java/org/pitest/bytecode/SignatureParserTest.java diff --git a/pitest-entry/src/main/java/org/pitest/mutationtest/build/intercept/staticinitializers/StaticInitializerInterceptor.java b/pitest-entry/src/main/java/org/pitest/mutationtest/build/intercept/staticinitializers/StaticInitializerInterceptor.java index 6c5f9052c..540d20c96 100644 --- a/pitest-entry/src/main/java/org/pitest/mutationtest/build/intercept/staticinitializers/StaticInitializerInterceptor.java +++ b/pitest-entry/src/main/java/org/pitest/mutationtest/build/intercept/staticinitializers/StaticInitializerInterceptor.java @@ -4,8 +4,10 @@ import org.objectweb.asm.Opcodes; import org.objectweb.asm.tree.AbstractInsnNode; import org.objectweb.asm.tree.FieldInsnNode; +import org.objectweb.asm.tree.FieldNode; import org.objectweb.asm.tree.InvokeDynamicInsnNode; import org.objectweb.asm.tree.MethodInsnNode; +import org.pitest.bytecode.SignatureParser; import org.pitest.bytecode.analysis.ClassTree; import org.pitest.bytecode.analysis.MethodTree; import org.pitest.classinfo.ClassName; @@ -21,6 +23,7 @@ import org.pitest.sequence.SequenceMatcher; import org.pitest.sequence.SequenceQuery; import org.pitest.sequence.Slot; +import org.pitest.sequence.SlotRead; import org.pitest.sequence.SlotWrite; import java.util.Arrays; @@ -53,11 +56,11 @@ * 1. In a static initializer (i.e ) * 2. In a private method or constructor called from or another private method in the call tree * - * */ class StaticInitializerInterceptor implements MutationInterceptor { static final Slot START = Slot.create(AbstractInsnNode.class); + static final Slot> DELAYED_EXECUTION_FIELDS = Slot.createSet(String.class); static final SequenceMatcher DELAYED_EXECUTION = QueryStart .any(AbstractInsnNode.class) @@ -66,36 +69,41 @@ class StaticInitializerInterceptor implements MutationInterceptor { .then(returnsDeferredExecutionCode().or(dynamicallyReturnsDeferredExecutionCode()).and(store(START.write()))) // allow for other method calls etc .zeroOrMore(QueryStart.match(anyInstruction())) - .then(enumConstructorCallAndStore().or(QueryStart.match(delayedExecutionField()))) + .then(enumConstructorCallAndStore().or(QueryStart.match(delayedExecutionField(DELAYED_EXECUTION_FIELDS.read())))) .zeroOrMore(QueryStart.match(anyInstruction())) .compile(QueryParams.params(AbstractInsnNode.class) .withIgnores(notAnInstruction()) ); - private static Match delayedExecutionField() { - return PUTSTATIC.and(isAUtilFunctionField()); + private static Match delayedExecutionField(SlotRead> delayedFields) { + return PUTSTATIC.and(isADelayedExecutionField(delayedFields)); } - private static Match isAUtilFunctionField() { + private static Match isADelayedExecutionField(SlotRead> delayedFields) { return (c,n) -> { FieldInsnNode fieldNode = ((FieldInsnNode) n); - return result( fieldNode.desc.startsWith("Ljava/util/function/"), c); + return result( c.retrieve(delayedFields).get().contains(fieldNode.name), c); }; } private static Match dynamicallyReturnsDeferredExecutionCode() { - return (c,n) -> result(n.getOpcode() == Opcodes.INVOKEDYNAMIC && returnDelayedExecutionType(((InvokeDynamicInsnNode) n).desc), c); + return (c,n) -> result(n.getOpcode() == Opcodes.INVOKEDYNAMIC && returnsDelayedExecutionType(((InvokeDynamicInsnNode) n).desc), c); } private static Match returnsDeferredExecutionCode() { - return (c,n) -> result(n.getOpcode() == Opcodes.INVOKESTATIC && returnDelayedExecutionType(((MethodInsnNode) n).desc), c); + return (c,n) -> result(n.getOpcode() == Opcodes.INVOKESTATIC && returnsDelayedExecutionType(((MethodInsnNode) n).desc), c); } - private static boolean returnDelayedExecutionType(String desc) { + private static boolean returnsDelayedExecutionType(String desc) { int endOfParams = desc.indexOf(')'); return endOfParams <= 0 || desc.substring(endOfParams + 1).startsWith("Ljava/util/function/"); } + + private static boolean isADelayedExecutionType(String type) { + return type.startsWith("java/util/function/"); + } + private static SequenceQuery enumConstructorCallAndStore() { return QueryStart.match(methodCallNamed("")).then(PUTSTATIC); } @@ -126,6 +134,15 @@ private void analyseClass(ClassTree tree) { final Optional clinit = tree.methods().stream().filter(nameEquals("")).findFirst(); if (clinit.isPresent()) { + + // Find delayed execution fields (Function, Supplier, List etc). Full generic signature is available in the + // declaration, but not on call. + Set delayedExecutionFields = tree.rawNode().fields.stream() + .filter(this::isDelayedExecutionField) + .map(n -> n.name) + .collect(Collectors.toSet()); + + // We can't see if a method *call* is private from the call site // so collect a set of private methods within the class first Set privateMethods = tree.methods().stream() @@ -133,7 +150,7 @@ private void analyseClass(ClassTree tree) { .map(MethodTree::asLocation) .collect(Collectors.toSet()); - Set storedToSupplier = findCallsStoredToDelayedExecutionCode(tree); + Set storedToSupplier = findCallsStoredToDelayedExecutionCode(tree, delayedExecutionFields); // Get map of each private method to the private methods it calls // Any call to a non private method breaks the chain @@ -153,20 +170,25 @@ private void analyseClass(ClassTree tree) { } } - private Set findCallsStoredToDelayedExecutionCode(ClassTree tree) { - return new HashSet<>(privateAndClinitCallsToDelayedExecutionCode(tree)); + private boolean isDelayedExecutionField(FieldNode fieldNode) { + return SignatureParser.extractTypes(fieldNode.signature).stream() + .anyMatch(StaticInitializerInterceptor::isADelayedExecutionType); + } + + private Set findCallsStoredToDelayedExecutionCode(ClassTree tree, Set delayedExecutionFields) { + return new HashSet<>(privateAndClinitCallsToDelayedExecutionCode(tree, delayedExecutionFields)); } - private Set privateAndClinitCallsToDelayedExecutionCode(ClassTree tree) { + private Set privateAndClinitCallsToDelayedExecutionCode(ClassTree tree, Set delayedExecutionFields) { return tree.methods().stream() .filter(m -> m.isPrivate() || m.rawNode().name.equals("")) - .flatMap(m -> delayedExecutionCall(m).stream().map(c -> new Call(m.asLocation(), c))) + .flatMap(m -> delayedExecutionCall(m, delayedExecutionFields).stream().map(c -> new Call(m.asLocation(), c))) .collect(Collectors.toSet()); } - private List delayedExecutionCall(MethodTree method) { - Context context = Context.start(); + private List delayedExecutionCall(MethodTree method, Set delayedExecutionFields) { + Context context = Context.start().store(DELAYED_EXECUTION_FIELDS.write(), delayedExecutionFields); return DELAYED_EXECUTION.contextMatches(method.instructions(), context).stream() .map(c -> c.retrieve(START.read()).get()) .flatMap(this::nodeToLocation) diff --git a/pitest-entry/src/main/java/org/pitest/sequence/Slot.java b/pitest-entry/src/main/java/org/pitest/sequence/Slot.java index 06da33173..798eb7028 100644 --- a/pitest-entry/src/main/java/org/pitest/sequence/Slot.java +++ b/pitest-entry/src/main/java/org/pitest/sequence/Slot.java @@ -1,6 +1,7 @@ package org.pitest.sequence; import java.util.List; +import java.util.Set; public final class Slot { public static Slot create(Class clazz) { @@ -11,6 +12,10 @@ public static Slot> createList(Class clazz) { return new Slot<>(); } + public static Slot> createSet(Class clazz) { + return new Slot<>(); + } + public SlotWrite write() { return new SlotWrite<>(this); } diff --git a/pitest-entry/src/test/java/com/example/staticinitializers/delayedexecution/EnumListOfSuppliers.java b/pitest-entry/src/test/java/com/example/staticinitializers/delayedexecution/EnumListOfSuppliers.java new file mode 100644 index 000000000..4a12684b8 --- /dev/null +++ b/pitest-entry/src/test/java/com/example/staticinitializers/delayedexecution/EnumListOfSuppliers.java @@ -0,0 +1,21 @@ +package com.example.staticinitializers.delayedexecution; + +import java.util.List; +import java.util.function.Supplier; + +import static java.util.Arrays.asList; + +public enum EnumListOfSuppliers { + A(EnumListOfSuppliers::canMutate), B(EnumListOfSuppliers::canMutate); + + private final List> supplier; + + EnumListOfSuppliers(Supplier supplier) { + this.supplier = asList(supplier); + } + + private static String canMutate() { + return "mutate me"; // mutate + } + +} diff --git a/pitest-entry/src/test/java/com/example/staticinitializers/delayedexecution/StaticListOfFunctions.java b/pitest-entry/src/test/java/com/example/staticinitializers/delayedexecution/StaticListOfFunctions.java new file mode 100644 index 000000000..cea2da4c4 --- /dev/null +++ b/pitest-entry/src/test/java/com/example/staticinitializers/delayedexecution/StaticListOfFunctions.java @@ -0,0 +1,19 @@ +package com.example.staticinitializers.delayedexecution; + +import java.util.function.Function; +import java.util.List; + +import static java.util.Arrays.asList; + +public class StaticListOfFunctions { + public static final List> FUNCTIONS = + asList(StaticListOfFunctions::canMutate, StaticListOfFunctions::canAlsoMutate); + + private static Integer canMutate(Integer a) { + return a + 1; + } + + private static Integer canAlsoMutate(Integer a) { + return a + 2; + } +} diff --git a/pitest-entry/src/test/java/org/pitest/mutationtest/build/intercept/staticinitializers/StaticInitializerInterceptorTest.java b/pitest-entry/src/test/java/org/pitest/mutationtest/build/intercept/staticinitializers/StaticInitializerInterceptorTest.java index 8822bd49d..d29dff285 100644 --- a/pitest-entry/src/test/java/org/pitest/mutationtest/build/intercept/staticinitializers/StaticInitializerInterceptorTest.java +++ b/pitest-entry/src/test/java/org/pitest/mutationtest/build/intercept/staticinitializers/StaticInitializerInterceptorTest.java @@ -8,9 +8,11 @@ import com.example.staticinitializers.NestedEnumWithLambdaInStaticInitializer; import com.example.staticinitializers.SecondLevelPrivateMethods; import com.example.staticinitializers.SingletonWithWorkInInitializer; +import com.example.staticinitializers.delayedexecution.EnumListOfSuppliers; import com.example.staticinitializers.delayedexecution.EnumMethodReferenceNotStored; import com.example.staticinitializers.delayedexecution.EnumMixedFields; import com.example.staticinitializers.delayedexecution.StaticFunctionField; +import com.example.staticinitializers.delayedexecution.StaticListOfFunctions; import com.example.staticinitializers.delayedexecution.StaticSupplierField; import com.example.staticinitializers.ThirdLevelPrivateMethods; import org.junit.Test; @@ -239,6 +241,28 @@ public void filtersMutationsForMethodReferencesUsedInEnumConstructor() { .verify(); } + @Test + public void mutatesMethodsStoredInListOfSuppliers() { + v.forClass(EnumListOfSuppliers.class) + .forMethod("canMutate") + .forAnyCode() + .mutantsAreGenerated() + .noMutantsAreFiltered() + .verify(); + } + + @Test + public void mutatesMethodsStoredInStaticListOfFunctions() { + v.forClass(StaticListOfFunctions.class) + .forMethod("canMutate") + .forAnyCode() + .mutantsAreGenerated() + .noMutantsAreFiltered() + .verify(); + } + + + private Predicate inMethod(String name, String desc) { return m -> m.getMethod().equals(name) && m.getId().getLocation().getMethodDesc().equals(desc); } diff --git a/pitest/src/main/java/org/pitest/bytecode/SignatureParser.java b/pitest/src/main/java/org/pitest/bytecode/SignatureParser.java new file mode 100644 index 000000000..5b9112b86 --- /dev/null +++ b/pitest/src/main/java/org/pitest/bytecode/SignatureParser.java @@ -0,0 +1,31 @@ +package org.pitest.bytecode; + +import org.objectweb.asm.signature.SignatureReader; +import org.objectweb.asm.signature.SignatureVisitor; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +public class SignatureParser { + + /** + * Extracts all types referenced in a field generic signature + */ + public static Set extractTypes(String signature) { + if (signature == null) { + return Collections.emptySet(); + } + + Set types = new HashSet<>(); + SignatureReader r = new SignatureReader(signature); + SignatureVisitor visitor = new SignatureVisitor(ASMVersion.asmVersion()) { + @Override + public void visitClassType(String name) { + types.add(name); + } + }; + r.acceptType(visitor); + return types; + } +} diff --git a/pitest/src/test/java/org/pitest/bytecode/SignatureParserTest.java b/pitest/src/test/java/org/pitest/bytecode/SignatureParserTest.java new file mode 100644 index 000000000..93fc1334d --- /dev/null +++ b/pitest/src/test/java/org/pitest/bytecode/SignatureParserTest.java @@ -0,0 +1,25 @@ +package org.pitest.bytecode; + +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class SignatureParserTest { + + @Test + public void noTypesForNullSignature() { + assertThat(SignatureParser.extractTypes(null)).isEmpty(); + } + + @Test + public void parsesSupplierOfStrings() { + String signature = "Ljava/util/function/Supplier;"; + assertThat(SignatureParser.extractTypes(signature)).containsExactlyInAnyOrder("java/util/function/Supplier", "java/lang/String"); + } + + @Test + public void parsesListsOfFunctions() { + String signature = "Ljava/util/List;>;"; + assertThat(SignatureParser.extractTypes(signature)).containsExactlyInAnyOrder("java/util/List", "java/util/function/Function", "java/lang/Integer"); + } +} \ No newline at end of file