Move scripting configuration into compiler plugin

This commit is contained in:
Ilya Chernikov
2018-02-26 19:20:53 +01:00
parent 1514a113b8
commit d05f67127d
23 changed files with 452 additions and 168 deletions
@@ -0,0 +1 @@
org.jetbrains.kotlin.scripting.compiler.plugin.ScriptingCommandLineProcessor
@@ -0,0 +1 @@
org.jetbrains.kotlin.scripting.compiler.plugin.ScriptingCompilerConfigurationComponentRegistrar
@@ -0,0 +1,53 @@
/*
* Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.scripting.compiler.plugin
import com.intellij.openapi.fileTypes.LanguageFileType
import kotlinx.coroutines.experimental.runBlocking
import org.jetbrains.kotlin.idea.KotlinFileType
import org.jetbrains.kotlin.name.Name
import org.jetbrains.kotlin.name.NameUtils
import org.jetbrains.kotlin.psi.KtScript
import org.jetbrains.kotlin.script.KotlinScriptDefinition
import kotlin.reflect.KClass
import kotlin.script.experimental.api.ScriptCompileConfigurationParams
import kotlin.script.experimental.api.ScriptDefinition
import kotlin.script.experimental.api.getOrNull
import kotlin.script.experimental.api.resultOrNull
import kotlin.script.experimental.dependencies.DependenciesResolver
import kotlin.script.experimental.jvm.impl.BridgeDependenciesResolver
class KotlinScriptDefinitionAdapterFromNewAPI(val scriptDefinition: ScriptDefinition) : KotlinScriptDefinition(scriptDefinition.baseClass) {
override val name: String get() = scriptDefinition.selector.name
// TODO: consider creating separate type (subtype? for kotlin scripts)
override val fileType: LanguageFileType = KotlinFileType.INSTANCE
override val annotationsForSamWithReceivers: List<String>
get() = emptyList()
override fun isScript(fileName: String): Boolean =
fileName.endsWith("." + scriptDefinition.selector.fileExtension)
override fun getScriptName(script: KtScript): Name {
val fileBasedName = NameUtils.getScriptNameForFile(script.containingKtFile.name)
return Name.identifier(scriptDefinition.selector.makeScriptName(fileBasedName.identifier))
}
override val dependencyResolver: DependenciesResolver by lazy {
BridgeDependenciesResolver(scriptDefinition.configurator)
}
override val acceptedAnnotations: List<KClass<out Annotation>> by lazy {
runBlocking {
scriptDefinition.configurator.baseConfiguration(null)
}.resultOrNull()?.getOrNull(ScriptCompileConfigurationParams.updateConfigurationOnAnnotations)?.toList()
?: emptyList()
}
}
@@ -0,0 +1,103 @@
/*
* Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.scripting.compiler.plugin
import java.io.File
import org.jetbrains.kotlin.compiler.plugin.CliOption
import org.jetbrains.kotlin.compiler.plugin.CliOptionProcessingException
import org.jetbrains.kotlin.compiler.plugin.CommandLineProcessor
import org.jetbrains.kotlin.config.CompilerConfiguration
import org.jetbrains.kotlin.config.CompilerConfigurationKey
object ScriptingConfigurationKeys {
val SCRIPT_DEFINITIONS: CompilerConfigurationKey<List<String>> =
CompilerConfigurationKey.create("Script definition classes")
val SCRIPT_DEFINITIONS_CLASSPATH: CompilerConfigurationKey<List<File>> =
CompilerConfigurationKey.create("Additional classpath for the script definitions")
val DISABLE_SCRIPT_DEFINITIONS_FROM_CLSSPATH_OPTION: CompilerConfigurationKey<Boolean> =
CompilerConfigurationKey.create("Do not extract script definitions from the compilation classpath")
val LEGACY_SCRIPT_RESOLVER_ENVIRONMENT_OPTION: CompilerConfigurationKey<MutableMap<String, Any?>> =
CompilerConfigurationKey.create("Script resolver environment")
}
class ScriptingCommandLineProcessor : CommandLineProcessor {
companion object {
val SCRIPT_DEFINITIONS_OPTION = CliOption(
"script-definitions", "<fully qualified class name[,]>", "Script definition classes",
required = false, allowMultipleOccurrences = true
)
val SCRIPT_DEFINITIONS_CLASSPATH_OPTION = CliOption(
"script-definitions-classpath", "<classpath entry[:]>", "Additional classpath for the script definitions",
required = false, allowMultipleOccurrences = true
)
val DISABLE_SCRIPT_DEFINITIONS_FROM_CLSSPATH_OPTION = CliOption(
"disable-script-definitions-from-classpath", "true/false", "Do not extract script definitions from the compilation classpath",
required = false, allowMultipleOccurrences = false
)
val LEGACY_SCRIPT_TEMPLATES_OPTION = CliOption(
"script-templates", "<fully qualified class name[,]>", "Script definition template classes",
required = false, allowMultipleOccurrences = true
)
val LEGACY_SCRIPT_RESOLVER_ENVIRONMENT_OPTION = CliOption(
"script-resolver-environment", "<key=value[,]>",
"Script resolver environment in key-value pairs (the value could be quoted and escaped)",
required = false, allowMultipleOccurrences = true
)
val PLUGIN_ID = "kotlin.scripting"
}
override val pluginId = PLUGIN_ID
override val pluginOptions =
listOf(
SCRIPT_DEFINITIONS_OPTION,
SCRIPT_DEFINITIONS_CLASSPATH_OPTION,
DISABLE_SCRIPT_DEFINITIONS_FROM_CLSSPATH_OPTION,
LEGACY_SCRIPT_TEMPLATES_OPTION,
LEGACY_SCRIPT_RESOLVER_ENVIRONMENT_OPTION
)
override fun processOption(option: CliOption, value: String, configuration: CompilerConfiguration) = when (option) {
SCRIPT_DEFINITIONS_OPTION, LEGACY_SCRIPT_TEMPLATES_OPTION -> {
val currentDefs = configuration.getList(ScriptingConfigurationKeys.SCRIPT_DEFINITIONS).toMutableList()
currentDefs.addAll(value.split(','))
configuration.put(ScriptingConfigurationKeys.SCRIPT_DEFINITIONS, currentDefs)
}
SCRIPT_DEFINITIONS_CLASSPATH_OPTION -> {
val currentCP = configuration.getList(ScriptingConfigurationKeys.SCRIPT_DEFINITIONS_CLASSPATH).toMutableList()
currentCP.addAll(value.split(File.pathSeparatorChar).map(::File))
configuration.put(ScriptingConfigurationKeys.SCRIPT_DEFINITIONS_CLASSPATH, currentCP)
}
DISABLE_SCRIPT_DEFINITIONS_FROM_CLSSPATH_OPTION -> {
configuration.put(
ScriptingConfigurationKeys.DISABLE_SCRIPT_DEFINITIONS_FROM_CLSSPATH_OPTION,
value.takeUnless { it.isBlank() }?.toBoolean() ?: true
)
}
LEGACY_SCRIPT_RESOLVER_ENVIRONMENT_OPTION -> {
val currentEnv = configuration.getMap(ScriptingConfigurationKeys.LEGACY_SCRIPT_RESOLVER_ENVIRONMENT_OPTION).toMutableMap()
// parses key/value pairs in the form <key>=<value>, where
// <key> - is a single word (\w+ pattern)
// <value> - optionally quoted string with allowed escaped chars (only double-quote and backslash chars are supported)
// TODO: implement generic unescaping
val envParseRe = """(\w+)=(?:"([^"\\]*(\\.[^"\\]*)*)"|([^\s]*))""".toRegex()
val unescapeRe = """\\(["\\])""".toRegex()
for (envParam in value.split(',')) {
val match = envParseRe.matchEntire(envParam)
if (match == null || match.groupValues.size < 4 || match.groupValues[1].isBlank()) {
throw CliOptionProcessingException("Unable to parse script-resolver-environment argument $envParam")
}
currentEnv[match.groupValues[1]] =
match.groupValues.drop(2).firstOrNull { it.isNotEmpty() }?.let { unescapeRe.replace(it, "\$1") }
}
configuration.put(ScriptingConfigurationKeys.LEGACY_SCRIPT_RESOLVER_ENVIRONMENT_OPTION, currentEnv)
}
else -> throw CliOptionProcessingException("Unknown option: ${option.name}")
}
}
@@ -0,0 +1,154 @@
/*
* Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.scripting.compiler.plugin
import com.intellij.mock.MockProject
import org.jetbrains.kotlin.cli.common.CLIConfigurationKeys
import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity
import org.jetbrains.kotlin.cli.common.messages.MessageCollector
import org.jetbrains.kotlin.cli.jvm.config.jvmClasspathRoots
import org.jetbrains.kotlin.compiler.plugin.ComponentRegistrar
import org.jetbrains.kotlin.config.CompilerConfiguration
import org.jetbrains.kotlin.config.JVMConfigurationKeys
import org.jetbrains.kotlin.extensions.CompilerConfigurationExtension
import org.jetbrains.kotlin.script.KotlinScriptDefinitionFromAnnotatedTemplate
import org.jetbrains.kotlin.utils.addToStdlib.firstIsInstanceOrNull
import java.io.File
import java.io.IOException
import java.net.URLClassLoader
import java.util.jar.JarFile
import kotlin.script.experimental.definitions.ScriptDefinitionFromAnnotatedBaseClass
class ScriptingCompilerConfigurationExtension(val project: MockProject) : CompilerConfigurationExtension {
override fun updateConfiguration(configuration: CompilerConfiguration) {
val messageCollector = configuration.get(CLIConfigurationKeys.MESSAGE_COLLECTOR_KEY) ?: MessageCollector.NONE
val explicitScriptDefinitions = configuration.getList(ScriptingConfigurationKeys.SCRIPT_DEFINITIONS)
val scriptDefinitions =
if (configuration.getBoolean(ScriptingConfigurationKeys.DISABLE_SCRIPT_DEFINITIONS_FROM_CLSSPATH_OPTION))
explicitScriptDefinitions
else
explicitScriptDefinitions + discoverScriptTemplatesInClasspath(configuration, messageCollector)
if (scriptDefinitions.isNotEmpty()) {
val projectRoot = project.run { basePath ?: baseDir?.canonicalPath }?.let(::File)
if (projectRoot != null) {
configuration.put(
ScriptingConfigurationKeys.LEGACY_SCRIPT_RESOLVER_ENVIRONMENT_OPTION,
"projectRoot",
projectRoot
)
}
configureScriptDefinitions(
scriptDefinitions,
configuration,
messageCollector,
configuration.getMap(ScriptingConfigurationKeys.LEGACY_SCRIPT_RESOLVER_ENVIRONMENT_OPTION)
)
}
}
}
class ScriptingCompilerConfigurationComponentRegistrar : ComponentRegistrar {
override fun registerProjectComponents(project: MockProject, configuration: CompilerConfiguration) {
CompilerConfigurationExtension.registerExtension(project, ScriptingCompilerConfigurationExtension(project))
}
}
private fun discoverScriptTemplatesInClasspath(configuration: CompilerConfiguration, messageCollector: MessageCollector): Iterable<String> {
val templates = arrayListOf<String>()
val templatesPath = "META-INF/kotlin/script/templates/"
for (dep in configuration.jvmClasspathRoots) {
when {
dep.isFile -> {
// this is the compiler behaviour, so the same logic implemented here
if (dep.extension == "jar") {
try {
with(JarFile(dep)) {
for (template in entries()) {
if (!template.isDirectory && template.name.startsWith(templatesPath)) {
val templateClassName = template.name.removePrefix(templatesPath)
templates.add(templateClassName)
messageCollector.report(
CompilerMessageSeverity.LOGGING,
"Configure scripting: Added template $templateClassName from $dep"
)
}
}
}
} catch (e: IOException) {
messageCollector.report(
CompilerMessageSeverity.WARNING,
"Configure scripting: unable to process classpath entry $dep: $e"
)
}
}
}
dep.isDirectory -> {
val dir = File(dep, templatesPath)
if (dir.isDirectory) {
dir.listFiles().forEach {
templates.add(it.name)
messageCollector.report(
CompilerMessageSeverity.LOGGING,
"Configure scripting: Added template ${it.name} from $dep"
)
}
}
}
else -> messageCollector.report(CompilerMessageSeverity.WARNING, "Configure scripting: Unknown classpath entry $dep")
}
}
return templates
}
fun configureScriptDefinitions(
scriptTemplates: List<String>,
configuration: CompilerConfiguration,
messageCollector: MessageCollector,
scriptResolverEnv: Map<String, Any?>
) {
val classpath = configuration.jvmClasspathRoots
// TODO: consider using escaping to allow kotlin escaped names in class names
if (scriptTemplates.isNotEmpty()) {
val classloader =
URLClassLoader(classpath.map { it.toURI().toURL() }.toTypedArray(), Thread.currentThread().contextClassLoader)
var hasErrors = false
for (template in scriptTemplates) {
try {
val cls = classloader.loadClass(template)
val def =
if (cls.annotations.firstIsInstanceOrNull<kotlin.script.experimental.annotations.KotlinScript>() != null) {
KotlinScriptDefinitionAdapterFromNewAPI(ScriptDefinitionFromAnnotatedBaseClass(cls.kotlin))
} else {
KotlinScriptDefinitionFromAnnotatedTemplate(cls.kotlin, scriptResolverEnv)
}
configuration.add(JVMConfigurationKeys.SCRIPT_DEFINITIONS, def)
messageCollector.report(
CompilerMessageSeverity.INFO,
"Added script definition $template to configuration: name = ${def.name}, " +
"resolver = ${def.dependencyResolver.javaClass.name}"
)
} catch (ex: ClassNotFoundException) {
messageCollector.report(CompilerMessageSeverity.ERROR, "Cannot find script definition template class $template")
hasErrors = true
} catch (ex: Exception) {
messageCollector.report(
CompilerMessageSeverity.ERROR,
"Error processing script definition template $template: ${ex.message}"
)
hasErrors = true
break
}
}
if (hasErrors) {
messageCollector.report(CompilerMessageSeverity.LOGGING, "(Classpath used for templates loading: $classpath)")
return
}
}
}