Skip to content

Commit 3ca308e

Browse files
committed
Introduce @ContributesScoped for Metro
In kotlin-inject-anvil we provide a custom code generator for `@ContributesBinding` when the class implements `Scoped`. The custom code generator handles all super type bindings properly. ```kotlin @Inject @singlein(AppScope::class) @ContributesBinding(AppScope::class) class Abc : Def, Scoped ``` Metro doesn't provide this kind of integration and it's required to create a graph interface: ```kotlin @Inject @singlein(AppScope::class) class Abc : Def, Scoped @ContributesTo(AppScope::class) interface AbcGraph { @BINDS val Abc.bindDef: Def @BINDS @IntoSet @Forscope(AppScope::class) val Abc.bindScoped: Scoped } ``` That's a lot of boilerplate. Therefore, we introduce `@ContributesScoped` to generate the code and restore the behavior from kotlin-inject-anvil. ```kotlin @Inject @singlein(AppScope::class) @ContributesScoped(AppScope::class) class Abc : Def, Scoped ``` Fixes #132
1 parent 1feb659 commit 3ca308e

File tree

11 files changed

+620
-35
lines changed

11 files changed

+620
-35
lines changed
Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,12 @@ import com.google.devtools.ksp.processing.SymbolProcessorProvider
77
import software.amazon.app.platform.ksp.CompositeSymbolProcessor
88
import software.amazon.app.platform.metro.processor.ContributesRendererProcessor
99
import software.amazon.app.platform.metro.processor.ContributesRobotProcessor
10+
import software.amazon.app.platform.metro.processor.ContributesScopedProcessor
1011

1112
/** Entry point for KSP to pick up our [SymbolProcessor]. */
1213
@AutoService(SymbolProcessorProvider::class)
1314
@Suppress("unused")
14-
public class KotlinInjectExtensionSymbolProcessorProvider : SymbolProcessorProvider {
15+
public class MetroExtensionSymbolProcessorProvider : SymbolProcessorProvider {
1516
override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor {
1617
return CompositeSymbolProcessor(
1718
ContributesRendererProcessor(
@@ -22,6 +23,10 @@ public class KotlinInjectExtensionSymbolProcessorProvider : SymbolProcessorProvi
2223
codeGenerator = environment.codeGenerator,
2324
logger = environment.logger,
2425
),
26+
ContributesScopedProcessor(
27+
codeGenerator = environment.codeGenerator,
28+
logger = environment.logger,
29+
),
2530
)
2631
}
2732
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
package software.amazon.app.platform.metro.processor
2+
3+
import com.google.devtools.ksp.KspExperimental
4+
import com.google.devtools.ksp.getAllSuperTypes
5+
import com.google.devtools.ksp.processing.CodeGenerator
6+
import com.google.devtools.ksp.processing.KSPLogger
7+
import com.google.devtools.ksp.processing.Resolver
8+
import com.google.devtools.ksp.processing.SymbolProcessor
9+
import com.google.devtools.ksp.symbol.KSAnnotated
10+
import com.google.devtools.ksp.symbol.KSClassDeclaration
11+
import com.squareup.kotlinpoet.AnnotationSpec
12+
import com.squareup.kotlinpoet.ClassName
13+
import com.squareup.kotlinpoet.FileSpec
14+
import com.squareup.kotlinpoet.PropertySpec
15+
import com.squareup.kotlinpoet.TypeSpec
16+
import com.squareup.kotlinpoet.ksp.addOriginatingKSFile
17+
import com.squareup.kotlinpoet.ksp.toClassName
18+
import com.squareup.kotlinpoet.ksp.writeTo
19+
import dev.zacsweers.metro.Binds
20+
import dev.zacsweers.metro.ContributesBinding
21+
import dev.zacsweers.metro.ContributesTo
22+
import dev.zacsweers.metro.ForScope
23+
import dev.zacsweers.metro.IntoSet
24+
import software.amazon.app.platform.inject.metro.ContributesScoped
25+
import software.amazon.app.platform.metro.METRO_LOOKUP_PACKAGE
26+
import software.amazon.app.platform.metro.MetroContextAware
27+
28+
/**
29+
* Generates the necessary code in order to support [ContributesScoped].
30+
*
31+
* ```
32+
* package app.platform.inject.metro.software.amazon.test
33+
*
34+
* @ContributesTo(scope = AbcScope::class)
35+
* public interface TestClassGraph {
36+
*
37+
* @Binds
38+
* val TestClass.bindSuperType: SuperType
39+
*
40+
* @Binds @IntoSet @ForScope(UserScope::class)
41+
* val TestClass.bindScoped: Scoped
42+
* }
43+
* ```
44+
*/
45+
@OptIn(KspExperimental::class)
46+
internal class ContributesScopedProcessor(
47+
private val codeGenerator: CodeGenerator,
48+
override val logger: KSPLogger,
49+
) : SymbolProcessor, MetroContextAware {
50+
51+
override fun process(resolver: Resolver): List<KSAnnotated> {
52+
resolver
53+
.getSymbolsWithAnnotation(ContributesScoped::class)
54+
.filterIsInstance<KSClassDeclaration>()
55+
.onEach {
56+
checkIsPublic(it)
57+
checkHasInjectAnnotation(it)
58+
checkImplementsScoped(it)
59+
checkSuperType(it)
60+
}
61+
.forEach { generateGraph(it) }
62+
63+
resolver
64+
.getSymbolsWithAnnotation(ContributesBinding::class)
65+
.filterIsInstance<KSClassDeclaration>()
66+
.forEach { checkDoesNotImplementScoped(it) }
67+
68+
return emptyList()
69+
}
70+
71+
private fun generateGraph(clazz: KSClassDeclaration) {
72+
val packageName = "${METRO_LOOKUP_PACKAGE}.${clazz.packageName.asString()}"
73+
val graphClassName = ClassName(packageName, "${clazz.innerClassNames()}Graph")
74+
val scopeClassName = clazz.scope().type.toClassName()
75+
76+
val fileSpec =
77+
FileSpec.builder(graphClassName)
78+
.addType(
79+
TypeSpec.interfaceBuilder(graphClassName)
80+
.addOriginatingKSFile(clazz.requireContainingFile())
81+
.addAnnotation(
82+
AnnotationSpec.builder(ContributesTo::class)
83+
.addMember("%T::class", scopeClassName)
84+
.build()
85+
)
86+
.addProperties(
87+
clazz.superTypes
88+
.filter { it.resolve().declaration.requireQualifiedName() != scopedFqName }
89+
.map {
90+
val type = it.resolve()
91+
PropertySpec.builder(
92+
"bind${type.declaration.innerClassNames()}",
93+
type.toClassName(),
94+
)
95+
.addAnnotation(Binds::class)
96+
.receiver(clazz.toClassName())
97+
.build()
98+
}
99+
.toList()
100+
)
101+
.addProperty(
102+
PropertySpec.builder("bind${clazz.innerClassNames()}Scoped", scopedClassName)
103+
.addAnnotation(Binds::class)
104+
.addAnnotation(IntoSet::class)
105+
.addAnnotation(
106+
AnnotationSpec.builder(ForScope::class)
107+
.addMember("%T::class", scopeClassName)
108+
.build()
109+
)
110+
.receiver(clazz.toClassName())
111+
.build()
112+
)
113+
.build()
114+
)
115+
.build()
116+
117+
fileSpec.writeTo(codeGenerator, aggregating = false)
118+
}
119+
120+
private fun checkHasInjectAnnotation(clazz: KSClassDeclaration) {
121+
check(clazz.annotations.any { it.isAnnotation(injectFqName) }, clazz) {
122+
"${clazz.simpleName.asString()} must be annotated with @Inject when " +
123+
"using @ContributesScoped."
124+
}
125+
}
126+
127+
private fun checkImplementsScoped(clazz: KSClassDeclaration) {
128+
val extendsScoped =
129+
clazz.getAllSuperTypes().any { it.declaration.qualifiedName?.asString() == scopedFqName }
130+
131+
check(extendsScoped, clazz) {
132+
"In order to use @ContributesScoped, ${clazz.simpleName.asString()} must " +
133+
"implement $scopedFqName."
134+
}
135+
}
136+
137+
private fun checkSuperType(clazz: KSClassDeclaration) {
138+
val superTypeCount =
139+
clazz.superTypes
140+
.filter { it.resolve().declaration.requireQualifiedName() != scopedFqName }
141+
.count()
142+
143+
check(superTypeCount < 2, clazz) {
144+
"In order to use @ContributesScoped, ${clazz.simpleName.asString()} is allowed to have only one " +
145+
"other super type besides Scoped."
146+
}
147+
}
148+
149+
private fun checkDoesNotImplementScoped(clazz: KSClassDeclaration) {
150+
check(
151+
clazz.superTypes.none { it.resolve().declaration.requireQualifiedName() == scopedFqName },
152+
clazz,
153+
) {
154+
"${clazz.simpleName.asString()} implements Scoped, but uses @ContributesBinding instead " +
155+
"of @ContributesScoped. When implementing Scoped the annotation @ContributesScoped " +
156+
"must be used instead of @ContributesBinding to bind both super types correctly. It's " +
157+
"not necessary to use @ContributesBinding."
158+
}
159+
}
160+
}

metro-extensions/contribute/impl-code-generators/src/test/kotlin/software/amazon/app/platform/inject/metro/CommonSourceCode.kt

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,26 +4,16 @@ package software.amazon.app.platform.inject.metro
44

55
import com.tschuchort.compiletesting.JvmCompilationResult
66
import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi
7-
import software.amazon.test.TestRendererGraph
8-
import software.amazon.test.TestRobotGraph
97

108
internal val JvmCompilationResult.graphInterface: Class<*>
119
get() = classLoader.loadClass("software.amazon.test.GraphInterface")
1210

13-
internal fun Class<*>.newTestRendererGraph(): TestRendererGraph {
11+
internal fun <T : Any> Class<*>.newMetroGraph(): T {
1412
val companionObject = fields.single().get(null)
13+
@Suppress("UNCHECKED_CAST")
1514
return classes
1615
.single { it.simpleName == "Companion" }
1716
.declaredMethods
1817
.single { it.name == "create" }
19-
.invoke(companionObject) as TestRendererGraph
20-
}
21-
22-
internal fun Class<*>.newTestRobotGraph(): TestRobotGraph {
23-
val companionObject = fields.single().get(null)
24-
return classes
25-
.single { it.simpleName == "Companion" }
26-
.declaredMethods
27-
.single { it.name == "create" }
28-
.invoke(companionObject) as TestRobotGraph
18+
.invoke(companionObject) as T
2919
}

metro-extensions/contribute/impl-code-generators/src/test/kotlin/software/amazon/app/platform/inject/metro/processor/ContributesRendererProcessorTest.kt

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,14 @@ import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi
2323
import org.junit.jupiter.api.Test
2424
import software.amazon.app.platform.inject.metro.compile
2525
import software.amazon.app.platform.inject.metro.graphInterface
26-
import software.amazon.app.platform.inject.metro.newTestRendererGraph
26+
import software.amazon.app.platform.inject.metro.newMetroGraph
2727
import software.amazon.app.platform.ksp.inner
2828
import software.amazon.app.platform.ksp.isAnnotatedWith
2929
import software.amazon.app.platform.metro.METRO_LOOKUP_PACKAGE
3030
import software.amazon.app.platform.renderer.Renderer
3131
import software.amazon.app.platform.renderer.RendererScope
3232
import software.amazon.app.platform.renderer.metro.RendererKey
33+
import software.amazon.test.TestRendererGraph
3334

3435
class ContributesRendererProcessorTest {
3536

@@ -90,12 +91,13 @@ class ContributesRendererProcessorTest {
9091
assertThat(getAnnotation(RendererKey::class.java).value).isEqualTo(model)
9192
}
9293

93-
assertThat(graphInterface.newTestRendererGraph().renderers.keys).containsOnly(model)
94+
assertThat(graphInterface.newMetroGraph<TestRendererGraph>().renderers.keys)
95+
.containsOnly(model)
9496

95-
assertThat(graphInterface.newTestRendererGraph().modelToRendererMapping.keys)
97+
assertThat(graphInterface.newMetroGraph<TestRendererGraph>().modelToRendererMapping.keys)
9698
.containsOnly(model)
9799

98-
assertThat(graphInterface.newTestRendererGraph().modelToRendererMapping.values)
100+
assertThat(graphInterface.newMetroGraph<TestRendererGraph>().modelToRendererMapping.values)
99101
.containsOnly(testRenderer.kotlin)
100102
}
101103
}
@@ -148,7 +150,8 @@ class ContributesRendererProcessorTest {
148150
assertThat(getAnnotation(RendererKey::class.java).value).isEqualTo(model)
149151
}
150152

151-
assertThat(graphInterface.newTestRendererGraph().renderers.keys).containsOnly(model)
153+
assertThat(graphInterface.newMetroGraph<TestRendererGraph>().renderers.keys)
154+
.containsOnly(model)
152155
}
153156
}
154157

@@ -197,7 +200,7 @@ class ContributesRendererProcessorTest {
197200
assertThat(this).isAnnotatedWith(IntoMap::class)
198201
}
199202

200-
assertThat(graphInterface.newTestRendererGraph().renderers.keys)
203+
assertThat(graphInterface.newMetroGraph<TestRendererGraph>().renderers.keys)
201204
.containsOnly(presenter.model.kotlin)
202205
}
203206
}
@@ -222,7 +225,8 @@ class ContributesRendererProcessorTest {
222225
""",
223226
graphInterfaceSource,
224227
) {
225-
assertThat(graphInterface.newTestRendererGraph().renderers.keys).containsOnly(model)
228+
assertThat(graphInterface.newMetroGraph<TestRendererGraph>().renderers.keys)
229+
.containsOnly(model)
226230
}
227231
}
228232

@@ -371,7 +375,7 @@ class ContributesRendererProcessorTest {
371375
assertThat(it).isAnnotatedWith(RendererKey::class)
372376
}
373377

374-
assertThat(graphInterface.newTestRendererGraph().renderers.keys)
378+
assertThat(graphInterface.newMetroGraph<TestRendererGraph>().renderers.keys)
375379
.containsExactlyInAnyOrder(
376380
presenter.model.kotlin,
377381
presenter.model.inner.kotlin,
@@ -402,7 +406,7 @@ class ContributesRendererProcessorTest {
402406
assertThat(it).isAnnotatedWith(ForScope::class)
403407
}
404408

405-
assertThat(graphInterface.newTestRendererGraph().modelToRendererMapping.keys)
409+
assertThat(graphInterface.newMetroGraph<TestRendererGraph>().modelToRendererMapping.keys)
406410
.containsExactlyInAnyOrder(
407411
presenter.model.kotlin,
408412
presenter.model.inner.kotlin,
@@ -411,7 +415,9 @@ class ContributesRendererProcessorTest {
411415
presenter.model.model2.kotlin,
412416
)
413417

414-
assertThat(graphInterface.newTestRendererGraph().modelToRendererMapping.values.distinct())
418+
assertThat(
419+
graphInterface.newMetroGraph<TestRendererGraph>().modelToRendererMapping.values.distinct()
420+
)
415421
.containsOnly(testRenderer.kotlin)
416422
}
417423
}
@@ -459,13 +465,13 @@ class ContributesRendererProcessorTest {
459465
)
460466
.containsOnly("provideSoftwareAmazonTestTestRendererPresenterModelKey")
461467

462-
assertThat(graphInterface.newTestRendererGraph().renderers.keys)
468+
assertThat(graphInterface.newMetroGraph<TestRendererGraph>().renderers.keys)
463469
.containsOnly(presenter.model.kotlin)
464470

465-
assertThat(graphInterface.newTestRendererGraph().modelToRendererMapping.keys)
471+
assertThat(graphInterface.newMetroGraph<TestRendererGraph>().modelToRendererMapping.keys)
466472
.containsOnly(presenter.model.kotlin)
467473

468-
assertThat(graphInterface.newTestRendererGraph().modelToRendererMapping.values)
474+
assertThat(graphInterface.newMetroGraph<TestRendererGraph>().modelToRendererMapping.values)
469475
.containsOnly(testRenderer.kotlin)
470476
}
471477
}
@@ -501,7 +507,8 @@ class ContributesRendererProcessorTest {
501507
"provideSoftwareAmazonTestTestRendererModelKey",
502508
)
503509

504-
assertThat(graphInterface.newTestRendererGraph().renderers.keys).containsOnly(model)
510+
assertThat(graphInterface.newMetroGraph<TestRendererGraph>().renderers.keys)
511+
.containsOnly(model)
505512
}
506513
}
507514

metro-extensions/contribute/impl-code-generators/src/test/kotlin/software/amazon/app/platform/inject/metro/processor/ContributesRobotGeneratorTest.kt

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,13 @@ import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi
2222
import org.junit.jupiter.api.Test
2323
import software.amazon.app.platform.inject.metro.compile
2424
import software.amazon.app.platform.inject.metro.graphInterface
25-
import software.amazon.app.platform.inject.metro.newTestRobotGraph
25+
import software.amazon.app.platform.inject.metro.newMetroGraph
2626
import software.amazon.app.platform.ksp.capitalize
2727
import software.amazon.app.platform.ksp.isAnnotatedWith
2828
import software.amazon.app.platform.metro.METRO_LOOKUP_PACKAGE
2929
import software.amazon.app.platform.renderer.metro.RobotKey
3030
import software.amazon.app.platform.robot.Robot
31+
import software.amazon.test.TestRobotGraph
3132

3233
class ContributesRobotGeneratorTest {
3334

@@ -66,7 +67,8 @@ class ContributesRobotGeneratorTest {
6667
assertThat(getAnnotation(RobotKey::class.java).value.java).isEqualTo(testRobot)
6768
}
6869

69-
assertThat(graphInterface.newTestRobotGraph().robots.keys).containsOnly(testRobot.kotlin)
70+
assertThat(graphInterface.newMetroGraph<TestRobotGraph>().robots.keys)
71+
.containsOnly(testRobot.kotlin)
7072
}
7173
}
7274

@@ -102,7 +104,8 @@ class ContributesRobotGeneratorTest {
102104
assertThat(getAnnotation(RobotKey::class.java).value.java).isEqualTo(testRobot)
103105
}
104106

105-
assertThat(graphInterface.newTestRobotGraph().robots.keys).containsOnly(testRobot.kotlin)
107+
assertThat(graphInterface.newMetroGraph<TestRobotGraph>().robots.keys)
108+
.containsOnly(testRobot.kotlin)
106109
}
107110
}
108111

@@ -213,10 +216,8 @@ class ContributesRobotGeneratorTest {
213216
import dev.zacsweers.metro.createGraph
214217
import dev.zacsweers.metro.DependencyGraph
215218
import dev.zacsweers.metro.SingleIn
216-
import software.amazon.app.platform.renderer.RendererGraph
217-
import software.amazon.test.TestRendererGraph
218219
219-
@DependencyGraph(AppScope::class, excludes = [RendererGraph::class])
220+
@DependencyGraph(AppScope::class)
220221
@SingleIn(AppScope::class)
221222
interface GraphInterface : TestRobotGraph {
222223
companion object {

0 commit comments

Comments
 (0)