Skip to content

Conversation

@simonresch
Copy link
Contributor

@simonresch simonresch commented Nov 5, 2025

Adds mutator support for generic classes.

Constructing mutators for generic classes would previously either fail or not work as expected due to parameter and return types being reported as java.lang.Object by the reflection API (type erasure). Since we can't provide a general Object mutator this would creation of a valid mutator for most generic classes.
Using the AnnotatedType of the class we resolve type parameters to create the correct underlying mutators.

Example fuzz test:

  public static class MyClass<L, S> {
    private final L[] list;
    private final Set<S> set;

    public MyClass(L[] list, Set<S> set) {
      this.list = list;
      this.set = set;
    }

    public L[] getList() {
      return this.list;
    }

    public Set<S> getSet() {
      return this.set;
    }
  }

  @FuzzTest
  void fuzzTest(@NotNull(constraint = PropertyConstraint.RECURSIVE) MyClass<String, Integer> data) {
    if (data.getList().length > 5
        && data.getList()[5].equals("foo")
        && data.getSet().size() == 5) {
      throw new RuntimeException("Can fuzz generics");
    }
  }

@simonresch simonresch force-pushed the CIF-1866-support-generic-classes branch 3 times, most recently from f119aa8 to 581c4d5 Compare November 5, 2025 15:54
@simonresch simonresch changed the title feat: support generic constructor based beans feat: add mutator support for generic classes Nov 5, 2025
@simonresch simonresch marked this pull request as ready for review November 5, 2025 16:07
@simonresch simonresch requested review from a team and Copilot November 5, 2025 16:59
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR adds support for generic classes in the mutator framework by resolving type parameters at runtime. Previously, generic classes would fail to create valid mutators due to type erasure causing parameter and return types to be reported as java.lang.Object. The implementation uses AnnotatedType to resolve type parameters and construct mutators with the correct concrete types.

Key Changes:

  • Added type resolution logic to handle generic type parameters in constructors, methods, and return types
  • Updated all aggregate mutator factories to resolve generic types before creating mutators
  • Added comprehensive test coverage for generic classes across different mutator types (setter-based, constructor-based, cached, records)

Reviewed Changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
BeanSupport.java Adds core type resolution methods for resolving generic type arguments in methods, constructors, and return types
AggregatesHelper.java Updates mutator creation to use resolved types instead of raw types from reflection API
CachedConstructorMutatorFactory.java Uses resolved parameter types when building mutators for generic classes
ConstructorBasedBeanMutatorFactory.java Resolves parameter types and return types when matching getters to constructors
SetterBasedBeanMutatorFactory.java Resolves setter parameter types to properly handle generic classes
SuperBuilderMutatorFactory.java Updates to use resolved parameter types for builder pattern support
SetterBasedBeanMutatorTest.java Adds test for generic class with setter-based mutation
RecordMutatorTest.java Adds test for generic record types
ConstructorBasedBeanMutatorTest.java Adds tests for generic classes with single and nested type parameters
CachedConstructorMutatorTest.java Adds test for generic class with cached constructor
StressTest.java Adds stress test cases for various generic class patterns

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@simonresch simonresch force-pushed the CIF-1866-support-generic-classes branch 2 times, most recently from 08f6331 to a9ae722 Compare November 6, 2025 08:45
@oetr oetr self-requested a review November 7, 2025 12:35
@simonresch simonresch force-pushed the CIF-1866-support-generic-classes branch from a9ae722 to f8cd737 Compare November 11, 2025 09:25
@simonresch simonresch marked this pull request as draft November 12, 2025 13:49
@simonresch
Copy link
Contributor Author

Added a commit to support non-trivial use of the type variable. For example the fuzz test

  public static class MyClass<L, S> {
    private final L[] list;
    private final Set<S> set;

    public MyClass(L[] list, Set<S> set) {
      this.list = list;
      this.set = set;
    }

    public L[] getList() {
      return this.list;
    }

    public Set<S> getSet() {
      return this.set;
    }
  }

  @FuzzTest
  void fuzzTest(@NotNull(constraint = PropertyConstraint.RECURSIVE) MyClass<String, Integer> data) {
    if (data.getList().length > 5
        && data.getList()[5].equals("foo")
        && data.getSet().size() == 5) {
      throw new RuntimeException("Can fuzz generics");
    }
  }

will create a mutator like this: Arguments[[String[], Set<Integer>] -> MyClass]

@simonresch simonresch marked this pull request as ready for review November 12, 2025 20:47
@simonresch simonresch requested review from Copilot and oetr November 12, 2025 20:47
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated no new comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link
Contributor

@oetr oetr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome, thanks!

The type resolution doesn't work for some cases, but other than that it looks solid. Great work!

* @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(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tested various generic Classes with the help of our ArgumentsMutatorFuzzTest.

To kick things off, this non-generic version works and gives us correct mutator: Arguments[[String] -> Child]

public static class Base {
    private String x;

    public void setX(String x) { this.x = x; }
    public String getX() { return x; }
  }
  public static class Child extends Base {}

@Solo
@SelfFuzzTest
public static void fuzzGenericClass(@NotNull(constraint = PropertyConstraint.RECURSIVE) Child pair) {}

The same but generic version with fixed generic type of the superclass Base gives us the trivial mutator that always returns null: Arguments[[] -> Child]
Expected: Arguments[[String]->Child]

public static class Base<T> {
    private T x;

    public void setX(T x) { this.x = x; }
    public T getX() { return x; }
  }
  public static class Child extends Base<String> {
  }

@Solo
@SelfFuzzTest
public static void fuzzGenericClass(@NotNull(constraint = PropertyConstraint.RECURSIVE) Child pair) {}

This gives us the wrong mutator: Arguments[[Integer] -> Child]
Expected: Arguments[[String] -> Child]

public static class Base<X> {
     protected X x;

     public void setX(X x) { this.x = x; }
     public X getX() { return x; }
   }
   public static class Child<U> extends Base<String> { }

 @Solo
 @SelfFuzzTest
 public static void fuzzGenericClass(@NotNull Child<Integer> pair) {}

A variation of above with the same problem--- it gives us the wrong mutator: Arguments[[String, String] -> Child]
Expected: Arguments[[Integer,String]->Child] (or first String and then Integer, depending on which class we start with first):

public static class Base<X> {
     protected X x;

     public void setX(X x) { this.x = x; }
     public X getX() { return x; }
   }
   public static class Child<U> extends Base<Integer> {
     protected U u;
     public U getU() { return u; }
     public void setU(U u) { this.u = u; }
   }

   @Solo
   @SelfFuzzTest
   public static void fuzzGenericClass(@NotNull Child<@NotNull String> pair) {
      if (pair == null) {
        throw new IllegalArgumentException("pair is null");
      }
      // child gets resolved
      String u = pair.getU();
      // base does not get resolved

      Object x = pair.getX();
      if (x == null) {
        return;
      }
      // intelliJ tells me to remove this, because it's "always false"!
      if (!(x instanceof Integer)) {
        throw new IllegalArgumentException("x is not an Integer: " + x);
      }
   }

The following fuzz test runs into the requirement error in resolveTypeArguments() in ParameterizedTypeSupport.java:75
Expected: Arguments[[String] -> Child]

public static class Base<T> {
     protected T x;

     public void setX(T x) { this.x = x; }
     public T getX() { return x; }
   }
   // Here U is not related to getters and setters
   public static class Child<T, U> extends Base<T> { }

   @Solo
   @SelfFuzzTest
   public static void fuzzGenericClass(@NotNull(constraint = PropertyConstraint.RECURSIVE) Child<String, Integer> pair) { }

Here are the values of involved variables:

clazz: class com.code_intelligence.selffuzz.mutation.ArgumentsMutatorFuzzTest$Base
typeParameters: [T]
classType: @com.code_intelligence.selffuzz.jazzer.mutation.annotation.NotNull(constraint="JAZZER_PROPERTY_CONSTRAINT_RECURSIVE") com.code_intelligence.selffuzz.mutation.ArgumentsMutatorFuzzTest$Child<java.lang.String, java.lang.Integer>
typeArguments: [java.lang.String, java.lang.Integer]

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great test cases! Yes, generic superclasses were completely missing in the implementation.

I added another commit that walks up all the superclasses (and dependencies) to build up a more accurate mapping of type variables. That seems to cover all the cases you found not to be working.

return annotated;
}

private static AnnotatedParameterizedType resolveParameterizedType(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because the reflection API has both ParameterizedType and AnnotatedParameterizedType, I find this name imprecise. Should we rename all functions that return annotated types into ...Annotated... and those that return Type into ...Raw...?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If there is only one version of those functions I would personally prefer the shorter name. Its clear from the return type, that we get an AnnotatedType, no?

Class<?> clazz, AnnotatedType classType, Stream<Type> types) {
Map<Type, List<Method>> gettersByType =
findMethods(clazz, BeanSupport::isGetter)
.collect(groupingBy(m -> resolveReturnType(m, classType)));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If m is not declared in classType, which here is the instantiated class of the concrete bean, the resolution doesn't work, as seen in the examples in another comment below.
To fix it, we should probably resolve all necessary generic types untill we reach the superclass where m is declared.
The same applies for the return types of the setters.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe the additions in ParameterizedTypeSupport.java a12e29d should take care of this by going up the entire superclass chain to resolve all type variables.

This enables mutator support for classes that use type variables as part
of an array, wildcard or other generic class.
E.g. `class MyClass<T> { MyClass(Set<T[]> setOfArrays){...} }`.
@simonresch simonresch force-pushed the CIF-1866-support-generic-classes branch from d77f4fc to a12e29d Compare November 20, 2025 09:20
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.
@simonresch simonresch force-pushed the CIF-1866-support-generic-classes branch from a12e29d to 3b402e9 Compare November 20, 2025 10:43
@simonresch simonresch requested a review from oetr November 20, 2025 10:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants