Skip to content

Commit

Permalink
mutation: Add support for value-based setters
Browse files Browse the repository at this point in the history
  • Loading branch information
fmeum authored and kyakdan committed Aug 19, 2024
1 parent a7c72bf commit 90d0a84
Show file tree
Hide file tree
Showing 3 changed files with 109 additions and 20 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import com.code_intelligence.jazzer.mutation.support.Preconditions;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.reflect.AnnotatedType;
import java.lang.reflect.Constructor;
import java.lang.reflect.Executable;
Expand Down Expand Up @@ -159,17 +160,39 @@ private static <R> Optional<SerializingMutator<R>> createChecked(

private static <R> Function<Object[], R> makeInstantiator(
MethodHandle newInstance, MethodHandle... setters) {
return objects -> {
try {
R instance = (R) newInstance.invoke();
for (int i = 0; i < setters.length; i++) {
setters[i].invoke(instance, objects[i]);
boolean settersAreChainable =
stream(setters)
.map(MethodHandle::type)
.map(MethodType::returnType)
.allMatch(returnType -> returnType.equals(newInstance.type().returnType()));
// If all setters are chainable, it's possible that the object is actually immutable and the
// setters return a new instance. In that case, we need to chain the setters in the instantiator
// or we will always return the default instance.
if (settersAreChainable) {
return objects -> {
try {
R instance = (R) newInstance.invoke();
for (int i = 0; i < setters.length; i++) {
instance = (R) setters[i].invoke(instance, objects[i]);
}
return instance;
} catch (Throwable e) {
throw new RuntimeException(e);
}
return instance;
} catch (Throwable e) {
throw new RuntimeException(e);
}
};
};
} else {
return objects -> {
try {
R instance = (R) newInstance.invoke();
for (int i = 0; i < setters.length; i++) {
setters[i].invoke(instance, objects[i]);
}
return instance;
} catch (Throwable e) {
throw new RuntimeException(e);
}
};
}
}

private static MethodHandle unreflectNewInstance(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

package com.code_intelligence.jazzer.mutation.mutator.aggregate;

import static com.code_intelligence.jazzer.mutation.support.StreamSupport.getOrEmpty;
import static com.code_intelligence.jazzer.mutation.support.StreamSupport.toArrayOrEmpty;
import static com.code_intelligence.jazzer.mutation.support.TypeSupport.asSubclassOrEmpty;
import static java.util.Arrays.stream;
Expand All @@ -24,6 +25,7 @@
import java.lang.reflect.Modifier;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Stream;

final class BeanMutatorFactory implements MutatorFactory {
@Override
Expand All @@ -50,12 +52,18 @@ public Optional<SerializingMutator<?>> tryCreate(
Method[] setters =
stream(clazz.getMethods())
.filter(method -> method.getParameterCount() == 1)
// Allow chainable setters.
// Allow chainable setters. The "withX" setters are commonly used on immutable
// types and return a new instance, so for those we need to assert that the
// return type is the same as the class.
.filter(
method ->
method.getReturnType().equals(void.class)
|| method.getReturnType().equals(clazz))
.filter(method -> method.getName().startsWith("set"))
.filter(
method ->
method.getName().startsWith("set")
|| (method.getName().startsWith("with")
&& method.getReturnType().equals(clazz)))
// Sort for deterministic ordering.
.sorted(comparing(Method::getName))
.toArray(Method[]::new);
Expand Down Expand Up @@ -111,15 +119,17 @@ public Optional<SerializingMutator<?>> tryCreate(
}

private static String getPropertyName(Method method) {
String name = method.getName();
if (name.startsWith("get")) {
return name.substring("get".length());
} else if (name.startsWith("set")) {
return name.substring("set".length());
} else if (name.startsWith("is")) {
return name.substring("is".length());
return Stream.of("get", "set", "is", "with")
.flatMap(prefix -> getOrEmpty(trimPrefix(method.getName(), prefix)))
.findFirst()
.orElseThrow(() -> new AssertionError("Unexpected method name: " + method.getName()));
}

private static Optional<String> trimPrefix(String name, String prefix) {
if (name.startsWith(prefix)) {
return Optional.of(name.substring(prefix.length()));
} else {
throw new AssertionError("Unexpected method name: " + name);
return Optional.empty();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,55 @@ public String toString() {
}
}

public static class ImmutableBuilder {
private final int i;
private final boolean b;

public ImmutableBuilder() {
this(0, false);
}

private ImmutableBuilder(int i, boolean b) {
this.i = i;
this.b = b;
}

public int getI() {
return i;
}

public boolean isB() {
return b;
}

public ImmutableBuilder withI(int i) {
return new ImmutableBuilder(i, b);
}

// Both withX and setX are supported on immutable builders.
public ImmutableBuilder setB(boolean b) {
return new ImmutableBuilder(i, b);
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof ImmutableBuilder)) return false;
ImmutableBuilder that = (ImmutableBuilder) o;
return i == that.i && b == that.b;
}

@Override
public int hashCode() {
return Objects.hash(i, b);
}

@Override
public String toString() {
return "ImmutableBuilder{" + "i=" + i + ", b=" + b + '}';
}
}

@SuppressWarnings("unused")
static Message getTestProtobufDefaultInstance() {
return TestProtobuf.getDefaultInstance();
Expand Down Expand Up @@ -491,6 +540,13 @@ void singleParam(int parameter) {}
false,
// Low due to recursion breaking initializing nested structs to null.
distinctElementsRatio(0.22),
manyDistinctElements()),
arguments(
new TypeHolder<@NotNull ImmutableBuilder>() {}.annotatedType(),
"[Boolean, Integer] -> ImmutableBuilder",
true,
// Low due to int and boolean fields having very few common values during init.
distinctElementsRatio(0.23),
manyDistinctElements()));
}

Expand Down

0 comments on commit 90d0a84

Please sign in to comment.