From 15f9ad9531a0818214e79e3f08ae2701f04f8017 Mon Sep 17 00:00:00 2001 From: Ilya Chernikov Date: Wed, 18 Apr 2018 17:23:39 +0200 Subject: [PATCH] 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. --- .../script/CliScriptDefinitionProvider.kt | 28 +-- .../script/CliScriptDependenciesProvider.kt | 6 +- .../cli/jvm/compiler/KotlinCoreEnvironment.kt | 3 + .../jvm/compiler/KotlinCoreEnvironment.kt.172 | 3 + .../jvm/compiler/KotlinCoreEnvironment.kt.173 | 3 + .../kotlin/config/JVMConfigurationKeys.java | 4 + .../script/KotlinScriptDefinitionProvider.kt | 75 ++++++- .../kotlin/script/ScriptDefinitionsSource.kt | 11 + .../kotlin/scripts/ScriptProviderTest.kt | 80 +++++++ .../core/script/ScriptDefinitionsManager.kt | 41 +--- .../core/script/ScriptDependenciesManager.kt | 1 + ...ScriptingCompilerConfigurationExtension.kt | 195 +++++++++++------- 12 files changed, 322 insertions(+), 128 deletions(-) create mode 100644 compiler/psi/src/org/jetbrains/kotlin/script/ScriptDefinitionsSource.kt create mode 100644 compiler/tests/org/jetbrains/kotlin/scripts/ScriptProviderTest.kt diff --git a/compiler/cli/src/org/jetbrains/kotlin/cli/common/script/CliScriptDefinitionProvider.kt b/compiler/cli/src/org/jetbrains/kotlin/cli/common/script/CliScriptDefinitionProvider.kt index 1998fce201c..9c77c96af4b 100644 --- a/compiler/cli/src/org/jetbrains/kotlin/cli/common/script/CliScriptDefinitionProvider.kt +++ b/compiler/cli/src/org/jetbrains/kotlin/cli/common/script/CliScriptDefinitionProvider.kt @@ -16,24 +16,15 @@ package org.jetbrains.kotlin.cli.common.script -import org.jetbrains.kotlin.script.KotlinScriptDefinition -import org.jetbrains.kotlin.script.ScriptDefinitionProvider -import org.jetbrains.kotlin.script.StandardScriptDefinition -import java.util.concurrent.locks.ReentrantReadWriteLock -import kotlin.concurrent.read +import org.jetbrains.kotlin.script.* import kotlin.concurrent.write -class CliScriptDefinitionProvider : ScriptDefinitionProvider { +class CliScriptDefinitionProvider : LazyScriptDefinitionProvider() { + private val definitionsFromSources: MutableList> = arrayListOf() private val definitions: MutableList = arrayListOf(StandardScriptDefinition) - private val lock = ReentrantReadWriteLock() - override fun findScriptDefinition(fileName: String) = lock.read { - definitions.firstOrNull { it.isScript(fileName) } - } - - override fun isScript(fileName: String) = lock.read { - definitions.any { it.isScript(fileName) } - } + override val currentDefinitions: Sequence = + definitionsFromSources.asSequence().flatMap { it } + definitions.asSequence() fun setScriptDefinitions(newDefinitions: List) { lock.write { @@ -41,4 +32,13 @@ class CliScriptDefinitionProvider : ScriptDefinitionProvider { definitions.addAll(newDefinitions) } } + + fun setScriptDefinitionsSources(newSources: List) { + lock.write { + definitionsFromSources.clear() + for (it in newSources) { + definitionsFromSources.add(it.definitions.constrainOnce()) + } + } + } } \ No newline at end of file diff --git a/compiler/cli/src/org/jetbrains/kotlin/cli/common/script/CliScriptDependenciesProvider.kt b/compiler/cli/src/org/jetbrains/kotlin/cli/common/script/CliScriptDependenciesProvider.kt index 640dff2967b..e8bf5b5fd92 100644 --- a/compiler/cli/src/org/jetbrains/kotlin/cli/common/script/CliScriptDependenciesProvider.kt +++ b/compiler/cli/src/org/jetbrains/kotlin/cli/common/script/CliScriptDependenciesProvider.kt @@ -19,10 +19,8 @@ package org.jetbrains.kotlin.cli.common.script import com.intellij.openapi.diagnostic.Logger import com.intellij.openapi.project.Project import com.intellij.openapi.vfs.VirtualFile -import org.jetbrains.kotlin.script.ScriptContentLoader -import org.jetbrains.kotlin.script.ScriptDefinitionProvider -import org.jetbrains.kotlin.script.ScriptDependenciesProvider -import org.jetbrains.kotlin.script.adjustByDefinition +import org.jetbrains.kotlin.script.findScriptDefinition +import org.jetbrains.kotlin.script.* import java.io.File import java.util.concurrent.locks.ReentrantReadWriteLock import kotlin.concurrent.read diff --git a/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/compiler/KotlinCoreEnvironment.kt b/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/compiler/KotlinCoreEnvironment.kt index 4120dfd2bb3..7615e2cd8f6 100644 --- a/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/compiler/KotlinCoreEnvironment.kt +++ b/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/compiler/KotlinCoreEnvironment.kt @@ -209,6 +209,9 @@ class KotlinCoreEnvironment private constructor( val scriptDefinitionProvider = ScriptDefinitionProvider.getInstance(project) as? CliScriptDefinitionProvider if (scriptDefinitionProvider != null) { + scriptDefinitionProvider.setScriptDefinitionsSources( + configuration.getList(JVMConfigurationKeys.SCRIPT_DEFINITIONS_SOURCES) + ) scriptDefinitionProvider.setScriptDefinitions( configuration.getList(JVMConfigurationKeys.SCRIPT_DEFINITIONS)) diff --git a/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/compiler/KotlinCoreEnvironment.kt.172 b/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/compiler/KotlinCoreEnvironment.kt.172 index 1f72a64f745..219d2ea5dfc 100644 --- a/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/compiler/KotlinCoreEnvironment.kt.172 +++ b/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/compiler/KotlinCoreEnvironment.kt.172 @@ -210,6 +210,9 @@ class KotlinCoreEnvironment private constructor( val scriptDefinitionProvider = ScriptDefinitionProvider.getInstance(project) as? CliScriptDefinitionProvider if (scriptDefinitionProvider != null) { + scriptDefinitionProvider.setScriptDefinitionsSources( + configuration.getList(JVMConfigurationKeys.SCRIPT_DEFINITIONS_SOURCES) + ) scriptDefinitionProvider.setScriptDefinitions( configuration.getList(JVMConfigurationKeys.SCRIPT_DEFINITIONS)) diff --git a/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/compiler/KotlinCoreEnvironment.kt.173 b/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/compiler/KotlinCoreEnvironment.kt.173 index ae40d3dffc5..43ec8562d30 100644 --- a/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/compiler/KotlinCoreEnvironment.kt.173 +++ b/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/compiler/KotlinCoreEnvironment.kt.173 @@ -212,6 +212,9 @@ class KotlinCoreEnvironment private constructor( val scriptDefinitionProvider = ScriptDefinitionProvider.getInstance(project) as? CliScriptDefinitionProvider if (scriptDefinitionProvider != null) { + scriptDefinitionProvider.setScriptDefinitionsSources( + configuration.getList(JVMConfigurationKeys.SCRIPT_DEFINITIONS_SOURCES) + ) scriptDefinitionProvider.setScriptDefinitions( configuration.getList(JVMConfigurationKeys.SCRIPT_DEFINITIONS)) diff --git a/compiler/frontend.java/src/org/jetbrains/kotlin/config/JVMConfigurationKeys.java b/compiler/frontend.java/src/org/jetbrains/kotlin/config/JVMConfigurationKeys.java index 7ff323a2059..43b16583d2e 100644 --- a/compiler/frontend.java/src/org/jetbrains/kotlin/config/JVMConfigurationKeys.java +++ b/compiler/frontend.java/src/org/jetbrains/kotlin/config/JVMConfigurationKeys.java @@ -21,6 +21,7 @@ import org.jetbrains.kotlin.load.java.JavaClassesTracker; import org.jetbrains.kotlin.load.kotlin.incremental.components.IncrementalCompilationComponents; import org.jetbrains.kotlin.modules.Module; import org.jetbrains.kotlin.script.KotlinScriptDefinition; +import org.jetbrains.kotlin.script.ScriptDefinitionsSource; import java.io.File; import java.util.List; @@ -49,6 +50,9 @@ public class JVMConfigurationKeys { public static final CompilerConfigurationKey> SCRIPT_DEFINITIONS = CompilerConfigurationKey.create("script definitions"); + public static final CompilerConfigurationKey> SCRIPT_DEFINITIONS_SOURCES = + CompilerConfigurationKey.create("script definitions sources"); + public static final CompilerConfigurationKey DISABLE_STANDARD_SCRIPT_DEFINITION = CompilerConfigurationKey.create("Disable standard kotlin script support"); diff --git a/compiler/psi/src/org/jetbrains/kotlin/script/KotlinScriptDefinitionProvider.kt b/compiler/psi/src/org/jetbrains/kotlin/script/KotlinScriptDefinitionProvider.kt index e8b7e5f07ff..d457fd16eb7 100644 --- a/compiler/psi/src/org/jetbrains/kotlin/script/KotlinScriptDefinitionProvider.kt +++ b/compiler/psi/src/org/jetbrains/kotlin/script/KotlinScriptDefinitionProvider.kt @@ -20,11 +20,13 @@ import com.intellij.openapi.components.ServiceManager import com.intellij.openapi.project.Project import com.intellij.openapi.vfs.VirtualFile import com.intellij.psi.PsiFile +import java.util.concurrent.locks.ReentrantReadWriteLock +import kotlin.concurrent.read +import kotlin.concurrent.write interface ScriptDefinitionProvider { fun findScriptDefinition(fileName: String): KotlinScriptDefinition? fun isScript(fileName: String): Boolean - fun findScriptDefinition(file: VirtualFile): KotlinScriptDefinition? = findScriptDefinition(file.name) companion object { fun getInstance(project: Project): ScriptDefinitionProvider = @@ -32,8 +34,79 @@ interface ScriptDefinitionProvider { } } +fun ScriptDefinitionProvider.findScriptDefinition(file: VirtualFile): KotlinScriptDefinition? = findScriptDefinition(file.name) + fun getScriptDefinition(file: VirtualFile, project: Project): KotlinScriptDefinition? = ScriptDefinitionProvider.getInstance(project).findScriptDefinition(file) fun getScriptDefinition(psiFile: PsiFile): KotlinScriptDefinition? = ScriptDefinitionProvider.getInstance(psiFile.project).findScriptDefinition(psiFile.name) + +abstract class LazyScriptDefinitionProvider : ScriptDefinitionProvider { + + protected val lock = ReentrantReadWriteLock() + + protected abstract val currentDefinitions: Sequence + + private var _cachedDefinitions: Sequence? = null + private val cachedDefinitions: Sequence + get() { + // assuming it is always called under read lock + assert(lock.readLockCount > 0) + if (_cachedDefinitions == null) lock.write { + _cachedDefinitions = CashingSequence(currentDefinitions.constrainOnce()) + } + return _cachedDefinitions!! + } + + protected fun clearCache() { + lock.write { + _cachedDefinitions = null + } + } + + protected open fun nonScriptFileName(fileName: String) = nonScriptFilenameSuffixes.any { + fileName.endsWith( it, ignoreCase = true) + } + + override fun findScriptDefinition(fileName: String): KotlinScriptDefinition? = + if (nonScriptFileName(fileName)) null + else lock.read { + cachedDefinitions.firstOrNull { it.isScript(fileName) } + } + + override fun isScript(fileName: String) = + if (nonScriptFileName(fileName)) false + else lock.read { + cachedDefinitions.any { it.isScript(fileName) } + } + + companion object { + // TODO: find a common place for storing kotlin-related extensions and reuse values from it everywhere + protected val nonScriptFilenameSuffixes = arrayOf(".kt", ".java") + } +} + +private class CashingSequence(from: Sequence) : Sequence { + + private val lock = ReentrantReadWriteLock() + private val sequenceIterator = from.iterator() + private val cache = arrayListOf() + + private inner class CashingIterator : Iterator { + + private val cacheIterator: Iterator = cache.iterator() + private var cacheRunOut = !cacheIterator.hasNext() + + private fun cacheHasNext() = !cacheRunOut && (cacheIterator.hasNext().also { if (!it) cacheRunOut = true }) + + override fun hasNext(): Boolean = lock.read { cacheHasNext() || sequenceIterator.hasNext() } + + override fun next(): T = lock.read { + if (cacheHasNext()) cacheIterator.next() + else sequenceIterator.next().also { lock.write { cache.add(it) } } + } + } + + override fun iterator(): Iterator = CashingIterator() +} diff --git a/compiler/psi/src/org/jetbrains/kotlin/script/ScriptDefinitionsSource.kt b/compiler/psi/src/org/jetbrains/kotlin/script/ScriptDefinitionsSource.kt new file mode 100644 index 00000000000..251cfb2c6de --- /dev/null +++ b/compiler/psi/src/org/jetbrains/kotlin/script/ScriptDefinitionsSource.kt @@ -0,0 +1,11 @@ +/* + * Copyright 2010-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.script + +interface ScriptDefinitionsSource { + + val definitions: Sequence +} \ No newline at end of file diff --git a/compiler/tests/org/jetbrains/kotlin/scripts/ScriptProviderTest.kt b/compiler/tests/org/jetbrains/kotlin/scripts/ScriptProviderTest.kt new file mode 100644 index 00000000000..13d8438835a --- /dev/null +++ b/compiler/tests/org/jetbrains/kotlin/scripts/ScriptProviderTest.kt @@ -0,0 +1,80 @@ +/* + * Copyright 2010-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.scripts + +import org.jetbrains.kotlin.cli.common.script.CliScriptDefinitionProvider +import org.jetbrains.kotlin.script.KotlinScriptDefinition +import org.jetbrains.kotlin.script.ScriptDefinitionsSource +import org.jetbrains.kotlin.test.testFramework.KtUsefulTestCase +import org.junit.Assert +import org.junit.Test +import java.util.concurrent.atomic.AtomicInteger +import kotlin.coroutines.experimental.buildSequence +import kotlin.script.templates.standard.ScriptTemplateWithArgs + +class ScriptProviderTest : KtUsefulTestCase() { + + @Test + fun testLazyScriptDefinitionsProvider() { + + val genDefCounter = AtomicInteger() + val standardDef = FakeScriptDefinition() + val shadedDef = FakeScriptDefinition(".x.kts") + val provider = CliScriptDefinitionProvider().apply { + setScriptDefinitions(listOf(shadedDef, standardDef)) + setScriptDefinitionsSources(listOf(TestScriptDefinitionSource(genDefCounter, ".y.kts", ".x.kts"))) + } + + Assert.assertEquals(0, genDefCounter.get()) + + provider.isScript("a.kt").let { + Assert.assertFalse(it) + Assert.assertEquals(0, genDefCounter.get()) + } + + provider.isScript("a.y.kts").let { + Assert.assertTrue(it) + Assert.assertEquals(1, genDefCounter.get()) + } + + provider.isScript("a.x.kts").let { + Assert.assertTrue(it) + Assert.assertEquals(2, genDefCounter.get()) + Assert.assertEquals(0, shadedDef.matchCounter.get()) + } + + provider.isScript("a.z.kts").let { + Assert.assertTrue(it) + Assert.assertEquals(2, genDefCounter.get()) + Assert.assertEquals(1, standardDef.matchCounter.get()) + } + + provider.isScript("a.ktx").let { + Assert.assertFalse(it) + Assert.assertEquals(2, genDefCounter.get()) + } + } +} + +private class FakeScriptDefinition(val suffix: String = ".kts") : KotlinScriptDefinition(ScriptTemplateWithArgs::class) { + val matchCounter = AtomicInteger() + override fun isScript(fileName: String): Boolean = fileName.endsWith(suffix).also { + if (it) matchCounter.incrementAndGet() + } +} + +private class TestScriptDefinitionSource(val counter: AtomicInteger, val defGens: Iterable<() -> FakeScriptDefinition>) : + ScriptDefinitionsSource +{ + constructor(counter: AtomicInteger, vararg suffixes: String) : this(counter, suffixes.map { { FakeScriptDefinition(it) } }) + + override val definitions: Sequence = buildSequence { + for (gen in defGens) { + counter.incrementAndGet() + yield(gen()) + } + } +} \ No newline at end of file diff --git a/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/ScriptDefinitionsManager.kt b/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/ScriptDefinitionsManager.kt index b409caebf2d..60d0f244ba4 100644 --- a/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/ScriptDefinitionsManager.kt +++ b/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/ScriptDefinitionsManager.kt @@ -29,10 +29,7 @@ import com.intellij.openapi.vfs.VfsUtil import com.intellij.openapi.vfs.VirtualFile import org.jetbrains.kotlin.idea.caches.project.SdkInfo import org.jetbrains.kotlin.idea.caches.project.getScriptRelatedModuleInfo -import org.jetbrains.kotlin.script.KotlinScriptDefinition -import org.jetbrains.kotlin.script.KotlinScriptDefinitionFromAnnotatedTemplate -import org.jetbrains.kotlin.script.ScriptDefinitionProvider -import org.jetbrains.kotlin.script.ScriptTemplatesProvider +import org.jetbrains.kotlin.script.* import org.jetbrains.kotlin.scripting.compiler.plugin.KotlinScriptDefinitionAdapterFromNewAPI import org.jetbrains.kotlin.utils.PathUtil import org.jetbrains.kotlin.utils.addToStdlib.firstIsInstanceOrNull @@ -40,8 +37,6 @@ import org.jetbrains.kotlin.utils.addToStdlib.flattenTo import java.io.File import java.lang.reflect.InvocationTargetException import java.net.URLClassLoader -import java.util.concurrent.locks.ReentrantReadWriteLock -import kotlin.concurrent.read import kotlin.concurrent.write import kotlin.script.dependencies.Environment import kotlin.script.dependencies.ScriptContents @@ -53,14 +48,12 @@ import kotlin.script.experimental.dependencies.ScriptDependencies import kotlin.script.experimental.dependencies.asSuccess import kotlin.script.templates.standard.ScriptTemplateWithArgs -class ScriptDefinitionsManager(private val project: Project) : ScriptDefinitionProvider { - private val lock = ReentrantReadWriteLock() +class ScriptDefinitionsManager(private val project: Project) : LazyScriptDefinitionProvider() { private var definitionsByContributor = mutableMapOf>() - private var definitions: List = emptyList() + private var definitions: Sequence? = null fun reloadDefinitionsBy(contributor: ScriptDefinitionContributor) = lock.write { - val notLoadedYet = definitions.isEmpty() - if (notLoadedYet) return + if (definitions == null) return // not loaded yet if (contributor !in definitionsByContributor) error("Unknown contributor: ${contributor.id}") @@ -70,32 +63,19 @@ class ScriptDefinitionsManager(private val project: Project) : ScriptDefinitionP } fun getDefinitionsBy(contributor: ScriptDefinitionContributor): List = lock.write { - val notLoadedYet = definitions.isEmpty() - if (notLoadedYet) return emptyList() + if (definitions == null) return emptyList() // not loaded yet if (contributor !in definitionsByContributor) error("Unknown contributor: ${contributor.id}") return definitionsByContributor[contributor] ?: emptyList() } - private fun currentDefinitions(): List { - val hasDefinitions = definitions.isNotEmpty() - when { - hasDefinitions -> return definitions - else -> { + override val currentDefinitions: Sequence + get() = + definitions ?: kotlin.run { reloadScriptDefinitions() - return definitions + definitions!! } - } - } - - override fun findScriptDefinition(fileName: String): KotlinScriptDefinition? = lock.read { - currentDefinitions().firstOrNull { it.isScript(fileName) } - } - - override fun isScript(fileName: String) = lock.read { - currentDefinitions().any { it.isScript(fileName) } - } private fun getContributors(): List { @Suppress("DEPRECATION") @@ -115,7 +95,8 @@ class ScriptDefinitionsManager(private val project: Project) : ScriptDefinitionP } private fun updateDefinitions() { - definitions = definitionsByContributor.values.flattenTo(mutableListOf()) + definitions = definitionsByContributor.values.flattenTo(mutableListOf()).asSequence() + clearCache() // TODO: clear by script type/definition ServiceManager.getService(project, ScriptDependenciesUpdater::class.java).clear() } diff --git a/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/ScriptDependenciesManager.kt b/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/ScriptDependenciesManager.kt index 754391677e4..5bff56c9fca 100644 --- a/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/ScriptDependenciesManager.kt +++ b/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/ScriptDependenciesManager.kt @@ -25,6 +25,7 @@ import com.intellij.util.io.URLUtil import org.jetbrains.annotations.TestOnly import org.jetbrains.kotlin.script.ScriptDefinitionProvider import org.jetbrains.kotlin.script.ScriptDependenciesProvider +import org.jetbrains.kotlin.script.findScriptDefinition import java.io.File import kotlin.script.experimental.dependencies.ScriptDependencies diff --git a/plugins/scripting/scripting-cli/src/org/jetbrains/kotlin/scripting/compiler/plugin/ScriptingCompilerConfigurationExtension.kt b/plugins/scripting/scripting-cli/src/org/jetbrains/kotlin/scripting/compiler/plugin/ScriptingCompilerConfigurationExtension.kt index 7421d70d98b..bbf5536f02d 100644 --- a/plugins/scripting/scripting-cli/src/org/jetbrains/kotlin/scripting/compiler/plugin/ScriptingCompilerConfigurationExtension.kt +++ b/plugins/scripting/scripting-cli/src/org/jetbrains/kotlin/scripting/compiler/plugin/ScriptingCompilerConfigurationExtension.kt @@ -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 { - val templates = arrayListOf() +class ScriptDefinitionsFromClasspathDiscoverySource( + private val configuration: CompilerConfiguration, + private val scriptResolverEnv: Map, + private val messageCollector: MessageCollector +) : ScriptDefinitionsSource { + + override val definitions: Sequence = 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 { 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() != 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, + messageCollector: MessageCollector +): KotlinScriptDefinition? { + try { + val cls = classloader.loadClass(template) + val def = + if (cls.annotations.firstIsInstanceOrNull() != 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 +}