Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

import org.objectweb.asm.tree.AbstractInsnNode;
import org.objectweb.asm.tree.FieldInsnNode;
import org.objectweb.asm.tree.LabelNode;
import org.objectweb.asm.tree.LookupSwitchInsnNode;
import org.objectweb.asm.tree.TableSwitchInsnNode;
import org.pitest.bytecode.analysis.MethodTree;
import org.pitest.mutationtest.build.intercept.Region;
import org.pitest.mutationtest.build.intercept.RegionInterceptor;
Expand All @@ -13,16 +16,18 @@
import org.pitest.sequence.Slot;
import org.pitest.sequence.SlotWrite;

import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;

import static org.pitest.bytecode.analysis.InstructionMatchers.anyInstruction;
import static org.pitest.bytecode.analysis.InstructionMatchers.labelNode;
import static org.pitest.bytecode.analysis.InstructionMatchers.methodCallNamed;
import static org.pitest.bytecode.analysis.InstructionMatchers.notAnInstruction;
import static org.pitest.bytecode.analysis.OpcodeMatchers.ALOAD;
import static org.pitest.bytecode.analysis.OpcodeMatchers.ATHROW;
import static org.pitest.bytecode.analysis.OpcodeMatchers.IALOAD;
import static org.pitest.bytecode.analysis.OpcodeMatchers.LOOKUPSWITCH;
import static org.pitest.bytecode.analysis.OpcodeMatchers.TABLESWITCH;
import static org.pitest.sequence.Result.result;

/**
Expand All @@ -43,14 +48,34 @@
public class EnumSwitchFilter extends RegionInterceptor {
static final Slot<AbstractInsnNode> START = Slot.create(AbstractInsnNode.class);
static final Slot<AbstractInsnNode> END = Slot.create(AbstractInsnNode.class);
static final Slot<LabelNode> DEFAULT = Slot.create(LabelNode.class);
static final Slot<AbstractInsnNode> DEFAULT_END = Slot.create(AbstractInsnNode.class);

static final SequenceMatcher<AbstractInsnNode> ENUM_SWITCH = QueryStart
.any(AbstractInsnNode.class)
.then(getStatic("$SwitchMap$").and(store(START.write())))
.then(ALOAD)
.then(methodCallNamed("ordinal"))
.then(IALOAD.and(store(END.write())))
.then(LOOKUPSWITCH.or(TABLESWITCH))
.then(lookupSwitch().or(tableSwitch()))
.zeroOrMore(QueryStart.match(anyInstruction()))
.compile(QueryParams.params(AbstractInsnNode.class)
.withIgnores(notAnInstruction())
);

// detects default handlers that look like autogenerated ones
static final SequenceMatcher<AbstractInsnNode> ENUM_SWITCH_DEFAULT = QueryStart
.any(AbstractInsnNode.class)
.then(getStatic("$SwitchMap$").and(store(START.write())))
.then(ALOAD)
.then(methodCallNamed("ordinal"))
.then(IALOAD.and(store(END.write())))
.then(lookupSwitch().or(tableSwitch()))
.zeroOrMore(QueryStart.match(anyInstruction()))
.then(labelNode(DEFAULT.read()))
.oneOrMore(QueryStart.match(anyInstruction()))
// this will also match if a custom handler has a throw, but hopefully that is rare
.then(ATHROW.and(store(DEFAULT_END.write())))
.zeroOrMore(QueryStart.match(anyInstruction()))
.compile(QueryParams.params(AbstractInsnNode.class)
.withIgnores(notAnInstruction())
Expand All @@ -65,12 +90,47 @@ private static Match<AbstractInsnNode> getStatic(String name) {
};
}

public static Match<AbstractInsnNode> lookupSwitch() {
return (c, t) -> {
if ( t instanceof LookupSwitchInsnNode) {
final LookupSwitchInsnNode call = (LookupSwitchInsnNode) t;
return result(true, c.store(DEFAULT.write(), call.dflt));
}
return result(false, c);
};
}

public static Match<AbstractInsnNode> tableSwitch() {
return (c, t) -> {
if ( t instanceof TableSwitchInsnNode) {
final TableSwitchInsnNode call = (TableSwitchInsnNode) t;
return result(true, c.store(DEFAULT.write(), call.dflt));
}
return result(false, c);
};
}


protected List<Region> computeRegions(MethodTree method) {
Context context = Context.start();
return ENUM_SWITCH.contextMatches(method.instructions(), context).stream()
var matches = ENUM_SWITCH.contextMatches(method.instructions(), context);
var body = matches.stream()
.map(c -> new Region(c.retrieve(START.read()).get(), c.retrieve(END.read()).get()))
.collect(Collectors.toList());
if (!body.isEmpty()) {
body.addAll(defaultHandler(method));
return body;
}

return Collections.emptyList();
}

private Collection<Region> defaultHandler(MethodTree method) {
Context context = Context.start();
var matches = ENUM_SWITCH_DEFAULT.contextMatches(method.instructions(), context);
return matches.stream()
.map(c -> new Region(c.retrieve(DEFAULT.read()).get(), c.retrieve(DEFAULT_END.read()).get()))
.collect(Collectors.toList());
}

private static Match<AbstractInsnNode> store(SlotWrite<AbstractInsnNode> slot) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package org.pitest.mutationtest.build.intercept.javafeatures;

public class Canary {
public static void mutateThisCall() {

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@
import org.pitest.verifier.interceptors.InterceptorVerifier;
import org.pitest.verifier.interceptors.VerifierStart;

import static org.pitest.bytecode.analysis.InstructionMatchers.methodCallNamed;
import static org.pitest.bytecode.analysis.InstructionMatchers.methodCallTo;
import static org.pitest.bytecode.analysis.InstructionMatchers.opCode;
import static org.pitest.bytecode.analysis.OpcodeMatchers.ATHROW;
import static org.pitest.bytecode.analysis.OpcodeMatchers.GETSTATIC;
import static org.pitest.bytecode.analysis.OpcodeMatchers.LOOKUPSWITCH;
import static org.pitest.bytecode.analysis.OpcodeMatchers.TABLESWITCH;
Expand Down Expand Up @@ -70,6 +73,27 @@ public void leavesLookupSwitchStatement() {
.noMutantsAreFiltered()
.verify();
}

@Test
public void filtersMutantsInAutogeneratedDefaultHandler() {
v.usingResourceFolder("matching")
.forClass("ExhaustiveSwitch")
.forCodeMatching(ATHROW.asPredicate())
.mutantsAreGenerated()
.allMutantsAreFiltered()
.verify();

}

@Test
public void doesNotFilterHandRolledDefaultHandler() {
v.forClass(HasEnumTableSwitch.class)
.forCodeMatching(methodCallNamed("mutateThisCall").asPredicate())
.mutantsAreGenerated()
.noMutantsAreFiltered()
.verify();

}
}

class NormalSwitch {
Expand Down Expand Up @@ -99,6 +123,7 @@ int foo(Letters i) {
case E:
return 2;
default:
Canary.mutateThisCall();
return 0;
}
}
Expand Down
Binary file not shown.
Binary file not shown.
5 changes: 5 additions & 0 deletions samples/src/main/java/com/example/matching/AnEnum.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.example.matching;

public enum AnEnum {
ONE, TWO, THREE, FOUR
}
12 changes: 12 additions & 0 deletions samples/src/main/java/com/example/matching/ExhaustiveSwitch.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.example.matching;

public class ExhaustiveSwitch {
private static String render(AnEnum e) {
return switch (e) {
case ONE -> "a";
case TWO -> "b";
case THREE -> "c";
case FOUR -> "d";
};
}
}
Loading