[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
@@ -29,5 +29,7 @@ import kotlin.reflect.KClass
*
* Such configurations may arise, for example, for multiplatform modules: consider analyzing JVM+JS module, where JS contributes
* default implementation of some particular service, and JVM contributes non-default.
*
* If you need more fine-grained control of clashes resolution, consider using [PlatformExtensionsClashResolver]
**/
annotation class DefaultImplementation(val impl: KClass<*>)
@@ -0,0 +1,52 @@
/*
* Copyright 2010-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.container
/**
* This is a marker-interface for components which are needed for common resolve
* facilities (like resolve, or deserialization), but are platform-specific.
*
* PlatformSpecificExtensions has to be present in the container in exactly one
* instance (hence common pattern with providing no-op DEFAULT/EMPTY implementation
* in the corresponding interface)
*
* In multiplatform modules such components require special treatment. Namely,
* if several components of the same type are provided, then it's not an illegal state;
* rather, we have to carefully resolve clash on case-by-case basis.
* See also [PlatformExtensionsClashResolver].
*/
interface PlatformSpecificExtension<S : PlatformSpecificExtension<S>>
/**
* Allows to specify which [PlatformSpecificExtension] should be used if there were two or more registrations
* for [applicableTo] class in the container.
*
* [PlatformExtensionsClashResolver] should be registred in the container via [useClashResolver]-extension.
*
* NB. YOU DON'T NEED this mechanism for the most popular case of "one or several default vs.
* zero or one non-default". Just use [DefaultImplementation], and default instances will be automatically
* discriminated (see respective KDoc).
* Use [PlatformExtensionsClashResolver] only for cases when you need more invloved logic.
*
* Example: [org.jetbrains.kotlin.resolve.IdentifierChecker]. It is used in platform-agnostic code,
* which resolves and checks identifiers for correctness. Each platform has it's own rules
* regarding identifier correctness. In MPP modules we can't choose only one IdentifierChecker;
* instead, we have to provide a "composite" IdentifierChecker which will launch checks of *each*
* platform.
*
*/
abstract class PlatformExtensionsClashResolver<E : PlatformSpecificExtension<E>>(val applicableTo: Class<E>) {
abstract fun resolveExtensionsClash(extensions: List<E>): E
class FallbackToDefault<E : PlatformSpecificExtension<E>>(
private val defaultValue: E,
applicableTo: Class<E>
) : PlatformExtensionsClashResolver<E>(applicableTo) {
override fun resolveExtensionsClash(extensions: List<E>): E = defaultValue
}
}