From 456a9f9ffdbdb70854e5d8284674ef3fe2d5e4cd Mon Sep 17 00:00:00 2001 From: Simon Resch Date: Wed, 5 Nov 2025 11:54:04 +0100 Subject: [PATCH 1/4] feat: support generic constructor based beans --- .../mutator/aggregate/AggregatesHelper.java | 39 ++++++----- .../mutator/aggregate/BeanSupport.java | 64 +++++++++++++++++-- .../ConstructorBasedBeanMutatorFactory.java | 13 ++-- .../SetterBasedBeanMutatorFactory.java | 1 + .../jazzer/mutation/mutator/StressTest.java | 32 ++++++++++ .../ConstructorBasedBeanMutatorTest.java | 46 +++++++++++++ .../mutator/aggregate/RecordMutatorTest.java | 11 ++++ 7 files changed, 177 insertions(+), 29 deletions(-) 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..31d66031e 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( @@ -105,14 +105,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), + asInstantiationFunction(lookup, newInstance, setters), + makeSingleGetter(unreflectMethods(lookup, getters)), + initialType, + /* isImmutable= */ false); } @SuppressWarnings("Immutable") 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..66ce269a3 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; @@ -37,6 +41,7 @@ import java.util.Map; import java.util.Optional; import java.util.function.Predicate; +import java.util.stream.IntStream; import java.util.stream.Stream; class BeanSupport { @@ -49,6 +54,53 @@ 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, Type type) { + if (!(classType instanceof AnnotatedParameterizedType)) { + return Optional.empty(); + } + + TypeVariable[] typeParameters = clazz.getTypeParameters(); + AnnotatedType[] typeArguments = + ((AnnotatedParameterizedType) classType).getAnnotatedActualTypeArguments(); + + return IntStream.range(0, typeParameters.length) + .filter(i -> typeParameters[i].equals(type)) + .mapToObj(i -> typeArguments[i]) + .findFirst(); + } + + // 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]).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()) + .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/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..a4379f059 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,6 +54,7 @@ clazz, stream(setters).map(BeanSupport::toPropertyName)) getters -> matchingReturnTypes( getters, + type, stream(setters) .map(setter -> setter.getAnnotatedParameterTypes()[0].getType()) .toArray(Type[]::new))) 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..4ca55d2dc 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 @@ -405,6 +405,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; @@ -895,6 +920,13 @@ void singleParam(int parameter) {} 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", 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"); + } } From 9e2b707273b74f5596d287447b1b229d2785e60b Mon Sep 17 00:00:00 2001 From: Simon Resch Date: Wed, 5 Nov 2025 15:35:51 +0100 Subject: [PATCH 2/4] feat: add support for generic setter based beans --- .../mutator/aggregate/AggregatesHelper.java | 12 +++--- .../SetterBasedBeanMutatorFactory.java | 2 +- .../aggregate/SuperBuilderMutatorFactory.java | 2 +- .../jazzer/mutation/mutator/StressTest.java | 38 +++++++++++++++++++ .../aggregate/SetterBasedBeanMutatorTest.java | 23 +++++++++++ 5 files changed, 68 insertions(+), 9 deletions(-) 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 31d66031e..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 @@ -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])); } @@ -107,7 +105,7 @@ static Optional> createMutator( return createMutator( factory, newInstance.getDeclaringClass(), - parameterTypes(setters), + parameterTypes(setters, initialType), asInstantiationFunction(lookup, newInstance, setters), makeSingleGetter(unreflectMethods(lookup, getters)), initialType, @@ -220,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/SetterBasedBeanMutatorFactory.java b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/aggregate/SetterBasedBeanMutatorFactory.java index a4379f059..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 @@ -56,7 +56,7 @@ clazz, stream(setters).map(BeanSupport::toPropertyName)) 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 4ca55d2dc..5822fd331 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; @@ -914,6 +946,12 @@ 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", 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>"); + } } From f8cd737e4b70843eedfc4c6936a5480ce5368010 Mon Sep 17 00:00:00 2001 From: Simon Resch Date: Wed, 5 Nov 2025 16:18:53 +0100 Subject: [PATCH 3/4] feat: support generic classes in ConstructorBasedBeanMutator --- .../CachedConstructorMutatorFactory.java | 3 ++- .../jazzer/mutation/mutator/StressTest.java | 27 +++++++++++++++++++ .../CachedConstructorMutatorTest.java | 17 ++++++++++++ 3 files changed, 46 insertions(+), 1 deletion(-) 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/test/java/com/code_intelligence/jazzer/mutation/mutator/StressTest.java b/src/test/java/com/code_intelligence/jazzer/mutation/mutator/StressTest.java index 5822fd331..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 @@ -492,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; @@ -971,6 +992,12 @@ void singleParam(int parameter) {} 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..d6cbe6c7a 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,21 @@ 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; + } + } + + @Test + void testGenericClass() { + SerializingMutator> mutator = + (SerializingMutator>) + Mutators.newFactory() + .createOrThrow(new TypeHolder>() {}.annotatedType()); + assertThat(mutator.toString()).startsWith("Nullable<[Nullable] -> GenericClass>"); + } } From d77f4fcd076b1259b321eb24e6d2277922bd8cb0 Mon Sep 17 00:00:00 2001 From: Simon Resch Date: Wed, 12 Nov 2025 07:55:33 +0100 Subject: [PATCH 4/4] feat: resolve type variables recursively This enables mutator support for classes that use type variables as part of an array, wildcard or other generic class. E.g. `class MyClass { MyClass(Set setOfArrays){...} }`. --- .../mutator/aggregate/BeanSupport.java | 31 +- .../support/ParameterizedTypeSupport.java | 492 ++++++++++++++++++ .../CachedConstructorMutatorTest.java | 17 + .../aggregate/SetterBasedBeanMutatorTest.java | 8 +- .../support/ParameterizedTypeSupportTest.java | 128 +++++ 5 files changed, 645 insertions(+), 31 deletions(-) create mode 100644 src/main/java/com/code_intelligence/jazzer/mutation/support/ParameterizedTypeSupport.java create mode 100644 src/test/java/com/code_intelligence/jazzer/mutation/support/ParameterizedTypeSupportTest.java 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 66ce269a3..d9d936453 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 @@ -16,6 +16,7 @@ package com.code_intelligence.jazzer.mutation.mutator.aggregate; +import static com.code_intelligence.jazzer.mutation.support.ParameterizedTypeSupport.resolveTypeArguments; import static com.code_intelligence.jazzer.mutation.support.StreamSupport.getOrEmpty; import static com.code_intelligence.jazzer.mutation.support.StreamSupport.toArrayOrEmpty; import static java.util.Arrays.stream; @@ -26,14 +27,12 @@ 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; @@ -41,7 +40,6 @@ import java.util.Map; import java.util.Optional; import java.util.function.Predicate; -import java.util.stream.IntStream; import java.util.stream.Stream; class BeanSupport { @@ -54,26 +52,6 @@ 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, Type type) { - if (!(classType instanceof AnnotatedParameterizedType)) { - return Optional.empty(); - } - - TypeVariable[] typeParameters = clazz.getTypeParameters(); - AnnotatedType[] typeArguments = - ((AnnotatedParameterizedType) classType).getAnnotatedActualTypeArguments(); - - return IntStream.range(0, typeParameters.length) - .filter(i -> typeParameters[i].equals(type)) - .mapToObj(i -> typeArguments[i]) - .findFirst(); - } - // Returns the annotated parameter types of a method or constructor resolving all generic type // arguments. public static AnnotatedType[] resolveAnnotatedParameterTypes( @@ -82,8 +60,7 @@ public static AnnotatedType[] resolveAnnotatedParameterTypes( 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]).orElse(annotated[i]); + result[i] = resolveTypeArguments(e.getDeclaringClass(), classType, annotated[i]); } return result; } @@ -96,8 +73,8 @@ public static Type[] resolveParameterTypes(Executable e, AnnotatedType classType } static Type resolveReturnType(Method method, AnnotatedType classType) { - return resolveTypeArgument(method.getDeclaringClass(), classType, method.getGenericReturnType()) - .orElse(method.getAnnotatedReturnType()) + return resolveTypeArguments( + method.getDeclaringClass(), classType, method.getAnnotatedReturnType()) .getType(); } diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/support/ParameterizedTypeSupport.java b/src/main/java/com/code_intelligence/jazzer/mutation/support/ParameterizedTypeSupport.java new file mode 100644 index 000000000..9951b9d01 --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/mutation/support/ParameterizedTypeSupport.java @@ -0,0 +1,492 @@ +/* + * Copyright 2025 Code Intelligence GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.code_intelligence.jazzer.mutation.support; + +import static com.code_intelligence.jazzer.mutation.support.Preconditions.require; +import static java.util.Arrays.stream; + +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedArrayType; +import java.lang.reflect.AnnotatedParameterizedType; +import java.lang.reflect.AnnotatedType; +import java.lang.reflect.AnnotatedWildcardType; +import java.lang.reflect.GenericArrayType; +import java.lang.reflect.Method; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.lang.reflect.TypeVariable; +import java.lang.reflect.WildcardType; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.StringJoiner; +import java.util.stream.IntStream; + +/** + * Utilities for resolving {@link AnnotatedType} trees that contain references to the type variables + * of a parameterized class. + * + *

The Java reflection API exposes mutator targets as {@link AnnotatedType}s. When the target + * instantiates a generic class such as {@code MyBean}, the bean's fields and accessors may + * still refer to the type variable {@code T}. The helper provided here walks those annotated type + * trees, replaces occurrences of the class' type variables with the concrete annotated arguments + * from the instantiation, and synthesizes fresh {@link AnnotatedType}s that retain annotations and + * nested generic structure. + */ +public class ParameterizedTypeSupport { + /** + * Replaces type variables in {@code type} with the annotated concrete type arguments from {@code + * classType}. + * + *

For example, given {@code class Box { List values; }} and the annotated type {@code + * Box<@NotNull String>}, calling this method with the {@code values} field's annotated type + * returns a new {@link AnnotatedType} representing {@code List<@NotNull String>}. + * + * @param clazz the generic class that declares the type variables + * @param classType the annotated instantiation of {@code clazz} + * @param type the annotated type to resolve (e.g. a constructor parameter or getter return type) + */ + public static AnnotatedType resolveTypeArguments( + Class clazz, AnnotatedType classType, AnnotatedType type) { + if (!(classType instanceof AnnotatedParameterizedType)) { + return type; + } + + TypeVariable[] typeParameters = clazz.getTypeParameters(); + AnnotatedType[] typeArguments = + ((AnnotatedParameterizedType) classType).getAnnotatedActualTypeArguments(); + + require(typeArguments.length == typeParameters.length); + + Map, AnnotatedType> mapping = new HashMap<>(); + for (int i = 0; i < typeParameters.length; i++) { + mapping.put(typeParameters[i], typeArguments[i]); + } + return resolveRecursive(type.getType(), type, mapping); + } + + /** + * Resolves {@code annotated} according to the substitutions provided in {@code mapping}. The + * method recreates wrapper objects for parameterized, array, and wildcard types so that their + * nested type variables are resolved as well. + */ + private static AnnotatedType resolveRecursive( + Type type, AnnotatedType annotated, Map, AnnotatedType> mapping) { + if (type instanceof ParameterizedType) { + // E.g. `List` + require(annotated instanceof AnnotatedParameterizedType); + return resolveParameterizedType( + (ParameterizedType) type, (AnnotatedParameterizedType) annotated, mapping); + } else if (type instanceof GenericArrayType) { + // E.g. `T[]` + require(annotated instanceof AnnotatedArrayType); + return resolveArrayType((GenericArrayType) type, (AnnotatedArrayType) annotated, mapping); + } else if (type instanceof WildcardType) { + // E.g. `? extends T` + require(annotated instanceof AnnotatedWildcardType); + return resolveWildcardType((AnnotatedWildcardType) annotated, mapping); + } else if (type instanceof TypeVariable) { + // E.g. `T` + AnnotatedType replacement = mapping.get(type); + if (replacement == null) { + return annotated; + } + return TypeSupport.forwardAnnotations(annotated, replacement); + } + return annotated; + } + + private static AnnotatedParameterizedType resolveParameterizedType( + ParameterizedType type, + AnnotatedParameterizedType annotated, + Map, AnnotatedType> mapping) { + AnnotatedType[] annotatedArgs = annotated.getAnnotatedActualTypeArguments(); + Type[] typeArgs = type.getActualTypeArguments(); + AnnotatedType[] resolvedArgs = + IntStream.range(0, annotatedArgs.length) + .mapToObj(i -> resolveRecursive(typeArgs[i], annotatedArgs[i], mapping)) + .toArray(AnnotatedType[]::new); + Type resolvedType = + new ParameterizedTypeWrapper( + type.getRawType(), + stream(resolvedArgs).map(AnnotatedType::getType).toArray(Type[]::new), + type.getOwnerType()); + return new AnnotatedParameterizedTypeWrapper(annotated, resolvedType, resolvedArgs); + } + + private static AnnotatedArrayType resolveArrayType( + GenericArrayType type, + AnnotatedArrayType annotated, + Map, AnnotatedType> mapping) { + AnnotatedType resolved = + resolveRecursive( + type.getGenericComponentType(), annotated.getAnnotatedGenericComponentType(), mapping); + Type resolvedType = new GenericArrayTypeWrapper(resolved.getType()); + return new AnnotatedArrayTypeWrapper(annotated, resolvedType, resolved); + } + + private static AnnotatedWildcardType resolveWildcardType( + AnnotatedWildcardType annotated, Map, AnnotatedType> mapping) { + AnnotatedType[] resolvedLower = + stream(annotated.getAnnotatedLowerBounds()) + .map(t -> resolveRecursive(t.getType(), t, mapping)) + .toArray(AnnotatedType[]::new); + AnnotatedType[] resolvedUpper = + stream(annotated.getAnnotatedUpperBounds()) + .map(t -> resolveRecursive(t.getType(), t, mapping)) + .toArray(AnnotatedType[]::new); + Type resolvedType = + new WildcardTypeWrapper( + stream(resolvedLower).map(AnnotatedType::getType).toArray(Type[]::new), + stream(resolvedUpper).map(AnnotatedType::getType).toArray(Type[]::new)); + return new AnnotatedWildcardTypeWrapper(annotated, resolvedType, resolvedLower, resolvedUpper); + } + + private static class WildcardTypeWrapper implements WildcardType { + private final Type[] lowerBounds; + private final Type[] upperBounds; + + public WildcardTypeWrapper(Type[] lowerBounds, Type[] upperBounds) { + this.lowerBounds = lowerBounds.clone(); + this.upperBounds = upperBounds.clone(); + } + + @Override + public Type[] getUpperBounds() { + return upperBounds.clone(); + } + + @Override + public Type[] getLowerBounds() { + return lowerBounds.clone(); + } + + @Override + public String toString() { + Type[] lowerBounds = getLowerBounds(); + Type[] bounds = lowerBounds; + StringBuilder sb = new StringBuilder(); + + if (lowerBounds.length > 0) sb.append("? super "); + else { + Type[] upperBounds = getUpperBounds(); + if (upperBounds.length > 0 && !upperBounds[0].equals(Object.class)) { + bounds = upperBounds; + sb.append("? extends "); + } else return "?"; + } + + StringJoiner sj = new StringJoiner(" & "); + for (Type bound : bounds) { + sj.add(bound.getTypeName()); + } + sb.append(sj); + + return sb.toString(); + } + + @Override + public boolean equals(Object other) { + if (!(other instanceof WildcardType)) { + return false; + } + WildcardType that = (WildcardType) other; + return Arrays.equals(getLowerBounds(), that.getLowerBounds()) + && Arrays.equals(getUpperBounds(), that.getUpperBounds()); + } + + @Override + public int hashCode() { + return Arrays.hashCode(getLowerBounds()) * 31 + Arrays.hashCode(getUpperBounds()); + } + } + + private static class GenericArrayTypeWrapper implements GenericArrayType { + + private final Type componentType; + + public GenericArrayTypeWrapper(Type componentType) { + this.componentType = componentType; + } + + @Override + public Type getGenericComponentType() { + return componentType; + } + + @Override + public String toString() { + return componentType.getTypeName() + "[]"; + } + + @Override + public boolean equals(Object other) { + if (!(other instanceof GenericArrayType)) { + return false; + } + GenericArrayType that = (GenericArrayType) other; + return componentType.equals(that.getGenericComponentType()); + } + + @Override + public int hashCode() { + return componentType.hashCode(); + } + } + + private static class ParameterizedTypeWrapper implements ParameterizedType { + private final Type rawType; + private final Type[] typeArguments; + private final Type ownerType; + + public ParameterizedTypeWrapper(Type rawType, Type[] typeArguments, Type ownerType) { + this.rawType = rawType; + this.typeArguments = typeArguments; + this.ownerType = ownerType; + } + + @Override + public Type[] getActualTypeArguments() { + return typeArguments; + } + + @Override + public Type getRawType() { + return rawType; + } + + @Override + public Type getOwnerType() { + return ownerType; + } + + @Override + public String toString() { + return rawType.getTypeName() + + "<" + + Arrays.stream(typeArguments) + .map(Type::getTypeName) + .reduce((a, b) -> a + "," + b) + .orElse("") + + ">"; + } + + @Override + public boolean equals(Object other) { + if (!(other instanceof ParameterizedType)) { + return false; + } + ParameterizedType that = (ParameterizedType) other; + return Objects.equals(getRawType(), that.getRawType()) + && Objects.equals(getOwnerType(), that.getOwnerType()) + && Arrays.equals(getActualTypeArguments(), that.getActualTypeArguments()); + } + + @Override + public int hashCode() { + return Objects.hash(getRawType(), getOwnerType(), Arrays.hashCode(getActualTypeArguments())); + } + } + + private static class AnnotatedTypeWrapper implements AnnotatedType { + final AnnotatedType annotatedType; + private final Type type; + + AnnotatedTypeWrapper(AnnotatedType annotatedType, Type type) { + this.annotatedType = annotatedType; + this.type = type; + } + + @Override + public Type getType() { + return type; + } + + @Override + public T getAnnotation(Class annotationClass) { + return annotatedType.getAnnotation(annotationClass); + } + + @Override + public Annotation[] getAnnotations() { + return annotatedType.getAnnotations(); + } + + @Override + public Annotation[] getDeclaredAnnotations() { + return annotatedType.getDeclaredAnnotations(); + } + + public AnnotatedType getAnnotatedOwnerType() { + return getAnnotatedOwnerTypeOrNull(annotatedType); + } + + @Override + public String toString() { + // TODO: include annotations in string + return type.toString(); + } + + protected boolean equalsTypeAndAnnotations(AnnotatedType that) { + return getType().equals(that.getType()) + // Treat ordering of annotations as significant + && Arrays.equals(getAnnotations(), that.getAnnotations()) + && Objects.equals(getAnnotatedOwnerType(), getAnnotatedOwnerTypeOrNull(that)); + } + + int baseHashCode() { + return type.hashCode() + ^ + // Acceptable to use Objects.hash rather than + // Arrays.deepHashCode since the elements of the array + // are not themselves arrays. + Objects.hash((Object[]) getAnnotations()) + ^ Objects.hash(getAnnotatedOwnerType()); + } + } + + private static class AnnotatedWildcardTypeWrapper extends AnnotatedTypeWrapper + implements AnnotatedWildcardType { + private final AnnotatedType[] upperBounds; + private final AnnotatedType[] lowerBounds; + + AnnotatedWildcardTypeWrapper( + AnnotatedType annotatedType, + Type type, + AnnotatedType[] lowerBounds, + AnnotatedType[] upperBounds) { + super(annotatedType, type); + this.upperBounds = upperBounds; + this.lowerBounds = lowerBounds; + } + + @Override + public AnnotatedType[] getAnnotatedLowerBounds() { + return lowerBounds; + } + + @Override + public AnnotatedType[] getAnnotatedUpperBounds() { + return upperBounds; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof AnnotatedWildcardType)) { + return false; + } + AnnotatedWildcardType that = (AnnotatedWildcardType) o; + return equalsTypeAndAnnotations(that) + && Arrays.equals(getAnnotatedLowerBounds(), that.getAnnotatedLowerBounds()) + && Arrays.equals(getAnnotatedUpperBounds(), that.getAnnotatedUpperBounds()); + } + + @Override + public int hashCode() { + return baseHashCode() + ^ Objects.hash((Object[]) getAnnotatedLowerBounds()) + ^ Objects.hash((Object[]) getAnnotatedUpperBounds()); + } + } + + private static class AnnotatedArrayTypeWrapper extends AnnotatedTypeWrapper + implements AnnotatedArrayType { + + private final AnnotatedType componentType; + + AnnotatedArrayTypeWrapper(AnnotatedType annotatedType, Type type, AnnotatedType componentType) { + super(annotatedType, type); + this.componentType = componentType; + } + + @Override + public AnnotatedType getAnnotatedGenericComponentType() { + return componentType; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof AnnotatedArrayType)) { + return false; + } + AnnotatedArrayType that = (AnnotatedArrayType) o; + return equalsTypeAndAnnotations(that) + && componentType.equals(that.getAnnotatedGenericComponentType()); + } + + @Override + public int hashCode() { + return baseHashCode() ^ Objects.hash(componentType); + } + } + + private static class AnnotatedParameterizedTypeWrapper extends AnnotatedTypeWrapper + implements AnnotatedParameterizedType { + private final AnnotatedType[] typeArguments; + + AnnotatedParameterizedTypeWrapper( + AnnotatedType annotatedType, Type type, AnnotatedType[] typeArguments) { + super(annotatedType, type); + this.typeArguments = typeArguments; + } + + @Override + public AnnotatedType[] getAnnotatedActualTypeArguments() { + return typeArguments; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof AnnotatedParameterizedType)) { + return false; + } + AnnotatedParameterizedType that = (AnnotatedParameterizedType) o; + return equalsTypeAndAnnotations(that) + && Arrays.equals( + getAnnotatedActualTypeArguments(), that.getAnnotatedActualTypeArguments()); + } + + @Override + public int hashCode() { + return baseHashCode() ^ Objects.hash((Object[]) getAnnotatedActualTypeArguments()); + } + } + + private static final Optional ANNOTATED_OWNER_TYPE_METHOD = + findAnnotatedOwnerTypeMethod(); + + private static Optional findAnnotatedOwnerTypeMethod() { + try { + return Optional.of(AnnotatedType.class.getMethod("getAnnotatedOwnerType")); + } catch (NoSuchMethodException e) { + return Optional.empty(); + } + } + + private static AnnotatedType getAnnotatedOwnerTypeOrNull(AnnotatedType annotatedType) { + if (annotatedType == null || !ANNOTATED_OWNER_TYPE_METHOD.isPresent()) { + return null; + } + try { + return (AnnotatedType) ANNOTATED_OWNER_TYPE_METHOD.get().invoke(annotatedType); + } catch (ReflectiveOperationException e) { + throw new IllegalStateException(e); + } + } +} 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 d6cbe6c7a..34b1a4b12 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 @@ -209,4 +209,21 @@ void testGenericClass() { .createOrThrow(new TypeHolder>() {}.annotatedType()); assertThat(mutator.toString()).startsWith("Nullable<[Nullable] -> GenericClass>"); } + + static class GenericListClass { + private final List values; + + GenericListClass(List values) { + this.values = values; + } + } + + @Test + void testGenericListClass() { + SerializingMutator> mutator = + (SerializingMutator>) + Mutators.newFactory() + .createOrThrow(new TypeHolder>() {}.annotatedType()); + assertThat(mutator.toString()).contains("List>"); + } } 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 45def15aa..a04d9d096 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 @@ -225,15 +225,15 @@ void propagateConstraint() { } public static class Generic { - T t; + T[] t; public Generic() {} - public void setT(T t) { + public void setT(T[] t) { this.t = t; } - public T getT() { + public T[] getT() { return t; } } @@ -244,6 +244,6 @@ void genericClass() { (SerializingMutator>) Mutators.newFactory() .createOrThrow(new TypeHolder>() {}.annotatedType()); - assertThat(mutator.toString()).isEqualTo("Nullable<[Nullable] -> Generic>"); + assertThat(mutator.toString()).isEqualTo("Nullable<[Nullable[]>] -> Generic>"); } } diff --git a/src/test/java/com/code_intelligence/jazzer/mutation/support/ParameterizedTypeSupportTest.java b/src/test/java/com/code_intelligence/jazzer/mutation/support/ParameterizedTypeSupportTest.java new file mode 100644 index 000000000..8245cb820 --- /dev/null +++ b/src/test/java/com/code_intelligence/jazzer/mutation/support/ParameterizedTypeSupportTest.java @@ -0,0 +1,128 @@ +/* + * Copyright 2024 Code Intelligence GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.code_intelligence.jazzer.mutation.support; + +import static com.google.common.truth.Truth.assertThat; + +import com.code_intelligence.jazzer.mutation.annotation.NotNull; +import java.lang.reflect.AnnotatedArrayType; +import java.lang.reflect.AnnotatedParameterizedType; +import java.lang.reflect.AnnotatedType; +import java.lang.reflect.AnnotatedWildcardType; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Test; + +class ParameterizedTypeSupportTest { + @Test + void resolveParameterizedType() throws NoSuchFieldException { + class Generic { + public List field; + } + AnnotatedType annotatedType = Generic.class.getDeclaredField("field").getAnnotatedType(); + AnnotatedParameterizedType classType = + (AnnotatedParameterizedType) new TypeHolder>() {}.annotatedType(); + AnnotatedType resolved = + ParameterizedTypeSupport.resolveTypeArguments(Generic.class, classType, annotatedType); + + assertThat(resolved).isInstanceOf(AnnotatedParameterizedType.class); + + AnnotatedParameterizedType parameterType = (AnnotatedParameterizedType) resolved; + assertThat(((ParameterizedType) parameterType.getType()).getRawType()).isEqualTo(List.class); + AnnotatedType elementType = parameterType.getAnnotatedActualTypeArguments()[0]; + assertThat(elementType.getType()).isEqualTo(String.class); + assertThat( + TypeSupport.annotatedTypeEquals( + classType.getAnnotatedActualTypeArguments()[0], elementType)) + .isTrue(); + } + + @Test + void resolveArrayType() throws NoSuchFieldException { + class Generic { + public T[] field; + } + AnnotatedType annotatedType = Generic.class.getDeclaredField("field").getAnnotatedType(); + AnnotatedParameterizedType classType = + (AnnotatedParameterizedType) new TypeHolder>() {}.annotatedType(); + AnnotatedType resolved = + ParameterizedTypeSupport.resolveTypeArguments(Generic.class, classType, annotatedType); + + assertThat(resolved).isInstanceOf(AnnotatedArrayType.class); + + AnnotatedArrayType arrayType = (AnnotatedArrayType) resolved; + assertThat(arrayType.getType().getTypeName()).isEqualTo(String[].class.getTypeName()); + AnnotatedType componentType = arrayType.getAnnotatedGenericComponentType(); + assertThat(componentType.getType()).isEqualTo(String.class); + assertThat( + TypeSupport.annotatedTypeEquals( + classType.getAnnotatedActualTypeArguments()[0], componentType)) + .isTrue(); + } + + @Test + void resolveWildcardType() throws NoSuchFieldException { + class Generic { + public List field; + } + AnnotatedType annotatedType = Generic.class.getDeclaredField("field").getAnnotatedType(); + AnnotatedParameterizedType classType = + (AnnotatedParameterizedType) new TypeHolder>() {}.annotatedType(); + AnnotatedType resolved = + ParameterizedTypeSupport.resolveTypeArguments(Generic.class, classType, annotatedType); + + AnnotatedParameterizedType parameterType = (AnnotatedParameterizedType) resolved; + assertThat(((ParameterizedType) parameterType.getType()).getRawType()).isEqualTo(List.class); + + AnnotatedType wildcardArgument = parameterType.getAnnotatedActualTypeArguments()[0]; + assertThat(wildcardArgument).isInstanceOf(AnnotatedWildcardType.class); + + AnnotatedWildcardType wildcardType = (AnnotatedWildcardType) wildcardArgument; + AnnotatedType[] upperBounds = wildcardType.getAnnotatedUpperBounds(); + assertThat(upperBounds).hasLength(1); + assertThat(upperBounds[0].getType()).isEqualTo(String.class); + assertThat( + TypeSupport.annotatedTypeEquals( + classType.getAnnotatedActualTypeArguments()[0], + wildcardType.getAnnotatedUpperBounds()[0])) + .isTrue(); + } + + @Test + void resolveParameterizedType_twoTypeArguments() throws NoSuchFieldException { + class Generic { + public @NotNull Map field; + } + AnnotatedType annotatedType = Generic.class.getDeclaredField("field").getAnnotatedType(); + AnnotatedType resolved = + ParameterizedTypeSupport.resolveTypeArguments( + Generic.class, + new TypeHolder>() {}.annotatedType(), + annotatedType); + + assertThat(resolved).isInstanceOf(AnnotatedParameterizedType.class); + AnnotatedParameterizedType annotatedParameterizedType = (AnnotatedParameterizedType) resolved; + Type resolvedType = annotatedParameterizedType.getType(); + assertThat(resolvedType).isInstanceOf(ParameterizedType.class); + ParameterizedType parameterizedType = (ParameterizedType) resolvedType; + assertThat(parameterizedType.getRawType()).isEqualTo(Map.class); + assertThat(parameterizedType.getActualTypeArguments()[0]).isEqualTo(String.class); + assertThat(parameterizedType.getActualTypeArguments()[1]).isEqualTo(Integer.class); + } +}