Repository: yandex/yatagan (kotlin)

Issue:
Conditional AssistedFactory
Yatagan doesn't allow now to add @Conditional to assisted factory. 
This doesn't allow to use conditional dependencies directly in assisted factory.

Pull request description:
Closes #56

Patch:
diff --git a/core/graph/impl/src/main/kotlin/bindings/AssistedInjectFactoryBindingImpl.kt b/core/graph/impl/src/main/kotlin/bindings/AssistedInjectFactoryBindingImpl.kt
index 20d6021e..3122e70d 100644
--- a/core/graph/impl/src/main/kotlin/bindings/AssistedInjectFactoryBindingImpl.kt
+++ b/core/graph/impl/src/main/kotlin/bindings/AssistedInjectFactoryBindingImpl.kt
@@ -20,6 +20,8 @@ import com.yandex.yatagan.base.memoize
 import com.yandex.yatagan.core.graph.BindingGraph
 import com.yandex.yatagan.core.graph.bindings.AssistedInjectFactoryBinding
 import com.yandex.yatagan.core.graph.bindings.Binding
+import com.yandex.yatagan.core.graph.impl.NonStaticConditionDependencies
+import com.yandex.yatagan.core.graph.impl.VariantMatch
 import com.yandex.yatagan.core.model.AssistedInjectFactoryModel
 import com.yandex.yatagan.core.model.NodeModel
 import com.yandex.yatagan.validation.MayBeInvalid
@@ -32,7 +34,7 @@ import com.yandex.yatagan.validation.format.bindingModelRepresentation
 internal class AssistedInjectFactoryBindingImpl(
     override val owner: BindingGraph,
     override val model: AssistedInjectFactoryModel,
-) : AssistedInjectFactoryBinding, BindingDefaultsMixin, ComparableByTargetBindingMixin {
+) : AssistedInjectFactoryBinding, ConditionalBindingMixin, ComparableByTargetBindingMixin {
     override val target: NodeModel
         get() = model.asNode()
 
@@ -40,6 +42,12 @@ internal class AssistedInjectFactoryBindingImpl(
         return visitor.visitAssistedInjectFactory(this)
     }
 
+    override val variantMatch: VariantMatch by lazy { VariantMatch(model, owner.variant) }
+
+    override val nonStaticConditionDependencies: NonStaticConditionDependencies by lazy {
+        NonStaticConditionDependencies(this@AssistedInjectFactoryBindingImpl)
+    }
+
     override val dependencies by lazy(LazyThreadSafetyMode.PUBLICATION) {
         model.assistedConstructorParameters
             .asSequence()
diff --git a/core/model/api/src/main/kotlin/AssistedInjectFactoryModel.kt b/core/model/api/src/main/kotlin/AssistedInjectFactoryModel.kt
index 9e484efc..ebf12318 100644
--- a/core/model/api/src/main/kotlin/AssistedInjectFactoryModel.kt
+++ b/core/model/api/src/main/kotlin/AssistedInjectFactoryModel.kt
@@ -23,7 +23,7 @@ import com.yandex.yatagan.lang.Type
 /**
  * Represents [com.yandex.yatagan.AssistedFactory].
  */
-public interface AssistedInjectFactoryModel : HasNodeModel {
+public interface AssistedInjectFactoryModel : HasNodeModel, ConditionalHoldingModel {
     /**
      * Factory method that takes assisted parameters and creates the instance via [assistedInjectConstructor],
      * passing "assisted" parameters as is, providing non-assisted (injected) dependencies from a graph.
diff --git a/core/model/impl/src/main/kotlin/AssistedInjectFactoryModelImpl.kt b/core/model/impl/src/main/kotlin/AssistedInjectFactoryModelImpl.kt
index ad58e4c3..ecaedf6a 100644
--- a/core/model/impl/src/main/kotlin/AssistedInjectFactoryModelImpl.kt
+++ b/core/model/impl/src/main/kotlin/AssistedInjectFactoryModelImpl.kt
@@ -19,6 +19,7 @@ package com.yandex.yatagan.core.model.impl
 import com.yandex.yatagan.base.ObjectCache
 import com.yandex.yatagan.core.model.AssistedInjectFactoryModel
 import com.yandex.yatagan.core.model.AssistedInjectFactoryModel.Parameter
+import com.yandex.yatagan.core.model.ConditionalHoldingModel
 import com.yandex.yatagan.core.model.HasNodeModel
 import com.yandex.yatagan.core.model.NodeModel
 import com.yandex.yatagan.lang.BuiltinAnnotation
@@ -39,6 +40,19 @@ internal class AssistedInjectFactoryModelImpl private constructor(
         assert(canRepresent(impl))
     }
 
+    private val assistedInjectClassConditionalModel by lazy {
+        assistedInjectType?.let {
+            ConditionalHoldingModelImpl(it.declaration.getAnnotations(BuiltinAnnotation.Conditional))
+        }
+    }
+
+    private val unexpectedFactoryConditionalModel by lazy {
+        ConditionalHoldingModelImpl(impl.getAnnotations(BuiltinAnnotation.Conditional))
+    }
+
+    override val conditionals: List<ConditionalHoldingModel.ConditionalWithFlavorConstraintsModel>
+        get() = assistedInjectClassConditionalModel?.conditionals ?: emptyList()
+
     override val factoryMethod: Method? by lazy {
         impl.methods.singleOrNull { it.isAbstract }
     }
@@ -88,6 +102,7 @@ internal class AssistedInjectFactoryModelImpl private constructor(
 
     override fun validate(validator: Validator) {
         validator.child(asNode())
+        assistedInjectClassConditionalModel?.let(validator::child)
 
         if (impl.kind != TypeDeclarationKind.Interface) {
             validator.reportError(Strings.Errors.assistedInjectFactoryNotInterface())
@@ -127,6 +142,10 @@ internal class AssistedInjectFactoryModelImpl private constructor(
                 addNote(Strings.Notes.assistedInjectMismatchFromFactory(params = allFactoryAssistedParameters))
             }
         }
+
+        if (unexpectedFactoryConditionalModel.conditionals.isNotEmpty()) {
+            validator.reportError(Strings.Errors.assistedInjectConditionalsOnFactory())
+        }
     }
 
     override fun toString(childContext: MayBeInvalid?) = modelRepresentation(
diff --git a/validation/format/src/main/kotlin/Strings.kt b/validation/format/src/main/kotlin/Strings.kt
index 896c114d..49475b88 100644
--- a/validation/format/src/main/kotlin/Strings.kt
+++ b/validation/format/src/main/kotlin/Strings.kt
@@ -382,6 +382,11 @@ object Strings {
             ("@AssistedInject constructor @Assisted parameters and " +
                     "factory function @Assisted parameters do not match.").toError()
 
+        @Covered
+        fun assistedInjectConditionalsOnFactory() = (
+                "@Conditional annotations should be specified on the class with the @AssistedInject, " +
+                        "not on its factory").toError()
+
         private const val AccessMessage = "public/internal"
 
         @Covered


Test patch:
diff --git a/testing/tests/src/test/kotlin/ConditionsTest.kt b/testing/tests/src/test/kotlin/ConditionsTest.kt
index d0952836..b8d53d7c 100644
--- a/testing/tests/src/test/kotlin/ConditionsTest.kt
+++ b/testing/tests/src/test/kotlin/ConditionsTest.kt
@@ -903,4 +903,60 @@ class ConditionsTest(
 
         compileRunAndValidate()
     }
+
+    @Test
+    fun `assisted-factory under conditions`() {
+        includeFromSourceSet(features)
+
+        givenKotlinSource("test.TestCase", """
+            import com.yandex.yatagan.*
+            import javax.inject.*
+
+            @Conditional(Conditions.FeatureA::class)
+            class MyClassA @Inject constructor()
+
+            @Conditional(Conditions.FeatureA::class) 
+            class MyClassB @AssistedInject constructor(@Assisted val i: Int, a: MyClassA)
+            
+            @AssistedFactory interface MyClassBFactory { fun create(i: Int): MyClassB }
+
+            @Component interface MyComponent {
+                val f: Optional<MyClassBFactory>
+            }
+
+            fun test() {
+                Features.enabledA = true
+                Yatagan.create(MyComponent::class.java).f.get().create(1)
+                Features.enabledA = false
+                assert(Yatagan.create(MyComponent::class.java).f.isPresent == false)
+            }
+        """.trimIndent())
+
+        compileRunAndValidate()
+    }
+
+    @Test
+    fun `assisted-factory under invalid conditions`() {
+        includeFromSourceSet(features)
+
+        givenKotlinSource("test.TestCase", """
+            import com.yandex.yatagan.*
+            import javax.inject.*
+
+            @Conditional(Conditions.FeatureB::class)
+            class MyClassA @Inject constructor()
+
+            @Conditional(Conditions.FeatureA::class) 
+            class MyClassB @AssistedInject constructor(@Assisted val i: Int, a: MyClassA)
+            
+            @Conditional(Conditions.FeatureB::class)  // illegal conditionals placement
+            @AssistedFactory interface MyClassBFactory { fun create(i: Int): MyClassB }
+
+            @Component interface MyComponent {
+                val f: Provider<MyClassBFactory>
+            }
+        """.trimIndent())
+
+        compileRunAndValidate()
+    }
 }
\ No newline at end of file
diff --git a/testing/tests/src/test/resources/golden/ConditionsTest/assisted-factory-under-invalid-conditions.golden.txt b/testing/tests/src/test/resources/golden/ConditionsTest/assisted-factory-under-invalid-conditions.golden.txt
new file mode 100644
index 00000000..77885224
--- /dev/null
+++ b/testing/tests/src/test/resources/golden/ConditionsTest/assisted-factory-under-invalid-conditions.golden.txt
@@ -0,0 +1,32 @@
+error: @Conditional annotations should be specified on the class with the @AssistedInject, not on its factory
+Encountered:
+  in graph for root-component test.MyComponent
+  in entry-point getF: [Provider] test.MyClassBFactory
+                       ^-[*1]-------------------------
+  here: [1*] assisted-factory test.MyClassBFactory::create(): test.MyClassB(.. 1 dependency)
+             ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+error: Invalid dependency of `test.MyClassBFactory` on `test.MyClassA`:
+`inject-constructor test.MyClassA()` with a condition:
+    (1) [test.Features.isEnabledB]
+cannot be injected into `assisted-factory test.MyClassBFactory::create(): test.MyClassB(.. 1 dependency)` with a condition:
+    (2) [test.Features.fooBar]
+without Optional<..> wrapper, because condition (2) does not imply condition (1)
+Encountered:
+  in graph for root-component test.MyComponent
+  in entry-point getF: [Provider] test.MyClassBFactory
+                       ^-[*1]-------------------------
+  here: [1*] assisted-factory test.MyClassBFactory::create(): test.MyClassB(.. 1 dependency)
+             ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+error: `assisted-factory test.MyClassBFactory::create(): test.MyClassB(.. 1 dependency)` with a condition:
+    (1) [test.Features.fooBar]
+cannot be directly exposed from an entry-point of `graph for root-component test.MyComponent` under condition:
+    (2) [always-present]
+without Optional<..> wrapper, because component condition (2) does not imply condition (1)
+Encountered:
+  in graph for root-component test.MyComponent
+  here: entry-point getF: [Provider] test.MyClassBFactory
+        ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
\ No newline at end of file