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 +}