Repository: avaje/avaje-inject
Pull request #817 (2025-05-08 00:52:25)

Support Nested Wildcard Lists
- fix cases where a list of generic wildcards generates bad code
- now generic types are registered with their main type as a key


Patch:
diff --git a/inject-generator/src/main/java/io/avaje/inject/generator/MethodReader.java b/inject-generator/src/main/java/io/avaje/inject/generator/MethodReader.java
index 92f6a29f..cc411927 100644
--- a/inject-generator/src/main/java/io/avaje/inject/generator/MethodReader.java
+++ b/inject-generator/src/main/java/io/avaje/inject/generator/MethodReader.java
@@ -563,8 +563,14 @@ final class MethodReader {
     }
 
     void builderGetDependency(Append writer, String builderName) {
-      writer.append(builderName).append(".").append(utilType.getMethod(nullable, isBeanMap));
-      if (!genericType.isGeneric() || genericType.param0().kind() == TypeKind.WILDCARD) {
+      final boolean wildcard = isWildcard();
+      final var wildParam = wildcard ? String.format("<%s>", genericType.shortWithoutAnnotations()) : "";
+      writer
+        .append(builderName)
+        .append(".")
+        .append(wildParam)
+        .append(utilType.getMethod(nullable, isBeanMap));
+      if (!genericType.isGeneric() || wildcard) {
         writer.append(Util.shortName(genericType.mainType())).append(".class");
       } else {
         writer.append("TYPE_").append(Util.shortName(genericType).replace(".", "_"));
@@ -585,6 +591,11 @@ final class MethodReader {
       writer.append(")");
     }
 
+    private boolean isWildcard() {
+      return genericType.isGeneric()
+        && genericType.componentTypes().stream().allMatch(g -> g.kind() == TypeKind.WILDCARD);
+    }
+
     String simpleName() {
       return simpleName;
     }
@@ -602,7 +613,7 @@ final class MethodReader {
     }
 
     Dependency dependsOn() {
-      return new Dependency(paramType, named, utilType.isCollection());
+      return new Dependency(isWildcard() ? genericType.mainType() : paramType, named, utilType.isCollection());
     }
 
     void addImports(ImportTypeMap importTypes) {
diff --git a/inject-generator/src/main/java/io/avaje/inject/generator/TypeExtendsReader.java b/inject-generator/src/main/java/io/avaje/inject/generator/TypeExtendsReader.java
index b7e9df67..dbaf5b15 100644
--- a/inject-generator/src/main/java/io/avaje/inject/generator/TypeExtendsReader.java
+++ b/inject-generator/src/main/java/io/avaje/inject/generator/TypeExtendsReader.java
@@ -210,13 +210,17 @@ final class TypeExtendsReader {
       final String type = Util.unwrapProvider(fullName);
 
       if (proxyBean || isPublic(element)) {
-        final var genericType = !Objects.equals(fullName, type) ? UType.parse(mirror).param0() : UType.parse(mirror);
+        UType uType = UType.parse(mirror);
+        final var genericType = !Objects.equals(fullName, type) ? uType.param0() : uType;
         // check if any unknown generic types are in the parameters (T,T2, etc.)
         final var knownType = genericType.componentTypes().stream()
           .flatMap(g -> Stream.concat(Stream.of(g), g.componentTypes().stream()))
           .noneMatch(g -> g.kind() == TypeKind.TYPEVAR);
 
         extendsTypes.add(knownType ? Util.unwrapProvider(mirror) : genericType);
+        if (uType.isGeneric()) {
+          extendsTypes.add(UType.parse(types().erasure(Util.stripProvider(mirror))));
+        }
         extendsInjection.read(element);
       }
 
@@ -257,6 +261,9 @@ final class TypeExtendsReader {
         }
       }
       interfaceTypes.add(rawUType);
+      if (rawUType.isGeneric()) {
+        interfaceTypes.add(UType.parse(types().erasure(Util.stripProvider(anInterface))));
+      }
       for (final TypeMirror supertype : types().directSupertypes(anInterface)) {
         readInterfacesOf(supertype);
       }
diff --git a/inject-generator/src/main/java/io/avaje/inject/generator/Util.java b/inject-generator/src/main/java/io/avaje/inject/generator/Util.java
index de9b34a9..473ab393 100644
--- a/inject-generator/src/main/java/io/avaje/inject/generator/Util.java
+++ b/inject-generator/src/main/java/io/avaje/inject/generator/Util.java
@@ -50,7 +50,7 @@ final class Util {
 
   static String classOfMethod(String method) {
     final int pos = method.lastIndexOf('.');
-    return (pos == -1) ? "" : method.substring(0, pos);
+    return pos == -1 ? "" : method.substring(0, pos);
   }
 
   static String shortMethod(String method) {
@@ -92,7 +92,7 @@ final class Util {
       return "";
     }
     pos = cls.lastIndexOf('.', pos - 1);
-    return (pos == -1) ? "" : cls.substring(0, pos);
+    return pos == -1 ? "" : cls.substring(0, pos);
   }
 
   static String unwrapProvider(String maybeProvider) {
@@ -104,11 +104,7 @@ final class Util {
   }
 
   static UType unwrapProvider(TypeMirror maybeProvider) {
-    if (isProvider(maybeProvider.toString())) {
-      return UType.parse(maybeProvider).param0();
-    } else {
-      return UType.parse(maybeProvider);
-    }
+    return UType.parse(stripProvider(maybeProvider));
   }
 
   static UType unwrapProvider(UType maybeProvider) {
@@ -119,6 +115,14 @@ final class Util {
     }
   }
 
+  static TypeMirror stripProvider(TypeMirror maybeProvider) {
+    if (isProvider(maybeProvider.toString())) {
+      return ((DeclaredType) maybeProvider).getTypeArguments().get(0);
+    } else {
+      return maybeProvider;
+    }
+  }
+
   static String initLower(String name) {
     final StringBuilder sb = new StringBuilder(name.length());
     boolean upper = true;


Test patch:
diff --git a/inject-test/src/test/java/org/example/coffee/CoffeeMakerTest.java b/inject-test/src/test/java/org/example/coffee/CoffeeMakerTest.java
index 531f3872..f85e0f64 100644
--- a/inject-test/src/test/java/org/example/coffee/CoffeeMakerTest.java
+++ b/inject-test/src/test/java/org/example/coffee/CoffeeMakerTest.java
@@ -5,6 +5,7 @@ import io.avaje.inject.BeanScope;
 import org.example.coffee.core.DuperPump;
 import org.example.coffee.generic.HazRepo;
 import org.example.coffee.generic.HazRepo$DI;
+import org.example.coffee.generic.Repository;
 import org.example.coffee.list.BSomei;
 import org.example.coffee.list.Somei;
 import org.example.coffee.provider.AProv;
@@ -115,7 +116,7 @@ class CoffeeMakerTest {
         .findFirst().orElse(null);
 
       assertThat(hazRepo.keys())
-        .containsExactly(name(HazRepo.class), name(HazRepo$DI.TYPE_RepositoryHazLong));
+        .containsExactly(name(HazRepo.class), name(HazRepo$DI.TYPE_RepositoryHazLong), name(Repository.class));
     }
   }
 
diff --git a/inject-test/src/test/java/org/example/coffee/generic/MultiGenericConsumer.java b/inject-test/src/test/java/org/example/coffee/generic/MultiGenericConsumer.java
index c6e85a30..0598c39b 100644
--- a/inject-test/src/test/java/org/example/coffee/generic/MultiGenericConsumer.java
+++ b/inject-test/src/test/java/org/example/coffee/generic/MultiGenericConsumer.java
@@ -1,5 +1,7 @@
 package org.example.coffee.generic;
 
+import java.util.List;
+
 import org.example.coffee.grind.AMusher;
 
 import jakarta.inject.Singleton;
@@ -16,18 +18,29 @@ class MultiGenericConsumer {
 
   private final AMusher aMusher;
 
-  MultiGenericConsumer(Repository<Haz, Long> hazRepo, AMusher aMusher, SomeGeneric<String> stringProcessor) {
+  private List<SomeGeneric<?>> list;
+
+  MultiGenericConsumer(
+      Repository<Haz, Long> hazRepo,
+      AMusher aMusher,
+      SomeGeneric<String> stringProcessor,
+      List<SomeGeneric<?>> list) {
     this.hazRepo = hazRepo;
     this.aMusher = aMusher;
     this.stringProcessor = stringProcessor;
+    this.list = list;
   }
 
   String findAndDo(long id) {
     final Haz byId = hazRepo.findById(id);
-    return (byId == null) ? "not found" : "found " + stringProcessor.process("" + byId.id);
+    return byId == null ? "not found" : "found " + stringProcessor.process("" + byId.id);
   }
 
   String mushString() {
     return aMusher.toString();
   }
+
+  List<SomeGeneric<?>> list() {
+    return list;
+  }
 }
diff --git a/inject-test/src/test/java/org/example/coffee/generic/MultiGenericConsumerTest.java b/inject-test/src/test/java/org/example/coffee/generic/MultiGenericConsumerTest.java
index 33bafb5d..42e602f6 100644
--- a/inject-test/src/test/java/org/example/coffee/generic/MultiGenericConsumerTest.java
+++ b/inject-test/src/test/java/org/example/coffee/generic/MultiGenericConsumerTest.java
@@ -1,9 +1,10 @@
 package org.example.coffee.generic;
 
-import io.avaje.inject.xtra.ApplicationScope;
+import static org.assertj.core.api.Assertions.assertThat;
+
 import org.junit.jupiter.api.Test;
 
-import static org.assertj.core.api.Assertions.assertThat;
+import io.avaje.inject.xtra.ApplicationScope;
 
 public class MultiGenericConsumerTest {
 
@@ -14,6 +15,6 @@ public class MultiGenericConsumerTest {
 
     assertThat(bean.findAndDo(34L)).isEqualTo("found 34 stuff");
     assertThat(bean.mushString()).isNotNull();
+    assertThat(bean.list()).isNotEmpty();
   }
-
 }