Skip to content

Commit

Permalink
Merge pull request #1344 from hcoles/feature/delayed_execution2
Browse files Browse the repository at this point in the history
Mutate delayed execution code stored in static fields
  • Loading branch information
hcoles authored Aug 28, 2024
2 parents 4847e40 + 74bcdfd commit 7497570
Show file tree
Hide file tree
Showing 9 changed files with 316 additions and 13 deletions.
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -11,20 +14,35 @@
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;
import java.util.Collections;
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;
import java.util.function.Predicate;
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.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.
Expand All @@ -39,6 +57,48 @@
*/
class StaticInitializerInterceptor implements MutationInterceptor {

static final Slot<AbstractInsnNode> START = Slot.create(AbstractInsnNode.class);

static final SequenceMatcher<AbstractInsnNode> DELAYED_EXECUTION = QueryStart
.any(AbstractInsnNode.class)
// look for calls returning delayed execution types. Unfortunately this is not guarantee that we
// store the result to an appropriate type
.then(returnsDeferredExecutionCode().or(dynamicallyReturnsDeferredExecutionCode()).and(store(START.write())))
// allow for other method calls etc
.zeroOrMore(QueryStart.match(anyInstruction()))
.then(enumConstructorCallAndStore().or(QueryStart.match(delayedExecutionField())))
.zeroOrMore(QueryStart.match(anyInstruction()))
.compile(QueryParams.params(AbstractInsnNode.class)
.withIgnores(notAnInstruction())
);

private static Match<AbstractInsnNode> delayedExecutionField() {
return PUTSTATIC.and(isAUtilFunctionField());
}

private static Match<AbstractInsnNode> isAUtilFunctionField() {
return (c,n) -> {
FieldInsnNode fieldNode = ((FieldInsnNode) n);
return result( fieldNode.desc.startsWith("Ljava/util/function/"), c);
};
}

private static Match<AbstractInsnNode> dynamicallyReturnsDeferredExecutionCode() {
return (c,n) -> result(n.getOpcode() == Opcodes.INVOKEDYNAMIC && returnDelayedExecutionType(((InvokeDynamicInsnNode) n).desc), c);
}

private static Match<AbstractInsnNode> 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<AbstractInsnNode> enumConstructorCallAndStore() {
return QueryStart.match(methodCallNamed("<init>")).then(PUTSTATIC);
}
private Predicate<MutationDetails> isStaticInitCode;

@Override
Expand Down Expand Up @@ -69,32 +129,53 @@ private void analyseClass(ClassTree tree) {
// 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<Location> privateMethods = tree.methods().stream()
.filter(m -> m.isPrivate())
.filter(MethodTree::isPrivate)
.map(MethodTree::asLocation)
.collect(Collectors.toSet());

Set<Call> storedToSupplier = findCallsStoredToDelayedExecutionCode(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<Location, List<Call>> callTree = tree.methods().stream()
.filter(m -> m.isPrivate() || m.rawNode().name.equals("<clinit>"))
.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<Location> visited = new HashSet<>();

visit(callTree, visited, clinit.get().asLocation());
Set<Location> calledOnlyFromStaticInitializer = new HashSet<>();

visit(callTree, calledOnlyFromStaticInitializer, clinit.get().asLocation());

this.isStaticInitCode = m -> visited.contains(m.getId().getLocation());
this.isStaticInitCode = m -> calledOnlyFromStaticInitializer.contains(m.getId().getLocation());
}
}

private Set<Call> findCallsStoredToDelayedExecutionCode(ClassTree tree) {
return new HashSet<>(privateAndClinitCallsToDelayedExecutionCode(tree));
}


private Set<Call> privateAndClinitCallsToDelayedExecutionCode(ClassTree tree) {
return tree.methods().stream()
.filter(m -> m.isPrivate() || m.rawNode().name.equals("<clinit>"))
.flatMap(m -> delayedExecutionCall(m).stream().map(c -> new Call(m.asLocation(), c)))
.collect(Collectors.toSet());
}

private List<Location> 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<Location> 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<Location> callsFor(ClassTree tree, MethodTree m) {
Expand Down Expand Up @@ -123,6 +204,18 @@ private void visit(Map<Location, List<Call>> callTree, Set<Location> visited, Lo
}
}

private Stream<Location> 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);
}
Expand Down Expand Up @@ -166,6 +259,10 @@ public InterceptorType type() {
return InterceptorType.FILTER;
}

private static Match<AbstractInsnNode> store(SlotWrite<AbstractInsnNode> slot) {
return (c, n) -> result(true, c.store(slot, n));
}

}

class Call {
Expand All @@ -184,4 +281,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);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.example.staticinitializers.delayedexecution;

import java.util.function.Supplier;

public enum EnumFieldMethodRef {
A(EnumFieldMethodRef::canMutate), B(EnumFieldMethodRef::canMutate);

private final Supplier<String> supplier;


EnumFieldMethodRef(Supplier<String> supplier) {
this.supplier = supplier;
}

private static String canMutate() {
return "Do not mutate";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.example.staticinitializers.delayedexecution;

import java.util.function.Supplier;

public enum EnumFieldSupplier {
A(canMutate()), B(canMutate());

private final Supplier<String> supplier;

EnumFieldSupplier(Supplier<String> supplier) {
this.supplier = supplier;
}

private static Supplier<String> canMutate() {
// don't mutate
System.out.println("ideally would mutate me");

return () -> "Do not mutate"; // mutate
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.example.staticinitializers.delayedexecution;

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public enum EnumMethodReferenceNotStored {
A(Arrays.asList(1,2,3));

private final List<Integer> l;
EnumMethodReferenceNotStored(List<Integer> list) {
l = list.stream()
.filter(this::doNotMutate)
.collect(Collectors.toList());
}

private boolean doNotMutate(Integer i) {
return i > 2;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.example.staticinitializers.delayedexecution;

import java.util.function.Supplier;

public enum EnumMixedFields {
A(EnumMixedFields::canMutate, doNotMutate()), B(EnumMixedFields::canMutate, doNotMutate());

private final Supplier<String> supplier;
private final String s;

EnumMixedFields(Supplier<String> supplier, String s) {
this.supplier = supplier;
this.s = s;
}

private static String canMutate() {
return "mutate me"; // mutate
}

private static String doNotMutate() {
return "Do not mutate";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.example.staticinitializers.delayedexecution;

import java.util.function.Function;

public class StaticFunctionField {
private static final Function<String,String> FOO = canMutate();

private static Function<String, String> canMutate() {
// don't mutate
System.out.println("ideally would mutate me");

return s -> s + "foo";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.example.staticinitializers.delayedexecution;

import java.util.function.Supplier;

public class StaticSupplierField {
final static Supplier<String> SUPPLER = canMutate();

private static Supplier<String> canMutate() {
// don't mutate
System.out.println("ideally would mutate me");

return () -> "Do not mutate"; // mutate
}
}
Loading

0 comments on commit 7497570

Please sign in to comment.