From 89bba9361567e09397b4a0686a54d418019ee6cc Mon Sep 17 00:00:00 2001 From: Ilya Muradyan Date: Fri, 27 Nov 2020 13:55:24 +0300 Subject: [PATCH] Introduce GetScriptingClassByClassLoader interface It is needed to override default JVM behaviour --- .../test/ImplicitsFromScriptResultTest.kt | 148 ++++++++++++++++++ .../jvm/jvmScriptingHostConfiguration.kt | 12 +- .../resolve/refineCompilationConfiguration.kt | 4 +- 3 files changed, 160 insertions(+), 4 deletions(-) create mode 100644 libraries/scripting/jvm-host-test/test/kotlin/script/experimental/jvmhost/test/ImplicitsFromScriptResultTest.kt diff --git a/libraries/scripting/jvm-host-test/test/kotlin/script/experimental/jvmhost/test/ImplicitsFromScriptResultTest.kt b/libraries/scripting/jvm-host-test/test/kotlin/script/experimental/jvmhost/test/ImplicitsFromScriptResultTest.kt new file mode 100644 index 00000000000..5c26519e9cf --- /dev/null +++ b/libraries/scripting/jvm-host-test/test/kotlin/script/experimental/jvmhost/test/ImplicitsFromScriptResultTest.kt @@ -0,0 +1,148 @@ +/* + * Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors. + * 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.jvmhost.test + +import junit.framework.TestCase +import kotlinx.coroutines.runBlocking +import java.io.BufferedOutputStream +import java.io.FileOutputStream +import java.nio.file.Files +import java.nio.file.Path +import kotlin.reflect.KClass +import kotlin.script.experimental.api.* +import kotlin.script.experimental.host.ScriptingHostConfiguration +import kotlin.script.experimental.host.getScriptingClass +import kotlin.script.experimental.host.with +import kotlin.script.experimental.jvm.* +import kotlin.script.experimental.jvm.impl.KJvmCompiledModuleInMemory +import kotlin.script.experimental.jvm.impl.KJvmCompiledScript +import kotlin.script.experimental.jvmhost.JvmScriptCompiler + +/** + * This test shows an ability of using KClasses loaded with classloaders + * other than default one, in the role of implicit receivers. For this reason, + * specific [GetScriptingClassByClassLoader] was implemented. Actually, + * as we use previously compiled snippets as implicits, we could achieve the same + * thing if we change the used compiler to one of the REPL ones. But if we are limited + * in our choice of compiler and only can tune the configuration, this is the only way. + * + * This test may be deleted or at least simplified when the option + * in [ScriptCompilationConfiguration] for saving previous classes in + * underlying module will be introduced. + */ +class ImplicitsFromScriptResultTest : TestCase() { + fun testImplicits() { + val host = CompilerHost() + + val snippets = listOf( + "val xyz0 = 42", + "fun f() = xyz0", + "val finalRes = xyz0 + f()", + ) + for (snippet in snippets) { + val res = host.compile(snippet) + assertTrue(res is ResultWithDiagnostics.Success) + } + } +} + +fun interface PreviousScriptClassesProvider { + fun get(): List> +} + +class GetScriptClassForImplicits( + private val previousScriptClassesProvider: PreviousScriptClassesProvider +) : GetScriptingClassByClassLoader { + private val getScriptingClass = JvmGetScriptingClass() + + private val lastClassLoader + get() = previousScriptClassesProvider.get().lastOrNull()?.java?.classLoader + + override fun invoke( + classType: KotlinType, + contextClass: KClass<*>, + hostConfiguration: ScriptingHostConfiguration + ): KClass<*> { + return getScriptingClass(classType, lastClassLoader ?: contextClass.java.classLoader, hostConfiguration) + } + + override fun invoke( + classType: KotlinType, + contextClassLoader: ClassLoader?, + hostConfiguration: ScriptingHostConfiguration + ): KClass<*> { + return getScriptingClass(classType, lastClassLoader ?: contextClassLoader, hostConfiguration) + } +} + +class CompilerHost { + private var counter = 0 + private val implicits = mutableListOf>() + private val outputDir: Path = Files.createTempDirectory("kotlin-scripting-jvm") + private val classWriter = ClassWriter(outputDir) + + init { + outputDir.toFile().deleteOnExit() + } + + private val myHostConfiguration = defaultJvmScriptingHostConfiguration.with { + getScriptingClass(GetScriptClassForImplicits(::getImplicitsClasses)) + } + + private val compileConfiguration = ScriptCompilationConfiguration { + hostConfiguration(myHostConfiguration) + + jvm { + dependencies(JvmDependency(outputDir.toFile())) + } + } + + private val evaluationConfiguration = ScriptEvaluationConfiguration() + + private val compiler = JvmScriptCompiler(myHostConfiguration) + + private fun getImplicitsClasses(): List> = implicits + + fun compile(code: String): ResultWithDiagnostics { + val source = SourceCodeTestImpl(counter++, code) + val refinedConfig = compileConfiguration.with { + implicitReceivers(*implicits.toTypedArray()) + } + val result = runBlocking { compiler.invoke(source, refinedConfig) } + val compiledScript = result.valueOrThrow() as KJvmCompiledScript + + classWriter.writeCompiledSnippet(compiledScript) + + val kClass = runBlocking { compiledScript.getClass(evaluationConfiguration) }.valueOrThrow() + implicits.add(kClass) + return result + } + + private class SourceCodeTestImpl(number: Int, override val text: String) : SourceCode { + override val name: String = "Line_$number" + override val locationId: String = "location_$number" + } +} + +class ClassWriter(private val outputDir: Path) { + fun writeCompiledSnippet(snippet: KJvmCompiledScript) { + val moduleInMemory = snippet.getCompiledModule() as KJvmCompiledModuleInMemory + moduleInMemory.compilerOutputFiles.forEach { (name, bytes) -> + if (name.endsWith(".class")) { + writeClass(bytes, outputDir.resolve(name)) + } + } + } + + private fun writeClass(classBytes: ByteArray, path: Path) { + FileOutputStream(path.toAbsolutePath().toString()).use { fos -> + BufferedOutputStream(fos).use { out -> + out.write(classBytes) + out.flush() + } + } + } +} diff --git a/libraries/scripting/jvm/src/kotlin/script/experimental/jvm/jvmScriptingHostConfiguration.kt b/libraries/scripting/jvm/src/kotlin/script/experimental/jvm/jvmScriptingHostConfiguration.kt index f51f812abed..ee998d0acb8 100644 --- a/libraries/scripting/jvm/src/kotlin/script/experimental/jvm/jvmScriptingHostConfiguration.kt +++ b/libraries/scripting/jvm/src/kotlin/script/experimental/jvm/jvmScriptingHostConfiguration.kt @@ -45,7 +45,11 @@ val defaultJvmScriptingHostConfiguration getScriptingClass(JvmGetScriptingClass()) } -class JvmGetScriptingClass : GetScriptingClass, Serializable { +interface GetScriptingClassByClassLoader : GetScriptingClass { + operator fun invoke(classType: KotlinType, contextClassLoader: ClassLoader?, hostConfiguration: ScriptingHostConfiguration): KClass<*> +} + +class JvmGetScriptingClass : GetScriptingClassByClassLoader, Serializable { @Transient private var dependencies: List? = null @@ -64,7 +68,11 @@ class JvmGetScriptingClass : GetScriptingClass, Serializable { invoke(classType, contextClass.java.classLoader, hostConfiguration) @Synchronized - operator fun invoke(classType: KotlinType, contextClassLoader: ClassLoader?, hostConfiguration: ScriptingHostConfiguration): KClass<*> { + override operator fun invoke( + classType: KotlinType, + contextClassLoader: ClassLoader?, + hostConfiguration: ScriptingHostConfiguration + ): KClass<*> { // checking if class already loaded in the same context val fromClass = classType.fromClass diff --git a/plugins/scripting/scripting-compiler-impl/src/org/jetbrains/kotlin/scripting/resolve/refineCompilationConfiguration.kt b/plugins/scripting/scripting-compiler-impl/src/org/jetbrains/kotlin/scripting/resolve/refineCompilationConfiguration.kt index 8e09af1a06a..6d2ea8ca635 100644 --- a/plugins/scripting/scripting-compiler-impl/src/org/jetbrains/kotlin/scripting/resolve/refineCompilationConfiguration.kt +++ b/plugins/scripting/scripting-compiler-impl/src/org/jetbrains/kotlin/scripting/resolve/refineCompilationConfiguration.kt @@ -352,8 +352,8 @@ fun getScriptCollectedData( val hostConfiguration = compilationConfiguration[ScriptCompilationConfiguration.hostConfiguration] ?: defaultJvmScriptingHostConfiguration val getScriptingClass = hostConfiguration[ScriptingHostConfiguration.getScriptingClass] - val jvmGetScriptingClass = (getScriptingClass as? JvmGetScriptingClass) - ?: throw IllegalArgumentException("Expecting JvmGetScriptingClass in the hostConfiguration[getScriptingClass], got $getScriptingClass") + val jvmGetScriptingClass = (getScriptingClass as? GetScriptingClassByClassLoader) + ?: throw IllegalArgumentException("Expecting class implementing GetScriptingClassByClassLoader in the hostConfiguration[getScriptingClass], got $getScriptingClass") val acceptedAnnotations = compilationConfiguration[ScriptCompilationConfiguration.refineConfigurationOnAnnotations]?.flatMap { it.annotations.mapNotNull { ann ->