From 23a46c1faec58e39630e6cfaf726f89781a780ff Mon Sep 17 00:00:00 2001
From: Josh Feinberg <15068619+joshafeinberg@users.noreply.github.com>
Date: Sat, 2 Dec 2023 22:09:08 -0600
Subject: [PATCH 1/7] Switch to using a processor based off the one currently
in circuit-code-gen
---
.idea/uiDesigner.xml | 124 +++
gradle/libs.versions.toml | 7 +
sample/build.gradle.kts | 1 +
.../circuitkotlininject/sample/Main.kt | 14 +-
.../sample/other/OtherScreen.kt | 30 +
ui-injector-annotations/build.gradle.kts | 5 +-
.../annotations/CircuitInject.kt | 2 +-
ui-injector-processor/build.gradle.kts | 9 +-
.../CircuitSymbolProcessorProvider.kt | 858 ++++++++++++++++++
.../processor/UiInjectorProcessor.kt | 39 +-
...processing.SymbolProcessorProvider.backup} | 0
11 files changed, 1070 insertions(+), 19 deletions(-)
create mode 100644 .idea/uiDesigner.xml
create mode 100644 sample/src/main/java/com/joshafeinberg/circuitkotlininject/sample/other/OtherScreen.kt
create mode 100644 ui-injector-processor/src/main/kotlin/com/joshafeinberg/circuitkotlininject/processor/CircuitSymbolProcessorProvider.kt
rename ui-injector-processor/src/main/resources/META-INF/services/{com.google.devtools.ksp.processing.SymbolProcessorProvider => com.google.devtools.ksp.processing.SymbolProcessorProvider.backup} (100%)
diff --git a/.idea/uiDesigner.xml b/.idea/uiDesigner.xml
new file mode 100644
index 0000000..2b63946
--- /dev/null
+++ b/.idea/uiDesigner.xml
@@ -0,0 +1,124 @@
+
+
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+
+
+
+ -
+
+
+
+
+
+ -
+
+
+
+
+
+ -
+
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+
+
+ -
+
+
+ -
+
+
+
+
+
\ No newline at end of file
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index f1bf70a..f07980d 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,6 +1,8 @@
[versions]
+anvil = "2.4.8"
app-cash-sqldelight = "2.0.0"
circuit = "0.17.0"
+dagger = "2.49"
io-ktor = "2.3.6"
kotlinInject = "0.6.3"
kotlinpoet = "1.15.2"
@@ -12,7 +14,12 @@ plugin-ktlint = "11.3.2"
plugin-mavenpublish = "0.21.0"
[libraries]
+anvil-annotations = { module = "com.squareup.anvil:annotations", version.ref = "anvil" }
+autoService-annotations = { module = "com.google.auto.service:auto-service-annotations", version = "1.1.1" }
+autoService-ksp = { module = "dev.zacsweers.autoservice:auto-service-ksp", version = "1.1.0" }
+circuit-codegen-annotations = { module = "com.slack.circuit:circuit-codegen-annotations", version.ref = "circuit" }
circuit-foundation = { module = "com.slack.circuit:circuit-foundation", version.ref = "circuit" }
+dagger = { module = "com.google.dagger:dagger", version.ref = "dagger" }
kotlin-inject-compiler-ksp = { module = "me.tatarka.inject:kotlin-inject-compiler-ksp", version.ref = "kotlinInject" }
kotlin-inject-runtime = { module = "me.tatarka.inject:kotlin-inject-runtime", version.ref = "kotlinInject" }
kotlinpoet = { module = "com.squareup:kotlinpoet", version.ref = "kotlinpoet" }
diff --git a/sample/build.gradle.kts b/sample/build.gradle.kts
index ca16f1f..6201378 100644
--- a/sample/build.gradle.kts
+++ b/sample/build.gradle.kts
@@ -22,4 +22,5 @@ dependencies {
ksp {
arg("circuit.codegen.package", "com.joshafeinberg.circuitkotlininject.sample")
arg("circuit.codegen.parent.component", "com.joshafeinberg.circuitkotlininject.sample.ParentComponent")
+ arg("circuit.codegen.mode", "kotlin_inject")
}
\ No newline at end of file
diff --git a/sample/src/main/java/com/joshafeinberg/circuitkotlininject/sample/Main.kt b/sample/src/main/java/com/joshafeinberg/circuitkotlininject/sample/Main.kt
index d2f1652..304090e 100644
--- a/sample/src/main/java/com/joshafeinberg/circuitkotlininject/sample/Main.kt
+++ b/sample/src/main/java/com/joshafeinberg/circuitkotlininject/sample/Main.kt
@@ -6,14 +6,15 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application
-import com.joshafeinberg.circuitkotlininject.processors.annotations.CircuitInject
+import com.joshafeinberg.circuitkotlininject.annotations.CircuitInject
+import com.slack.circuit.foundation.Circuit
import com.slack.circuit.foundation.CircuitCompositionLocals
import com.slack.circuit.foundation.CircuitContent
import com.slack.circuit.runtime.CircuitUiState
import com.slack.circuit.runtime.presenter.Presenter
import com.slack.circuit.runtime.screen.Screen
-import me.tatarka.inject.annotations.Component
-import me.tatarka.inject.annotations.Provides
+import com.slack.circuit.runtime.ui.Ui
+import me.tatarka.inject.annotations.*
fun main() = application {
val parentComponent = remember { ParentComponent::class.create() }
@@ -40,7 +41,10 @@ abstract class ParentComponent {
}
-@CircuitInject(MyScreen::class, MyScreen.MyScreenState::class)
+@Scope
+annotation class AppScope
+
+@com.slack.circuit.codegen.annotations.CircuitInject(MyScreen::class, AppScope::class)
@Composable
fun MyScreen(state: MyScreen.MyScreenState, modifier: Modifier) {
Text(state.visibleString)
@@ -54,7 +58,7 @@ data object MyScreen : Screen {
}
-@CircuitInject(MyScreen::class, MyScreen.MyScreenState::class)
+@com.slack.circuit.codegen.annotations.CircuitInject(MyScreen::class, AppScope::class)
class MyScreenPresenter(private val injectedString: String) : Presenter {
@Composable
override fun present(): MyScreen.MyScreenState {
diff --git a/sample/src/main/java/com/joshafeinberg/circuitkotlininject/sample/other/OtherScreen.kt b/sample/src/main/java/com/joshafeinberg/circuitkotlininject/sample/other/OtherScreen.kt
new file mode 100644
index 0000000..ea7a201
--- /dev/null
+++ b/sample/src/main/java/com/joshafeinberg/circuitkotlininject/sample/other/OtherScreen.kt
@@ -0,0 +1,30 @@
+package com.joshafeinberg.circuitkotlininject.sample.other
+
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import com.joshafeinberg.circuitkotlininject.sample.AppScope
+import com.slack.circuit.runtime.CircuitUiState
+import com.slack.circuit.runtime.presenter.Presenter
+import com.slack.circuit.runtime.screen.Screen
+
+@com.slack.circuit.codegen.annotations.CircuitInject(OtherScreen::class, AppScope::class)
+@Composable
+fun OtherScreen(modifier: Modifier) {
+ Text("Other Screen")
+}
+
+data object OtherScreen : Screen {
+
+ data object OtherScreenState : CircuitUiState
+
+}
+
+@com.slack.circuit.codegen.annotations.CircuitInject(OtherScreen::class, AppScope::class)
+class OtherScreenPresenter(private val injectedString: String) : Presenter {
+ @Composable
+ override fun present(): OtherScreen.OtherScreenState {
+ return OtherScreen.OtherScreenState
+ }
+
+}
\ No newline at end of file
diff --git a/ui-injector-annotations/build.gradle.kts b/ui-injector-annotations/build.gradle.kts
index 4876c17..625aa43 100644
--- a/ui-injector-annotations/build.gradle.kts
+++ b/ui-injector-annotations/build.gradle.kts
@@ -7,13 +7,14 @@ kotlin {
jvm {
jvmToolchain(17)
}
- js(IR) {
+ /*js(IR) {
browser()
- }
+ }*/
sourceSets {
commonMain {
dependencies {
compileOnly(libs.circuit.foundation)
+ api(libs.circuit.codegen.annotations)
}
}
}
diff --git a/ui-injector-annotations/src/commonMain/kotlin/com/joshafeinberg/circuitkotlininject/annotations/CircuitInject.kt b/ui-injector-annotations/src/commonMain/kotlin/com/joshafeinberg/circuitkotlininject/annotations/CircuitInject.kt
index 577323e..c183f2a 100644
--- a/ui-injector-annotations/src/commonMain/kotlin/com/joshafeinberg/circuitkotlininject/annotations/CircuitInject.kt
+++ b/ui-injector-annotations/src/commonMain/kotlin/com/joshafeinberg/circuitkotlininject/annotations/CircuitInject.kt
@@ -5,4 +5,4 @@ import com.slack.circuit.runtime.screen.Screen
import kotlin.reflect.KClass
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS)
-annotation class CircuitInject(val screen: KClass, val state: KClass)
+annotation class CircuitInject(val screen: KClass)
diff --git a/ui-injector-processor/build.gradle.kts b/ui-injector-processor/build.gradle.kts
index 7a3bf2e..30e8600 100644
--- a/ui-injector-processor/build.gradle.kts
+++ b/ui-injector-processor/build.gradle.kts
@@ -1,6 +1,7 @@
plugins {
kotlin("jvm")
- alias(libs.plugins.mavenpublish)
+ alias(libs.plugins.ksp)
+ // alias(libs.plugins.mavenpublish)
}
dependencies {
@@ -8,4 +9,10 @@ dependencies {
implementation(libs.ksp.processor)
implementation(libs.kotlinpoet)
implementation(libs.kotlinpoet.ksp)
+ implementation(libs.dagger)
+ implementation(libs.anvil.annotations)
+ implementation(libs.kotlin.inject.runtime)
+ implementation(libs.autoService.annotations)
+
+ ksp(libs.autoService.ksp)
}
diff --git a/ui-injector-processor/src/main/kotlin/com/joshafeinberg/circuitkotlininject/processor/CircuitSymbolProcessorProvider.kt b/ui-injector-processor/src/main/kotlin/com/joshafeinberg/circuitkotlininject/processor/CircuitSymbolProcessorProvider.kt
new file mode 100644
index 0000000..03367e2
--- /dev/null
+++ b/ui-injector-processor/src/main/kotlin/com/joshafeinberg/circuitkotlininject/processor/CircuitSymbolProcessorProvider.kt
@@ -0,0 +1,858 @@
+// Copyright (C) 2022 Slack Technologies, LLC
+// SPDX-License-Identifier: Apache-2.0
+@file:Suppress("UnsafeCallOnNullableType")
+
+package com.slack.circuit.codegen
+
+import com.google.auto.service.AutoService
+import com.google.devtools.ksp.KspExperimental
+import com.google.devtools.ksp.containingFile
+import com.google.devtools.ksp.getAllSuperTypes
+import com.google.devtools.ksp.getVisibility
+import com.google.devtools.ksp.isAnnotationPresent
+import com.google.devtools.ksp.processing.*
+import com.google.devtools.ksp.symbol.*
+import com.squareup.anvil.annotations.ContributesMultibinding
+import com.squareup.kotlinpoet.*
+import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
+import com.squareup.kotlinpoet.ksp.addOriginatingKSFile
+import com.squareup.kotlinpoet.ksp.toClassName
+import com.squareup.kotlinpoet.ksp.toTypeName
+import com.squareup.kotlinpoet.ksp.writeTo
+import dagger.assisted.AssistedFactory
+import java.util.Locale
+import javax.inject.Inject
+import kotlin.reflect.KClass
+
+private const val CIRCUIT_RUNTIME_BASE_PACKAGE = "com.slack.circuit.runtime"
+private const val DAGGER_PACKAGE = "dagger"
+private const val DAGGER_HILT_PACKAGE = "$DAGGER_PACKAGE.hilt"
+private const val DAGGER_HILT_CODEGEN_PACKAGE = "$DAGGER_HILT_PACKAGE.codegen"
+private const val DAGGER_MULTIBINDINGS_PACKAGE = "$DAGGER_PACKAGE.multibindings"
+private const val CIRCUIT_RUNTIME_UI_PACKAGE = "$CIRCUIT_RUNTIME_BASE_PACKAGE.ui"
+private const val CIRCUIT_RUNTIME_SCREEN_PACKAGE = "$CIRCUIT_RUNTIME_BASE_PACKAGE.screen"
+private const val CIRCUIT_RUNTIME_PRESENTER_PACKAGE = "$CIRCUIT_RUNTIME_BASE_PACKAGE.presenter"
+private const val KOTLIN_INJECT_BASE_PACKAGE = "me.tatarka.inject.annotations"
+private val MODIFIER = ClassName("androidx.compose.ui", "Modifier")
+private val CIRCUIT_INJECT_ANNOTATION =
+ ClassName("com.slack.circuit.codegen.annotations", "CircuitInject")
+private val CIRCUIT = ClassName("com.slack.circuit.foundation", "Circuit")
+private val CIRCUIT_PRESENTER = ClassName(CIRCUIT_RUNTIME_PRESENTER_PACKAGE, "Presenter")
+private val CIRCUIT_PRESENTER_FACTORY = CIRCUIT_PRESENTER.nestedClass("Factory")
+private val CIRCUIT_UI = ClassName(CIRCUIT_RUNTIME_UI_PACKAGE, "Ui")
+private val CIRCUIT_UI_FACTORY = CIRCUIT_UI.nestedClass("Factory")
+private val CIRCUIT_UI_STATE = ClassName(CIRCUIT_RUNTIME_BASE_PACKAGE, "CircuitUiState")
+private val SCREEN = ClassName(CIRCUIT_RUNTIME_SCREEN_PACKAGE, "Screen")
+private val NAVIGATOR = ClassName(CIRCUIT_RUNTIME_BASE_PACKAGE, "Navigator")
+private val CIRCUIT_CONTEXT = ClassName(CIRCUIT_RUNTIME_BASE_PACKAGE, "CircuitContext")
+private val DAGGER_MODULE = ClassName(DAGGER_PACKAGE, "Module")
+private val DAGGER_BINDS = ClassName(DAGGER_PACKAGE, "Binds")
+private val DAGGER_INSTALL_IN = ClassName(DAGGER_HILT_PACKAGE, "InstallIn")
+private val DAGGER_ORIGINATING_ELEMENT =
+ ClassName(DAGGER_HILT_CODEGEN_PACKAGE, "OriginatingElement")
+private val DAGGER_INTO_SET = ClassName(DAGGER_MULTIBINDINGS_PACKAGE, "IntoSet")
+private val KOTLIN_INJECT_COMPONENT = ClassName(KOTLIN_INJECT_BASE_PACKAGE, "Component")
+private val KOTLIN_INJECT_INTO_SET = ClassName(KOTLIN_INJECT_BASE_PACKAGE, "IntoSet")
+private val KOTLIN_INJECT_PROVIDES = ClassName(KOTLIN_INJECT_BASE_PACKAGE, "Provides")
+private const val MODULE = "Module"
+private const val FACTORY = "Factory"
+private const val CIRCUIT_CODEGEN_MODE = "circuit.codegen.mode"
+private const val CIRCUIT_COMPONENT_PACKAGE = "circuit.codegen.package"
+private const val CIRCUIT_PARENT_COMPONENT = "circuit.codegen.parent.component"
+
+@AutoService(SymbolProcessorProvider::class)
+public class CircuitSymbolProcessorProvider : SymbolProcessorProvider {
+ override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor {
+ return CircuitSymbolProcessor(
+ environment.logger,
+ environment.codeGenerator,
+ environment.options,
+ environment.platforms
+ )
+ }
+}
+
+private class CircuitSymbols private constructor(resolver: Resolver) {
+ val modifier = resolver.loadKSType(MODIFIER.canonicalName)
+ val circuitUiState = resolver.loadKSType(CIRCUIT_UI_STATE.canonicalName)
+ val screen = resolver.loadKSType(SCREEN.canonicalName)
+ val navigator = resolver.loadKSType(NAVIGATOR.canonicalName)
+
+ companion object {
+ fun create(resolver: Resolver): CircuitSymbols? {
+ @Suppress("SwallowedException")
+ return try {
+ CircuitSymbols(resolver)
+ } catch (e: IllegalStateException) {
+ null
+ }
+ }
+ }
+}
+
+private fun Resolver.loadKSType(name: String): KSType =
+ loadOptionalKSType(name) ?: error("Could not find $name in classpath")
+
+private fun Resolver.loadOptionalKSType(name: String?): KSType? {
+ if (name == null) return null
+ return getClassDeclarationByName(getKSNameFromString(name))?.asType(emptyList())
+}
+
+private class CircuitSymbolProcessor(
+ private val logger: KSPLogger,
+ private val codeGenerator: CodeGenerator,
+ private val options: Map,
+ private val platforms: List,
+) : SymbolProcessor {
+
+ // todo - this is bad
+ private val bindMethodsToCreate = mutableListOf()
+
+ override fun process(resolver: Resolver): List {
+ val symbols = CircuitSymbols.create(resolver) ?: return emptyList()
+ val codegenMode =
+ options[CIRCUIT_CODEGEN_MODE].let { mode ->
+ if (mode == null) {
+ CodegenMode.ANVIL
+ } else {
+ CodegenMode.entries.find { it.name.equals(mode, ignoreCase = true) }
+ ?: run {
+ logger.error("Unrecognised option for codegen mode \"$mode\".")
+ return emptyList()
+ }
+ }
+ }
+
+ if (!codegenMode.supportsPlatforms(platforms)) {
+ logger.error("Unsupported platforms for codegen mode ${codegenMode.name}. $platforms")
+ return emptyList()
+ }
+
+ val componentPackage = options[CIRCUIT_COMPONENT_PACKAGE]
+ if (componentPackage == null) {
+ logger.error("Should set component's package")
+ }
+
+ val parentComponent = options[CIRCUIT_PARENT_COMPONENT]?.let {
+ ClassName.bestGuess(it.trim())
+ }
+
+ resolver.getSymbolsWithAnnotation(CIRCUIT_INJECT_ANNOTATION.canonicalName).forEach {
+ annotatedElement ->
+ when (annotatedElement) {
+ is KSClassDeclaration ->
+ generateFactory(annotatedElement, InstantiationType.CLASS, symbols, codegenMode)
+ is KSFunctionDeclaration ->
+ generateFactory(annotatedElement, InstantiationType.FUNCTION, symbols, codegenMode)
+ else ->
+ logger.error(
+ "CircuitInject is only applicable on classes and functions.",
+ annotatedElement
+ )
+ }
+ }
+
+ createComponentFile(
+ bindMethodsToCreate,
+ componentPackage,
+ parentComponent?.let { listOf(it) }
+ )
+
+
+ return emptyList()
+ }
+
+ private fun generateFactory(
+ annotatedElement: KSAnnotated,
+ instantiationType: InstantiationType,
+ symbols: CircuitSymbols,
+ codegenMode: CodegenMode,
+ ) {
+ val circuitInjectAnnotation =
+ annotatedElement.annotations.first {
+ it.annotationType.resolve().declaration.qualifiedName?.asString() ==
+ CIRCUIT_INJECT_ANNOTATION.canonicalName
+ }
+ val screenKSType = circuitInjectAnnotation.arguments[0].value as KSType
+ val screenIsObject =
+ screenKSType.declaration.let { it is KSClassDeclaration && it.classKind == ClassKind.OBJECT }
+ val screenType = screenKSType.toTypeName()
+ val scope = (circuitInjectAnnotation.arguments[1].value as KSType).toTypeName()
+
+ val factoryData =
+ computeFactoryData(annotatedElement, symbols, screenKSType, instantiationType, codegenMode, logger)
+ ?: return
+
+ val className =
+ factoryData.className.replaceFirstChar { char ->
+ char.takeIf { char.isLowerCase() }?.run { uppercase(Locale.getDefault()) }
+ ?: char.toString()
+ }
+
+ val builder =
+ TypeSpec.classBuilder(className + FACTORY)
+ .primaryConstructor(
+ FunSpec.constructorBuilder()
+ .addAnnotation(codegenMode.injectAnnotation())
+ .addParameters(factoryData.constructorParams)
+ .build()
+ )
+ .apply {
+ if (factoryData.constructorParams.isNotEmpty()) {
+ for (param in factoryData.constructorParams) {
+ addProperty(
+ PropertySpec.builder(param.name, param.type, KModifier.PRIVATE)
+ .initializer(param.name)
+ .build()
+ )
+ }
+ }
+
+ codegenMode.annotateFactory(builder = this, scope = scope)
+ }
+ val screenBranch =
+ if (screenIsObject) {
+ CodeBlock.of("%T", screenType)
+ } else {
+ CodeBlock.of("is·%T", screenType)
+ }
+ val typeSpec =
+ when (factoryData.factoryType) {
+ FactoryType.PRESENTER ->
+ builder.buildPresenterFactory(annotatedElement, screenBranch, factoryData.codeBlock)
+ FactoryType.UI ->
+ builder.buildUiFactory(annotatedElement, screenBranch, factoryData.codeBlock)
+ }
+
+ // Note: We can't directly reference the top-level class declaration for top-level functions in
+ // kotlin. For annotatedElements which as top-level functions, topLevelClass will be null.
+ val topLevelDeclaration = (annotatedElement as KSDeclaration).topLevelDeclaration()
+ val topLevelClass = (topLevelDeclaration as? KSClassDeclaration)?.toClassName()
+
+ val originatingFile = listOfNotNull(annotatedElement.containingFile)
+
+ bindMethodsToCreate.add(
+ KotlinInjectBindingResult(
+ className + FACTORY,
+ factoryData.packageName,
+ factoryData.factoryType,
+ originatingFile
+ )
+ )
+
+ FileSpec.get(factoryData.packageName, typeSpec)
+ .writeTo(
+ codeGenerator = codeGenerator,
+ aggregating = false,
+ originatingKSFiles = originatingFile
+ )
+
+ val additionalType =
+ codegenMode.produceAdditionalTypeSpec(
+ factoryType = factoryData.factoryType,
+ factory = ClassName(factoryData.packageName, className + FACTORY),
+ scope = scope,
+ topLevelClass = topLevelClass
+ ) ?: return
+
+ FileSpec.get(factoryData.packageName, additionalType)
+ .writeTo(
+ codeGenerator = codeGenerator,
+ aggregating = false,
+ originatingKSFiles = originatingFile
+ )
+ }
+
+ private data class FactoryData(
+ val className: String,
+ val packageName: String,
+ val factoryType: FactoryType,
+ val constructorParams: List,
+ val codeBlock: CodeBlock,
+ )
+
+ /** Computes the data needed to generate a factory. */
+ // Detekt and ktfmt don't agree on whether or not the rectangle rule makes for readable code.
+ @Suppress("ComplexMethod", "LongMethod", "ReturnCount")
+ @OptIn(KspExperimental::class)
+ private fun computeFactoryData(
+ annotatedElement: KSAnnotated,
+ symbols: CircuitSymbols,
+ screenKSType: KSType,
+ instantiationType: InstantiationType,
+ codegenMode: CodegenMode,
+ logger: KSPLogger,
+ ): FactoryData? {
+ val className: String
+ val packageName: String
+ val factoryType: FactoryType
+ val constructorParams = mutableListOf()
+ val codeBlock: CodeBlock
+
+ when (instantiationType) {
+ InstantiationType.FUNCTION -> {
+ val fd = annotatedElement as KSFunctionDeclaration
+ fd.checkVisibility(logger) {
+ return null
+ }
+ val name = annotatedElement.simpleName.getShortName()
+ className = name
+ packageName = fd.packageName.asString()
+ factoryType =
+ if (name.endsWith("Presenter")) {
+ FactoryType.PRESENTER
+ } else {
+ FactoryType.UI
+ }
+ val assistedParams =
+ fd.assistedParameters(symbols, logger, screenKSType, codegenMode, factoryType == FactoryType.PRESENTER)
+ codeBlock =
+ when (factoryType) {
+ FactoryType.PRESENTER ->
+ CodeBlock.of(
+ "%M·{·%M(%L)·}",
+ MemberName(CIRCUIT_RUNTIME_PRESENTER_PACKAGE, "presenterOf"),
+ MemberName(packageName, name),
+ assistedParams
+ )
+ FactoryType.UI -> {
+ // State param is optional
+ val stateParam =
+ fd.parameters.singleOrNull { parameter ->
+ symbols.circuitUiState.isAssignableFrom(parameter.type.resolve())
+ }
+
+ // Modifier param is required
+ val modifierParam =
+ fd.parameters.singleOrNull { parameter ->
+ symbols.modifier.isAssignableFrom(parameter.type.resolve())
+ }
+ ?: run {
+ logger.error("UI composable functions must have a Modifier parameter!", fd)
+ return null
+ }
+
+ /*
+ Diagram of what goes into generating a function!
+ - State parameter is _optional_ and can be omitted if it's static state.
+ - When omitted, the argument becomes _ and the param is omitted entirely.
+ - is either the State or CircuitUiState if no state param is used.
+ - Modifier parameter is required.
+ - Assisted parameters can be 0 or more extra supported assisted parameters.
+
+ Optional state param
+ Optional state arg │
+ │ │ Required modifier param
+ │ Req modifier arg │ │
+ ┌─── ui function │ │ │ │ Any assisted params
+ │ │ │ Composable │ │ │
+ │ State type │ │ │ │ │ │
+ │ │ │ │ │ │ │ │
+ │ │ │ │ │ │ │ │
+ └──────┴───── ──┴── ───┴──── ──┴───── ───────┴───── ────────┴────────── ────────┴────────
+ ui { state, modifier -> Function(state = state, modifier = modifier, ) }
+ ────────────────────────────────────────────────────────────────────────────────────────────────────
+
+ Diagram generated with asciiflow. You can make new ones or edit with this link.
+ https://asciiflow.com/#/share/eJzVVM1KxDAQfpVhTgr1IizLlt2CCF4F9ZhLdGclkKbd%2FMCW0rfwcXwan8SsWW27sd0qXixzmDTffN83bSY1Kp4TpspJmaDkFWlMsWa4Y5guZvOEYeWzy%2FnCZ5Z21i8Ywg%2Be29KKQnEJxnJLUHLNc8bUKIjr55jo7eXVR1R62Dplo4Xc0dYJTWvIi7XYCNIDnnpVvqjF9%2BzF2sFlsBvCCdg49bTvsVcx4vtb2u7ySlXAjRHG%2BlY%2BOjBBdYSqza6LvCwMf5Q0VS%2FqDjq%2FzVYlDfV1zPMrpWHSf6w1JeDz4HfejGCH9ybrTQT%2BUan%2FEk4s7%2Fdj%2F%2BAPUQZ1uAOSdtwuMrg5TM9ZuB9WEWb1lSawPBqL7Bwahg027%2Byjz8s%3D)
+ */
+
+ @Suppress("IfThenToElvis") // The elvis is less readable here
+ val stateType =
+ if (stateParam == null) CIRCUIT_UI_STATE else stateParam.type.resolve().toTypeName()
+ val stateArg = if (stateParam == null) "_" else "state"
+ val stateParamBlock =
+ if (stateParam == null) CodeBlock.of("")
+ else CodeBlock.of("%L·=·state,·", stateParam.name!!.getShortName())
+ val modifierParamBlock =
+ CodeBlock.of("%L·=·modifier", modifierParam.name!!.getShortName())
+ val assistedParamsBlock =
+ if (assistedParams.isEmpty()) {
+ CodeBlock.of("")
+ } else {
+ CodeBlock.of(",·%L", assistedParams)
+ }
+ CodeBlock.of(
+ "%M<%T>·{·%L,·modifier·->·%M(%L%L%L)·}",
+ MemberName(CIRCUIT_RUNTIME_UI_PACKAGE, "ui"),
+ stateType,
+ stateArg,
+ MemberName(packageName, name),
+ stateParamBlock,
+ modifierParamBlock,
+ assistedParamsBlock
+ )
+ }
+ }
+ }
+ InstantiationType.CLASS -> {
+ val cd = annotatedElement as KSClassDeclaration
+ cd.checkVisibility(logger) {
+ return null
+ }
+ val isAssisted = cd.isAnnotationPresent(AssistedFactory::class)
+ val creatorOrConstructor: KSFunctionDeclaration?
+ val targetClass: KSClassDeclaration
+ if (isAssisted) {
+ val creatorFunction = cd.getAllFunctions().filter { it.isAbstract }.single()
+ creatorOrConstructor = creatorFunction
+ targetClass = creatorFunction.returnType!!.resolve().declaration as KSClassDeclaration
+ targetClass.checkVisibility(logger) {
+ return null
+ }
+ } else {
+ creatorOrConstructor = cd.primaryConstructor
+ targetClass = cd
+ }
+ val useProvider =
+ !isAssisted && creatorOrConstructor?.isAnnotationPresent(codegenMode.injectAnnotation()) == true
+ className = targetClass.simpleName.getShortName()
+ packageName = targetClass.packageName.asString()
+ factoryType =
+ targetClass
+ .getAllSuperTypes()
+ .mapNotNull {
+ when (it.declaration.qualifiedName?.asString()) {
+ CIRCUIT_UI.canonicalName -> FactoryType.UI
+ CIRCUIT_PRESENTER.canonicalName -> FactoryType.PRESENTER
+ else -> null
+ }
+ }
+ .firstOrNull()
+ ?: run {
+ logger.error(
+ "Factory must be for a UI or Presenter class, but was " +
+ "${targetClass.qualifiedName?.asString()}. " +
+ "Supertypes: ${targetClass.getAllSuperTypes().toList()}",
+ targetClass
+ )
+ return null
+ }
+ val assistedParams =
+ if (useProvider) {
+ // Nothing to do here, we'll just use the provider directly.
+ CodeBlock.of("")
+ } else {
+ creatorOrConstructor?.assistedParameters(
+ symbols,
+ logger,
+ screenKSType,
+ codegenMode,
+ allowNavigator = factoryType == FactoryType.PRESENTER
+ )
+ }
+ codeBlock =
+ if (useProvider) {
+ // Inject a Provider that we'll call get() on.
+ constructorParams.add(
+ ParameterSpec.builder(
+ "provider",
+ ClassName("javax.inject", "Provider")
+ .parameterizedBy(targetClass.toClassName())
+ )
+ .build()
+ )
+ CodeBlock.of("provider.get()")
+ } else if (isAssisted) {
+ // Inject the target class's assisted factory that we'll call its create() on.
+ constructorParams.add(ParameterSpec.builder("factory", cd.toClassName()).build())
+ CodeBlock.of(
+ "factory.%L(%L)",
+ creatorOrConstructor!!.simpleName.getShortName(),
+ assistedParams
+ )
+ } else {
+ if (codegenMode == CodegenMode.KOTLIN_INJECT) {
+ cd.primaryConstructor?.parameters?.forEach { parameter ->
+ constructorParams.add(ParameterSpec.builder(parameter.name!!.asString(), parameter.type.toTypeName()).build())
+ }
+ }
+ // Simple constructor call, no injection.
+ CodeBlock.of("%T(%L)", targetClass.toClassName(), assistedParams)
+ }
+ }
+ }
+ return FactoryData(className, packageName, factoryType, constructorParams, codeBlock)
+ }
+
+ private fun createComponentFile(
+ symbols: List,
+ componentPackage: String?,
+ parentClasses: List?
+ ) {
+ val bindMethods = symbols.map { bindingResult ->
+ if (bindingResult.factoryType == FactoryType.PRESENTER) {
+ PropertySpec.builder("bind",
+ CIRCUIT_PRESENTER_FACTORY, KModifier.PROTECTED)
+ .receiver(ClassName(bindingResult.factoryPackage, bindingResult.factoryName))
+ .getter(
+ FunSpec.getterBuilder()
+ .addAnnotation(KOTLIN_INJECT_PROVIDES)
+ .addAnnotation(KOTLIN_INJECT_INTO_SET)
+ .addStatement("return this")
+ .build(),
+ )
+ .build()
+ } else {
+ PropertySpec.builder("bind",
+ CIRCUIT_UI_FACTORY, KModifier.PROTECTED)
+ .receiver(ClassName(bindingResult.factoryPackage, bindingResult.factoryName))
+ .getter(
+ FunSpec.getterBuilder()
+ .addAnnotation(KOTLIN_INJECT_PROVIDES)
+ .addAnnotation(KOTLIN_INJECT_INTO_SET)
+ .addStatement("return this")
+ .build(),
+ )
+ .build()
+ }
+ }
+
+ val injectableCircuitProperty = PropertySpec.builder("circuit", CIRCUIT, KModifier.ABSTRACT)
+ .build()
+
+ val circuitProvider = FunSpec.builder("providesCircuit")
+ .addAnnotation(KOTLIN_INJECT_PROVIDES)
+ .returns(CIRCUIT)
+ .addParameter("uiFactories", Set::class.asClassName().parameterizedBy(CIRCUIT_UI_FACTORY))
+ .addParameter("presenterFactories", Set::class.asClassName().parameterizedBy(CIRCUIT_PRESENTER_FACTORY))
+ .addStatement("return %T.Builder().addUiFactories(uiFactories).addPresenterFactories(presenterFactories).build()", CIRCUIT)
+ .build()
+
+ val parameters = parentClasses?.map {
+ ParameterSpec.builder(it.simpleName.lowercase(Locale.getDefault()), it)
+ .build()
+ }
+
+ val properties = parentClasses?.map {
+ val name = it.simpleName.lowercase(Locale.getDefault())
+ PropertySpec.builder(name, it)
+ .addAnnotation(KOTLIN_INJECT_COMPONENT)
+ .initializer(name)
+ .build()
+ }
+
+ val classSpec = TypeSpec.classBuilder("CircuitComponent")
+ .addAnnotation(KOTLIN_INJECT_COMPONENT)
+ .addModifiers(KModifier.INTERNAL, KModifier.ABSTRACT)
+ .primaryConstructor(
+ FunSpec.constructorBuilder()
+ .apply {
+ parameters?.let {
+ addParameters(it)
+ }
+ }
+ .build(),
+ )
+ .apply {
+ properties?.let {
+ addProperties(it)
+ }
+ }
+ .addProperty(injectableCircuitProperty)
+ .addFunction(circuitProvider)
+ .addProperties(bindMethods.toList())
+ .build()
+
+ try {
+ FileSpec.builder(componentPackage ?: "", "CircuitComponent")
+ .addType(classSpec)
+ .build()
+ .writeTo(
+ codeGenerator,
+ Dependencies(false, *symbols.flatMap { it.originatingFiles }.toList().toTypedArray())
+ )
+ } catch (e: Exception) {
+ logger.warn("This is bad but I'll figure it out later")
+ }
+ }
+}
+
+private data class AssistedType(
+ val factoryName: String,
+ val type: TypeName,
+ val name: String,
+)
+
+/**
+ * Returns a [CodeBlock] representation of all named assisted parameters on this
+ * [KSFunctionDeclaration] to be used in generated invocation code.
+ *
+ * Example: this function
+ *
+ * ```kotlin
+ * @Composable
+ * fun HomePresenter(screen: Screen, navigator: Navigator)
+ * ```
+ *
+ * Yields this CodeBlock: `screen = screen, navigator = navigator`
+ */
+private fun KSFunctionDeclaration.assistedParameters(
+ symbols: CircuitSymbols,
+ logger: KSPLogger,
+ screenType: KSType,
+ codegenMode: CodegenMode,
+ allowNavigator: Boolean,
+): CodeBlock {
+ return buildSet {
+ for (param in parameters) {
+ fun MutableSet.addOrError(element: E) {
+ val added = add(element)
+ if (!added) {
+ logger.error("Multiple parameters of type $element are not allowed.", param)
+ }
+ }
+
+ val type = param.type.resolve()
+ when {
+ type.isInstanceOf(symbols.screen) -> {
+ if (screenType.isSameDeclarationAs(type)) {
+ addOrError(AssistedType("screen", type.toTypeName(), param.name!!.getShortName()))
+ } else {
+ logger.error("Screen type mismatch. Expected $screenType but found $type", param)
+ }
+ }
+ type.isInstanceOf(symbols.navigator) -> {
+ if (allowNavigator) {
+ addOrError(AssistedType("navigator", type.toTypeName(), param.name!!.getShortName()))
+ } else {
+ logger.error(
+ "Navigator type mismatch. Navigators are not injectable on this type.",
+ param
+ )
+ }
+ }
+ type.isInstanceOf(symbols.circuitUiState) ||
+ type.isInstanceOf(symbols.modifier) -> Unit
+ codegenMode == CodegenMode.KOTLIN_INJECT -> {
+ addOrError(AssistedType(param.name!!.asString(), param.type.resolve().toTypeName(), param.name!!.asString()))
+ }
+ }
+ }
+ }
+ .toList()
+ .map { CodeBlock.of("${it.name} = ${it.factoryName}") }
+ .joinToCode(",·")
+}
+
+private fun KSType.isSameDeclarationAs(type: KSType): Boolean {
+ return this.declaration == type.declaration
+}
+
+private fun KSType.isInstanceOf(type: KSType): Boolean {
+ return type.isAssignableFrom(this)
+}
+
+private fun TypeSpec.Builder.buildUiFactory(
+ originatingSymbol: KSAnnotated,
+ screenBranch: CodeBlock,
+ instantiationCodeBlock: CodeBlock,
+): TypeSpec {
+ return addSuperinterface(CIRCUIT_UI_FACTORY)
+ .addFunction(
+ FunSpec.builder("create")
+ .addModifiers(KModifier.OVERRIDE)
+ .addParameter("screen", SCREEN)
+ .addParameter("context", CIRCUIT_CONTEXT)
+ .returns(CIRCUIT_UI.parameterizedBy(STAR).copy(nullable = true))
+ .beginControlFlow("return·when·(screen)")
+ .addStatement("%L·->·%L", screenBranch, instantiationCodeBlock)
+ .addStatement("else·->·null")
+ .endControlFlow()
+ .build()
+ )
+ .addOriginatingKSFile(originatingSymbol.containingFile!!)
+ .build()
+}
+
+private fun TypeSpec.Builder.buildPresenterFactory(
+ originatingSymbol: KSAnnotated,
+ screenBranch: CodeBlock,
+ instantiationCodeBlock: CodeBlock,
+): TypeSpec {
+ // The TypeSpec below will generate something similar to the following.
+ // public class AboutPresenterFactory : Presenter.Factory {
+ // public override fun create(
+ // screen: Screen,
+ // navigator: Navigator,
+ // context: CircuitContext,
+ // ): Presenter<*>? = when (screen) {
+ // is AboutScreen -> AboutPresenter()
+ // is AboutScreen -> presenterOf { AboutPresenter() }
+ // else -> null
+ // }
+ // }
+
+ return addSuperinterface(CIRCUIT_PRESENTER_FACTORY)
+ .addFunction(
+ FunSpec.builder("create")
+ .addModifiers(KModifier.OVERRIDE)
+ .addParameter("screen", SCREEN)
+ .addParameter("navigator", NAVIGATOR)
+ .addParameter("context", CIRCUIT_CONTEXT)
+ .returns(CIRCUIT_PRESENTER.parameterizedBy(STAR).copy(nullable = true))
+ .beginControlFlow("return when (screen)")
+ .addStatement("%L·->·%L", screenBranch, instantiationCodeBlock)
+ .addStatement("else·->·null")
+ .endControlFlow()
+ .build()
+ )
+ .addOriginatingKSFile(originatingSymbol.containingFile!!)
+ .build()
+}
+
+private enum class FactoryType {
+ PRESENTER,
+ UI
+}
+
+private enum class InstantiationType {
+ FUNCTION,
+ CLASS
+}
+
+private enum class CodegenMode {
+ /**
+ * The Anvil Codegen mode
+ *
+ * This mode annotates generated factory types with [ContributesMultibinding], allowing for Anvil
+ * to automatically wire the generated class up to Dagger's multibinding system within a given
+ * scope (e.g. AppScope).
+ *
+ * ```kotlin
+ * @ContributesMultibinding(AppScope::class)
+ * public class FavoritesPresenterFactory @Inject constructor(
+ * private val factory: FavoritesPresenter.Factory,
+ * ) : Presenter.Factory { ... }
+ * ```
+ */
+ ANVIL {
+ override fun supportsPlatforms(platforms: List): Boolean {
+ // Anvil only supports JVM & Android
+ return platforms.all { it is JvmPlatformInfo }
+ }
+
+ override fun annotateFactory(builder: TypeSpec.Builder, scope: TypeName) {
+ builder.addAnnotation(
+ AnnotationSpec.builder(ContributesMultibinding::class).addMember("%T::class", scope).build()
+ )
+ }
+ },
+
+ /**
+ * The Hilt Codegen mode
+ *
+ * This mode provides an additional type, a Hilt module, which binds the generated factory, wiring
+ * up multibinding in the Hilt DI framework. The scope provided via [CircuitInject] is used to
+ * define the dagger component the factory provider is installed in.
+ *
+ * ```kotlin
+ * @Module
+ * @InstallIn(SingletonComponent::class)
+ * @OriginatingElement(topLevelClass = FavoritesPresenter::class)
+ * public abstract class FavoritesPresenterFactoryModule {
+ * @Binds
+ * @IntoSet
+ * public abstract
+ * fun bindFavoritesPresenterFactory(favoritesPresenterFactory: FavoritesPresenterFactory):
+ * Presenter.Factory
+ * }
+ * ```
+ */
+ HILT {
+ override fun supportsPlatforms(platforms: List): Boolean {
+ // Hilt only supports JVM & Android
+ return platforms.all { it is JvmPlatformInfo }
+ }
+
+ override fun produceAdditionalTypeSpec(
+ factory: ClassName,
+ factoryType: FactoryType,
+ scope: TypeName,
+ topLevelClass: ClassName?,
+ ): TypeSpec {
+ val moduleAnnotations =
+ listOfNotNull(
+ AnnotationSpec.builder(DAGGER_MODULE).build(),
+ AnnotationSpec.builder(DAGGER_INSTALL_IN).addMember("%T::class", scope).build(),
+ topLevelClass?.let {
+ AnnotationSpec.builder(DAGGER_ORIGINATING_ELEMENT)
+ .addMember("%L = %T::class", "topLevelClass", topLevelClass)
+ .build()
+ }
+ )
+
+ val providerAnnotations =
+ listOf(
+ AnnotationSpec.builder(DAGGER_BINDS).build(),
+ AnnotationSpec.builder(DAGGER_INTO_SET).build(),
+ )
+
+ val providerReturns =
+ if (factoryType == FactoryType.UI) {
+ CIRCUIT_UI_FACTORY
+ } else {
+ CIRCUIT_PRESENTER_FACTORY
+ }
+
+ val factoryName = factory.simpleName
+
+ val providerSpec =
+ FunSpec.builder("bind${factoryName}")
+ .addModifiers(KModifier.ABSTRACT)
+ .addAnnotations(providerAnnotations)
+ .addParameter(name = factoryName.replaceFirstChar { it.lowercase() }, type = factory)
+ .returns(providerReturns)
+ .build()
+
+ return TypeSpec.classBuilder(factory.peerClass(factoryName + MODULE))
+ .addModifiers(KModifier.ABSTRACT)
+ .addAnnotations(moduleAnnotations)
+ .addFunction(providerSpec)
+ .build()
+ }
+ },
+ KOTLIN_INJECT {
+ override fun supportsPlatforms(platforms: List): Boolean {
+ return true
+ }
+
+ override fun injectAnnotation(): KClass = me.tatarka.inject.annotations.Inject::class
+ };
+
+ open fun annotateFactory(builder: TypeSpec.Builder, scope: TypeName) {}
+
+ open fun produceAdditionalTypeSpec(
+ factory: ClassName,
+ factoryType: FactoryType,
+ scope: TypeName,
+ topLevelClass: ClassName?,
+ ): TypeSpec? {
+ return null
+ }
+
+ abstract fun supportsPlatforms(platforms: List): Boolean
+
+ open fun injectAnnotation(): KClass = Inject::class
+}
+
+private inline fun KSDeclaration.checkVisibility(logger: KSPLogger, returnBody: () -> Unit) {
+ if (!getVisibility().isVisible) {
+ logger.error("CircuitInject is not applicable to private or local functions and classes.", this)
+ returnBody()
+ }
+}
+
+private fun KSDeclaration.topLevelDeclaration(): KSDeclaration {
+ return parentDeclaration?.topLevelDeclaration() ?: this
+}
+
+private val Visibility.isVisible: Boolean
+ get() = this != Visibility.PRIVATE && this != Visibility.LOCAL
+
+private data class KotlinInjectBindingResult(
+ val factoryName: String,
+ val factoryPackage: String,
+ val factoryType: FactoryType,
+ val originatingFiles: List,
+)
\ No newline at end of file
diff --git a/ui-injector-processor/src/main/kotlin/com/joshafeinberg/circuitkotlininject/processor/UiInjectorProcessor.kt b/ui-injector-processor/src/main/kotlin/com/joshafeinberg/circuitkotlininject/processor/UiInjectorProcessor.kt
index 85299a0..6d74124 100644
--- a/ui-injector-processor/src/main/kotlin/com/joshafeinberg/circuitkotlininject/processor/UiInjectorProcessor.kt
+++ b/ui-injector-processor/src/main/kotlin/com/joshafeinberg/circuitkotlininject/processor/UiInjectorProcessor.kt
@@ -60,6 +60,7 @@ class UiInjectorProcessor(
private val codeGenerator: CodeGenerator,
) : SymbolProcessor {
override fun process(resolver: Resolver): List {
+ val circuitSymbols = CircuitSymbols.create(resolver) ?: return emptyList()
val componentPackage = options[CIRCUIT_COMPONENT_PACKAGE]
if (componentPackage == null) {
logger.error("Should set component's package")
@@ -75,7 +76,6 @@ class UiInjectorProcessor(
.map { ksAnnotated ->
val circuitInject = ksAnnotated.annotations.firstOrNull { it.shortName.asString() == CircuitInject::class.simpleName }
val screen = circuitInject.getScreenQualifiedRoute()
- val state = circuitInject.getStateQualifiedRoute()
when (ksAnnotated) {
is KSFunctionDeclaration -> NeededInjector(
@@ -83,7 +83,7 @@ class UiInjectorProcessor(
ksAnnotated.simpleName.getShortName(),
ksAnnotated.packageName.asString(),
screen,
- state,
+ ksAnnotated.parameters.firstOrNull { circuitSymbols.circuitUiState.isAssignableFrom(it.type.resolve()) }?.type?.resolve(),
null,
)
is KSClassDeclaration -> NeededInjector(
@@ -91,7 +91,7 @@ class UiInjectorProcessor(
ksAnnotated.simpleName.getShortName(),
ksAnnotated.packageName.asString(),
screen,
- state,
+ null,
ksAnnotated.primaryConstructor?.parameters,
)
else -> {
@@ -122,13 +122,6 @@ class UiInjectorProcessor(
return this.arguments.firstOrNull { it.name?.asString() == "screen" }?.value as? KSType
}
- private fun KSAnnotation?.getStateQualifiedRoute(): KSType? {
- if (this == null) {
- error("State is not provided")
- }
- return this.arguments.firstOrNull { it.name?.asString() == "state" }?.value as? KSType
- }
-
/**
* Creates all the [CIRCUIT_UI_FACTORY] and [CIRCUIT_PRESENTER_FACTORY]. Currently only supports
* Presenters defined as a class
@@ -336,3 +329,29 @@ class UiInjectorProcessor(
val isPresenter: Boolean = name.endsWith("Presenter")
}
}
+
+private class CircuitSymbols private constructor(resolver: Resolver) {
+ val modifier = resolver.loadKSType(MODIFIER.canonicalName)
+ val circuitUiState = resolver.loadKSType(CIRCUIT_UI_STATE.canonicalName)
+ val screen = resolver.loadKSType(SCREEN.canonicalName)
+ val navigator = resolver.loadKSType(NAVIGATOR.canonicalName)
+
+ companion object {
+ fun create(resolver: Resolver): CircuitSymbols? {
+ @Suppress("SwallowedException")
+ return try {
+ CircuitSymbols(resolver)
+ } catch (e: IllegalStateException) {
+ null
+ }
+ }
+ }
+}
+
+private fun Resolver.loadKSType(name: String): KSType =
+ loadOptionalKSType(name) ?: error("Could not find $name in classpath")
+
+private fun Resolver.loadOptionalKSType(name: String?): KSType? {
+ if (name == null) return null
+ return getClassDeclarationByName(getKSNameFromString(name))?.asType(emptyList())
+}
diff --git a/ui-injector-processor/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider b/ui-injector-processor/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider.backup
similarity index 100%
rename from ui-injector-processor/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider
rename to ui-injector-processor/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider.backup
From 0639e2202eb6d666a51327ac2153ef740fd68147 Mon Sep 17 00:00:00 2001
From: Josh Feinberg <15068619+joshafeinberg@users.noreply.github.com>
Date: Sat, 2 Dec 2023 22:38:03 -0600
Subject: [PATCH 2/7] Include annotation as Circuit doesn't support js yet
---
.../artifacts/ui_injector_annotations_js.xml | 4 +-
.../circuitkotlininject/sample/Main.kt | 7 +-
ui-injector-annotations/build.gradle.kts | 6 +-
.../annotations/CircuitInject.kt | 128 +++++++++++++++++-
.../annotations/CircuitInjectOld.kt | 7 +
.../processor/UiInjectorProcessor.kt | 6 +-
6 files changed, 142 insertions(+), 16 deletions(-)
create mode 100644 ui-injector-annotations/src/commonMain/kotlin/com/joshafeinberg/circuitkotlininject/annotations/CircuitInjectOld.kt
diff --git a/.idea/artifacts/ui_injector_annotations_js.xml b/.idea/artifacts/ui_injector_annotations_js.xml
index 9899437..d54842a 100644
--- a/.idea/artifacts/ui_injector_annotations_js.xml
+++ b/.idea/artifacts/ui_injector_annotations_js.xml
@@ -1,8 +1,6 @@
$PROJECT_DIR$/ui-injector-annotations/build/libs
-
-
-
+
\ No newline at end of file
diff --git a/sample/src/main/java/com/joshafeinberg/circuitkotlininject/sample/Main.kt b/sample/src/main/java/com/joshafeinberg/circuitkotlininject/sample/Main.kt
index 304090e..2b1dd73 100644
--- a/sample/src/main/java/com/joshafeinberg/circuitkotlininject/sample/Main.kt
+++ b/sample/src/main/java/com/joshafeinberg/circuitkotlininject/sample/Main.kt
@@ -6,15 +6,14 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application
-import com.joshafeinberg.circuitkotlininject.annotations.CircuitInject
-import com.slack.circuit.foundation.Circuit
import com.slack.circuit.foundation.CircuitCompositionLocals
import com.slack.circuit.foundation.CircuitContent
import com.slack.circuit.runtime.CircuitUiState
import com.slack.circuit.runtime.presenter.Presenter
import com.slack.circuit.runtime.screen.Screen
-import com.slack.circuit.runtime.ui.Ui
-import me.tatarka.inject.annotations.*
+import me.tatarka.inject.annotations.Component
+import me.tatarka.inject.annotations.Provides
+import me.tatarka.inject.annotations.Scope
fun main() = application {
val parentComponent = remember { ParentComponent::class.create() }
diff --git a/ui-injector-annotations/build.gradle.kts b/ui-injector-annotations/build.gradle.kts
index 625aa43..0f69259 100644
--- a/ui-injector-annotations/build.gradle.kts
+++ b/ui-injector-annotations/build.gradle.kts
@@ -7,14 +7,14 @@ kotlin {
jvm {
jvmToolchain(17)
}
- /*js(IR) {
+ js(IR) {
browser()
- }*/
+ }
sourceSets {
commonMain {
dependencies {
compileOnly(libs.circuit.foundation)
- api(libs.circuit.codegen.annotations)
+ // api(libs.circuit.codegen.annotations)
}
}
}
diff --git a/ui-injector-annotations/src/commonMain/kotlin/com/joshafeinberg/circuitkotlininject/annotations/CircuitInject.kt b/ui-injector-annotations/src/commonMain/kotlin/com/joshafeinberg/circuitkotlininject/annotations/CircuitInject.kt
index c183f2a..0f92798 100644
--- a/ui-injector-annotations/src/commonMain/kotlin/com/joshafeinberg/circuitkotlininject/annotations/CircuitInject.kt
+++ b/ui-injector-annotations/src/commonMain/kotlin/com/joshafeinberg/circuitkotlininject/annotations/CircuitInject.kt
@@ -1,8 +1,130 @@
-package com.joshafeinberg.circuitkotlininject.annotations
+// Copyright (C) 2022 Slack Technologies, LLC
+// SPDX-License-Identifier: Apache-2.0
+package com.slack.circuit.codegen.annotations
+import androidx.compose.runtime.Composable
+import com.slack.circuit.foundation.Circuit
import com.slack.circuit.runtime.CircuitUiState
+import com.slack.circuit.runtime.Navigator
+import com.slack.circuit.runtime.presenter.Presenter
import com.slack.circuit.runtime.screen.Screen
+import com.slack.circuit.runtime.ui.Ui
import kotlin.reflect.KClass
-@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS)
-annotation class CircuitInject(val screen: KClass)
+/**
+ * This annotation is used to mark a UI or presenter class or function for code generation. When
+ * annotated, the type's corresponding factory will be generated and keyed with the defined [screen]
+ * .
+ *
+ * The generated factories are then contributed to Anvil via
+ * [com.squareup.anvil.annotations.ContributesMultibinding] and scoped with the provided [scope]
+ * key.
+ *
+ * ## Classes
+ *
+ * [Presenter] and [Ui] classes can be annotated and have their corresponding [Presenter.Factory] or
+ * [Ui.Factory] classes generated for them.
+ *
+ * **Presenter**
+ *
+ * ```kotlin
+ * @CircuitInject(HomeScreen::class, AppScope::class)
+ * class HomePresenter @Inject constructor(...) : Presenter { ... }
+ *
+ * // Generates
+ * @ContributesMultibinding(AppScope::class)
+ * class HomePresenterFactory @Inject constructor() : Presenter.Factory { ... }
+ * ```
+ *
+ * **UI**
+ *
+ * ```kotlin
+ * @CircuitInject(HomeScreen::class, AppScope::class)
+ * class HomeUi @Inject constructor(...) : Ui { ... }
+ *
+ * // Generates
+ * @ContributesMultibinding(AppScope::class)
+ * class HomeUiFactory @Inject constructor() : Ui.Factory { ... }
+ * ```
+ *
+ * ## Functions
+ *
+ * Simple functions can be annotated and have a corresponding [Presenter.Factory] generated. This is
+ * primarily useful for simple cases where a class is just technical tedium.
+ *
+ * **Requirements**
+ * - Presenter function names _must_ end in `Presenter`, otherwise they will be treated as UI
+ * functions.
+ * - Presenter functions _must_ return a [CircuitUiState] type.
+ * - UI functions can optionally accept a [CircuitUiState] type as a parameter, but it is not
+ * required.
+ * - UI functions _must_ return [Unit].
+ * - Both presenter and UI functions _must_ be [Composable].
+ *
+ * **Presenter**
+ *
+ * ```kotlin
+ * @CircuitInject(HomeScreen::class, AppScope::class)
+ * @Composable
+ * fun HomePresenter(): HomeState { ... }
+ *
+ * // Generates
+ * @ContributesMultibinding(AppScope::class)
+ * class HomePresenterFactory @Inject constructor() : Presenter.Factory { ... }
+ * ```
+ *
+ * **UI**
+ *
+ * ```kotlin
+ * @CircuitInject(HomeScreen::class, AppScope::class)
+ * @Composable
+ * fun Home(state: HomeState) { ... }
+ *
+ * // Generates
+ * @ContributesMultibinding(AppScope::class)
+ * class HomeUiFactory @Inject constructor() : Ui.Factory { ... }
+ * ```
+ *
+ * ## Assisted injection
+ *
+ * Any type that is offered in [Presenter.Factory] and [Ui.Factory] can be offered as an assisted
+ * injection to types using Dagger [dagger.assisted.AssistedInject]. For these cases, the
+ * [dagger.assisted.AssistedFactory] -annotated interface should be annotated with [CircuitInject]
+ * instead of the enclosing class.
+ *
+ * Types available for assisted injection are:
+ * - [Screen] – the screen key used to create the [Presenter] or [Ui].
+ * - [Navigator] – (presenters only)
+ * - [Circuit]
+ *
+ * Each should only be defined at-most once.
+ *
+ * **Examples**
+ *
+ * ```kotlin
+ * // Function example
+ * @CircuitInject(HomeScreen::class, AppScope::class)
+ * @Composable
+ * fun HomePresenter(screen: Screen, navigator: Navigator): HomeState { ... }
+ *
+ * // Class example
+ * class HomePresenter @AssistedInject constructor(
+ * @Assisted screen: Screen,
+ * @Assisted navigator: Navigator,
+ * ...
+ * ) : Presenter {
+ * // ...
+ *
+ * @CircuitInject(HomeScreen::class, AppScope::class)
+ * @AssistedFactory
+ * fun interface Factory {
+ * fun create(screen: Screen, navigator: Navigator): HomePresenter
+ * }
+ * }
+ * ```
+ */
+@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION)
+public annotation class CircuitInject(
+ val screen: KClass,
+ val scope: KClass<*>,
+)
\ No newline at end of file
diff --git a/ui-injector-annotations/src/commonMain/kotlin/com/joshafeinberg/circuitkotlininject/annotations/CircuitInjectOld.kt b/ui-injector-annotations/src/commonMain/kotlin/com/joshafeinberg/circuitkotlininject/annotations/CircuitInjectOld.kt
new file mode 100644
index 0000000..b7b8bd8
--- /dev/null
+++ b/ui-injector-annotations/src/commonMain/kotlin/com/joshafeinberg/circuitkotlininject/annotations/CircuitInjectOld.kt
@@ -0,0 +1,7 @@
+package com.joshafeinberg.circuitkotlininject.annotations
+
+import com.slack.circuit.runtime.screen.Screen
+import kotlin.reflect.KClass
+
+@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS)
+annotation class CircuitInjectOld(val screen: KClass)
diff --git a/ui-injector-processor/src/main/kotlin/com/joshafeinberg/circuitkotlininject/processor/UiInjectorProcessor.kt b/ui-injector-processor/src/main/kotlin/com/joshafeinberg/circuitkotlininject/processor/UiInjectorProcessor.kt
index 6d74124..b838ade 100644
--- a/ui-injector-processor/src/main/kotlin/com/joshafeinberg/circuitkotlininject/processor/UiInjectorProcessor.kt
+++ b/ui-injector-processor/src/main/kotlin/com/joshafeinberg/circuitkotlininject/processor/UiInjectorProcessor.kt
@@ -15,7 +15,7 @@ import com.google.devtools.ksp.symbol.KSNode
import com.google.devtools.ksp.symbol.KSType
import com.google.devtools.ksp.symbol.KSValueParameter
import com.google.devtools.ksp.validate
-import com.joshafeinberg.circuitkotlininject.annotations.CircuitInject
+import com.joshafeinberg.circuitkotlininject.annotations.CircuitInjectOld
import com.squareup.kotlinpoet.ClassName
import com.squareup.kotlinpoet.FileSpec
import com.squareup.kotlinpoet.FunSpec
@@ -71,10 +71,10 @@ class UiInjectorProcessor(
}
val symbols = resolver
- .getSymbolsWithAnnotation(CircuitInject::class.qualifiedName!!)
+ .getSymbolsWithAnnotation(CircuitInjectOld::class.qualifiedName!!)
.filter(KSNode::validate)
.map { ksAnnotated ->
- val circuitInject = ksAnnotated.annotations.firstOrNull { it.shortName.asString() == CircuitInject::class.simpleName }
+ val circuitInject = ksAnnotated.annotations.firstOrNull { it.shortName.asString() == CircuitInjectOld::class.simpleName }
val screen = circuitInject.getScreenQualifiedRoute()
when (ksAnnotated) {
From 393ad6db15793a68980d8465ad3041b475c42712 Mon Sep 17 00:00:00 2001
From: Josh Feinberg <15068619+joshafeinberg@users.noreply.github.com>
Date: Sat, 2 Dec 2023 22:50:44 -0600
Subject: [PATCH 3/7] Fix maven publish
---
.idea/artifacts/ui_injector_annotations_js.xml | 4 +++-
build.gradle.kts | 2 ++
ui-injector-processor/build.gradle.kts | 2 +-
3 files changed, 6 insertions(+), 2 deletions(-)
diff --git a/.idea/artifacts/ui_injector_annotations_js.xml b/.idea/artifacts/ui_injector_annotations_js.xml
index d54842a..9899437 100644
--- a/.idea/artifacts/ui_injector_annotations_js.xml
+++ b/.idea/artifacts/ui_injector_annotations_js.xml
@@ -1,6 +1,8 @@
$PROJECT_DIR$/ui-injector-annotations/build/libs
-
+
+
+
\ No newline at end of file
diff --git a/build.gradle.kts b/build.gradle.kts
index 90cb64d..c98b4f1 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -11,4 +11,6 @@ allprojects {
plugins {
alias(libs.plugins.kotlin.multiplatform) apply false
alias(libs.plugins.ktlint) apply true
+ alias(libs.plugins.ksp) apply false
+ alias(libs.plugins.mavenpublish) apply false
}
diff --git a/ui-injector-processor/build.gradle.kts b/ui-injector-processor/build.gradle.kts
index 30e8600..554c176 100644
--- a/ui-injector-processor/build.gradle.kts
+++ b/ui-injector-processor/build.gradle.kts
@@ -1,7 +1,7 @@
plugins {
kotlin("jvm")
alias(libs.plugins.ksp)
- // alias(libs.plugins.mavenpublish)
+ alias(libs.plugins.mavenpublish)
}
dependencies {
From 0db8a424e944b0caf1046145aca805fec9f92aba Mon Sep 17 00:00:00 2001
From: Josh Feinberg <15068619+joshafeinberg@users.noreply.github.com>
Date: Sat, 2 Dec 2023 23:07:48 -0600
Subject: [PATCH 4/7] Fix some type issues
---
.../joshafeinberg/circuitkotlininject/sample/Main.kt | 5 ++++-
.../processor/CircuitSymbolProcessorProvider.kt | 12 ++++++++++--
2 files changed, 14 insertions(+), 3 deletions(-)
diff --git a/sample/src/main/java/com/joshafeinberg/circuitkotlininject/sample/Main.kt b/sample/src/main/java/com/joshafeinberg/circuitkotlininject/sample/Main.kt
index 2b1dd73..d3c83eb 100644
--- a/sample/src/main/java/com/joshafeinberg/circuitkotlininject/sample/Main.kt
+++ b/sample/src/main/java/com/joshafeinberg/circuitkotlininject/sample/Main.kt
@@ -58,7 +58,10 @@ data object MyScreen : Screen {
}
@com.slack.circuit.codegen.annotations.CircuitInject(MyScreen::class, AppScope::class)
-class MyScreenPresenter(private val injectedString: String) : Presenter {
+class MyScreenPresenter(
+ private val injectedString: String,
+ private val screen: MyScreen,
+) : Presenter {
@Composable
override fun present(): MyScreen.MyScreenState {
return MyScreen.MyScreenState(injectedString)
diff --git a/ui-injector-processor/src/main/kotlin/com/joshafeinberg/circuitkotlininject/processor/CircuitSymbolProcessorProvider.kt b/ui-injector-processor/src/main/kotlin/com/joshafeinberg/circuitkotlininject/processor/CircuitSymbolProcessorProvider.kt
index 03367e2..1d3aebb 100644
--- a/ui-injector-processor/src/main/kotlin/com/joshafeinberg/circuitkotlininject/processor/CircuitSymbolProcessorProvider.kt
+++ b/ui-injector-processor/src/main/kotlin/com/joshafeinberg/circuitkotlininject/processor/CircuitSymbolProcessorProvider.kt
@@ -464,7 +464,15 @@ private class CircuitSymbolProcessor(
} else {
if (codegenMode == CodegenMode.KOTLIN_INJECT) {
cd.primaryConstructor?.parameters?.forEach { parameter ->
- constructorParams.add(ParameterSpec.builder(parameter.name!!.asString(), parameter.type.toTypeName()).build())
+ val resolvedType = parameter.type.resolve()
+ if (!resolvedType.isInstanceOf(symbols.screen) && !resolvedType.isInstanceOf(symbols.navigator)) {
+ constructorParams.add(
+ ParameterSpec.builder(
+ parameter.name!!.asString(),
+ resolvedType.toTypeName()
+ ).build()
+ )
+ }
}
}
// Simple constructor call, no injection.
@@ -631,7 +639,7 @@ private fun KSFunctionDeclaration.assistedParameters(
}
}
.toList()
- .map { CodeBlock.of("${it.name} = ${it.factoryName}") }
+ .map { CodeBlock.of("${it.name} = ${it.factoryName} as ${it.type}") }
.joinToCode(",·")
}
From f0bb129a5234062ceca49c611d055f17a0624da2 Mon Sep 17 00:00:00 2001
From: Josh Feinberg <15068619+joshafeinberg@users.noreply.github.com>
Date: Sat, 2 Dec 2023 23:18:59 -0600
Subject: [PATCH 5/7] Remove typecast and fix data object issues in when clause
---
.../processor/CircuitSymbolProcessorProvider.kt | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/ui-injector-processor/src/main/kotlin/com/joshafeinberg/circuitkotlininject/processor/CircuitSymbolProcessorProvider.kt b/ui-injector-processor/src/main/kotlin/com/joshafeinberg/circuitkotlininject/processor/CircuitSymbolProcessorProvider.kt
index 1d3aebb..044a06e 100644
--- a/ui-injector-processor/src/main/kotlin/com/joshafeinberg/circuitkotlininject/processor/CircuitSymbolProcessorProvider.kt
+++ b/ui-injector-processor/src/main/kotlin/com/joshafeinberg/circuitkotlininject/processor/CircuitSymbolProcessorProvider.kt
@@ -175,7 +175,7 @@ private class CircuitSymbolProcessor(
}
val screenKSType = circuitInjectAnnotation.arguments[0].value as KSType
val screenIsObject =
- screenKSType.declaration.let { it is KSClassDeclaration && it.classKind == ClassKind.OBJECT }
+ screenKSType.declaration.let { it is KSClassDeclaration && it.classKind == ClassKind.OBJECT && !it.modifiers.contains(Modifier.DATA) }
val screenType = screenKSType.toTypeName()
val scope = (circuitInjectAnnotation.arguments[1].value as KSType).toTypeName()
@@ -639,7 +639,7 @@ private fun KSFunctionDeclaration.assistedParameters(
}
}
.toList()
- .map { CodeBlock.of("${it.name} = ${it.factoryName} as ${it.type}") }
+ .map { CodeBlock.of("${it.name} = ${it.factoryName}") }
.joinToCode(",·")
}
From 100fd9de5bc788402adfa71ac63cea6bb7bf3439 Mon Sep 17 00:00:00 2001
From: Josh Feinberg <15068619+joshafeinberg@users.noreply.github.com>
Date: Sun, 3 Dec 2023 12:26:59 -0600
Subject: [PATCH 6/7] Move component generator to its own processor
---
.../sample/other/OtherScreen.kt | 2 +-
.../CircuitSymbolProcessorProvider.kt | 156 ++-----------
.../KotlinInjectComponentProcessorProvider.kt | 210 ++++++++++++++++++
3 files changed, 229 insertions(+), 139 deletions(-)
create mode 100644 ui-injector-processor/src/main/kotlin/com/joshafeinberg/circuitkotlininject/processor/KotlinInjectComponentProcessorProvider.kt
diff --git a/sample/src/main/java/com/joshafeinberg/circuitkotlininject/sample/other/OtherScreen.kt b/sample/src/main/java/com/joshafeinberg/circuitkotlininject/sample/other/OtherScreen.kt
index ea7a201..e8211fa 100644
--- a/sample/src/main/java/com/joshafeinberg/circuitkotlininject/sample/other/OtherScreen.kt
+++ b/sample/src/main/java/com/joshafeinberg/circuitkotlininject/sample/other/OtherScreen.kt
@@ -21,7 +21,7 @@ data object OtherScreen : Screen {
}
@com.slack.circuit.codegen.annotations.CircuitInject(OtherScreen::class, AppScope::class)
-class OtherScreenPresenter(private val injectedString: String) : Presenter {
+class OtherScreenPresenter : Presenter {
@Composable
override fun present(): OtherScreen.OtherScreenState {
return OtherScreen.OtherScreenState
diff --git a/ui-injector-processor/src/main/kotlin/com/joshafeinberg/circuitkotlininject/processor/CircuitSymbolProcessorProvider.kt b/ui-injector-processor/src/main/kotlin/com/joshafeinberg/circuitkotlininject/processor/CircuitSymbolProcessorProvider.kt
index 044a06e..959a7a5 100644
--- a/ui-injector-processor/src/main/kotlin/com/joshafeinberg/circuitkotlininject/processor/CircuitSymbolProcessorProvider.kt
+++ b/ui-injector-processor/src/main/kotlin/com/joshafeinberg/circuitkotlininject/processor/CircuitSymbolProcessorProvider.kt
@@ -32,11 +32,9 @@ private const val DAGGER_MULTIBINDINGS_PACKAGE = "$DAGGER_PACKAGE.multibindings"
private const val CIRCUIT_RUNTIME_UI_PACKAGE = "$CIRCUIT_RUNTIME_BASE_PACKAGE.ui"
private const val CIRCUIT_RUNTIME_SCREEN_PACKAGE = "$CIRCUIT_RUNTIME_BASE_PACKAGE.screen"
private const val CIRCUIT_RUNTIME_PRESENTER_PACKAGE = "$CIRCUIT_RUNTIME_BASE_PACKAGE.presenter"
-private const val KOTLIN_INJECT_BASE_PACKAGE = "me.tatarka.inject.annotations"
private val MODIFIER = ClassName("androidx.compose.ui", "Modifier")
private val CIRCUIT_INJECT_ANNOTATION =
ClassName("com.slack.circuit.codegen.annotations", "CircuitInject")
-private val CIRCUIT = ClassName("com.slack.circuit.foundation", "Circuit")
private val CIRCUIT_PRESENTER = ClassName(CIRCUIT_RUNTIME_PRESENTER_PACKAGE, "Presenter")
private val CIRCUIT_PRESENTER_FACTORY = CIRCUIT_PRESENTER.nestedClass("Factory")
private val CIRCUIT_UI = ClassName(CIRCUIT_RUNTIME_UI_PACKAGE, "Ui")
@@ -51,14 +49,9 @@ private val DAGGER_INSTALL_IN = ClassName(DAGGER_HILT_PACKAGE, "InstallIn")
private val DAGGER_ORIGINATING_ELEMENT =
ClassName(DAGGER_HILT_CODEGEN_PACKAGE, "OriginatingElement")
private val DAGGER_INTO_SET = ClassName(DAGGER_MULTIBINDINGS_PACKAGE, "IntoSet")
-private val KOTLIN_INJECT_COMPONENT = ClassName(KOTLIN_INJECT_BASE_PACKAGE, "Component")
-private val KOTLIN_INJECT_INTO_SET = ClassName(KOTLIN_INJECT_BASE_PACKAGE, "IntoSet")
-private val KOTLIN_INJECT_PROVIDES = ClassName(KOTLIN_INJECT_BASE_PACKAGE, "Provides")
private const val MODULE = "Module"
private const val FACTORY = "Factory"
private const val CIRCUIT_CODEGEN_MODE = "circuit.codegen.mode"
-private const val CIRCUIT_COMPONENT_PACKAGE = "circuit.codegen.package"
-private const val CIRCUIT_PARENT_COMPONENT = "circuit.codegen.parent.component"
@AutoService(SymbolProcessorProvider::class)
public class CircuitSymbolProcessorProvider : SymbolProcessorProvider {
@@ -104,9 +97,6 @@ private class CircuitSymbolProcessor(
private val options: Map,
private val platforms: List,
) : SymbolProcessor {
-
- // todo - this is bad
- private val bindMethodsToCreate = mutableListOf()
override fun process(resolver: Resolver): List {
val symbols = CircuitSymbols.create(resolver) ?: return emptyList()
@@ -128,22 +118,14 @@ private class CircuitSymbolProcessor(
return emptyList()
}
- val componentPackage = options[CIRCUIT_COMPONENT_PACKAGE]
- if (componentPackage == null) {
- logger.error("Should set component's package")
- }
-
- val parentComponent = options[CIRCUIT_PARENT_COMPONENT]?.let {
- ClassName.bestGuess(it.trim())
- }
-
- resolver.getSymbolsWithAnnotation(CIRCUIT_INJECT_ANNOTATION.canonicalName).forEach {
- annotatedElement ->
+ resolver.getSymbolsWithAnnotation(CIRCUIT_INJECT_ANNOTATION.canonicalName).forEach { annotatedElement ->
when (annotatedElement) {
is KSClassDeclaration ->
generateFactory(annotatedElement, InstantiationType.CLASS, symbols, codegenMode)
+
is KSFunctionDeclaration ->
generateFactory(annotatedElement, InstantiationType.FUNCTION, symbols, codegenMode)
+
else ->
logger.error(
"CircuitInject is only applicable on classes and functions.",
@@ -152,13 +134,6 @@ private class CircuitSymbolProcessor(
}
}
- createComponentFile(
- bindMethodsToCreate,
- componentPackage,
- parentComponent?.let { listOf(it) }
- )
-
-
return emptyList()
}
@@ -175,7 +150,11 @@ private class CircuitSymbolProcessor(
}
val screenKSType = circuitInjectAnnotation.arguments[0].value as KSType
val screenIsObject =
- screenKSType.declaration.let { it is KSClassDeclaration && it.classKind == ClassKind.OBJECT && !it.modifiers.contains(Modifier.DATA) }
+ screenKSType.declaration.let {
+ it is KSClassDeclaration && it.classKind == ClassKind.OBJECT && !it.modifiers.contains(
+ Modifier.DATA
+ )
+ }
val screenType = screenKSType.toTypeName()
val scope = (circuitInjectAnnotation.arguments[1].value as KSType).toTypeName()
@@ -220,6 +199,7 @@ private class CircuitSymbolProcessor(
when (factoryData.factoryType) {
FactoryType.PRESENTER ->
builder.buildPresenterFactory(annotatedElement, screenBranch, factoryData.codeBlock)
+
FactoryType.UI ->
builder.buildUiFactory(annotatedElement, screenBranch, factoryData.codeBlock)
}
@@ -230,15 +210,6 @@ private class CircuitSymbolProcessor(
val topLevelClass = (topLevelDeclaration as? KSClassDeclaration)?.toClassName()
val originatingFile = listOfNotNull(annotatedElement.containingFile)
-
- bindMethodsToCreate.add(
- KotlinInjectBindingResult(
- className + FACTORY,
- factoryData.packageName,
- factoryData.factoryType,
- originatingFile
- )
- )
FileSpec.get(factoryData.packageName, typeSpec)
.writeTo(
@@ -305,7 +276,13 @@ private class CircuitSymbolProcessor(
FactoryType.UI
}
val assistedParams =
- fd.assistedParameters(symbols, logger, screenKSType, codegenMode, factoryType == FactoryType.PRESENTER)
+ fd.assistedParameters(
+ symbols,
+ logger,
+ screenKSType,
+ codegenMode,
+ factoryType == FactoryType.PRESENTER
+ )
codeBlock =
when (factoryType) {
FactoryType.PRESENTER ->
@@ -315,6 +292,7 @@ private class CircuitSymbolProcessor(
MemberName(packageName, name),
assistedParams
)
+
FactoryType.UI -> {
// State param is optional
val stateParam =
@@ -385,6 +363,7 @@ private class CircuitSymbolProcessor(
}
}
}
+
InstantiationType.CLASS -> {
val cd = annotatedElement as KSClassDeclaration
cd.checkVisibility(logger) {
@@ -482,98 +461,6 @@ private class CircuitSymbolProcessor(
}
return FactoryData(className, packageName, factoryType, constructorParams, codeBlock)
}
-
- private fun createComponentFile(
- symbols: List,
- componentPackage: String?,
- parentClasses: List?
- ) {
- val bindMethods = symbols.map { bindingResult ->
- if (bindingResult.factoryType == FactoryType.PRESENTER) {
- PropertySpec.builder("bind",
- CIRCUIT_PRESENTER_FACTORY, KModifier.PROTECTED)
- .receiver(ClassName(bindingResult.factoryPackage, bindingResult.factoryName))
- .getter(
- FunSpec.getterBuilder()
- .addAnnotation(KOTLIN_INJECT_PROVIDES)
- .addAnnotation(KOTLIN_INJECT_INTO_SET)
- .addStatement("return this")
- .build(),
- )
- .build()
- } else {
- PropertySpec.builder("bind",
- CIRCUIT_UI_FACTORY, KModifier.PROTECTED)
- .receiver(ClassName(bindingResult.factoryPackage, bindingResult.factoryName))
- .getter(
- FunSpec.getterBuilder()
- .addAnnotation(KOTLIN_INJECT_PROVIDES)
- .addAnnotation(KOTLIN_INJECT_INTO_SET)
- .addStatement("return this")
- .build(),
- )
- .build()
- }
- }
-
- val injectableCircuitProperty = PropertySpec.builder("circuit", CIRCUIT, KModifier.ABSTRACT)
- .build()
-
- val circuitProvider = FunSpec.builder("providesCircuit")
- .addAnnotation(KOTLIN_INJECT_PROVIDES)
- .returns(CIRCUIT)
- .addParameter("uiFactories", Set::class.asClassName().parameterizedBy(CIRCUIT_UI_FACTORY))
- .addParameter("presenterFactories", Set::class.asClassName().parameterizedBy(CIRCUIT_PRESENTER_FACTORY))
- .addStatement("return %T.Builder().addUiFactories(uiFactories).addPresenterFactories(presenterFactories).build()", CIRCUIT)
- .build()
-
- val parameters = parentClasses?.map {
- ParameterSpec.builder(it.simpleName.lowercase(Locale.getDefault()), it)
- .build()
- }
-
- val properties = parentClasses?.map {
- val name = it.simpleName.lowercase(Locale.getDefault())
- PropertySpec.builder(name, it)
- .addAnnotation(KOTLIN_INJECT_COMPONENT)
- .initializer(name)
- .build()
- }
-
- val classSpec = TypeSpec.classBuilder("CircuitComponent")
- .addAnnotation(KOTLIN_INJECT_COMPONENT)
- .addModifiers(KModifier.INTERNAL, KModifier.ABSTRACT)
- .primaryConstructor(
- FunSpec.constructorBuilder()
- .apply {
- parameters?.let {
- addParameters(it)
- }
- }
- .build(),
- )
- .apply {
- properties?.let {
- addProperties(it)
- }
- }
- .addProperty(injectableCircuitProperty)
- .addFunction(circuitProvider)
- .addProperties(bindMethods.toList())
- .build()
-
- try {
- FileSpec.builder(componentPackage ?: "", "CircuitComponent")
- .addType(classSpec)
- .build()
- .writeTo(
- codeGenerator,
- Dependencies(false, *symbols.flatMap { it.originatingFiles }.toList().toTypedArray())
- )
- } catch (e: Exception) {
- logger.warn("This is bad but I'll figure it out later")
- }
- }
}
private data class AssistedType(
@@ -857,10 +744,3 @@ private fun KSDeclaration.topLevelDeclaration(): KSDeclaration {
private val Visibility.isVisible: Boolean
get() = this != Visibility.PRIVATE && this != Visibility.LOCAL
-
-private data class KotlinInjectBindingResult(
- val factoryName: String,
- val factoryPackage: String,
- val factoryType: FactoryType,
- val originatingFiles: List,
-)
\ No newline at end of file
diff --git a/ui-injector-processor/src/main/kotlin/com/joshafeinberg/circuitkotlininject/processor/KotlinInjectComponentProcessorProvider.kt b/ui-injector-processor/src/main/kotlin/com/joshafeinberg/circuitkotlininject/processor/KotlinInjectComponentProcessorProvider.kt
new file mode 100644
index 0000000..74a6853
--- /dev/null
+++ b/ui-injector-processor/src/main/kotlin/com/joshafeinberg/circuitkotlininject/processor/KotlinInjectComponentProcessorProvider.kt
@@ -0,0 +1,210 @@
+package com.joshafeinberg.circuitkotlininject.processor
+
+import com.google.auto.service.AutoService
+import com.google.devtools.ksp.closestClassDeclaration
+import com.google.devtools.ksp.processing.CodeGenerator
+import com.google.devtools.ksp.processing.Dependencies
+import com.google.devtools.ksp.processing.KSPLogger
+import com.google.devtools.ksp.processing.Resolver
+import com.google.devtools.ksp.processing.SymbolProcessor
+import com.google.devtools.ksp.processing.SymbolProcessorEnvironment
+import com.google.devtools.ksp.processing.SymbolProcessorProvider
+import com.google.devtools.ksp.symbol.KSAnnotated
+import com.google.devtools.ksp.symbol.KSFile
+import com.squareup.kotlinpoet.ClassName
+import com.squareup.kotlinpoet.FileSpec
+import com.squareup.kotlinpoet.FunSpec
+import com.squareup.kotlinpoet.KModifier
+import com.squareup.kotlinpoet.ParameterSpec
+import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
+import com.squareup.kotlinpoet.PropertySpec
+import com.squareup.kotlinpoet.TypeSpec
+import com.squareup.kotlinpoet.asClassName
+import com.squareup.kotlinpoet.ksp.toTypeName
+import com.squareup.kotlinpoet.ksp.writeTo
+import java.util.*
+
+private const val CIRCUIT_RUNTIME_BASE_PACKAGE = "com.slack.circuit.runtime"
+private const val KOTLIN_INJECT_BASE_PACKAGE = "me.tatarka.inject.annotations"
+private const val CIRCUIT_RUNTIME_UI_PACKAGE = "${CIRCUIT_RUNTIME_BASE_PACKAGE}.ui"
+private const val CIRCUIT_RUNTIME_PRESENTER_PACKAGE = "${CIRCUIT_RUNTIME_BASE_PACKAGE}.presenter"
+private val CIRCUIT = ClassName("com.slack.circuit.foundation", "Circuit")
+private val CIRCUIT_UI = ClassName(CIRCUIT_RUNTIME_UI_PACKAGE, "Ui")
+private val CIRCUIT_UI_FACTORY = CIRCUIT_UI.nestedClass("Factory")
+private val CIRCUIT_PRESENTER = ClassName(CIRCUIT_RUNTIME_PRESENTER_PACKAGE, "Presenter")
+private val CIRCUIT_PRESENTER_FACTORY = CIRCUIT_PRESENTER.nestedClass("Factory")
+private val KOTLIN_INJECT_COMPONENT = ClassName(KOTLIN_INJECT_BASE_PACKAGE, "Component")
+private val KOTLIN_INJECT_INTO_SET = ClassName(KOTLIN_INJECT_BASE_PACKAGE, "IntoSet")
+private val KOTLIN_INJECT_PROVIDES = ClassName(KOTLIN_INJECT_BASE_PACKAGE, "Provides")
+private const val CIRCUIT_CODEGEN_MODE = "circuit.codegen.mode"
+private const val CIRCUIT_COMPONENT_PACKAGE = "circuit.codegen.package"
+private const val CIRCUIT_PARENT_COMPONENT = "circuit.codegen.parent.component"
+
+@AutoService(SymbolProcessorProvider::class)
+public class KotlinInjectComponentProcessorProvider : SymbolProcessorProvider {
+ override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor {
+ return KotlinInjectComponentProcessor(
+ environment.logger,
+ environment.codeGenerator,
+ environment.options,
+ )
+ }
+}
+
+private class KotlinInjectComponentProcessor(
+ private val logger: KSPLogger,
+ private val codeGenerator: CodeGenerator,
+ private val options: Map,
+) : SymbolProcessor {
+ override fun process(resolver: Resolver): List {
+ if (options[CIRCUIT_CODEGEN_MODE] != "kotlin_inject") {
+ return emptyList()
+ }
+
+ val componentPackage = options[CIRCUIT_COMPONENT_PACKAGE]
+ if (componentPackage == null) {
+ logger.error("Should set component's package")
+ }
+
+ val parentComponent = options[CIRCUIT_PARENT_COMPONENT]?.let {
+ ClassName.bestGuess(it.trim())
+ }
+
+ val bindingResults = mutableListOf()
+
+ resolver.getNewFiles().forEach { file ->
+ file.declarations.forEach { declarations ->
+ declarations.closestClassDeclaration()?.let { classDeclaration ->
+ val isCircuitFactory = classDeclaration.superTypes.any {
+ val typeName = it.resolve().toTypeName()
+ typeName == CIRCUIT_UI_FACTORY || typeName == CIRCUIT_PRESENTER_FACTORY
+ }
+ if (isCircuitFactory) {
+ bindingResults.add(
+ KotlinInjectBindingResult(
+ classDeclaration.simpleName.asString(),
+ classDeclaration.packageName.asString(),
+ classDeclaration.superTypes.any { it.resolve().toTypeName() == CIRCUIT_PRESENTER_FACTORY },
+ originatingFiles = listOf(file)
+ )
+ )
+ }
+ }
+ }
+ }
+
+ createComponentFile(
+ bindingResults,
+ componentPackage,
+ parentComponent?.let { listOf(it) },
+ )
+
+ return emptyList()
+ }
+
+ private fun createComponentFile(
+ symbols: List,
+ componentPackage: String?,
+ parentClasses: List?
+ ) {
+ if (symbols.isEmpty()) {
+ return
+ }
+
+ val bindMethods = symbols.map { bindingResult ->
+ if (bindingResult.isPresenter) {
+ PropertySpec.builder(
+ "bind",
+ CIRCUIT_PRESENTER_FACTORY, KModifier.PROTECTED
+ )
+ .receiver(ClassName(bindingResult.factoryPackage, bindingResult.factoryName))
+ .getter(
+ FunSpec.getterBuilder()
+ .addAnnotation(KOTLIN_INJECT_PROVIDES)
+ .addAnnotation(KOTLIN_INJECT_INTO_SET)
+ .addStatement("return this")
+ .build(),
+ )
+ .build()
+ } else {
+ PropertySpec.builder(
+ "bind",
+ CIRCUIT_UI_FACTORY, KModifier.PROTECTED
+ )
+ .receiver(ClassName(bindingResult.factoryPackage, bindingResult.factoryName))
+ .getter(
+ FunSpec.getterBuilder()
+ .addAnnotation(KOTLIN_INJECT_PROVIDES)
+ .addAnnotation(KOTLIN_INJECT_INTO_SET)
+ .addStatement("return this")
+ .build(),
+ )
+ .build()
+ }
+ }
+
+ val injectableCircuitProperty = PropertySpec.builder("circuit", CIRCUIT, KModifier.ABSTRACT)
+ .build()
+
+ val circuitProvider = FunSpec.builder("providesCircuit")
+ .addAnnotation(KOTLIN_INJECT_PROVIDES)
+ .returns(CIRCUIT)
+ .addParameter("uiFactories", Set::class.asClassName().parameterizedBy(CIRCUIT_UI_FACTORY))
+ .addParameter("presenterFactories", Set::class.asClassName().parameterizedBy(CIRCUIT_PRESENTER_FACTORY))
+ .addStatement("return %T.Builder().addUiFactories(uiFactories).addPresenterFactories(presenterFactories).build()",
+ CIRCUIT
+ )
+ .build()
+
+ val parameters = parentClasses?.map {
+ ParameterSpec.builder(it.simpleName.lowercase(Locale.getDefault()), it)
+ .build()
+ }
+
+ val properties = parentClasses?.map {
+ val name = it.simpleName.lowercase(Locale.getDefault())
+ PropertySpec.builder(name, it)
+ .addAnnotation(KOTLIN_INJECT_COMPONENT)
+ .initializer(name)
+ .build()
+ }
+
+ val classSpec = TypeSpec.classBuilder("CircuitComponent")
+ .addAnnotation(KOTLIN_INJECT_COMPONENT)
+ .addModifiers(KModifier.INTERNAL, KModifier.ABSTRACT)
+ .primaryConstructor(
+ FunSpec.constructorBuilder()
+ .apply {
+ parameters?.let {
+ addParameters(it)
+ }
+ }
+ .build(),
+ )
+ .apply {
+ properties?.let {
+ addProperties(it)
+ }
+ }
+ .addProperty(injectableCircuitProperty)
+ .addFunction(circuitProvider)
+ .addProperties(bindMethods.toList())
+ .build()
+
+ FileSpec.builder(componentPackage ?: "", "CircuitComponent")
+ .addType(classSpec)
+ .build()
+ .writeTo(
+ codeGenerator,
+ Dependencies(false, *symbols.flatMap { it.originatingFiles }.toList().toTypedArray())
+ )
+ }
+
+}
+
+private data class KotlinInjectBindingResult(
+ val factoryName: String,
+ val factoryPackage: String,
+ val isPresenter: Boolean,
+ val originatingFiles: List,
+)
From 6a3374e449118199308cfbb7109614775dd6558e Mon Sep 17 00:00:00 2001
From: Josh Feinberg <15068619+joshafeinberg@users.noreply.github.com>
Date: Sun, 3 Dec 2023 14:18:07 -0600
Subject: [PATCH 7/7] Delete old code
---
.idea/gradle.xml | 1 +
.../processor/UiInjectorProcessor.kt | 357 ------------------
.../processor/UiInjectorProcessorProvider.kt | 16 -
3 files changed, 1 insertion(+), 373 deletions(-)
delete mode 100644 ui-injector-processor/src/main/kotlin/com/joshafeinberg/circuitkotlininject/processor/UiInjectorProcessor.kt
delete mode 100644 ui-injector-processor/src/main/kotlin/com/joshafeinberg/circuitkotlininject/processor/UiInjectorProcessorProvider.kt
diff --git a/.idea/gradle.xml b/.idea/gradle.xml
index d2d826a..90e979c 100644
--- a/.idea/gradle.xml
+++ b/.idea/gradle.xml
@@ -1,5 +1,6 @@
+