Add new experimental scripting API
This commit is contained in:
+27
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/*
|
||||
* 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 kotlin.script.experimental.annotations
|
||||
|
||||
import kotlin.reflect.KClass
|
||||
import kotlin.script.experimental.api.ScriptConfigurator
|
||||
import kotlin.script.experimental.api.ScriptRunner
|
||||
import kotlin.script.experimental.api.ScriptSelector
|
||||
import kotlin.script.experimental.basic.DefaultScriptSelector
|
||||
import kotlin.script.experimental.basic.DummyRunner
|
||||
import kotlin.script.experimental.basic.PassThroughConfigurator
|
||||
|
||||
@Target(AnnotationTarget.CLASS)
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
annotation class KotlinScript(
|
||||
val selector: KClass<out ScriptSelector> = DefaultScriptSelector::class,
|
||||
val configurator: KClass<out ScriptConfigurator> = PassThroughConfigurator::class,
|
||||
val runner: KClass<out ScriptRunner<*>> = DummyRunner::class
|
||||
)
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* 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 kotlin.script.experimental.api
|
||||
|
||||
interface ScriptCompiler {
|
||||
|
||||
suspend fun compile(
|
||||
configuration: ScriptCompileConfiguration,
|
||||
configurator: ScriptConfigurator? = null
|
||||
): ResultWithDiagnostics<CompiledScript<*>>
|
||||
}
|
||||
|
||||
|
||||
interface CompiledScript<out ScriptBase : Any> {
|
||||
|
||||
val configuration: ScriptCompileConfiguration
|
||||
|
||||
suspend fun instantiate(scriptEvaluationEnvironment: ScriptEvaluationEnvironment): ResultWithDiagnostics<ScriptBase>
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
@file:Suppress("unused")
|
||||
|
||||
package kotlin.script.experimental.api
|
||||
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
object ScriptCompileConfigurationParams {
|
||||
|
||||
val scriptSourceFragments by typedKey<ScriptSourceFragments>()
|
||||
|
||||
val scriptSignature by typedKey<ScriptSignature>()
|
||||
|
||||
val importedPackages by typedKey<Iterable<String>>()
|
||||
|
||||
val restrictions by typedKey<ResolvingRestrictions>()
|
||||
|
||||
val importedScripts by typedKey<Iterable<ScriptSource>>()
|
||||
|
||||
val dependencies by typedKey<Iterable<ScriptDependency>>()
|
||||
|
||||
val compilerOptions by typedKey<Iterable<String>>() // Q: CommonCompilerOptions instead?
|
||||
|
||||
val updateConfigurationOnAnnotations by typedKey<Iterable<KClass<out Annotation>>>()
|
||||
|
||||
val updateConfigurationOnSections by typedKey<Iterable<String>>()
|
||||
}
|
||||
|
||||
typealias ScriptCompileConfiguration = HeterogeneousMap
|
||||
|
||||
fun ScriptSource.toScriptCompileConfiguration(vararg pairs: Pair<TypedKey<*>, Any?>) =
|
||||
ScriptCompileConfiguration(ScriptCompileConfigurationParams.scriptSourceFragments to ScriptSourceFragments(this, null), *pairs)
|
||||
|
||||
object ProcessedScriptDataParams {
|
||||
val annotations by typedKey<Iterable<Annotation>>()
|
||||
|
||||
val fragments by typedKey<Iterable<ScriptSourceNamedFragment>>()
|
||||
}
|
||||
|
||||
typealias ProcessedScriptData = HeterogeneousMap
|
||||
|
||||
|
||||
interface ScriptConfigurator {
|
||||
|
||||
// with null scriptSource should return a generic configuration for the script type
|
||||
suspend fun baseConfiguration(scriptSource: ScriptSource?) : ResultWithDiagnostics<ScriptCompileConfiguration>
|
||||
|
||||
suspend fun refineConfiguration(
|
||||
configuration: ScriptCompileConfiguration,
|
||||
processedScriptData: ProcessedScriptData = ProcessedScriptData()
|
||||
): ResultWithDiagnostics<ScriptCompileConfiguration>
|
||||
}
|
||||
|
||||
fun ScriptSource?.toConfigEntry(): Pair<TypedKey<*>, Any?> =
|
||||
ScriptCompileConfigurationParams.scriptSourceFragments to this?.let { ScriptSourceFragments(this, null) }
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
@file:Suppress("unused")
|
||||
|
||||
package kotlin.script.experimental.api
|
||||
|
||||
import java.net.URL
|
||||
import kotlin.reflect.KClass
|
||||
import kotlin.reflect.KType
|
||||
|
||||
interface ScriptSource {
|
||||
val location: URL?
|
||||
val text: String?
|
||||
|
||||
data class Position(val line: Int, val col: Int, val absolutePos: Int? = null)
|
||||
data class Range(val start: Position, val end: Position)
|
||||
data class Location(val start: Position, val end: Position? = null)
|
||||
}
|
||||
|
||||
data class ScriptSourceNamedFragment(val name: String?, val range: ScriptSource.Range)
|
||||
|
||||
open class ScriptSourceFragments(
|
||||
val originalSource: ScriptSource,
|
||||
val fragments: List<ScriptSourceNamedFragment>?)
|
||||
|
||||
open class ProvidedDeclarations(
|
||||
val implicitReceivers: List<KType> = emptyList(), // previous scripts, etc.
|
||||
val contextVariables: Map<String, KType> = emptyMap() // external variables
|
||||
// Q: do we need context constants and/or types here, e.g.
|
||||
// val contextConstants: Map<String, Any?> // or with KType as well
|
||||
// val contextTypes: List<KType> // additional (to the classpath) types provided by the environment
|
||||
// alternatively:
|
||||
// val contextDeclarations: List<Tuple<DeclarationKind, String?, KType, Any?> // kind, name, type, value
|
||||
// OR: it should be a HeterogeneousMap too
|
||||
) {
|
||||
object Empty : ProvidedDeclarations()
|
||||
}
|
||||
|
||||
open class ScriptSignature(
|
||||
val scriptBase: KClass<*>,
|
||||
val providedDeclarations: ProvidedDeclarations
|
||||
)
|
||||
|
||||
open class ResolvingRestrictions {
|
||||
data class Rule(
|
||||
val allow: Boolean,
|
||||
val pattern: String // FQN wildcard
|
||||
)
|
||||
|
||||
val rules: Iterable<Rule> = arrayListOf()
|
||||
}
|
||||
|
||||
interface ScriptDependency {
|
||||
// Q: anything generic here?
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
@file:Suppress("unused")
|
||||
|
||||
package kotlin.script.experimental.api
|
||||
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
interface ScriptDefinition {
|
||||
val baseClass: KClass<*>
|
||||
val selector: ScriptSelector
|
||||
val configurator: ScriptConfigurator
|
||||
val runner: ScriptRunner<*>?
|
||||
}
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
@file:Suppress("unused")
|
||||
|
||||
package kotlin.script.experimental.api
|
||||
|
||||
data class ScriptDiagnostic(
|
||||
val message: String,
|
||||
val severity: Severity = Severity.ERROR,
|
||||
val location: ScriptSource.Location? = null,
|
||||
val exception: Throwable? = null
|
||||
) {
|
||||
enum class Severity { ERROR, WARNING, INFO, DEBUG }
|
||||
}
|
||||
|
||||
sealed class ResultWithDiagnostics<out R> {
|
||||
abstract val reports: List<ScriptDiagnostic>
|
||||
|
||||
data class Success<out R>(
|
||||
val value: R,
|
||||
override val reports: List<ScriptDiagnostic> = listOf()
|
||||
) : ResultWithDiagnostics<R>()
|
||||
|
||||
data class Failure(
|
||||
override val reports: List<ScriptDiagnostic>
|
||||
) : ResultWithDiagnostics<Nothing>() {
|
||||
constructor(vararg reports: ScriptDiagnostic) : this(reports.asList())
|
||||
}
|
||||
}
|
||||
|
||||
operator fun <R> List<ScriptDiagnostic>.plus(res: ResultWithDiagnostics<R>): ResultWithDiagnostics<R> = when (res) {
|
||||
is ResultWithDiagnostics.Success -> ResultWithDiagnostics.Success(res.value, this + res.reports)
|
||||
is ResultWithDiagnostics.Failure -> ResultWithDiagnostics.Failure(this + res.reports)
|
||||
}
|
||||
|
||||
fun <R : Any> R.asSuccess(reports: List<ScriptDiagnostic> = listOf()): ResultWithDiagnostics.Success<R> =
|
||||
ResultWithDiagnostics.Success(this, reports)
|
||||
|
||||
fun Throwable.asDiagnostics(customMessage: String? = null, location: ScriptSource.Location? = null): ScriptDiagnostic =
|
||||
ScriptDiagnostic(customMessage ?: message ?: "$this", ScriptDiagnostic.Severity.ERROR, location, this)
|
||||
|
||||
fun String.asErrorDiagnostics(location: ScriptSource.Location? = null): ScriptDiagnostic =
|
||||
ScriptDiagnostic(this, ScriptDiagnostic.Severity.ERROR, location)
|
||||
|
||||
fun<R> ResultWithDiagnostics<R>.resultOrNull(): R? = when (this) {
|
||||
is ResultWithDiagnostics.Success<R> -> value
|
||||
else -> null
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
@file:Suppress("unused")
|
||||
|
||||
package kotlin.script.experimental.api
|
||||
|
||||
object ScriptEvaluationEnvironmentParams {
|
||||
val implicitReceivers by typedKey<List<Any>>()
|
||||
val contextVariables by typedKey<Map<String, Any?>>() // external variables
|
||||
val constructorArgs by typedKey<List<Any?>>()
|
||||
val runArgs by typedKey<List<Any?>>()
|
||||
}
|
||||
|
||||
typealias ScriptEvaluationEnvironment = HeterogeneousMap
|
||||
|
||||
data class EvaluationResult(val returnValue: Any?, val environment: ScriptEvaluationEnvironment)
|
||||
|
||||
// NOTE: name inconsistency: run vs evaluate
|
||||
interface ScriptRunner<in ScriptBase : Any> {
|
||||
|
||||
suspend fun run(
|
||||
compiledScript: CompiledScript<ScriptBase>,
|
||||
scriptEvaluationEnvironment: ScriptEvaluationEnvironment
|
||||
): ResultWithDiagnostics<EvaluationResult>
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
@file:Suppress("unused")
|
||||
|
||||
package kotlin.script.experimental.api
|
||||
|
||||
interface ScriptSelector {
|
||||
|
||||
val name: String
|
||||
|
||||
val fileExtension: String // for preliminary selection by file type, e.g. in ide
|
||||
|
||||
fun makeScriptName(scriptFileName: String?): String
|
||||
|
||||
fun isKnownScript(script: ScriptSource): Boolean
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* 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 kotlin.script.experimental.api
|
||||
|
||||
import kotlin.reflect.KProperty
|
||||
|
||||
data class TypedKey<T>(val name: String)
|
||||
|
||||
class TypedKeyDelegate<T> {
|
||||
operator fun getValue(thisRef: Any?, property: KProperty<*>): TypedKey<T> = TypedKey(property.name)
|
||||
}
|
||||
|
||||
fun <T> typedKey() = TypedKeyDelegate<T>()
|
||||
|
||||
class HeterogeneousMap(val data: Map<TypedKey<*>, Any?> = hashMapOf()) {
|
||||
constructor(vararg pairs: Pair<TypedKey<*>, Any?>) : this(hashMapOf(*pairs))
|
||||
}
|
||||
|
||||
fun HeterogeneousMap.cloneWith(vararg pairs: Pair<TypedKey<*>, Any?>) = HeterogeneousMap(HashMap(data).apply { putAll(pairs) })
|
||||
|
||||
operator fun <T> HeterogeneousMap.get(key: TypedKey<T>): T =
|
||||
if (data.containsKey(key)) data[key] as T
|
||||
else throw IllegalArgumentException("Unknown key $key")
|
||||
|
||||
fun <T> HeterogeneousMap.getOrNull(key: TypedKey<T>): T? = data[key] as T?
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* 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 kotlin.script.experimental.basic
|
||||
|
||||
import kotlin.reflect.KClass
|
||||
import kotlin.reflect.full.findAnnotation
|
||||
import kotlin.script.experimental.api.*
|
||||
|
||||
|
||||
@Target(AnnotationTarget.CLASS)
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
annotation class KotlinScriptFileExtension(val extension: String)
|
||||
|
||||
open class DefaultScriptSelector(val baseClass: KClass<Any>? = null) : ScriptSelector {
|
||||
|
||||
override val name: String = "Kotlin script"
|
||||
|
||||
override val fileExtension: String = baseClass?.findAnnotation<KotlinScriptFileExtension>()?.extension ?: "kts"
|
||||
|
||||
override fun makeScriptName(scriptFileName: String?): String = scriptFileName?.removeSuffix(".$fileExtension") ?: "Kotlin script"
|
||||
|
||||
override fun isKnownScript(script: ScriptSource): Boolean =
|
||||
script.location?.file?.endsWith(fileExtension) ?: true
|
||||
}
|
||||
|
||||
class PassThroughConfigurator(val baseClass: KClass<Any>? = null) : ScriptConfigurator {
|
||||
|
||||
override suspend fun baseConfiguration(scriptSource: ScriptSource?): ResultWithDiagnostics<ScriptCompileConfiguration> =
|
||||
(when (scriptSource) {
|
||||
null -> ScriptCompileConfiguration()
|
||||
else -> ScriptCompileConfiguration(scriptSource.toConfigEntry())
|
||||
}).asSuccess()
|
||||
|
||||
override suspend fun refineConfiguration(
|
||||
configuration: ScriptCompileConfiguration,
|
||||
processedScriptData: ProcessedScriptData
|
||||
): ResultWithDiagnostics<ScriptCompileConfiguration> =
|
||||
configuration.asSuccess()
|
||||
}
|
||||
|
||||
class DummyRunner<ScriptBase : Any>(val baseClass: KClass<ScriptBase>? = null) : ScriptRunner<ScriptBase> {
|
||||
override suspend fun run(
|
||||
compiledScript: CompiledScript<ScriptBase>,
|
||||
scriptEvaluationEnvironment: ScriptEvaluationEnvironment
|
||||
): ResultWithDiagnostics<EvaluationResult> =
|
||||
ResultWithDiagnostics.Failure("not implemented".asErrorDiagnostics())
|
||||
}
|
||||
|
||||
// TODO: from org.jetbrains.kotlin.utils.addToStdlib, take it from the stdlib when available
|
||||
private inline fun <reified T : Any> Iterable<*>.firstIsInstanceOrNull(): T? {
|
||||
for (element in this) if (element is T) return element
|
||||
return null
|
||||
}
|
||||
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* 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 kotlin.script.experimental.definitions
|
||||
|
||||
import kotlin.reflect.KClass
|
||||
import kotlin.reflect.full.primaryConstructor
|
||||
import kotlin.script.experimental.annotations.KotlinScript
|
||||
import kotlin.script.experimental.api.ScriptDefinition
|
||||
|
||||
open class ScriptDefinitionFromAnnotatedBaseClass(final override val baseClass: KClass<*>) : ScriptDefinition {
|
||||
private val annotation = baseClass.java.getAnnotation(KotlinScript::class.java)
|
||||
?: throw IllegalArgumentException("Expecting KotlinScript on the $baseClass")
|
||||
|
||||
override val selector by lazy { annotation.selector.instantiateScriptHandler() }
|
||||
override val configurator by lazy { annotation.configurator.instantiateScriptHandler() }
|
||||
override val runner by lazy { annotation.runner.instantiateScriptHandler() }
|
||||
|
||||
private fun <T : Any> KClass<T>.instantiateScriptHandler(): T {
|
||||
val fqn = this.qualifiedName!!
|
||||
val klass: KClass<T> = (baseClass.java.classLoader.loadClass(fqn) as Class<T>).kotlin
|
||||
// TODO: fix call after deciding on constructor parameters
|
||||
return klass.objectInstance ?: klass.primaryConstructor!!.call(baseClass)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Copyright 2010-2018 JetBrains s.r.o.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
@file:Suppress("unused")
|
||||
|
||||
package kotlin.script.experimental.host
|
||||
|
||||
import kotlinx.coroutines.experimental.CoroutineScope
|
||||
import kotlinx.coroutines.experimental.runBlocking
|
||||
import kotlin.script.experimental.api.*
|
||||
|
||||
abstract class BasicScriptingHost<ScriptBase : Any>(
|
||||
val configurator: ScriptConfigurator,
|
||||
val compiler: ScriptCompiler,
|
||||
val runner: ScriptRunner<ScriptBase>
|
||||
) {
|
||||
open fun <T> runInCoroutineContext(block: suspend CoroutineScope.() -> T): T = runBlocking { block() }
|
||||
|
||||
open fun eval(configuration: ScriptCompileConfiguration, environment: ScriptEvaluationEnvironment): ResultWithDiagnostics<EvaluationResult> =
|
||||
runInCoroutineContext {
|
||||
val compiled = compiler.compile(configuration, configurator)
|
||||
when (compiled) {
|
||||
is ResultWithDiagnostics.Failure -> compiled
|
||||
is ResultWithDiagnostics.Success -> {
|
||||
val compiledScript = compiled.value!! as CompiledScript<ScriptBase>
|
||||
runner.run(compiledScript, environment)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* 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 kotlin.script.experimental.host
|
||||
|
||||
import kotlin.script.experimental.api.ScriptSource
|
||||
import kotlin.script.experimental.api.ScriptSourceFragments
|
||||
import kotlin.script.experimental.api.ScriptSourceNamedFragment
|
||||
import java.io.File
|
||||
import java.net.URL
|
||||
|
||||
fun ScriptSourceFragments.isWholeFile(): Boolean = fragments?.isEmpty() ?: true
|
||||
|
||||
fun ScriptSource.getScriptText(): String = when {
|
||||
text != null -> text!!
|
||||
location != null ->
|
||||
location!!.openStream().bufferedReader().readText()
|
||||
else -> throw RuntimeException("unable to get text from null script")
|
||||
}
|
||||
|
||||
fun ScriptSourceFragments.getMergedScriptText(): String {
|
||||
val originalScriptText = originalSource.getScriptText()
|
||||
return if (isWholeFile()) {
|
||||
originalScriptText
|
||||
} else {
|
||||
val sb = StringBuilder(originalScriptText.length)
|
||||
var prevFragment: ScriptSourceNamedFragment? = null
|
||||
for (fragment in fragments!!) {
|
||||
val fragmentStartPos = fragment.range.start.absolutePos
|
||||
val fragmentEndPos = fragment.range.end.absolutePos
|
||||
if (fragmentStartPos == null || fragmentEndPos == null)
|
||||
throw RuntimeException("Script fragments require absolute positions (received: $fragment)")
|
||||
val curPos = if (prevFragment == null) 0 else prevFragment.range.end.absolutePos!!
|
||||
if (prevFragment != null && prevFragment.range.end.absolutePos!! > fragmentStartPos) throw RuntimeException("Unsorted or overlapping fragments: previous: $prevFragment, current: $fragment")
|
||||
if (curPos < fragmentStartPos) {
|
||||
sb.append(
|
||||
originalScriptText.subSequence(
|
||||
curPos,
|
||||
fragmentStartPos
|
||||
).map { if (it == '\r' || it == '\n') it else ' ' }) // preserving lines layout
|
||||
}
|
||||
sb.append(originalScriptText.subSequence(fragmentStartPos, fragmentEndPos))
|
||||
prevFragment = fragment
|
||||
}
|
||||
sb.toString()
|
||||
}
|
||||
}
|
||||
|
||||
open class FileScriptSource(val file: File) : ScriptSource {
|
||||
override val location: URL? get() = file.toURI().toURL()
|
||||
override val text: String? get() = null
|
||||
}
|
||||
|
||||
fun File.toScriptSource(): ScriptSource = FileScriptSource(this)
|
||||
|
||||
open class StringScriptSource(val source: String) : ScriptSource {
|
||||
override val location: URL? get() = null
|
||||
override val text: String? get() = source
|
||||
}
|
||||
|
||||
fun String.toScriptSource(): ScriptSource = StringScriptSource(this)
|
||||
Reference in New Issue
Block a user