Skip to content

Commit 2044cec

Browse files
committed
feat: support generic constructor based beans
1 parent 57628a7 commit 2044cec

File tree

7 files changed

+177
-29
lines changed

7 files changed

+177
-29
lines changed

src/main/java/com/code_intelligence/jazzer/mutation/mutator/aggregate/AggregatesHelper.java

Lines changed: 19 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@
1818

1919
import static com.code_intelligence.jazzer.mutation.combinator.MutatorCombinators.mutateThenMap;
2020
import static com.code_intelligence.jazzer.mutation.combinator.MutatorCombinators.mutateThenMapToImmutable;
21+
import static com.code_intelligence.jazzer.mutation.mutator.aggregate.BeanSupport.resolveAnnotatedParameterTypes;
22+
import static com.code_intelligence.jazzer.mutation.mutator.aggregate.BeanSupport.resolveParameterTypes;
23+
import static com.code_intelligence.jazzer.mutation.mutator.aggregate.BeanSupport.resolveReturnType;
2124
import static com.code_intelligence.jazzer.mutation.support.PropertyConstraintSupport.propagatePropertyConstraints;
2225
import static com.code_intelligence.jazzer.mutation.support.ReflectionSupport.unreflectMethod;
2326
import static com.code_intelligence.jazzer.mutation.support.ReflectionSupport.unreflectMethods;
@@ -60,25 +63,22 @@ static Optional<SerializingMutator<?>> createMutator(
6063
getters.length, instantiator));
6164
for (int i = 0; i < getters.length; i++) {
6265
Preconditions.check(
63-
getters[i]
64-
.getAnnotatedReturnType()
65-
.getType()
66-
.equals(instantiator.getAnnotatedParameterTypes()[i].getType()),
66+
resolveReturnType(getters[i], initialType)
67+
.equals(resolveParameterTypes(instantiator, initialType)[i]),
6768
String.format(
6869
"Parameter %d of %s does not match return type of %s", i, instantiator, getters[i]));
6970
}
7071

7172
// TODO: Ideally, we would have the mutator framework pass in a Lookup for the fuzz test class.
7273
MethodHandles.Lookup lookup = MethodHandles.lookup();
7374
return createMutator(
74-
factory,
75-
instantiator.getDeclaringClass(),
76-
instantiator.getAnnotatedParameterTypes(),
77-
asInstantiationFunction(lookup, instantiator),
78-
makeSingleGetter(unreflectMethods(lookup, getters)),
79-
initialType,
80-
isImmutable)
81-
.map(m -> m);
75+
factory,
76+
instantiator.getDeclaringClass(),
77+
resolveAnnotatedParameterTypes(instantiator, initialType),
78+
asInstantiationFunction(lookup, instantiator),
79+
makeSingleGetter(unreflectMethods(lookup, getters)),
80+
initialType,
81+
isImmutable);
8282
}
8383

8484
static Optional<SerializingMutator<?>> createMutator(
@@ -105,14 +105,13 @@ static Optional<SerializingMutator<?>> createMutator(
105105
// TODO: Ideally, we would have the mutator framework pass in a Lookup for the fuzz test class.
106106
MethodHandles.Lookup lookup = MethodHandles.lookup();
107107
return createMutator(
108-
factory,
109-
newInstance.getDeclaringClass(),
110-
parameterTypes(setters),
111-
asInstantiationFunction(lookup, newInstance, setters),
112-
makeSingleGetter(unreflectMethods(lookup, getters)),
113-
initialType,
114-
/* isImmutable= */ false)
115-
.map(m -> m);
108+
factory,
109+
newInstance.getDeclaringClass(),
110+
parameterTypes(setters),
111+
asInstantiationFunction(lookup, newInstance, setters),
112+
makeSingleGetter(unreflectMethods(lookup, getters)),
113+
initialType,
114+
/* isImmutable= */ false);
116115
}
117116

118117
@SuppressWarnings("Immutable")

src/main/java/com/code_intelligence/jazzer/mutation/mutator/aggregate/BeanSupport.java

Lines changed: 59 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,14 @@
2626
import static java.util.stream.Collectors.toMap;
2727

2828
import java.beans.Introspector;
29+
import java.lang.reflect.AnnotatedParameterizedType;
30+
import java.lang.reflect.AnnotatedType;
2931
import java.lang.reflect.Constructor;
32+
import java.lang.reflect.Executable;
3033
import java.lang.reflect.Method;
3134
import java.lang.reflect.Modifier;
3235
import java.lang.reflect.Type;
36+
import java.lang.reflect.TypeVariable;
3337
import java.util.Arrays;
3438
import java.util.Comparator;
3539
import java.util.HashMap;
@@ -49,6 +53,54 @@ static Optional<Class<?>> optionalClassForName(String targetClassName) {
4953
}
5054
}
5155

56+
// Returns the resolved type argument for a generic class if one exists.
57+
// For example: For the class `class MyClass<T> {}` with annotated type `MyClass<String>`,
58+
// calling `resolveTypeArgument(MyClass.class, annotatedType, "T")` returns
59+
// `Optional.of(String.class)`.
60+
private static Optional<AnnotatedType> resolveTypeArgument(
61+
Class<?> clazz, AnnotatedType classType, String typeName) {
62+
if (classType instanceof AnnotatedParameterizedType) {
63+
TypeVariable<?>[] typeParameters = clazz.getTypeParameters();
64+
AnnotatedType[] typeArguments =
65+
((AnnotatedParameterizedType) classType).getAnnotatedActualTypeArguments();
66+
for (int i = 0; i < typeParameters.length; i++) {
67+
if (typeParameters[i].getName().equals(typeName)) {
68+
return Optional.of(typeArguments[i]);
69+
}
70+
}
71+
}
72+
return Optional.empty();
73+
}
74+
75+
// Returns the annotated parameter types of a method or constructor resolving all generic type
76+
// arguments.
77+
public static AnnotatedType[] resolveAnnotatedParameterTypes(
78+
Executable e, AnnotatedType classType) {
79+
Type[] generic = e.getGenericParameterTypes();
80+
AnnotatedType[] annotated = e.getAnnotatedParameterTypes();
81+
AnnotatedType[] result = new AnnotatedType[generic.length];
82+
for (int i = 0; i < generic.length; i++) {
83+
result[i] =
84+
resolveTypeArgument(e.getDeclaringClass(), classType, generic[i].getTypeName())
85+
.orElse(annotated[i]);
86+
}
87+
return result;
88+
}
89+
90+
// Returns the parameter types of a method or constructor resolving all generic type arguments.
91+
public static Type[] resolveParameterTypes(Executable e, AnnotatedType classType) {
92+
return stream(resolveAnnotatedParameterTypes(e, classType))
93+
.map(AnnotatedType::getType)
94+
.toArray(Type[]::new);
95+
}
96+
97+
static Type resolveReturnType(Method method, AnnotatedType classType) {
98+
return resolveTypeArgument(
99+
method.getDeclaringClass(), classType, method.getGenericReturnType().getTypeName())
100+
.orElse(method.getAnnotatedReturnType())
101+
.getType();
102+
}
103+
52104
static boolean isConcreteClass(Class<?> clazz) {
53105
return !Modifier.isAbstract(clazz.getModifiers());
54106
}
@@ -88,9 +140,11 @@ static Optional<Method[]> findGettersByPropertyNames(
88140
propertyNames.map(gettersByPropertyName::get).map(Optional::ofNullable), Method[]::new);
89141
}
90142

91-
static Optional<Method[]> findGettersByPropertyTypes(Class<?> clazz, Stream<Class<?>> types) {
92-
Map<Class<?>, List<Method>> gettersByType =
93-
findMethods(clazz, BeanSupport::isGetter).collect(groupingBy(Method::getReturnType));
143+
static Optional<Method[]> findGettersByPropertyTypes(
144+
Class<?> clazz, AnnotatedType classType, Stream<Type> types) {
145+
Map<Type, List<Method>> gettersByType =
146+
findMethods(clazz, BeanSupport::isGetter)
147+
.collect(groupingBy(m -> resolveReturnType(m, classType)));
94148
return toArrayOrEmpty(
95149
types.map(
96150
type -> {
@@ -122,10 +176,10 @@ private static Optional<String> trimPrefix(String name, String prefix) {
122176
}
123177
}
124178

125-
static boolean matchingReturnTypes(Method[] methods, Type[] types) {
179+
static boolean matchingReturnTypes(Method[] methods, AnnotatedType classType, Type[] types) {
126180
for (int i = 0; i < methods.length; i++) {
127181
// TODO: Support Optional<T> getters, which often have a corresponding T setter.
128-
if (!methods[i].getAnnotatedReturnType().getType().equals(types[i])) {
182+
if (!resolveReturnType(methods[i], classType).equals(types[i])) {
129183
return false;
130184
}
131185
}

src/main/java/com/code_intelligence/jazzer/mutation/mutator/aggregate/ConstructorBasedBeanMutatorFactory.java

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import static com.code_intelligence.jazzer.mutation.mutator.aggregate.BeanSupport.findGettersByPropertyNames;
2121
import static com.code_intelligence.jazzer.mutation.mutator.aggregate.BeanSupport.findGettersByPropertyTypes;
2222
import static com.code_intelligence.jazzer.mutation.mutator.aggregate.BeanSupport.matchingReturnTypes;
23+
import static com.code_intelligence.jazzer.mutation.mutator.aggregate.BeanSupport.resolveParameterTypes;
2324
import static com.code_intelligence.jazzer.mutation.support.StreamSupport.findFirstPresent;
2425
import static com.code_intelligence.jazzer.mutation.support.TypeSupport.asSubclassOrEmpty;
2526
import static java.util.Arrays.stream;
@@ -50,11 +51,13 @@ public Optional<SerializingMutator<?>> tryCreate(
5051
.filter(constructor -> constructor.getParameterCount() > 0)
5152
.map(
5253
constructor ->
53-
findParameterGetters(clazz, constructor)
54+
findParameterGetters(clazz, type, constructor)
5455
.filter(
5556
getters ->
5657
matchingReturnTypes(
57-
getters, constructor.getParameterTypes()))
58+
getters,
59+
type,
60+
resolveParameterTypes(constructor, type)))
5861
.flatMap(
5962
getters -> {
6063
// Try to create mutator based on constructor and getters,
@@ -65,7 +68,8 @@ public Optional<SerializingMutator<?>> tryCreate(
6568
}))));
6669
}
6770

68-
private Optional<Method[]> findParameterGetters(Class<?> clazz, Constructor<?> constructor) {
71+
private Optional<Method[]> findParameterGetters(
72+
Class<?> clazz, AnnotatedType type, Constructor<?> constructor) {
6973
// Prefer explicit Java Bean ConstructorProperties annotation to determine parameter names.
7074
ConstructorProperties parameterNames = constructor.getAnnotation(ConstructorProperties.class);
7175
if (parameterNames != null
@@ -78,7 +82,8 @@ private Optional<Method[]> findParameterGetters(Class<?> clazz, Constructor<?> c
7882
return findGettersByPropertyNames(clazz, stream(parameters).map(Parameter::getName));
7983
} else {
8084
// Last fallback to parameter types.
81-
return findGettersByPropertyTypes(clazz, stream(parameters).map(Parameter::getType));
85+
return findGettersByPropertyTypes(
86+
clazz, type, stream(resolveParameterTypes(constructor, type)));
8287
}
8388
}
8489
}

src/main/java/com/code_intelligence/jazzer/mutation/mutator/aggregate/SetterBasedBeanMutatorFactory.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ clazz, stream(setters).map(BeanSupport::toPropertyName))
5454
getters ->
5555
matchingReturnTypes(
5656
getters,
57+
type,
5758
stream(setters)
5859
.map(setter -> setter.getAnnotatedParameterTypes()[0].getType())
5960
.toArray(Type[]::new)))

src/test/java/com/code_intelligence/jazzer/mutation/mutator/StressTest.java

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -405,6 +405,31 @@ public String toString() {
405405
}
406406
}
407407

408+
public static class GenericConstructorBasedBean<T> {
409+
T t;
410+
411+
GenericConstructorBasedBean(T t) {
412+
this.t = t;
413+
}
414+
415+
public T getT() {
416+
return t;
417+
}
418+
419+
@Override
420+
public boolean equals(Object o) {
421+
if (this == o) return true;
422+
if (o == null || getClass() != o.getClass()) return false;
423+
GenericConstructorBasedBean<T> that = (GenericConstructorBasedBean<T>) o;
424+
return Objects.equals(this.t, that.t);
425+
}
426+
427+
@Override
428+
public int hashCode() {
429+
return Objects.hash(this.t);
430+
}
431+
}
432+
408433
public static class OnlyConstructorBean {
409434
private final String foo;
410435
private final List<Integer> bar;
@@ -895,6 +920,13 @@ void singleParam(int parameter) {}
895920
false,
896921
manyDistinctElements(),
897922
manyDistinctElements()),
923+
arguments(
924+
new TypeHolder<
925+
@NotNull GenericConstructorBasedBean<@NotNull String>>() {}.annotatedType(),
926+
"[String] -> GenericConstructorBasedBean",
927+
false,
928+
manyDistinctElements(),
929+
manyDistinctElements()),
898930
arguments(
899931
new TypeHolder<@NotNull OnlyConstructorBean>() {}.annotatedType(),
900932
"[Nullable<String>, Nullable<List<Nullable<Integer>>>, Boolean] -> OnlyConstructorBean",

src/test/java/com/code_intelligence/jazzer/mutation/mutator/aggregate/ConstructorBasedBeanMutatorTest.java

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,4 +241,50 @@ void propagateConstraint() {
241241
assertThat(mutator.toString())
242242
.isEqualTo("[Boolean, String, Integer] -> ConstructorPropertiesAnnotatedBean");
243243
}
244+
245+
public static class CustomPair<L, R> {
246+
L left;
247+
R right;
248+
249+
public CustomPair(L left, R right) {
250+
this.left = left;
251+
this.right = right;
252+
}
253+
254+
public L getLeft() {
255+
return this.left;
256+
}
257+
258+
public R getRight() {
259+
return this.right;
260+
}
261+
}
262+
263+
@Test
264+
void genericClass() {
265+
// Note: We can't use @NotNull here since Java 8 does not retain the annotations for generic
266+
// classes.
267+
SerializingMutator<CustomPair<String, Integer>> mutator =
268+
(SerializingMutator<CustomPair<String, Integer>>)
269+
Mutators.newFactory()
270+
.createOrThrow(new TypeHolder<CustomPair<String, Integer>>() {}.annotatedType());
271+
assertThat(mutator.toString())
272+
.isEqualTo("Nullable<[Nullable<String>, Nullable<Integer>] -> CustomPair>");
273+
}
274+
275+
@Test
276+
void genericClassLayered() {
277+
// Note: We can't use @NotNull here since Java 8 does not retain the annotations for generic
278+
// classes.
279+
SerializingMutator<CustomPair<CustomPair<String, Boolean>, Integer>> mutator2 =
280+
(SerializingMutator<CustomPair<CustomPair<String, Boolean>, Integer>>)
281+
Mutators.newFactory()
282+
.createOrThrow(
283+
new TypeHolder<
284+
CustomPair<CustomPair<String, Boolean>, Integer>>() {}.annotatedType());
285+
assertThat(mutator2.toString())
286+
.isEqualTo(
287+
"Nullable<[Nullable<[Nullable<String>, Nullable<Boolean>] -> CustomPair>,"
288+
+ " Nullable<Integer>] -> CustomPair>");
289+
}
244290
}

src/test/java/com/code_intelligence/jazzer/mutation/mutator/aggregate/RecordMutatorTest.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,4 +175,15 @@ void propagateConstraint() {
175175
assertThat(mutator.toString())
176176
.isEqualTo("[[List<Integer>] -> PropagateInnerTypeRecord] -> PropagateTypeRecord");
177177
}
178+
179+
record GenericRecord<T>(T t) {}
180+
181+
@Test
182+
void testGenericRecord() {
183+
SerializingMutator<GenericRecord<String>> mutator =
184+
(SerializingMutator<GenericRecord<String>>)
185+
Mutators.newFactory()
186+
.createOrThrow(new TypeHolder<@NotNull GenericRecord<String>>() {}.annotatedType());
187+
assertThat(mutator.toString()).isEqualTo("[Nullable<String>] -> GenericRecord");
188+
}
178189
}

0 commit comments

Comments
 (0)