diff --git a/pitest-entry/src/main/java/org/pitest/mutationtest/build/intercept/javafeatures/MethodReferenceNullCheckFilter.java b/pitest-entry/src/main/java/org/pitest/mutationtest/build/intercept/javafeatures/MethodReferenceNullCheckFilter.java new file mode 100644 index 000000000..4c9a0432c --- /dev/null +++ b/pitest-entry/src/main/java/org/pitest/mutationtest/build/intercept/javafeatures/MethodReferenceNullCheckFilter.java @@ -0,0 +1,92 @@ +package org.pitest.mutationtest.build.intercept.javafeatures; + +import static org.pitest.bytecode.analysis.InstructionMatchers.anyInstruction; +import static org.pitest.bytecode.analysis.InstructionMatchers.isInstruction; +import static org.pitest.bytecode.analysis.InstructionMatchers.methodCallTo; +import static org.pitest.bytecode.analysis.InstructionMatchers.notAnInstruction; +import static org.pitest.bytecode.analysis.InstructionMatchers.opCode; + +import java.util.Collection; +import java.util.Objects; +import java.util.function.Predicate; + +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.tree.AbstractInsnNode; +import org.pitest.bytecode.analysis.ClassTree; +import org.pitest.bytecode.analysis.MethodMatchers; +import org.pitest.bytecode.analysis.MethodTree; +import org.pitest.classinfo.ClassName; +import org.pitest.functional.FCollection; +import org.pitest.functional.prelude.Prelude; +import org.pitest.mutationtest.build.InterceptorType; +import org.pitest.mutationtest.build.MutationInterceptor; +import org.pitest.mutationtest.engine.Mutater; +import org.pitest.mutationtest.engine.MutationDetails; +import org.pitest.sequence.Context; +import org.pitest.sequence.QueryParams; +import org.pitest.sequence.QueryStart; +import org.pitest.sequence.SequenceMatcher; +import org.pitest.sequence.Slot; + +/** + * Filters out the calls to Objects.requireNotNull the compiler inserts when using method references. + * + */ +public class MethodReferenceNullCheckFilter implements MutationInterceptor { + + private static final boolean DEBUG = false; + + private static final Slot MUTATED_INSTRUCTION = Slot.create(AbstractInsnNode.class); + + static final SequenceMatcher NULL_CHECK = QueryStart + .any(AbstractInsnNode.class) + .then(methodCallTo(ClassName.fromClass(Objects.class), "requireNonNull").and(isInstruction(MUTATED_INSTRUCTION.read()))) + .then(opCode(Opcodes.POP)) + .then(opCode(Opcodes.INVOKEDYNAMIC)) + .zeroOrMore(QueryStart.match(anyInstruction())) + .compile(QueryParams.params(AbstractInsnNode.class) + .withIgnores(notAnInstruction()) + .withDebug(DEBUG) + ); + + + private ClassTree currentClass; + + @Override + public InterceptorType type() { + return InterceptorType.FILTER; + } + + @Override + public void begin(ClassTree clazz) { + this.currentClass = clazz; + } + + @Override + public Collection intercept( + Collection mutations, Mutater m) { + return FCollection.filter(mutations, Prelude.not(isAnImplicitNullCheck())); + } + + private Predicate isAnImplicitNullCheck() { + return a -> { + final int instruction = a.getInstructionIndex(); + final MethodTree method = MethodReferenceNullCheckFilter.this.currentClass.methods().stream() + .filter(MethodMatchers.forLocation(a.getId().getLocation())) + .findFirst() + .get(); + + final AbstractInsnNode mutatedInstruction = method.instruction(instruction); + + final Context context = Context.start(method.instructions(), DEBUG); + context.store(MUTATED_INSTRUCTION.write(), mutatedInstruction); + return NULL_CHECK.matches(method.instructions(), context); + }; + } + + @Override + public void end() { + this.currentClass = null; + } + +} diff --git a/pitest-entry/src/main/java/org/pitest/mutationtest/build/intercept/javafeatures/MethodReferenceNullCheckFilterFactory.java b/pitest-entry/src/main/java/org/pitest/mutationtest/build/intercept/javafeatures/MethodReferenceNullCheckFilterFactory.java new file mode 100644 index 000000000..6fd0505fa --- /dev/null +++ b/pitest-entry/src/main/java/org/pitest/mutationtest/build/intercept/javafeatures/MethodReferenceNullCheckFilterFactory.java @@ -0,0 +1,27 @@ +package org.pitest.mutationtest.build.intercept.javafeatures; + +import org.pitest.mutationtest.build.InterceptorParameters; +import org.pitest.mutationtest.build.MutationInterceptor; +import org.pitest.mutationtest.build.MutationInterceptorFactory; +import org.pitest.plugin.Feature; + +public class MethodReferenceNullCheckFilterFactory implements MutationInterceptorFactory { + + @Override + public String description() { + return "Method reference null check filter"; + } + + @Override + public MutationInterceptor createInterceptor(InterceptorParameters params) { + return new MethodReferenceNullCheckFilter(); + } + + @Override + public Feature provides() { + return Feature.named("FMRNULL") + .withOnByDefault(true) + .withDescription("Filters mutations in compiler generated code that inserts Objects.requireNonNull for method references"); + } + +} \ No newline at end of file diff --git a/pitest-entry/src/main/resources/META-INF/services/org.pitest.mutationtest.build.MutationInterceptorFactory b/pitest-entry/src/main/resources/META-INF/services/org.pitest.mutationtest.build.MutationInterceptorFactory index 54c0faedb..99eddcc62 100644 --- a/pitest-entry/src/main/resources/META-INF/services/org.pitest.mutationtest.build.MutationInterceptorFactory +++ b/pitest-entry/src/main/resources/META-INF/services/org.pitest.mutationtest.build.MutationInterceptorFactory @@ -6,6 +6,7 @@ org.pitest.mutationtest.build.intercept.annotations.ExcludedAnnotationIntercepto org.pitest.mutationtest.build.intercept.javafeatures.InlinedFinallyBlockFilterFactory org.pitest.mutationtest.build.intercept.javafeatures.TryWithResourcesFilterFactory org.pitest.mutationtest.build.intercept.javafeatures.ImplicitNullCheckFilterFactory +org.pitest.mutationtest.build.intercept.javafeatures.MethodReferenceNullCheckFilterFactory org.pitest.mutationtest.build.intercept.javafeatures.ForEachLoopFilterFactory org.pitest.mutationtest.build.intercept.logging.LoggingCallsFilterFactory org.pitest.mutationtest.build.intercept.timeout.InfiniteForLoopFilterFactory diff --git a/pitest-entry/src/test/java/org/pitest/mutationtest/build/MutationDiscoveryTest.java b/pitest-entry/src/test/java/org/pitest/mutationtest/build/MutationDiscoveryTest.java index 098350cb1..3c3cac21b 100644 --- a/pitest-entry/src/test/java/org/pitest/mutationtest/build/MutationDiscoveryTest.java +++ b/pitest-entry/src/test/java/org/pitest/mutationtest/build/MutationDiscoveryTest.java @@ -157,6 +157,21 @@ public void shouldFilterImplicitNullChecksInLambdas() { assertThat(foundWhenDisabled.size()).isGreaterThan(foundByDefault.size()); } + + @Test + public void shouldFilterObjectsRequireNonNullCallsForMethodReferences() { + final ClassName clazz = ClassName.fromString("requirenotnull/MethodReferenceNullChecks_javac"); + + this.data.setMutators(Collections.singletonList("ALL")); + + final Collection foundByDefault = findMutants(clazz); + + this.data.setFeatures(Collections.singletonList("-FMRNULL")); + + final Collection foundWhenDisabled = findMutants(clazz); + + assertThat(foundWhenDisabled.size()).isGreaterThan(foundByDefault.size()); + } @Test public void shouldFilterMutationsToForLoopIncrements() { diff --git a/pitest-entry/src/test/java/org/pitest/mutationtest/build/intercept/javafeatures/MethodReferenceNullCheckFilterTest.java b/pitest-entry/src/test/java/org/pitest/mutationtest/build/intercept/javafeatures/MethodReferenceNullCheckFilterTest.java new file mode 100644 index 000000000..f44c76150 --- /dev/null +++ b/pitest-entry/src/test/java/org/pitest/mutationtest/build/intercept/javafeatures/MethodReferenceNullCheckFilterTest.java @@ -0,0 +1,44 @@ +package org.pitest.mutationtest.build.intercept.javafeatures; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Objects; + +import org.junit.Test; +import org.pitest.mutationtest.build.InterceptorType; +import org.pitest.mutationtest.engine.gregor.config.Mutator; + +public class MethodReferenceNullCheckFilterTest { + + private static final String PATH = "requirenotnull/{0}_{1}"; + + MethodReferenceNullCheckFilter testee = new MethodReferenceNullCheckFilter(); + + FilterTester verifier = new FilterTester(PATH, this.testee, Mutator.all()); + + @Test + public void shouldDeclareTypeAsFilter() { + assertThat(this.testee.type()).isEqualTo(InterceptorType.FILTER); + } + + @Test + public void filtersRequireNotNullChecksForMethodReferences() { + this.verifier.assertFiltersNMutationFromSample(2, "MethodReferenceNullChecks"); + } + + @Test + public void shouldNotFilterDeadCallsToGetClassInNonLambdaMethods() { + this.verifier.assertFiltersNMutationFromClass(0, HasNormalRequireNonNullCheck.class); + } + +} + + +class HasNormalRequireNonNullCheck { + String aField; + public void amethod() { + Objects.requireNonNull(aField); + System.out.println(aField); + } +} + diff --git a/pitest-entry/src/test/resources/sampleClasses/requirenotnull/MethodReferenceNullChecks_javac.class.bin b/pitest-entry/src/test/resources/sampleClasses/requirenotnull/MethodReferenceNullChecks_javac.class.bin new file mode 100644 index 000000000..916ae8064 Binary files /dev/null and b/pitest-entry/src/test/resources/sampleClasses/requirenotnull/MethodReferenceNullChecks_javac.class.bin differ