From c89f2c118710db1d0809df22fc0d9e9fc3c71271 Mon Sep 17 00:00:00 2001 From: Andrei Shikov Date: Sat, 31 Aug 2019 23:47:27 +0100 Subject: [PATCH 01/12] Move proposal to md file --- PROPOSAL.md | 73 ++++++++++++++++++ build.gradle | 2 +- src/main/kotlin/Client.kt | 21 +++++ src/main/kotlin/Main.kt | 135 --------------------------------- src/main/kotlin/lib/Library.kt | 13 ++++ 5 files changed, 108 insertions(+), 136 deletions(-) create mode 100644 PROPOSAL.md create mode 100644 src/main/kotlin/Client.kt delete mode 100644 src/main/kotlin/Main.kt create mode 100644 src/main/kotlin/lib/Library.kt diff --git a/PROPOSAL.md b/PROPOSAL.md new file mode 100644 index 0000000..c979c24 --- /dev/null +++ b/PROPOSAL.md @@ -0,0 +1,73 @@ +The main topic of this note is to explore robust DI compile-time "safe" container implementation relying on language itself, not annotation processing. + +I see several main points of implementation for MVP: +1. Exposing dependencies from container +2. Wiring(binding) dependency graph +3. Injection (constructor and instance fields) +4. Scoping (local singletons) +5. Subscoping and providing parts of the parent graph to children. + +## Exposing dependencies: +The container providing dependencies can be called a `Component` in analogy with dagger. +To associate each graph with correct instance calls, we can use a named component interface. To distinguish those (and to go away from annotations `TODO: why?`), marker `interface Component` can be used. + +Kotlin has extension functions which can be leveraged here, providing a single entry point for every call. +```kotlin +// Base function defined in library +fun Component.get(): T = TODO("Should be generated") + +// Definition of component in the client code +interface FooComponent : Component + +// Generated functions for each endpoint +fun FooComponent.getBar(): Bar = ... +``` +Base function will be substituted for generated one at compile time. + +Component definition can be done using Kotlin DSL and compiler transformations. `TODO: provide more details` +```kotlin +// Library +fun component(vararg dependencies: Any?): T = TODO() + +// Client +fun init() { + val fooComponent = component( + bind(barInstance), + bind(::bar1Provider) + ) + + val instance: Bar = fooComponent.get() +} +``` + +It should be possible to generate the endpoints dynamically based on the usage in code. There is a problem of implementation details implicitly escaping interface of DI container, so it is questionable direction to proceed. + +## Wiring + +`TODO: module definition` +`TODO: check macwire for examples` + +## Injection + +`TODO: more details` + +Field injection: +```kotlin +// Library +interface Injectable { + fun inject(): ReadOnlyProperty, R> = // delegate +} + +// Client +class SomeClass : Injectable { + val someField: Foo by inject() // Can we make it compile time safe? +} +``` + +## Scoping + +`TODO: local scoping and their relation` + +## Subscoping + +`TODO: parent - children relationship` diff --git a/build.gradle b/build.gradle index 0327c7b..5b7704c 100644 --- a/build.gradle +++ b/build.gradle @@ -13,7 +13,7 @@ plugins { // id "org.jetbrains.kotlin.kapt" version "1.3.50" } -apply plugin: 'di-compiler-plugin' +//apply plugin: 'di-compiler-plugin' group 'me.shika.test' version '1.0-SNAPSHOT' diff --git a/src/main/kotlin/Client.kt b/src/main/kotlin/Client.kt new file mode 100644 index 0000000..6e173dc --- /dev/null +++ b/src/main/kotlin/Client.kt @@ -0,0 +1,21 @@ +import lib.Component +import lib.bind +import lib.component +import lib.get + +interface Main : Component + +// Generated +fun Main.getInt(): Int = 0 + +fun main() { + val intInstance = 0 + + val componentImpl = component
( + bind(intInstance), + bind { it: Int -> it.toString() } + ) + + println(componentImpl.get()) + println(componentImpl.get()) +} diff --git a/src/main/kotlin/Main.kt b/src/main/kotlin/Main.kt deleted file mode 100644 index 5e85b69..0000000 --- a/src/main/kotlin/Main.kt +++ /dev/null @@ -1,135 +0,0 @@ -import dagger.Component -import dagger.Module -import dagger.Provides -import java.io.File -import javax.inject.Inject -import javax.inject.Scope - -@TestScope -@Component( - modules = [TestModuleInstance::class, TestModule::class, AbstractModule::class], - dependencies = [Dependency::class] -) -interface Main : Common { - @Component.Factory - interface Factory { - fun build( - dependency: Dependency, - testModuleInstance: TestModuleInstance -// @BindsInstance test12: List - ): Main - } - - fun scopedInjected(): ScopedInjected - fun inject(instance: Injected2) - fun test2(): Long - fun file(): File -// fun test3(): List -} - -interface Common { - fun provider(): String - fun providerLambda(): () -> String - fun providerLambdaInt(): () -> Int - fun injected(): Injected -} - -interface Dependency { - fun file(): File -} - -@Module -object TestModule { - @JvmStatic - @Provides - fun string(int: Int): String = int.toString() - - @TestScope - @JvmStatic - @Provides - fun lambdaString(int: Int, string: String): () -> String = { int.toString() + string }.also { println("Invoked lambdaString") } - - @JvmStatic - @Provides - fun lambdaInt(int: Int): () -> Int = { int } -} - -class Injected @Inject constructor(val lambda: () -> String) { - init { - println("Created injected") - } -} - -@TestScope -class ScopedInjected @Inject constructor(val lambda: () -> String) { - init { - println("Created scoped injected") - } -} - -class Injected2 { - @Inject - lateinit var lambdaString: () -> String - @set:Inject - var test1: Int? = null - @Inject - lateinit var lambdaInt: () -> Int - @Inject - fun setString(string: String, int: Int) { - println("Have set $string $int") - } -} - -@Module -class TestModuleInstance(val int: Int) { - @TestScope - @Provides - fun integer(): Int = int.also { println("Invoked int") } - -// @Provides -// fun nullableInteger(): Int? = null -} - -@Scope -annotation class TestScope - -@Module -abstract class AbstractModule { - - @Module - companion object { - @TestScope - @JvmStatic - @Provides - fun test12(): Long = 666L - } -} - -@Module -class ZeroParameterModuleInstance() { - @Provides - fun longLambda(value: Long): () -> Long = { value } -} - -fun main(args: Array) { -// val component = DaggerMain.factory().build(TestModuleInstance(3), listOf()) - val component = DaggerMain( - TestModuleInstance(3), - object : Dependency { - override fun file(): File = File("Hello") - } - ) - println(component.injected().lambda()) - println(component.injected().lambda()) - println(component.scopedInjected().lambda()) - println(component.scopedInjected().lambda()) - - println(component.providerLambda()()) - - val injected2 = Injected2() - component.inject(injected2) - println(injected2.lambdaString()) - - println(component.test2()) - println(component.file()) -} diff --git a/src/main/kotlin/lib/Library.kt b/src/main/kotlin/lib/Library.kt new file mode 100644 index 0000000..a6b8a5c --- /dev/null +++ b/src/main/kotlin/lib/Library.kt @@ -0,0 +1,13 @@ +package lib + +interface Component + +interface Binding + +fun Component.get(): T = TODO("Should be generated") + +fun component(vararg dependencies: Binding<*>): T = TODO("Should be generated") + +fun bind(instance: T): Binding = TODO("Should be generated") + +fun bind(instanceProvider: (A) -> B): Binding = TODO("Should be generated") From 25884d7cad3c3415af575779eefeb2e7c7d4946a Mon Sep 17 00:00:00 2001 From: Andrei Shikov Date: Sun, 1 Sep 2019 01:46:27 +0100 Subject: [PATCH 02/12] Better wording; add interface for module def --- PROPOSAL.md | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/PROPOSAL.md b/PROPOSAL.md index c979c24..601f29c 100644 --- a/PROPOSAL.md +++ b/PROPOSAL.md @@ -1,6 +1,6 @@ -The main topic of this note is to explore robust DI compile-time "safe" container implementation relying on language itself, not annotation processing. +This note explores compile-time "safe" DI container implementation relying on language itself, as opposed to annotation processing. -I see several main points of implementation for MVP: +The topics to explore for MVP: 1. Exposing dependencies from container 2. Wiring(binding) dependency graph 3. Injection (constructor and instance fields) @@ -8,8 +8,9 @@ I see several main points of implementation for MVP: 5. Subscoping and providing parts of the parent graph to children. ## Exposing dependencies: -The container providing dependencies can be called a `Component` in analogy with dagger. -To associate each graph with correct instance calls, we can use a named component interface. To distinguish those (and to go away from annotations `TODO: why?`), marker `interface Component` can be used. +The container providing dependencies called a `Component` in analogy with dagger. +To associate each graph with correct instance calls, we can use a named component interface. +To distinguish those (and to go away from annotations `TODO: why?`), marker `interface Component` can be used. Kotlin has extension functions which can be leveraged here, providing a single entry point for every call. ```kotlin @@ -27,20 +28,25 @@ Base function will be substituted for generated one at compile time. Component definition can be done using Kotlin DSL and compiler transformations. `TODO: provide more details` ```kotlin // Library -fun component(vararg dependencies: Any?): T = TODO() +fun component(vararg dependencies: Binding<*>): T = TODO() +fun module(definition: () -> Array>): Array // Client fun init() { val fooComponent = component( bind(barInstance), - bind(::bar1Provider) + bind(::bar1Provider), + *module(::fooBarModule) ) val instance: Bar = fooComponent.get() } ``` -It should be possible to generate the endpoints dynamically based on the usage in code. There is a problem of implementation details implicitly escaping interface of DI container, so it is questionable direction to proceed. +It should be possible to generate the endpoints dynamically based on the usage in code. +Problems to address: + - Runtime branching in parameters + - implementation details implicitly escaping interface of DI container. ## Wiring From 024de72ade27ac776bd66feb8bf197a617a1fdd3 Mon Sep 17 00:00:00 2001 From: Andrei Shikov Date: Sun, 1 Sep 2019 04:20:42 +0100 Subject: [PATCH 03/12] A little bit more experiment --- src/main/kotlin/Client.kt | 20 +++++++++++++------- src/main/kotlin/lib/Library.kt | 14 ++++++++------ 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/src/main/kotlin/Client.kt b/src/main/kotlin/Client.kt index 6e173dc..6845b94 100644 --- a/src/main/kotlin/Client.kt +++ b/src/main/kotlin/Client.kt @@ -1,21 +1,27 @@ -import lib.Component -import lib.bind -import lib.component -import lib.get +import lib.* interface Main : Component -// Generated -fun Main.getInt(): Int = 0 +interface Dependency { + fun longInstance(): Long +} fun main() { val intInstance = 0 val componentImpl = component
( bind(intInstance), - bind { it: Int -> it.toString() } + module( + bind { it: Int -> it.toString() } + ), + dependency( + object : Dependency { + override fun longInstance(): Long = 1L + } + ) ) println(componentImpl.get()) println(componentImpl.get()) + println(componentImpl.get()) } diff --git a/src/main/kotlin/lib/Library.kt b/src/main/kotlin/lib/Library.kt index a6b8a5c..d186229 100644 --- a/src/main/kotlin/lib/Library.kt +++ b/src/main/kotlin/lib/Library.kt @@ -2,12 +2,14 @@ package lib interface Component -interface Binding +interface Binding +interface Container : Binding +interface Instance : Binding fun Component.get(): T = TODO("Should be generated") +fun component(vararg dependencies: Binding): T = TODO("Should be generated") -fun component(vararg dependencies: Binding<*>): T = TODO("Should be generated") - -fun bind(instance: T): Binding = TODO("Should be generated") - -fun bind(instanceProvider: (A) -> B): Binding = TODO("Should be generated") +fun module(vararg dependencies: Binding): Container = TODO() +fun dependency(value: T): Container = TODO() +fun bind(instance: T): Instance = TODO() +fun bind(instanceProvider: (A) -> B): Instance = TODO() From c33896611fcf11ca60a7f035b57de8e3f55ef501 Mon Sep 17 00:00:00 2001 From: Andrei Shikov Date: Tue, 3 Sep 2019 01:44:04 +0100 Subject: [PATCH 04/12] Test call resolution --- build.gradle | 2 +- .../{test => di}/DiCommandLineProcessor.kt | 2 +- .../shika/di/DiCompilerAnalysisExtension.kt | 132 ++++++++++++++++++ .../DiCompilerComponentRegistrar.kt | 28 ++-- .../shika/di/DiCompilerStorageContributor.kt | 28 ++++ .../dagger/DaggerBindingDescriptor.kt | 10 +- .../dagger/DaggerBindingResolver.kt | 16 +-- .../dagger/DaggerComponentDescriptor.kt | 4 +- .../dagger/DaggerComponentRenderer.kt | 19 ++- .../dagger/DaggerFactoryRenderer.kt | 18 ++- .../dagger/DaggerMembersInjectorRenderer.kt | 17 ++- .../me/shika/{test => di}/dagger/utils.kt | 11 +- .../main/java/me/shika/{test => di}/logger.kt | 2 +- .../me/shika/{test => di}/model/resolution.kt | 2 +- .../{test => di}/resolver/ResolverContext.kt | 2 +- .../me/shika/{test => di}/resolver/utils.kt | 2 +- .../shika/test/DiCompilerAnalysisExtension.kt | 92 ------------ src/main/kotlin/Client.kt | 22 ++- 18 files changed, 251 insertions(+), 158 deletions(-) rename buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/{test => di}/DiCommandLineProcessor.kt (98%) create mode 100644 buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/DiCompilerAnalysisExtension.kt rename buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/{test => di}/DiCompilerComponentRegistrar.kt (63%) create mode 100644 buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/DiCompilerStorageContributor.kt rename buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/{test => di}/dagger/DaggerBindingDescriptor.kt (96%) rename buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/{test => di}/dagger/DaggerBindingResolver.kt (92%) rename buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/{test => di}/dagger/DaggerComponentDescriptor.kt (95%) rename buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/{test => di}/dagger/DaggerComponentRenderer.kt (89%) rename buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/{test => di}/dagger/DaggerFactoryRenderer.kt (91%) rename buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/{test => di}/dagger/DaggerMembersInjectorRenderer.kt (86%) rename buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/{test => di}/dagger/utils.kt (84%) rename buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/{test => di}/logger.kt (97%) rename buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/{test => di}/model/resolution.kt (98%) rename buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/{test => di}/resolver/ResolverContext.kt (90%) rename buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/{test => di}/resolver/utils.kt (86%) delete mode 100644 buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/test/DiCompilerAnalysisExtension.kt diff --git a/build.gradle b/build.gradle index 5b7704c..0327c7b 100644 --- a/build.gradle +++ b/build.gradle @@ -13,7 +13,7 @@ plugins { // id "org.jetbrains.kotlin.kapt" version "1.3.50" } -//apply plugin: 'di-compiler-plugin' +apply plugin: 'di-compiler-plugin' group 'me.shika.test' version '1.0-SNAPSHOT' diff --git a/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/test/DiCommandLineProcessor.kt b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/DiCommandLineProcessor.kt similarity index 98% rename from buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/test/DiCommandLineProcessor.kt rename to buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/DiCommandLineProcessor.kt index 2acd3d0..997e762 100644 --- a/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/test/DiCommandLineProcessor.kt +++ b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/DiCommandLineProcessor.kt @@ -1,4 +1,4 @@ -package me.shika.test +package me.shika.di import com.google.auto.service.AutoService import org.jetbrains.kotlin.compiler.plugin.AbstractCliOption diff --git a/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/DiCompilerAnalysisExtension.kt b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/DiCompilerAnalysisExtension.kt new file mode 100644 index 0000000..71627fd --- /dev/null +++ b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/DiCompilerAnalysisExtension.kt @@ -0,0 +1,132 @@ +package me.shika.di + +import me.shika.di.resolver.ResolverContext +import me.shika.di.resolver.classDescriptor +import org.jetbrains.kotlin.analyzer.AnalysisResult +import org.jetbrains.kotlin.cli.common.messages.MessageCollector +import org.jetbrains.kotlin.com.intellij.openapi.project.Project +import org.jetbrains.kotlin.com.intellij.psi.PsiElement +import org.jetbrains.kotlin.container.ComponentProvider +import org.jetbrains.kotlin.container.get +import org.jetbrains.kotlin.context.ProjectContext +import org.jetbrains.kotlin.descriptors.FunctionDescriptor +import org.jetbrains.kotlin.descriptors.ModuleDescriptor +import org.jetbrains.kotlin.name.FqName +import org.jetbrains.kotlin.psi.KtCallExpression +import org.jetbrains.kotlin.psi.KtConstructor +import org.jetbrains.kotlin.psi.KtDeclarationWithBody +import org.jetbrains.kotlin.psi.KtExpression +import org.jetbrains.kotlin.psi.KtFile +import org.jetbrains.kotlin.psi.KtNamedFunction +import org.jetbrains.kotlin.psi.KtTreeVisitorVoid +import org.jetbrains.kotlin.resolve.BindingTrace +import org.jetbrains.kotlin.resolve.BodyResolver +import org.jetbrains.kotlin.resolve.calls.callUtil.getResolvedCall +import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameSafe +import org.jetbrains.kotlin.resolve.descriptorUtil.getSuperInterfaces +import org.jetbrains.kotlin.resolve.jvm.extensions.AnalysisHandlerExtension +import org.jetbrains.kotlin.resolve.lazy.ResolveSession +import java.io.File + +class DiCompilerAnalysisExtension( + private val sourcesDir: File, + private val reporter: MessageCollector +) : AnalysisHandlerExtension { + private var generatedFiles = false // fixme one more hack + + override fun doAnalysis( + project: Project, + module: ModuleDescriptor, + projectContext: ProjectContext, + files: Collection, + bindingTrace: BindingTrace, + componentProvider: ComponentProvider + ): AnalysisResult? { + if (generatedFiles) return null + + val addedFiles = mutableListOf() + val resolveSession = componentProvider.get() + val bodyResolver = componentProvider.get() + val resolverContext = ResolverContext(module, bindingTrace, resolveSession) + + files.forEach { file -> + file.accept( + recursiveExpressionVisitor { + when (it) { + is KtCallExpression -> { + var declaration: PsiElement? = it + while ((declaration !is KtNamedFunction && declaration !is KtConstructor<*>) && declaration != null) { + declaration = declaration.parent + } + if (declaration == null) return@recursiveExpressionVisitor false + declaration as KtDeclarationWithBody + + val descriptor = resolveSession.resolveToDescriptor(declaration) + if (descriptor is FunctionDescriptor) { + bodyResolver.resolveFunctionBody( + resolveSession.declarationScopeProvider.getOuterDataFlowInfoForDeclaration(declaration), + bindingTrace, + declaration, + descriptor, + resolveSession.declarationScopeProvider.getResolutionScopeForDeclaration(declaration) + ) + } + val call = it.getResolvedCall(bindingTrace.bindingContext) + + // TODO: Check for correct descriptor + + val callClass = call + ?.extensionReceiver + ?.type + ?.classDescriptor() + + val isComponent = callClass?.getSuperInterfaces() + ?.any { it.fqNameSafe == FqName("lib.Component") } == true + + if (!isComponent) return@recursiveExpressionVisitor false + + val type = call!!.typeArguments[call.candidateDescriptor.typeParameters.first()] + + println("Found exposed type $type") + true + } + else -> false + } + } + ) + } + + if (addedFiles.isEmpty()) return null + + generatedFiles = true + return if (bindingTrace.bindingContext.diagnostics.isEmpty()) { + AnalysisResult.RetryWithAdditionalRoots( + bindingContext = bindingTrace.bindingContext, + moduleDescriptor = module, + additionalJavaRoots = emptyList(), + additionalKotlinRoots = addedFiles + ) // Repeat with my files pls + } else { + AnalysisResult.compilationError(bindingTrace.bindingContext) + } + } + + override fun analysisCompleted( + project: Project, + module: ModuleDescriptor, + bindingTrace: BindingTrace, + files: Collection + ): AnalysisResult? { + + return null + } +} + +private fun recursiveExpressionVisitor(block: (KtExpression) -> Boolean) = + object : KtTreeVisitorVoid() { + override fun visitExpression(expression: KtExpression) { + if (!block(expression)) { + super.visitExpression(expression) + } + } + } diff --git a/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/test/DiCompilerComponentRegistrar.kt b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/DiCompilerComponentRegistrar.kt similarity index 63% rename from buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/test/DiCompilerComponentRegistrar.kt rename to buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/DiCompilerComponentRegistrar.kt index 475d15e..c42f9fe 100644 --- a/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/test/DiCompilerComponentRegistrar.kt +++ b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/DiCompilerComponentRegistrar.kt @@ -1,15 +1,14 @@ -package me.shika.test +package me.shika.di import com.google.auto.service.AutoService -import me.shika.test.DiCommandLineProcessor.Companion.KEY_ENABLED -import me.shika.test.DiCommandLineProcessor.Companion.KEY_SOURCES +import me.shika.di.DiCommandLineProcessor.Companion.KEY_ENABLED +import me.shika.di.DiCommandLineProcessor.Companion.KEY_SOURCES import org.jetbrains.kotlin.cli.common.CLIConfigurationKeys import org.jetbrains.kotlin.cli.common.messages.MessageCollector import org.jetbrains.kotlin.com.intellij.mock.MockProject import org.jetbrains.kotlin.compiler.plugin.ComponentRegistrar import org.jetbrains.kotlin.config.CompilerConfiguration -import org.jetbrains.kotlin.config.JVMConfigurationKeys.IR -import org.jetbrains.kotlin.extensions.CompilerConfigurationExtension +import org.jetbrains.kotlin.extensions.StorageComponentContainerContributor import org.jetbrains.kotlin.resolve.jvm.extensions.AnalysisHandlerExtension @AutoService(ComponentRegistrar::class) @@ -24,18 +23,23 @@ class DiCompilerComponentRegistrar: ComponentRegistrar { sourcesDir.deleteRecursively() sourcesDir.mkdirs() - CompilerConfigurationExtension.registerExtension( - project, - object : CompilerConfigurationExtension { - override fun updateConfiguration(configuration: CompilerConfiguration) { - configuration.put(IR, false) - } - }) +// CompilerConfigurationExtension.registerExtension( +// project, +// object : CompilerConfigurationExtension { +// override fun updateConfiguration(configuration: CompilerConfiguration) { +// configuration.put(IR, false) +// } +// }) AnalysisHandlerExtension.registerExtension( project, DiCompilerAnalysisExtension(sourcesDir = sourcesDir, reporter = reporter) ) + + StorageComponentContainerContributor.registerExtension( + project, + DiCompilerStorageContributor() + ) } } diff --git a/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/DiCompilerStorageContributor.kt b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/DiCompilerStorageContributor.kt new file mode 100644 index 0000000..91a7ac4 --- /dev/null +++ b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/DiCompilerStorageContributor.kt @@ -0,0 +1,28 @@ +package me.shika.di + +import org.jetbrains.kotlin.com.intellij.psi.PsiElement +import org.jetbrains.kotlin.container.StorageComponentContainer +import org.jetbrains.kotlin.container.useInstance +import org.jetbrains.kotlin.descriptors.ModuleDescriptor +import org.jetbrains.kotlin.extensions.StorageComponentContainerContributor +import org.jetbrains.kotlin.platform.TargetPlatform +import org.jetbrains.kotlin.resolve.calls.checkers.CallChecker +import org.jetbrains.kotlin.resolve.calls.checkers.CallCheckerContext +import org.jetbrains.kotlin.resolve.calls.model.ResolvedCall + +class DiCompilerStorageContributor : StorageComponentContainerContributor { + override fun registerModuleComponents( + container: StorageComponentContainer, + platform: TargetPlatform, + moduleDescriptor: ModuleDescriptor + ) { + container.useInstance(DiCompilerCallChecker()) + } +} + +class DiCompilerCallChecker : CallChecker { + override fun check(resolvedCall: ResolvedCall<*>, reportOn: PsiElement, context: CallCheckerContext) { + println(resolvedCall.candidateDescriptor) + } + +} diff --git a/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/test/dagger/DaggerBindingDescriptor.kt b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/dagger/DaggerBindingDescriptor.kt similarity index 96% rename from buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/test/dagger/DaggerBindingDescriptor.kt rename to buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/dagger/DaggerBindingDescriptor.kt index 4bae451..68f08a7 100644 --- a/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/test/dagger/DaggerBindingDescriptor.kt +++ b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/dagger/DaggerBindingDescriptor.kt @@ -1,9 +1,9 @@ -package me.shika.test.dagger +package me.shika.di.dagger -import me.shika.test.model.Binding -import me.shika.test.model.Endpoint -import me.shika.test.model.Injectable -import me.shika.test.resolver.classDescriptor +import me.shika.di.model.Binding +import me.shika.di.model.Endpoint +import me.shika.di.model.Injectable +import me.shika.di.resolver.classDescriptor import org.jetbrains.kotlin.builtins.KotlinBuiltIns import org.jetbrains.kotlin.contracts.parsing.isEqualsDescriptor import org.jetbrains.kotlin.descriptors.ClassDescriptor diff --git a/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/test/dagger/DaggerBindingResolver.kt b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/dagger/DaggerBindingResolver.kt similarity index 92% rename from buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/test/dagger/DaggerBindingResolver.kt rename to buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/dagger/DaggerBindingResolver.kt index 2ad1a36..f45db32 100644 --- a/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/test/dagger/DaggerBindingResolver.kt +++ b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/dagger/DaggerBindingResolver.kt @@ -1,12 +1,12 @@ -package me.shika.test.dagger +package me.shika.di.dagger -import me.shika.test.AMBIGUOUS_BINDING -import me.shika.test.DaggerErrorMessages -import me.shika.test.model.Binding -import me.shika.test.model.Endpoint -import me.shika.test.model.GraphNode -import me.shika.test.model.ResolveResult -import me.shika.test.resolver.classDescriptor +import me.shika.di.AMBIGUOUS_BINDING +import me.shika.di.DaggerErrorMessages +import me.shika.di.model.Binding +import me.shika.di.model.Endpoint +import me.shika.di.model.GraphNode +import me.shika.di.model.ResolveResult +import me.shika.di.resolver.classDescriptor import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity import org.jetbrains.kotlin.cli.common.messages.MessageCollector import org.jetbrains.kotlin.descriptors.DeclarationDescriptor diff --git a/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/test/dagger/DaggerComponentDescriptor.kt b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/dagger/DaggerComponentDescriptor.kt similarity index 95% rename from buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/test/dagger/DaggerComponentDescriptor.kt rename to buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/dagger/DaggerComponentDescriptor.kt index 68365e1..07e822b 100644 --- a/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/test/dagger/DaggerComponentDescriptor.kt +++ b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/dagger/DaggerComponentDescriptor.kt @@ -1,6 +1,6 @@ -package me.shika.test.dagger +package me.shika.di.dagger -import me.shika.test.resolver.ResolverContext +import me.shika.di.resolver.ResolverContext import org.jetbrains.kotlin.descriptors.ClassDescriptor import org.jetbrains.kotlin.descriptors.Modality import org.jetbrains.kotlin.descriptors.annotations.AnnotationDescriptor diff --git a/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/test/dagger/DaggerComponentRenderer.kt b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/dagger/DaggerComponentRenderer.kt similarity index 89% rename from buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/test/dagger/DaggerComponentRenderer.kt rename to buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/dagger/DaggerComponentRenderer.kt index 49aff2b..faec739 100644 --- a/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/test/dagger/DaggerComponentRenderer.kt +++ b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/dagger/DaggerComponentRenderer.kt @@ -1,11 +1,17 @@ -package me.shika.test.dagger +package me.shika.di.dagger -import com.squareup.kotlinpoet.* +import com.squareup.kotlinpoet.ClassName +import com.squareup.kotlinpoet.FileSpec +import com.squareup.kotlinpoet.FunSpec import com.squareup.kotlinpoet.KModifier.OVERRIDE import com.squareup.kotlinpoet.KModifier.PRIVATE -import me.shika.test.model.Endpoint -import me.shika.test.model.ResolveResult -import me.shika.test.resolver.classDescriptor +import com.squareup.kotlinpoet.ParameterizedTypeName +import com.squareup.kotlinpoet.PropertySpec +import com.squareup.kotlinpoet.TypeName +import com.squareup.kotlinpoet.TypeSpec +import me.shika.di.model.Endpoint +import me.shika.di.model.ResolveResult +import me.shika.di.resolver.classDescriptor import org.jetbrains.kotlin.cli.common.messages.MessageCollector import org.jetbrains.kotlin.descriptors.ClassDescriptor import org.jetbrains.kotlin.descriptors.ClassKind @@ -65,7 +71,8 @@ class DaggerComponentRenderer( private fun TypeSpec.Builder.addBindings(results: List) = apply { val factoryRenderer = DaggerFactoryRenderer(this, componentClassName) - val membersInjectorRenderer = DaggerMembersInjectorRenderer(this, componentClassName, factoryRenderer) + val membersInjectorRenderer = + DaggerMembersInjectorRenderer(this, componentClassName, factoryRenderer) results.groupBy { it.endpoint.source } .map { (_, results) -> val result = results.first() diff --git a/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/test/dagger/DaggerFactoryRenderer.kt b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/dagger/DaggerFactoryRenderer.kt similarity index 91% rename from buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/test/dagger/DaggerFactoryRenderer.kt rename to buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/dagger/DaggerFactoryRenderer.kt index 3359e44..55aa962 100644 --- a/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/test/dagger/DaggerFactoryRenderer.kt +++ b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/dagger/DaggerFactoryRenderer.kt @@ -1,8 +1,16 @@ -package me.shika.test.dagger - -import com.squareup.kotlinpoet.* -import me.shika.test.model.Binding -import me.shika.test.model.GraphNode +package me.shika.di.dagger + +import com.squareup.kotlinpoet.ClassName +import com.squareup.kotlinpoet.CodeBlock +import com.squareup.kotlinpoet.FunSpec +import com.squareup.kotlinpoet.KModifier +import com.squareup.kotlinpoet.MemberName +import com.squareup.kotlinpoet.ParameterizedTypeName +import com.squareup.kotlinpoet.PropertySpec +import com.squareup.kotlinpoet.TypeName +import com.squareup.kotlinpoet.TypeSpec +import me.shika.di.model.Binding +import me.shika.di.model.GraphNode import org.jetbrains.kotlin.descriptors.ClassDescriptor import org.jetbrains.kotlin.descriptors.FunctionDescriptor import org.jetbrains.kotlin.utils.addIfNotNull diff --git a/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/test/dagger/DaggerMembersInjectorRenderer.kt b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/dagger/DaggerMembersInjectorRenderer.kt similarity index 86% rename from buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/test/dagger/DaggerMembersInjectorRenderer.kt rename to buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/dagger/DaggerMembersInjectorRenderer.kt index 3fe8937..c769900 100644 --- a/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/test/dagger/DaggerMembersInjectorRenderer.kt +++ b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/dagger/DaggerMembersInjectorRenderer.kt @@ -1,9 +1,16 @@ -package me.shika.test.dagger +package me.shika.di.dagger -import com.squareup.kotlinpoet.* -import me.shika.test.model.Endpoint -import me.shika.test.model.Injectable -import me.shika.test.model.ResolveResult +import com.squareup.kotlinpoet.ClassName +import com.squareup.kotlinpoet.CodeBlock +import com.squareup.kotlinpoet.FunSpec +import com.squareup.kotlinpoet.KModifier +import com.squareup.kotlinpoet.ParameterizedTypeName +import com.squareup.kotlinpoet.PropertySpec +import com.squareup.kotlinpoet.TypeName +import com.squareup.kotlinpoet.TypeSpec +import me.shika.di.model.Endpoint +import me.shika.di.model.Injectable +import me.shika.di.model.ResolveResult class DaggerMembersInjectorRenderer( private val componentBuilder: TypeSpec.Builder, diff --git a/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/test/dagger/utils.kt b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/dagger/utils.kt similarity index 84% rename from buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/test/dagger/utils.kt rename to buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/dagger/utils.kt index 066ce40..50a3a13 100644 --- a/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/test/dagger/utils.kt +++ b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/dagger/utils.kt @@ -1,6 +1,11 @@ -package me.shika.test.dagger - -import com.squareup.kotlinpoet.* +package me.shika.di.dagger + +import com.squareup.kotlinpoet.ClassName +import com.squareup.kotlinpoet.FunSpec +import com.squareup.kotlinpoet.ParameterSpec +import com.squareup.kotlinpoet.PropertySpec +import com.squareup.kotlinpoet.TypeName +import com.squareup.kotlinpoet.TypeSpec import org.jetbrains.kotlin.descriptors.DeclarationDescriptor import org.jetbrains.kotlin.descriptors.annotations.AnnotationDescriptor import org.jetbrains.kotlin.name.FqName diff --git a/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/test/logger.kt b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/logger.kt similarity index 97% rename from buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/test/logger.kt rename to buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/logger.kt index 1488079..8d04fa1 100644 --- a/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/test/logger.kt +++ b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/logger.kt @@ -1,4 +1,4 @@ -package me.shika.test +package me.shika.di import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity import org.jetbrains.kotlin.cli.common.messages.MessageCollector diff --git a/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/test/model/resolution.kt b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/model/resolution.kt similarity index 98% rename from buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/test/model/resolution.kt rename to buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/model/resolution.kt index c456312..5dae4c9 100644 --- a/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/test/model/resolution.kt +++ b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/model/resolution.kt @@ -1,4 +1,4 @@ -package me.shika.test.model +package me.shika.di.model import org.jetbrains.kotlin.descriptors.ClassConstructorDescriptor import org.jetbrains.kotlin.descriptors.ClassDescriptor diff --git a/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/test/resolver/ResolverContext.kt b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/resolver/ResolverContext.kt similarity index 90% rename from buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/test/resolver/ResolverContext.kt rename to buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/resolver/ResolverContext.kt index b3768e5..8ec77bd 100644 --- a/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/test/resolver/ResolverContext.kt +++ b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/resolver/ResolverContext.kt @@ -1,4 +1,4 @@ -package me.shika.test.resolver +package me.shika.di.resolver import org.jetbrains.kotlin.descriptors.ModuleDescriptor import org.jetbrains.kotlin.resolve.BindingTrace diff --git a/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/test/resolver/utils.kt b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/resolver/utils.kt similarity index 86% rename from buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/test/resolver/utils.kt rename to buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/resolver/utils.kt index 0e9f35a..65a1a87 100644 --- a/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/test/resolver/utils.kt +++ b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/resolver/utils.kt @@ -1,4 +1,4 @@ -package me.shika.test.resolver +package me.shika.di.resolver import org.jetbrains.kotlin.descriptors.ClassDescriptor import org.jetbrains.kotlin.types.KotlinType diff --git a/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/test/DiCompilerAnalysisExtension.kt b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/test/DiCompilerAnalysisExtension.kt deleted file mode 100644 index 0d972bb..0000000 --- a/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/test/DiCompilerAnalysisExtension.kt +++ /dev/null @@ -1,92 +0,0 @@ -package me.shika.test - -import me.shika.test.dagger.* -import me.shika.test.resolver.ResolverContext -import org.jetbrains.kotlin.analyzer.AnalysisResult -import org.jetbrains.kotlin.cli.common.messages.MessageCollector -import org.jetbrains.kotlin.com.intellij.openapi.project.Project -import org.jetbrains.kotlin.container.ComponentProvider -import org.jetbrains.kotlin.container.get -import org.jetbrains.kotlin.context.ProjectContext -import org.jetbrains.kotlin.descriptors.ClassDescriptor -import org.jetbrains.kotlin.descriptors.ModuleDescriptor -import org.jetbrains.kotlin.psi.KtFile -import org.jetbrains.kotlin.psi.classRecursiveVisitor -import org.jetbrains.kotlin.resolve.BindingTrace -import org.jetbrains.kotlin.resolve.jvm.extensions.AnalysisHandlerExtension -import org.jetbrains.kotlin.resolve.lazy.ResolveSession -import java.io.File - -class DiCompilerAnalysisExtension( - private val sourcesDir: File, - private val reporter: MessageCollector -) : AnalysisHandlerExtension { - private var generatedFiles = false // fixme one more hack - - override fun doAnalysis( - project: Project, - module: ModuleDescriptor, - projectContext: ProjectContext, - files: Collection, - bindingTrace: BindingTrace, - componentProvider: ComponentProvider - ): AnalysisResult? { - if (generatedFiles) return null - - val addedFiles = mutableListOf() - val resolveSession = componentProvider.get() - val resolverContext = ResolverContext(module, bindingTrace, resolveSession) - - files.forEach { file -> - file.accept( - classRecursiveVisitor { ktClass -> - val classDescriptor = resolveSession.resolveToDescriptor(ktClass) as ClassDescriptor - if (classDescriptor.isComponent()) { - val component = DaggerComponentDescriptor( - classDescriptor, - file, - resolverContext - ) - val bindings = DaggerBindingDescriptor(component) - val resolver = DaggerBindingResolver(reporter, bindings) - val renderer = DaggerComponentRenderer(component, reporter) - - val fileSpec = renderer.render(resolver.resolve()) - fileSpec.writeTo(sourcesDir) - - val filePath = fileSpec.packageName.split('.') - .dropLastWhile { it.isEmpty() } - .takeIf { !it.isEmpty() } - ?.joinToString(File.separator, postfix = File.separator) - .orEmpty() - addedFiles += sourcesDir.resolve(filePath + "${fileSpec.name}.kt") - } - } - ) - } - - if (addedFiles.isEmpty()) return AnalysisResult.Companion.success(bindingTrace.bindingContext, module) - - generatedFiles = true - return if (bindingTrace.bindingContext.diagnostics.isEmpty()) { - AnalysisResult.RetryWithAdditionalRoots( - bindingContext = bindingTrace.bindingContext, - moduleDescriptor = module, - additionalJavaRoots = emptyList(), - additionalKotlinRoots = addedFiles - ) // Repeat with my files pls - } else { - AnalysisResult.compilationError(bindingTrace.bindingContext) - } - } - - override fun analysisCompleted( - project: Project, - module: ModuleDescriptor, - bindingTrace: BindingTrace, - files: Collection - ): AnalysisResult? { - - return null - } -} diff --git a/src/main/kotlin/Client.kt b/src/main/kotlin/Client.kt index 6845b94..82d1fdc 100644 --- a/src/main/kotlin/Client.kt +++ b/src/main/kotlin/Client.kt @@ -1,27 +1,21 @@ -import lib.* +import lib.Component +import lib.bind +import lib.component +import lib.get interface Main : Component -interface Dependency { - fun longInstance(): Long -} - fun main() { val intInstance = 0 val componentImpl = component
( bind(intInstance), - module( - bind { it: Int -> it.toString() } - ), - dependency( - object : Dependency { - override fun longInstance(): Long = 1L - } - ) + bind { it: Int -> it.toString() } ) println(componentImpl.get()) println(componentImpl.get()) - println(componentImpl.get()) } + +private fun Main.provideString() = + get() From 764c1e401a3985e1657d6315e6fe1c1216040cf2 Mon Sep 17 00:00:00 2001 From: Andrei Shikov Date: Tue, 3 Sep 2019 01:47:36 +0100 Subject: [PATCH 05/12] Update proposal with shortcomings --- PROPOSAL.md | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/PROPOSAL.md b/PROPOSAL.md index 601f29c..15d382d 100644 --- a/PROPOSAL.md +++ b/PROPOSAL.md @@ -25,6 +25,9 @@ fun FooComponent.getBar(): Bar = ... ``` Base function will be substituted for generated one at compile time. +Found shortcomings: +1. The component can be referenced in another module, which imposes some problem on correct validation + Component definition can be done using Kotlin DSL and compiler transformations. `TODO: provide more details` ```kotlin // Library @@ -45,13 +48,17 @@ fun init() { It should be possible to generate the endpoints dynamically based on the usage in code. Problems to address: - - Runtime branching in parameters + - Runtime branching in parameters and method call - implementation details implicitly escaping interface of DI container. + - Multiple definitions of component in the same module (different modules make it even worse) ## Wiring `TODO: module definition` -`TODO: check macwire for examples` + +`TODO: explore macwire` + +`TODO: explore boost di` ## Injection From 5f5074db10c5a3a67bfddfaac96d53982b0309d1 Mon Sep 17 00:00:00 2001 From: Andrei Shikov Date: Wed, 4 Sep 2019 02:41:10 +0100 Subject: [PATCH 06/12] Update proposal with container instance definitions --- PROPOSAL.md | 42 +++++++++++++++++++----------------------- 1 file changed, 19 insertions(+), 23 deletions(-) diff --git a/PROPOSAL.md b/PROPOSAL.md index 15d382d..7d84201 100644 --- a/PROPOSAL.md +++ b/PROPOSAL.md @@ -8,30 +8,27 @@ The topics to explore for MVP: 5. Subscoping and providing parts of the parent graph to children. ## Exposing dependencies: -The container providing dependencies called a `Component` in analogy with dagger. -To associate each graph with correct instance calls, we can use a named component interface. -To distinguish those (and to go away from annotations `TODO: why?`), marker `interface Component` can be used. - -Kotlin has extension functions which can be leveraged here, providing a single entry point for every call. +The problem of exposing dependencies can be reduced to constructor injection. +Similarly to dagger, which is generating implementation for an interface, we can imagine the container for +exposed types as a class. +The framework task in this scenario is to wire chain of the constructors with correct dependencies. ```kotlin -// Base function defined in library -fun Component.get(): T = TODO("Should be generated") +class FooComponent( + val bar: Bar, + val otherDependency: Other.Dependency +) -// Definition of component in the client code -interface FooComponent : Component +// or -// Generated functions for each endpoint -fun FooComponent.getBar(): Bar = ... +interface FooComponent { + val bar: Bar + val otherDependency: Other.Dependency +} ``` -Base function will be substituted for generated one at compile time. - -Found shortcomings: -1. The component can be referenced in another module, which imposes some problem on correct validation - Component definition can be done using Kotlin DSL and compiler transformations. `TODO: provide more details` ```kotlin // Library -fun component(vararg dependencies: Binding<*>): T = TODO() +fun component(vararg dependencies: Binding<*>): T = TODO() fun module(definition: () -> Array>): Array // Client @@ -42,15 +39,14 @@ fun init() { *module(::fooBarModule) ) - val instance: Bar = fooComponent.get() + val instance: Bar = fooComponent.bar } ``` +This way, the dependency graph can be defined in multiple ways. The framework task is to ensure that +all the types are known in the compile time and validate the graph. -It should be possible to generate the endpoints dynamically based on the usage in code. -Problems to address: - - Runtime branching in parameters and method call - - implementation details implicitly escaping interface of DI container. - - Multiple definitions of component in the same module (different modules make it even worse) +Food for thought: + - Is such "dynamic" definition providing better user experience rather than "static graph" that Dagger use? ## Wiring From edd17955d9e5dabc8378086e8db6b2928f42a88d Mon Sep 17 00:00:00 2001 From: Andrei Shikov Date: Fri, 6 Sep 2019 01:19:23 +0100 Subject: [PATCH 07/12] Replace vararg function with a constructor call --- build.gradle | 2 +- .../java/me/shika/di/DiCodegenExtension.kt | 63 +++++++++++++++ .../shika/di/DiCompilerAnalysisExtension.kt | 77 +++++-------------- .../shika/di/DiCompilerComponentRegistrar.kt | 6 ++ .../shika/di/DiCompilerStorageContributor.kt | 7 +- src/main/kotlin/Client.kt | 23 ++---- src/main/kotlin/lib/Library.kt | 14 +--- 7 files changed, 103 insertions(+), 89 deletions(-) create mode 100644 buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/DiCodegenExtension.kt diff --git a/build.gradle b/build.gradle index 0327c7b..e7e9515 100644 --- a/build.gradle +++ b/build.gradle @@ -44,5 +44,5 @@ compileTestKotlin { } application { - mainClassName = 'MainKt' + mainClassName = 'ClientKt' } diff --git a/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/DiCodegenExtension.kt b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/DiCodegenExtension.kt new file mode 100644 index 0000000..5c8be9f --- /dev/null +++ b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/DiCodegenExtension.kt @@ -0,0 +1,63 @@ +package me.shika.di + +import org.jetbrains.kotlin.codegen.JvmKotlinType +import org.jetbrains.kotlin.codegen.StackValue +import org.jetbrains.kotlin.codegen.extensions.ExpressionCodegenExtension +import org.jetbrains.kotlin.descriptors.findClassAcrossModuleDependencies +import org.jetbrains.kotlin.name.ClassId +import org.jetbrains.kotlin.resolve.DelegatingBindingTrace +import org.jetbrains.kotlin.resolve.calls.model.ExpressionValueArgument +import org.jetbrains.kotlin.resolve.calls.model.MutableDataFlowInfoForArguments +import org.jetbrains.kotlin.resolve.calls.model.ResolvedCall +import org.jetbrains.kotlin.resolve.calls.model.ResolvedCallImpl +import org.jetbrains.kotlin.resolve.calls.smartcasts.DataFlowInfo +import org.jetbrains.kotlin.resolve.calls.tasks.TracingStrategy +import org.jetbrains.kotlin.resolve.descriptorUtil.module + +class DiCodegenExtension : ExpressionCodegenExtension { + override fun applyFunction( + receiver: StackValue, + resolvedCall: ResolvedCall<*>, + c: ExpressionCodegenExtension.Context + ): StackValue? { + if (resolvedCall in calls) { + println("Need to be overriden") + val module = c.codegen.context.functionDescriptor.module + val cls = module.findClassAcrossModuleDependencies(ClassId.fromString("Foo"))!! + val clsType = c.typeMapper.mapType(cls) + val ctor = cls.unsubstitutedPrimaryConstructor!! + + val arguments = resolvedCall.valueArguments.entries.first().value.arguments + ctor.valueParameters.zip(arguments).forEachIndexed { i, pair -> + val (param, argument) = pair + val type = c.codegen.bindingContext.getType(argument.getArgumentExpression()!!)!! + c.codegen.defaultCallGenerator.genValueAndPut( + param, + argument.getArgumentExpression()!!, + JvmKotlinType(c.typeMapper.mapType(type)), + i + ) + } + + val ctorResolvedCall = ResolvedCallImpl( + resolvedCall.call, + ctor, + resolvedCall.dispatchReceiver, + resolvedCall.extensionReceiver, + resolvedCall.explicitReceiverKind, + null, + DelegatingBindingTrace(c.codegen.bindingContext, "test"), + TracingStrategy.EMPTY, + MutableDataFlowInfoForArguments.WithoutArgumentsCheck(DataFlowInfo.EMPTY) + ) + + resolvedCall.valueArguments.entries.first().value.arguments.forEachIndexed { index, valueArgument -> + ctorResolvedCall.recordValueArgument(ctor.valueParameters[index], ExpressionValueArgument(valueArgument)) + } + + return c.codegen.generateConstructorCall(ctorResolvedCall, clsType) + } else { + return super.applyFunction(receiver, resolvedCall, c) + } + } +} diff --git a/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/DiCompilerAnalysisExtension.kt b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/DiCompilerAnalysisExtension.kt index 71627fd..8f2a8b7 100644 --- a/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/DiCompilerAnalysisExtension.kt +++ b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/DiCompilerAnalysisExtension.kt @@ -1,29 +1,20 @@ package me.shika.di import me.shika.di.resolver.ResolverContext -import me.shika.di.resolver.classDescriptor import org.jetbrains.kotlin.analyzer.AnalysisResult import org.jetbrains.kotlin.cli.common.messages.MessageCollector import org.jetbrains.kotlin.com.intellij.openapi.project.Project -import org.jetbrains.kotlin.com.intellij.psi.PsiElement import org.jetbrains.kotlin.container.ComponentProvider import org.jetbrains.kotlin.container.get import org.jetbrains.kotlin.context.ProjectContext -import org.jetbrains.kotlin.descriptors.FunctionDescriptor import org.jetbrains.kotlin.descriptors.ModuleDescriptor -import org.jetbrains.kotlin.name.FqName -import org.jetbrains.kotlin.psi.KtCallExpression -import org.jetbrains.kotlin.psi.KtConstructor -import org.jetbrains.kotlin.psi.KtDeclarationWithBody import org.jetbrains.kotlin.psi.KtExpression import org.jetbrains.kotlin.psi.KtFile -import org.jetbrains.kotlin.psi.KtNamedFunction import org.jetbrains.kotlin.psi.KtTreeVisitorVoid import org.jetbrains.kotlin.resolve.BindingTrace import org.jetbrains.kotlin.resolve.BodyResolver -import org.jetbrains.kotlin.resolve.calls.callUtil.getResolvedCall -import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameSafe -import org.jetbrains.kotlin.resolve.descriptorUtil.getSuperInterfaces +import org.jetbrains.kotlin.resolve.TypeResolver +import org.jetbrains.kotlin.resolve.calls.model.VarargValueArgument import org.jetbrains.kotlin.resolve.jvm.extensions.AnalysisHandlerExtension import org.jetbrains.kotlin.resolve.lazy.ResolveSession import java.io.File @@ -42,59 +33,14 @@ class DiCompilerAnalysisExtension( bindingTrace: BindingTrace, componentProvider: ComponentProvider ): AnalysisResult? { - if (generatedFiles) return null + return null val addedFiles = mutableListOf() val resolveSession = componentProvider.get() val bodyResolver = componentProvider.get() val resolverContext = ResolverContext(module, bindingTrace, resolveSession) + val typeResolver = componentProvider.get() - files.forEach { file -> - file.accept( - recursiveExpressionVisitor { - when (it) { - is KtCallExpression -> { - var declaration: PsiElement? = it - while ((declaration !is KtNamedFunction && declaration !is KtConstructor<*>) && declaration != null) { - declaration = declaration.parent - } - if (declaration == null) return@recursiveExpressionVisitor false - declaration as KtDeclarationWithBody - - val descriptor = resolveSession.resolveToDescriptor(declaration) - if (descriptor is FunctionDescriptor) { - bodyResolver.resolveFunctionBody( - resolveSession.declarationScopeProvider.getOuterDataFlowInfoForDeclaration(declaration), - bindingTrace, - declaration, - descriptor, - resolveSession.declarationScopeProvider.getResolutionScopeForDeclaration(declaration) - ) - } - val call = it.getResolvedCall(bindingTrace.bindingContext) - - // TODO: Check for correct descriptor - - val callClass = call - ?.extensionReceiver - ?.type - ?.classDescriptor() - - val isComponent = callClass?.getSuperInterfaces() - ?.any { it.fqNameSafe == FqName("lib.Component") } == true - - if (!isComponent) return@recursiveExpressionVisitor false - - val type = call!!.typeArguments[call.candidateDescriptor.typeParameters.first()] - - println("Found exposed type $type") - true - } - else -> false - } - } - ) - } if (addedFiles.isEmpty()) return null @@ -117,8 +63,21 @@ class DiCompilerAnalysisExtension( bindingTrace: BindingTrace, files: Collection ): AnalysisResult? { + if (generatedFiles) return null + generatedFiles = true - return null + calls.forEach { resolvedCall -> + val resultType = resolvedCall.typeArguments[resolvedCall.candidateDescriptor.typeParameters.first()] + val arguments = resolvedCall.valueArguments[resolvedCall.resultingDescriptor.valueParameters.first()] as VarargValueArgument + val argumentTypes = arguments.arguments.map { bindingTrace.getType(it.getArgumentExpression()!!) } + println("Found $resultType injection using $argumentTypes}") + } + return AnalysisResult.RetryWithAdditionalRoots( + bindingContext = bindingTrace.bindingContext, + moduleDescriptor = module, + additionalJavaRoots = emptyList(), + additionalKotlinRoots = emptyList() + ) // Repeat with my files pls } } diff --git a/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/DiCompilerComponentRegistrar.kt b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/DiCompilerComponentRegistrar.kt index c42f9fe..6078efe 100644 --- a/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/DiCompilerComponentRegistrar.kt +++ b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/DiCompilerComponentRegistrar.kt @@ -5,6 +5,7 @@ import me.shika.di.DiCommandLineProcessor.Companion.KEY_ENABLED import me.shika.di.DiCommandLineProcessor.Companion.KEY_SOURCES import org.jetbrains.kotlin.cli.common.CLIConfigurationKeys import org.jetbrains.kotlin.cli.common.messages.MessageCollector +import org.jetbrains.kotlin.codegen.extensions.ExpressionCodegenExtension import org.jetbrains.kotlin.com.intellij.mock.MockProject import org.jetbrains.kotlin.compiler.plugin.ComponentRegistrar import org.jetbrains.kotlin.config.CompilerConfiguration @@ -36,6 +37,11 @@ class DiCompilerComponentRegistrar: ComponentRegistrar { DiCompilerAnalysisExtension(sourcesDir = sourcesDir, reporter = reporter) ) + ExpressionCodegenExtension.registerExtension( + project, + DiCodegenExtension() + ) + StorageComponentContainerContributor.registerExtension( project, DiCompilerStorageContributor() diff --git a/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/DiCompilerStorageContributor.kt b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/DiCompilerStorageContributor.kt index 91a7ac4..accc37a 100644 --- a/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/DiCompilerStorageContributor.kt +++ b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/DiCompilerStorageContributor.kt @@ -5,10 +5,12 @@ import org.jetbrains.kotlin.container.StorageComponentContainer import org.jetbrains.kotlin.container.useInstance import org.jetbrains.kotlin.descriptors.ModuleDescriptor import org.jetbrains.kotlin.extensions.StorageComponentContainerContributor +import org.jetbrains.kotlin.name.FqName import org.jetbrains.kotlin.platform.TargetPlatform import org.jetbrains.kotlin.resolve.calls.checkers.CallChecker import org.jetbrains.kotlin.resolve.calls.checkers.CallCheckerContext import org.jetbrains.kotlin.resolve.calls.model.ResolvedCall +import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameSafe class DiCompilerStorageContributor : StorageComponentContainerContributor { override fun registerModuleComponents( @@ -22,7 +24,10 @@ class DiCompilerStorageContributor : StorageComponentContainerContributor { class DiCompilerCallChecker : CallChecker { override fun check(resolvedCall: ResolvedCall<*>, reportOn: PsiElement, context: CallCheckerContext) { - println(resolvedCall.candidateDescriptor) + if (resolvedCall.candidateDescriptor.fqNameSafe == FqName("lib.component")) { + calls.add(resolvedCall) + } } } +val calls: MutableList> = mutableListOf() diff --git a/src/main/kotlin/Client.kt b/src/main/kotlin/Client.kt index 82d1fdc..7e37b10 100644 --- a/src/main/kotlin/Client.kt +++ b/src/main/kotlin/Client.kt @@ -1,21 +1,14 @@ -import lib.Component -import lib.bind import lib.component -import lib.get - -interface Main : Component fun main() { - val intInstance = 0 - - val componentImpl = component
( - bind(intInstance), - bind { it: Int -> it.toString() } + val foo = component( + 0, + "" ) - - println(componentImpl.get()) - println(componentImpl.get()) + println(foo) } -private fun Main.provideString() = - get() +data class Foo( + val int: Int, + val string: String +) diff --git a/src/main/kotlin/lib/Library.kt b/src/main/kotlin/lib/Library.kt index d186229..8732575 100644 --- a/src/main/kotlin/lib/Library.kt +++ b/src/main/kotlin/lib/Library.kt @@ -1,15 +1,3 @@ package lib -interface Component - -interface Binding -interface Container : Binding -interface Instance : Binding - -fun Component.get(): T = TODO("Should be generated") -fun component(vararg dependencies: Binding): T = TODO("Should be generated") - -fun module(vararg dependencies: Binding): Container = TODO() -fun dependency(value: T): Container = TODO() -fun bind(instance: T): Instance = TODO() -fun bind(instanceProvider: (A) -> B): Instance = TODO() +fun component(vararg dependencies: Any?): T = TODO("Should be generated") From ec41826e4049f6304d258b8cc2a60ca25fd886d0 Mon Sep 17 00:00:00 2001 From: Andrei Shikov Date: Sun, 8 Sep 2019 21:55:37 +0100 Subject: [PATCH 08/12] Parse graph for functional def of component --- .../java/me/shika/di/DiCodegenExtension.kt | 2 + .../shika/di/DiCompilerAnalysisExtension.kt | 48 ++++-- .../java/me/shika/di/DiCompilerCallChecker.kt | 17 ++ .../shika/di/DiCompilerStorageContributor.kt | 15 -- .../di/dagger/DaggerBindingDescriptor.kt | 119 -------------- .../shika/di/dagger/DaggerBindingResolver.kt | 99 ------------ .../di/dagger/DaggerComponentDescriptor.kt | 46 ------ .../di/dagger/DaggerComponentRenderer.kt | 144 ----------------- .../shika/di/dagger/DaggerFactoryRenderer.kt | 149 ------------------ .../dagger/DaggerMembersInjectorRenderer.kt | 88 ----------- .../src/main/java/me/shika/di/dagger/utils.kt | 55 ------- .../main/java/me/shika/di/model/resolution.kt | 58 +------ .../shika/di/resolver/ComponentDescriptor.kt | 9 ++ .../me/shika/di/resolver/GraphResolution.kt | 43 +++++ .../me/shika/di/resolver/ResolverContext.kt | 8 +- .../main/java/me/shika/di/resolver/names.kt | 10 ++ .../main/java/me/shika/di/resolver/utils.kt | 3 + .../resolver/validation/DiBindingProcessor.kt | 8 + .../validation/ExtractAnonymousTypes.kt | 23 +++ .../resolver/validation/ExtractFunctions.kt | 38 +++++ .../di/resolver/validation/ParseParameters.kt | 17 ++ .../validation/ReportBindingDuplicates.kt | 19 +++ src/main/kotlin/Client.kt | 7 + 23 files changed, 236 insertions(+), 789 deletions(-) create mode 100644 buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/DiCompilerCallChecker.kt delete mode 100644 buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/dagger/DaggerBindingDescriptor.kt delete mode 100644 buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/dagger/DaggerBindingResolver.kt delete mode 100644 buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/dagger/DaggerComponentDescriptor.kt delete mode 100644 buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/dagger/DaggerComponentRenderer.kt delete mode 100644 buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/dagger/DaggerFactoryRenderer.kt delete mode 100644 buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/dagger/DaggerMembersInjectorRenderer.kt delete mode 100644 buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/dagger/utils.kt create mode 100644 buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/resolver/ComponentDescriptor.kt create mode 100644 buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/resolver/GraphResolution.kt create mode 100644 buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/resolver/names.kt create mode 100644 buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/resolver/validation/DiBindingProcessor.kt create mode 100644 buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/resolver/validation/ExtractAnonymousTypes.kt create mode 100644 buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/resolver/validation/ExtractFunctions.kt create mode 100644 buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/resolver/validation/ParseParameters.kt create mode 100644 buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/resolver/validation/ReportBindingDuplicates.kt diff --git a/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/DiCodegenExtension.kt b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/DiCodegenExtension.kt index 5c8be9f..7a57120 100644 --- a/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/DiCodegenExtension.kt +++ b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/DiCodegenExtension.kt @@ -1,5 +1,6 @@ package me.shika.di +import me.shika.di.resolver.COMPONENT_CALLS import org.jetbrains.kotlin.codegen.JvmKotlinType import org.jetbrains.kotlin.codegen.StackValue import org.jetbrains.kotlin.codegen.extensions.ExpressionCodegenExtension @@ -20,6 +21,7 @@ class DiCodegenExtension : ExpressionCodegenExtension { resolvedCall: ResolvedCall<*>, c: ExpressionCodegenExtension.Context ): StackValue? { + val calls = c.codegen.bindingContext.getKeys(COMPONENT_CALLS) if (resolvedCall in calls) { println("Need to be overriden") val module = c.codegen.context.functionDescriptor.module diff --git a/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/DiCompilerAnalysisExtension.kt b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/DiCompilerAnalysisExtension.kt index 8f2a8b7..29c1b0a 100644 --- a/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/DiCompilerAnalysisExtension.kt +++ b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/DiCompilerAnalysisExtension.kt @@ -1,6 +1,15 @@ package me.shika.di +import me.shika.di.model.Binding +import me.shika.di.resolver.COMPONENT_CALLS +import me.shika.di.resolver.ComponentDescriptor import me.shika.di.resolver.ResolverContext +import me.shika.di.resolver.resolveGraph +import me.shika.di.resolver.resultType +import me.shika.di.resolver.validation.ExtractAnonymousTypes +import me.shika.di.resolver.validation.ExtractFunctions +import me.shika.di.resolver.validation.ParseParameters +import me.shika.di.resolver.validation.ReportBindingDuplicates import org.jetbrains.kotlin.analyzer.AnalysisResult import org.jetbrains.kotlin.cli.common.messages.MessageCollector import org.jetbrains.kotlin.com.intellij.openapi.project.Project @@ -8,15 +17,13 @@ import org.jetbrains.kotlin.container.ComponentProvider import org.jetbrains.kotlin.container.get import org.jetbrains.kotlin.context.ProjectContext import org.jetbrains.kotlin.descriptors.ModuleDescriptor -import org.jetbrains.kotlin.psi.KtExpression import org.jetbrains.kotlin.psi.KtFile -import org.jetbrains.kotlin.psi.KtTreeVisitorVoid import org.jetbrains.kotlin.resolve.BindingTrace import org.jetbrains.kotlin.resolve.BodyResolver import org.jetbrains.kotlin.resolve.TypeResolver -import org.jetbrains.kotlin.resolve.calls.model.VarargValueArgument import org.jetbrains.kotlin.resolve.jvm.extensions.AnalysisHandlerExtension import org.jetbrains.kotlin.resolve.lazy.ResolveSession +import org.jetbrains.kotlin.storage.LockBasedStorageManager import java.io.File class DiCompilerAnalysisExtension( @@ -38,7 +45,6 @@ class DiCompilerAnalysisExtension( val addedFiles = mutableListOf() val resolveSession = componentProvider.get() val bodyResolver = componentProvider.get() - val resolverContext = ResolverContext(module, bindingTrace, resolveSession) val typeResolver = componentProvider.get() @@ -66,11 +72,28 @@ class DiCompilerAnalysisExtension( if (generatedFiles) return null generatedFiles = true + val calls = bindingTrace.getKeys(COMPONENT_CALLS) + + val processors = listOf( + ParseParameters(), + ExtractFunctions(), + ExtractAnonymousTypes(), + ReportBindingDuplicates() + ) + calls.forEach { resolvedCall -> - val resultType = resolvedCall.typeArguments[resolvedCall.candidateDescriptor.typeParameters.first()] - val arguments = resolvedCall.valueArguments[resolvedCall.resultingDescriptor.valueParameters.first()] as VarargValueArgument - val argumentTypes = arguments.arguments.map { bindingTrace.getType(it.getArgumentExpression()!!) } - println("Found $resultType injection using $argumentTypes}") + val context = ResolverContext(bindingTrace, LockBasedStorageManager.NO_LOCKS, resolvedCall) + val resultType = resolvedCall.resultType + + val bindings = processors.fold(emptySequence(), { bindings, processor -> + with(processor) { + context.process(bindings) + } + }) + + val descriptor = ComponentDescriptor(resultType!!, bindings.toList()) + val graph = context.resolveGraph(descriptor) + println(graph) } return AnalysisResult.RetryWithAdditionalRoots( bindingContext = bindingTrace.bindingContext, @@ -80,12 +103,3 @@ class DiCompilerAnalysisExtension( ) // Repeat with my files pls } } - -private fun recursiveExpressionVisitor(block: (KtExpression) -> Boolean) = - object : KtTreeVisitorVoid() { - override fun visitExpression(expression: KtExpression) { - if (!block(expression)) { - super.visitExpression(expression) - } - } - } diff --git a/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/DiCompilerCallChecker.kt b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/DiCompilerCallChecker.kt new file mode 100644 index 0000000..39c5699 --- /dev/null +++ b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/DiCompilerCallChecker.kt @@ -0,0 +1,17 @@ +package me.shika.di + +import me.shika.di.resolver.COMPONENT_CALLS +import me.shika.di.resolver.COMPONENT_FUN_FQ_NAME +import org.jetbrains.kotlin.com.intellij.psi.PsiElement +import org.jetbrains.kotlin.resolve.calls.checkers.CallChecker +import org.jetbrains.kotlin.resolve.calls.checkers.CallCheckerContext +import org.jetbrains.kotlin.resolve.calls.model.ResolvedCall +import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameSafe + +class DiCompilerCallChecker : CallChecker { + override fun check(resolvedCall: ResolvedCall<*>, reportOn: PsiElement, context: CallCheckerContext) { + if (resolvedCall.candidateDescriptor.fqNameSafe == COMPONENT_FUN_FQ_NAME) { + context.trace.record(COMPONENT_CALLS, resolvedCall) + } + } +} diff --git a/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/DiCompilerStorageContributor.kt b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/DiCompilerStorageContributor.kt index accc37a..8ecb8e0 100644 --- a/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/DiCompilerStorageContributor.kt +++ b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/DiCompilerStorageContributor.kt @@ -1,16 +1,10 @@ package me.shika.di -import org.jetbrains.kotlin.com.intellij.psi.PsiElement import org.jetbrains.kotlin.container.StorageComponentContainer import org.jetbrains.kotlin.container.useInstance import org.jetbrains.kotlin.descriptors.ModuleDescriptor import org.jetbrains.kotlin.extensions.StorageComponentContainerContributor -import org.jetbrains.kotlin.name.FqName import org.jetbrains.kotlin.platform.TargetPlatform -import org.jetbrains.kotlin.resolve.calls.checkers.CallChecker -import org.jetbrains.kotlin.resolve.calls.checkers.CallCheckerContext -import org.jetbrains.kotlin.resolve.calls.model.ResolvedCall -import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameSafe class DiCompilerStorageContributor : StorageComponentContainerContributor { override fun registerModuleComponents( @@ -22,12 +16,3 @@ class DiCompilerStorageContributor : StorageComponentContainerContributor { } } -class DiCompilerCallChecker : CallChecker { - override fun check(resolvedCall: ResolvedCall<*>, reportOn: PsiElement, context: CallCheckerContext) { - if (resolvedCall.candidateDescriptor.fqNameSafe == FqName("lib.component")) { - calls.add(resolvedCall) - } - } - -} -val calls: MutableList> = mutableListOf() diff --git a/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/dagger/DaggerBindingDescriptor.kt b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/dagger/DaggerBindingDescriptor.kt deleted file mode 100644 index 68f08a7..0000000 --- a/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/dagger/DaggerBindingDescriptor.kt +++ /dev/null @@ -1,119 +0,0 @@ -package me.shika.di.dagger - -import me.shika.di.model.Binding -import me.shika.di.model.Endpoint -import me.shika.di.model.Injectable -import me.shika.di.resolver.classDescriptor -import org.jetbrains.kotlin.builtins.KotlinBuiltIns -import org.jetbrains.kotlin.contracts.parsing.isEqualsDescriptor -import org.jetbrains.kotlin.descriptors.ClassDescriptor -import org.jetbrains.kotlin.descriptors.DeclarationDescriptor -import org.jetbrains.kotlin.descriptors.FunctionDescriptor -import org.jetbrains.kotlin.descriptors.PropertyDescriptor -import org.jetbrains.kotlin.name.FqName -import org.jetbrains.kotlin.name.Name -import org.jetbrains.kotlin.resolve.scopes.DescriptorKindFilter -import org.jetbrains.kotlin.resolve.scopes.getDescriptorsFiltered -import org.jetbrains.kotlin.types.typeUtil.isInt - -class DaggerBindingDescriptor( - val componentDescriptor: DaggerComponentDescriptor -) { - val bindings = findBindings() - val endpoints = findEndpoints() - - private fun findBindings(): List { - val modules = componentDescriptor.modules - val dependencies = componentDescriptor.dependencies - val instances = componentDescriptor.moduleInstances - val moduleBindings = modules.flatMap { module -> - val companionBindings = module.companionObjectDescriptor - ?.allDescriptors(DescriptorKindFilter.FUNCTIONS) - ?: emptyList() - - (module.allDescriptors(DescriptorKindFilter.FUNCTIONS) + companionBindings) - .filter { it.annotations.hasAnnotation(PROVIDES_FQ_NAME) } - .filterIsInstance() - .map { - when (module) { - in instances -> Binding.InstanceFunction(module, it, it.scopeAnnotations()) - else -> Binding.StaticFunction(it, it.scopeAnnotations()) - } - } - } - val dependencyBindings = dependencies.flatMap { dependency -> - dependency.allDescriptors(DescriptorKindFilter.FUNCTIONS) - .filterIsInstance() - .filterNot { it.isFromAny() } - .map { - Binding.InstanceFunction(dependency, it, it.scopeAnnotations()) - } - } - - return moduleBindings + dependencyBindings - } - - private fun findEndpoints(): Set { - val scope = componentDescriptor.definition.unsubstitutedMemberScope - val functions = scope.getDescriptorsFiltered(DescriptorKindFilter.FUNCTIONS) - .asSequence() - .filterIsInstance() - .filterNot { it.isFromAny() } - - // TODO go through each instead of filtering - - val exposedTypes = functions.getExposedTypes() - val injectables = functions.getInjectedTypes() - - return (exposedTypes + injectables).toSet() - } - - private fun Sequence.getExposedTypes() = - filter { it.valueParameters.isEmpty() } - .map { Endpoint.Exposed(it) } - - private fun Sequence.getInjectedTypes() = - filter { it.valueParameters.size == 1 && KotlinBuiltIns.isUnit(it.returnType!!) } - .flatMap { func -> - val cls = func.valueParameters.first().type.classDescriptor()!! - val fields = - cls.allDescriptors(DescriptorKindFilter.VARIABLES) - .asSequence() - .filterIsInstance() - .filter { - it.annotations.hasAnnotation(INJECT_FQ_NAME) || - it.backingField?.annotations?.hasAnnotation(INJECT_FQ_NAME) == true || - it.setter?.annotations?.hasAnnotation(INJECT_FQ_NAME) == true - } - .map { - Endpoint.Injected(func, Injectable.Property(cls, it)) - } - - val setters = cls.allDescriptors(DescriptorKindFilter.FUNCTIONS) - .asSequence() - .filterIsInstance() - .filter { it.annotations.hasAnnotation(INJECT_FQ_NAME) } - .map { - Endpoint.Injected(func, Injectable.Setter(cls, it)) - } - - fields + setters - } -} - -val INJECT_FQ_NAME = FqName("javax.inject.Inject") -val PROVIDES_FQ_NAME = FqName("dagger.Provides") - -private fun ClassDescriptor.allDescriptors(kindFilter: DescriptorKindFilter) = - unsubstitutedMemberScope.getDescriptorsFiltered(kindFilter) { true } - -private fun DeclarationDescriptor.isHashCodeDescriptor() = - this is FunctionDescriptor && name == Name.identifier("hashCode") - && returnType?.isInt() == true && valueParameters.isEmpty() - -private fun DeclarationDescriptor.isToStringDescriptor() = - this is FunctionDescriptor && name == Name.identifier("toString") - && KotlinBuiltIns.isString(returnType) && valueParameters.isEmpty() - -private fun FunctionDescriptor.isFromAny() = - isEqualsDescriptor() || isHashCodeDescriptor() || isToStringDescriptor() diff --git a/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/dagger/DaggerBindingResolver.kt b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/dagger/DaggerBindingResolver.kt deleted file mode 100644 index f45db32..0000000 --- a/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/dagger/DaggerBindingResolver.kt +++ /dev/null @@ -1,99 +0,0 @@ -package me.shika.di.dagger - -import me.shika.di.AMBIGUOUS_BINDING -import me.shika.di.DaggerErrorMessages -import me.shika.di.model.Binding -import me.shika.di.model.Endpoint -import me.shika.di.model.GraphNode -import me.shika.di.model.ResolveResult -import me.shika.di.resolver.classDescriptor -import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity -import org.jetbrains.kotlin.cli.common.messages.MessageCollector -import org.jetbrains.kotlin.descriptors.DeclarationDescriptor -import org.jetbrains.kotlin.diagnostics.reportFromPlugin -import org.jetbrains.kotlin.ir.expressions.typeParametersCount -import org.jetbrains.kotlin.js.resolve.diagnostics.findPsi -import org.jetbrains.kotlin.types.KotlinType -import org.jetbrains.kotlin.types.checker.NewKotlinTypeChecker -import org.jetbrains.kotlin.types.typeUtil.makeNotNullable - -class DaggerBindingResolver( - val reporter: MessageCollector, - val bindingDescriptor: DaggerBindingDescriptor -) { - val component = bindingDescriptor.componentDescriptor - val storage = component.context.resolveSession.storageManager - val trace = component.context.trace - - val cachedNodeResolve = storage.createMemoizedFunction, GraphNode> { (type, source) -> - type.resolveNode(source) - } - - fun resolve(): List = - bindingDescriptor.endpoints.map { it.resolveDependencies() } - - private fun Endpoint.resolveDependencies(): ResolveResult { - val nodes = types.map { cachedNodeResolve(it!! to source) } - return ResolveResult(this, nodes) - } - - private fun KotlinType.resolveNode(source: DeclarationDescriptor): GraphNode { - val canProvide = bindingDescriptor.bindings.filter { - val returnType = it.type ?: return@filter false - NewKotlinTypeChecker.equalTypes(returnType, this) - || NewKotlinTypeChecker.equalTypes(returnType, makeNotNullable()) - } + listOfNotNull(this.injectableConstructor()) - - if (canProvide.isEmpty()) { - reporter.report( - CompilerMessageSeverity.EXCEPTION, - "Cannot provide $this" - ) - } - - if (canProvide.size > 1) { - trace.reportFromPlugin( - AMBIGUOUS_BINDING.on(source.findPsi()!!), - DaggerErrorMessages - ) -// reporter.report( -// CompilerMessageSeverity.ERROR, -// "Ambiguous binding for $this: found $canProvide" -// ) - } - - val binding = canProvide.first() - val bindingDescriptor = binding.resolvedDescriptor - - val scopeAnnotations = binding.scopes.map { it.fqName } - val componentScopeNames = component.scopes.map { it.fqName } - if (!componentScopeNames.containsAll(scopeAnnotations)) { - reporter.report( - CompilerMessageSeverity.EXCEPTION, - "Component ${component.definition} and ${bindingDescriptor.name} scopes do not match: " + - "component - $componentScopeNames," + - " binding - $scopeAnnotations" - ) - } - - return GraphNode(binding, binding.resolveDependencies()) - } - - private fun Binding.resolveDependencies(): List = - resolvedDescriptor.valueParameters - .map { - cachedNodeResolve(it.type to resolvedDescriptor) - } - - // TODO Remove from here? - private fun KotlinType.injectableConstructor(): Binding.Constructor? { - val classDescriptor = classDescriptor() ?: return null - val injectableConstructors = classDescriptor.constructors.filter { - it.annotations.hasAnnotation(INJECT_FQ_NAME) && it.typeParametersCount == 0 // TODO: type parameterized injections? - } - if (injectableConstructors.size > 1) { - reporter.report(CompilerMessageSeverity.EXCEPTION, "Class can have only one @Inject annotated constructor, found $injectableConstructors") - } - return injectableConstructors.firstOrNull()?.let { Binding.Constructor(it, classDescriptor.scopeAnnotations()) } - } -} diff --git a/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/dagger/DaggerComponentDescriptor.kt b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/dagger/DaggerComponentDescriptor.kt deleted file mode 100644 index 07e822b..0000000 --- a/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/dagger/DaggerComponentDescriptor.kt +++ /dev/null @@ -1,46 +0,0 @@ -package me.shika.di.dagger - -import me.shika.di.resolver.ResolverContext -import org.jetbrains.kotlin.descriptors.ClassDescriptor -import org.jetbrains.kotlin.descriptors.Modality -import org.jetbrains.kotlin.descriptors.annotations.AnnotationDescriptor -import org.jetbrains.kotlin.name.FqName -import org.jetbrains.kotlin.psi.KtFile -import org.jetbrains.kotlin.resolve.DescriptorUtils -import org.jetbrains.kotlin.resolve.annotations.argumentValue -import org.jetbrains.kotlin.resolve.constants.KClassValue - -class DaggerComponentDescriptor( - val definition: ClassDescriptor, - val file: KtFile, - val context: ResolverContext -) { - val annotation = definition.componentAnnotation()!! - val modules = annotation.value(context, "modules") - val moduleInstances = modules.filter { it.isInstance() } - val dependencies = annotation.value(context, "dependencies") - val scopes = definition.scopeAnnotations() -} - -private val DAGGER_COMPONENT_ANNOTATION = FqName("dagger.Component") - -private fun ClassDescriptor.componentAnnotation() = - if (modality == Modality.ABSTRACT && this !is AnnotationDescriptor) { - annotations.findAnnotation(DAGGER_COMPONENT_ANNOTATION) - } else { - null - } - -private fun AnnotationDescriptor.value(context: ResolverContext, name: String) = - (argumentValue(name)?.value as? List) - ?.mapNotNull { - val value = it.getArgumentType(context.module) - value.constructor.declarationDescriptor as? ClassDescriptor - } - ?: emptyList() - -private fun ClassDescriptor.isInstance() = - !DescriptorUtils.isObject(this) && modality != Modality.ABSTRACT - -fun ClassDescriptor.isComponent() = - annotations.hasAnnotation(DAGGER_COMPONENT_ANNOTATION) diff --git a/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/dagger/DaggerComponentRenderer.kt b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/dagger/DaggerComponentRenderer.kt deleted file mode 100644 index faec739..0000000 --- a/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/dagger/DaggerComponentRenderer.kt +++ /dev/null @@ -1,144 +0,0 @@ -package me.shika.di.dagger - -import com.squareup.kotlinpoet.ClassName -import com.squareup.kotlinpoet.FileSpec -import com.squareup.kotlinpoet.FunSpec -import com.squareup.kotlinpoet.KModifier.OVERRIDE -import com.squareup.kotlinpoet.KModifier.PRIVATE -import com.squareup.kotlinpoet.ParameterizedTypeName -import com.squareup.kotlinpoet.PropertySpec -import com.squareup.kotlinpoet.TypeName -import com.squareup.kotlinpoet.TypeSpec -import me.shika.di.model.Endpoint -import me.shika.di.model.ResolveResult -import me.shika.di.resolver.classDescriptor -import org.jetbrains.kotlin.cli.common.messages.MessageCollector -import org.jetbrains.kotlin.descriptors.ClassDescriptor -import org.jetbrains.kotlin.descriptors.ClassKind -import org.jetbrains.kotlin.descriptors.FunctionDescriptor -import org.jetbrains.kotlin.name.FqName -import org.jetbrains.kotlin.name.parentOrNull -import org.jetbrains.kotlin.renderer.render -import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameSafe -import org.jetbrains.kotlin.types.KotlinType - -class DaggerComponentRenderer( - private val componentDescriptor: DaggerComponentDescriptor, - private val messageCollector: MessageCollector -) { - private val definition = componentDescriptor.definition - private val componentClassName = ClassName( - componentDescriptor.file.packageFqName.render(), - componentDescriptor.nameString() - ) - - fun render(results: List): FileSpec = - FileSpec.builder(componentClassName.packageName, componentClassName.simpleName) - .addType(renderComponent(results)) - .build() - - private fun renderComponent(results: List): TypeSpec = - TypeSpec.classBuilder(componentDescriptor.nameString()) - .extendComponent() - .addModuleInstances() - .addBindings(results) - .build() - - private fun TypeSpec.Builder.extendComponent() = apply { - if (definition.kind == ClassKind.INTERFACE) { - addSuperinterface(definition.className()) - } else { - superclass(definition.className()) - } - } - - private fun TypeSpec.Builder.addModuleInstances() = apply { - val properties = (componentDescriptor.moduleInstances + componentDescriptor.dependencies).map { - val name = it.className().simpleName.decapitalize() - PropertySpec.builder(name, it.className(), PRIVATE) - .initializer(name) - .build() - } - val params = properties.map { it.toParameter() } - - primaryConstructor( - FunSpec.constructorBuilder() - .addParameters(params) - .build() - ) - addProperties(properties) - } - - private fun TypeSpec.Builder.addBindings(results: List) = apply { - val factoryRenderer = DaggerFactoryRenderer(this, componentClassName) - val membersInjectorRenderer = - DaggerMembersInjectorRenderer(this, componentClassName, factoryRenderer) - results.groupBy { it.endpoint.source } - .map { (_, results) -> - val result = results.first() - when (val endpoint = result.endpoint) { - is Endpoint.Exposed -> { - val factory = factoryRenderer.getFactory(result.graph.single()) - addFunction(endpoint.source, override = true) { - addCode("return %N.get()", factory) - } - } - is Endpoint.Injected -> { - addFunction(endpoint.source, override = true) { - val injectedValue = parameters.first() - val membersInjector = membersInjectorRenderer.getMembersInjector(injectedValue.type, results) - addCode("%N.injectMembers(${injectedValue.name})", membersInjector) - } - } - } - } - } -} - -private fun ClassDescriptor.className() = - ClassName( - packageName = fqNameSafe.parentOrNull()?.takeIf { it != FqName.ROOT }?.asString().orEmpty(), - simpleName = name.asString() - ) - -internal fun KotlinType.typeName(): TypeName? = - classDescriptor()?.className()?.let { - with(ParameterizedTypeName.Companion) { - val type = if (arguments.isNotEmpty()) { - val arguments = arguments.mapNotNull { it.type.typeName() }.toTypedArray() - it.parameterizedBy(*arguments) - } else { - it - } - type.copy(nullable = isMarkedNullable) - } - } - -private fun TypeSpec.Builder.addFunction( - signature: FunctionDescriptor, - override: Boolean = false, - builder: FunSpec.Builder.() -> Unit -) { - addFunction( - FunSpec.builder(signature.name.asString()) - .apply { - if (override) addModifiers(OVERRIDE) - signature.valueParameters.forEach { - addParameter(it.name.asString(), it.type.typeName()!!) - } - returns(signature.returnType!!.typeName()!!) - } - .apply(builder) - .build() - ) -} - -internal fun TypeName.name() = - when (this) { - is ClassName -> simpleName - is ParameterizedTypeName -> rawType.simpleName - else -> "" - } - -internal fun DaggerComponentDescriptor.nameString() = - "Dagger${definition.name}" diff --git a/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/dagger/DaggerFactoryRenderer.kt b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/dagger/DaggerFactoryRenderer.kt deleted file mode 100644 index 55aa962..0000000 --- a/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/dagger/DaggerFactoryRenderer.kt +++ /dev/null @@ -1,149 +0,0 @@ -package me.shika.di.dagger - -import com.squareup.kotlinpoet.ClassName -import com.squareup.kotlinpoet.CodeBlock -import com.squareup.kotlinpoet.FunSpec -import com.squareup.kotlinpoet.KModifier -import com.squareup.kotlinpoet.MemberName -import com.squareup.kotlinpoet.ParameterizedTypeName -import com.squareup.kotlinpoet.PropertySpec -import com.squareup.kotlinpoet.TypeName -import com.squareup.kotlinpoet.TypeSpec -import me.shika.di.model.Binding -import me.shika.di.model.GraphNode -import org.jetbrains.kotlin.descriptors.ClassDescriptor -import org.jetbrains.kotlin.descriptors.FunctionDescriptor -import org.jetbrains.kotlin.utils.addIfNotNull - -class DaggerFactoryRenderer(private val componentBuilder: TypeSpec.Builder, private val componentName: ClassName) { - fun getFactory(graphNode: GraphNode): PropertySpec? { - val signature = graphNode.value.resolvedDescriptor - val returnType = signature.returnType?.typeName() ?: return null // FIXME report - val providerType = returnType.provider() - - return componentBuilder.propertySpecs.find { it.type == providerType }.ifNull { - componentBuilder.addFactory(graphNode, signature, returnType, providerType) - } - } - - private fun TypeSpec.Builder.addFactory( - graphNode: GraphNode, - signature: FunctionDescriptor, - returnType: TypeName, - providerType: TypeName - ): PropertySpec? { - val parent = signature.containingDeclaration as ClassDescriptor - val parentType = parent.type() - val name = graphNode.bindingName(parentType) - - val factoryType = componentName.nestedClass("${name}_Factory") - val factoryMemberName = "${name}_Provider".decapitalize() - val instanceProperty = (graphNode.value as? Binding.InstanceFunction)?.toProperty() - - val depsFactories = graphNode.dependencies.mapNotNull { getFactory(it) } - val factoryProperties = depsFactories.toMutableList() - .apply { addIfNotNull(instanceProperty) } - - addType( - classWithFactories(factoryProperties, factoryType, providerType) - .addFunction( - FunSpec.builder("get") - .addModifiers(KModifier.OVERRIDE) - .returns(returnType) - .addCode(graphNode.providerBody(instanceProperty, signature, depsFactories, parentType)) - .build() - ) - .build() - ) - - // Factory property in component (cached if scoped) - val property = PropertySpec.builder(factoryMemberName, providerType) - .factoryProperty(factoryType, depsFactories, parentType, graphNode.value.scopes.isNotEmpty()) - .build() - - addProperty(property) - return property - } - - private fun GraphNode.providerBody( - instanceProperty: PropertySpec?, - signature: FunctionDescriptor, - depsFactories: List, - parentType: TypeName? - ) = when (value) { - is Binding.StaticFunction -> { - CodeBlock.of( - "return %T.${signature.name}(${depsFactories.joinToString(",") { "${it.name}.get()" }})", - parentType - ) - } - is Binding.Constructor -> { - CodeBlock.of( - "return %T(${depsFactories.joinToString(",") { "${it.name}.get()" }})", - parentType - ) - } - is Binding.InstanceFunction -> { - CodeBlock.of( - "return %N.${signature.name}(${depsFactories.joinToString(",") { "${it.name}.get()" }})", - instanceProperty!!.name - ) - } - } - - private fun PropertySpec.Builder.factoryProperty( - factoryType: TypeName, - depsFactories: List, - parentType: TypeName?, - isScoped: Boolean - ) = apply { - val params = depsFactories.map { it.name }.toMutableList() - componentBuilder.propertySpecs.find { it.type == parentType }?.let { params += it.name } - - val doubleCheckName = MemberName( - ClassName("dagger.internal", "DoubleCheck"), - "provider" - ) - - val args = hashMapOf( - "doubleCheck" to doubleCheckName, - "factoryType" to factoryType - ) - - initializer( - CodeBlock.builder() - .addNamed( - "%factoryType:T(${params.joinToString()})".letIf(isScoped) { - "%doubleCheck:M($it)" - }, - args - ) - .build() - ) - } - - private fun TypeName.provider() = - with(ParameterizedTypeName.Companion) { - ClassName("javax.inject", "Provider") - .parameterizedBy(this@provider) - } - - private fun Binding.InstanceFunction.toProperty(): PropertySpec { - val moduleType = instance.defaultType.typeName() - return PropertySpec.builder(moduleType!!.name().decapitalize(), moduleType, KModifier.PRIVATE) - .initializer(moduleType.name().decapitalize()) - .build() - } - - private fun ClassDescriptor.type() = if (isCompanionObject) { - (containingDeclaration as ClassDescriptor).defaultType.typeName() - } else { - defaultType.typeName() - } - - private fun GraphNode.bindingName(parentType: TypeName?) = when (value) { - is Binding.StaticFunction, - is Binding.InstanceFunction -> "${parentType?.name()}_${value.resolvedDescriptor.name.asString().capitalize()}" - is Binding.Constructor -> "${value.descriptor.constructedClass.name}" - } -} diff --git a/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/dagger/DaggerMembersInjectorRenderer.kt b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/dagger/DaggerMembersInjectorRenderer.kt deleted file mode 100644 index c769900..0000000 --- a/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/dagger/DaggerMembersInjectorRenderer.kt +++ /dev/null @@ -1,88 +0,0 @@ -package me.shika.di.dagger - -import com.squareup.kotlinpoet.ClassName -import com.squareup.kotlinpoet.CodeBlock -import com.squareup.kotlinpoet.FunSpec -import com.squareup.kotlinpoet.KModifier -import com.squareup.kotlinpoet.ParameterizedTypeName -import com.squareup.kotlinpoet.PropertySpec -import com.squareup.kotlinpoet.TypeName -import com.squareup.kotlinpoet.TypeSpec -import me.shika.di.model.Endpoint -import me.shika.di.model.Injectable -import me.shika.di.model.ResolveResult - -class DaggerMembersInjectorRenderer( - private val componentBuilder: TypeSpec.Builder, - private val componentName: ClassName, - private val factoryRenderer: DaggerFactoryRenderer -) { - fun getMembersInjector(injectedType: TypeName, results: List): PropertySpec? { - val membersInjectorType = injectedType.injector() - - return componentBuilder.propertySpecs.find { it.type == membersInjectorType } - ?: componentBuilder.addMembersInjector(injectedType, results) - } - - private fun TypeSpec.Builder.addMembersInjector(injectedTypeName: TypeName, results: List): PropertySpec { - val injectorTypeName = componentName.nestedClass("${injectedTypeName.name()}_MembersInjector") - val injectedParamName = injectedTypeName.name().decapitalize() - - val injectedFactories = results.map { (it.endpoint as Endpoint.Injected).value } - .zip(results.map { it.graph.map { factoryRenderer.getFactory(it) } }) - - val factoryParams = injectedFactories.flatMap { it.second }.filterNotNull().distinct() - - val type = classWithFactories(factoryParams, injectorTypeName, injectedTypeName.injector()) - .addFunction( - FunSpec.builder("injectMembers") - .addModifiers(KModifier.OVERRIDE) - .addParameter(injectedParamName, injectedTypeName) - .apply { - injectedFactories.generateInjects(injectedParamName).forEach { - addCode(it) - } - } - .build() - ) - .build() - addType(type) - - val property = PropertySpec.builder( - injectorTypeName.name().decapitalize(), - injectedTypeName.injector(), - KModifier.PRIVATE - ).initializer( - injectorTypeName.let { - val params = Array(factoryParams.size) { "%N" }.joinToString() - CodeBlock.of("%T($params)", it, *factoryParams.toTypedArray()) - } - ) - .build() - addProperty(property) - - return property - } - - private fun List>>.generateInjects(injectedParamName: String): List = - map { (injectable, factories) -> - when (injectable) { - is Injectable.Setter -> { - val setter = injectable.descriptor.name.asString() - val params = factories.joinToString { "${it?.name}.get()" } - CodeBlock.of("$injectedParamName.$setter($params)\n") - } - is Injectable.Property -> { - val parameter = injectable.descriptor.name.asString() - val factory = factories.single() - val value = "${factory?.name}.get()" - CodeBlock.of("$injectedParamName.$parameter = $value\n") - } - } - } - - private fun TypeName.injector() = with(ParameterizedTypeName.Companion) { - ClassName("dagger", "MembersInjector") - .parameterizedBy(this@injector) - } -} diff --git a/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/dagger/utils.kt b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/dagger/utils.kt deleted file mode 100644 index 50a3a13..0000000 --- a/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/dagger/utils.kt +++ /dev/null @@ -1,55 +0,0 @@ -package me.shika.di.dagger - -import com.squareup.kotlinpoet.ClassName -import com.squareup.kotlinpoet.FunSpec -import com.squareup.kotlinpoet.ParameterSpec -import com.squareup.kotlinpoet.PropertySpec -import com.squareup.kotlinpoet.TypeName -import com.squareup.kotlinpoet.TypeSpec -import org.jetbrains.kotlin.descriptors.DeclarationDescriptor -import org.jetbrains.kotlin.descriptors.annotations.AnnotationDescriptor -import org.jetbrains.kotlin.name.FqName -import org.jetbrains.kotlin.resolve.descriptorUtil.annotationClass - -internal fun DeclarationDescriptor.scopeAnnotations(): List = - annotations.filter { - it.annotationClass?.annotations?.hasAnnotation(SCOPE_FQ_NAME) == true - } - -internal fun T?.ifNull(action: () -> T?): T? = - this ?: action() - -internal fun T.letIf(condition: Boolean, block: (T) -> T) = - if (condition) { - block(this) - } else { - this - } - -internal fun PropertySpec.toParameter() = - ParameterSpec.builder(name, type, *modifiers.toTypedArray()) - .build() - -fun classWithFactories( - factories: List, - type: ClassName, - superInterface: TypeName -): TypeSpec.Builder { - val properties = factories.map { - PropertySpec.builder(it.name, it.type, *it.modifiers.toTypedArray()) - .initializer(it.name) - .build() - } - - // Inner static class to generate binding - return TypeSpec.classBuilder(type) - .addSuperinterface(superInterface) - .addProperties(properties) - .primaryConstructor( - FunSpec.constructorBuilder() - .addParameters(properties.map { it.toParameter() }) - .build() - ) -} - -private val SCOPE_FQ_NAME = FqName("javax.inject.Scope") diff --git a/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/model/resolution.kt b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/model/resolution.kt index 5dae4c9..b4d81ea 100644 --- a/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/model/resolution.kt +++ b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/model/resolution.kt @@ -1,61 +1,13 @@ package me.shika.di.model -import org.jetbrains.kotlin.descriptors.ClassConstructorDescriptor -import org.jetbrains.kotlin.descriptors.ClassDescriptor -import org.jetbrains.kotlin.descriptors.FunctionDescriptor -import org.jetbrains.kotlin.descriptors.PropertyDescriptor -import org.jetbrains.kotlin.descriptors.annotations.AnnotationDescriptor +import org.jetbrains.kotlin.types.KotlinType data class GraphNode(val value: Binding, val dependencies: List) -data class ResolveResult(val endpoint: Endpoint, val graph: List) - -sealed class Endpoint { - abstract val source: FunctionDescriptor - - data class Exposed(override val source: FunctionDescriptor) : Endpoint() - data class Injected(override val source: FunctionDescriptor, val value: Injectable) : Endpoint() - - val types get() = when (this) { - is Injected -> value.types - is Exposed -> listOf(source.returnType) - } -} - -sealed class Injectable { - data class Setter(val cls: ClassDescriptor, val descriptor: FunctionDescriptor): Injectable() - data class Property(val cls: ClassDescriptor, val descriptor: PropertyDescriptor): Injectable() - - val types get() = when(this) { - is Setter -> descriptor.valueParameters.map { it.type } - is Property -> listOf(descriptor.returnType) - } -} - sealed class Binding { - abstract val scopes: List - - data class InstanceFunction( - val instance: ClassDescriptor, - val descriptor: FunctionDescriptor, - override val scopes: List - ): Binding() - - data class StaticFunction( - val descriptor: FunctionDescriptor, - override val scopes: List - ): Binding() - - data class Constructor( - val descriptor: ClassConstructorDescriptor, - override val scopes: List - ): Binding() - - val resolvedDescriptor get() = when (this) { - is InstanceFunction -> descriptor - is StaticFunction -> descriptor - is Constructor -> descriptor - } + abstract val type: KotlinType - val type get() = resolvedDescriptor.returnType + data class Instance(override val type: KotlinType): Binding() + data class Function(val from: List, override val type: KotlinType): Binding() + data class Constructor(val from: List, override val type: KotlinType): Binding() } diff --git a/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/resolver/ComponentDescriptor.kt b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/resolver/ComponentDescriptor.kt new file mode 100644 index 0000000..ad211c2 --- /dev/null +++ b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/resolver/ComponentDescriptor.kt @@ -0,0 +1,9 @@ +package me.shika.di.resolver + +import me.shika.di.model.Binding +import org.jetbrains.kotlin.types.KotlinType + +data class ComponentDescriptor( + val returnType: KotlinType, + val bindings: List +) diff --git a/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/resolver/GraphResolution.kt b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/resolver/GraphResolution.kt new file mode 100644 index 0000000..bcf6aa2 --- /dev/null +++ b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/resolver/GraphResolution.kt @@ -0,0 +1,43 @@ +package me.shika.di.resolver + +import me.shika.di.model.Binding +import me.shika.di.model.GraphNode +import org.jetbrains.kotlin.descriptors.Visibilities +import org.jetbrains.kotlin.types.KotlinType + +fun ResolverContext.resolveGraph(componentDescriptor: ComponentDescriptor): GraphNode = + GraphResolver(this, componentDescriptor).resolve() + + +class GraphResolver( + private val resolverContext: ResolverContext, + private val componentDescriptor: ComponentDescriptor +) { + private val resolveMemoized = resolverContext.storageManager.createMemoizedFunction { it: KotlinType -> + it.resolve() + } + + fun resolve() = + resolveMemoized(componentDescriptor.returnType) + + private fun KotlinType.resolve(): GraphNode { + val binding = componentDescriptor.bindings.firstOrNull { it.type == this } + ?: constructor() + ?: TODO("Cannot find binding for $this") + + val bindingDeps = when (binding) { + is Binding.Instance -> emptyList() + is Binding.Function -> binding.from + is Binding.Constructor -> binding.from + } + + val children = bindingDeps.map { resolveMemoized(it) } + return GraphNode(binding, children) + } + + private fun KotlinType.constructor() = classDescriptor() + ?.constructors + ?.single { it.visibility == Visibilities.PUBLIC || it.visibility == Visibilities.INTERNAL } + ?.let { Binding.Constructor(it.valueParameters.map { it.type }, this) } + +} diff --git a/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/resolver/ResolverContext.kt b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/resolver/ResolverContext.kt index 8ec77bd..13a6042 100644 --- a/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/resolver/ResolverContext.kt +++ b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/resolver/ResolverContext.kt @@ -1,11 +1,11 @@ package me.shika.di.resolver -import org.jetbrains.kotlin.descriptors.ModuleDescriptor import org.jetbrains.kotlin.resolve.BindingTrace -import org.jetbrains.kotlin.resolve.lazy.ResolveSession +import org.jetbrains.kotlin.resolve.calls.model.ResolvedCall +import org.jetbrains.kotlin.storage.StorageManager class ResolverContext( - val module: ModuleDescriptor, val trace: BindingTrace, - val resolveSession: ResolveSession + val storageManager: StorageManager, + val resolvedCall: ResolvedCall<*> ) diff --git a/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/resolver/names.kt b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/resolver/names.kt new file mode 100644 index 0000000..9f210c6 --- /dev/null +++ b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/resolver/names.kt @@ -0,0 +1,10 @@ +package me.shika.di.resolver + +import org.jetbrains.kotlin.name.FqName +import org.jetbrains.kotlin.resolve.calls.model.ResolvedCall +import org.jetbrains.kotlin.util.slicedMap.Slices +import org.jetbrains.kotlin.util.slicedMap.WritableSlice + +val COMPONENT_CALLS: WritableSlice, Boolean> = Slices.createCollectiveSetSlice>() + +internal val COMPONENT_FUN_FQ_NAME = FqName("lib.component") diff --git a/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/resolver/utils.kt b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/resolver/utils.kt index 65a1a87..1401cc7 100644 --- a/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/resolver/utils.kt +++ b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/resolver/utils.kt @@ -1,6 +1,9 @@ package me.shika.di.resolver import org.jetbrains.kotlin.descriptors.ClassDescriptor +import org.jetbrains.kotlin.resolve.calls.model.ResolvedCall import org.jetbrains.kotlin.types.KotlinType fun KotlinType.classDescriptor() = constructor.declarationDescriptor as? ClassDescriptor + +val ResolvedCall<*>.resultType get() = typeArguments[candidateDescriptor.typeParameters.single()] diff --git a/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/resolver/validation/DiBindingProcessor.kt b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/resolver/validation/DiBindingProcessor.kt new file mode 100644 index 0000000..dd58f14 --- /dev/null +++ b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/resolver/validation/DiBindingProcessor.kt @@ -0,0 +1,8 @@ +package me.shika.di.resolver.validation + +import me.shika.di.model.Binding +import me.shika.di.resolver.ResolverContext + +interface DiBindingProcessor { + fun ResolverContext.process(resolvedBindings: Sequence): Sequence +} diff --git a/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/resolver/validation/ExtractAnonymousTypes.kt b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/resolver/validation/ExtractAnonymousTypes.kt new file mode 100644 index 0000000..ceec211 --- /dev/null +++ b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/resolver/validation/ExtractAnonymousTypes.kt @@ -0,0 +1,23 @@ +package me.shika.di.resolver.validation + +import me.shika.di.model.Binding +import me.shika.di.resolver.ResolverContext +import org.jetbrains.kotlin.resolve.DescriptorUtils +import org.jetbrains.kotlin.types.typeUtil.immediateSupertypes + +class ExtractAnonymousTypes : DiBindingProcessor { + override fun ResolverContext.process(resolvedBindings: Sequence): Sequence = + resolvedBindings.map { + when (it) { + is Binding.Instance -> it.resolveAnonymousBindings() + else -> it + } + } + + private fun Binding.Instance.resolveAnonymousBindings(): Binding.Instance = + if (DescriptorUtils.isAnonymousObject(type.constructor.declarationDescriptor!!)) { + copy(type = type.immediateSupertypes().single()) + } else { + this + } +} diff --git a/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/resolver/validation/ExtractFunctions.kt b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/resolver/validation/ExtractFunctions.kt new file mode 100644 index 0000000..4233454 --- /dev/null +++ b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/resolver/validation/ExtractFunctions.kt @@ -0,0 +1,38 @@ +package me.shika.di.resolver.validation + +import me.shika.di.model.Binding +import me.shika.di.resolver.ResolverContext +import org.jetbrains.kotlin.builtins.functions.FunctionClassDescriptor.Kind.Function +import org.jetbrains.kotlin.builtins.functions.FunctionClassDescriptor.Kind.KFunction +import org.jetbrains.kotlin.builtins.functions.FunctionClassDescriptor.Kind.KSuspendFunction +import org.jetbrains.kotlin.builtins.functions.FunctionClassDescriptor.Kind.SuspendFunction +import org.jetbrains.kotlin.builtins.getFunctionalClassKind +import org.jetbrains.kotlin.types.KotlinType + +class ExtractFunctions : DiBindingProcessor { + override fun ResolverContext.process(resolvedBindings: Sequence): Sequence = + resolvedBindings.map { + when (it) { + is Binding.Instance -> it.functionBinding() ?: it + else -> it + } + } + + private fun Binding.Instance.functionBinding(): Binding.Function? { + val kind = type.constructor.declarationDescriptor?.getFunctionalClassKind() ?: return null + + return when (kind) { + Function, + KFunction -> type.extractFunctionBinding() + SuspendFunction, + KSuspendFunction -> TODO() + } + } + + private fun KotlinType.extractFunctionBinding(): Binding.Function { + val from = arguments.dropLast(1).map { it.type } + val to = arguments.last().type + + return Binding.Function(from, to) + } +} diff --git a/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/resolver/validation/ParseParameters.kt b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/resolver/validation/ParseParameters.kt new file mode 100644 index 0000000..1ae0bd5 --- /dev/null +++ b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/resolver/validation/ParseParameters.kt @@ -0,0 +1,17 @@ +package me.shika.di.resolver.validation + +import me.shika.di.model.Binding +import me.shika.di.resolver.ResolverContext + +class ParseParameters : DiBindingProcessor { + override fun ResolverContext.process(resolvedBindings: Sequence): Sequence { + val arguments = resolvedCall.valueArguments + if (arguments.size != 1) { + return emptySequence() + } + + val varargArgument = arguments.values.first() + val argumentTypes = varargArgument.arguments.map { trace.getType(it.getArgumentExpression()!!)!! } + return resolvedBindings + argumentTypes.map { Binding.Instance(it) } + } +} diff --git a/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/resolver/validation/ReportBindingDuplicates.kt b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/resolver/validation/ReportBindingDuplicates.kt new file mode 100644 index 0000000..a137ce3 --- /dev/null +++ b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/resolver/validation/ReportBindingDuplicates.kt @@ -0,0 +1,19 @@ +package me.shika.di.resolver.validation + +import me.shika.di.model.Binding +import me.shika.di.resolver.ResolverContext + +class ReportBindingDuplicates : DiBindingProcessor { + override fun ResolverContext.process(resolvedBindings: Sequence): Sequence = + resolvedBindings.also { checkDuplicates(it) } + + private fun ResolverContext.checkDuplicates(bindings: Sequence) { + bindings + .groupBy { it.type } + .filter { it.value.size > 1 } + .forEach { + // trace.reportFromPlugin() + TODO("report duplicates") + } + } +} diff --git a/src/main/kotlin/Client.kt b/src/main/kotlin/Client.kt index 7e37b10..1ef077f 100644 --- a/src/main/kotlin/Client.kt +++ b/src/main/kotlin/Client.kt @@ -2,13 +2,20 @@ import lib.component fun main() { val foo = component( + ::Bar, 0, "" ) + println(foo) } data class Foo( + val bar: Bar, val int: Int, val string: String ) + +data class Bar(val int: Int) + +fun bar(): Int = 0 From 882b7dc9786926bd47a1547dd6426c37eab0427f Mon Sep 17 00:00:00 2001 From: Andrei Shikov Date: Mon, 9 Sep 2019 20:27:38 +0100 Subject: [PATCH 09/12] Render base function and substitute it --- .../java/me/shika/di/DiCodegenExtension.kt | 44 +++++++++---------- .../shika/di/DiCompilerAnalysisExtension.kt | 16 +++++-- .../java/me/shika/di/render/GraphRenderer.kt | 6 +++ .../di/render/GraphToFunctionRenderer.kt | 29 ++++++++++++ .../src/main/java/me/shika/di/render/utils.kt | 41 +++++++++++++++++ .../main/java/me/shika/di/resolver/names.kt | 2 + src/main/kotlin/Client.kt | 4 +- 7 files changed, 113 insertions(+), 29 deletions(-) create mode 100644 buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/render/GraphRenderer.kt create mode 100644 buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/render/GraphToFunctionRenderer.kt create mode 100644 buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/render/utils.kt diff --git a/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/DiCodegenExtension.kt b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/DiCodegenExtension.kt index 7a57120..b97d57a 100644 --- a/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/DiCodegenExtension.kt +++ b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/DiCodegenExtension.kt @@ -1,13 +1,11 @@ package me.shika.di +import me.shika.di.render.findTopLevelFunctionForName import me.shika.di.resolver.COMPONENT_CALLS -import org.jetbrains.kotlin.codegen.JvmKotlinType import org.jetbrains.kotlin.codegen.StackValue import org.jetbrains.kotlin.codegen.extensions.ExpressionCodegenExtension -import org.jetbrains.kotlin.descriptors.findClassAcrossModuleDependencies -import org.jetbrains.kotlin.name.ClassId +import org.jetbrains.kotlin.name.FqName import org.jetbrains.kotlin.resolve.DelegatingBindingTrace -import org.jetbrains.kotlin.resolve.calls.model.ExpressionValueArgument import org.jetbrains.kotlin.resolve.calls.model.MutableDataFlowInfoForArguments import org.jetbrains.kotlin.resolve.calls.model.ResolvedCall import org.jetbrains.kotlin.resolve.calls.model.ResolvedCallImpl @@ -25,25 +23,24 @@ class DiCodegenExtension : ExpressionCodegenExtension { if (resolvedCall in calls) { println("Need to be overriden") val module = c.codegen.context.functionDescriptor.module - val cls = module.findClassAcrossModuleDependencies(ClassId.fromString("Foo"))!! - val clsType = c.typeMapper.mapType(cls) - val ctor = cls.unsubstitutedPrimaryConstructor!! + val function = module.findTopLevelFunctionForName(FqName.ROOT, FqName("component_Foo"))!! + val clsType = c.typeMapper.mapType(function.returnType!!) val arguments = resolvedCall.valueArguments.entries.first().value.arguments - ctor.valueParameters.zip(arguments).forEachIndexed { i, pair -> - val (param, argument) = pair - val type = c.codegen.bindingContext.getType(argument.getArgumentExpression()!!)!! - c.codegen.defaultCallGenerator.genValueAndPut( - param, - argument.getArgumentExpression()!!, - JvmKotlinType(c.typeMapper.mapType(type)), - i - ) - } +// function.valueParameters.zip(arguments).forEachIndexed { i, pair -> +// val (param, argument) = pair +// val type = c.codegen.bindingContext.getType(argument.getArgumentExpression()!!)!! +// c.codegen.defaultCallGenerator.genValueAndPut( +// param, +// argument.getArgumentExpression()!!, +// JvmKotlinType(c.typeMapper.mapType(type)), +// i +// ) +// } val ctorResolvedCall = ResolvedCallImpl( resolvedCall.call, - ctor, + function, resolvedCall.dispatchReceiver, resolvedCall.extensionReceiver, resolvedCall.explicitReceiverKind, @@ -53,11 +50,14 @@ class DiCodegenExtension : ExpressionCodegenExtension { MutableDataFlowInfoForArguments.WithoutArgumentsCheck(DataFlowInfo.EMPTY) ) - resolvedCall.valueArguments.entries.first().value.arguments.forEachIndexed { index, valueArgument -> - ctorResolvedCall.recordValueArgument(ctor.valueParameters[index], ExpressionValueArgument(valueArgument)) - } +// resolvedCall.valueArguments.entries.first().value.arguments.forEachIndexed { index, valueArgument -> +// ctorResolvedCall.recordValueArgument(function.valueParameters[index], ExpressionValueArgument(valueArgument)) +// } - return c.codegen.generateConstructorCall(ctorResolvedCall, clsType) + val method = c.typeMapper.mapToCallableMethod(function, false, resolvedCall = ctorResolvedCall) + return StackValue.functionCall(clsType, function.returnType) { + c.codegen.invokeMethodWithArguments(method, ctorResolvedCall, StackValue.none()) + } } else { return super.applyFunction(receiver, resolvedCall, c) } diff --git a/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/DiCompilerAnalysisExtension.kt b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/DiCompilerAnalysisExtension.kt index 29c1b0a..420fd24 100644 --- a/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/DiCompilerAnalysisExtension.kt +++ b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/DiCompilerAnalysisExtension.kt @@ -1,6 +1,7 @@ package me.shika.di import me.shika.di.model.Binding +import me.shika.di.render.GraphToFunctionRenderer import me.shika.di.resolver.COMPONENT_CALLS import me.shika.di.resolver.ComponentDescriptor import me.shika.di.resolver.ResolverContext @@ -69,11 +70,14 @@ class DiCompilerAnalysisExtension( bindingTrace: BindingTrace, files: Collection ): AnalysisResult? { - if (generatedFiles) return null - generatedFiles = true - val calls = bindingTrace.getKeys(COMPONENT_CALLS) + if (generatedFiles) { + // record new descriptor + return null + } + generatedFiles = true + val processors = listOf( ParseParameters(), ExtractFunctions(), @@ -93,13 +97,17 @@ class DiCompilerAnalysisExtension( val descriptor = ComponentDescriptor(resultType!!, bindings.toList()) val graph = context.resolveGraph(descriptor) + println(graph) + + val fileSpec = GraphToFunctionRenderer(context).invoke(graph) + fileSpec.writeTo(sourcesDir) } return AnalysisResult.RetryWithAdditionalRoots( bindingContext = bindingTrace.bindingContext, moduleDescriptor = module, additionalJavaRoots = emptyList(), - additionalKotlinRoots = emptyList() + additionalKotlinRoots = listOf(sourcesDir) ) // Repeat with my files pls } } diff --git a/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/render/GraphRenderer.kt b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/render/GraphRenderer.kt new file mode 100644 index 0000000..15b49ee --- /dev/null +++ b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/render/GraphRenderer.kt @@ -0,0 +1,6 @@ +package me.shika.di.render + +import com.squareup.kotlinpoet.FileSpec +import me.shika.di.model.GraphNode + +interface GraphRenderer : (GraphNode) -> FileSpec diff --git a/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/render/GraphToFunctionRenderer.kt b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/render/GraphToFunctionRenderer.kt new file mode 100644 index 0000000..ab54fe7 --- /dev/null +++ b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/render/GraphToFunctionRenderer.kt @@ -0,0 +1,29 @@ +package me.shika.di.render + +import com.squareup.kotlinpoet.FileSpec +import com.squareup.kotlinpoet.FunSpec +import me.shika.di.model.GraphNode +import me.shika.di.resolver.ResolverContext +import me.shika.di.resolver.classDescriptor +import me.shika.di.resolver.resultType +import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameSafe + +class GraphToFunctionRenderer(private val resolverContext: ResolverContext) : GraphRenderer { + private val returnType = resolverContext.resolvedCall.resultType + + override fun invoke(node: GraphNode): FileSpec = + FileSpec.builder(packageName()?.asString().orEmpty(), "${fileName()?.asString()}_generated.kt") + .addFunction( + FunSpec.builder("component_${returnType?.classDescriptor()?.name}") + .returns(returnType?.typeName()!!) + .addCode("TODO()") + .build() + ) + .build() + + private fun packageName() = + resolverContext.parentDescriptor()?.fqNameSafe?.parent() + + private fun fileName() = + resolverContext.parentDescriptor()?.name +} diff --git a/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/render/utils.kt b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/render/utils.kt new file mode 100644 index 0000000..32d5548 --- /dev/null +++ b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/render/utils.kt @@ -0,0 +1,41 @@ +package me.shika.di.render + +import com.squareup.kotlinpoet.ClassName +import me.shika.di.resolver.ResolverContext +import me.shika.di.resolver.classDescriptor +import org.jetbrains.kotlin.backend.common.serialization.findTopLevelDescriptor +import org.jetbrains.kotlin.com.intellij.psi.PsiElement +import org.jetbrains.kotlin.descriptors.FunctionDescriptor +import org.jetbrains.kotlin.descriptors.ModuleDescriptor +import org.jetbrains.kotlin.incremental.components.NoLookupLocation +import org.jetbrains.kotlin.name.FqName +import org.jetbrains.kotlin.psi.KtDeclaration +import org.jetbrains.kotlin.resolve.BindingContext +import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameSafe +import org.jetbrains.kotlin.types.KotlinType + +fun KotlinType.typeName(): ClassName? = classDescriptor()?.fqNameSafe?.let { + ClassName(it.parent().asString(), it.shortName().asString()) +} + +fun PsiElement.parentDeclaration(): KtDeclaration? { + var declaration: PsiElement? = this + while (declaration != null && declaration !is KtDeclaration) declaration = declaration.parent + return declaration as? KtDeclaration +} + +fun ResolverContext.parentDescriptor() = + resolvedCall.call.calleeExpression?.parentDeclaration()?.let { + trace[BindingContext.DECLARATION_TO_DESCRIPTOR, it] + }?.findTopLevelDescriptor() + +fun ModuleDescriptor.findTopLevelFunctionForName(packageFqName: FqName, functionName: FqName): FunctionDescriptor? { + val packageViewDescriptor = getPackage(packageFqName) + val segments = functionName.pathSegments() + val topLevelFunctions = packageViewDescriptor.memberScope.getContributedFunctions( + segments.first(), + NoLookupLocation.FROM_DESERIALIZATION + ).filter { it.name == functionName.shortName() } + .firstOrNull() + return topLevelFunctions +} diff --git a/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/resolver/names.kt b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/resolver/names.kt index 9f210c6..a9ca2b5 100644 --- a/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/resolver/names.kt +++ b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/resolver/names.kt @@ -1,10 +1,12 @@ package me.shika.di.resolver +import org.jetbrains.kotlin.descriptors.FunctionDescriptor import org.jetbrains.kotlin.name.FqName import org.jetbrains.kotlin.resolve.calls.model.ResolvedCall import org.jetbrains.kotlin.util.slicedMap.Slices import org.jetbrains.kotlin.util.slicedMap.WritableSlice val COMPONENT_CALLS: WritableSlice, Boolean> = Slices.createCollectiveSetSlice>() +val GENERATED_COMPONENT: WritableSlice, FunctionDescriptor> = Slices.createSimpleSlice() internal val COMPONENT_FUN_FQ_NAME = FqName("lib.component") diff --git a/src/main/kotlin/Client.kt b/src/main/kotlin/Client.kt index 1ef077f..1efb6fa 100644 --- a/src/main/kotlin/Client.kt +++ b/src/main/kotlin/Client.kt @@ -4,10 +4,8 @@ fun main() { val foo = component( ::Bar, 0, - "" + { it: Int -> it.toString() } ) - - println(foo) } data class Foo( From fae070ac756c143f86a42f692fdc9086bcd3aa61 Mon Sep 17 00:00:00 2001 From: Andrei Shikov Date: Sat, 14 Sep 2019 00:57:22 +0100 Subject: [PATCH 10/12] Actually generate some code --- .../java/me/shika/di/DiCodegenExtension.kt | 42 ++++++----- .../shika/di/DiCompilerAnalysisExtension.kt | 53 +++---------- .../shika/di/DiCompilerComponentRegistrar.kt | 2 +- .../di/render/GraphToFunctionRenderer.kt | 75 ++++++++++++++++--- .../src/main/java/me/shika/di/render/utils.kt | 73 +++++++++++------- .../main/java/me/shika/di/resolver/names.kt | 3 +- .../main/java/me/shika/di/resolver/utils.kt | 51 +++++++++++++ .../di/resolver/validation/ParseParameters.kt | 6 +- src/main/kotlin/Client.kt | 21 ++++++ 9 files changed, 217 insertions(+), 109 deletions(-) diff --git a/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/DiCodegenExtension.kt b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/DiCodegenExtension.kt index b97d57a..8bfeb2b 100644 --- a/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/DiCodegenExtension.kt +++ b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/DiCodegenExtension.kt @@ -1,11 +1,14 @@ package me.shika.di -import me.shika.di.render.findTopLevelFunctionForName import me.shika.di.resolver.COMPONENT_CALLS +import me.shika.di.resolver.GENERATED_CALL_NAME +import me.shika.di.resolver.findTopLevelFunctionForName +import me.shika.di.resolver.varargArguments +import org.jetbrains.kotlin.codegen.JvmKotlinType import org.jetbrains.kotlin.codegen.StackValue import org.jetbrains.kotlin.codegen.extensions.ExpressionCodegenExtension -import org.jetbrains.kotlin.name.FqName import org.jetbrains.kotlin.resolve.DelegatingBindingTrace +import org.jetbrains.kotlin.resolve.calls.model.ExpressionValueArgument import org.jetbrains.kotlin.resolve.calls.model.MutableDataFlowInfoForArguments import org.jetbrains.kotlin.resolve.calls.model.ResolvedCall import org.jetbrains.kotlin.resolve.calls.model.ResolvedCallImpl @@ -19,24 +22,23 @@ class DiCodegenExtension : ExpressionCodegenExtension { resolvedCall: ResolvedCall<*>, c: ExpressionCodegenExtension.Context ): StackValue? { - val calls = c.codegen.bindingContext.getKeys(COMPONENT_CALLS) - if (resolvedCall in calls) { - println("Need to be overriden") + val isComponentCall = c.codegen.bindingContext[COMPONENT_CALLS, resolvedCall] == true + if (isComponentCall) { val module = c.codegen.context.functionDescriptor.module - val function = module.findTopLevelFunctionForName(FqName.ROOT, FqName("component_Foo"))!! + val function = module.findTopLevelFunctionForName(c.codegen.bindingContext[GENERATED_CALL_NAME, resolvedCall]!!)!! val clsType = c.typeMapper.mapType(function.returnType!!) - val arguments = resolvedCall.valueArguments.entries.first().value.arguments -// function.valueParameters.zip(arguments).forEachIndexed { i, pair -> -// val (param, argument) = pair -// val type = c.codegen.bindingContext.getType(argument.getArgumentExpression()!!)!! -// c.codegen.defaultCallGenerator.genValueAndPut( -// param, -// argument.getArgumentExpression()!!, -// JvmKotlinType(c.typeMapper.mapType(type)), -// i -// ) -// } + val arguments = resolvedCall.varargArguments() + function.valueParameters.zip(arguments).forEachIndexed { i, pair -> + val (param, argument) = pair + val type = c.codegen.bindingContext.getType(argument.getArgumentExpression()!!)!! + c.codegen.defaultCallGenerator.genValueAndPut( + param, + argument.getArgumentExpression()!!, + JvmKotlinType(c.typeMapper.mapType(type)), + i + ) + } val ctorResolvedCall = ResolvedCallImpl( resolvedCall.call, @@ -50,9 +52,9 @@ class DiCodegenExtension : ExpressionCodegenExtension { MutableDataFlowInfoForArguments.WithoutArgumentsCheck(DataFlowInfo.EMPTY) ) -// resolvedCall.valueArguments.entries.first().value.arguments.forEachIndexed { index, valueArgument -> -// ctorResolvedCall.recordValueArgument(function.valueParameters[index], ExpressionValueArgument(valueArgument)) -// } + resolvedCall.valueArguments.entries.first().value.arguments.forEachIndexed { index, valueArgument -> + ctorResolvedCall.recordValueArgument(function.valueParameters[index], ExpressionValueArgument(valueArgument)) + } val method = c.typeMapper.mapToCallableMethod(function, false, resolvedCall = ctorResolvedCall) return StackValue.functionCall(clsType, function.returnType) { diff --git a/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/DiCompilerAnalysisExtension.kt b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/DiCompilerAnalysisExtension.kt index 420fd24..9539e89 100644 --- a/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/DiCompilerAnalysisExtension.kt +++ b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/DiCompilerAnalysisExtension.kt @@ -4,7 +4,9 @@ import me.shika.di.model.Binding import me.shika.di.render.GraphToFunctionRenderer import me.shika.di.resolver.COMPONENT_CALLS import me.shika.di.resolver.ComponentDescriptor +import me.shika.di.resolver.GENERATED_CALL_NAME import me.shika.di.resolver.ResolverContext +import me.shika.di.resolver.generateCallNames import me.shika.di.resolver.resolveGraph import me.shika.di.resolver.resultType import me.shika.di.resolver.validation.ExtractAnonymousTypes @@ -12,57 +14,19 @@ import me.shika.di.resolver.validation.ExtractFunctions import me.shika.di.resolver.validation.ParseParameters import me.shika.di.resolver.validation.ReportBindingDuplicates import org.jetbrains.kotlin.analyzer.AnalysisResult -import org.jetbrains.kotlin.cli.common.messages.MessageCollector import org.jetbrains.kotlin.com.intellij.openapi.project.Project -import org.jetbrains.kotlin.container.ComponentProvider -import org.jetbrains.kotlin.container.get -import org.jetbrains.kotlin.context.ProjectContext import org.jetbrains.kotlin.descriptors.ModuleDescriptor +import org.jetbrains.kotlin.name.FqName import org.jetbrains.kotlin.psi.KtFile import org.jetbrains.kotlin.resolve.BindingTrace -import org.jetbrains.kotlin.resolve.BodyResolver -import org.jetbrains.kotlin.resolve.TypeResolver import org.jetbrains.kotlin.resolve.jvm.extensions.AnalysisHandlerExtension -import org.jetbrains.kotlin.resolve.lazy.ResolveSession import org.jetbrains.kotlin.storage.LockBasedStorageManager import java.io.File class DiCompilerAnalysisExtension( - private val sourcesDir: File, - private val reporter: MessageCollector + private val sourcesDir: File ) : AnalysisHandlerExtension { - private var generatedFiles = false // fixme one more hack - - override fun doAnalysis( - project: Project, - module: ModuleDescriptor, - projectContext: ProjectContext, - files: Collection, - bindingTrace: BindingTrace, - componentProvider: ComponentProvider - ): AnalysisResult? { - return null - - val addedFiles = mutableListOf() - val resolveSession = componentProvider.get() - val bodyResolver = componentProvider.get() - val typeResolver = componentProvider.get() - - - if (addedFiles.isEmpty()) return null - - generatedFiles = true - return if (bindingTrace.bindingContext.diagnostics.isEmpty()) { - AnalysisResult.RetryWithAdditionalRoots( - bindingContext = bindingTrace.bindingContext, - moduleDescriptor = module, - additionalJavaRoots = emptyList(), - additionalKotlinRoots = addedFiles - ) // Repeat with my files pls - } else { - AnalysisResult.compilationError(bindingTrace.bindingContext) - } - } + private var generatedFiles = false override fun analysisCompleted( project: Project, @@ -71,9 +35,12 @@ class DiCompilerAnalysisExtension( files: Collection ): AnalysisResult? { val calls = bindingTrace.getKeys(COMPONENT_CALLS) + calls.generateCallNames(bindingTrace) + .forEach { (call, name) -> + bindingTrace.record(GENERATED_CALL_NAME, call, FqName(name)) + } if (generatedFiles) { - // record new descriptor return null } generatedFiles = true @@ -98,8 +65,6 @@ class DiCompilerAnalysisExtension( val descriptor = ComponentDescriptor(resultType!!, bindings.toList()) val graph = context.resolveGraph(descriptor) - println(graph) - val fileSpec = GraphToFunctionRenderer(context).invoke(graph) fileSpec.writeTo(sourcesDir) } diff --git a/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/DiCompilerComponentRegistrar.kt b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/DiCompilerComponentRegistrar.kt index 6078efe..7e351b1 100644 --- a/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/DiCompilerComponentRegistrar.kt +++ b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/DiCompilerComponentRegistrar.kt @@ -34,7 +34,7 @@ class DiCompilerComponentRegistrar: ComponentRegistrar { AnalysisHandlerExtension.registerExtension( project, - DiCompilerAnalysisExtension(sourcesDir = sourcesDir, reporter = reporter) + DiCompilerAnalysisExtension(sourcesDir = sourcesDir) ) ExpressionCodegenExtension.registerExtension( diff --git a/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/render/GraphToFunctionRenderer.kt b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/render/GraphToFunctionRenderer.kt index ab54fe7..c16858f 100644 --- a/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/render/GraphToFunctionRenderer.kt +++ b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/render/GraphToFunctionRenderer.kt @@ -1,29 +1,82 @@ package me.shika.di.render +import com.squareup.kotlinpoet.CodeBlock import com.squareup.kotlinpoet.FileSpec import com.squareup.kotlinpoet.FunSpec +import me.shika.di.model.Binding import me.shika.di.model.GraphNode +import me.shika.di.resolver.GENERATED_CALL_NAME import me.shika.di.resolver.ResolverContext -import me.shika.di.resolver.classDescriptor import me.shika.di.resolver.resultType -import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameSafe +import me.shika.di.resolver.varargArguments +import org.jetbrains.kotlin.psi.ValueArgument +import org.jetbrains.kotlin.renderer.render class GraphToFunctionRenderer(private val resolverContext: ResolverContext) : GraphRenderer { private val returnType = resolverContext.resolvedCall.resultType - override fun invoke(node: GraphNode): FileSpec = - FileSpec.builder(packageName()?.asString().orEmpty(), "${fileName()?.asString()}_generated.kt") + private val fqName = resolverContext.trace[GENERATED_CALL_NAME, resolverContext.resolvedCall] + + override fun invoke(node: GraphNode): FileSpec { + val packageName = fqName?.parent()?.render().orEmpty() + val name = fqName?.shortName()?.asString().orEmpty() + val parameters = resolverContext.resolvedCall.varargArguments() + return FileSpec.builder(packageName, name) .addFunction( - FunSpec.builder("component_${returnType?.classDescriptor()?.name}") + FunSpec.builder(name) .returns(returnType?.typeName()!!) - .addCode("TODO()") + .addParameters(parameters.parameters(resolverContext.trace)) + .addCode( + CodeBlock.builder() + .renderGraph(node, parameters) + .build() + ) .build() ) .build() + } + + private fun CodeBlock.Builder.renderGraph(node: GraphNode, parameters: List): CodeBlock.Builder { + val parameterTypes = parameters.map { resolverContext.trace.getType(it.getArgumentExpression()!!)!! } + val nodeSet = mutableSetOf().apply { node.topSort(this) } + nodeSet.forEach { + val binding = it.value + when (binding) { + is Binding.Instance -> { /* Is a parameter directly */ } + is Binding.Function -> { + val from = binding.from + val pIndex = parameterTypes.indexOfFirst { it.arguments.lastOrNull()?.type == binding.type } + val parameters = from.map { + val pIndex = parameterTypes.indexOf(it) + if (pIndex == -1) { + it.typeName().variableName() + } else { + "p$pIndex" + } + } + addStatement( + "val ${binding.type.typeName().variableName()} = p$pIndex(${parameters.joinToString()})" + ) + } + is Binding.Constructor -> { + val from = binding.from + val parameters = from.map { + val pIndex = parameterTypes.indexOf(it) + if (pIndex == -1) { + it.typeName().variableName() + } else { + "p$pIndex" + } + } + addStatement( + "val ${binding.type.typeName().variableName()} = %T(${parameters.joinToString()})", binding.type.typeName() + ) + } + } + } - private fun packageName() = - resolverContext.parentDescriptor()?.fqNameSafe?.parent() - - private fun fileName() = - resolverContext.parentDescriptor()?.name + addStatement("return ${node.value.type.typeName().variableName()}") + + return this + } } diff --git a/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/render/utils.kt b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/render/utils.kt index 32d5548..de04a1d 100644 --- a/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/render/utils.kt +++ b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/render/utils.kt @@ -1,41 +1,58 @@ package me.shika.di.render import com.squareup.kotlinpoet.ClassName -import me.shika.di.resolver.ResolverContext +import com.squareup.kotlinpoet.Dynamic +import com.squareup.kotlinpoet.LambdaTypeName +import com.squareup.kotlinpoet.ParameterSpec +import com.squareup.kotlinpoet.ParameterizedTypeName +import com.squareup.kotlinpoet.TypeName +import com.squareup.kotlinpoet.TypeVariableName +import com.squareup.kotlinpoet.WildcardTypeName +import me.shika.di.model.GraphNode import me.shika.di.resolver.classDescriptor -import org.jetbrains.kotlin.backend.common.serialization.findTopLevelDescriptor -import org.jetbrains.kotlin.com.intellij.psi.PsiElement -import org.jetbrains.kotlin.descriptors.FunctionDescriptor -import org.jetbrains.kotlin.descriptors.ModuleDescriptor -import org.jetbrains.kotlin.incremental.components.NoLookupLocation -import org.jetbrains.kotlin.name.FqName -import org.jetbrains.kotlin.psi.KtDeclaration -import org.jetbrains.kotlin.resolve.BindingContext +import org.jetbrains.kotlin.psi.ValueArgument +import org.jetbrains.kotlin.resolve.BindingTrace import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameSafe import org.jetbrains.kotlin.types.KotlinType -fun KotlinType.typeName(): ClassName? = classDescriptor()?.fqNameSafe?.let { +fun KotlinType.typeName(): TypeName? = classDescriptor()?.fqNameSafe?.let { + val types = arguments.map { it.type.typeName()!! } ClassName(it.parent().asString(), it.shortName().asString()) + .let { + if (types.isNotEmpty()) { + with(ParameterizedTypeName.Companion) { + it.parameterizedBy(*types.toTypedArray()) + } + } else { + it + } + } } -fun PsiElement.parentDeclaration(): KtDeclaration? { - var declaration: PsiElement? = this - while (declaration != null && declaration !is KtDeclaration) declaration = declaration.parent - return declaration as? KtDeclaration -} +fun TypeName?.variableName(): String = + when (this) { + is ClassName -> canonicalName.replace(".", "_") + is ParameterizedTypeName -> { + this.rawType.variableName() + + this.typeArguments.joinToString(separator = "_", prefix = "_") { it.variableName() } + } + is TypeVariableName, + is WildcardTypeName, + is Dynamic, + is LambdaTypeName, + null -> TODO() + }.decapitalize() -fun ResolverContext.parentDescriptor() = - resolvedCall.call.calleeExpression?.parentDeclaration()?.let { - trace[BindingContext.DECLARATION_TO_DESCRIPTOR, it] - }?.findTopLevelDescriptor() +fun List.parameters(trace: BindingTrace): List { + return mapIndexed { i, arg -> + val type = trace.getType(arg.getArgumentExpression()!!) + val typeName = type!!.typeName()!! + ParameterSpec.builder("p$i", typeName) + .build() + } +} -fun ModuleDescriptor.findTopLevelFunctionForName(packageFqName: FqName, functionName: FqName): FunctionDescriptor? { - val packageViewDescriptor = getPackage(packageFqName) - val segments = functionName.pathSegments() - val topLevelFunctions = packageViewDescriptor.memberScope.getContributedFunctions( - segments.first(), - NoLookupLocation.FROM_DESERIALIZATION - ).filter { it.name == functionName.shortName() } - .firstOrNull() - return topLevelFunctions +fun GraphNode.topSort(result: MutableSet) { + dependencies.forEach { it.topSort(result) } + result.add(this) } diff --git a/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/resolver/names.kt b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/resolver/names.kt index a9ca2b5..f056f06 100644 --- a/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/resolver/names.kt +++ b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/resolver/names.kt @@ -1,12 +1,11 @@ package me.shika.di.resolver -import org.jetbrains.kotlin.descriptors.FunctionDescriptor import org.jetbrains.kotlin.name.FqName import org.jetbrains.kotlin.resolve.calls.model.ResolvedCall import org.jetbrains.kotlin.util.slicedMap.Slices import org.jetbrains.kotlin.util.slicedMap.WritableSlice val COMPONENT_CALLS: WritableSlice, Boolean> = Slices.createCollectiveSetSlice>() -val GENERATED_COMPONENT: WritableSlice, FunctionDescriptor> = Slices.createSimpleSlice() +val GENERATED_CALL_NAME: WritableSlice, FqName> = Slices.createSimpleSlice() internal val COMPONENT_FUN_FQ_NAME = FqName("lib.component") diff --git a/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/resolver/utils.kt b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/resolver/utils.kt index 1401cc7..8dc3c02 100644 --- a/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/resolver/utils.kt +++ b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/resolver/utils.kt @@ -1,9 +1,60 @@ package me.shika.di.resolver +import org.jetbrains.kotlin.backend.common.serialization.findTopLevelDescriptor +import org.jetbrains.kotlin.com.intellij.psi.PsiElement import org.jetbrains.kotlin.descriptors.ClassDescriptor +import org.jetbrains.kotlin.descriptors.FunctionDescriptor +import org.jetbrains.kotlin.descriptors.ModuleDescriptor +import org.jetbrains.kotlin.incremental.components.NoLookupLocation +import org.jetbrains.kotlin.name.FqName +import org.jetbrains.kotlin.name.Name +import org.jetbrains.kotlin.psi.KtDeclaration +import org.jetbrains.kotlin.renderer.render +import org.jetbrains.kotlin.resolve.BindingContext +import org.jetbrains.kotlin.resolve.BindingTrace import org.jetbrains.kotlin.resolve.calls.model.ResolvedCall +import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameSafe import org.jetbrains.kotlin.types.KotlinType fun KotlinType.classDescriptor() = constructor.declarationDescriptor as? ClassDescriptor val ResolvedCall<*>.resultType get() = typeArguments[candidateDescriptor.typeParameters.single()] + +fun ResolvedCall<*>.varargArguments() = valueArguments.values.first().arguments + +fun Collection>.generateCallNames(bindingTrace: BindingTrace): List, String>> = + map { + val parent = it.parentDescriptor(bindingTrace) + val pkg = parent?.fqNameSafe?.parent() ?: FqName.ROOT + val typeName = it.resultType?.classDescriptor()?.name + val functionName = COMPONENT_FUN_FQ_NAME.shortName() + val name = Name.identifier("${parent?.name}_${functionName}_${typeName}_generated") + it to pkg.child(name) + }.groupBy { it.second } + .flatMap { (_, values) -> + values.mapIndexed { i, (call, value) -> + call to value.render() + "_$i" + } + } + +fun PsiElement.parentDeclaration(): KtDeclaration? { + var declaration: PsiElement? = this + while (declaration != null && declaration !is KtDeclaration) declaration = declaration.parent + return declaration as? KtDeclaration +} + +fun ResolvedCall<*>.parentDescriptor(trace: BindingTrace) = + call.calleeExpression?.parentDeclaration()?.let { + trace[BindingContext.DECLARATION_TO_DESCRIPTOR, it] + }?.findTopLevelDescriptor() + +fun ModuleDescriptor.findTopLevelFunctionForName(functionFqName: FqName): FunctionDescriptor? { + val packageFqName = functionFqName.parent() + val functionName = functionFqName.shortName() + + val packageViewDescriptor = getPackage(packageFqName) + return packageViewDescriptor.memberScope.getContributedFunctions( + functionName, + NoLookupLocation.FROM_DESERIALIZATION + ).firstOrNull { it.name == functionName } +} diff --git a/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/resolver/validation/ParseParameters.kt b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/resolver/validation/ParseParameters.kt index 1ae0bd5..13423c5 100644 --- a/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/resolver/validation/ParseParameters.kt +++ b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/resolver/validation/ParseParameters.kt @@ -2,16 +2,16 @@ package me.shika.di.resolver.validation import me.shika.di.model.Binding import me.shika.di.resolver.ResolverContext +import me.shika.di.resolver.varargArguments class ParseParameters : DiBindingProcessor { override fun ResolverContext.process(resolvedBindings: Sequence): Sequence { val arguments = resolvedCall.valueArguments if (arguments.size != 1) { - return emptySequence() + TODO() } - val varargArgument = arguments.values.first() - val argumentTypes = varargArgument.arguments.map { trace.getType(it.getArgumentExpression()!!)!! } + val argumentTypes = resolvedCall.varargArguments().map { trace.getType(it.getArgumentExpression()!!)!! } return resolvedBindings + argumentTypes.map { Binding.Instance(it) } } } diff --git a/src/main/kotlin/Client.kt b/src/main/kotlin/Client.kt index 1efb6fa..92b8b4e 100644 --- a/src/main/kotlin/Client.kt +++ b/src/main/kotlin/Client.kt @@ -3,9 +3,17 @@ import lib.component fun main() { val foo = component( ::Bar, + 1, + { it: Int -> it.toString() } + ) + + val foo1 = component( 0, { it: Int -> it.toString() } ) + + println(foo) + println(foo1) } data class Foo( @@ -17,3 +25,16 @@ data class Foo( data class Bar(val int: Int) fun bar(): Int = 0 + + +fun qwe() { + +// component( +// bind(0), +// bindF { foo: Foo, s: String -> Bar(foo, s) }, +// bindF(::lol) +// ) +} + +fun bind(t: T): T = TODO() +fun bindF(r: Any?): T = TODO() From 4ab280a5f1210e25d9f52fc0acb6ab4ecae140e0 Mon Sep 17 00:00:00 2001 From: Andrei Shikov Date: Fri, 20 Sep 2019 23:46:07 +0100 Subject: [PATCH 11/12] Experiment with syntax --- PROPOSAL.md | 4 +-- .../java/me/shika/di/DiCompilerCallChecker.kt | 28 +++++++++++++++++++ .../main/java/me/shika/di/resolver/names.kt | 7 +++++ src/main/kotlin/Client.kt | 24 ++++++---------- src/main/kotlin/lib/Library.kt | 11 +++++++- 5 files changed, 56 insertions(+), 18 deletions(-) diff --git a/PROPOSAL.md b/PROPOSAL.md index 7d84201..d915f56 100644 --- a/PROPOSAL.md +++ b/PROPOSAL.md @@ -63,12 +63,12 @@ Food for thought: Field injection: ```kotlin // Library -interface Injectable { +interface Injectable { fun inject(): ReadOnlyProperty, R> = // delegate } // Client -class SomeClass : Injectable { +class SomeClass : Injectable { val someField: Foo by inject() // Can we make it compile time safe? } ``` diff --git a/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/DiCompilerCallChecker.kt b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/DiCompilerCallChecker.kt index 39c5699..a7c3e17 100644 --- a/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/DiCompilerCallChecker.kt +++ b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/DiCompilerCallChecker.kt @@ -2,16 +2,44 @@ package me.shika.di import me.shika.di.resolver.COMPONENT_CALLS import me.shika.di.resolver.COMPONENT_FUN_FQ_NAME +import me.shika.di.resolver.MODULE_FUN_FQ_NAME +import me.shika.di.resolver.MODULE_GET +import me.shika.di.resolver.classDescriptor import org.jetbrains.kotlin.com.intellij.psi.PsiElement +import org.jetbrains.kotlin.descriptors.ValueParameterDescriptor +import org.jetbrains.kotlin.name.FqName +import org.jetbrains.kotlin.resolve.bindingContextUtil.getReferenceTargets import org.jetbrains.kotlin.resolve.calls.checkers.CallChecker import org.jetbrains.kotlin.resolve.calls.checkers.CallCheckerContext import org.jetbrains.kotlin.resolve.calls.model.ResolvedCall import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameSafe +import org.jetbrains.kotlin.resolve.scopes.receivers.ExpressionReceiver class DiCompilerCallChecker : CallChecker { override fun check(resolvedCall: ResolvedCall<*>, reportOn: PsiElement, context: CallCheckerContext) { + val bContext = context.trace.bindingContext if (resolvedCall.candidateDescriptor.fqNameSafe == COMPONENT_FUN_FQ_NAME) { context.trace.record(COMPONENT_CALLS, resolvedCall) } + + if (resolvedCall.candidateDescriptor.fqNameSafe == MODULE_FUN_FQ_NAME) { + + } + + if (resolvedCall.candidateDescriptor.fqNameSafe == MODULE_GET) { + val receiver = (resolvedCall.extensionReceiver as ExpressionReceiver).expression + val receiverRef = receiver.getReferenceTargets(bContext) + } + + if (resolvedCall.hasModuleArgument()) { + val modules = resolvedCall.valueArguments.filter { it.key.isModule() } + println(modules) + } } } + +fun ResolvedCall<*>.hasModuleArgument() = + valueArguments.keys.any { it.isModule() } + +fun ValueParameterDescriptor.isModule() = + type.classDescriptor()?.fqNameSafe == FqName("lib.Module") diff --git a/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/resolver/names.kt b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/resolver/names.kt index f056f06..8a312c7 100644 --- a/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/resolver/names.kt +++ b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/resolver/names.kt @@ -1,5 +1,6 @@ package me.shika.di.resolver +import org.jetbrains.kotlin.descriptors.DeclarationDescriptor import org.jetbrains.kotlin.name.FqName import org.jetbrains.kotlin.resolve.calls.model.ResolvedCall import org.jetbrains.kotlin.util.slicedMap.Slices @@ -8,4 +9,10 @@ import org.jetbrains.kotlin.util.slicedMap.WritableSlice val COMPONENT_CALLS: WritableSlice, Boolean> = Slices.createCollectiveSetSlice>() val GENERATED_CALL_NAME: WritableSlice, FqName> = Slices.createSimpleSlice() +val MODULE_CALL_USAGES: WritableSlice>> = Slices.createSimpleSlice() +val MODULE_TYPE_RETRIEVAL: WritableSlice>> = Slices.createSimpleSlice() +val MODULE_DEFINITION: WritableSlice>> = Slices.createSimpleSlice() + internal val COMPONENT_FUN_FQ_NAME = FqName("lib.component") +internal val MODULE_FUN_FQ_NAME = FqName("lib.module") +internal val MODULE_GET = FqName("lib.get") diff --git a/src/main/kotlin/Client.kt b/src/main/kotlin/Client.kt index 92b8b4e..8d36d13 100644 --- a/src/main/kotlin/Client.kt +++ b/src/main/kotlin/Client.kt @@ -1,4 +1,9 @@ +import lib.Module +import lib.bind import lib.component +import lib.get +import lib.module +import lib.with fun main() { val foo = component( @@ -14,6 +19,10 @@ fun main() { println(foo) println(foo1) + + component( + bind() with { int: Int -> } + ) } data class Foo( @@ -23,18 +32,3 @@ data class Foo( ) data class Bar(val int: Int) - -fun bar(): Int = 0 - - -fun qwe() { - -// component( -// bind(0), -// bindF { foo: Foo, s: String -> Bar(foo, s) }, -// bindF(::lol) -// ) -} - -fun bind(t: T): T = TODO() -fun bindF(r: Any?): T = TODO() diff --git a/src/main/kotlin/lib/Library.kt b/src/main/kotlin/lib/Library.kt index 8732575..218228b 100644 --- a/src/main/kotlin/lib/Library.kt +++ b/src/main/kotlin/lib/Library.kt @@ -1,3 +1,12 @@ package lib -fun component(vararg dependencies: Any?): T = TODO("Should be generated") +fun component(vararg dependencies: Binding<*>): T = TODO("Should be generated") + +interface Binding +interface Instance : Binding +interface Provider : Binding + +fun bind(t: T): T = TODO() +fun bind(): Binding = TODO() +infix fun Binding.with(factory: R): Provider = TODO() +typealias Factory = () -> T From 34158ecf3988208e024ca0dd383c8ddb8123f7f3 Mon Sep 17 00:00:00 2001 From: Andrei Shikov Date: Thu, 24 Oct 2019 00:00:24 +0100 Subject: [PATCH 12/12] Some updates on proposal for compiler plugin --- PROPOSAL.md | 31 ++++++++++++++++++++++--------- 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/PROPOSAL.md b/PROPOSAL.md index d915f56..a1f7958 100644 --- a/PROPOSAL.md +++ b/PROPOSAL.md @@ -1,6 +1,6 @@ This note explores compile-time "safe" DI container implementation relying on language itself, as opposed to annotation processing. -The topics to explore for MVP: +Topics to explore for MVP: 1. Exposing dependencies from container 2. Wiring(binding) dependency graph 3. Injection (constructor and instance fields) @@ -21,23 +21,36 @@ class FooComponent( // or interface FooComponent { + // scoped val bar: Bar - val otherDependency: Other.Dependency + // unscoped + fun otherDependency(): Other.Dependency } ``` Component definition can be done using Kotlin DSL and compiler transformations. `TODO: provide more details` ```kotlin // Library -fun component(vararg dependencies: Binding<*>): T = TODO() -fun module(definition: () -> Array>): Array +fun component(vararg dependencies: Any?): T = TODO() +fun modules(vararg modules: Module, block: () -> Unit) +interface Module // Marker // Client +object Module : k.Module { + fun providesInt(): Int = 0L +} + +class Module1() : k.Module { + fun bar(int: Int): Bar = Bar(int) +} + fun init() { - val fooComponent = component( - bind(barInstance), - bind(::bar1Provider), - *module(::fooBarModule) - ) + val long = 0 + modules(Module, Module1()) { // available as 'this' + val fooComponent = component( + instance(long), + instance(::bar1) + ) + } val instance: Bar = fooComponent.bar }