Skip to content

Commit ee08366

Browse files
committed
Add feature to pass options to compiler plugins and parse arbitrary string options
1 parent 31ea32f commit ee08366

File tree

8 files changed

+285
-63
lines changed

8 files changed

+285
-63
lines changed

build.gradle

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
buildscript {
2-
ext.kotlin_version = '1.3.60'
2+
ext.kotlin_version = '1.3.61'
33

44
repositories {
55
mavenCentral()
@@ -15,7 +15,7 @@ buildscript {
1515

1616
plugins {
1717
id 'java'
18-
id 'org.jetbrains.kotlin.jvm' version '1.3.60'
18+
id 'org.jetbrains.kotlin.jvm' version '1.3.61'
1919
}
2020

2121
apply plugin: 'kotlin'
@@ -44,9 +44,12 @@ dependencies {
4444
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8"
4545
testImplementation group: 'junit', name: 'junit', version: '4.12'
4646

47+
compileOnly "com.google.auto.service:auto-service:1.0-rc2"
48+
kapt "com.google.auto.service:auto-service:1.0-rc2"
49+
4750
testImplementation "org.assertj:assertj-core:3.11.1"
4851
testImplementation "org.mockito:mockito-core:3.1.0"
49-
testImplementation "com.nhaarman.mockitokotlin2:mockito-kotlin:2.0.0"
52+
testImplementation "com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0"
5053

5154
testImplementation 'com.squareup:kotlinpoet:1.4.0'
5255
testImplementation 'com.squareup:javapoet:1.11.1'
@@ -56,12 +59,12 @@ dependencies {
5659

5760
// This dependency is only needed as a "sample" compiler plugin to test that
5861
// running compiler plugins passed via the pluginClasspath CLI option works
59-
testRuntime "org.jetbrains.kotlin:kotlin-scripting-compiler:1.3.60"
62+
testRuntime "org.jetbrains.kotlin:kotlin-scripting-compiler:1.3.61"
6063

6164
// The Kotlin compiler should be near the end of the list because its .jar file includes
6265
// an obsolete version of Guava
63-
implementation "org.jetbrains.kotlin:kotlin-compiler-embeddable:1.3.60"
64-
implementation "org.jetbrains.kotlin:kotlin-annotation-processing-embeddable:1.3.60"
66+
implementation "org.jetbrains.kotlin:kotlin-compiler-embeddable:1.3.61"
67+
implementation "org.jetbrains.kotlin:kotlin-annotation-processing-embeddable:1.3.61"
6568
}
6669

6770
compileKotlin {

src/main/kotlin/com/tschuchort/compiletesting/KotlinCompilation.kt

Lines changed: 100 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,13 @@ import org.jetbrains.kotlin.base.kapt3.KaptFlag
2424
import org.jetbrains.kotlin.base.kapt3.KaptOptions
2525
import org.jetbrains.kotlin.cli.common.ExitCode
2626
import org.jetbrains.kotlin.cli.common.arguments.K2JVMCompilerArguments
27+
import org.jetbrains.kotlin.cli.common.arguments.parseCommandLineArguments
28+
import org.jetbrains.kotlin.cli.common.arguments.validateArguments
2729
import org.jetbrains.kotlin.cli.common.messages.MessageRenderer
2830
import org.jetbrains.kotlin.cli.common.messages.PrintingMessageCollector
2931
import org.jetbrains.kotlin.cli.jvm.K2JVMCompiler
3032
import org.jetbrains.kotlin.cli.jvm.plugins.ServiceLoaderLite
33+
import org.jetbrains.kotlin.compiler.plugin.CommandLineProcessor
3134
import org.jetbrains.kotlin.compiler.plugin.ComponentRegistrar
3235
import org.jetbrains.kotlin.config.JVMAssertionsMode
3336
import org.jetbrains.kotlin.config.JvmDefaultMode
@@ -45,6 +48,11 @@ import java.nio.file.Paths
4548
import javax.annotation.processing.Processor
4649
import javax.tools.*
4750

51+
data class PluginOption(val pluginId: PluginId, val optionName: OptionName, val optionValue: OptionValue)
52+
53+
typealias PluginId = String
54+
typealias OptionName = String
55+
typealias OptionValue = String
4856

4957
@Suppress("MemberVisibilityCanBePrivate")
5058
class KotlinCompilation {
@@ -56,7 +64,7 @@ class KotlinCompilation {
5664
}
5765

5866
/** Arbitrary arguments to be passed to kapt */
59-
var kaptArgs: MutableMap<String, String> = mutableMapOf()
67+
var kaptArgs: MutableMap<OptionName, OptionValue> = mutableMapOf()
6068

6169
/**
6270
* Paths to directories or .jar files that contain classes
@@ -75,6 +83,11 @@ class KotlinCompilation {
7583
*/
7684
var compilerPlugins: List<ComponentRegistrar> = emptyList()
7785

86+
/**
87+
* Commandline processors for compiler plugins that should be added to the compilation
88+
*/
89+
var commandLineProcessors: List<CommandLineProcessor> = emptyList()
90+
7891
/** Source files to be compiled */
7992
var sources: List<SourceFile> = emptyList()
8093

@@ -213,7 +226,12 @@ class KotlinCompilation {
213226
var sanitizeParentheses: Boolean = false
214227

215228
/** Paths to output directories for friend modules (whose internals should be visible) */
216-
var friendPaths: MutableList<File> = mutableListOf()
229+
var friendPaths: List<File> = emptyList()
230+
231+
/** Additional string arguments to the Kotlin compiler */
232+
var kotlincArguments: List<String> = emptyList()
233+
/** Options to be passed to compiler plugins: -P plugin:<pluginId>:<optionName>=<value>*/
234+
var pluginOptions: List<PluginOption> = emptyList()
217235

218236
/**
219237
* Path to the JDK to be used
@@ -355,87 +373,119 @@ class KotlinCompilation {
355373
= sourcesGeneratedByAnnotationProcessor + compiledClassAndResourceFiles + generatedStubFiles
356374
}
357375

376+
358377
// setup common arguments for the two kotlinc calls
359-
private fun commonK2JVMArgs() = K2JVMCompilerArguments().also { it ->
360-
it.destination = classesDir.absolutePath
361-
it.classpath = commonClasspaths().joinToString(separator = File.pathSeparator)
378+
private fun commonK2JVMArgs() = K2JVMCompilerArguments().also { args ->
379+
args.destination = classesDir.absolutePath
380+
args.classpath = commonClasspaths().joinToString(separator = File.pathSeparator)
362381

363-
it.pluginClasspaths = pluginClasspaths.map(File::getAbsolutePath).toTypedArray()
382+
args.pluginClasspaths = pluginClasspaths.map(File::getAbsolutePath).toTypedArray()
364383

365384
if(jdkHome != null) {
366-
it.jdkHome = jdkHome!!.absolutePath
385+
args.jdkHome = jdkHome!!.absolutePath
367386
}
368387
else {
369388
log("Using option -no-jdk. Kotlinc won't look for a JDK.")
370-
it.noJdk = true
389+
args.noJdk = true
371390
}
372391

373-
it.verbose = verbose
374-
it.includeRuntime = includeRuntime
392+
args.verbose = verbose
393+
args.includeRuntime = includeRuntime
375394

376395
// the compiler should never look for stdlib or reflect in the
377396
// kotlinHome directory (which is null anyway). We will put them
378397
// in the classpath manually if they're needed
379-
it.noStdlib = true
380-
it.noReflect = true
398+
args.noStdlib = true
399+
args.noReflect = true
381400

382401
if(moduleName != null)
383-
it.moduleName = moduleName
402+
args.moduleName = moduleName
384403

385-
it.jvmTarget = jvmTarget
386-
it.javaParameters = javaParameters
387-
it.useIR = useIR
404+
args.jvmTarget = jvmTarget
405+
args.javaParameters = javaParameters
406+
args.useIR = useIR
388407

389408
if(javaModulePath != null)
390-
it.javaModulePath = javaModulePath!!.toString()
409+
args.javaModulePath = javaModulePath!!.toString()
391410

392-
it.additionalJavaModules = additionalJavaModules.map(File::getAbsolutePath).toTypedArray()
393-
it.noCallAssertions = noCallAssertions
394-
it.noParamAssertions = noParamAssertions
395-
it.noReceiverAssertions = noReceiverAssertions
396-
it.strictJavaNullabilityAssertions = strictJavaNullabilityAssertions
397-
it.noOptimize = noOptimize
411+
args.additionalJavaModules = additionalJavaModules.map(File::getAbsolutePath).toTypedArray()
412+
args.noCallAssertions = noCallAssertions
413+
args.noParamAssertions = noParamAssertions
414+
args.noReceiverAssertions = noReceiverAssertions
415+
args.strictJavaNullabilityAssertions = strictJavaNullabilityAssertions
416+
args.noOptimize = noOptimize
398417

399418
if(constructorCallNormalizationMode != null)
400-
it.constructorCallNormalizationMode = constructorCallNormalizationMode
419+
args.constructorCallNormalizationMode = constructorCallNormalizationMode
401420

402421
if(assertionsMode != null)
403-
it.assertionsMode = assertionsMode
422+
args.assertionsMode = assertionsMode
404423

405424
if(buildFile != null)
406-
it.buildFile = buildFile!!.toString()
425+
args.buildFile = buildFile!!.toString()
407426

408-
it.inheritMultifileParts = inheritMultifileParts
409-
it.useTypeTable = useTypeTable
427+
args.inheritMultifileParts = inheritMultifileParts
428+
args.useTypeTable = useTypeTable
410429

411430
if(declarationsOutputPath != null)
412-
it.declarationsOutputPath = declarationsOutputPath!!.toString()
431+
args.declarationsOutputPath = declarationsOutputPath!!.toString()
413432

414-
it.singleModule = singleModule
433+
args.singleModule = singleModule
415434

416435
if(javacArguments.isNotEmpty())
417-
it.javacArguments = javacArguments.toTypedArray()
436+
args.javacArguments = javacArguments.toTypedArray()
418437

419438
if(supportCompatqualCheckerFrameworkAnnotations != null)
420-
it.supportCompatqualCheckerFrameworkAnnotations = supportCompatqualCheckerFrameworkAnnotations
439+
args.supportCompatqualCheckerFrameworkAnnotations = supportCompatqualCheckerFrameworkAnnotations
421440

422-
it.jvmDefault = jvmDefault
423-
it.strictMetadataVersionSemantics = strictMetadataVersionSemantics
424-
it.sanitizeParentheses = sanitizeParentheses
441+
args.jvmDefault = jvmDefault
442+
args.strictMetadataVersionSemantics = strictMetadataVersionSemantics
443+
args.sanitizeParentheses = sanitizeParentheses
425444

426445
if(friendPaths.isNotEmpty())
427-
it.friendPaths = friendPaths.map(File::getAbsolutePath).toTypedArray()
446+
args.friendPaths = friendPaths.map(File::getAbsolutePath).toTypedArray()
428447

429448
if(scriptResolverEnvironment.isNotEmpty())
430-
it.scriptResolverEnvironment = scriptResolverEnvironment.map { (key, value) -> "$key=\"$value\"" }.toTypedArray()
431-
432-
it.noExceptionOnExplicitEqualsForBoxedNull = noExceptionOnExplicitEqualsForBoxedNull
433-
it.skipRuntimeVersionCheck = skipRuntimeVersionCheck
434-
it.suppressWarnings = suppressWarnings
435-
it.allWarningsAsErrors = allWarningsAsErrors
436-
it.reportOutputFiles = reportOutputFiles
437-
it.reportPerf = reportPerformance
449+
args.scriptResolverEnvironment = scriptResolverEnvironment.map { (key, value) -> "$key=\"$value\"" }.toTypedArray()
450+
451+
args.noExceptionOnExplicitEqualsForBoxedNull = noExceptionOnExplicitEqualsForBoxedNull
452+
args.skipRuntimeVersionCheck = skipRuntimeVersionCheck
453+
args.suppressWarnings = suppressWarnings
454+
args.allWarningsAsErrors = allWarningsAsErrors
455+
args.reportOutputFiles = reportOutputFiles
456+
args.reportPerf = reportPerformance
457+
args.javaPackagePrefix = javaPackagePrefix
458+
args.suppressMissingBuiltinsError = suppressMissingBuiltinsError
459+
460+
/**
461+
* It's not possible to pass dynamic [CommandLineProcessor] instancess directly to the [K2JVMCompiler]
462+
* because the compiler discoveres them on the classpath through a service locator, so we need to apply
463+
* the same trick as with [ComponentRegistrar]s: We put our own static [CommandLineProcessor] on the
464+
* classpath which in turn calls the user's dynamic [CommandLineProcessor] instances.
465+
*/
466+
MainCommandLineProcessor.threadLocalParameters.set(
467+
MainCommandLineProcessor.ThreadLocalParameters(commandLineProcessors)
468+
)
469+
470+
/**
471+
* Our [MainCommandLineProcessor] only has access to the CLI options that belong to its own plugin ID.
472+
* So in order to be able to access CLI options that are meant for other [CommandLineProcessor]s we
473+
* wrap these CLI options, send them to our own plugin ID and later unwrap them again to forward them
474+
* to the correct [CommandLineProcessor].
475+
*/
476+
args.pluginOptions = pluginOptions.map { (pluginId, optionName, optionValue) ->
477+
"plugin:${MainCommandLineProcessor.pluginId}:${MainCommandLineProcessor.encodeForeignOptionName(pluginId, optionName)}=$optionValue"
478+
}.toTypedArray()
479+
480+
/* Parse extra CLI arguments that are given as strings so users can specify arguments that are not yet
481+
implemented here as well-typed properties. */
482+
parseCommandLineArguments(kotlincArguments, args)
483+
484+
validateArguments(args.errors)?.let {
485+
throw IllegalArgumentException("Errors parsing kotlinc CLI arguments:\n$it")
486+
}
438487
}
488+
439489
/** Performs the 1st and 2nd compilation step to generate stubs and run annotation processors */
440490
private fun stubsAndApt(sourceFiles: List<File>): ExitCode {
441491
if(annotationProcessors.isEmpty()) {
@@ -467,7 +517,7 @@ class KotlinCompilation {
467517
*
468518
*/
469519
MainComponentRegistrar.threadLocalParameters.set(
470-
MainComponentRegistrar.Parameters(
520+
MainComponentRegistrar.ThreadLocalParameters(
471521
annotationProcessors.map { IncrementalProcessor(it, DeclaredProcType.NON_INCREMENTAL) },
472522
kaptOptions,
473523
compilerPlugins
@@ -555,14 +605,13 @@ class KotlinCompilation {
555605
* the list is set to empty
556606
*/
557607
MainComponentRegistrar.threadLocalParameters.set(
558-
MainComponentRegistrar.Parameters(
608+
MainComponentRegistrar.ThreadLocalParameters(
559609
listOf(),
560610
KaptOptions.Builder(),
561611
compilerPlugins
562612
)
563613
)
564614

565-
566615
val sources = sourceFiles +
567616
kaptKotlinGeneratedDir.listFilesRecursively() +
568617
kaptSourceDir.listFilesRecursively()
@@ -572,7 +621,7 @@ class KotlinCompilation {
572621
return ExitCode.OK
573622

574623
// in this step also include source files generated by kapt in the previous step
575-
val k2JvmArgs = commonK2JVMArgs().also {jvmArgs->
624+
val k2JvmArgs = commonK2JVMArgs().also { jvmArgs->
576625
jvmArgs.pluginClasspaths = (jvmArgs.pluginClasspaths ?: emptyArray()) + arrayOf(getResourcesPath())
577626
jvmArgs.freeArgs = sources.map(File::getAbsolutePath).distinct()
578627
}
@@ -777,7 +826,9 @@ class KotlinCompilation {
777826

778827
private fun commonClasspaths() = mutableListOf<File>().apply {
779828
addAll(classpaths)
780-
addAll(listOfNotNull(kotlinStdLibJar, kotlinReflectJar, kotlinScriptRuntimeJar))
829+
addAll(listOfNotNull(kotlinStdLibJar, kotlinStdLibCommonJar, kotlinStdLibJdkJar,
830+
kotlinReflectJar, kotlinScriptRuntimeJar
831+
))
781832

782833
if(inheritClassPath) {
783834
addAll(hostClasspaths)
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package com.tschuchort.compiletesting
2+
3+
import com.google.auto.service.AutoService
4+
import org.jetbrains.kotlin.compiler.plugin.AbstractCliOption
5+
import org.jetbrains.kotlin.compiler.plugin.CliOption
6+
import org.jetbrains.kotlin.compiler.plugin.CommandLineProcessor
7+
import org.jetbrains.kotlin.config.CompilerConfiguration
8+
import java.util.*
9+
10+
@AutoService(CommandLineProcessor::class)
11+
internal class MainCommandLineProcessor : CommandLineProcessor {
12+
override val pluginId: String = Companion.pluginId
13+
14+
override val pluginOptions: Collection<AbstractCliOption>
15+
get() = threadLocalParameters.get()?.pluginOptions
16+
?: error("MainCommandLineProcessor::pluginOptions accessed before thread local parameters have been set")
17+
18+
companion object {
19+
const val pluginId = "com.tschuchort.compiletesting.maincommandlineprocessor"
20+
21+
/** This CommandLineProcessor is instantiated by K2JVMCompiler using
22+
* a service locator. So we can't just pass parameters to it easily.
23+
* Instead we need to use a thread-local global variable to pass
24+
* any parameters that change between compilations
25+
*/
26+
val threadLocalParameters: ThreadLocal<ThreadLocalParameters> = ThreadLocal()
27+
28+
private fun encode(str: String) = str //Base64.getEncoder().encodeToString(str.toByteArray()).replace('=', '%')
29+
30+
private fun decode(str: String) = str // String(Base64.getDecoder().decode(str.replace('%', '=')))
31+
32+
fun encodeForeignOptionName(processorPluginId: PluginId, optionName: OptionName)
33+
= encode(processorPluginId) + ":" + encode(optionName)
34+
35+
fun decodeForeignOptionName(str: String): Pair<PluginId, OptionName> {
36+
return Regex("(.*?):(.*)").matchEntire(str)?.groupValues?.let { (_, encodedPluginId, encodedOptionName) ->
37+
Pair(decode(encodedPluginId), decode(encodedOptionName))
38+
}
39+
?: error("Could not decode foreign option name: '$str'.")
40+
}
41+
}
42+
43+
class ThreadLocalParameters(cliProcessors: List<CommandLineProcessor>) {
44+
val cliProcessorsByPluginId: Map<PluginId, List<CommandLineProcessor>>
45+
= cliProcessors.groupBy(CommandLineProcessor::pluginId)
46+
47+
val optionByProcessorAndName: Map<Pair<CommandLineProcessor, OptionName>, AbstractCliOption>
48+
= cliProcessors.flatMap { cliProcessor ->
49+
cliProcessor.pluginOptions.map { option ->
50+
Pair(Pair(cliProcessor, option.optionName), option)
51+
}
52+
}.toMap()
53+
54+
val pluginOptions = cliProcessorsByPluginId.flatMap { (processorPluginId, cliProcessors) ->
55+
cliProcessors.flatMap { cliProcessor ->
56+
cliProcessor.pluginOptions.map { option ->
57+
CliOption(
58+
encodeForeignOptionName(processorPluginId, option.optionName),
59+
option.valueDescription,
60+
option.description,
61+
option.required,
62+
option.allowMultipleOccurrences
63+
)
64+
}
65+
}
66+
}
67+
}
68+
69+
override fun processOption(option: AbstractCliOption, value: String, configuration: CompilerConfiguration) {
70+
val (foreignPluginId, foreignOptionName) = decodeForeignOptionName(option.optionName)
71+
val params = threadLocalParameters.get()
72+
73+
params.cliProcessorsByPluginId[foreignPluginId]?.forEach { cliProcessor ->
74+
cliProcessor.processOption(
75+
params.optionByProcessorAndName[Pair(cliProcessor, foreignOptionName)]
76+
?: error("Could not get AbstractCliOption for option '$foreignOptionName'"),
77+
value, configuration
78+
)
79+
}
80+
?: error("No CommandLineProcessor given for option '$foreignOptionName' for plugin ID $foreignPluginId")
81+
}
82+
}

0 commit comments

Comments
 (0)