[Injection] Introduce PlatformSpecificExtensions and PlatformExtensionsClashResolver

This commit introduces the ability to register a PlatformExtensionClashResolver
in a container. Each PlatformExtensionClashResolver has a corresponding
PlatformSpecificExtensions.

If, during container composition, several instances of
PlatformSpecificExtensions were registred, instead of throwing
InvalidCardinalityException, corresponding PlatformExtensionClashResolver
will be asked to resolve clash.

This allows to make injection more composable and less coupled across
different contributors of service, providing a basis for such motivating
cases as composing containers with both JS and JVM services (for analysis
of multiplatform modules).
Previously, that would be impossible:
a) JS would inject default instances for some services which would clash
with non-default JVM services (like SyntheticScopes)
b) Also, there are a very few services for which *both* platforms provide
non-default implementations, so they should be merged manually on
case-by-case basis (e.g., IdentifierChecker)
This commit is contained in:
Dmitry Savvinov
2019-03-13 17:29:50 +03:00
parent b631e89ea7
commit 72e28d37fc
16 changed files with 209 additions and 26 deletions
@@ -102,6 +102,11 @@ class StorageComponentContainer(
return this
}
internal fun registerClashResolvers(resolvers: List<PlatformExtensionsClashResolver<*>>): StorageComponentContainer {
componentStorage.registerClashResolvers(resolvers)
return this
}
override fun <T> create(request: Class<T>): T {
val constructorBinding = request.bindToConstructor(unknownContext)
val args = constructorBinding.argumentDescriptors.map { it.getValue() }.toTypedArray()
@@ -50,6 +50,10 @@ fun StorageComponentContainer.useInstanceIfNotNull(instance: Any?) {
if (instance != null) registerInstance(instance)
}
fun StorageComponentContainer.useClashResolver(clashResolver: PlatformExtensionsClashResolver<*>) {
registerClashResolvers(listOf(clashResolver))
}
inline operator fun <reified T : Any> ComponentProvider.getValue(thisRef: Any?, desc: KProperty<*>): T {
return getService(T::class.java)
}
@@ -70,4 +70,20 @@ internal class ComponentRegistry {
}
registrationMap += other.registrationMap
}
fun resolveClashesIfAny(container: ComponentContainer, clashResolvers: List<PlatformExtensionsClashResolver<*>>) {
/*
The idea is to create descriptor, which is very similar to other SingletonDescriptor, but instead of calling
constructor we call 'resolveExtensionsClash' with values of clashed components as arguments.
By mimicking the usual descriptors we get lazy evaluation and consistency checks for free.
*/
for (resolver in clashResolvers) {
val clashedComponents = registrationMap[resolver.applicableTo] as? Collection<ComponentDescriptor> ?: continue
if (clashedComponents.size <= 1) continue
val substituteDescriptor = ClashResolutionDescriptor(container, resolver, clashedComponents.toList())
registrationMap[resolver.applicableTo] = substituteDescriptor
}
}
}
@@ -17,6 +17,7 @@
package org.jetbrains.kotlin.container
import java.io.Closeable
import java.lang.IllegalStateException
import java.lang.reflect.Type
import java.util.*
@@ -144,6 +145,29 @@ open class SingletonTypeComponentDescriptor(container: ComponentContainer, val k
override fun toString(): String = "Singleton: ${klass.simpleName}"
}
internal class ClashResolutionDescriptor<E : PlatformSpecificExtension<E>>(
container: ComponentContainer,
private val resolver: PlatformExtensionsClashResolver<E>,
private val clashedComponents: List<ComponentDescriptor>
) : SingletonDescriptor(container) {
override fun createInstance(context: ValueResolveContext): Any {
state = ComponentState.Initializing
val extensions = computeArguments(clashedComponents) as List<E>
val resolution = resolver.resolveExtensionsClash(extensions)
state = ComponentState.Initialized
return resolution
}
override fun getRegistrations(): Iterable<Type> {
throw IllegalStateException("Shouldn't be called")
}
override fun getDependencies(context: ValueResolveContext): Collection<Type> {
throw IllegalStateException("Shouldn't be called")
}
}
class ImplicitSingletonTypeComponentDescriptor(container: ComponentContainer, klass: Class<*>) : SingletonTypeComponentDescriptor(container, klass) {
override fun toString(): String {
return "Implicit: ${klass.simpleName}"
@@ -19,6 +19,7 @@ package org.jetbrains.kotlin.container
import com.intellij.util.containers.MultiMap
import java.io.Closeable
import java.io.PrintStream
import java.lang.IllegalStateException
import java.lang.reflect.Modifier
import java.lang.reflect.ParameterizedType
import java.lang.reflect.Type
@@ -37,13 +38,19 @@ internal class InvalidCardinalityException(message: String) : Exception(message)
class ComponentStorage(private val myId: String, parent: ComponentStorage?) : ValueResolver {
var state = ComponentStorageState.Initial
private val registry = ComponentRegistry()
init {
parent?.let { registry.addAll(it.registry) }
}
private val descriptors = LinkedHashSet<ComponentDescriptor>()
private val dependencies = MultiMap.createLinkedSet<ComponentDescriptor, Type>()
private val clashResolvers = ArrayList<PlatformExtensionsClashResolver<*>>()
private val registry = ComponentRegistry()
init {
parent?.let {
registry.addAll(it.registry)
clashResolvers.addAll(it.clashResolvers)
}
}
override fun resolve(request: Type, context: ValueResolveContext): ValueDescriptor? {
fun ComponentDescriptor.isDefaultComponent(): Boolean =
@@ -107,6 +114,10 @@ class ComponentStorage(private val myId: String, parent: ComponentStorage?) : Va
return registry.tryGetEntry(request)
}
internal fun registerClashResolvers(resolvers: List<PlatformExtensionsClashResolver<*>>) {
clashResolvers.addAll(resolvers)
}
internal fun registerDescriptors(context: ComponentResolveContext, items: List<ComponentDescriptor>) {
if (state == ComponentStorageState.Disposed) {
throw ContainerConsistencyException("Cannot register descriptors in $state state")
@@ -135,6 +146,7 @@ class ComponentStorage(private val myId: String, parent: ComponentStorage?) : Va
val implicits = inspectDependenciesAndRegisterAdhoc(context, descriptors)
registry.resolveClashesIfAny(context.container, clashResolvers)
injectProperties(context, descriptors + implicits)
}