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
@@ -1,9 +1,6 @@
package org.pitest.mutationtest.build.intercept.javafeatures;

import org.objectweb.asm.Type;
import org.objectweb.asm.tree.AbstractInsnNode;
import org.objectweb.asm.tree.InvokeDynamicInsnNode;
import org.objectweb.asm.tree.RecordComponentNode;
import org.objectweb.asm.tree.LineNumberNode;
import org.pitest.bytecode.analysis.ClassTree;
import org.pitest.bytecode.analysis.MethodTree;
import org.pitest.mutationtest.build.InterceptorType;
Expand All @@ -12,11 +9,16 @@
import org.pitest.mutationtest.engine.MutationDetails;

import java.util.Collection;
import java.util.Optional;
import java.util.Set;
import java.util.OptionalInt;
import java.util.function.Predicate;
import java.util.stream.Collectors;

/**
* Simple filter for autogenerated methods in records (eg constructors and accessors).
*
* Relies on the compiler including the line number of the line on which the record is declared
* within the autogenerated methods.
*/
public class RecordFilter implements MutationInterceptor {

private boolean isRecord;
Expand All @@ -33,6 +35,7 @@ public void begin(ClassTree clazz) {
currentClass = clazz;
}


@Override
public Collection<MutationDetails> intercept(Collection<MutationDetails> mutations, Mutater m) {
if (isRecord) {
Expand All @@ -44,67 +47,39 @@ public Collection<MutationDetails> intercept(Collection<MutationDetails> mutatio
}

private Predicate<MutationDetails> makeMethodFilter(ClassTree currentClass) {
Set<String> accessorNames = currentClass.recordComponents().stream()
.map(this::toName)
.collect(Collectors.toSet());
return m -> !accessorNames.contains(m.getMethod()) && !isStandardMethod(accessorNames.size(), m);
}

private boolean isStandardMethod(int numberOfComponents, MutationDetails m) {
return isRecordInit(m, numberOfComponents)
|| isRecordEquals(m)
|| isRecordHashCode(m)
|| isRecordToString(m);
}

private boolean isRecordInit(MutationDetails m, int numberOfComponents) {
// constructors with the same airty as the generated ones, but different
// types won't get mutated. They're probably rare enough that this doesn't matter.
int airty = Type.getArgumentTypes(m.getId().getLocation().getMethodDesc()).length;
return m.getMethod().equals("<init>") && airty == numberOfComponents;
}

private boolean isRecordEquals(MutationDetails m) {
return m.getId().getLocation().getMethodDesc().equals("(Ljava/lang/Object;)Z")
&& m.getMethod().equals("equals")
&& hasDynamicObjectMethodsCall(m);
int declarationLine = currentClass.methods().stream()
.map(this::singleLineMethod)
.filter(OptionalInt::isPresent)
.mapToInt(OptionalInt::getAsInt)
.min()
.orElse(-1);

var avoid = currentClass.methods().stream()
.filter( m -> hasLine(m, declarationLine))
.map(MethodTree::asLocation)
.collect(Collectors.toSet());

return m -> !avoid.contains(m.getId().getLocation());
}

private boolean isRecordHashCode(MutationDetails m) {
return m.getId().getLocation().getMethodDesc().equals("()I")
&& m.getMethod().equals("hashCode")
&& hasDynamicObjectMethodsCall(m);
}

private boolean isRecordToString(MutationDetails m) {
return m.getId().getLocation().getMethodDesc().equals("()Ljava/lang/String;")
&& m.getMethod().equals("toString")
&& hasDynamicObjectMethodsCall(m);
}

private boolean hasDynamicObjectMethodsCall(MutationDetails mutation) {
// java/lang/runtime/ObjectMethods was added to support records and can be used as a marker
// for an auth generated equals method. It's not likely that a custom method would
// contain a dynamic call to it
Optional<MethodTree> method = currentClass.method(mutation.getId().getLocation());
return method.filter(m -> m.instructions().stream().anyMatch(this::isInvokeDynamicCallToObjectMethods))
.isPresent();
}

private boolean isInvokeDynamicCallToObjectMethods(AbstractInsnNode node) {
if (node instanceof InvokeDynamicInsnNode) {
InvokeDynamicInsnNode call = (InvokeDynamicInsnNode) node;
return call.bsm.getOwner().equals("java/lang/runtime/ObjectMethods")
&& call.bsm.getName().equals("bootstrap");
// iff the method has only one line, return it
private OptionalInt singleLineMethod(MethodTree methodTree) {
var lines = methodTree.instructions().stream()
.filter(n -> n instanceof LineNumberNode)
.map(n -> ((LineNumberNode) n).line)
.collect(Collectors.toSet());

if (lines.size() == 1) {
return OptionalInt.of(lines.iterator().next());
}
return false;
return OptionalInt.empty();
}

private String toName(RecordComponentNode recordComponentNode) {
return recordComponentNode.name;
private boolean hasLine(MethodTree m, int declarationLine) {
return m.instructions().stream().anyMatch(n -> n instanceof LineNumberNode && ((LineNumberNode) n).line == declarationLine);
}


@Override
public void end() {
currentClass = null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public void findsNoMutantsInPureRecord() {
}

@Test
public void findsMutantsInNormalClass() {
public void filtersNoMutantsFromNormalClass() {
this.verifier.assertFiltersNoMutationsMatching(m -> true, NotARecord.class);
}

Expand Down Expand Up @@ -64,6 +64,11 @@ public void mutatesCustomToStringMethods() {
"RecordWithCustomToString");
}

@Test
public void mutatesCompactConstructor() {
this.verifier.assertFiltersNoMutationsMatching(inMethodCalled("<init>"), "RecordWithCompactConstructor");
}

private Predicate<MutationDetails> removesSysOutCall() {
return m -> m.getDescription().contains("PrintStream::println");
}
Expand Down
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.example;

public record RecordWithCompactConstructor(int quantity) {
public RecordWithCompactConstructor {
if (quantity <= 0) {
System.out.println("mutate me");
throw new IllegalArgumentException("quantity must be positive");
}
}

@Override
public int quantity() {
if (quantity > 100) {
return 100;
}
return quantity;
}
}
Loading