Implement quick non-script detection and lazy script discovery..

so ".kt" and ".java" files are not considered as scripts and quickly
filtered out, and for the other files the the checks are implemented
using sequences, mechanisms provided to supply script definitions
lazily, and script discovery is implemented using this mechanisms.
This commit is contained in:
Ilya Chernikov
2018-04-18 17:23:39 +02:00
parent 969340167f
commit 15f9ad9531
12 changed files with 322 additions and 128 deletions
@@ -14,13 +14,17 @@ 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.KotlinScriptDefinition
import org.jetbrains.kotlin.script.KotlinScriptDefinitionFromAnnotatedTemplate
import org.jetbrains.kotlin.script.ScriptDefinitionsSource
import org.jetbrains.kotlin.script.StandardScriptDefinition
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.coroutines.experimental.buildSequence
import kotlin.script.experimental.annotations.KotlinScript
import kotlin.script.experimental.api.ScriptingEnvironment
import kotlin.script.experimental.api.ScriptingEnvironmentProperties
import kotlin.script.experimental.definitions.ScriptDefinitionFromAnnotatedBaseClass
@@ -32,28 +36,24 @@ class ScriptingCompilerConfigurationExtension(val project: MockProject) : Compil
if (!configuration.getBoolean(ScriptingConfigurationKeys.DISABLE_SCRIPTING_PLUGIN_OPTION)) {
val messageCollector = configuration.get(CLIConfigurationKeys.MESSAGE_COLLECTOR_KEY) ?: MessageCollector.NONE
val projectRoot = project.run { basePath ?: baseDir?.canonicalPath }?.let(::File)
if (projectRoot != null) {
configuration.put(
ScriptingConfigurationKeys.LEGACY_SCRIPT_RESOLVER_ENVIRONMENT_OPTION,
"projectRoot",
projectRoot
)
}
val scriptResolverEnv = configuration.getMap(ScriptingConfigurationKeys.LEGACY_SCRIPT_RESOLVER_ENVIRONMENT_OPTION)
val explicitScriptDefinitions = configuration.getList(ScriptingConfigurationKeys.SCRIPT_DEFINITIONS)
val scriptDefinitions =
if (configuration.getBoolean(ScriptingConfigurationKeys.DISABLE_SCRIPT_DEFINITIONS_FROM_CLASSPATH_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
)
}
if (explicitScriptDefinitions.isNotEmpty()) {
configureScriptDefinitions(
scriptDefinitions,
explicitScriptDefinitions,
configuration,
messageCollector,
configuration.getMap(ScriptingConfigurationKeys.LEGACY_SCRIPT_RESOLVER_ENVIRONMENT_OPTION)
scriptResolverEnv
)
}
// If not disabled explicitly, we should always support at least the standard script definition
@@ -62,6 +62,15 @@ class ScriptingCompilerConfigurationExtension(val project: MockProject) : Compil
) {
configuration.add(JVMConfigurationKeys.SCRIPT_DEFINITIONS, StandardScriptDefinition)
}
configuration.add(
JVMConfigurationKeys.SCRIPT_DEFINITIONS_SOURCES,
ScriptDefinitionsFromClasspathDiscoverySource(
configuration,
scriptResolverEnv,
messageCollector
)
)
}
}
}
@@ -72,54 +81,71 @@ class ScriptingCompilerConfigurationComponentRegistrar : ComponentRegistrar {
}
}
private fun discoverScriptTemplatesInClasspath(configuration: CompilerConfiguration, messageCollector: MessageCollector): Iterable<String> {
val templates = arrayListOf<String>()
class ScriptDefinitionsFromClasspathDiscoverySource(
private val configuration: CompilerConfiguration,
private val scriptResolverEnv: Map<String, Any?>,
private val messageCollector: MessageCollector
) : ScriptDefinitionsSource {
override val definitions: Sequence<KotlinScriptDefinition> = run {
val classpath = configuration.jvmClasspathRoots
// TODO: consider using escaping to allow kotlin escaped names in class names
val classloader =
URLClassLoader(classpath.map { it.toURI().toURL() }.toTypedArray(), Thread.currentThread().contextClassLoader)
discoverScriptTemplatesInClasspath(configuration, messageCollector).mapNotNull {
loadScriptDefinition(classloader, it, scriptResolverEnv, messageCollector)
}
}
}
private fun discoverScriptTemplatesInClasspath(configuration: CompilerConfiguration, messageCollector: MessageCollector): Sequence<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"
)
return buildSequence {
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)
yield(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"
)
}
} 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"
)
dep.isDirectory -> {
val dir = File(dep, templatesPath)
if (dir.isDirectory) {
dir.listFiles().forEach {
yield(it.name)
messageCollector.report(
CompilerMessageSeverity.LOGGING,
"Configure scripting: Added template ${it.name} from $dep"
)
}
}
}
}
else -> {
// assuming that invalid classpath entries will be reported elsewhere anyway, so do not spam user with additional warnings here
messageCollector.report(CompilerMessageSeverity.LOGGING, "Configure scripting: Unknown classpath entry $dep")
else -> {
// assuming that invalid classpath entries will be reported elsewhere anyway, so do not spam user with additional warnings here
messageCollector.report(CompilerMessageSeverity.LOGGING, "Configure scripting: Unknown classpath entry $dep")
}
}
}
}
return templates
}
fun configureScriptDefinitions(
@@ -135,32 +161,10 @@ fun configureScriptDefinitions(
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(ScriptingEnvironment(ScriptingEnvironmentProperties.baseClass to cls.kotlin))
)
} else {
KotlinScriptDefinitionFromAnnotatedTemplate(cls.kotlin, scriptResolverEnv)
}
val def = loadScriptDefinition(classloader, template, scriptResolverEnv, messageCollector)
if (!hasErrors && def == null) hasErrors = true
if (def != null) {
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) {
@@ -169,3 +173,36 @@ fun configureScriptDefinitions(
}
}
}
private fun loadScriptDefinition(
classloader: URLClassLoader,
template: String,
scriptResolverEnv: Map<String, Any?>,
messageCollector: MessageCollector
): KotlinScriptDefinition? {
try {
val cls = classloader.loadClass(template)
val def =
if (cls.annotations.firstIsInstanceOrNull<KotlinScript>() != null) {
KotlinScriptDefinitionAdapterFromNewAPI(
ScriptDefinitionFromAnnotatedBaseClass(ScriptingEnvironment(ScriptingEnvironmentProperties.baseClass to cls.kotlin))
)
} else {
KotlinScriptDefinitionFromAnnotatedTemplate(cls.kotlin, scriptResolverEnv)
}
messageCollector.report(
CompilerMessageSeverity.INFO,
"Added script definition $template to configuration: name = ${def.name}, " +
"resolver = ${def.dependencyResolver.javaClass.name}"
)
return def
} catch (ex: ClassNotFoundException) {
messageCollector.report(CompilerMessageSeverity.ERROR, "Cannot find script definition template class $template")
} catch (ex: Exception) {
messageCollector.report(
CompilerMessageSeverity.ERROR,
"Error processing script definition template $template: ${ex.message}"
)
}
return null
}