diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 10afb74b..0eb91cf6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -4,6 +4,9 @@ on: pull_request: branches: - '*' + push: + branches: + - '*' concurrency: group: build-${{ github.ref }} @@ -22,7 +25,7 @@ jobs: - name: Validate Gradle Wrapper uses: gradle/wrapper-validation-action@v1 - - name: Set up JDK 11 + - name: Set up JDK 17 uses: actions/setup-java@v3 with: distribution: 'zulu' @@ -31,10 +34,10 @@ jobs: - name: Setup Gradle uses: gradle/gradle-build-action@v2 - - name: Install Compiler + - name: Install run: cd projects && ./install.sh - - name: Run Sandbox Test + - name: Run Tests run: cd examples && ./test.sh diff --git a/examples/android-coffee-maker/src/main/java/org/koin/sample/androidx/di/AppModules.kt b/examples/android-coffee-maker/src/main/java/org/koin/sample/androidx/di/AppModules.kt index 5aaa2825..d2623d0a 100644 --- a/examples/android-coffee-maker/src/main/java/org/koin/sample/androidx/di/AppModules.kt +++ b/examples/android-coffee-maker/src/main/java/org/koin/sample/androidx/di/AppModules.kt @@ -4,11 +4,12 @@ import org.koin.core.annotation.ComponentScan import org.koin.core.annotation.Module import org.koin.sample.android.library.CommonModule import org.koin.sample.androidx.repository.RepositoryModule +import org.koin.sample.clients.ClientModule @Module(includes = [DataModule::class]) @ComponentScan("org.koin.sample.androidx.app") class AppModule -@Module(includes = [CommonModule::class, RepositoryModule::class]) +@Module(includes = [CommonModule::class, ClientModule::class, RepositoryModule::class]) @ComponentScan("org.koin.sample.androidx.data") internal class DataModule \ No newline at end of file diff --git a/examples/android-coffee-maker/src/test/java/AndroidModuleTest.kt b/examples/android-coffee-maker/src/test/java/AndroidModuleTest.kt index 4ffa8a8c..c3fc34d2 100644 --- a/examples/android-coffee-maker/src/test/java/AndroidModuleTest.kt +++ b/examples/android-coffee-maker/src/test/java/AndroidModuleTest.kt @@ -1,10 +1,12 @@ package org.koin.sample.androidx +import io.ktor.client.HttpClient import it.example.component.ExampleSingleton import org.junit.Test import org.koin.core.context.startKoin import org.koin.core.context.stopKoin import org.koin.core.parameter.parametersOf +import org.koin.core.qualifier.named import org.koin.ksp.generated.module import org.koin.sample.android.library.CommonRepository import org.koin.sample.android.library.MyScope @@ -41,6 +43,8 @@ class AndroidModuleTest { assert(koin.getOrNull() != null) + assert(koin.get(named("clientA")) != koin.get(named("clientB"))) + stopKoin() } diff --git a/examples/android-library/build.gradle.kts b/examples/android-library/build.gradle.kts index 65d19b74..697961ab 100644 --- a/examples/android-library/build.gradle.kts +++ b/examples/android-library/build.gradle.kts @@ -38,7 +38,8 @@ dependencies { implementation(libs.android.appcompat) ksp(libs.koin.ksp) implementation(project(":coffee-maker-module")) - + api(libs.ktor.core) + implementation(libs.ktor.cio) testImplementation(libs.koin.test) } diff --git a/examples/android-library/src/main/java/org/koin/sample/clients/ClientModule.kt b/examples/android-library/src/main/java/org/koin/sample/clients/ClientModule.kt new file mode 100644 index 00000000..23a6886b --- /dev/null +++ b/examples/android-library/src/main/java/org/koin/sample/clients/ClientModule.kt @@ -0,0 +1,26 @@ +package org.koin.sample.clients + +import io.ktor.client.HttpClient +import io.ktor.client.engine.cio.CIO +import org.koin.core.annotation.Module +import org.koin.core.annotation.Named +import org.koin.core.annotation.Single + +@Module(includes = [ClientModuleA::class, ClientModuleB::class]) +class ClientModule + +@Module +class ClientModuleA { + + @Single + @Named("clientA") + fun createClient() = HttpClient(CIO) {} +} + +@Module +class ClientModuleB { + + @Single + @Named("clientB") + fun createClient() = HttpClient(CIO) {} +} \ No newline at end of file diff --git a/examples/coffee-maker/src/main/kotlin/org/koin/example/CoffeeApp.kt b/examples/coffee-maker/src/main/kotlin/org/koin/example/CoffeeApp.kt index 4f95a1ca..f1bc27ae 100644 --- a/examples/coffee-maker/src/main/kotlin/org/koin/example/CoffeeApp.kt +++ b/examples/coffee-maker/src/main/kotlin/org/koin/example/CoffeeApp.kt @@ -4,7 +4,6 @@ import org.koin.core.component.KoinComponent import org.koin.core.component.inject import org.koin.core.context.startKoin import org.koin.core.logger.Level -import org.koin.core.time.measureDuration import org.koin.example.coffee.CoffeeMaker import org.koin.example.di.CoffeeAppModule import org.koin.example.di.CoffeeTesterModule @@ -12,6 +11,7 @@ import org.koin.example.tea.TeaModule import org.koin.example.test.ext.ExternalModule import org.koin.example.test.scope.ScopeModule import org.koin.ksp.generated.* +import kotlin.time.measureTime class CoffeeApp : KoinComponent { val maker: CoffeeMaker by inject() @@ -37,13 +37,8 @@ fun main() { } val coffeeShop = CoffeeApp() - measureDuration("Got Coffee") { + val t = measureTime { coffeeShop.maker.brew() } -} - -fun measureDuration(msg: String, code: () -> Unit): Double { - val duration = measureDuration(code) - println("$msg in $duration ms") - return duration + println("Got Coffee in $t") } \ No newline at end of file diff --git a/examples/coffee-maker/src/test/java/CoffeeAppTest.kt b/examples/coffee-maker/src/test/java/CoffeeAppTest.kt index 393cea11..5eff2351 100644 --- a/examples/coffee-maker/src/test/java/CoffeeAppTest.kt +++ b/examples/coffee-maker/src/test/java/CoffeeAppTest.kt @@ -11,7 +11,6 @@ import org.koin.example.coffee.MyDetachCoffeeComponent import org.koin.example.coffee.pump.PumpCounter import org.koin.example.di.CoffeeAppModule import org.koin.example.di.CoffeeTesterModule -import org.koin.example.measureDuration import org.koin.example.tea.TeaModule import org.koin.example.tea.TeaPot import org.koin.example.test.CoffeeMakerTester @@ -22,6 +21,7 @@ import org.koin.example.test.include.IncludedComponent import org.koin.example.test.scope.* import org.koin.ksp.generated.module import org.koin.mp.KoinPlatformTools +import kotlin.time.measureTime class CoffeeAppTest { @@ -44,9 +44,10 @@ class CoffeeAppTest { } val coffeeShop = CoffeeApp() - measureDuration("Got Coffee") { + val time = measureTime { coffeeShop.maker.brew() } + println("Got Coffee in $time") // Tests val koin = KoinPlatformTools.defaultContext().get() diff --git a/examples/gradle.properties b/examples/gradle.properties index d4ef8189..795c0d2e 100644 --- a/examples/gradle.properties +++ b/examples/gradle.properties @@ -10,4 +10,7 @@ kotlin.code.style=official #Android android.useAndroidX=true androidMinSDK=21 -androidCompileSDK=34 \ No newline at end of file +androidCompileSDK=34 + +#KSP +ksp.useKSP2=true \ No newline at end of file diff --git a/examples/gradle/libs.versions.toml b/examples/gradle/libs.versions.toml index 22c51a7b..b2734604 100644 --- a/examples/gradle/libs.versions.toml +++ b/examples/gradle/libs.versions.toml @@ -4,13 +4,14 @@ # Core kotlin = "2.0.21" -koin = "4.0.1-RC2" -koinAnnotations = "2.0.0-Beta2" +koin = "4.0.2" +koinAnnotations = "2.0.0-Beta4" ksp = "2.0.21-1.0.28" junit = "4.13.2" # Android agp = "8.3.2" androidCompat = "1.7.0" +ktor = "2.3.12" [libraries] koin-core = { module = "io.insert-koin:koin-core", version.ref = "koin" } @@ -21,6 +22,8 @@ koin-ksp = { module = "io.insert-koin:koin-ksp-compiler", version.ref = "koinAnn ksp-api = {module = "com.google.devtools.ksp:symbol-processing-api", version.ref = "ksp"} android-appcompat = {module = "androidx.appcompat:appcompat", version.ref = "androidCompat"} junit = {module = "junit:junit",version.ref ="junit"} +ktor-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" } +ktor-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktor" } [plugins] #kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } diff --git a/examples/other-ksp/src/main/kotlin/org/koin/example/animal/Animal.kt b/examples/other-ksp/src/main/kotlin/org/koin/example/animal/Animal.kt index e25e6dcc..1f751ea1 100644 --- a/examples/other-ksp/src/main/kotlin/org/koin/example/animal/Animal.kt +++ b/examples/other-ksp/src/main/kotlin/org/koin/example/animal/Animal.kt @@ -13,6 +13,9 @@ public class Cat : Animal public class Bunny(public val color: String) : Animal +@Single +public class Farm(@WhiteBunny public val whiteBunny: Bunny, @BlackBunny public val blackBunny: Bunny) + @Named public annotation class WhiteBunny @@ -22,7 +25,6 @@ public annotation class BlackBunny @Module @ComponentScan public class AnimalModule { - @Factory public fun animal(cat: Cat, dog: Dog): Animal = if (randomBoolean()) cat else dog diff --git a/examples/other-ksp/src/main/kotlin/org/koin/example/by/example/ByExampleSingle.kt b/examples/other-ksp/src/main/kotlin/org/koin/example/by/example/ByExampleSingle.kt new file mode 100644 index 00000000..6bc97b9a --- /dev/null +++ b/examples/other-ksp/src/main/kotlin/org/koin/example/by/example/ByExampleSingle.kt @@ -0,0 +1,12 @@ +package org.koin.example.by.example + +import org.koin.core.annotation.ComponentScan +import org.koin.core.annotation.Module +import org.koin.core.annotation.Single + +@Module +@ComponentScan +public class ByModule + +@Single +public class ByExampleSingle \ No newline at end of file diff --git a/examples/other-ksp/src/main/kotlin/org/koin/example/defaultparam/DefaultParam.kt b/examples/other-ksp/src/main/kotlin/org/koin/example/defaultparam/DefaultParam.kt new file mode 100644 index 00000000..f37d5fd1 --- /dev/null +++ b/examples/other-ksp/src/main/kotlin/org/koin/example/defaultparam/DefaultParam.kt @@ -0,0 +1,16 @@ +package org.koin.example.defaultparam + +import org.koin.core.annotation.ComponentScan +import org.koin.core.annotation.Factory +import org.koin.core.annotation.InjectedParam +import org.koin.core.annotation.Module + +@Module +@ComponentScan +public class MyModule + +public const val COMPONENT_DEFAULT: String = "default" + +@Factory +public class Component(@InjectedParam public val param: String = COMPONENT_DEFAULT) + diff --git a/examples/other-ksp/src/main/kotlin/org/koin/example/scope/ScopeModule.kt b/examples/other-ksp/src/main/kotlin/org/koin/example/scope/ScopeModule.kt index 565ff6c0..4c647caf 100644 --- a/examples/other-ksp/src/main/kotlin/org/koin/example/scope/ScopeModule.kt +++ b/examples/other-ksp/src/main/kotlin/org/koin/example/scope/ScopeModule.kt @@ -10,6 +10,7 @@ public class MyScope @Scoped public class MyScopedInstance +@Scope(name = "my_scope") @Factory public class MyScopeFactory( public val oc : MyOtherComponent, diff --git a/examples/other-ksp/src/test/kotlin/org.koin.example/TestModule.kt b/examples/other-ksp/src/test/kotlin/org.koin.example/TestModule.kt index 0de1cbc3..4a7fe8e4 100644 --- a/examples/other-ksp/src/test/kotlin/org.koin.example/TestModule.kt +++ b/examples/other-ksp/src/test/kotlin/org.koin.example/TestModule.kt @@ -4,10 +4,16 @@ import org.junit.Test import org.koin.core.Koin import org.koin.core.error.NoDefinitionFoundException import org.koin.core.logger.Level +import org.koin.core.parameter.parametersOf import org.koin.core.qualifier.named import org.koin.core.qualifier.qualifier import org.koin.dsl.koinApplication import org.koin.example.animal.* +import org.koin.example.by.example.ByExampleSingle +import org.koin.example.by.example.ByModule +import org.koin.example.defaultparam.COMPONENT_DEFAULT +import org.koin.example.defaultparam.Component +import org.koin.example.defaultparam.MyModule import org.koin.example.`interface`.MyInterfaceExt import org.koin.example.newmodule.* import org.koin.example.newmodule.ComponentWithProps.Companion.DEFAULT_ID @@ -20,6 +26,7 @@ import org.koin.ksp.generated.defaultModule import org.koin.ksp.generated.module import kotlin.test.assertEquals import kotlin.test.assertFailsWith +import kotlin.test.assertNotNull import kotlin.test.assertTrue class TestModule { @@ -34,7 +41,9 @@ class TestModule { MyModule3().module, MyModule2().module, AnimalModule().module, - ScopeModule().module + ScopeModule().module, + ByModule().module, + MyModule().module ) }.koin @@ -58,13 +67,12 @@ class TestModule { assertTrue { animals.any { it is Cat } } val scope = koin.createScope("my_scope_id", named("my_scope")) - assertTrue { - koin.get().msi == scope.get() + scope.get().msi == scope.get() } assertTrue { - koin.get().msi == koin.get().msi + scope.get().msi == scope.get().msi } assertFailsWith(NoDefinitionFoundException::class) { @@ -72,8 +80,18 @@ class TestModule { } assertEquals("White", koin.get(qualifier()).color) + + val farm = koin.get() + assertEquals("White", farm.whiteBunny.color) + assertEquals("Black", farm.blackBunny.color) + + assertNotNull(koin.getOrNull()) + + assertEquals(COMPONENT_DEFAULT,koin.get().param) // display warning in build logs } + + private fun randomGetAnimal(koin: Koin): Animal { val a = koin.get() println("animal: $a") diff --git a/examples/test.sh b/examples/test.sh index 43ac1cf3..820713b2 100755 --- a/examples/test.sh +++ b/examples/test.sh @@ -1,3 +1,4 @@ #!/bin/sh ./gradlew testDebug --no-build-cache +./gradlew :other-ksp:test :coffee-maker:test :compile-perf:test --no-build-cache diff --git a/projects/gradle.properties b/projects/gradle.properties index d871d13d..16baec20 100644 --- a/projects/gradle.properties +++ b/projects/gradle.properties @@ -7,9 +7,10 @@ org.gradle.parallel=true #Kotlin kotlin.code.style=official #Koin -koinAnnotationsVersion=2.0.0-Beta3 +koinAnnotationsVersion=2.0.0-Beta4 #Android android.useAndroidX=true androidMinSDK=14 androidCompileSDK=34 + #android.nonTransitiveRClass=true diff --git a/projects/gradle/libs.versions.toml b/projects/gradle/libs.versions.toml index 3b8bbd22..b90a433c 100644 --- a/projects/gradle/libs.versions.toml +++ b/projects/gradle/libs.versions.toml @@ -4,7 +4,7 @@ # Core kotlin = "2.0.21" -koin = "4.0.1" +koin = "4.0.2" ksp = "2.0.21-1.0.28" publish = "2.0.0" dokka = "1.9.10" diff --git a/projects/koin-annotations/src/commonMain/kotlin/org/koin/core/annotation/CoreAnnotations.kt b/projects/koin-annotations/src/commonMain/kotlin/org/koin/core/annotation/CoreAnnotations.kt index 79918dab..cf66e20e 100644 --- a/projects/koin-annotations/src/commonMain/kotlin/org/koin/core/annotation/CoreAnnotations.kt +++ b/projects/koin-annotations/src/commonMain/kotlin/org/koin/core/annotation/CoreAnnotations.kt @@ -233,12 +233,4 @@ annotation class ComponentScan(val value: String = "") * Tag a dependency as already provided by Koin (like DSL declaration, or internals) */ @Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.VALUE_PARAMETER) -annotation class Provided - -/** - * Internal usage for components discovery in generated package - * - * @param value: package of declared definition - */ -@Target(AnnotationTarget.CLASS, AnnotationTarget.FIELD, AnnotationTarget.FUNCTION) -annotation class Definition(val value: String = "") \ No newline at end of file +annotation class Provided \ No newline at end of file diff --git a/projects/koin-annotations/src/commonMain/kotlin/org/koin/meta/annotations/MetaAnnotations.kt b/projects/koin-annotations/src/commonMain/kotlin/org/koin/meta/annotations/MetaAnnotations.kt new file mode 100644 index 00000000..0fd13d08 --- /dev/null +++ b/projects/koin-annotations/src/commonMain/kotlin/org/koin/meta/annotations/MetaAnnotations.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2017-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.koin.meta.annotations + +/** + * All following Annotations are intended for Internal use only + * + * @author Arnaud Giuliani + */ + +/** + * Internal usage for components discovery in generated package + * + * @param value: package of declared definition + */ +@Target(AnnotationTarget.CLASS, AnnotationTarget.FIELD, AnnotationTarget.FUNCTION) +annotation class ExternalDefinition(val value: String = "") + +/** + * Meta Definition annotation to help represents + * @param value: Definition full path + * @param dependencies - Parameters Tags to check + * @param scope - Scope where it's declared + */ +@Target(AnnotationTarget.CLASS) +annotation class MetaDefinition(val value: String = "", val dependencies: Array = [], val scope : String = "") + +/** + * Meta Definition annotation to help represents + * @param value: Definition full path + * @param includes - Includes Module Tags to check + */ +@Target(AnnotationTarget.CLASS) +annotation class MetaModule(val value: String = "", val includes: Array = []) diff --git a/projects/koin-ksp-compiler/src/jvmMain/kotlin/org/koin/compiler/BuilderProcessor.kt b/projects/koin-ksp-compiler/src/jvmMain/kotlin/org/koin/compiler/BuilderProcessor.kt index 11c0eb93..73191960 100644 --- a/projects/koin-ksp-compiler/src/jvmMain/kotlin/org/koin/compiler/BuilderProcessor.kt +++ b/projects/koin-ksp-compiler/src/jvmMain/kotlin/org/koin/compiler/BuilderProcessor.kt @@ -20,28 +20,28 @@ import com.google.devtools.ksp.symbol.KSAnnotated import org.koin.compiler.KspOptions.* import org.koin.compiler.generator.KoinCodeGenerator import org.koin.compiler.metadata.KoinMetaData +import org.koin.compiler.metadata.KoinTagWriter import org.koin.compiler.scanner.KoinMetaDataScanner +import org.koin.compiler.scanner.KoinTagMetaDataScanner import org.koin.compiler.verify.KoinConfigChecker -import org.koin.compiler.verify.KoinTagWriter +import kotlin.time.measureTime class BuilderProcessor( - codeGenerator: CodeGenerator, + private val codeGenerator: CodeGenerator, private val logger: KSPLogger, private val options: Map ) : SymbolProcessor { - private val isComposeViewModelActive = isComposeViewModelActive() || isKoinComposeViewModelActive() - private val koinCodeGenerator = KoinCodeGenerator(codeGenerator, logger, isComposeViewModelActive) + private val isViewModelMPActive = isKoinViewModelMPActive() + private val koinCodeGenerator = KoinCodeGenerator(codeGenerator, logger, isViewModelMPActive) private val koinMetaDataScanner = KoinMetaDataScanner(logger) - private val koinTagWriter = KoinTagWriter(codeGenerator,logger) - private val koinConfigChecker = KoinConfigChecker(codeGenerator, logger) override fun process(resolver: Resolver): List { initComponents(resolver) logger.logging("Scan symbols ...") - val invalidSymbols = koinMetaDataScanner.scanSymbols(resolver) + val invalidSymbols = koinMetaDataScanner.findInvalidSymbols(resolver) if (invalidSymbols.isNotEmpty()) { logger.logging("Invalid symbols found (${invalidSymbols.size}), waiting for next round") return invalidSymbols @@ -54,43 +54,52 @@ class BuilderProcessor( ) logger.logging("Build metadata ...") - val moduleList = koinMetaDataScanner.scanKoinModules(defaultModule) + val moduleList = koinMetaDataScanner.scanKoinModules( + defaultModule, + resolver + ) logger.logging("Generate code ...") koinCodeGenerator.generateModules(moduleList, defaultModule, isDefaultModuleActive()) - val allModules = moduleList + defaultModule - koinTagWriter.writeAllTags(moduleList, defaultModule) - - if (isConfigCheckActive()) { - logger.warn("Check Configuration ...") - koinConfigChecker.verifyDefinitionDeclarations(allModules, resolver) - koinConfigChecker.verifyModuleIncludes(allModules, resolver) + val isConfigCheckActive = isConfigCheckActive() + + // Tags are used to verify generated content (KMP) + KoinTagWriter(codeGenerator, logger, resolver, isConfigCheckActive) + .writeAllTags(moduleList, defaultModule) + + val isAlreadyGenerated = codeGenerator.generatedFile.isEmpty() + if (isConfigCheckActive && isAlreadyGenerated) { + logger.warn("Koin Configuration Check ...") + val t = measureTime { + + val metaTagScanner = KoinTagMetaDataScanner(logger, resolver) + val invalidsMetaSymbols = metaTagScanner.findInvalidSymbols() + if (invalidsMetaSymbols.isNotEmpty()) { + logger.logging("Invalid symbols found (${invalidsMetaSymbols.size}), waiting for next round") + return invalidSymbols + } + + val checker = KoinConfigChecker(logger, resolver) + checker.verifyMetaModules(metaTagScanner.findMetaModules()) + checker.verifyMetaDefinitions(metaTagScanner.findMetaDefinitions()) + } + logger.warn("Koin Configuration Check done in $t") } return emptyList() } private fun initComponents(resolver: Resolver) { koinCodeGenerator.resolver = resolver - koinTagWriter.resolver = resolver } private fun isConfigCheckActive(): Boolean { return options.getOrDefault(KOIN_CONFIG_CHECK.name, "false") == true.toString() } - //TODO Use Koin 4.0 ViewModel DSL - @Deprecated("use isKoinComposeViewModelActive") - private fun isComposeViewModelActive(): Boolean { - val option = options.getOrDefault(USE_COMPOSE_VIEWMODEL.name, "false") == true.toString() - if (option) logger.warn("[Deprecated] 'USE_COMPOSE_VIEWMODEL' arg is deprecated. Please use 'KOIN_USE_COMPOSE_VIEWMODEL'") - return option - } - - private fun isKoinComposeViewModelActive(): Boolean { - val option = - options.getOrDefault(KOIN_USE_COMPOSE_VIEWMODEL.name, "false") == true.toString() - if (option) logger.warn("Activate Compose ViewModel for @KoinViewModel generation") + // Allow to disable usage of ViewModel MP API and + private fun isKoinViewModelMPActive(): Boolean { + val option = options.getOrDefault(KOIN_USE_COMPOSE_VIEWMODEL.name, "true") == true.toString() return option } @@ -100,11 +109,10 @@ class BuilderProcessor( } } - class BuilderProcessorProvider : SymbolProcessorProvider { override fun create( environment: SymbolProcessorEnvironment ): SymbolProcessor { return BuilderProcessor(environment.codeGenerator, environment.logger, environment.options) } -} \ No newline at end of file +} diff --git a/projects/koin-ksp-compiler/src/jvmMain/kotlin/org/koin/compiler/KspOptions.kt b/projects/koin-ksp-compiler/src/jvmMain/kotlin/org/koin/compiler/KspOptions.kt index a2d8b2c5..9faaaab1 100644 --- a/projects/koin-ksp-compiler/src/jvmMain/kotlin/org/koin/compiler/KspOptions.kt +++ b/projects/koin-ksp-compiler/src/jvmMain/kotlin/org/koin/compiler/KspOptions.kt @@ -19,7 +19,5 @@ enum class KspOptions { KOIN_CONFIG_CHECK, KOIN_DEFAULT_MODULE, - //TODO Remove isComposeViewModelActive with Koin 4 - USE_COMPOSE_VIEWMODEL, KOIN_USE_COMPOSE_VIEWMODEL } \ No newline at end of file diff --git a/projects/koin-ksp-compiler/src/jvmMain/kotlin/org/koin/compiler/generator/DefinitionWriter.kt b/projects/koin-ksp-compiler/src/jvmMain/kotlin/org/koin/compiler/generator/DefinitionWriter.kt index b905177e..ce94bb4e 100644 --- a/projects/koin-ksp-compiler/src/jvmMain/kotlin/org/koin/compiler/generator/DefinitionWriter.kt +++ b/projects/koin-ksp-compiler/src/jvmMain/kotlin/org/koin/compiler/generator/DefinitionWriter.kt @@ -22,8 +22,9 @@ import org.koin.compiler.generator.KoinCodeGenerator.Companion.LOGGER import org.koin.compiler.metadata.KoinMetaData import org.koin.compiler.metadata.KoinMetaData.Module.Companion.DEFINE_PREFIX import org.koin.compiler.metadata.SINGLE +import org.koin.compiler.metadata.TagFactory import org.koin.compiler.scanner.ext.filterForbiddenKeywords -import org.koin.compiler.verify.ext.getResolution +import org.koin.compiler.resolver.getResolution import java.io.OutputStream class DefinitionWriter( @@ -40,13 +41,13 @@ class DefinitionWriter( } if (def.alreadyGenerated == true){ - LOGGER.logging("skip ${def.label} -> ${def.getTagName()} - already generated") + LOGGER.logging("skip ${def.label} -> ${TagFactory.getTag(def)} - already generated") } else { if (def.isExpect.not()){ LOGGER.logging("write definition ${def.label} ...") val param = def.parameters.generateParamFunction() - val ctor = generateConstructor(def.parameters) + val ctor = generateConstructor(def, def.parameters) val binds = generateBindings(def.bindings) val qualifier = def.qualifier.generateQualifier() val createAtStart = if (def.isType(SINGLE) && def.isCreatedAtStart == true) { @@ -90,7 +91,7 @@ class DefinitionWriter( ctor: String, binds: String ) { - writeln("@Definition(\"${def.packageName}\")") + writeln("@ExternalDefinition(\"${def.packageName}\")") writeln("public fun Module.$DEFINE_PREFIX${def.label}() : KoinDefinition<*> = ${def.keyword.keyword}($qualifier$createAtStart) { ${param}${prefix}$ctor } $binds") } @@ -150,7 +151,8 @@ class DefinitionWriter( return parents.reversed() } - private fun generateConstructor(constructorParameters: List): String { + private fun generateConstructor(def : KoinMetaData.Definition, constructorParameters: List): String { + warnDefaultValues(constructorParameters, def) val paramsWithoutDefaultValues = constructorParameters.filter { !it.hasDefault || it is KoinMetaData.DefinitionParameter.Property} return paramsWithoutDefaultValues.joinToString(prefix = "(", separator = ",", postfix = ")") { ctorParam -> val isNullable: Boolean = ctorParam.nullable @@ -183,6 +185,18 @@ class DefinitionWriter( } } + private fun warnDefaultValues( + constructorParameters: List, + def: KoinMetaData.Definition + ) { + constructorParameters.filter { it.hasDefault && it is KoinMetaData.DefinitionParameter.Dependency } + .forEach { ctorParam -> + if (ctorParam.hasDefault) { + LOGGER.warn("Definition ${def.packageName}.${def.label} is using parameter '${ctorParam.name}' with default value. Injection is skipped for this parameter.") + } + } + } + companion object { const val TAB = "\t" const val CREATED_AT_START = "createdAtStart=true" diff --git a/projects/koin-ksp-compiler/src/jvmMain/kotlin/org/koin/compiler/generator/KoinCodeGenerator.kt b/projects/koin-ksp-compiler/src/jvmMain/kotlin/org/koin/compiler/generator/KoinCodeGenerator.kt index b535bd75..c5393354 100644 --- a/projects/koin-ksp-compiler/src/jvmMain/kotlin/org/koin/compiler/generator/KoinCodeGenerator.kt +++ b/projects/koin-ksp-compiler/src/jvmMain/kotlin/org/koin/compiler/generator/KoinCodeGenerator.kt @@ -19,13 +19,13 @@ import com.google.devtools.ksp.processing.CodeGenerator import com.google.devtools.ksp.processing.KSPLogger import com.google.devtools.ksp.processing.Resolver import org.koin.compiler.metadata.KoinMetaData -import org.koin.compiler.verify.ext.getResolution +import org.koin.compiler.resolver.getResolution class KoinCodeGenerator( val codeGenerator: CodeGenerator, val logger: KSPLogger, //TODO Remove isComposeViewModelActive with Koin 4 - val isComposeViewModelActive: Boolean + val isViewModelMPActive: Boolean ) { lateinit var resolver: Resolver @@ -57,7 +57,7 @@ class KoinCodeGenerator( if (defaultModule.alreadyGenerated == false && hasDefaultDefinitions){ defaultModule.setCurrentDefinitionsToExternals() - DefaultModuleWriter(codeGenerator, resolver, defaultModule, generateDefaultModule).writeModule(isComposeViewModelActive) + DefaultModuleWriter(codeGenerator, resolver, defaultModule, generateDefaultModule).writeModule(isViewModelMPActive) } } @@ -67,7 +67,7 @@ class KoinCodeGenerator( checkAlreadyGenerated(module) if (module.alreadyGenerated == false){ - ClassModuleWriter(codeGenerator, resolver, module).writeModule(isComposeViewModelActive) + ClassModuleWriter(codeGenerator, resolver, module).writeModule(isViewModelMPActive) } } diff --git a/projects/koin-ksp-compiler/src/jvmMain/kotlin/org/koin/compiler/generator/ModuleWriter.kt b/projects/koin-ksp-compiler/src/jvmMain/kotlin/org/koin/compiler/generator/ModuleWriter.kt index 3cc7aa77..95e062b0 100644 --- a/projects/koin-ksp-compiler/src/jvmMain/kotlin/org/koin/compiler/generator/ModuleWriter.kt +++ b/projects/koin-ksp-compiler/src/jvmMain/kotlin/org/koin/compiler/generator/ModuleWriter.kt @@ -21,11 +21,12 @@ import com.google.devtools.ksp.processing.Resolver import org.koin.compiler.generator.DefinitionWriter.Companion.CREATED_AT_START import org.koin.compiler.generator.DefinitionWriter.Companion.TAB import org.koin.compiler.generator.ext.getNewFile -import org.koin.compiler.metadata.KOIN_VIEWMODEL -import org.koin.compiler.metadata.KOIN_VIEWMODEL_COMPOSE +import org.koin.compiler.metadata.KOIN_VIEWMODEL_ANDROID +import org.koin.compiler.metadata.KOIN_VIEWMODEL_MP import org.koin.compiler.metadata.KoinMetaData import org.koin.compiler.scanner.ext.filterForbiddenKeywords import org.koin.compiler.generator.ext.toSourceString +import org.koin.compiler.type.clearPackageSymbols import java.io.OutputStream abstract class ModuleWriter( @@ -46,15 +47,15 @@ abstract class ModuleWriter( private lateinit var definitionFactory : DefinitionWriterFactory private val modulePath = "${module.packageName}.${module.name}" - private val generatedField = "${module.packageName("_")}_${module.name}" + private val generatedField = "${module.packageName("_").clearPackageSymbols()}_${module.name}" //TODO Remove isComposeViewModelActive with Koin 4 - fun writeModule(isComposeViewModelActive: Boolean) { + fun writeModule(isViewModelMPActive: Boolean) { fileStream = createFileStream() definitionFactory = DefinitionWriterFactory(resolver, fileStream!!) writeHeader() - writeHeaderImports(isComposeViewModelActive) + writeHeaderImports(isViewModelMPActive) if (hasExternalDefinitions) { writeExternalDefinitionImports() @@ -82,7 +83,7 @@ abstract class ModuleWriter( private fun writeExternalDefinitionImports() { writeln(""" - import org.koin.core.annotation.Definition + import org.koin.meta.annotations.ExternalDefinition import org.koin.core.definition.KoinDefinition """.trimIndent()) } @@ -91,19 +92,19 @@ abstract class ModuleWriter( writeln(MODULE_HEADER) } - open fun writeHeaderImports(isComposeViewModelActive: Boolean) { - writeln(generateImports(module.definitions, isComposeViewModelActive)) + open fun writeHeaderImports(isViewModelMPActive: Boolean) { + writeln(generateImports(module.definitions, isViewModelMPActive)) } private fun generateImports( definitions: List, - isComposeViewModelActive: Boolean + isViewModelMPActive: Boolean ): String { return definitions.map { definition -> definition.keyword } .toSet() .mapNotNull { keyword -> - if (isComposeViewModelActive && keyword == KOIN_VIEWMODEL) { - KOIN_VIEWMODEL_COMPOSE.import.let { "import $it" } + if (isViewModelMPActive && keyword == KOIN_VIEWMODEL_ANDROID) { + KOIN_VIEWMODEL_MP.import.let { "import $it" } } else { keyword.import?.let { "import $it" } } diff --git a/projects/koin-ksp-compiler/src/jvmMain/kotlin/org/koin/compiler/metadata/AnnotationMetadata.kt b/projects/koin-ksp-compiler/src/jvmMain/kotlin/org/koin/compiler/metadata/AnnotationMetadata.kt index 8b233f47..655082e2 100644 --- a/projects/koin-ksp-compiler/src/jvmMain/kotlin/org/koin/compiler/metadata/AnnotationMetadata.kt +++ b/projects/koin-ksp-compiler/src/jvmMain/kotlin/org/koin/compiler/metadata/AnnotationMetadata.kt @@ -37,18 +37,17 @@ val SINGLETON = DefinitionAnnotation("single", annotationType = Singleton::class val FACTORY = DefinitionAnnotation("factory", annotationType = Factory::class) val SCOPE = DefinitionAnnotation("scoped", annotationType = Scope::class) val SCOPED = DefinitionAnnotation("scoped", annotationType = Scoped::class) -val KOIN_VIEWMODEL = DefinitionAnnotation("viewModel", "org.koin.androidx.viewmodel.dsl.viewModel", KoinViewModel::class) -//TODO Remove isComposeViewModelActive with Koin 4 -val KOIN_VIEWMODEL_COMPOSE = DefinitionAnnotation("viewModel", "org.koin.core.module.dsl.viewModel", KoinViewModel::class) +val KOIN_VIEWMODEL_ANDROID = DefinitionAnnotation("viewModel", "org.koin.androidx.viewmodel.dsl.viewModel", KoinViewModel::class) +val KOIN_VIEWMODEL_MP = DefinitionAnnotation("viewModel", "org.koin.core.module.dsl.viewModel", KoinViewModel::class) val KOIN_WORKER = DefinitionAnnotation("worker", "org.koin.androidx.workmanager.dsl.worker", KoinWorker::class) -val DEFINITION_ANNOTATION_LIST = listOf(SINGLE, SINGLETON,FACTORY, SCOPE, SCOPED,KOIN_VIEWMODEL, KOIN_WORKER) +val DEFINITION_ANNOTATION_LIST = listOf(SINGLE, SINGLETON,FACTORY, SCOPE, SCOPED,KOIN_VIEWMODEL_ANDROID, KOIN_WORKER) val DEFINITION_ANNOTATION_LIST_TYPES = DEFINITION_ANNOTATION_LIST.map { it.annotationType } val DEFINITION_ANNOTATION_LIST_NAMES = DEFINITION_ANNOTATION_LIST.map { it.annotationName?.lowercase(Locale.getDefault()) } -val SCOPE_DEFINITION_ANNOTATION_LIST = listOf(SCOPED, FACTORY, KOIN_VIEWMODEL, KOIN_WORKER) +val SCOPE_DEFINITION_ANNOTATION_LIST = listOf(SCOPED, FACTORY, KOIN_VIEWMODEL_ANDROID, KOIN_WORKER) val SCOPE_DEFINITION_ANNOTATION_LIST_NAMES = SCOPE_DEFINITION_ANNOTATION_LIST.map { it.annotationName?.lowercase(Locale.getDefault()) } @@ -61,7 +60,7 @@ fun getExtraScopeAnnotation(annotations: Map): DefinitionA val definitionAnnotation = when (key) { SCOPED.annotationName -> SCOPED FACTORY.annotationName -> FACTORY - KOIN_VIEWMODEL.annotationName -> KOIN_VIEWMODEL + KOIN_VIEWMODEL_ANDROID.annotationName -> KOIN_VIEWMODEL_ANDROID KOIN_WORKER.annotationName -> KOIN_WORKER else -> null } diff --git a/projects/koin-ksp-compiler/src/jvmMain/kotlin/org/koin/compiler/metadata/KoinMetaData.kt b/projects/koin-ksp-compiler/src/jvmMain/kotlin/org/koin/compiler/metadata/KoinMetaData.kt index ea133acc..e879439d 100644 --- a/projects/koin-ksp-compiler/src/jvmMain/kotlin/org/koin/compiler/metadata/KoinMetaData.kt +++ b/projects/koin-ksp-compiler/src/jvmMain/kotlin/org/koin/compiler/metadata/KoinMetaData.kt @@ -26,7 +26,6 @@ fun PackageName.camelCase() = split(".").joinToString("") { it.capitalize() } sealed class KoinMetaData { - data class Module( val packageName: PackageName, val name: String, @@ -44,13 +43,10 @@ sealed class KoinMetaData { var alreadyGenerated : Boolean? = null - fun getTagName() = packageName.camelCase() + name.capitalize() + if (isExpect) "Exp" else "" - fun packageName(separator: String): String { return if (isDefault) "" else { - val default = Locale.getDefault() - packageName.split(".").joinToString(separator) { it.lowercase(default) } + splitPackage(packageName, separator) } } @@ -85,10 +81,9 @@ sealed class KoinMetaData { data class ModuleInclude( val packageName: PackageName, val className : String, - val isExpect : Boolean - ){ - fun getTagName() = packageName.camelCase() + className.capitalize() + if (isExpect) "Exp" else "" - } + val isExpect : Boolean, + val isActual : Boolean + ) enum class ModuleType { FIELD, CLASS, OBJECT; @@ -104,7 +99,14 @@ sealed class KoinMetaData { fun getValue(): String { return when (this) { is StringScope -> name - is ClassScope -> "${type.packageName}.${type.simpleName}" + is ClassScope -> "${type.packageName.asString()}.${type.simpleName.asString()}" + } + } + + fun getTagValue(): String { + return when (this) { + is StringScope -> name + is ClassScope -> "${type.packageName.asString()}.${type.simpleName.asString()}" } } } @@ -146,7 +148,8 @@ sealed class KoinMetaData { val keyword: DefinitionAnnotation, val bindings: List, val scope: Scope? = null, - val isExpect : Boolean + val isExpect : Boolean, + val isActual : Boolean ) : KoinMetaData() { var alreadyGenerated : Boolean? = null @@ -157,8 +160,6 @@ sealed class KoinMetaData { val packageNamePrefix: String = if (packageName.isEmpty()) "" else "${packageName}." - fun getTagName() = packageName.camelCase() + label.capitalize() + if (isExpect) "Exp" else "" - override fun equals(other: Any?): Boolean { if (this === other) return true if (javaClass != other?.javaClass) return false @@ -189,8 +190,9 @@ sealed class KoinMetaData { parameters: List = emptyList(), bindings: List, scope: Scope? = null, - isExpect : Boolean - ) : Definition(functionName, parameters, packageName, qualifier, isCreatedAtStart, keyword, bindings, scope, isExpect) { + isExpect : Boolean, + isActual : Boolean + ) : Definition(functionName, parameters, packageName, qualifier, isCreatedAtStart, keyword, bindings, scope, isExpect,isActual) { var isClassFunction: Boolean = true } @@ -203,7 +205,8 @@ sealed class KoinMetaData { val constructorParameters: List = emptyList(), bindings: List, scope: Scope? = null, - isExpect : Boolean + isExpect : Boolean, + isActual : Boolean ) : Definition( className, constructorParameters, @@ -213,10 +216,9 @@ sealed class KoinMetaData { keyword, bindings, scope, - isExpect + isExpect, + isActual ) - - } sealed class DefinitionParameter(val nullable: Boolean = false) { @@ -254,6 +256,9 @@ sealed class KoinMetaData { } } +internal fun splitPackage(packageName : String, separator: String, default: Locale = Locale.getDefault()) : String = + packageName.split(".").joinToString(separator) { it.lowercase(default) } + private fun KSDeclaration.getQualifiedName(): String { val packageName = packageName.asString() @@ -267,4 +272,4 @@ private fun KSDeclaration.getQualifiedName(): String { } ?: run { "${this.packageName.asString()}.${simpleName.asString()}" } -} \ No newline at end of file +} diff --git a/projects/koin-ksp-compiler/src/jvmMain/kotlin/org/koin/compiler/metadata/KoinTagWriter.kt b/projects/koin-ksp-compiler/src/jvmMain/kotlin/org/koin/compiler/metadata/KoinTagWriter.kt new file mode 100644 index 00000000..6acaf654 --- /dev/null +++ b/projects/koin-ksp-compiler/src/jvmMain/kotlin/org/koin/compiler/metadata/KoinTagWriter.kt @@ -0,0 +1,191 @@ +package org.koin.compiler.metadata + +import org.koin.compiler.generator.ext.appendText +import com.google.devtools.ksp.processing.CodeGenerator +import com.google.devtools.ksp.processing.KSPLogger +import com.google.devtools.ksp.processing.Resolver +import com.google.devtools.ksp.symbol.KSDeclaration +import org.koin.compiler.generator.ext.getNewFile +import org.koin.compiler.resolver.isAlreadyExisting +import org.koin.compiler.type.typeWhiteList +import org.koin.compiler.verify.* +import java.io.OutputStream +import java.nio.file.Files +import java.security.DigestOutputStream +import java.security.MessageDigest +import kotlin.io.path.createTempFile +import kotlin.io.path.outputStream + +const val TAG_PREFIX = "KoinMeta_" +// Avoid looooong name with full SHA as file name. Let's take 8 first digits +private const val TAG_FILE_HASH_LIMIT = 8 + +class KoinTagWriter( + val codeGenerator: CodeGenerator, + val logger: KSPLogger, + val resolver: Resolver, + val isConfigCheckActive : Boolean +) { + private val alreadyDeclaredTags: ArrayList = arrayListOf() + private var _tagFileStream : OutputStream? = null + private val fileStream : OutputStream + get() = _tagFileStream ?: error("KoinTagWriter - tagFileStream is null") + + fun writeAllTags( + moduleList: List, + default: KoinMetaData.Module + ) { + val isAlreadyGenerated = codeGenerator.generatedFile.isEmpty() + if (!isAlreadyGenerated) { + logger.logging("Koin Tags Generation ...") + createTagsForModules(moduleList, default) + } + } + + /** + * To realize [reproducible-builds](https://reproducible-builds.org/), write everything to a temporal file + * then copy it to the tag file. + * By this method, we can compute the digest of tag file and use it to name it. + * + * @author Kengo TODA + */ + @OptIn(ExperimentalStdlibApi::class) + private fun createTagsForModules( + moduleList: List, + default: KoinMetaData.Module, + ) { + val allDefinitions = (moduleList + default).flatMap { it.definitions } + val tempFile = createTempFile("KoinMeta", ".kt") + val sha256 = MessageDigest.getInstance("SHA-256"); + DigestOutputStream(tempFile.outputStream(), sha256).buffered().use { + _tagFileStream = it + if (isConfigCheckActive){ + writeImports() + } + writeModuleTags(moduleList) + writeDefinitionsTags(allDefinitions) + } + + val tagFileName = "KoinMeta-${sha256.digest().toHexString(HexFormat.Default).take(TAG_FILE_HASH_LIMIT)}" + writeTagFile(tagFileName).buffered().use { Files.copy(tempFile, it) } + } + + private fun writeModuleTags( + allModules: List + ) { + allModules.forEach { m -> writeModuleTag(m) } + } + + private fun writeDefinitionsTags( + allDefinitions: List, + ) { + allDefinitions.forEach { def -> writeDefinitionAndBindingsTags(def) } + } + + private fun writeTagFile(tagFileName: String): OutputStream { + val fileStream = codeGenerator.getNewFile(fileName = tagFileName) + fileStream.appendText("package $codeGenerationPackage\n") + return fileStream + } + + private fun writeModuleTag( + module: KoinMetaData.Module + ) { + if (module.alreadyGenerated == null){ + module.alreadyGenerated = resolver.isAlreadyExisting(module) + } + + if (module.alreadyGenerated == false){ + val tag = TagFactory.getTag(module) + if (tag !in alreadyDeclaredTags) { + if (isConfigCheckActive){ + val metaLine = MetaAnnotationFactory.generate(module) + writeMeta(metaLine) + } + writeTag(tag) + } + } + } + + private fun writeDefinitionAndBindingsTags( + def: KoinMetaData.Definition, + ) { + writeDefinitionTag(def) + def.bindings.forEach { writeBindingTag(def,it) } + if (def.isScoped() && def.scope is KoinMetaData.Scope.ClassScope){ + writeScopeTag(def.scope.type) + } + } + + private fun writeScopeTag( + scope: KSDeclaration + ) { + val name = scope.qualifiedName?.asString() + if (name !in typeWhiteList) { + val tag = TagFactory.getTag(scope) + val alreadyGenerated = resolver.isAlreadyExisting(tag) + if (tag !in alreadyDeclaredTags && !alreadyGenerated) { + writeTag(tag) + } + } + } + + private fun writeDefinitionTag( + definition: KoinMetaData.Definition + ) { + if (definition.alreadyGenerated == null){ + definition.alreadyGenerated = resolver.isAlreadyExisting(definition) + } + + if (!definition.isExpect && definition.alreadyGenerated == false){ + val tag = TagFactory.getTag(definition) + if (tag !in alreadyDeclaredTags) { + if (isConfigCheckActive){ + val metaLine = MetaAnnotationFactory.generate(definition) + writeMeta(metaLine) + } + writeTag(tag) + } + } + } + + private fun writeBindingTag( + def: KoinMetaData.Definition, + binding: KSDeclaration + ) { + val name = binding.qualifiedName?.asString() + if (name !in typeWhiteList) { + val tag = TagFactory.getTag(def, binding) + val alreadyGenerated = resolver.isAlreadyExisting(tag) + if (tag !in alreadyDeclaredTags && !alreadyGenerated) { + writeTag(tag) + } + } + } + + private fun writeTag( + tag: String + ) { + val line = prepareTagLine(tag) + fileStream.appendText(line) + alreadyDeclaredTags.add(tag) + } + + private fun writeMeta( + meta: String + ) { + fileStream.appendText("\n$meta") + } + + private fun writeImports() { + fileStream.appendText(""" + + import org.koin.meta.annotations.MetaDefinition + import org.koin.meta.annotations.MetaModule + """.trimIndent()) + } + + private fun prepareTagLine(tagName : String) : String { + return "\npublic class $TAG_PREFIX$tagName" + } +} diff --git a/projects/koin-ksp-compiler/src/jvmMain/kotlin/org/koin/compiler/metadata/MetaAnnotationFactory.kt b/projects/koin-ksp-compiler/src/jvmMain/kotlin/org/koin/compiler/metadata/MetaAnnotationFactory.kt new file mode 100644 index 00000000..dc561937 --- /dev/null +++ b/projects/koin-ksp-compiler/src/jvmMain/kotlin/org/koin/compiler/metadata/MetaAnnotationFactory.kt @@ -0,0 +1,58 @@ +package org.koin.compiler.metadata + +import org.koin.compiler.metadata.KoinMetaData.DependencyKind +import org.koin.compiler.type.typeWhiteList +import org.koin.meta.annotations.MetaDefinition +import org.koin.meta.annotations.MetaModule + +object MetaAnnotationFactory { + private val metaModule = MetaModule::class.simpleName!! + private val metaDefinition = MetaDefinition::class.simpleName!! + private val whiteListTags = typeWhiteList.map { TagFactory.getTagFromFullPath(it) } + + fun generate(module: KoinMetaData.Module): String { + val fullpath = module.packageName + "." + module.name + + val includesTags = if (module.includes?.isNotEmpty() == true) { + module.includes.joinToString("\",\"", prefix = "\"", postfix = "\"") { TagFactory.getTag(it) } + } else null + val includesString = includesTags?.let { ", includes=[$it]" } ?: "" + + return """ + @$metaModule("$fullpath"$includesString) + """.trimIndent() + } + + fun generate(def: KoinMetaData.Definition): String { + val fullpath = def.packageName + "." + def.label + val dependencies = def.parameters.filterIsInstance() + + val cleanedDependencies = dependencies + .filter { !it.alreadyProvided && !it.hasDefault && !it.isNullable } + .mapNotNull { + if (it.kind == DependencyKind.Single) TagFactory.getTag(it) + else { + val ksDeclaration = extractLazyOrListType(it) + ksDeclaration?.let { TagFactory.getTag(def, ksDeclaration) } + } + } + .filter { it !in whiteListTags } + + val depsTags = if (cleanedDependencies.isNotEmpty()) cleanedDependencies.joinToString( + "\",\"", + prefix = "\"", + postfix = "\"" + ) else null + + val depsString = depsTags?.let { ", dependencies=[$it]" } ?: "" + + val scopeDef = if (def.isScoped()) def.scope?.getTagValue()?.camelCase() else null + val scopeString = scopeDef?.let { ", scope=\"$it\"" } ?: "" + return """ + @$metaDefinition("$fullpath"$depsString$scopeString) + """.trimIndent() + } + + private fun extractLazyOrListType(it: KoinMetaData.DefinitionParameter.Dependency) = + it.type.arguments.first().type?.resolve()?.declaration +} \ No newline at end of file diff --git a/projects/koin-ksp-compiler/src/jvmMain/kotlin/org/koin/compiler/metadata/TagFactory.kt b/projects/koin-ksp-compiler/src/jvmMain/kotlin/org/koin/compiler/metadata/TagFactory.kt new file mode 100644 index 00000000..0be8a032 --- /dev/null +++ b/projects/koin-ksp-compiler/src/jvmMain/kotlin/org/koin/compiler/metadata/TagFactory.kt @@ -0,0 +1,95 @@ +package org.koin.compiler.metadata + +import com.google.devtools.ksp.symbol.KSClassDeclaration +import com.google.devtools.ksp.symbol.KSDeclaration +import org.koin.compiler.scanner.ext.filterForbiddenKeywords +import org.koin.compiler.scanner.ext.getPackageName +import org.koin.compiler.type.clearPackageSymbols +import org.koin.compiler.verify.DefinitionVerification +import org.koin.compiler.verify.qualifiedNameCamelCase +import java.util.* + +const val KOIN_TAG_SEPARATOR = "_" +private const val QUALIFIER_SYMBOL = "Q_" +private const val SCOPE_SYMBOL = "S_" + +object TagFactory { + + fun getTag(module: KoinMetaData.Module): String { + return with(module) { + listOfNotNull( + packageName.camelCase().clearPackageSymbols() + name, + if (isExpect) "Expect" else null, + if (isActual) "Actual" else null + ).joinToString(separator = KOIN_TAG_SEPARATOR) + } + } + + fun getTag(module: KoinMetaData.ModuleInclude): String { + return with(module) { + listOfNotNull( + packageName.camelCase().clearPackageSymbols() + className.replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() }, + if (isExpect) "Expect" else null, + if (isActual) "Actual" else null + ).joinToString(separator = KOIN_TAG_SEPARATOR) + } + } + + fun getTag(definition: KoinMetaData.Definition, clazz: KSDeclaration): String { + return with(definition) { + listOfNotNull( + clazz.qualifiedNameCamelCase()?.clearPackageSymbols() ?: "", + qualifier?.let { "$QUALIFIER_SYMBOL${escapeTagClass(it)}" }, + scope?.getTagValue()?.camelCase()?.let { "$SCOPE_SYMBOL$it" }, + if (isExpect) "Expect" else null, + if (isActual) "Actual" else null + ).joinToString(separator = KOIN_TAG_SEPARATOR) + } + } + + fun getTag(classTag: String, dv: DefinitionVerification) = "$classTag$KOIN_TAG_SEPARATOR$SCOPE_SYMBOL${dv.scope}" + + fun getTag(clazz: KSDeclaration): String = clazz.qualifiedNameCamelCase() ?: "" + + fun getTag(definition: KoinMetaData.Definition): String { + return with(definition) { + listOfNotNull( + packageName.camelCase().clearPackageSymbols() + label.replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() }, + qualifier?.let { "$QUALIFIER_SYMBOL${escapeTagClass(it)}" }, + scope?.getTagValue()?.camelCase()?.let { "$SCOPE_SYMBOL$it" }, + if (isExpect) "Expect" else null, + if (isActual) "Actual" else null + ).joinToString(separator = KOIN_TAG_SEPARATOR) + } + } + + fun getTag(dep: KoinMetaData.DefinitionParameter.Dependency): String { + return with(dep) { + val ksClassDeclaration = (dep.type.declaration as KSClassDeclaration) + val packageName = ksClassDeclaration.getPackageName().filterForbiddenKeywords() + val className = ksClassDeclaration.simpleName.asString() + val isExpect = ksClassDeclaration.isExpect + val isActual = ksClassDeclaration.isActual + + val packageNameConsolidated = + packageName.clearPackageSymbols().camelCase() + className.replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() } + listOfNotNull( + packageNameConsolidated, + qualifier?.let { "$QUALIFIER_SYMBOL${escapeTagClass(it)}" }, + if (isExpect) "Expect" else null, + if (isActual) "Actual" else null + ).joinToString(separator = KOIN_TAG_SEPARATOR) + } + } + + private fun escapeTagClass(qualifier : String) : String { + return if (!qualifier.contains(".")) qualifier + else { + qualifier.split(".").joinToString("") { it.capitalize() } + } + } + + fun getTagFromFullPath(path: String) : String { + return path.split(".").joinToString(separator = "") { it.capitalize() } + } +} \ No newline at end of file diff --git a/projects/koin-ksp-compiler/src/jvmMain/kotlin/org/koin/compiler/resolver/ResolverExt.kt b/projects/koin-ksp-compiler/src/jvmMain/kotlin/org/koin/compiler/resolver/ResolverExt.kt new file mode 100644 index 00000000..be80da6b --- /dev/null +++ b/projects/koin-ksp-compiler/src/jvmMain/kotlin/org/koin/compiler/resolver/ResolverExt.kt @@ -0,0 +1,36 @@ +package org.koin.compiler.resolver + +import com.google.devtools.ksp.processing.Resolver +import com.google.devtools.ksp.symbol.KSDeclaration +import org.koin.compiler.metadata.KoinMetaData +import org.koin.compiler.metadata.TagFactory +import org.koin.compiler.metadata.TAG_PREFIX +import org.koin.compiler.verify.codeGenerationPackage + +fun Resolver.isAlreadyExisting(mod : KoinMetaData.Module) : Boolean { + return getResolution(mod) != null +} + +fun Resolver.getResolution(mod : KoinMetaData.Module) : KSDeclaration?{ + return getResolutionForTag(TagFactory.getTag(mod)) +} + +fun Resolver.isAlreadyExisting(def : KoinMetaData.Definition) : Boolean { + return getResolution(def) != null +} + +fun Resolver.getResolution(def : KoinMetaData.Definition) : KSDeclaration?{ + return getResolutionForTag(TagFactory.getTag(def)) +} + +fun Resolver.isAlreadyExisting(tag : String?) : Boolean { + return getResolutionForTag(tag) != null +} + +fun Resolver.getResolutionForTag(tag : String?) : KSDeclaration?{ + return getResolutionForClass("$codeGenerationPackage.$TAG_PREFIX$tag") +} + +fun Resolver.getResolutionForClass(name : String) : KSDeclaration?{ + return getClassDeclarationByName(getKSNameFromString(name)) +} \ No newline at end of file diff --git a/projects/koin-ksp-compiler/src/jvmMain/kotlin/org/koin/compiler/scanner/ClassComponentScanner.kt b/projects/koin-ksp-compiler/src/jvmMain/kotlin/org/koin/compiler/scanner/ClassComponentScanner.kt index 89f800db..e009a741 100644 --- a/projects/koin-ksp-compiler/src/jvmMain/kotlin/org/koin/compiler/scanner/ClassComponentScanner.kt +++ b/projects/koin-ksp-compiler/src/jvmMain/kotlin/org/koin/compiler/scanner/ClassComponentScanner.kt @@ -56,24 +56,23 @@ class ClassComponentScanner( val ctorParams = ksClassDeclaration.primaryConstructor?.parameters?.getParameters() val isExpect = ksClassDeclaration.isExpect -// val isActual = ksClassDeclaration.isActual -// LOGGER.info("definition - $packageName $className - isExpect:$isExpect isActual:$isActual") + val isActual = ksClassDeclaration.isActual return when (annotationName) { SINGLE.annotationName -> { - createSingleDefinition(annotation, packageName, qualifier, className, ctorParams, allBindings, isExpect) + createSingleDefinition(annotation, packageName, qualifier, className, ctorParams, allBindings, isExpect, isActual = isActual) } SINGLETON.annotationName -> { - createSingleDefinition(annotation, packageName, qualifier, className, ctorParams, allBindings, isExpect) + createSingleDefinition(annotation, packageName, qualifier, className, ctorParams, allBindings, isExpect, isActual = isActual) } FACTORY.annotationName -> { - createClassDefinition(FACTORY,packageName, qualifier, className, ctorParams, allBindings, isExpect = isExpect) + createClassDefinition(FACTORY,packageName, qualifier, className, ctorParams, allBindings, isExpect = isExpect, isActual = isActual) } - KOIN_VIEWMODEL.annotationName -> { - createClassDefinition(KOIN_VIEWMODEL,packageName, qualifier, className, ctorParams, allBindings, isExpect = isExpect) + KOIN_VIEWMODEL_ANDROID.annotationName -> { + createClassDefinition(KOIN_VIEWMODEL_ANDROID,packageName, qualifier, className, ctorParams, allBindings, isExpect = isExpect, isActual = isActual) } KOIN_WORKER.annotationName -> { - createClassDefinition(KOIN_WORKER,packageName, qualifier, className, ctorParams, allBindings, isExpect = isExpect) + createClassDefinition(KOIN_WORKER,packageName, qualifier, className, ctorParams, allBindings, isExpect = isExpect, isActual = isActual) } SCOPE.annotationName -> { val scopeData : KoinMetaData.Scope = annotation.arguments.getScope() @@ -81,7 +80,7 @@ class ClassComponentScanner( val extraAnnotation = annotations[extraAnnotationDefinition?.annotationName] val extraDeclaredBindings = extraAnnotation?.let { declaredBindings(it) } val extraScopeBindings = if(extraDeclaredBindings?.hasDefaultUnitValue() == false) extraDeclaredBindings else allBindings - createClassDefinition(extraAnnotationDefinition ?: SCOPE,packageName, qualifier, className, ctorParams, extraScopeBindings,scope = scopeData, isExpect = isExpect) + createClassDefinition(extraAnnotationDefinition ?: SCOPE,packageName, qualifier, className, ctorParams, extraScopeBindings,scope = scopeData, isExpect = isExpect, isActual = isActual) } else -> error("Unknown annotation type: $annotationName") } @@ -95,10 +94,11 @@ class ClassComponentScanner( ctorParams: List?, allBindings: List, isExpect : Boolean, + isActual : Boolean ): KoinMetaData.Definition.ClassDefinition { val createdAtStart: Boolean = annotation.arguments.firstOrNull { it.name?.asString() == "createdAtStart" }?.value as Boolean? ?: false - return createClassDefinition(SINGLE, packageName, qualifier, className, ctorParams, allBindings, isCreatedAtStart = createdAtStart, isExpect= isExpect) + return createClassDefinition(SINGLE, packageName, qualifier, className, ctorParams, allBindings, isCreatedAtStart = createdAtStart, isExpect= isExpect, isActual = isActual) } private fun createClassDefinition( @@ -111,6 +111,7 @@ class ClassComponentScanner( isCreatedAtStart : Boolean? = null, scope: KoinMetaData.Scope? = null, isExpect : Boolean, + isActual : Boolean ): KoinMetaData.Definition.ClassDefinition { return KoinMetaData.Definition.ClassDefinition( packageName = packageName, @@ -121,7 +122,8 @@ class ClassComponentScanner( bindings = allBindings, keyword = keyword, scope = scope, - isExpect = isExpect + isExpect = isExpect, + isActual = isActual ) } } \ No newline at end of file diff --git a/projects/koin-ksp-compiler/src/jvmMain/kotlin/org/koin/compiler/scanner/FunctionScanner.kt b/projects/koin-ksp-compiler/src/jvmMain/kotlin/org/koin/compiler/scanner/FunctionScanner.kt index 6eec89f7..f5ef6010 100644 --- a/projects/koin-ksp-compiler/src/jvmMain/kotlin/org/koin/compiler/scanner/FunctionScanner.kt +++ b/projects/koin-ksp-compiler/src/jvmMain/kotlin/org/koin/compiler/scanner/FunctionScanner.kt @@ -45,27 +45,28 @@ abstract class FunctionScanner( val allBindings: List = returnedType?.let { foundBindings + it } ?: foundBindings val functionParameters = ksFunctionDeclaration.parameters.getParameters() val isExpect = ksFunctionDeclaration.isExpect + val isActual = ksFunctionDeclaration.isActual return when (annotationName) { SINGLE.annotationName -> { - createSingleDefinition(annotation, packageName, qualifier, functionName, functionParameters, allBindings, isExpect) + createSingleDefinition(annotation, packageName, qualifier, functionName, functionParameters, allBindings, isExpect, isActual = isActual) } SINGLETON.annotationName -> { - createSingleDefinition(annotation, packageName, qualifier, functionName, functionParameters, allBindings, isExpect) + createSingleDefinition(annotation, packageName, qualifier, functionName, functionParameters, allBindings, isExpect, isActual = isActual) } FACTORY.annotationName -> { - createDefinition(FACTORY,packageName,qualifier,functionName,functionParameters,allBindings, isExpect = isExpect) + createDefinition(FACTORY,packageName,qualifier,functionName,functionParameters,allBindings, isExpect = isExpect, isActual = isActual) } - KOIN_VIEWMODEL.annotationName -> { - createDefinition(KOIN_VIEWMODEL,packageName,qualifier,functionName,functionParameters,allBindings, isExpect = isExpect) + KOIN_VIEWMODEL_ANDROID.annotationName -> { + createDefinition(KOIN_VIEWMODEL_ANDROID,packageName,qualifier,functionName,functionParameters,allBindings, isExpect = isExpect, isActual = isActual) } KOIN_WORKER.annotationName -> { - createDefinition(KOIN_WORKER,packageName,qualifier,functionName,functionParameters,allBindings, isExpect = isExpect) + createDefinition(KOIN_WORKER,packageName,qualifier,functionName,functionParameters,allBindings, isExpect = isExpect, isActual = isActual) } SCOPE.annotationName -> { val scopeData : KoinMetaData.Scope = annotation.arguments.getScope() val extraAnnotation = getExtraScopeAnnotation(annotations) - createDefinition(extraAnnotation ?: SCOPE,packageName,qualifier,functionName,functionParameters,allBindings,scope = scopeData, isExpect = isExpect) + createDefinition(extraAnnotation ?: SCOPE,packageName,qualifier,functionName,functionParameters,allBindings,scope = scopeData, isExpect = isExpect, isActual = isActual) } else -> null } @@ -78,7 +79,8 @@ abstract class FunctionScanner( functionName: String, functionParameters: List, allBindings: List, - isExpect : Boolean + isExpect : Boolean, + isActual : Boolean ): KoinMetaData.Definition.FunctionDefinition { val createdAtStart: Boolean = annotation.arguments.firstOrNull { it.name?.asString() == "createdAtStart" }?.value as Boolean? @@ -91,7 +93,8 @@ abstract class FunctionScanner( functionParameters, allBindings, isCreatedAtStart = createdAtStart, - isExpect = isExpect + isExpect = isExpect, + isActual = isActual ) } @@ -104,7 +107,8 @@ abstract class FunctionScanner( allBindings: List, isCreatedAtStart : Boolean? = null, scope: KoinMetaData.Scope? = null, - isExpect : Boolean + isExpect : Boolean, + isActual : Boolean ): KoinMetaData.Definition.FunctionDefinition { return KoinMetaData.Definition.FunctionDefinition( packageName = packageName, @@ -115,7 +119,8 @@ abstract class FunctionScanner( bindings = allBindings, keyword = keyword, scope = scope, - isExpect = isExpect + isExpect = isExpect, + isActual = isActual ).apply { isClassFunction = isModuleFunction } } diff --git a/projects/koin-ksp-compiler/src/jvmMain/kotlin/org/koin/compiler/scanner/KoinMetaDataScanner.kt b/projects/koin-ksp-compiler/src/jvmMain/kotlin/org/koin/compiler/scanner/KoinMetaDataScanner.kt index 75b5e7c6..00d18521 100644 --- a/projects/koin-ksp-compiler/src/jvmMain/kotlin/org/koin/compiler/scanner/KoinMetaDataScanner.kt +++ b/projects/koin-ksp-compiler/src/jvmMain/kotlin/org/koin/compiler/scanner/KoinMetaDataScanner.kt @@ -29,6 +29,7 @@ import org.koin.compiler.util.anyMatch import org.koin.core.annotation.Definition import org.koin.core.annotation.Module import org.koin.core.annotation.PropertyValue +import org.koin.meta.annotations.ExternalDefinition class KoinMetaDataScanner( private val logger: KSPLogger @@ -38,24 +39,10 @@ class KoinMetaDataScanner( private val componentMetadataScanner = ClassComponentScanner(logger) private val functionMetadataScanner = FunctionComponentScanner(logger) - private var validModuleSymbols = mutableListOf() - private var validDefinitionSymbols = mutableListOf() - private var defaultProperties = mutableListOf() - private var externalDefinitions = listOf() + fun findInvalidSymbols(resolver: Resolver): List { + val invalidModuleSymbols = resolver.getInvalidModuleSymbols() + val invalidDefinitionSymbols = resolver.getInvalidDefinitionSymbols() - - @OptIn(KspExperimental::class) - fun scanSymbols(resolver: Resolver): List { - val moduleSymbols = resolver.getSymbolsWithAnnotation(Module::class.qualifiedName!!).toList() - val definitionSymbols = DEFINITION_ANNOTATION_LIST_TYPES.flatMap { annotation -> - resolver.getSymbolsWithAnnotation(annotation.qualifiedName!!) - } - - validModuleSymbols.addAll(moduleSymbols.filter { it.validate() }) - validDefinitionSymbols.addAll(definitionSymbols.filter { it.validate() }) - - val invalidModuleSymbols = moduleSymbols.filter { !it.validate() } - val invalidDefinitionSymbols = definitionSymbols.filter { !it.validate() } val invalidSymbols = invalidModuleSymbols + invalidDefinitionSymbols if (invalidSymbols.isNotEmpty()) { logger.logging("Invalid definition symbols found.") @@ -63,28 +50,28 @@ class KoinMetaDataScanner( return invalidSymbols } - val propertyValueSymbols = resolver.getSymbolsWithAnnotation(PropertyValue::class.qualifiedName!!).toList() - defaultProperties.addAll(propertyValueSymbols.filter { it.validate() }) - - externalDefinitions = resolver.getDeclarationsFromPackage("org.koin.ksp.generated") - .filter { a -> a.annotations.any { it.shortName.asString() == DEFINITION_ANNOTATION } } - .toList() - return emptyList() } - fun scanKoinModules(defaultModule: KoinMetaData.Module): List { - val moduleList = scanClassModules() + fun scanKoinModules( + defaultModule: KoinMetaData.Module, + resolver: Resolver + ): List { + val moduleList = scanClassModules(resolver) val index = moduleList.generateScanComponentIndex() - scanClassComponents(defaultModule, index) - scanFunctionComponents(defaultModule, index) - scanDefaultProperties(index+defaultModule) - scanExternalDefinitions(index) + scanClassComponents(defaultModule, index, resolver) + scanFunctionComponents(defaultModule, index, resolver) + scanDefaultProperties(index+defaultModule, resolver) + scanExternalDefinitions(index, resolver) return moduleList } - private fun scanDefaultProperties(index: List) { - val propertyValues: List = defaultProperties.mapNotNull { def -> + private fun scanDefaultProperties( + index: List, + resolver: Resolver + ) { + val propertyValues: List = resolver.getValidPropertySymbols() + .mapNotNull { def -> def.annotations .first { it.shortName.asString() == PropertyValue::class.simpleName } .let { a -> @@ -108,8 +95,11 @@ class KoinMetaDataScanner( } } - private fun scanExternalDefinitions(index: List) { - externalDefinitions + private fun scanExternalDefinitions( + index: List, + resolver: Resolver + ) { + resolver.getExternalDefinitionSymbols() .filter { !it.isExpect } .mapNotNull { definitionDeclaration -> definitionDeclaration.annotations @@ -124,9 +114,9 @@ class KoinMetaDataScanner( } } - private fun scanClassModules(): List { + private fun scanClassModules(resolver: Resolver): List { logger.logging("scan modules ...") - return validModuleSymbols + return resolver.getValidModuleSymbols() .filterIsInstance() .map { moduleMetadataScanner.createClassModule(it) } .toList() @@ -153,11 +143,12 @@ class KoinMetaDataScanner( private fun scanFunctionComponents( defaultModule: KoinMetaData.Module, - scanComponentIndex: List + scanComponentIndex: List, + resolver: Resolver ): List { logger.logging("scan functions ...") - val definitions = validDefinitionSymbols + val definitions = resolver.getValidDefinitionSymbols() .filterIsInstance() .mapNotNull { functionMetadataScanner.createFunctionDefinition(it) } .toList() @@ -168,11 +159,12 @@ class KoinMetaDataScanner( private fun scanClassComponents( defaultModule: KoinMetaData.Module, - scanComponentIndex: List + scanComponentIndex: List, + resolver: Resolver ): List { logger.logging("scan definitions ...") - val definitions = validDefinitionSymbols + val definitions = resolver.getValidDefinitionSymbols() .filterIsInstance() .map { componentMetadataScanner.createClassDefinition(it) } .toList() @@ -201,7 +193,47 @@ class KoinMetaDataScanner( private fun logInvalidEntities(classDeclarationList: List) { classDeclarationList.forEach { logger.logging("Invalid entity: $it") } } + + private fun Resolver.getInvalidModuleSymbols(): List { + return this.getSymbolsWithAnnotation(Module::class.qualifiedName!!) + .filter { !it.validate() } + .toList() + } + + private fun Resolver.getInvalidDefinitionSymbols(): List { + return DEFINITION_ANNOTATION_LIST_TYPES.flatMap { annotation -> + this.getSymbolsWithAnnotation(annotation.qualifiedName!!) + .filter { !it.validate() } + } + } + + private fun Resolver.getValidModuleSymbols(): List { + return this.getSymbolsWithAnnotation(Module::class.qualifiedName!!) + .filter { it.validate() } + .toList() + } + + private fun Resolver.getValidDefinitionSymbols(): List { + return DEFINITION_ANNOTATION_LIST_TYPES.flatMap { annotation -> + this.getSymbolsWithAnnotation(annotation.qualifiedName!!) + .filter { it.validate() } + } + } + + private fun Resolver.getValidPropertySymbols(): List { + return this.getSymbolsWithAnnotation(PropertyValue::class.qualifiedName!!) + .filter { it.validate() } + .toList() + } + + @OptIn(KspExperimental::class) + private fun Resolver.getExternalDefinitionSymbols(): List { + return this.getDeclarationsFromPackage("org.koin.ksp.generated") + .filter { a -> a.annotations.any { it.shortName.asString() == DEFINITION_ANNOTATION } } + .toList() + } + companion object { - private val DEFINITION_ANNOTATION = Definition::class.simpleName + private val DEFINITION_ANNOTATION = ExternalDefinition::class.simpleName } } diff --git a/projects/koin-ksp-compiler/src/jvmMain/kotlin/org/koin/compiler/scanner/KoinTagMetaDataScanner.kt b/projects/koin-ksp-compiler/src/jvmMain/kotlin/org/koin/compiler/scanner/KoinTagMetaDataScanner.kt new file mode 100644 index 00000000..9217f435 --- /dev/null +++ b/projects/koin-ksp-compiler/src/jvmMain/kotlin/org/koin/compiler/scanner/KoinTagMetaDataScanner.kt @@ -0,0 +1,71 @@ +/* + * Copyright 2017-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.koin.compiler.scanner + +import com.google.devtools.ksp.processing.KSPLogger +import com.google.devtools.ksp.processing.Resolver +import com.google.devtools.ksp.symbol.KSAnnotated +import com.google.devtools.ksp.symbol.KSAnnotation +import com.google.devtools.ksp.validate +import org.koin.meta.annotations.MetaDefinition +import org.koin.meta.annotations.MetaModule + +class KoinTagMetaDataScanner( + private val logger: KSPLogger, + private val resolver: Resolver +) { + + fun findInvalidSymbols(): List { + val invalidModuleSymbols = resolver.getMetaModuleSymbols(isValid = false) + val invalidDefinitionSymbols = resolver.getMetaDefinitionSymbols(isValid = false) + + val invalidSymbols = invalidModuleSymbols + invalidDefinitionSymbols + if (invalidSymbols.isNotEmpty()) { + logger.logging("Invalid definition symbols found.") + logInvalidEntities(invalidSymbols) + return invalidSymbols + } + + return emptyList() + } + + fun findMetaModules(): List { + logger.warn("scan meta modules ...") + return resolver.getMetaModuleSymbols(isValid = true).map { it.annotations.first() } + } + + fun findMetaDefinitions(): List { + logger.warn("scan meta definitions ...") + return resolver.getMetaDefinitionSymbols(isValid = true).map { it.annotations.first() } + } + + private fun Resolver.getMetaModuleSymbols(isValid : Boolean): List { + return this.getSymbolsWithAnnotation(MetaModule::class.qualifiedName!!) + .filter { isValid == it.validate() } + .toList() + } + + private fun Resolver.getMetaDefinitionSymbols(isValid : Boolean): List { + return this.getSymbolsWithAnnotation(MetaDefinition::class.qualifiedName!!) + .filter { isValid == it.validate() } + .toList() + } + + private fun logInvalidEntities(classDeclarationList: List) { + classDeclarationList.forEach { logger.logging("Invalid entity: $it") } + } + +} diff --git a/projects/koin-ksp-compiler/src/jvmMain/kotlin/org/koin/compiler/scanner/ModuleScanner.kt b/projects/koin-ksp-compiler/src/jvmMain/kotlin/org/koin/compiler/scanner/ModuleScanner.kt index d07a759e..62041cc4 100644 --- a/projects/koin-ksp-compiler/src/jvmMain/kotlin/org/koin/compiler/scanner/ModuleScanner.kt +++ b/projects/koin-ksp-compiler/src/jvmMain/kotlin/org/koin/compiler/scanner/ModuleScanner.kt @@ -114,7 +114,7 @@ class ModuleScanner( private fun KSDeclaration.mapModuleInclude(): KoinMetaData.ModuleInclude { val packageName: String = packageName.asString() val className = simpleName.asString() - return KoinMetaData.ModuleInclude(packageName, className, isExpect) + return KoinMetaData.ModuleInclude(packageName, className, isExpect, isActual) } } diff --git a/projects/koin-ksp-compiler/src/jvmMain/kotlin/org/koin/compiler/scanner/ext/KspExt.kt b/projects/koin-ksp-compiler/src/jvmMain/kotlin/org/koin/compiler/scanner/ext/KspExt.kt index 3c68c9c7..c3050bcb 100644 --- a/projects/koin-ksp-compiler/src/jvmMain/kotlin/org/koin/compiler/scanner/ext/KspExt.kt +++ b/projects/koin-ksp-compiler/src/jvmMain/kotlin/org/koin/compiler/scanner/ext/KspExt.kt @@ -19,6 +19,7 @@ import com.google.devtools.ksp.symbol.* import org.koin.compiler.metadata.KoinMetaData import org.koin.compiler.metadata.isScopeAnnotation import org.koin.compiler.metadata.isValidAnnotation +import org.koin.compiler.type.forbiddenKeywords import org.koin.core.annotation.* fun KSAnnotated.getKoinAnnotations(): Map { @@ -73,14 +74,11 @@ fun List.getQualifier(): KoinMetaData.Qualifier { ?: error("Qualifier annotation needs parameters: either type value or name") } -private val qualifierAnnotations = listOf("Named", "Qualifier") +private val qualifierAnnotations = listOf(Named::class.simpleName, Qualifier::class.simpleName) fun KSAnnotated.getQualifier(): String? { val qualifierAnnotation = annotations.firstOrNull { a -> val annotationName = a.shortName.asString() - if (annotationName in qualifierAnnotations) true - else (a.annotationType.resolve().declaration as KSClassDeclaration).annotations.any { a2 -> - a2.shortName.asString() in qualifierAnnotations - } + annotationName in qualifierAnnotations || a.annotationType.resolve().isCustomQualifierAnnotation() } return qualifierAnnotation?.let { when(it.shortName.asString()){ @@ -127,23 +125,36 @@ private fun getParameter(param: KSValueParameter): KoinMetaData.DefinitionParame } //TODO type value for ScopeId else -> { - val kind = when { - isList -> KoinMetaData.DependencyKind.List - isLazy -> KoinMetaData.DependencyKind.Lazy - else -> KoinMetaData.DependencyKind.Single + val annotationType = firstAnnotation?.annotationType?.resolve() + if (annotationType != null && annotationType.isCustomQualifierAnnotation()) { + KoinMetaData.DefinitionParameter.Dependency(name = paramName, qualifier = annotationType.declaration.qualifiedName?.asString(), isNullable = isNullable, hasDefault = hasDefault, type = resolvedType, alreadyProvided = hasProvidedAnnotation(param)) + } else { + val kind = when { + isList -> KoinMetaData.DependencyKind.List + isLazy -> KoinMetaData.DependencyKind.Lazy + else -> KoinMetaData.DependencyKind.Single + } + KoinMetaData.DefinitionParameter.Dependency(name = paramName, hasDefault = hasDefault, kind = kind, isNullable = isNullable, type = resolvedType, alreadyProvided = hasProvidedAnnotation(param)) } - KoinMetaData.DefinitionParameter.Dependency(name = paramName, hasDefault = hasDefault, kind = kind, isNullable = isNullable, type = resolvedType, alreadyProvided = hasProvidedAnnotation(param)) } } } +private fun KSType.isCustomQualifierAnnotation(): Boolean { + return (declaration as KSClassDeclaration).annotations.any { it.shortName.asString() in qualifierAnnotations } +} + internal fun List.getValueArgument(): String? { return firstOrNull { a -> a.name?.asString() == "value" }?.value as? String? } +internal fun List.getScopeArgument(): String? { + return firstOrNull { a -> a.name?.asString() == "scope" }?.value as? String? +} + fun KSClassDeclaration.getPackageName() : String = packageName.asString() -val forbiddenKeywords = listOf("in","interface") + fun String.filterForbiddenKeywords() : String{ return split(".").joinToString(".") { if (it in forbiddenKeywords) "`$it`" else it diff --git a/projects/koin-ksp-compiler/src/jvmMain/kotlin/org/koin/compiler/type/BlockedTypes.kt b/projects/koin-ksp-compiler/src/jvmMain/kotlin/org/koin/compiler/type/BlockedTypes.kt new file mode 100644 index 00000000..3338a935 --- /dev/null +++ b/projects/koin-ksp-compiler/src/jvmMain/kotlin/org/koin/compiler/type/BlockedTypes.kt @@ -0,0 +1,5 @@ +package org.koin.compiler.type + +fun String.clearPackageSymbols() = replace("`","").replace("'","") + +val forbiddenKeywords = listOf("in","by","interface") \ No newline at end of file diff --git a/projects/koin-ksp-compiler/src/jvmMain/kotlin/org/koin/compiler/verify/TypeWhiteList.kt b/projects/koin-ksp-compiler/src/jvmMain/kotlin/org/koin/compiler/type/TypeWhiteList.kt similarity index 67% rename from projects/koin-ksp-compiler/src/jvmMain/kotlin/org/koin/compiler/verify/TypeWhiteList.kt rename to projects/koin-ksp-compiler/src/jvmMain/kotlin/org/koin/compiler/type/TypeWhiteList.kt index 903265ba..f2a49707 100644 --- a/projects/koin-ksp-compiler/src/jvmMain/kotlin/org/koin/compiler/verify/TypeWhiteList.kt +++ b/projects/koin-ksp-compiler/src/jvmMain/kotlin/org/koin/compiler/type/TypeWhiteList.kt @@ -1,4 +1,4 @@ -package org.koin.compiler.verify +package org.koin.compiler.type internal val typeWhiteList = listOf( @@ -9,5 +9,7 @@ internal val typeWhiteList = listOf( "androidx.appcompat.app.AppCompatActivity", "androidx.fragment.app.Fragment", "androidx.lifecycle.SavedStateHandle", - "androidx.lifecycle.ViewModel" + "androidx.lifecycle.ViewModel", + "androidx.work.WorkerParameters", + "org.koin.ktor.plugin.RequestScope" ) \ No newline at end of file diff --git a/projects/koin-ksp-compiler/src/jvmMain/kotlin/org/koin/compiler/verify/KoinConfigChecker.kt b/projects/koin-ksp-compiler/src/jvmMain/kotlin/org/koin/compiler/verify/KoinConfigChecker.kt index 97906dc3..a5241350 100644 --- a/projects/koin-ksp-compiler/src/jvmMain/kotlin/org/koin/compiler/verify/KoinConfigChecker.kt +++ b/projects/koin-ksp-compiler/src/jvmMain/kotlin/org/koin/compiler/verify/KoinConfigChecker.kt @@ -15,96 +15,78 @@ */ package org.koin.compiler.verify -import com.google.devtools.ksp.processing.CodeGenerator import com.google.devtools.ksp.processing.KSPLogger import com.google.devtools.ksp.processing.Resolver +import com.google.devtools.ksp.symbol.KSAnnotation import com.google.devtools.ksp.symbol.KSDeclaration -import org.koin.compiler.metadata.KoinMetaData -import org.koin.compiler.verify.ext.getResolutionForTag +import com.google.devtools.ksp.symbol.KSValueArgument +import org.koin.compiler.metadata.TagFactory +import org.koin.compiler.resolver.isAlreadyExisting +import org.koin.compiler.scanner.ext.getScopeArgument +import org.koin.compiler.scanner.ext.getValueArgument +import org.koin.compiler.type.clearPackageSymbols const val codeGenerationPackage = "org.koin.ksp.generated" +data class DefinitionVerification(val value: String, val dependencies: ArrayList?, val scope: String?) + /** * Koin Configuration Checker */ -class KoinConfigChecker(val codeGenerator: CodeGenerator, val logger: KSPLogger) { +class KoinConfigChecker(val logger: KSPLogger, val resolver: Resolver) { - fun verifyDefinitionDeclarations( - moduleList: List, - resolver: Resolver - ) { - val isAlreadyGenerated = codeGenerator.generatedFile.isEmpty() - val allDefinitions = moduleList.flatMap { it.definitions } - if (isAlreadyGenerated) { - verifyDependencies(allDefinitions, resolver) - } + fun verifyMetaModules(metaModules: List) { + metaModules + .mapNotNull(::extractMetaModuleValues) + .forEach { (value,includes) -> + if (!includes.isNullOrEmpty()) verifyMetaModule(value,includes) + } } - private fun verifyDependencies( - allDefinitions: List, - resolver: Resolver - ) { - allDefinitions.forEach { def -> - def.parameters - .filterIsInstance() - .forEach { param -> - checkDependency(param, resolver, def) - //TODO Check Cycle - } + private fun verifyMetaModule(value: String, includes: ArrayList) { + includes.forEach { i -> + val exists = resolver.isAlreadyExisting(i) + if (!exists) { + logger.error("--> Missing Module Definition :'${i}' included in '$value'. Fix your configuration: add @Module annotation on the class.") + } } } - private fun checkDependency( - param: KoinMetaData.DefinitionParameter.Dependency, - resolver: Resolver, - def: KoinMetaData.Definition - ) { - if (!param.hasDefault && !param.isNullable && !param.alreadyProvided) { - checkDependencyIsDefined(param, resolver, def) - } + private fun extractMetaModuleValues(a: KSAnnotation): Pair?>? { + val value = a.arguments.getValueArgument() + val includes = if (value != null) a.arguments.getArray("includes") else null + return value?.let { it to includes } } - - private fun checkDependencyIsDefined( - dependencyToCheck: KoinMetaData.DefinitionParameter.Dependency, - resolver: Resolver, - definition: KoinMetaData.Definition, - ) { - val label = definition.label - val scope = (definition.scope as? KoinMetaData.Scope.ClassScope)?.type?.qualifiedName?.asString() - var targetTypeToCheck: KSDeclaration = dependencyToCheck.type.declaration - - if (targetTypeToCheck.simpleName.asString() == "List" || targetTypeToCheck.simpleName.asString() == "Lazy") { - targetTypeToCheck = - dependencyToCheck.type.arguments.firstOrNull()?.type?.resolve()?.declaration ?: targetTypeToCheck - } - - val parameterFullName = targetTypeToCheck.qualifiedName?.asString() - if (parameterFullName !in typeWhiteList && parameterFullName != null) { - val cn = targetTypeToCheck.qualifiedNameCamelCase() - val resolution = resolver.getResolutionForTag(cn) - val isNotScopeType = scope != parameterFullName - if (resolution == null && isNotScopeType) { - logger.error("--> Missing Definition type '$parameterFullName' for '${definition.packageName}.$label'. Fix your configuration to define type '${targetTypeToCheck.simpleName.asString()}'.") + fun verifyMetaDefinitions(metaDefinitions: List) { + metaDefinitions + .mapNotNull(::extractMetaDefinitionValues) + .forEach { + if (!it.dependencies.isNullOrEmpty()) verifyMetaDefinition(it) } - } } - fun verifyModuleIncludes(modules: List, resolver: Resolver) { - val noGenFile = codeGenerator.generatedFile.isEmpty() - if (noGenFile) { - modules.forEach { m -> - val mn = m.packageName + "." + m.name - m.includes?.forEach { inc -> - val prop = resolver.getResolutionForTag(inc.getTagName()) - if (prop == null) { - logger.error("--> Module Undefined :'${inc.className}' included in '$mn'. Fix your configuration: add @Module annotation on '${inc.className}' class.") - } - } + private fun verifyMetaDefinition(dv : DefinitionVerification) { + dv.dependencies?.forEach { i -> + val tag = i.clearPackageSymbols() + val exists = if (dv.scope == null) resolver.isAlreadyExisting(tag) else resolver.isAlreadyExisting(tag) || resolver.isAlreadyExisting(TagFactory.getTag(tag,dv)) + if (!exists) { + logger.error("--> Missing Definition :'${i}' used by '${dv.value}'. Fix your configuration: add definition annotation on the class.") } } } + + private fun extractMetaDefinitionValues(a: KSAnnotation): DefinitionVerification? { + val value = a.arguments.getValueArgument() + val includes = if (value != null) a.arguments.getArray("dependencies") else null + val scope = if (value != null) a.arguments.getScopeArgument() else null + return value?.let { DefinitionVerification(value,includes,scope) } + } + + private fun List.getArray(name : String): ArrayList? { + return firstOrNull { a -> a.name?.asString() == name }?.value as? ArrayList? + } } internal fun KSDeclaration.qualifiedNameCamelCase() = qualifiedName?.asString()?.split(".")?.joinToString(separator = "") { it.capitalize() } diff --git a/projects/koin-ksp-compiler/src/jvmMain/kotlin/org/koin/compiler/verify/KoinTagWriter.kt b/projects/koin-ksp-compiler/src/jvmMain/kotlin/org/koin/compiler/verify/KoinTagWriter.kt deleted file mode 100644 index 03f1931c..00000000 --- a/projects/koin-ksp-compiler/src/jvmMain/kotlin/org/koin/compiler/verify/KoinTagWriter.kt +++ /dev/null @@ -1,150 +0,0 @@ -package org.koin.compiler.verify - -import org.koin.compiler.generator.ext.appendText -import com.google.devtools.ksp.processing.CodeGenerator -import com.google.devtools.ksp.processing.KSPLogger -import com.google.devtools.ksp.processing.Resolver -import com.google.devtools.ksp.symbol.KSDeclaration -import org.koin.compiler.generator.ext.getNewFile -import org.koin.compiler.metadata.KoinMetaData -import org.koin.compiler.verify.ext.getResolution -import org.koin.compiler.verify.ext.getResolutionForTag -import java.io.OutputStream -import java.nio.file.Files -import java.security.DigestOutputStream -import java.security.MessageDigest -import kotlin.io.path.createTempFile -import kotlin.io.path.outputStream - -const val tagPrefix = "KoinDef" - -class KoinTagWriter(val codeGenerator: CodeGenerator, val logger: KSPLogger) { - - lateinit var resolver: Resolver - - fun writeAllTags( - moduleList: List, - default : KoinMetaData.Module - ) { - val isAlreadyGenerated = codeGenerator.generatedFile.isEmpty() - if (!isAlreadyGenerated) { - logger.logging("Koin Tags Generation ...") - createTagFile(moduleList, default) - } - } - - /** - * To realize [reproducible-builds](https://reproducible-builds.org/), write everything to a temporal file - * then copy it to the tag file. - * By this method, we can compute the digest of tag file and use it to name it. - * - * @author Kengo TODA - */ - @OptIn(ExperimentalStdlibApi::class) - private fun createTagFile( - moduleList: List, - default : KoinMetaData.Module, - ) { - val allDefinitions = (moduleList + default).flatMap { it.definitions } - val tempFile = createTempFile("KoinMeta", ".kt") - val sha256 = MessageDigest.getInstance("SHA-256"); - DigestOutputStream(tempFile.outputStream(), sha256).buffered().use { - writeModuleTags(moduleList, it) - writeDefinitionsTags(allDefinitions, it) - } - - val tagFileName = "KoinMeta-${sha256.digest().toHexString(HexFormat.Default)}" - writeTagFile(tagFileName).buffered().use { - Files.copy(tempFile, it) - } - } - - private fun writeModuleTags(allModules: List, tagFileStream : OutputStream) { - val alreadyDeclaredTags = arrayListOf() - allModules.forEach { m -> writeModuleTag(tagFileStream,m,alreadyDeclaredTags) } - } - - private fun writeDefinitionsTags(allDefinitions: List, tagFileStream : OutputStream) { - val alreadyDeclaredTags = arrayListOf() - - allDefinitions.forEach { def -> writeDefinitionTag(tagFileStream, def, alreadyDeclaredTags) } - } - - private fun writeTagFile(tagFileName: String): OutputStream { - val fileStream = codeGenerator.getNewFile(fileName = tagFileName) - fileStream.appendText("package $codeGenerationPackage\n") - return fileStream - } - - private fun writeModuleTag( - fileStream: OutputStream, - mod: KoinMetaData.Module, - alreadyDeclared: ArrayList - ) { - logger.logging("writeModuleTag? ${mod.name}") - if (mod.alreadyGenerated == null){ - mod.alreadyGenerated = resolver.getResolution(mod) != null - } - - if (mod.alreadyGenerated == false){ - val className = mod.getTagName() - if (className !in alreadyDeclared) { - writeTagLine(className, fileStream, alreadyDeclared) - } - } - } - - private fun writeDefinitionTag( - fileStream: OutputStream, - def: KoinMetaData.Definition, - alreadyDeclared: ArrayList - ) { - writeClassTag(def, alreadyDeclared, fileStream) - def.bindings.forEach { writeBindingTag(it, alreadyDeclared, fileStream) } - } - - private fun writeClassTag( - def: KoinMetaData.Definition, - alreadyDeclared: java.util.ArrayList, - fileStream: OutputStream - ) { - if (def.alreadyGenerated == null){ - def.alreadyGenerated = resolver.getResolution(def) != null - } - - if (!def.isExpect && def.alreadyGenerated == false){ - val className = def.getTagName() - if (className !in alreadyDeclared) { - writeTagLine(className, fileStream, alreadyDeclared) - } - } - } - - private fun writeBindingTag( - binding: KSDeclaration, - alreadyDeclared: ArrayList, - fileStream: OutputStream - ) { - binding.qualifiedName?.asString()?.let { name -> - if (name !in typeWhiteList) { - binding.qualifiedNameCamelCase()?.let { className -> - val alreadyGenerated = resolver.getResolutionForTag(className) != null - if (className !in alreadyDeclared && !alreadyGenerated) { - writeTagLine(className, fileStream, alreadyDeclared) - } - } - } - } - } - - private fun writeTagLine( - tagName: String, - fileStream: OutputStream, - alreadyDeclared: java.util.ArrayList - ) { -// LOGGER.logging("tag: $tagName") - val tag = "public class $tagPrefix$tagName" - fileStream.appendText("\n$tag") - alreadyDeclared.add(tagName) - } -} diff --git a/projects/koin-ksp-compiler/src/jvmMain/kotlin/org/koin/compiler/verify/ext/ResolverExt.kt b/projects/koin-ksp-compiler/src/jvmMain/kotlin/org/koin/compiler/verify/ext/ResolverExt.kt deleted file mode 100644 index bb3c0cc6..00000000 --- a/projects/koin-ksp-compiler/src/jvmMain/kotlin/org/koin/compiler/verify/ext/ResolverExt.kt +++ /dev/null @@ -1,24 +0,0 @@ -package org.koin.compiler.verify.ext - -import com.google.devtools.ksp.processing.Resolver -import com.google.devtools.ksp.symbol.KSDeclaration -import org.koin.compiler.metadata.KoinMetaData -import org.koin.compiler.verify.codeGenerationPackage -import org.koin.compiler.verify.tagPrefix - - -fun Resolver.getResolution(mod : KoinMetaData.Module) : KSDeclaration?{ - return getResolutionForTag(mod.getTagName()) -} - -fun Resolver.getResolution(def : KoinMetaData.Definition) : KSDeclaration?{ - return getResolutionForTag(def.getTagName()) -} - -fun Resolver.getResolutionForTag(tag : String?) : KSDeclaration?{ - return getResolutionForClass("$codeGenerationPackage.$tagPrefix$tag") -} - -fun Resolver.getResolutionForClass(name : String) : KSDeclaration?{ - return getClassDeclarationByName(getKSNameFromString(name)) -} \ No newline at end of file