Skip to content

Commit 7e2545d

Browse files
yigittschuchortdev
authored andcommitted
Use a custom registrar to invoke KSP
This commit gets rid of classpath generation and instead just uses a custom registrar to register and implementation of where plugins are pre-defined. I've also changed the symbol processors to be a list similar to KAPT and also added an extension property to get the folder where generated KSP code is kept
1 parent a4012a9 commit 7e2545d

File tree

5 files changed

+134
-137
lines changed

5 files changed

+134
-137
lines changed

README.md

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ To test KSP processors, you need to add a dependency to the ksp module:
124124

125125
```Groovy
126126
dependencies {
127+
implementation 'com.github.tschuchortdev:kotlin-compile-testing:1.2.9'
127128
implementation 'com.github.tschuchortdev:kotlin-compile-testing-ksp:1.2.9'
128129
}
129130
```
@@ -134,12 +135,14 @@ This module adds a new function to the `KotlinCompilation` to specify KSP proces
134135
class MySymbolProcessor : SymbolProcessor {
135136
// implementation of the SymbolProcessor from the KSP API
136137
}
137-
138-
val result = KotlinCompilation().apply {
138+
val compilation = KotlinCompilation().apply {
139139
sources = listOf(source)
140-
symbolProcessors(MySymbolProcessor::class.java)
141-
}.compile()
140+
symbolProcessors = listOf(MySymbolProcessor())
141+
}
142+
val result = compilation.compile()
142143
```
144+
All code generated by the KSP processor will be written into the `KotlinCompilation.kspSourcesDir` directory.
145+
143146

144147
## Projects that use Kotlin-Compile-Testing
145148

ksp/src/main/kotlin/com/tschuchort/compiletesting/Ksp.kt

Lines changed: 80 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -3,66 +3,96 @@
33
*/
44
package com.tschuchort.compiletesting
55

6-
import org.jetbrains.kotlin.ksp.KotlinSymbolProcessingCommandLineProcessor
7-
import org.jetbrains.kotlin.ksp.KotlinSymbolProcessingComponentRegistrar
6+
import org.jetbrains.kotlin.com.intellij.mock.MockProject
7+
import org.jetbrains.kotlin.compiler.plugin.ComponentRegistrar
8+
import org.jetbrains.kotlin.config.CompilerConfiguration
9+
import org.jetbrains.kotlin.ksp.AbstractKotlinSymbolProcessingExtension
10+
import org.jetbrains.kotlin.ksp.KspOptions
811
import org.jetbrains.kotlin.ksp.processing.SymbolProcessor
12+
import org.jetbrains.kotlin.resolve.jvm.extensions.AnalysisHandlerExtension
13+
import org.jetbrains.kotlin.utils.addToStdlib.firstIsInstanceOrNull
914
import java.io.File
1015

11-
private const val KSP_PLUGIN_ID = "org.jetbrains.kotlin.ksp"
12-
13-
private fun KotlinCompilation.initAndGetKspConfig(): KspConfiguration {
14-
val config = KspConfiguration(workingDir.resolve("ksp"))
15-
if (config.workingDir.exists()) {
16-
// already configured, just return
17-
return config
16+
/**
17+
* The list of symbol processors for the kotlin compilation.
18+
* https://goo.gle/ksp
19+
*/
20+
var KotlinCompilation.symbolProcessors: List<SymbolProcessor>
21+
get() = getKspRegistrar().processors
22+
set(value) {
23+
val registrar = getKspRegistrar()
24+
registrar.processors = value
1825
}
19-
config.classesOutDir.mkdirs()
20-
config.sourcesOurDir.mkdirs()
21-
config.syntheticSources.mkdirs()
22-
val kspOptions = listOf(
23-
PluginOption(KSP_PLUGIN_ID, "apclasspath", config.syntheticSources.path),
24-
PluginOption(KSP_PLUGIN_ID, "classes", config.classesOutDir.path),
25-
PluginOption(KSP_PLUGIN_ID, "sources", config.sourcesOurDir.path)
26-
)
27-
compilerPlugins = compilerPlugins + KotlinSymbolProcessingComponentRegistrar()
28-
commandLineProcessors = commandLineProcessors + listOf(KotlinSymbolProcessingCommandLineProcessor())
29-
pluginOptions = pluginOptions + kspOptions
30-
return config
31-
}
3226

33-
fun KotlinCompilation.symbolProcessors(
34-
vararg processors: Class<out SymbolProcessor>
27+
/**
28+
* The directory where generates KSP sources are written
29+
*/
30+
val KotlinCompilation.kspSourcesDir: File
31+
get() = kspWorkingDir.resolve("sources")
32+
33+
/**
34+
* The working directory for KSP
35+
*/
36+
private val KotlinCompilation.kspWorkingDir: File
37+
get() = workingDir.resolve("ksp")
38+
39+
/**
40+
* The directory where compiled KSP classes are written
41+
*/
42+
// TODO this seems to be ignored by KSP and it is putting classes into regular classes directory
43+
// but we still need to provide it in the KSP options builder as it is required
44+
// once it works, we should make the property public.
45+
private val KotlinCompilation.kspClassesDir: File
46+
get() = kspWorkingDir.resolve("classes")
47+
48+
/**
49+
* Custom subclass of [AbstractKotlinSymbolProcessingExtension] where processors are pre-defined instead of being
50+
* loaded via ServiceLocator.
51+
*/
52+
private class KspTestExtension(
53+
options: KspOptions,
54+
private val processors: List<SymbolProcessor>
55+
) : AbstractKotlinSymbolProcessingExtension(
56+
options = options,
57+
testMode = false
3558
) {
36-
check(processors.isNotEmpty()) {
37-
"Must provide at least 1 symbol processor"
38-
}
39-
val config = initAndGetKspConfig()
59+
override fun loadProcessors() = processors
60+
}
4061

41-
// create a fake classpath that references our symbol processor
42-
config.syntheticSources.apply {
43-
resolve("META-INF/services/org.jetbrains.kotlin.ksp.processing.SymbolProcessor").apply {
44-
parentFile.mkdirs()
45-
// keep existing ones in case this function is called multiple times
46-
val existing = if (exists()) {
47-
readLines(Charsets.UTF_8)
48-
} else {
49-
emptyList()
62+
/**
63+
* Registers the [KspTestExtension] to load the given list of processors.
64+
*/
65+
private class KspCompileTestingComponentRegistrar(
66+
private val compilation: KotlinCompilation
67+
) : ComponentRegistrar {
68+
var processors = emptyList<SymbolProcessor>()
69+
override fun registerProjectComponents(project: MockProject, configuration: CompilerConfiguration) {
70+
if (processors.isEmpty()) {
71+
return
72+
}
73+
val options = KspOptions.Builder().apply {
74+
this.classesOutputDir = compilation.kspClassesDir.also {
75+
it.deleteRecursively()
76+
it.mkdirs()
5077
}
51-
val processorNames = existing + processors.map {
52-
it.typeName
78+
this.sourcesOutputDir = compilation.kspSourcesDir.also {
79+
it.deleteRecursively()
80+
it.mkdirs()
5381
}
54-
writeText(
55-
processorNames.joinToString(System.lineSeparator())
56-
)
57-
}
82+
}.build()
83+
val registrar = KspTestExtension(options, processors)
84+
AnalysisHandlerExtension.registerExtension(project, registrar)
5885
}
59-
this.kaptSourceDir
6086
}
6187

62-
private data class KspConfiguration(
63-
val workingDir: File
64-
) {
65-
val classesOutDir = workingDir.resolve("classesOutput")
66-
val sourcesOurDir = workingDir.resolve("sourcesOutput")
67-
val syntheticSources = workingDir.resolve("synthetic-ksp-service")
88+
/**
89+
* Gets the test registrar from the plugin list or adds if it does not exist.
90+
*/
91+
private fun KotlinCompilation.getKspRegistrar(): KspCompileTestingComponentRegistrar {
92+
compilerPlugins.firstIsInstanceOrNull<KspCompileTestingComponentRegistrar>()?.let {
93+
return it
94+
}
95+
val kspRegistrar = KspCompileTestingComponentRegistrar(this)
96+
compilerPlugins = compilerPlugins + kspRegistrar
97+
return kspRegistrar
6898
}

ksp/src/test/kotlin/com/tschuchort/compiletesting/AbstractSymbolProcessor.kt renamed to ksp/src/test/kotlin/com/tschuchort/compiletesting/AbstractTestSymbolProcessor.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@ import org.jetbrains.kotlin.ksp.processing.CodeGenerator
44
import org.jetbrains.kotlin.ksp.processing.Resolver
55
import org.jetbrains.kotlin.ksp.processing.SymbolProcessor
66

7-
internal open class AbstractSymbolProcessor : SymbolProcessor {
7+
/**
8+
* Helper class to write tests, only used in Ksp Compile Testing tests, not a public API.
9+
*/
10+
internal open class AbstractTestSymbolProcessor : SymbolProcessor {
811
protected lateinit var codeGenerator: CodeGenerator
912
override fun finish() {
1013
}

ksp/src/test/kotlin/com/tschuchort/compiletesting/DelegatingSymbolProcessorRule.kt

Lines changed: 0 additions & 58 deletions
This file was deleted.

ksp/src/test/kotlin/com/tschuchort/compiletesting/KspTest.kt

Lines changed: 43 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -8,37 +8,33 @@ import org.assertj.core.api.Assertions.assertThat
88
import org.jetbrains.kotlin.ksp.processing.Resolver
99
import org.jetbrains.kotlin.ksp.processing.SymbolProcessor
1010
import org.jetbrains.kotlin.ksp.symbol.KSClassDeclaration
11-
import org.junit.Rule
1211
import org.junit.Test
1312
import org.junit.runner.RunWith
1413
import org.junit.runners.JUnit4
14+
import org.mockito.Mockito.`when`
1515

1616
@RunWith(JUnit4::class)
1717
class KspTest {
18-
@Rule
19-
@JvmField
20-
val processorRule = DelegatingSymbolProcessorRule()
21-
2218
@Test
2319
fun failedKspTest() {
20+
val instance = mock<SymbolProcessor>()
21+
`when`(instance.process(any())).thenThrow(
22+
RuntimeException("intentional fail")
23+
)
2424
val result = KotlinCompilation().apply {
2525
sources = listOf(DUMMY_KOTLIN_SRC)
26-
symbolProcessors(processorRule.delegateTo(object : AbstractSymbolProcessor() {
27-
override fun process(resolver: Resolver) {
28-
throw RuntimeException("intentional fail")
29-
}
30-
}))
26+
symbolProcessors = listOf(instance)
3127
}.compile()
3228
assertThat(result.exitCode).isEqualTo(ExitCode.INTERNAL_ERROR)
3329
assertThat(result.messages).contains("intentional fail")
3430
}
3531

3632
@Test
37-
fun processorIsCalled() {
33+
fun allProcessorMethodsAreCalled() {
3834
val instance = mock<SymbolProcessor>()
3935
val result = KotlinCompilation().apply {
4036
sources = listOf(DUMMY_KOTLIN_SRC)
41-
symbolProcessors(processorRule.delegateTo(instance))
37+
symbolProcessors = listOf(instance)
4238
}.compile()
4339
assertThat(result.exitCode).isEqualTo(ExitCode.OK)
4440
instance.inOrder {
@@ -69,7 +65,7 @@ class KspTest {
6965
}
7066
""".trimIndent()
7167
)
72-
val processor = object : AbstractSymbolProcessor() {
68+
val processor = object : AbstractTestSymbolProcessor() {
7369
override fun process(resolver: Resolver) {
7470
val symbols = resolver.getSymbolsWithAnnotation("foo.bar.TestAnnotation")
7571
assertThat(symbols.size).isEqualTo(1)
@@ -92,7 +88,7 @@ class KspTest {
9288
}
9389
val result = KotlinCompilation().apply {
9490
sources = listOf(annotation, targetClass)
95-
symbolProcessors(processorRule.delegateTo(processor))
91+
symbolProcessors = listOf(processor)
9692
}.compile()
9793
assertThat(result.exitCode).isEqualTo(ExitCode.OK)
9894
}
@@ -111,25 +107,48 @@ class KspTest {
111107
)
112108
val result = KotlinCompilation().apply {
113109
sources = listOf(source)
114-
symbolProcessors(Write_foo_bar_A::class.java, Write_foo_bar_B::class.java)
115-
symbolProcessors(Write_foo_bar_C::class.java)
110+
symbolProcessors = listOf(
111+
ClassGeneratingProcessor("generated", "A"),
112+
ClassGeneratingProcessor("generated", "B"))
113+
symbolProcessors = symbolProcessors + ClassGeneratingProcessor("generated", "C")
116114
}.compile()
117115
assertThat(result.exitCode).isEqualTo(ExitCode.OK)
118116
}
119117

120-
@Suppress("ClassName")
121-
internal class Write_foo_bar_A() : ClassGeneratingProcessor("generated", "A")
122-
123-
@Suppress("ClassName")
124-
internal class Write_foo_bar_B() : ClassGeneratingProcessor("generated", "B")
118+
@Test
119+
fun readProcessors() {
120+
val instance1 = mock<SymbolProcessor>()
121+
val instance2 = mock<SymbolProcessor>()
122+
KotlinCompilation().apply {
123+
symbolProcessors = listOf(instance1)
124+
assertThat(symbolProcessors).containsExactly(instance1)
125+
symbolProcessors = listOf(instance2)
126+
assertThat(symbolProcessors).containsExactly(instance2)
127+
symbolProcessors = symbolProcessors + instance1
128+
assertThat(symbolProcessors).containsExactly(instance2, instance1)
129+
}
130+
}
125131

126-
@Suppress("ClassName")
127-
internal class Write_foo_bar_C() : ClassGeneratingProcessor("generated", "C")
132+
@Test
133+
fun outputDirectoryContents() {
134+
val compilation = KotlinCompilation().apply {
135+
sources = listOf(DUMMY_KOTLIN_SRC)
136+
symbolProcessors = listOf(ClassGeneratingProcessor("generated", "Gen"))
137+
}
138+
val result = compilation.compile()
139+
assertThat(result.exitCode).isEqualTo(ExitCode.OK)
140+
val generatedSources = compilation.kspSourcesDir.walkTopDown().filter {
141+
it.isFile
142+
}.toList()
143+
assertThat(generatedSources).containsExactly(
144+
compilation.kspSourcesDir.resolve("generated/Gen.kt")
145+
)
146+
}
128147

129148
internal open class ClassGeneratingProcessor(
130149
private val packageName: String,
131150
private val className: String
132-
) : AbstractSymbolProcessor() {
151+
) : AbstractTestSymbolProcessor() {
133152
override fun process(resolver: Resolver) {
134153
super.process(resolver)
135154
codeGenerator.createNewFile(packageName, className).writeText(

0 commit comments

Comments
 (0)