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 ed8a23e97..4b225e9b1 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 @@ -1,6 +1,9 @@ package org.pitest.mutationtest.build.intercept.staticinitializers; import org.objectweb.asm.Handle; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.FieldInsnNode; import org.objectweb.asm.tree.InvokeDynamicInsnNode; import org.objectweb.asm.tree.MethodInsnNode; import org.pitest.bytecode.analysis.ClassTree; @@ -11,6 +14,14 @@ import org.pitest.mutationtest.engine.Location; import org.pitest.mutationtest.engine.Mutater; import org.pitest.mutationtest.engine.MutationDetails; +import org.pitest.sequence.Context; +import org.pitest.sequence.Match; +import org.pitest.sequence.QueryParams; +import org.pitest.sequence.QueryStart; +import org.pitest.sequence.SequenceMatcher; +import org.pitest.sequence.SequenceQuery; +import org.pitest.sequence.Slot; +import org.pitest.sequence.SlotWrite; import java.util.Arrays; import java.util.Collection; @@ -18,6 +29,7 @@ import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.function.Function; @@ -25,6 +37,13 @@ import java.util.stream.Collectors; import java.util.stream.Stream; +import static org.pitest.bytecode.analysis.InstructionMatchers.anyInstruction; +import static org.pitest.bytecode.analysis.InstructionMatchers.isA; +import static org.pitest.bytecode.analysis.InstructionMatchers.methodCallNamed; +import static org.pitest.bytecode.analysis.InstructionMatchers.notAnInstruction; +import static org.pitest.bytecode.analysis.OpcodeMatchers.PUTSTATIC; +import static org.pitest.sequence.Result.result; + /** * Identifies and marks mutations in code that is active during class * Initialisation. @@ -39,6 +58,29 @@ */ class StaticInitializerInterceptor implements MutationInterceptor { + static final Slot START = Slot.create(AbstractInsnNode.class); + + static final SequenceMatcher DELAYED_EXECUTION = QueryStart + .any(AbstractInsnNode.class) + .then(returnsDeferredExecutionCode().or(isA(InvokeDynamicInsnNode.class)).and(store(START.write()))) + .then(enumConstructorCallAndStore().or(QueryStart.match(PUTSTATIC))) + .zeroOrMore(QueryStart.match(anyInstruction())) + .compile(QueryParams.params(AbstractInsnNode.class) + .withIgnores(notAnInstruction()) + ); + + private static Match returnsDeferredExecutionCode() { + return (c,n) -> result(n.getOpcode() == Opcodes.INVOKESTATIC && returnDelayedExecutionType(((MethodInsnNode) n).desc), c); + } + + private static boolean returnDelayedExecutionType(String desc) { + int endOfParams = desc.indexOf(')'); + return endOfParams <= 0 || desc.substring(endOfParams + 1).startsWith("Ljava/util/function/"); + } + + private static SequenceQuery enumConstructorCallAndStore() { + return QueryStart.match(methodCallNamed("")).then(PUTSTATIC); + } private Predicate isStaticInitCode; @Override @@ -73,28 +115,54 @@ private void analyseClass(ClassTree tree) { .map(MethodTree::asLocation) .collect(Collectors.toSet()); + Set storedToSupplier = findsCallsStoredToSuppliers(tree); + // Get map of each private method to the private methods it calls - // Any call to a no private method breaks the chain + // Any call to a non private method breaks the chain Map> callTree = tree.methods().stream() .filter(m -> m.isPrivate() || m.rawNode().name.equals("")) .flatMap(m -> allCallsFor(tree, m).stream().map(c -> new Call(m.asLocation(), c))) .filter(c -> privateMethods.contains(c.to())) + .filter(c -> !storedToSupplier.contains(c)) .collect(Collectors.groupingBy(Call::from)); - Set visited = new HashSet<>(); - visit(callTree, visited, clinit.get().asLocation()); + Set calledOnlyFromStaticInitializer = new HashSet<>(); - this.isStaticInitCode = m -> visited.contains(m.getId().getLocation()); + visit(callTree, calledOnlyFromStaticInitializer, clinit.get().asLocation()); + + this.isStaticInitCode = m -> calledOnlyFromStaticInitializer.contains(m.getId().getLocation()); } } + private Set findsCallsStoredToSuppliers(ClassTree tree) { + Set all = new HashSet<>(directClinitCallsToDelayedExecutionCode(tree)); + all.addAll(storedViaEnumConstructor()); + return all; + } + + private Set storedViaEnumConstructor() { +return Collections.emptySet(); + } + + private Set directClinitCallsToDelayedExecutionCode(ClassTree tree) { + return tree.methods().stream() + .filter(m -> m.isPrivate() || m.rawNode().name.equals("")) + .flatMap(m -> delayedExecutionCall(m).stream().map(c -> new Call(m.asLocation(), c))) + .collect(Collectors.toSet()); + } + + private List delayedExecutionCall(MethodTree method) { + Context context = Context.start(); + return DELAYED_EXECUTION.contextMatches(method.instructions(), context).stream() + .map(c -> c.retrieve(START.read()).get()) + .flatMap(this::nodeToLocation) + .collect(Collectors.toList()); + } + private List allCallsFor(ClassTree tree, MethodTree m) { - // temporarily disable dynamic calls as they are more likely to be involved - // in storing delayed execution code within static fields. - return callsFor(tree,m).collect(Collectors.toList()); - // return Stream.concat(callsFor(tree,m), invokeDynamicCallsFor(tree,m)) - // .collect(Collectors.toList()); + return Stream.concat(callsFor(tree,m), invokeDynamicCallsFor(tree,m)) + .collect(Collectors.toList()); } private Stream callsFor(ClassTree tree, MethodTree m) { @@ -123,6 +191,18 @@ private void visit(Map> callTree, Set visited, Lo } } + private Stream nodeToLocation(AbstractInsnNode n) { + if (n instanceof MethodInsnNode) { + return Stream.of(asLocation((MethodInsnNode) n)); + } + + if (n instanceof InvokeDynamicInsnNode) { + return asLocation((InvokeDynamicInsnNode) n); + } + + return Stream.empty(); + } + private Location asLocation(MethodInsnNode call) { return Location.location(ClassName.fromString(call.owner), call.name, call.desc); } @@ -166,6 +246,10 @@ public InterceptorType type() { return InterceptorType.FILTER; } + private static Match store(SlotWrite slot) { + return (c, n) -> result(true, c.store(slot, n)); + } + } class Call { @@ -184,4 +268,21 @@ Location from() { Location to() { return to; } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Call call = (Call) o; + return Objects.equals(from, call.from) && Objects.equals(to, call.to); + } + + @Override + public int hashCode() { + return Objects.hash(from, to); + } } \ No newline at end of file diff --git a/pitest-entry/src/test/java/com/example/staticinitializers/EnumFieldSupplier.java b/pitest-entry/src/test/java/com/example/staticinitializers/EnumFieldSupplier.java new file mode 100644 index 000000000..1e1111643 --- /dev/null +++ b/pitest-entry/src/test/java/com/example/staticinitializers/EnumFieldSupplier.java @@ -0,0 +1,20 @@ +package com.example.staticinitializers; + +import java.util.function.Supplier; + +public enum EnumFieldSupplier { + A(canMutate()); + + private final Supplier supplier; + + EnumFieldSupplier(Supplier supplier) { + this.supplier = supplier; + } + + private static Supplier canMutate() { + // don't mutate + System.out.println("ideally would mutate me"); + + return () -> "Do not mutate"; // mutate + } +} diff --git a/pitest-entry/src/test/java/com/example/staticinitializers/StaticFunctionField.java b/pitest-entry/src/test/java/com/example/staticinitializers/StaticFunctionField.java new file mode 100644 index 000000000..aef6d53e0 --- /dev/null +++ b/pitest-entry/src/test/java/com/example/staticinitializers/StaticFunctionField.java @@ -0,0 +1,14 @@ +package com.example.staticinitializers; + +import java.util.function.Function; + +public class StaticFunctionField { + private static final Function FOO = canMutate(); + + private static Function canMutate() { + // don't mutate + System.out.println("ideally would mutate me"); + + return s -> s + "foo"; + } +} diff --git a/pitest-entry/src/test/java/com/example/staticinitializers/StaticSupplierField.java b/pitest-entry/src/test/java/com/example/staticinitializers/StaticSupplierField.java new file mode 100644 index 000000000..c7a82c0df --- /dev/null +++ b/pitest-entry/src/test/java/com/example/staticinitializers/StaticSupplierField.java @@ -0,0 +1,14 @@ +package com.example.staticinitializers; + +import java.util.function.Supplier; + +public class StaticSupplierField { + final static Supplier SUPPLER = canMutate(); + + private static Supplier canMutate() { + // don't mutate + System.out.println("ideally would mutate me"); + + return () -> "Do not mutate"; // mutate + } +} 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 644f1a972..e4abb6b66 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 @@ -1,11 +1,14 @@ package org.pitest.mutationtest.build.intercept.staticinitializers; import com.example.staticinitializers.BrokenChain; +import com.example.staticinitializers.EnumFieldSupplier; import com.example.staticinitializers.EnumWithLambdaInConstructor; import com.example.staticinitializers.MethodsCallsEachOtherInLoop; import com.example.staticinitializers.NestedEnumWithLambdaInStaticInitializer; import com.example.staticinitializers.SecondLevelPrivateMethods; import com.example.staticinitializers.SingletonWithWorkInInitializer; +import com.example.staticinitializers.StaticFunctionField; +import com.example.staticinitializers.StaticSupplierField; import com.example.staticinitializers.ThirdLevelPrivateMethods; import org.junit.Ignore; import org.junit.Test; @@ -146,7 +149,6 @@ public void analysisDoesNotGetStuckInInfiniteLoop() { } @Test - @Ignore("temporally disabled while filtering reworked") public void filtersMutantsInEnumPrivateMethodsCalledViaMethodRef() { v.forClass(EnumWithLambdaInConstructor.class) .forMutantsMatching(inMethodStartingWith("doStuff")) @@ -156,7 +158,6 @@ public void filtersMutantsInEnumPrivateMethodsCalledViaMethodRef() { } @Test - @Ignore("temporally disabled while filtering reworked") public void filtersMutantsInLambdaCalledFromStaticInitializerInNestedEnum() { v.forClass(NestedEnumWithLambdaInStaticInitializer.TOYS.class) .forMutantsMatching(inMethodStartingWith("lambda")) @@ -165,6 +166,36 @@ public void filtersMutantsInLambdaCalledFromStaticInitializerInNestedEnum() { .verify(); } + @Test + public void doesNotSuppressDownStreamMutationsForCodeStoredInSuppliers() { + v.forClass(StaticSupplierField.class) + .forMethod("canMutate") + .forAnyCode() + .mutantsAreGenerated() + .noMutantsAreFiltered() + .verify(); + } + + @Test + public void doesNotSuppressDownStreamMutationsForCodeStoredInFunctions() { + v.forClass(StaticFunctionField.class) + .forMethod("canMutate") + .forAnyCode() + .mutantsAreGenerated() + .noMutantsAreFiltered() + .verify(); + } + + @Test + public void doesNotSuppressDownStreamMutationsForEnumFieldSuppliers() { + v.forClass(EnumFieldSupplier.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); }