diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/mutator/aggregate/AggregatesHelper.java b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/aggregate/AggregatesHelper.java index 33248356b..aa20fd291 100644 --- a/src/main/java/com/code_intelligence/jazzer/mutation/mutator/aggregate/AggregatesHelper.java +++ b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/aggregate/AggregatesHelper.java @@ -18,6 +18,9 @@ import static com.code_intelligence.jazzer.mutation.combinator.MutatorCombinators.mutateThenMap; import static com.code_intelligence.jazzer.mutation.combinator.MutatorCombinators.mutateThenMapToImmutable; +import static com.code_intelligence.jazzer.mutation.mutator.aggregate.BeanSupport.resolveAnnotatedParameterTypes; +import static com.code_intelligence.jazzer.mutation.mutator.aggregate.BeanSupport.resolveParameterTypes; +import static com.code_intelligence.jazzer.mutation.mutator.aggregate.BeanSupport.resolveReturnType; import static com.code_intelligence.jazzer.mutation.support.PropertyConstraintSupport.propagatePropertyConstraints; import static com.code_intelligence.jazzer.mutation.support.ReflectionSupport.unreflectMethod; import static com.code_intelligence.jazzer.mutation.support.ReflectionSupport.unreflectMethods; @@ -60,10 +63,8 @@ static Optional> createMutator( getters.length, instantiator)); for (int i = 0; i < getters.length; i++) { Preconditions.check( - getters[i] - .getAnnotatedReturnType() - .getType() - .equals(instantiator.getAnnotatedParameterTypes()[i].getType()), + resolveReturnType(getters[i], initialType) + .equals(resolveParameterTypes(instantiator, initialType)[i]), String.format( "Parameter %d of %s does not match return type of %s", i, instantiator, getters[i])); } @@ -71,14 +72,13 @@ static Optional> createMutator( // TODO: Ideally, we would have the mutator framework pass in a Lookup for the fuzz test class. MethodHandles.Lookup lookup = MethodHandles.lookup(); return createMutator( - factory, - instantiator.getDeclaringClass(), - instantiator.getAnnotatedParameterTypes(), - asInstantiationFunction(lookup, instantiator), - makeSingleGetter(unreflectMethods(lookup, getters)), - initialType, - isImmutable) - .map(m -> m); + factory, + instantiator.getDeclaringClass(), + resolveAnnotatedParameterTypes(instantiator, initialType), + asInstantiationFunction(lookup, instantiator), + makeSingleGetter(unreflectMethods(lookup, getters)), + initialType, + isImmutable); } static Optional> createMutator( @@ -94,10 +94,8 @@ static Optional> createMutator( getters.length, setters.length)); for (int i = 0; i < getters.length; i++) { Preconditions.check( - getters[i] - .getAnnotatedReturnType() - .getType() - .equals(setters[i].getAnnotatedParameterTypes()[0].getType()), + resolveReturnType(getters[i], initialType) + .equals(resolveParameterTypes(setters[i], initialType)[0]), String.format( "Parameter of %s does not match return type of %s", setters[i], getters[i])); } @@ -105,14 +103,13 @@ static Optional> createMutator( // TODO: Ideally, we would have the mutator framework pass in a Lookup for the fuzz test class. MethodHandles.Lookup lookup = MethodHandles.lookup(); return createMutator( - factory, - newInstance.getDeclaringClass(), - parameterTypes(setters), - asInstantiationFunction(lookup, newInstance, setters), - makeSingleGetter(unreflectMethods(lookup, getters)), - initialType, - /* isImmutable= */ false) - .map(m -> m); + factory, + newInstance.getDeclaringClass(), + parameterTypes(setters, initialType), + asInstantiationFunction(lookup, newInstance, setters), + makeSingleGetter(unreflectMethods(lookup, getters)), + initialType, + /* isImmutable= */ false); } @SuppressWarnings("Immutable") @@ -221,9 +218,9 @@ private static Function asInstantiatorFunction( } } - static AnnotatedType[] parameterTypes(Method[] methods) { + static AnnotatedType[] parameterTypes(Method[] methods, AnnotatedType classType) { return stream(methods) - .map(Method::getAnnotatedParameterTypes) + .map(m -> resolveAnnotatedParameterTypes(m, classType)) .flatMap(Arrays::stream) .toArray(AnnotatedType[]::new); } diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/mutator/aggregate/BeanSupport.java b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/aggregate/BeanSupport.java index fd8b0ff52..1b75a9eff 100644 --- a/src/main/java/com/code_intelligence/jazzer/mutation/mutator/aggregate/BeanSupport.java +++ b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/aggregate/BeanSupport.java @@ -26,10 +26,14 @@ import static java.util.stream.Collectors.toMap; import java.beans.Introspector; +import java.lang.reflect.AnnotatedParameterizedType; +import java.lang.reflect.AnnotatedType; import java.lang.reflect.Constructor; +import java.lang.reflect.Executable; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.lang.reflect.Type; +import java.lang.reflect.TypeVariable; import java.util.Arrays; import java.util.Comparator; import java.util.HashMap; @@ -49,6 +53,54 @@ static Optional> optionalClassForName(String targetClassName) { } } + // Returns the resolved type argument for a generic class if one exists. + // For example: For the class `class MyClass {}` with annotated type `MyClass`, + // calling `resolveTypeArgument(MyClass.class, annotatedType, "T")` returns + // `Optional.of(String.class)`. + private static Optional resolveTypeArgument( + Class clazz, AnnotatedType classType, String typeName) { + if (classType instanceof AnnotatedParameterizedType) { + TypeVariable[] typeParameters = clazz.getTypeParameters(); + AnnotatedType[] typeArguments = + ((AnnotatedParameterizedType) classType).getAnnotatedActualTypeArguments(); + for (int i = 0; i < typeParameters.length; i++) { + if (typeParameters[i].getName().equals(typeName)) { + return Optional.of(typeArguments[i]); + } + } + } + return Optional.empty(); + } + + // Returns the annotated parameter types of a method or constructor resolving all generic type + // arguments. + public static AnnotatedType[] resolveAnnotatedParameterTypes( + Executable e, AnnotatedType classType) { + Type[] generic = e.getGenericParameterTypes(); + AnnotatedType[] annotated = e.getAnnotatedParameterTypes(); + AnnotatedType[] result = new AnnotatedType[generic.length]; + for (int i = 0; i < generic.length; i++) { + result[i] = + resolveTypeArgument(e.getDeclaringClass(), classType, generic[i].getTypeName()) + .orElse(annotated[i]); + } + return result; + } + + // Returns the parameter types of a method or constructor resolving all generic type arguments. + public static Type[] resolveParameterTypes(Executable e, AnnotatedType classType) { + return stream(resolveAnnotatedParameterTypes(e, classType)) + .map(AnnotatedType::getType) + .toArray(Type[]::new); + } + + static Type resolveReturnType(Method method, AnnotatedType classType) { + return resolveTypeArgument( + method.getDeclaringClass(), classType, method.getGenericReturnType().getTypeName()) + .orElse(method.getAnnotatedReturnType()) + .getType(); + } + static boolean isConcreteClass(Class clazz) { return !Modifier.isAbstract(clazz.getModifiers()); } @@ -88,9 +140,11 @@ static Optional findGettersByPropertyNames( propertyNames.map(gettersByPropertyName::get).map(Optional::ofNullable), Method[]::new); } - static Optional findGettersByPropertyTypes(Class clazz, Stream> types) { - Map, List> gettersByType = - findMethods(clazz, BeanSupport::isGetter).collect(groupingBy(Method::getReturnType)); + static Optional findGettersByPropertyTypes( + Class clazz, AnnotatedType classType, Stream types) { + Map> gettersByType = + findMethods(clazz, BeanSupport::isGetter) + .collect(groupingBy(m -> resolveReturnType(m, classType))); return toArrayOrEmpty( types.map( type -> { @@ -122,10 +176,10 @@ private static Optional trimPrefix(String name, String prefix) { } } - static boolean matchingReturnTypes(Method[] methods, Type[] types) { + static boolean matchingReturnTypes(Method[] methods, AnnotatedType classType, Type[] types) { for (int i = 0; i < methods.length; i++) { // TODO: Support Optional getters, which often have a corresponding T setter. - if (!methods[i].getAnnotatedReturnType().getType().equals(types[i])) { + if (!resolveReturnType(methods[i], classType).equals(types[i])) { return false; } } diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/mutator/aggregate/CachedConstructorMutatorFactory.java b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/aggregate/CachedConstructorMutatorFactory.java index 3098cf1ce..8f00b623a 100644 --- a/src/main/java/com/code_intelligence/jazzer/mutation/mutator/aggregate/CachedConstructorMutatorFactory.java +++ b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/aggregate/CachedConstructorMutatorFactory.java @@ -18,6 +18,7 @@ import static com.code_intelligence.jazzer.mutation.mutator.aggregate.AggregatesHelper.asInstantiationFunction; import static com.code_intelligence.jazzer.mutation.mutator.aggregate.BeanSupport.findConstructorsByParameterCount; +import static com.code_intelligence.jazzer.mutation.mutator.aggregate.BeanSupport.resolveAnnotatedParameterTypes; import static com.code_intelligence.jazzer.mutation.support.StreamSupport.findFirstPresent; import static com.code_intelligence.jazzer.mutation.support.TypeSupport.asSubclassOrEmpty; @@ -63,7 +64,7 @@ private static Optional> buildMutator( return AggregatesHelper.createMutator( factory, constructor.getDeclaringClass(), - constructor.getAnnotatedParameterTypes(), + resolveAnnotatedParameterTypes(constructor, initialType), fromParametersToObject, fromObjectToParameters, initialType, diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/mutator/aggregate/ConstructorBasedBeanMutatorFactory.java b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/aggregate/ConstructorBasedBeanMutatorFactory.java index 9b2705e55..92c675411 100644 --- a/src/main/java/com/code_intelligence/jazzer/mutation/mutator/aggregate/ConstructorBasedBeanMutatorFactory.java +++ b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/aggregate/ConstructorBasedBeanMutatorFactory.java @@ -20,6 +20,7 @@ import static com.code_intelligence.jazzer.mutation.mutator.aggregate.BeanSupport.findGettersByPropertyNames; import static com.code_intelligence.jazzer.mutation.mutator.aggregate.BeanSupport.findGettersByPropertyTypes; import static com.code_intelligence.jazzer.mutation.mutator.aggregate.BeanSupport.matchingReturnTypes; +import static com.code_intelligence.jazzer.mutation.mutator.aggregate.BeanSupport.resolveParameterTypes; import static com.code_intelligence.jazzer.mutation.support.StreamSupport.findFirstPresent; import static com.code_intelligence.jazzer.mutation.support.TypeSupport.asSubclassOrEmpty; import static java.util.Arrays.stream; @@ -50,11 +51,13 @@ public Optional> tryCreate( .filter(constructor -> constructor.getParameterCount() > 0) .map( constructor -> - findParameterGetters(clazz, constructor) + findParameterGetters(clazz, type, constructor) .filter( getters -> matchingReturnTypes( - getters, constructor.getParameterTypes())) + getters, + type, + resolveParameterTypes(constructor, type))) .flatMap( getters -> { // Try to create mutator based on constructor and getters, @@ -65,7 +68,8 @@ public Optional> tryCreate( })))); } - private Optional findParameterGetters(Class clazz, Constructor constructor) { + private Optional findParameterGetters( + Class clazz, AnnotatedType type, Constructor constructor) { // Prefer explicit Java Bean ConstructorProperties annotation to determine parameter names. ConstructorProperties parameterNames = constructor.getAnnotation(ConstructorProperties.class); if (parameterNames != null @@ -78,7 +82,8 @@ private Optional findParameterGetters(Class clazz, Constructor c return findGettersByPropertyNames(clazz, stream(parameters).map(Parameter::getName)); } else { // Last fallback to parameter types. - return findGettersByPropertyTypes(clazz, stream(parameters).map(Parameter::getType)); + return findGettersByPropertyTypes( + clazz, type, stream(resolveParameterTypes(constructor, type))); } } } diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/mutator/aggregate/SetterBasedBeanMutatorFactory.java b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/aggregate/SetterBasedBeanMutatorFactory.java index 3a544a3ce..1c71ee746 100644 --- a/src/main/java/com/code_intelligence/jazzer/mutation/mutator/aggregate/SetterBasedBeanMutatorFactory.java +++ b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/aggregate/SetterBasedBeanMutatorFactory.java @@ -54,8 +54,9 @@ clazz, stream(setters).map(BeanSupport::toPropertyName)) getters -> matchingReturnTypes( getters, + type, stream(setters) - .map(setter -> setter.getAnnotatedParameterTypes()[0].getType()) + .map(setter -> resolveParameterTypes(setter, type)[0]) .toArray(Type[]::new))) .flatMap( getters -> diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/mutator/aggregate/SuperBuilderMutatorFactory.java b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/aggregate/SuperBuilderMutatorFactory.java index 588080f34..b1fd47ae3 100644 --- a/src/main/java/com/code_intelligence/jazzer/mutation/mutator/aggregate/SuperBuilderMutatorFactory.java +++ b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/aggregate/SuperBuilderMutatorFactory.java @@ -69,7 +69,7 @@ public Optional> tryCreate( return AggregatesHelper.createMutator( factory, clazz, - parameterTypes(builderSetters), + parameterTypes(builderSetters, type), fromParametersToObject, fromObjectToParameters, type, diff --git a/src/test/java/com/code_intelligence/jazzer/mutation/mutator/StressTest.java b/src/test/java/com/code_intelligence/jazzer/mutation/mutator/StressTest.java index 29e0aad35..0e723541d 100644 --- a/src/test/java/com/code_intelligence/jazzer/mutation/mutator/StressTest.java +++ b/src/test/java/com/code_intelligence/jazzer/mutation/mutator/StressTest.java @@ -363,6 +363,38 @@ public String toString() { } } + public static class GenericImmutableBuilder { + private final T t; + + public GenericImmutableBuilder() { + t = null; + } + + private GenericImmutableBuilder(T t) { + this.t = t; + } + + public T getT() { + return t; + } + + public GenericImmutableBuilder setT(T t) { + return new GenericImmutableBuilder(t); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + return Objects.equals(t, ((GenericImmutableBuilder) o).t); + } + + @Override + public int hashCode() { + return Objects.hash(t); + } + } + public static class ConstructorBasedBean { private final boolean foo; private final String bar; @@ -405,6 +437,31 @@ public String toString() { } } + public static class GenericConstructorBasedBean { + T t; + + GenericConstructorBasedBean(T t) { + this.t = t; + } + + public T getT() { + return t; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + GenericConstructorBasedBean that = (GenericConstructorBasedBean) o; + return Objects.equals(this.t, that.t); + } + + @Override + public int hashCode() { + return Objects.hash(this.t); + } + } + public static class OnlyConstructorBean { private final String foo; private final List bar; @@ -435,6 +492,27 @@ public String toString() { } } + public static class GenericOnlyConstructorBean { + private final T t; + + GenericOnlyConstructorBean(T t) { + this.t = t; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + GenericOnlyConstructorBean that = (GenericOnlyConstructorBean) o; + return Objects.equals(t, that.t); + } + + @Override + public int hashCode() { + return Objects.hash(t); + } + } + public static class SuperBuilderTarget { private final String foo; @@ -889,18 +967,37 @@ void singleParam(int parameter) {} // Low due to int and boolean fields having very few common values during init. distinctElementsRatio(0.23), manyDistinctElements()), + arguments( + new TypeHolder<@NotNull GenericImmutableBuilder<@NotNull String>>() {}.annotatedType(), + "[String] -> GenericImmutableBuilder", + false, + manyDistinctElements(), + manyDistinctElements()), arguments( new TypeHolder<@NotNull ConstructorBasedBean>() {}.annotatedType(), "[Boolean, Nullable, Integer] -> ConstructorBasedBean", false, manyDistinctElements(), manyDistinctElements()), + arguments( + new TypeHolder< + @NotNull GenericConstructorBasedBean<@NotNull String>>() {}.annotatedType(), + "[String] -> GenericConstructorBasedBean", + false, + manyDistinctElements(), + manyDistinctElements()), arguments( new TypeHolder<@NotNull OnlyConstructorBean>() {}.annotatedType(), "[Nullable, Nullable>>, Boolean] -> OnlyConstructorBean", false, manyDistinctElements(), manyDistinctElements()), + arguments( + new TypeHolder<@NotNull GenericOnlyConstructorBean>() {}.annotatedType(), + "[Nullable] -> GenericOnlyConstructorBean", + false, + manyDistinctElements(), + manyDistinctElements()), arguments( new TypeHolder<@NotNull List>() {}.annotatedType(), "List, Nullable>>, Boolean] ->" diff --git a/src/test/java/com/code_intelligence/jazzer/mutation/mutator/aggregate/CachedConstructorMutatorTest.java b/src/test/java/com/code_intelligence/jazzer/mutation/mutator/aggregate/CachedConstructorMutatorTest.java index d5d0de941..fe50024ca 100644 --- a/src/test/java/com/code_intelligence/jazzer/mutation/mutator/aggregate/CachedConstructorMutatorTest.java +++ b/src/test/java/com/code_intelligence/jazzer/mutation/mutator/aggregate/CachedConstructorMutatorTest.java @@ -192,4 +192,34 @@ void testEmptyArgsConstructor() throws IOException { EmptyArgs read = mutator.readExclusive(new ByteArrayInputStream(new byte[] {})); mutator.writeExclusive(read, new ByteArrayOutputStream()); } + + static class GenericClass { + private final T t; + + GenericClass(T t) { + this.t = t; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + GenericClass that = (GenericClass) o; + return Objects.equals(t, that.t); + } + + @Override + public int hashCode() { + return Objects.hash(t); + } + } + + @Test + void testGenericClass() { + SerializingMutator> mutator = + (SerializingMutator>) + Mutators.newFactory() + .createOrThrow(new TypeHolder>() {}.annotatedType()); + assertThat(mutator.toString()).startsWith("Nullable<[Nullable] -> GenericClass>"); + } } diff --git a/src/test/java/com/code_intelligence/jazzer/mutation/mutator/aggregate/ConstructorBasedBeanMutatorTest.java b/src/test/java/com/code_intelligence/jazzer/mutation/mutator/aggregate/ConstructorBasedBeanMutatorTest.java index 23d36c652..8a4c08ea4 100644 --- a/src/test/java/com/code_intelligence/jazzer/mutation/mutator/aggregate/ConstructorBasedBeanMutatorTest.java +++ b/src/test/java/com/code_intelligence/jazzer/mutation/mutator/aggregate/ConstructorBasedBeanMutatorTest.java @@ -241,4 +241,50 @@ void propagateConstraint() { assertThat(mutator.toString()) .isEqualTo("[Boolean, String, Integer] -> ConstructorPropertiesAnnotatedBean"); } + + public static class CustomPair { + L left; + R right; + + public CustomPair(L left, R right) { + this.left = left; + this.right = right; + } + + public L getLeft() { + return this.left; + } + + public R getRight() { + return this.right; + } + } + + @Test + void genericClass() { + // Note: We can't use @NotNull here since Java 8 does not retain the annotations for generic + // classes. + SerializingMutator> mutator = + (SerializingMutator>) + Mutators.newFactory() + .createOrThrow(new TypeHolder>() {}.annotatedType()); + assertThat(mutator.toString()) + .isEqualTo("Nullable<[Nullable, Nullable] -> CustomPair>"); + } + + @Test + void genericClassLayered() { + // Note: We can't use @NotNull here since Java 8 does not retain the annotations for generic + // classes. + SerializingMutator, Integer>> mutator2 = + (SerializingMutator, Integer>>) + Mutators.newFactory() + .createOrThrow( + new TypeHolder< + CustomPair, Integer>>() {}.annotatedType()); + assertThat(mutator2.toString()) + .isEqualTo( + "Nullable<[Nullable<[Nullable, Nullable] -> CustomPair>," + + " Nullable] -> CustomPair>"); + } } diff --git a/src/test/java/com/code_intelligence/jazzer/mutation/mutator/aggregate/RecordMutatorTest.java b/src/test/java/com/code_intelligence/jazzer/mutation/mutator/aggregate/RecordMutatorTest.java index c75c0a69f..4f950c0c6 100644 --- a/src/test/java/com/code_intelligence/jazzer/mutation/mutator/aggregate/RecordMutatorTest.java +++ b/src/test/java/com/code_intelligence/jazzer/mutation/mutator/aggregate/RecordMutatorTest.java @@ -175,4 +175,15 @@ void propagateConstraint() { assertThat(mutator.toString()) .isEqualTo("[[List] -> PropagateInnerTypeRecord] -> PropagateTypeRecord"); } + + record GenericRecord(T t) {} + + @Test + void testGenericRecord() { + SerializingMutator> mutator = + (SerializingMutator>) + Mutators.newFactory() + .createOrThrow(new TypeHolder<@NotNull GenericRecord>() {}.annotatedType()); + assertThat(mutator.toString()).isEqualTo("[Nullable] -> GenericRecord"); + } } diff --git a/src/test/java/com/code_intelligence/jazzer/mutation/mutator/aggregate/SetterBasedBeanMutatorTest.java b/src/test/java/com/code_intelligence/jazzer/mutation/mutator/aggregate/SetterBasedBeanMutatorTest.java index a408397d6..45def15aa 100644 --- a/src/test/java/com/code_intelligence/jazzer/mutation/mutator/aggregate/SetterBasedBeanMutatorTest.java +++ b/src/test/java/com/code_intelligence/jazzer/mutation/mutator/aggregate/SetterBasedBeanMutatorTest.java @@ -223,4 +223,27 @@ void propagateConstraint() { .isEqualTo( "[Integer, RecursionBreaking((cycle) -> RecursiveTypeBean)] -> RecursiveTypeBean"); } + + public static class Generic { + T t; + + public Generic() {} + + public void setT(T t) { + this.t = t; + } + + public T getT() { + return t; + } + } + + @Test + void genericClass() { + SerializingMutator> mutator = + (SerializingMutator>) + Mutators.newFactory() + .createOrThrow(new TypeHolder>() {}.annotatedType()); + assertThat(mutator.toString()).isEqualTo("Nullable<[Nullable] -> Generic>"); + } }