From 1f4e0415657c15e38e5527a5976c317a2f3dd512 Mon Sep 17 00:00:00 2001 From: Simon Resch Date: Wed, 5 Nov 2025 11:54:04 +0100 Subject: [PATCH 1/5] 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 07eb3842e..68807edb3 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; @@ -901,6 +926,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 3bb995587d9b181badc3552b5f60be0b08505249 Mon Sep 17 00:00:00 2001 From: Simon Resch Date: Wed, 5 Nov 2025 15:35:51 +0100 Subject: [PATCH 2/5] 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 68807edb3..b4aa8b12b 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; @@ -920,6 +952,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 2940750bc9665023197a2911b18903f3dcf5a6db Mon Sep 17 00:00:00 2001 From: Simon Resch Date: Wed, 5 Nov 2025 16:18:53 +0100 Subject: [PATCH 3/5] 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 b4aa8b12b..cbbc018d7 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; @@ -977,6 +998,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 4bac9d35193f6b04330541d1ce2f2453990297df Mon Sep 17 00:00:00 2001 From: Simon Resch Date: Wed, 12 Nov 2025 07:55:33 +0100 Subject: [PATCH 4/5] 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..cdbb812b8 --- /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.clone(); + this.ownerType = ownerType; + } + + @Override + public Type[] getActualTypeArguments() { + return typeArguments.clone(); + } + + @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.clone(); + this.lowerBounds = lowerBounds.clone(); + } + + @Override + public AnnotatedType[] getAnnotatedLowerBounds() { + return lowerBounds.clone(); + } + + @Override + public AnnotatedType[] getAnnotatedUpperBounds() { + return upperBounds.clone(); + } + + @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.clone(); + } + + @Override + public AnnotatedType[] getAnnotatedActualTypeArguments() { + return typeArguments.clone(); + } + + @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); + } +} From 6fe1e52b4d2d6d4e8effb63b951f872ac26819fe Mon Sep 17 00:00:00 2001 From: Simon Resch Date: Thu, 20 Nov 2025 10:18:47 +0100 Subject: [PATCH 5/5] feat: mutator support for generic superclasses This commit adds mutator support for classes that have generic superclasses. The inheritance chain is walked up collecting all type parameters. This is also done for all interfaces. --- .../mutator/aggregate/BeanSupport.java | 27 ++++-- .../support/ParameterizedTypeSupport.java | 51 +++++++++-- .../aggregate/SetterBasedBeanMutatorTest.java | 10 +++ .../support/ParameterizedTypeSupportTest.java | 85 +++++++++++++++++++ 4 files changed, 156 insertions(+), 17 deletions(-) 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 d9d936453..42882f2cc 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 @@ -32,6 +32,7 @@ import java.lang.reflect.Executable; import java.lang.reflect.Method; import java.lang.reflect.Modifier; +import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.util.Arrays; import java.util.Comparator; @@ -52,17 +53,27 @@ static Optional> optionalClassForName(String targetClassName) { } } + private static Class rawBeanType(Type classType) { + if (classType instanceof Class) { + return (Class) classType; + } else if (classType instanceof ParameterizedType) { + return rawBeanType(((ParameterizedType) classType).getRawType()); + } else { + // Bail out on wildcard types or type variables neither of which are supported as "top level" + // types in a @FuzzTest. Also bail out on generic array types as they are handled by the + // ArrayMutatorFactory. + throw new UnsupportedOperationException("Unsupported type: " + classType); + } + } + // 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] = resolveTypeArguments(e.getDeclaringClass(), classType, annotated[i]); - } - return result; + Class clazz = rawBeanType(classType.getType()); + return stream(e.getAnnotatedParameterTypes()) + .map(t -> resolveTypeArguments(clazz, classType, t)) + .toArray(AnnotatedType[]::new); } // Returns the parameter types of a method or constructor resolving all generic type arguments. @@ -74,7 +85,7 @@ public static Type[] resolveParameterTypes(Executable e, AnnotatedType classType static Type resolveReturnType(Method method, AnnotatedType classType) { return resolveTypeArguments( - method.getDeclaringClass(), classType, method.getAnnotatedReturnType()) + rawBeanType(classType.getType()), 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 index cdbb812b8..b7fcfb937 100644 --- a/src/main/java/com/code_intelligence/jazzer/mutation/support/ParameterizedTypeSupport.java +++ b/src/main/java/com/code_intelligence/jazzer/mutation/support/ParameterizedTypeSupport.java @@ -64,21 +64,50 @@ public class ParameterizedTypeSupport { */ public static AnnotatedType resolveTypeArguments( Class clazz, AnnotatedType classType, AnnotatedType type) { - if (!(classType instanceof AnnotatedParameterizedType)) { + Map, AnnotatedType> mapping = new HashMap<>(); + updateTypeMappings(clazz, classType, mapping); + if (mapping.isEmpty()) { return type; } + return resolveRecursive(type.getType(), type, mapping); + } - TypeVariable[] typeParameters = clazz.getTypeParameters(); - AnnotatedType[] typeArguments = - ((AnnotatedParameterizedType) classType).getAnnotatedActualTypeArguments(); + private static void updateTypeMappings( + Class clazz, + AnnotatedType annotatedClazzType, + Map, AnnotatedType> mapping) { + if (annotatedClazzType instanceof AnnotatedParameterizedType) { + TypeVariable[] typeParameters = clazz.getTypeParameters(); + AnnotatedType[] typeArguments = + ((AnnotatedParameterizedType) annotatedClazzType).getAnnotatedActualTypeArguments(); + require(typeArguments.length == typeParameters.length); + for (int i = 0; i < typeParameters.length; i++) { + mapping.put(typeParameters[i], typeArguments[i]); + } + } - require(typeArguments.length == typeParameters.length); + Class superClass = clazz.getSuperclass(); + AnnotatedType annotatedSuperclass = clazz.getAnnotatedSuperclass(); + Type genericSuperclass = clazz.getGenericSuperclass(); + if (superClass != null && annotatedSuperclass != null && genericSuperclass != null) { + AnnotatedType resolvedSuperclass = + resolveRecursive(genericSuperclass, annotatedSuperclass, mapping); + updateTypeMappings(superClass, resolvedSuperclass, mapping); + } - Map, AnnotatedType> mapping = new HashMap<>(); - for (int i = 0; i < typeParameters.length; i++) { - mapping.put(typeParameters[i], typeArguments[i]); + Class[] interfaces = clazz.getInterfaces(); + AnnotatedType[] annotatedInterfaces = clazz.getAnnotatedInterfaces(); + Type[] genericInterfaces = clazz.getGenericInterfaces(); + for (int i = 0; i < interfaces.length; i++) { + AnnotatedType annotatedInterface = annotatedInterfaces[i]; + Type genericInterface = genericInterfaces[i]; + if (annotatedInterface == null || genericInterface == null) { + continue; + } + AnnotatedType resolvedInterface = + resolveRecursive(genericInterface, annotatedInterface, mapping); + updateTypeMappings(interfaces[i], resolvedInterface, mapping); } - return resolveRecursive(type.getType(), type, mapping); } /** @@ -107,6 +136,10 @@ private static AnnotatedType resolveRecursive( if (replacement == null) { return annotated; } + if (replacement instanceof AnnotatedWildcardType) { + // Forwarding annotations to wildcard types is not supported + return replacement; + } return TypeSupport.forwardAnnotations(annotated, replacement); } return annotated; 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 a04d9d096..22db5774b 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 @@ -246,4 +246,14 @@ void genericClass() { .createOrThrow(new TypeHolder>() {}.annotatedType()); assertThat(mutator.toString()).isEqualTo("Nullable<[Nullable[]>] -> Generic>"); } + + public static class Child extends Generic {} + + @Test + void genericClassChild() { + SerializingMutator mutator = + (SerializingMutator) + Mutators.newFactory().createOrThrow(new TypeHolder() {}.annotatedType()); + assertThat(mutator.toString()).isEqualTo("Nullable<[Nullable[]>] -> Child>"); + } } 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 index 8245cb820..d187a78c1 100644 --- a/src/test/java/com/code_intelligence/jazzer/mutation/support/ParameterizedTypeSupportTest.java +++ b/src/test/java/com/code_intelligence/jazzer/mutation/support/ParameterizedTypeSupportTest.java @@ -125,4 +125,89 @@ class Generic { assertThat(parameterizedType.getActualTypeArguments()[0]).isEqualTo(String.class); assertThat(parameterizedType.getActualTypeArguments()[1]).isEqualTo(Integer.class); } + + @Test + void resolveParameterizedTypeChildClass() throws NoSuchFieldException { + class Base { + public Map field; + } + class Child extends Base {} + AnnotatedType annotatedType = Child.class.getField("field").getAnnotatedType(); + AnnotatedParameterizedType classType = + (AnnotatedParameterizedType) new TypeHolder>() {}.annotatedType(); + AnnotatedType resolved = + ParameterizedTypeSupport.resolveTypeArguments(Child.class, classType, annotatedType); + + assertThat(resolved).isInstanceOf(AnnotatedParameterizedType.class); + + AnnotatedParameterizedType parameterType = (AnnotatedParameterizedType) resolved; + assertThat(((ParameterizedType) parameterType.getType()).getRawType()).isEqualTo(Map.class); + AnnotatedType[] elementTypes = parameterType.getAnnotatedActualTypeArguments(); + assertThat(elementTypes).hasLength(2); + assertThat(elementTypes[0].getType()).isEqualTo(String.class); + assertThat( + TypeSupport.annotatedTypeEquals( + classType.getAnnotatedActualTypeArguments()[0], elementTypes[1])) + .isTrue(); + } + + @Test + void resolveParameterizedType_multiLevelHierarchy() throws NoSuchFieldException { + class Root { + public List field; + } + class Middle extends Root> {} + class Leaf extends Middle {} + class Concrete extends Leaf {} + + AnnotatedType annotatedType = Concrete.class.getField("field").getAnnotatedType(); + AnnotatedType classType = new TypeHolder() {}.annotatedType(); + AnnotatedType resolved = + ParameterizedTypeSupport.resolveTypeArguments(Concrete.class, classType, annotatedType); + + assertThat(resolved).isInstanceOf(AnnotatedParameterizedType.class); + + AnnotatedParameterizedType outerList = (AnnotatedParameterizedType) resolved; + assertThat(((ParameterizedType) outerList.getType()).getRawType()).isEqualTo(List.class); + AnnotatedType nestedListType = outerList.getAnnotatedActualTypeArguments()[0]; + assertThat(nestedListType).isInstanceOf(AnnotatedParameterizedType.class); + + AnnotatedParameterizedType innerList = (AnnotatedParameterizedType) nestedListType; + assertThat(((ParameterizedType) innerList.getType()).getRawType()).isEqualTo(List.class); + AnnotatedType innerElement = innerList.getAnnotatedActualTypeArguments()[0]; + assertThat(innerElement.getType()).isEqualTo(String.class); + } + + private interface LocalSupplier { + List supply(); + } + + private interface AnnotatedSupplier extends LocalSupplier> {} + + @Test + void resolveParameterizedType_interfaceHierarchy() throws NoSuchMethodException { + AnnotatedType annotatedType = LocalSupplier.class.getMethod("supply").getAnnotatedReturnType(); + AnnotatedParameterizedType interfaceType = + (AnnotatedParameterizedType) + new TypeHolder>() {}.annotatedType(); + AnnotatedType resolved = + ParameterizedTypeSupport.resolveTypeArguments( + AnnotatedSupplier.class, interfaceType, annotatedType); + + assertThat(resolved).isInstanceOf(AnnotatedParameterizedType.class); + + AnnotatedParameterizedType outerList = (AnnotatedParameterizedType) resolved; + assertThat(((ParameterizedType) outerList.getType()).getRawType()).isEqualTo(List.class); + + AnnotatedType nestedType = outerList.getAnnotatedActualTypeArguments()[0]; + assertThat(nestedType).isInstanceOf(AnnotatedParameterizedType.class); + + AnnotatedParameterizedType innerList = (AnnotatedParameterizedType) nestedType; + assertThat(((ParameterizedType) innerList.getType()).getRawType()).isEqualTo(List.class); + AnnotatedType terminalElement = innerList.getAnnotatedActualTypeArguments()[0]; + assertThat( + TypeSupport.annotatedTypeEquals( + interfaceType.getAnnotatedActualTypeArguments()[0], terminalElement)) + .isTrue(); + } }