Skip to content

Commit

Permalink
support singletons without annotation in module
Browse files Browse the repository at this point in the history
  • Loading branch information
sergeshustoff committed Feb 19, 2023
1 parent a8500b5 commit 26f511a
Show file tree
Hide file tree
Showing 7 changed files with 167 additions and 12 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,8 @@ class DiNewApiCodeGenerator(

val singletons = module?.let { Annotations.singletonsByConstructor(module) }.orEmpty()
val providedByConstructor = getProvidedByConstructor(function)
val resolvedDependency = dependencies.resolveDependency(original.type, function, providedByConstructor, singletons)
val moduleScopes = module?.let { Annotations.getModuleScopes(module) }.orEmpty()
val resolvedDependency = dependencies.resolveDependency(original.type, function, providedByConstructor, singletons, moduleScopes)
incrementalHelper?.recordFunctionDependency(function, resolvedDependency)
return injectionBuilder.buildResolvedDependencyCall(module, function, resolvedDependency, original)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ data class AvailableDependencies(
type: IrType,
forFunction: IrFunction,
providedByConstructor: Set<IrType>,
singletons: Set<IrType>
singletons: Set<IrType>,
moduleScopes: Set<IrType>
): ResolvedDependency? {
return resolveDependencyInternal(
DependencyId(type),
Expand All @@ -31,6 +32,7 @@ data class AvailableDependencies(
providedByConstructor = providedByConstructor,
singletons = singletons,
forbidFunctionParams = false,
moduleScopes = moduleScopes,
)
)
}
Expand All @@ -47,7 +49,8 @@ data class AvailableDependencies(
return null
}

val isClassInSingletonList = id.type.classOrNull?.defaultType in context.singletons
val isClassInSingletonList = id.type.classOrNull?.defaultType in context.singletons ||
Annotations.isInjectableSingletonInScopes(id.type, context.moduleScopes)
if (isClassInSingletonList && id.type.anyTypeArgument { true }) {
context.forFunction.error("Generic types can't be singletons")
}
Expand Down Expand Up @@ -220,6 +223,7 @@ data class AvailableDependencies(
val usedTypes: List<IrType>,
val providedByConstructor: Set<IrType>,
val singletons: Set<IrType>,
val forbidFunctionParams: Boolean
val forbidFunctionParams: Boolean,
val moduleScopes: Set<IrType>,
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,7 @@ import org.jetbrains.kotlin.ir.declarations.IrClass
import org.jetbrains.kotlin.ir.declarations.IrFunction
import org.jetbrains.kotlin.ir.expressions.IrClassReference
import org.jetbrains.kotlin.ir.expressions.IrVararg
import org.jetbrains.kotlin.ir.types.IrType
import org.jetbrains.kotlin.ir.types.classFqName
import org.jetbrains.kotlin.ir.types.classOrNull
import org.jetbrains.kotlin.ir.types.defaultType
import org.jetbrains.kotlin.ir.types.*
import org.jetbrains.kotlin.ir.util.getAnnotation
import org.jetbrains.kotlin.ir.util.hasAnnotation
import org.jetbrains.kotlin.ir.util.isAnnotation
Expand All @@ -17,14 +14,16 @@ import org.jetbrains.kotlin.name.FqName

object Annotations {
private val createAnnotation = FqName("dev.shustoff.dikt.Create")
private val CreateSingleAnnotation = FqName("dev.shustoff.dikt.CreateSingle")
private val createSingleAnnotation = FqName("dev.shustoff.dikt.CreateSingle")
private val providedAnnotation = FqName("dev.shustoff.dikt.Provide")
private val useModulesAnnotation = FqName("dev.shustoff.dikt.UseModules")
private val providesMembersAnnotation = FqName("dev.shustoff.dikt.ProvidesMembers")
private val injectByConstructorsAnnotation = FqName("dev.shustoff.dikt.InjectByConstructors")
private val oldUseConstructorsAnnotation = FqName("dev.shustoff.dikt.UseConstructors")
private val moduleSingletonsAnnotation = FqName("dev.shustoff.dikt.InjectSingleByConstructors")
private val injectable = FqName("dev.shustoff.dikt.Injectable")
private val injectableSingle = FqName("dev.shustoff.dikt.InjectableSingleInScope")
private val moduleScopesAnnotation = FqName("dev.shustoff.dikt.ModuleScopes")

fun getUsedModules(descriptor: IrAnnotationContainer): List<IrType> {
val annotation = descriptor.getAnnotation(useModulesAnnotation)
Expand All @@ -35,18 +34,28 @@ object Annotations {
.orEmpty()
}

fun getModuleScopes(descriptor: IrClass): Set<IrType> {
val annotation = descriptor.getAnnotation(moduleScopesAnnotation)

return (annotation?.getValueArgument(0) as? IrVararg)
?.elements
?.mapNotNull { (it as? IrClassReference)?.classType }
.orEmpty()
.toSet()
}

fun providesMembers(descriptor: IrAnnotationContainer): Boolean {
return descriptor.hasAnnotation(providesMembersAnnotation)
}

fun isByDi(descriptor: IrFunction): Boolean {
return descriptor.annotations.hasAnnotation(createAnnotation)
|| descriptor.annotations.hasAnnotation(CreateSingleAnnotation)
|| descriptor.annotations.hasAnnotation(createSingleAnnotation)
|| descriptor.annotations.hasAnnotation(providedAnnotation)
}

fun isCached(descriptor: IrFunction): Boolean {
return descriptor.hasAnnotation(CreateSingleAnnotation)
return descriptor.hasAnnotation(createSingleAnnotation)
}

fun singletonsByConstructor(module: IrClass): Set<IrType> {
Expand Down Expand Up @@ -80,4 +89,12 @@ object Annotations {
fun isInjectable(type: IrType): Boolean {
return type.superTypes().any { it.classFqName == injectable }
}

fun isInjectableSingletonInScopes(type: IrType, scopes: Set<IrType>): Boolean {
val singletonParent = type.classOrNull?.superTypes()
?.firstOrNull { it.classFqName == injectableSingle } as? IrSimpleType
val scope = singletonParent?.arguments?.firstOrNull()?.typeOrNull ?: return false

return scope in scopes
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
package dev.shustoff.dikt.compiler

import com.google.common.truth.Truth
import com.tschuchort.compiletesting.KotlinCompilation
import com.tschuchort.compiletesting.SourceFile
import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder

@OptIn(ExperimentalCompilerApi::class)
class InjectableSingleInScopeTest {

@Rule
@JvmField
var folder: TemporaryFolder = TemporaryFolder()


@Test
fun `can compile for singleton injectable`() {
val result = compile(
folder.root,
SourceFile.kotlin(
"MyModule.kt",
"""
package dev.shustoff.dikt.compiler
import dev.shustoff.dikt.*
class Scope
class TestObject : InjectableSingleInScope<Scope>
@ModuleScopes(Scope::class)
class MyModule {
fun injectable(): TestObject = resolve()
}
"""
)
)
Truth.assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.OK)
}

@Test
fun `fail on parameters in cached injectable`() {
val result = compile(
folder.root,
SourceFile.kotlin(
"MyModule.kt",
"""
package dev.shustoff.dikt.compiler
import dev.shustoff.dikt.*
class Scope
class TestObject(val name: String) : InjectableSingleInScope<Scope>
@ModuleScopes(Scope::class)
class MyModule {
fun injectable(name: String): TestObject = resolve()
}
"""
)
)
Truth.assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.COMPILATION_ERROR)
Truth.assertThat(result.messages).contains("Can't resolve dependency kotlin.String")
}

@Test
fun `don't resolve singleton from nested module (because it's singleton)`() {
val result = compile(
folder.root,
SourceFile.kotlin(
"MyModule.kt",
"""
package dev.shustoff.dikt.compiler
import dev.shustoff.dikt.*
class Scope
class Dependency
class TestObject(val dependency: Dependency): InjectableSingleInScope<Scope>
class NestedModule(private val dependency: Dependency) {
fun injectable() = resolve<TestObject>()
}
@ModuleScopes(Scope::class)
class MyModule(@ProvidesMembers val module: NestedModule) {
fun injectable(): TestObject = resolve()
}
"""
)
)
Truth.assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.COMPILATION_ERROR)
Truth.assertThat(result.messages).contains("Can't resolve dependency dev.shustoff.dikt.compiler.TestObject")
}

@Test
fun `fail on cached extension functions`() {
val result = compile(
folder.root,
SourceFile.kotlin(
"MyModule.kt",
"""
package dev.shustoff.dikt.compiler
import dev.shustoff.dikt.*
class Scope
class TestObject(val name: String) : InjectableSingleInScope<Scope>
fun String.injectable(): TestObject = resolve()
"""
)
)

Truth.assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.COMPILATION_ERROR)
Truth.assertThat(result.messages).contains("Can't resolve dependency dev.shustoff.dikt.compiler.TestObject")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,11 @@ class DiktGradlePlugin : KotlinCompilerPluginSupportPlugin {
target.extensions.create("dikt", DiktGradleExtension::class.java)
target.afterEvaluate { project ->
project.configurations.filter { it.name.endsWith("implementation", ignoreCase = true) }.forEach {
project.dependencies.add(it.name, project.dependencies.create("io.github.sergeshustoff.dikt:dikt:$version"))
project.dependencies.add(it.name, project.dependencies.create("io.github.sergeshustoff.dikt:dikt-internal:$version"))
}
project.configurations.filter { it.name.endsWith("api", ignoreCase = true) }.forEach {
project.dependencies.add(it.name, project.dependencies.create("io.github.sergeshustoff.dikt:dikt:$version"))
}
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package dev.shustoff.dikt

import kotlin.reflect.KClass

@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.SOURCE)
annotation class ModuleScopes(vararg val scopes: KClass<*>)
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package dev.shustoff.dikt

interface InjectableSingleInScope<T: Any>

0 comments on commit 26f511a

Please sign in to comment.