Repository: avaje/avaje-inject
Pull request #517 (2024-03-09 15:38:54)

Fix Factory Providers
- Fixes generated Modules not compiling in certain cases when using `Provider` in factory classes
- Fixes DI classes not compiling with Providers of Generic Types
- remove duplicate auto-provides in dependency metadata


Patch:
diff --git a/inject-generator/src/main/java/io/avaje/inject/generator/MetaData.java b/inject-generator/src/main/java/io/avaje/inject/generator/MetaData.java
index d1062101..faba30a1 100644
--- a/inject-generator/src/main/java/io/avaje/inject/generator/MetaData.java
+++ b/inject-generator/src/main/java/io/avaje/inject/generator/MetaData.java
@@ -85,7 +85,7 @@ final class MetaData {
     if (Util.isVoid(type)) {
       return "void_" + Util.trimMethod(method);
     } else {
-      final String trimType = Util.trimMethod(type);
+      final String trimType = Util.trimMethod(Util.unwrapProvider(type));
       if (name != null) {
         return trimType + "_" + name.replaceAll("[^a-zA-Z0-9_$]+", "_");
       } else {
@@ -243,7 +243,11 @@ final class MetaData {
     if (size > 1) {
       sb.eol().append("        ");
     }
+    var seen = new HashSet<String>();
     for (int i = 0; i < types.size(); i++) {
+      if (!seen.add(types.get(i))) {
+        continue;
+      }
       if (i > 0) {
         sb.append(",").eol().append("        ");
       }
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 328776ff..efa25583 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
@@ -181,19 +181,22 @@ final class MethodReader {
 
   void builderAddBeanProvider(Append writer) {
     if (isVoid) {
-      writer.append("Error - void @Prototype method ?").eol();
+      APContext.logError("Error - void @Prototype method ?", element);
       return;
     }
     if (optionalType) {
-      writer.append("Error - Optional type with @Prototype method is not supported").eol();
+      APContext.logError("Error - Optional type with @Prototype method is not supported", element);
       return;
     }
     String indent = "    ";
+    writer.indent(indent).append("  builder.");
     if (prototype) {
-      writer.indent(indent).append("  builder.asPrototype().registerProvider(() -> {").eol();
-    } else {
-      writer.indent(indent).append("  builder.asSecondary().registerProvider(() -> {").eol();
+      writer.append("asPrototype()").eol();
+    } else if(secondary) {
+      writer.append("asSecondary()").eol();
     }
+    writer.indent(".registerProvider(() -> {");
+
     writer.indent(indent).append("    return ");
     writer.append("factory.%s(", methodName);
     for (int i = 0; i < params.size(); i++) {
@@ -224,7 +227,11 @@ final class MethodReader {
       } else if (secondary) {
         writer.append(".asSecondary()");
       }
-      writer.append(".register(bean);").eol();
+      if (Util.isProvider(returnTypeRaw)) {
+        writer.append(".registerProvider(bean);").eol();
+      } else {
+        writer.append(".register(bean);").eol();
+      }
       if (notEmpty(initMethod)) {
         writer.indent(indent).append("builder.addPostConstruct($bean::%s);", initMethod).eol();
       }
@@ -340,7 +347,7 @@ final class MethodReader {
   }
 
   boolean isUseProviderForSecondary() {
-    return secondary && !optionalType;
+    return secondary && !optionalType && !Util.isProvider(returnTypeRaw);
   }
 
   boolean isPublic() {
@@ -424,8 +431,6 @@ final class MethodReader {
       writer.append(builderName).append(".").append(utilType.getMethod(nullable, isBeanMap));
       if (!genericType.isGeneric() || genericType.param0().kind() == TypeKind.WILDCARD) {
         writer.append(Util.shortName(genericType.mainType())).append(".class");
-      } else if (isProvider()) {
-        writer.append(providerParam()).append(".class");
       } else {
         writer.append("TYPE_").append(Util.shortName(genericType).replace(".", "_"));
       }
diff --git a/inject-generator/src/main/java/io/avaje/inject/generator/SimpleBeanWriter.java b/inject-generator/src/main/java/io/avaje/inject/generator/SimpleBeanWriter.java
index afa1bb7a..2fdf2238 100644
--- a/inject-generator/src/main/java/io/avaje/inject/generator/SimpleBeanWriter.java
+++ b/inject-generator/src/main/java/io/avaje/inject/generator/SimpleBeanWriter.java
@@ -2,6 +2,7 @@ package io.avaje.inject.generator;
 
 import static io.avaje.inject.generator.APContext.createSourceFile;
 import static io.avaje.inject.generator.APContext.logError;
+import static java.util.stream.Collectors.toSet;
 
 import java.io.IOException;
 import java.io.Writer;
@@ -69,13 +70,18 @@ final class SimpleBeanWriter {
 
   private void writeGenericTypeFields() {
     // collect all types to prevent duplicates
-    Set<UType> genericTypes = beanReader.allGenericTypes();
-    if (!genericTypes.isEmpty()) {
+    Set<UType> genericTypes =
+      beanReader.allGenericTypes().stream()
+        .map(Util::unwrapProvider)
+        .filter(UType::isGeneric)
+        .collect(toSet());
 
+    if (!genericTypes.isEmpty()) {
       final Map<String, String> seenShortNames = new HashMap<>();
       final Set<String> writtenFields = new HashSet<>();
 
-      for (final UType type : genericTypes) {
+      for (final UType utype : genericTypes) {
+        var type = Util.unwrapProvider(utype);
         final var fieldName = Util.shortName(type).replace(".", "_");
         final var components = type.componentTypes();
         if (components.size() == 1 && components.get(0).kind() == TypeKind.WILDCARD
@@ -86,7 +92,6 @@ final class SimpleBeanWriter {
 
         writer.append("  public static final Type TYPE_%s =", fieldName).eol()
           .append("      new GenericType<");
-
         writeGenericType(type, seenShortNames, writer);
         // use fully qualified types here rather than use type.writeShort(writer)
         writer.append(">(){}.type();").eol();
diff --git a/inject-generator/src/main/java/io/avaje/inject/generator/TypeAppender.java b/inject-generator/src/main/java/io/avaje/inject/generator/TypeAppender.java
index 364bc6c8..775814af 100644
--- a/inject-generator/src/main/java/io/avaje/inject/generator/TypeAppender.java
+++ b/inject-generator/src/main/java/io/avaje/inject/generator/TypeAppender.java
@@ -1,5 +1,7 @@
 package io.avaje.inject.generator;
 
+import static java.lang.annotation.ElementType.LOCAL_VARIABLE;
+
 import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Set;
@@ -19,7 +21,8 @@ final class TypeAppender {
     this.importTypes = importTypes;
   }
 
-  void add(UType type) {
+  void add(UType utype) {
+    var type = Util.unwrapProvider(utype);
     var components = type.componentTypes();
     if (isAddGenericType(type, components)) {
       addUType(type);
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 1e6d8363..cc02d3c1 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
@@ -118,11 +118,11 @@ final class TypeExtendsReader {
       return List.of();
     }
     if (baseTypeIsInterface) {
-      return List.of(Util.unwrapProvider(baseType.asType()));
+      return List.of(Util.unwrapProvider(baseUType));
     }
     var autoProvides = new ArrayList<>(interfaceTypes);
     autoProvides.addAll(extendsTypes);
-    autoProvides.add(Util.unwrapProvider(baseType.asType()));
+    autoProvides.add(Util.unwrapProvider(baseUType));
     return autoProvides;
   }
 
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 c94e48ea..721e78f5 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
@@ -91,6 +91,14 @@ final class Util {
     }
   }
 
+  static UType unwrapProvider(UType maybeProvider) {
+    if (isProvider(maybeProvider.mainType())) {
+      return maybeProvider.param0();
+    } else {
+      return maybeProvider;
+    }
+  }
+
   static String initLower(String name) {
     final StringBuilder sb = new StringBuilder(name.length());
     boolean upper = true;


Test patch:
diff --git a/inject-generator/src/test/java/io/avaje/inject/generator/models/valid/provider/FactoryProvider.java b/inject-generator/src/test/java/io/avaje/inject/generator/models/valid/provider/FactoryProvider.java
new file mode 100644
index 00000000..7d7c5bc2
--- /dev/null
+++ b/inject-generator/src/test/java/io/avaje/inject/generator/models/valid/provider/FactoryProvider.java
@@ -0,0 +1,23 @@
+package io.avaje.inject.generator.models.valid.provider;
+
+import java.util.function.Supplier;
+
+import io.avaje.inject.Bean;
+import io.avaje.inject.Factory;
+import io.avaje.inject.Secondary;
+import jakarta.inject.Provider;
+
+@Factory
+public class FactoryProvider {
+
+  @Bean
+  @Secondary
+  Provider<Grinder> grinder() {
+    return Grinder::new;
+  }
+
+  @Bean
+  Provider<Supplier<String>> supply() {
+    return () -> () -> "";
+  }
+}
diff --git a/inject-generator/src/test/java/io/avaje/inject/generator/models/valid/provider/Grinder.java b/inject-generator/src/test/java/io/avaje/inject/generator/models/valid/provider/Grinder.java
new file mode 100644
index 00000000..96b3a3dc
--- /dev/null
+++ b/inject-generator/src/test/java/io/avaje/inject/generator/models/valid/provider/Grinder.java
@@ -0,0 +1,3 @@
+package io.avaje.inject.generator.models.valid.provider;
+
+public class Grinder {}
diff --git a/inject-generator/src/test/java/io/avaje/inject/generator/models/valid/provider/ProviderUser.java b/inject-generator/src/test/java/io/avaje/inject/generator/models/valid/provider/ProviderUser.java
new file mode 100644
index 00000000..2c46f571
--- /dev/null
+++ b/inject-generator/src/test/java/io/avaje/inject/generator/models/valid/provider/ProviderUser.java
@@ -0,0 +1,18 @@
+package io.avaje.inject.generator.models.valid.provider;
+
+import java.util.function.Supplier;
+
+import jakarta.inject.Provider;
+import jakarta.inject.Singleton;
+
+@Singleton
+public class ProviderUser {
+
+  Provider<Grinder> grinder;
+  Provider<Supplier<String>> supplier;
+
+  public ProviderUser(Provider<Grinder> grinder, Provider<Supplier<String>> supplier) {
+    this.grinder = grinder;
+    this.supplier = supplier;
+  }
+}
diff --git a/inject-test/src/test/java/org/example/coffee/provider/FactoryProvider.java b/inject-test/src/test/java/org/example/coffee/provider/FactoryProvider.java
new file mode 100644
index 00000000..176d6da1
--- /dev/null
+++ b/inject-test/src/test/java/org/example/coffee/provider/FactoryProvider.java
@@ -0,0 +1,25 @@
+package org.example.coffee.provider;
+
+import java.util.function.Supplier;
+
+import io.avaje.inject.Bean;
+import io.avaje.inject.Factory;
+import io.avaje.inject.Secondary;
+import jakarta.inject.Named;
+import jakarta.inject.Provider;
+
+@Factory
+public class FactoryProvider {
+
+  @Bean
+  @Secondary
+  @Named("second")
+  Provider<String> second() {
+    return () -> "Nah, I'd win";
+  }
+
+  @Bean
+  Provider<Supplier<String>> supply() {
+    return () -> () -> "Stand proud Provider, you were strong";
+  }
+}
diff --git a/inject-test/src/test/java/org/example/coffee/provider/FactoryProviderTest.java b/inject-test/src/test/java/org/example/coffee/provider/FactoryProviderTest.java
new file mode 100644
index 00000000..b1868a14
--- /dev/null
+++ b/inject-test/src/test/java/org/example/coffee/provider/FactoryProviderTest.java
@@ -0,0 +1,22 @@
+package org.example.coffee.provider;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.util.function.Supplier;
+
+import org.junit.jupiter.api.Test;
+
+import io.avaje.inject.BeanScope;
+import io.avaje.inject.spi.GenericType;
+
+class FactoryProviderTest {
+
+  @Test
+  void test() {
+    try (var scope = BeanScope.builder().build()) {
+      Supplier<String> prov = scope.get(new GenericType<Supplier<String>>() {});
+      assertThat(prov.get()).isEqualTo("Stand proud Provider, you were strong");
+      assertThat(scope.get(String.class, "second")).isEqualTo("Nah, I'd win");
+    }
+  }
+}