From b6236826f4dad25e4ae189cee211e0e6dd4d9a02 Mon Sep 17 00:00:00 2001 From: Henry Coles Date: Tue, 4 Aug 2026 11:18:08 +0100 Subject: [PATCH] filter mutants in enum switch default block Depending on the java version and compiler, a default block may be created to throw a runtime error if a suposedly exhaustive match fails. --- .../javafeatures/EnumSwitchFilter.java | 68 ++++++++++++++++-- .../build/intercept/javafeatures/Canary.java | 7 ++ .../javafeatures/EnumSwitchTest.java | 25 +++++++ .../matching/ExhaustiveSwitch$1.class.bin | Bin 0 -> 820 bytes .../matching/ExhaustiveSwitch.class.bin | Bin 0 -> 901 bytes .../java/com/example/matching/AnEnum.java | 5 ++ .../example/matching/ExhaustiveSwitch.java | 12 ++++ 7 files changed, 113 insertions(+), 4 deletions(-) create mode 100644 pitest-entry/src/test/java/org/pitest/mutationtest/build/intercept/javafeatures/Canary.java create mode 100644 pitest-entry/src/test/resources/sampleClasses/matching/ExhaustiveSwitch$1.class.bin create mode 100644 pitest-entry/src/test/resources/sampleClasses/matching/ExhaustiveSwitch.class.bin create mode 100644 samples/src/main/java/com/example/matching/AnEnum.java create mode 100644 samples/src/main/java/com/example/matching/ExhaustiveSwitch.java diff --git a/pitest-entry/src/main/java/org/pitest/mutationtest/build/intercept/javafeatures/EnumSwitchFilter.java b/pitest-entry/src/main/java/org/pitest/mutationtest/build/intercept/javafeatures/EnumSwitchFilter.java index 44424e03c..43aca3038 100644 --- a/pitest-entry/src/main/java/org/pitest/mutationtest/build/intercept/javafeatures/EnumSwitchFilter.java +++ b/pitest-entry/src/main/java/org/pitest/mutationtest/build/intercept/javafeatures/EnumSwitchFilter.java @@ -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; @@ -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; /** @@ -43,6 +48,8 @@ public class EnumSwitchFilter extends RegionInterceptor { static final Slot START = Slot.create(AbstractInsnNode.class); static final Slot END = Slot.create(AbstractInsnNode.class); + static final Slot DEFAULT = Slot.create(LabelNode.class); + static final Slot DEFAULT_END = Slot.create(AbstractInsnNode.class); static final SequenceMatcher ENUM_SWITCH = QueryStart .any(AbstractInsnNode.class) @@ -50,7 +57,25 @@ public class EnumSwitchFilter extends RegionInterceptor { .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 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()) @@ -65,12 +90,47 @@ private static Match getStatic(String name) { }; } + public static Match 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 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 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 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 store(SlotWrite slot) { diff --git a/pitest-entry/src/test/java/org/pitest/mutationtest/build/intercept/javafeatures/Canary.java b/pitest-entry/src/test/java/org/pitest/mutationtest/build/intercept/javafeatures/Canary.java new file mode 100644 index 000000000..85b97320b --- /dev/null +++ b/pitest-entry/src/test/java/org/pitest/mutationtest/build/intercept/javafeatures/Canary.java @@ -0,0 +1,7 @@ +package org.pitest.mutationtest.build.intercept.javafeatures; + +public class Canary { + public static void mutateThisCall() { + + } +} diff --git a/pitest-entry/src/test/java/org/pitest/mutationtest/build/intercept/javafeatures/EnumSwitchTest.java b/pitest-entry/src/test/java/org/pitest/mutationtest/build/intercept/javafeatures/EnumSwitchTest.java index af9616228..722d31888 100644 --- a/pitest-entry/src/test/java/org/pitest/mutationtest/build/intercept/javafeatures/EnumSwitchTest.java +++ b/pitest-entry/src/test/java/org/pitest/mutationtest/build/intercept/javafeatures/EnumSwitchTest.java @@ -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; @@ -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 { @@ -99,6 +123,7 @@ int foo(Letters i) { case E: return 2; default: + Canary.mutateThisCall(); return 0; } } diff --git a/pitest-entry/src/test/resources/sampleClasses/matching/ExhaustiveSwitch$1.class.bin b/pitest-entry/src/test/resources/sampleClasses/matching/ExhaustiveSwitch$1.class.bin new file mode 100644 index 0000000000000000000000000000000000000000..3a1f69237576bd920678ae84f6c133b8404c18a8 GIT binary patch literal 820 zcma)4+iuf95Ix&CabjF*N-v?M5WsB{3bjCj7cNp&95reZOHF7W`m}LY#Ko7&m(cIw zg=auQ2%&xeABC7TsUi@nkgT0Kqcd~P?)b;gZ|4ABz|t^-f(k`P5haFIHwr9q#)DH| zSOHJnkr%$Pc0xPM0*2C<`YW2o(`x5H23d*Rc&~#jZ&anAUu6;J*S(18V(R=5S zl=+BZ!~C6h_^C-v=ERG6;l;f0GIQLjkoT)P=3p=soUYBVcBv_O*LfY+vB024@z4vo zPo4EfOT`ky$_XEH%jb0AZq&=%QPUIt(2nCMmP%C}H*%%^kt6Y{j++t}`+EnrEi1Ql z+?Exk>AXHrafiW}j&uem!c7^<&t2aOz4T?Sd`RrwXeb!w+FmHSSuhZBpAUSh%(o+# z`-eRCB%Ty!dntF{((6pH){C;(6-`feEdKqX+ftuq+o9`639Ygt(or;2@ql6dzji`~ zyCO;VqJ+HbtuPevuFsQ1Bu0&PT%*5;8CAJunNj5~?ol)ey+?sEp!pT-{JZuU>L>Zl ze!(@x`2b}kAyDVElYW--fQr)(I3ayDTv3`USnvl@?fN5fU8kUJ{T^6z|wSU83 zLE@A8U=vOB-5+H<3vGxEF-~&l&fGia-gEEVU%$Wo0PqZ3CSr)|&@3d76v!UQV_ET} z|F-h7eWaXFAo@$#k4zjC$-5>P?Vs9EIE+L~MYvD3-0xNTqoWIxmLC5u_ zH{sXNafP{NMKpYZ2QqZqo~n6rIIMM9o2myvKQOU~xPfZ|LK?V1)i$s~gc#F zus%2Y#S#UyTEC+NGELW42cuqF1+OHN3gnu7M|y`caHIQ~Q4727ko{}^SM(kJLZJMP z3Iw(b^D)zY>=c``;#*BBI`8;B6NWnO{WT2JPy%MFKMEYR=SGz+ zUFSvIFloLTVhy)AanXuFL^H=d&s&9BP$x>K5T7URHSUoc zh~o}-Yx)3pVe@8U9UHXddHyg72JC);UiylK@<$rE@{|a#Q3{j?l!uhZ-?8}m6iYvI zSI5ZYu8(2nmd8lsZhoRSPLjYfbCV9Xs2@Sc6HfjMq)?@8#%33rlgLHBO(R**HBGDP ZC^5)=nqo{@rq)QqV2u&&=){#Z{s40b%fA2s literal 0 HcmV?d00001 diff --git a/samples/src/main/java/com/example/matching/AnEnum.java b/samples/src/main/java/com/example/matching/AnEnum.java new file mode 100644 index 000000000..1769981f6 --- /dev/null +++ b/samples/src/main/java/com/example/matching/AnEnum.java @@ -0,0 +1,5 @@ +package com.example.matching; + +public enum AnEnum { + ONE, TWO, THREE, FOUR +} diff --git a/samples/src/main/java/com/example/matching/ExhaustiveSwitch.java b/samples/src/main/java/com/example/matching/ExhaustiveSwitch.java new file mode 100644 index 000000000..e76969032 --- /dev/null +++ b/samples/src/main/java/com/example/matching/ExhaustiveSwitch.java @@ -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"; + }; + } +}