Repository: evant/kotlin-inject
Pull request #51 (2020-08-29 18:20:02)

Dynamic scoped modules


Patch:
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 39f511f..e3a5bc4 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -14,11 +14,11 @@
  
 To debug the compiler, you can write the code you want to test and then run
  ```
-gradlew :integration-tests-kapt:test --no-daemon -Dorg.gradle.debug=true -Dkotlin.compiler.execution.strategy=in-process
+./gradlew :integration-tests-kapt:test --no-daemon -Dorg.gradle.debug=true -Dkotlin.compiler.execution.strategy=in-process
 ```
 or
 ```
-gradlew :integration-tests-ksp:test --no-daemon -Dorg.gradle.debug=true -Dkotlin.compiler.execution.strategy=in-process
+./gradlew :integration-tests-ksp:test --no-daemon -Dorg.gradle.debug=true -Dkotlin.compiler.execution.strategy=in-process
 ```
 for kapt or ksp respectively. Then you can create and run a remote run configuration in intellij to attach the debugger.
  
diff --git a/kotlin-inject-compiler-core/src/main/kotlin/me/tatarka/inject/compiler/InjectGenerator.kt b/kotlin-inject-compiler-core/src/main/kotlin/me/tatarka/inject/compiler/InjectGenerator.kt
index a920bfd..3bdb025 100644
--- a/kotlin-inject-compiler-core/src/main/kotlin/me/tatarka/inject/compiler/InjectGenerator.kt
+++ b/kotlin-inject-compiler-core/src/main/kotlin/me/tatarka/inject/compiler/InjectGenerator.kt
@@ -1,7 +1,18 @@
 package me.tatarka.inject.compiler
 
-import com.squareup.kotlinpoet.*
+import com.squareup.kotlinpoet.ANY
+import com.squareup.kotlinpoet.ClassName
+import com.squareup.kotlinpoet.CodeBlock
+import com.squareup.kotlinpoet.FileSpec
+import com.squareup.kotlinpoet.FunSpec
+import com.squareup.kotlinpoet.KModifier
+import com.squareup.kotlinpoet.ParameterSpec
+import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
 import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.plusParameter
+import com.squareup.kotlinpoet.PropertySpec
+import com.squareup.kotlinpoet.STRING
+import com.squareup.kotlinpoet.TypeSpec
+import com.squareup.kotlinpoet.asClassName
 import me.tatarka.inject.annotations.Component
 import me.tatarka.inject.annotations.Inject
 import me.tatarka.inject.annotations.Provides
@@ -11,7 +22,7 @@ import kotlin.reflect.KClass
 class InjectGenerator(provider: AstProvider, private val options: Options) :
     AstProvider by provider {
 
-    fun generate(astClass: AstClass, scopedClasses: Collection<AstClass> = emptyList()): FileSpec {
+    fun generate(astClass: AstClass): FileSpec {
         if (AstModifier.ABSTRACT !in astClass.modifiers) {
             throw FailedToGenerateException("@Component class: $astClass must be abstract", astClass)
         } else if (AstModifier.PRIVATE in astClass.modifiers) {
@@ -20,9 +31,7 @@ class InjectGenerator(provider: AstProvider, private val options: Options) :
 
         val constructor = astClass.primaryConstructor
 
-        val scopedInjects = scopedClasses.filter { it.isInject(options) }
-
-        val injectComponent = generateInjectComponent(astClass, constructor, scopedInjects)
+        val injectComponent = generateInjectComponent(astClass, constructor)
         val createFunction = generateCreate(astClass, constructor, injectComponent)
 
         return FileSpec.builder(astClass.packageName, "Inject${astClass.name}")
@@ -31,34 +40,19 @@ class InjectGenerator(provider: AstProvider, private val options: Options) :
             .build()
     }
 
-    fun generateScopedInterfaces(scopedClasses: Collection<AstClass>): List<FileSpec> {
-        val scopedInjects = scopedClasses.filter { it.isInject(options) }
-        val scopedInterfaces = scopedClasses.filter { !it.isInject(options) && !it.hasAnnotation<Component>() }
-        return scopedInterfaces.map {
-            FileSpec.builder(it.packageName, "Inject${it.name}")
-                .addType(generateScopedInterface(it, scopedInjects))
-                .build()
-        }
-    }
-
     private fun generateInjectComponent(
         astClass: AstClass,
-        constructor: AstConstructor?,
-        scopedInjects: Collection<AstClass>
+        constructor: AstConstructor?
     ): TypeSpec {
-        val context = collectTypes(astClass, scopedInjects, isComponent = true)
+        val context = collectTypes(astClass, isComponent = true)
+        val scope = astClass.scopeClass(messenger, options)
 
         return TypeSpec.classBuilder("Inject${astClass.name}")
             .addOriginatingElement(astClass)
             .superclass(astClass.asClassName())
             .apply {
-                if (context.scopeInterface != null) {
-                    addSuperinterface(
-                        ClassName(
-                            context.scopeInterface.packageName,
-                            "Inject${context.scopeInterface.name}"
-                        )
-                    )
+                if (scope != null) {
+                    addSuperinterface(ClassName("me.tatarka.inject.internal", "ScopedComponent"))
                 }
 
                 if (constructor != null) {
@@ -72,21 +66,12 @@ class InjectGenerator(provider: AstProvider, private val options: Options) :
                 }
 
                 try {
-                    for (type in context.collector.scoped) {
-                        val codeBlock = CodeBlock.builder()
-                        codeBlock.add("lazy { ")
-                        codeBlock.add(provide(TypeKey(type), context.withoutScoped(type)))
-                        codeBlock.add(" }")
-
+                    if (scope != null) {
+                        val mapType = ClassName("me.tatarka.inject.internal", "LazyMap")
                         addProperty(
-                            PropertySpec.builder(
-                                type.simpleName.asScopedProp(),
-                                type.asTypeName()
-                            ).apply {
-                                if (context.scopeInterface != null) {
-                                    addModifiers(KModifier.OVERRIDE)
-                                }
-                            }.delegate(codeBlock.build())
+                            PropertySpec.builder("_scoped", mapType)
+                                .addModifiers(KModifier.OVERRIDE)
+                                .initializer("%T()", mapType)
                                 .build()
                         )
                     }
@@ -94,10 +79,11 @@ class InjectGenerator(provider: AstProvider, private val options: Options) :
                     astClass.visitInheritanceChain { parentClass ->
                         for (method in parentClass.methods) {
                             if (method.isProvider()) {
-                                context.use(method) { context ->
+                                val returnType = method.returnTypeFor(astClass)
+
+                                context.withoutProvider(returnType).use(method) { context ->
                                     val codeBlock = CodeBlock.builder()
                                     codeBlock.add("return ")
-                                    val returnType = method.returnTypeFor(astClass)
 
                                     codeBlock.add(
                                         provide(
@@ -131,26 +117,6 @@ class InjectGenerator(provider: AstProvider, private val options: Options) :
             .build()
     }
 
-    private fun generateScopedInterface(
-        astClass: AstClass,
-        scopedInjects: Collection<AstClass>
-    ): TypeSpec {
-        val context = collectTypes(astClass, scopedInjects, isComponent = false)
-
-        return TypeSpec.interfaceBuilder("Inject${astClass.name}")
-            .addOriginatingElement(astClass)
-            .apply {
-                for (type in context.collector.scoped) {
-                    addProperty(
-                        PropertySpec.builder(
-                            type.simpleName.asScopedProp(),
-                            type.asTypeName()
-                        ).build()
-                    )
-                }
-            }.build()
-    }
-
     private fun generateCreate(
         element: AstClass,
         constructor: AstConstructor?,
@@ -197,15 +163,13 @@ class InjectGenerator(provider: AstProvider, private val options: Options) :
 
     private fun collectTypes(
         astClass: AstClass,
-        scopedInjects: Collection<AstClass>,
         isComponent: Boolean
     ): Context {
         val typeCollector = TypeCollector(
             provider = this,
             options = options,
             astClass = astClass,
-            isComponent = isComponent,
-            scopedInjects = scopedInjects
+            isComponent = isComponent
         )
         val cycleDetector = CycleDetector()
         val elementScopeClass = astClass.scopeClass(messenger, options)
@@ -231,7 +195,7 @@ class InjectGenerator(provider: AstProvider, private val options: Options) :
             )
         return when (result) {
             is Result.Provides -> provideProvides(result, context)
-            is Result.Scoped -> provideScoped(key, result)
+            is Result.Scoped -> provideScoped(key, result, context)
             is Result.Constructor -> provideConstructor(result.constructor, context)
             is Result.Container -> provideContainer(result, context)
             is Result.Function -> provideFunction(result, context)
@@ -330,13 +294,19 @@ class InjectGenerator(provider: AstProvider, private val options: Options) :
         codeBlock.build()
     }
 
-    private fun provideScoped(key: TypeKey, result: Result.Scoped): CodeBlock {
-        val codeBlock = CodeBlock.builder()
-        if (result.name != null) {
-            codeBlock.add("(%L as %T).", result.name, ClassName(result.astClass.packageName, "Inject${result.astClass.name}"))
-        }
-        codeBlock.add("%N", key.type.simpleName.asScopedProp())
-        return codeBlock.build()
+    private fun provideScoped(key: TypeKey, result: Result.Scoped, context: Context): CodeBlock {
+        return CodeBlock.builder().apply {
+            if (result.name != null) {
+                add(
+                    "(%L as %T).",
+                    result.name,
+                    ClassName("me.tatarka.inject.internal", "ScopedComponent")
+                )
+            }
+            add("_scoped.get(%S)", key.type).beginControlFlow("{")
+            add(provide(key, context.withoutScoped(key.type)))
+            endControlFlow()
+        }.build()
     }
 
     private fun provideContainer(containerResult: Result.Container, context: Context): CodeBlock {
@@ -432,7 +402,7 @@ class InjectGenerator(provider: AstProvider, private val options: Options) :
     }
 
     private fun Context.find(key: TypeKey): Result? {
-        val typeCreator = collector.resolve(key)
+        val typeCreator = collector.resolve(key, skipSelf = key.type == skipProvider)
         if (typeCreator != null) {
             return typeCreator.toResult(skipScoped)
         }
@@ -495,10 +465,13 @@ class InjectGenerator(provider: AstProvider, private val options: Options) :
         val scopeInterface: AstClass? = null,
         val args: List<Pair<AstType, String>> = emptyList(),
         val skipScoped: AstType? = null,
+        val skipProvider: AstType? = null,
         val parentScopeName: String? = null
     ) {
         fun withoutScoped(scoped: AstType) = copy(skipScoped = scoped)
 
+        fun withoutProvider(provider: AstType) = copy(skipProvider = provider)
+
         fun withSource(source: AstElement) = copy(source = source)
 
         fun withParentScopeName(name: String?): Context {
diff --git a/kotlin-inject-compiler-core/src/main/kotlin/me/tatarka/inject/compiler/TypeCollector.kt b/kotlin-inject-compiler-core/src/main/kotlin/me/tatarka/inject/compiler/TypeCollector.kt
index 2e21b7d..ca4bb8c 100644
--- a/kotlin-inject-compiler-core/src/main/kotlin/me/tatarka/inject/compiler/TypeCollector.kt
+++ b/kotlin-inject-compiler-core/src/main/kotlin/me/tatarka/inject/compiler/TypeCollector.kt
@@ -24,6 +24,9 @@ class TypeCollector private constructor(private val provider: AstProvider, priva
     // Map of types to inject and how to obtain them
     private val types = mutableMapOf<TypeKey, TypeCreator>()
 
+    // Map of types that can be provided abstract methods
+    private val providerMethods = mutableMapOf<TypeKey, TypeCreator>()
+
     // List of scoped types this component needs to provide
     private val _scoped = mutableListOf<AstType>()
 
@@ -79,9 +82,7 @@ class TypeCollector private constructor(private val provider: AstProvider, priva
                         providesMethods.add(method)
                     }
                 }
-                // If we aren't a component ourselves, allow obtaining types from providers as these may be contributed from the
-                // eventual implementation
-                if (!isComponent && method.isProvider()) {
+                if (method.isProvider()) {
                     providerMethods.add(method)
                 }
             }
@@ -120,7 +121,13 @@ class TypeCollector private constructor(private val provider: AstProvider, priva
         for (method in providerMethods) {
             val returnType = method.returnTypeFor(astClass)
             val key = TypeKey(returnType)
-            addMethod(key, method, accessor, scopedComponent = null)
+            if (isComponent) {
+                addProviderMethod(key, method, accessor)
+            } else {
+                // If we aren't a component ourselves, allow obtaining types from providers as these may be contributed from the
+                // eventual implementation
+                addMethod(key, method, accessor, scopedComponent = null)
+            }
         }
 
         val constructor = astClass.primaryConstructor
@@ -168,6 +175,12 @@ class TypeCollector private constructor(private val provider: AstProvider, priva
         }
     }
 
+    private fun addProviderMethod(key: TypeKey, method: AstMethod, accessor: String?) {
+        if (!providerMethods.containsKey(key)) {
+            providerMethods[key] = method(method, accessor, scopedComponent = null)
+        }
+    }
+
     private fun method(method: AstMethod, accessor: String?, scopedComponent: AstClass?) = TypeCreator.Method(
         method = method,
         accessor = accessor,
@@ -182,7 +195,13 @@ class TypeCollector private constructor(private val provider: AstProvider, priva
 
     val scoped: Iterable<AstType> = _scoped
 
-    fun resolve(key: TypeKey): TypeCreator? {
+    fun resolve(key: TypeKey, skipSelf: Boolean = false): TypeCreator? {
+        if (!skipSelf) {
+            val result = providerMethods[key]
+            if (result != null) {
+                return result
+            }
+        }
         val result = types[key]
         if (result != null) {
             return result
diff --git a/kotlin-inject-compiler-kapt/src/main/kotlin/me/tatarka/inject/compiler/kapt/InjectCompiler.kt b/kotlin-inject-compiler-kapt/src/main/kotlin/me/tatarka/inject/compiler/kapt/InjectCompiler.kt
index efcee8a..01245d0 100644
--- a/kotlin-inject-compiler-kapt/src/main/kotlin/me/tatarka/inject/compiler/kapt/InjectCompiler.kt
+++ b/kotlin-inject-compiler-kapt/src/main/kotlin/me/tatarka/inject/compiler/kapt/InjectCompiler.kt
@@ -1,24 +1,37 @@
 package me.tatarka.inject.compiler.kapt
 
 import me.tatarka.inject.annotations.Component
+import me.tatarka.inject.compiler.AstClass
 import me.tatarka.inject.compiler.FailedToGenerateException
 import me.tatarka.inject.compiler.InjectGenerator
 import me.tatarka.inject.compiler.scopeClass
+import me.tatarka.inject.compiler.scopeType
 import javax.annotation.processing.RoundEnvironment
 import javax.lang.model.element.TypeElement
 
 class InjectCompiler : BaseInjectCompiler() {
 
+    private val annotationNames = mutableSetOf<String>()
+
+    init {
+        annotationNames.add(Component::class.java.canonicalName)
+    }
+
     override fun process(elements: Set<TypeElement>, env: RoundEnvironment): Boolean {
         val generator = InjectGenerator(this, options)
+        val allScopedClasses = mutableSetOf<AstClass>()
 
         for (element in env.getElementsAnnotatedWith(Component::class.java)) {
             if (element !is TypeElement) continue
 
             val astClass = element.toAstClass()
             val scopeClass = astClass.scopeClass(messenger, options)
-            // Only deal with non-scoped components
-            if (scopeClass != null) continue
+            if (scopeClass != null) {
+                val scopeType = scopeClass.scopeType(options)!!
+                annotationNames.add(scopeType.toString())
+                val scopedClasses = scopedClasses(scopeType, env)
+                allScopedClasses.addAll(scopedClasses)
+            }
 
             try {
                 val file = generator.generate(astClass)
@@ -32,7 +45,11 @@ class InjectCompiler : BaseInjectCompiler() {
         return false
     }
 
-    override fun getSupportedAnnotationTypes(): Set<String> = setOf(
-        Component::class.java.canonicalName
-    )
+    private fun scopedClasses(scopeType: AstClass, env: RoundEnvironment): List<AstClass> {
+        return env.getElementsAnnotatedWith(scopeType.element).mapNotNull {
+            (it as? TypeElement)?.toAstClass()
+        }
+    }
+
+    override fun getSupportedAnnotationTypes(): Set<String> = annotationNames
 }
diff --git a/kotlin-inject-compiler-kapt/src/main/kotlin/me/tatarka/inject/compiler/kapt/ScopedInjectCompiler.kt b/kotlin-inject-compiler-kapt/src/main/kotlin/me/tatarka/inject/compiler/kapt/ScopedInjectCompiler.kt
deleted file mode 100644
index 4721007..0000000
--- a/kotlin-inject-compiler-kapt/src/main/kotlin/me/tatarka/inject/compiler/kapt/ScopedInjectCompiler.kt
+++ /dev/null
@@ -1,66 +0,0 @@
-package me.tatarka.inject.compiler.kapt
-
-import me.tatarka.inject.annotations.Component
-import me.tatarka.inject.annotations.Inject
-import me.tatarka.inject.annotations.Scope
-import me.tatarka.inject.compiler.*
-import javax.annotation.processing.RoundEnvironment
-import javax.lang.model.element.TypeElement
-
-class ScopedInjectCompiler : BaseInjectCompiler() {
-
-    private val scopedAnnotationNames = mutableSetOf<String>()
-
-    init {
-        scopedAnnotationNames.add(Component::class.java.canonicalName)
-        scopedAnnotationNames.add(Inject::class.java.canonicalName)
-    }
-
-    override fun process(elements: Set<TypeElement>, env: RoundEnvironment): Boolean {
-        val allScopedClasses = mutableSetOf<AstClass>()
-        val generator = InjectGenerator(this, options)
-
-        for (element in env.getElementsAnnotatedWith(Component::class.java)) {
-            if (element !is TypeElement) continue
-            val astClass = element.toAstClass()
-            // Only deal with scoped components
-            val scopedClass = astClass.scopeClass(messenger, options) ?: continue
-
-            val scopeType = scopedClass.scopeType(options)!!
-            scopedAnnotationNames.add(scopeType.toString())
-
-            val scopedClasses = scopedClasses(scopeType, env)
-            allScopedClasses.addAll(scopedClasses)
-
-            try {
-                val file = generator.generate(astClass, scopedClasses)
-                file.writeTo(filer)
-            } catch (e: FailedToGenerateException) {
-                error(e.message.orEmpty(), e.element)
-                // Continue so we can see all errors
-                continue
-            }
-        }
-
-        try {
-            for (file in generator.generateScopedInterfaces(allScopedClasses)) {
-                file.writeTo(filer)
-            }
-        } catch (e: FailedToGenerateException) {
-            error(e.message.orEmpty(), e.element)
-            // Continue so we can see all errors
-        }
-
-        return false
-    }
-
-    private fun scopedClasses(scopeType: AstClass, env: RoundEnvironment): List<AstClass> {
-        return env.getElementsAnnotatedWith(scopeType.element).mapNotNull {
-            (it as? TypeElement)?.toAstClass()
-        }
-    }
-
-    override fun getSupportedAnnotationTypes(): Set<String> {
-        return scopedAnnotationNames
-    }
-}
\ No newline at end of file
diff --git a/kotlin-inject-compiler-kapt/src/main/resources/META-INF/gradle/incremental.annotation.processors b/kotlin-inject-compiler-kapt/src/main/resources/META-INF/gradle/incremental.annotation.processors
index 1ea4955..2c91629 100644
--- a/kotlin-inject-compiler-kapt/src/main/resources/META-INF/gradle/incremental.annotation.processors
+++ b/kotlin-inject-compiler-kapt/src/main/resources/META-INF/gradle/incremental.annotation.processors
@@ -1,2 +1,1 @@
 me.tatarka.inject.compiler.kapt.InjectCompiler,isolating
-me.tatarka.inject.compiler.kapt.ScopedInjectCompiler,aggregating
diff --git a/kotlin-inject-compiler-kapt/src/main/resources/META-INF/services/javax.annotation.processing.Processor b/kotlin-inject-compiler-kapt/src/main/resources/META-INF/services/javax.annotation.processing.Processor
index 0186311..25e32ed 100644
--- a/kotlin-inject-compiler-kapt/src/main/resources/META-INF/services/javax.annotation.processing.Processor
+++ b/kotlin-inject-compiler-kapt/src/main/resources/META-INF/services/javax.annotation.processing.Processor
@@ -1,2 +1,1 @@
 me.tatarka.inject.compiler.kapt.InjectCompiler
-me.tatarka.inject.compiler.kapt.ScopedInjectCompiler
diff --git a/kotlin-inject-compiler-ksp/src/main/kotlin/me/tatarka/inject/compiler/ksp/InjectProcessor.kt b/kotlin-inject-compiler-ksp/src/main/kotlin/me/tatarka/inject/compiler/ksp/InjectProcessor.kt
index a2d2412..41b37e5 100644
--- a/kotlin-inject-compiler-ksp/src/main/kotlin/me/tatarka/inject/compiler/ksp/InjectProcessor.kt
+++ b/kotlin-inject-compiler-ksp/src/main/kotlin/me/tatarka/inject/compiler/ksp/InjectProcessor.kt
@@ -39,11 +39,12 @@ class InjectProcessor : SymbolProcessor, KSAstProvider {
             val scopedClass = astClass.scopeClass(messenger, options)
 
             val scopeType = scopedClass?.scopeType(options)
-            val scopedClasses = scopeType?.let { scopedClasses(it, resolver) } ?: emptyList()
-            allScopeClasses.addAll(scopedClasses)
+            if (scopeType != null) {
+                allScopeClasses.addAll(scopedClasses(scopeType, resolver))
+            }
 
             try {
-                val file = generator.generate(astClass, scopedClasses)
+                val file = generator.generate(astClass)
                 file.writeTo(codeGenerator)
             } catch (e: FailedToGenerateException) {
                 error(e.message.orEmpty(), e.element)
@@ -51,15 +52,6 @@ class InjectProcessor : SymbolProcessor, KSAstProvider {
                 continue
             }
         }
-
-        try {
-            for (file in generator.generateScopedInterfaces(allScopeClasses)) {
-                file.writeTo(codeGenerator)
-            }
-        } catch (e: FailedToGenerateException) {
-            error(e.message.orEmpty(), e.element)
-            // Continue so we can see all errors
-        }
     }
 
     private fun scopedClasses(scopeType: AstClass, resolver: Resolver): List<AstClass> {
diff --git a/kotlin-inject-runtime/src/main/kotlin/me/tatarka/inject/internal/ScopedComponent.kt b/kotlin-inject-runtime/src/main/kotlin/me/tatarka/inject/internal/ScopedComponent.kt
new file mode 100644
index 0000000..d0bd5f6
--- /dev/null
+++ b/kotlin-inject-runtime/src/main/kotlin/me/tatarka/inject/internal/ScopedComponent.kt
@@ -0,0 +1,37 @@
+package me.tatarka.inject.internal
+
+import java.util.concurrent.ConcurrentHashMap
+
+interface ScopedComponent {
+    val _scoped: LazyMap
+}
+
+private val NULL = Any()
+
+class LazyMap {
+    private val map = ConcurrentHashMap<String, Any>()
+
+    fun <T> get(key: String, init: () -> T): T {
+        val result = map[key]
+        if (result == null) {
+            synchronized(map) {
+                var result = map[key]
+                if (result == null) {
+                    result = init() ?: NULL
+                    map[key] = result
+                }
+                return coerceResult(result)
+            }
+        } else {
+            return coerceResult(result)
+        }
+    }
+
+    private fun <T> coerceResult(result: Any): T {
+        return if (result === NULL) {
+            null
+        } else {
+            result
+        } as T
+    }
+}
\ No newline at end of file
diff --git a/settings.gradle b/settings.gradle
index d4a50c3..1ea2d87 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -23,3 +23,4 @@ include ':kotlin-inject-runtime'
 include ':compiler-tests'
 include ':integration-tests-kapt'
 include ':integration-tests-ksp'
+include ':integration-tests-module'


Test patch:
diff --git a/compiler-tests/src/test/kotlin/me/tatarka/inject/test/ProjectCompiler.kt b/compiler-tests/src/test/kotlin/me/tatarka/inject/test/ProjectCompiler.kt
index 00ff7c9..6f94944 100644
--- a/compiler-tests/src/test/kotlin/me/tatarka/inject/test/ProjectCompiler.kt
+++ b/compiler-tests/src/test/kotlin/me/tatarka/inject/test/ProjectCompiler.kt
@@ -7,7 +7,6 @@ import com.tschuchort.compiletesting.KotlinCompilation
 import com.tschuchort.compiletesting.SourceFile
 import com.tschuchort.compiletesting.symbolProcessors
 import me.tatarka.inject.compiler.kapt.InjectCompiler
-import me.tatarka.inject.compiler.kapt.ScopedInjectCompiler
 import me.tatarka.inject.compiler.ksp.InjectProcessor
 import java.io.ByteArrayOutputStream
 import java.io.File
@@ -34,7 +33,7 @@ class ProjectCompiler(private val root: File, private val target: Target) {
             inheritClassPath = true
             when (target) {
                 Target.kapt -> {
-                    annotationProcessors = listOf(InjectCompiler(), ScopedInjectCompiler())
+                    annotationProcessors = listOf(InjectCompiler())
                 }
                 Target.ksp -> {
                     symbolProcessors = listOf(InjectProcessor())
diff --git a/integration-tests-kapt/build.gradle b/integration-tests-kapt/build.gradle
index 0e6e2a6..5c747d3 100644
--- a/integration-tests-kapt/build.gradle
+++ b/integration-tests-kapt/build.gradle
@@ -1,11 +1,13 @@
 plugins {
     id 'org.jetbrains.kotlin.jvm'
     id 'org.jetbrains.kotlin.kapt'
+    id "me.champeau.gradle.jmh" version "0.5.1"
 }
 
 dependencies {
     kapt project(':kotlin-inject-compiler-kapt')
     implementation project(':kotlin-inject-runtime')
+    implementation project(':integration-tests-module')
 
     implementation 'org.jetbrains.kotlin:kotlin-stdlib'
 
@@ -37,3 +39,8 @@ targetCompatibility = 1.8
 tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach {
     kotlinOptions.jvmTarget = "1.8"
 }
+
+jmh {
+    // https://github.com/melix/jmh-gradle-plugin/issues/159
+    duplicateClassesStrategy = DuplicatesStrategy.EXCLUDE
+}
\ No newline at end of file
diff --git a/integration-tests-kapt/src/jmh/java/me/tatarka/inject/benchmark/ScopedAccessBenchmark.kt b/integration-tests-kapt/src/jmh/java/me/tatarka/inject/benchmark/ScopedAccessBenchmark.kt
new file mode 100644
index 0000000..2ee35be
--- /dev/null
+++ b/integration-tests-kapt/src/jmh/java/me/tatarka/inject/benchmark/ScopedAccessBenchmark.kt
@@ -0,0 +1,50 @@
+package me.tatarka.inject.benchmark
+
+import me.tatarka.inject.internal.LazyMap
+import me.tatarka.inject.internal.ScopedComponent
+import org.openjdk.jmh.annotations.Benchmark
+import org.openjdk.jmh.annotations.Scope
+import java.util.concurrent.ConcurrentHashMap
+
+// @Inject @MyScope
+class Foo
+
+// @MyScope @Component
+abstract class ScopedAccessComponent {
+    abstract val foo: Foo
+}
+
+
+class InjectLazyScopedAccessComponent : ScopedAccessComponent() {
+    private val _foo by lazy {
+        Foo()
+    }
+
+    override val foo: Foo
+        get() = _foo
+}
+
+class InjectDynamicScopedAccessComponent : ScopedAccessComponent(), ScopedComponent {
+    override val _scoped = LazyMap()
+
+    override val foo: Foo = _scoped.get("me.tatarka.inject.benchmark.Foo") { Foo() }
+}
+
+open class ScopedAccessBenchmark {
+
+    @org.openjdk.jmh.annotations.State(Scope.Benchmark)
+    open class State {
+        val lazyComponent = InjectLazyScopedAccessComponent()
+        val dynamicComponent = InjectDynamicScopedAccessComponent()
+    }
+
+    @Benchmark
+    fun lazy_scoped_access_component(state: State): Foo {
+        return state.lazyComponent.foo
+    }
+
+    @Benchmark
+    fun dynamic_scoped_access_compoennt(state: State): Foo {
+        return state.dynamicComponent.foo
+    }
+}
\ No newline at end of file
diff --git a/integration-tests-ksp/build.gradle b/integration-tests-ksp/build.gradle
index 85fd547..a63249b 100644
--- a/integration-tests-ksp/build.gradle
+++ b/integration-tests-ksp/build.gradle
@@ -6,6 +6,7 @@ plugins {
 dependencies {
     ksp project(':kotlin-inject-compiler-ksp')
     implementation project(':kotlin-inject-runtime')
+    implementation project(':integration-tests-module')
 
     implementation 'org.jetbrains.kotlin:kotlin-stdlib'
 
diff --git a/integration-tests-module/.gitignore b/integration-tests-module/.gitignore
new file mode 100644
index 0000000..796b96d
--- /dev/null
+++ b/integration-tests-module/.gitignore
@@ -0,0 +1,1 @@
+/build
diff --git a/integration-tests-module/build.gradle b/integration-tests-module/build.gradle
new file mode 100644
index 0000000..a1c115d
--- /dev/null
+++ b/integration-tests-module/build.gradle
@@ -0,0 +1,15 @@
+plugins {
+    id 'org.jetbrains.kotlin.jvm'
+}
+
+dependencies {
+    implementation project(':kotlin-inject-runtime')
+    implementation 'org.jetbrains.kotlin:kotlin-stdlib'
+}
+
+sourceCompatibility = 1.8
+targetCompatibility = 1.8
+
+tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach {
+    kotlinOptions.jvmTarget = "1.8"
+}
diff --git a/integration-tests-module/src/main/kotlin/me/tatarka/inject/test/module/Models.kt b/integration-tests-module/src/main/kotlin/me/tatarka/inject/test/module/Models.kt
new file mode 100644
index 0000000..61d8fed
--- /dev/null
+++ b/integration-tests-module/src/main/kotlin/me/tatarka/inject/test/module/Models.kt
@@ -0,0 +1,10 @@
+package me.tatarka.inject.test.module
+
+import me.tatarka.inject.annotations.Inject
+import me.tatarka.inject.annotations.Scope
+
+@Inject class ExternalFoo
+
+@Scope annotation class ExternalScope
+
+@ExternalScope @Inject class ScopedExternalFoo
\ No newline at end of file
diff --git a/integration-tests/src/test/kotlin/me/tatarka/inject/test/ScopeTest.kt b/integration-tests/src/test/kotlin/me/tatarka/inject/test/ScopeTest.kt
index 925e3d6..24e7362 100644
--- a/integration-tests/src/test/kotlin/me/tatarka/inject/test/ScopeTest.kt
+++ b/integration-tests/src/test/kotlin/me/tatarka/inject/test/ScopeTest.kt
@@ -10,6 +10,8 @@ import me.tatarka.inject.annotations.Provides
 import me.tatarka.inject.test.different.DifferentPackageFoo
 import me.tatarka.inject.test.different.DifferentPackageScopedComponent
 import me.tatarka.inject.test.different.create
+import me.tatarka.inject.test.module.ExternalScope
+import me.tatarka.inject.test.module.ScopedExternalFoo
 import kotlin.test.BeforeTest
 import kotlin.test.Test
 
@@ -58,6 +60,20 @@ class ScopedBar()
     abstract val foo: DifferentPackageFoo
 }
 
+@Component @ExternalScope abstract class ExternalScopedComponent {
+    abstract val foo: ScopedExternalFoo
+}
+
+@Inject class UseBar1(val bar: ScopedBar)
+
+@Inject class UseBar2(val bar: ScopedBar)
+
+@CustomScope @Component abstract class MultipleUseScopedComponent {
+    abstract val bar1: UseBar1
+
+    abstract val bar2: UseBar2
+}
+
 class ScopeTest {
     @BeforeTest
     fun setup() {
@@ -130,4 +146,10 @@ class ScopeTest {
 
         assertThat(component.foo).isSameAs(component.foo)
     }
+
+    @Test fun generates_a_component_that_reuses_the_same_scoped_dependency() {
+        val component = MultipleUseScopedComponent::class.create()
+
+        assertThat(component.bar1.bar).isSameAs(component.bar2.bar)
+    }
 }
\ No newline at end of file