Skip to content

Commit

Permalink
identify fields with a prescan
Browse files Browse the repository at this point in the history
  • Loading branch information
hcoles committed Aug 29, 2024
1 parent 54dd01a commit 45d9f34
Show file tree
Hide file tree
Showing 7 changed files with 163 additions and 16 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -53,11 +56,11 @@
* 1. In a static initializer (i.e <clinit>)
* 2. In a private method or constructor called from <clinit> or another private method in the call tree
*
*
*/
class StaticInitializerInterceptor implements MutationInterceptor {

static final Slot<AbstractInsnNode> START = Slot.create(AbstractInsnNode.class);
static final Slot<Set<String>> DELAYED_EXECUTION_FIELDS = Slot.createSet(String.class);

static final SequenceMatcher<AbstractInsnNode> DELAYED_EXECUTION = QueryStart
.any(AbstractInsnNode.class)
Expand All @@ -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<AbstractInsnNode> delayedExecutionField() {
return PUTSTATIC.and(isAUtilFunctionField());
private static Match<AbstractInsnNode> delayedExecutionField(SlotRead<Set<String>> delayedFields) {
return PUTSTATIC.and(isADelayedExecutionField(delayedFields));
}

private static Match<AbstractInsnNode> isAUtilFunctionField() {
private static Match<AbstractInsnNode> isADelayedExecutionField(SlotRead<Set<String>> 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<AbstractInsnNode> 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<AbstractInsnNode> 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<AbstractInsnNode> enumConstructorCallAndStore() {
return QueryStart.match(methodCallNamed("<init>")).then(PUTSTATIC);
}
Expand Down Expand Up @@ -126,14 +134,23 @@ private void analyseClass(ClassTree tree) {
final Optional<MethodTree> clinit = tree.methods().stream().filter(nameEquals("<clinit>")).findFirst();

if (clinit.isPresent()) {

// Find delayed execution fields (Function, Supplier, List<Supplier> etc). Full generic signature is available in the
// declaration, but not on call.
Set<String> 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<Location> privateMethods = tree.methods().stream()
.filter(MethodTree::isPrivate)
.map(MethodTree::asLocation)
.collect(Collectors.toSet());

Set<Call> storedToSupplier = findCallsStoredToDelayedExecutionCode(tree);
Set<Call> 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
Expand All @@ -153,20 +170,25 @@ private void analyseClass(ClassTree tree) {
}
}

private Set<Call> findCallsStoredToDelayedExecutionCode(ClassTree tree) {
return new HashSet<>(privateAndClinitCallsToDelayedExecutionCode(tree));
private boolean isDelayedExecutionField(FieldNode fieldNode) {
return SignatureParser.extractTypes(fieldNode.signature).stream()
.anyMatch(StaticInitializerInterceptor::isADelayedExecutionType);
}

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


private Set<Call> privateAndClinitCallsToDelayedExecutionCode(ClassTree tree) {
private Set<Call> privateAndClinitCallsToDelayedExecutionCode(ClassTree tree, Set<String> delayedExecutionFields) {
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)))
.flatMap(m -> delayedExecutionCall(m, delayedExecutionFields).stream().map(c -> new Call(m.asLocation(), c)))
.collect(Collectors.toSet());
}

private List<Location> delayedExecutionCall(MethodTree method) {
Context context = Context.start();
private List<Location> delayedExecutionCall(MethodTree method, Set<String> 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)
Expand Down
5 changes: 5 additions & 0 deletions pitest-entry/src/main/java/org/pitest/sequence/Slot.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.pitest.sequence;

import java.util.List;
import java.util.Set;

public final class Slot<T> {
public static <T> Slot<T> create(Class<T> clazz) {
Expand All @@ -11,6 +12,10 @@ public static <T> Slot<List<T>> createList(Class<T> clazz) {
return new Slot<>();
}

public static <T> Slot<Set<T>> createSet(Class<T> clazz) {
return new Slot<>();
}

public SlotWrite<T> write() {
return new SlotWrite<>(this);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String>> supplier;

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

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

}
Original file line number Diff line number Diff line change
@@ -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<Function<Integer, Integer>> FUNCTIONS =
asList(StaticListOfFunctions::canMutate, StaticListOfFunctions::canAlsoMutate);

private static Integer canMutate(Integer a) {
return a + 1;
}

private static Integer canAlsoMutate(Integer a) {
return a + 2;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<MutationDetails> inMethod(String name, String desc) {
return m -> m.getMethod().equals(name) && m.getId().getLocation().getMethodDesc().equals(desc);
}
Expand Down
31 changes: 31 additions & 0 deletions pitest/src/main/java/org/pitest/bytecode/SignatureParser.java
Original file line number Diff line number Diff line change
@@ -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<String> extractTypes(String signature) {
if (signature == null) {
return Collections.emptySet();
}

Set<String> 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;
}
}
25 changes: 25 additions & 0 deletions pitest/src/test/java/org/pitest/bytecode/SignatureParserTest.java
Original file line number Diff line number Diff line change
@@ -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<Ljava/lang/String;>;";
assertThat(SignatureParser.extractTypes(signature)).containsExactlyInAnyOrder("java/util/function/Supplier", "java/lang/String");
}

@Test
public void parsesListsOfFunctions() {
String signature = "Ljava/util/List<Ljava/util/function/Function<Ljava/lang/Integer;Ljava/lang/Integer;>;>;";
assertThat(SignatureParser.extractTypes(signature)).containsExactlyInAnyOrder("java/util/List", "java/util/function/Function", "java/lang/Integer");
}
}

0 comments on commit 45d9f34

Please sign in to comment.