diff --git a/PROPOSAL.md b/PROPOSAL.md new file mode 100644 index 0000000..a1f7958 --- /dev/null +++ b/PROPOSAL.md @@ -0,0 +1,95 @@ +This note explores compile-time "safe" DI container implementation relying on language itself, as opposed to annotation processing. + +Topics to explore 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 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 +class FooComponent( + val bar: Bar, + val otherDependency: Other.Dependency +) + +// or + +interface FooComponent { + // scoped + val bar: Bar + // 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: 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 long = 0 + modules(Module, Module1()) { // available as 'this' + val fooComponent = component( + instance(long), + instance(::bar1) + ) + } + + 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. + +Food for thought: + - Is such "dynamic" definition providing better user experience rather than "static graph" that Dagger use? + +## Wiring + +`TODO: module definition` + +`TODO: explore macwire` + +`TODO: explore boost di` + +## 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..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..8bfeb2b --- /dev/null +++ b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/DiCodegenExtension.kt @@ -0,0 +1,67 @@ +package me.shika.di + +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.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? { + val isComponentCall = c.codegen.bindingContext[COMPONENT_CALLS, resolvedCall] == true + if (isComponentCall) { + val module = c.codegen.context.functionDescriptor.module + val function = module.findTopLevelFunctionForName(c.codegen.bindingContext[GENERATED_CALL_NAME, resolvedCall]!!)!! + val clsType = c.typeMapper.mapType(function.returnType!!) + + 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, + function, + 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(function.valueParameters[index], ExpressionValueArgument(valueArgument)) + } + + 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/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..9539e89 --- /dev/null +++ b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/DiCompilerAnalysisExtension.kt @@ -0,0 +1,78 @@ +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.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 +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.com.intellij.openapi.project.Project +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.jvm.extensions.AnalysisHandlerExtension +import org.jetbrains.kotlin.storage.LockBasedStorageManager +import java.io.File + +class DiCompilerAnalysisExtension( + private val sourcesDir: File +) : AnalysisHandlerExtension { + private var generatedFiles = false + + override fun analysisCompleted( + project: Project, + module: ModuleDescriptor, + bindingTrace: BindingTrace, + 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) { + return null + } + generatedFiles = true + + val processors = listOf( + ParseParameters(), + ExtractFunctions(), + ExtractAnonymousTypes(), + ReportBindingDuplicates() + ) + + calls.forEach { resolvedCall -> + 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) + + val fileSpec = GraphToFunctionRenderer(context).invoke(graph) + fileSpec.writeTo(sourcesDir) + } + return AnalysisResult.RetryWithAdditionalRoots( + bindingContext = bindingTrace.bindingContext, + moduleDescriptor = module, + additionalJavaRoots = emptyList(), + additionalKotlinRoots = listOf(sourcesDir) + ) // Repeat with my files pls + } +} 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..a7c3e17 --- /dev/null +++ b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/DiCompilerCallChecker.kt @@ -0,0 +1,45 @@ +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/test/DiCompilerComponentRegistrar.kt b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/DiCompilerComponentRegistrar.kt similarity index 53% 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..7e351b1 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,15 @@ -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.codegen.extensions.ExpressionCodegenExtension 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,17 +24,27 @@ 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) + DiCompilerAnalysisExtension(sourcesDir = sourcesDir) + ) + + 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 new file mode 100644 index 0000000..8ecb8e0 --- /dev/null +++ b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/DiCompilerStorageContributor.kt @@ -0,0 +1,18 @@ +package me.shika.di + +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 + +class DiCompilerStorageContributor : StorageComponentContainerContributor { + override fun registerModuleComponents( + container: StorageComponentContainer, + platform: TargetPlatform, + moduleDescriptor: ModuleDescriptor + ) { + container.useInstance(DiCompilerCallChecker()) + } +} + 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/di/model/resolution.kt b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/model/resolution.kt new file mode 100644 index 0000000..b4d81ea --- /dev/null +++ b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/model/resolution.kt @@ -0,0 +1,13 @@ +package me.shika.di.model + +import org.jetbrains.kotlin.types.KotlinType + +data class GraphNode(val value: Binding, val dependencies: List) + +sealed class Binding { + abstract val type: KotlinType + + 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/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..c16858f --- /dev/null +++ b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/render/GraphToFunctionRenderer.kt @@ -0,0 +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.resultType +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 + + 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(name) + .returns(returnType?.typeName()!!) + .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() + ) + } + } + } + + 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 new file mode 100644 index 0000000..de04a1d --- /dev/null +++ b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/render/utils.kt @@ -0,0 +1,58 @@ +package me.shika.di.render + +import com.squareup.kotlinpoet.ClassName +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.psi.ValueArgument +import org.jetbrains.kotlin.resolve.BindingTrace +import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameSafe +import org.jetbrains.kotlin.types.KotlinType + +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 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 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 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/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 new file mode 100644 index 0000000..13a6042 --- /dev/null +++ b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/resolver/ResolverContext.kt @@ -0,0 +1,11 @@ +package me.shika.di.resolver + +import org.jetbrains.kotlin.resolve.BindingTrace +import org.jetbrains.kotlin.resolve.calls.model.ResolvedCall +import org.jetbrains.kotlin.storage.StorageManager + +class ResolverContext( + val trace: BindingTrace, + 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..8a312c7 --- /dev/null +++ b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/resolver/names.kt @@ -0,0 +1,18 @@ +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 +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/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 new file mode 100644 index 0000000..8dc3c02 --- /dev/null +++ b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/di/resolver/utils.kt @@ -0,0 +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/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..13423c5 --- /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 +import me.shika.di.resolver.varargArguments + +class ParseParameters : DiBindingProcessor { + override fun ResolverContext.process(resolvedBindings: Sequence): Sequence { + val arguments = resolvedCall.valueArguments + if (arguments.size != 1) { + TODO() + } + + val argumentTypes = resolvedCall.varargArguments().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/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/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/test/dagger/DaggerBindingDescriptor.kt deleted file mode 100644 index 4bae451..0000000 --- a/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/test/dagger/DaggerBindingDescriptor.kt +++ /dev/null @@ -1,119 +0,0 @@ -package me.shika.test.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 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/test/dagger/DaggerBindingResolver.kt b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/test/dagger/DaggerBindingResolver.kt deleted file mode 100644 index 2ad1a36..0000000 --- a/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/test/dagger/DaggerBindingResolver.kt +++ /dev/null @@ -1,99 +0,0 @@ -package me.shika.test.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 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/test/dagger/DaggerComponentDescriptor.kt b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/test/dagger/DaggerComponentDescriptor.kt deleted file mode 100644 index 68365e1..0000000 --- a/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/test/dagger/DaggerComponentDescriptor.kt +++ /dev/null @@ -1,46 +0,0 @@ -package me.shika.test.dagger - -import me.shika.test.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/test/dagger/DaggerComponentRenderer.kt b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/test/dagger/DaggerComponentRenderer.kt deleted file mode 100644 index 49aff2b..0000000 --- a/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/test/dagger/DaggerComponentRenderer.kt +++ /dev/null @@ -1,137 +0,0 @@ -package me.shika.test.dagger - -import com.squareup.kotlinpoet.* -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 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/test/dagger/DaggerFactoryRenderer.kt b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/test/dagger/DaggerFactoryRenderer.kt deleted file mode 100644 index 3359e44..0000000 --- a/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/test/dagger/DaggerFactoryRenderer.kt +++ /dev/null @@ -1,141 +0,0 @@ -package me.shika.test.dagger - -import com.squareup.kotlinpoet.* -import me.shika.test.model.Binding -import me.shika.test.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/test/dagger/DaggerMembersInjectorRenderer.kt b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/test/dagger/DaggerMembersInjectorRenderer.kt deleted file mode 100644 index 3fe8937..0000000 --- a/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/test/dagger/DaggerMembersInjectorRenderer.kt +++ /dev/null @@ -1,81 +0,0 @@ -package me.shika.test.dagger - -import com.squareup.kotlinpoet.* -import me.shika.test.model.Endpoint -import me.shika.test.model.Injectable -import me.shika.test.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/test/dagger/utils.kt b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/test/dagger/utils.kt deleted file mode 100644 index 066ce40..0000000 --- a/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/test/dagger/utils.kt +++ /dev/null @@ -1,50 +0,0 @@ -package me.shika.test.dagger - -import com.squareup.kotlinpoet.* -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/test/model/resolution.kt b/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/test/model/resolution.kt deleted file mode 100644 index c456312..0000000 --- a/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/test/model/resolution.kt +++ /dev/null @@ -1,61 +0,0 @@ -package me.shika.test.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 - -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 - } - - val type get() = resolvedDescriptor.returnType -} 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/test/resolver/ResolverContext.kt deleted file mode 100644 index b3768e5..0000000 --- a/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/test/resolver/ResolverContext.kt +++ /dev/null @@ -1,11 +0,0 @@ -package me.shika.test.resolver - -import org.jetbrains.kotlin.descriptors.ModuleDescriptor -import org.jetbrains.kotlin.resolve.BindingTrace -import org.jetbrains.kotlin.resolve.lazy.ResolveSession - -class ResolverContext( - val module: ModuleDescriptor, - val trace: BindingTrace, - val resolveSession: ResolveSession -) 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/test/resolver/utils.kt deleted file mode 100644 index 0e9f35a..0000000 --- a/buildSrc/compiler-plugin/kotlin-plugin/src/main/java/me/shika/test/resolver/utils.kt +++ /dev/null @@ -1,6 +0,0 @@ -package me.shika.test.resolver - -import org.jetbrains.kotlin.descriptors.ClassDescriptor -import org.jetbrains.kotlin.types.KotlinType - -fun KotlinType.classDescriptor() = constructor.declarationDescriptor as? ClassDescriptor diff --git a/src/main/kotlin/Client.kt b/src/main/kotlin/Client.kt new file mode 100644 index 0000000..8d36d13 --- /dev/null +++ b/src/main/kotlin/Client.kt @@ -0,0 +1,34 @@ +import lib.Module +import lib.bind +import lib.component +import lib.get +import lib.module +import lib.with + +fun main() { + val foo = component( + ::Bar, + 1, + { it: Int -> it.toString() } + ) + + val foo1 = component( + 0, + { it: Int -> it.toString() } + ) + + println(foo) + println(foo1) + + component( + bind() with { int: Int -> } + ) +} + +data class Foo( + val bar: Bar, + val int: Int, + val string: String +) + +data class Bar(val int: Int) 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..218228b --- /dev/null +++ b/src/main/kotlin/lib/Library.kt @@ -0,0 +1,12 @@ +package lib + +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