diff --git a/libraries/scripting/common/src/kotlin/script/experimental/api/scriptCompilation.kt b/libraries/scripting/common/src/kotlin/script/experimental/api/scriptCompilation.kt index d4b4994ed7d..a62b9b53cd5 100644 --- a/libraries/scripting/common/src/kotlin/script/experimental/api/scriptCompilation.kt +++ b/libraries/scripting/common/src/kotlin/script/experimental/api/scriptCompilation.kt @@ -7,6 +7,7 @@ package kotlin.script.experimental.api +import java.io.File import java.io.Serializable import kotlin.reflect.KClass import kotlin.script.experimental.host.ScriptingHostConfiguration @@ -106,6 +107,17 @@ val ScriptCompilationConfigurationKeys.implicitReceivers by PropertiesCollection */ val ScriptCompilationConfigurationKeys.providedProperties by PropertiesCollection.key>() // external variables +/** + * Variable name that holds a {@link File} instance pointing to the location of the script file + */ +val ScriptCompilationConfigurationKeys.scriptFileLocationVariable by PropertiesCollection.key() + +/** + * File pointing to the location of the script file. Note that in some cases it might not be possible + * to determine script file location properly - in this case the file is an empty file + */ +val ScriptCompilationConfigurationKeys.scriptFileLocation by PropertiesCollection.key() + /** * The list of import expressions that will be implicitly applied to the script body, the syntax is the same as for the "import" statement */ diff --git a/libraries/scripting/jvm-host/src/kotlin/script/experimental/jvmhost/jsr223/propertiesFromContext.kt b/libraries/scripting/jvm-host/src/kotlin/script/experimental/jvmhost/jsr223/propertiesFromContext.kt index cdbabb8676d..fd0153f9030 100644 --- a/libraries/scripting/jvm-host/src/kotlin/script/experimental/jvmhost/jsr223/propertiesFromContext.kt +++ b/libraries/scripting/jvm-host/src/kotlin/script/experimental/jvmhost/jsr223/propertiesFromContext.kt @@ -41,12 +41,14 @@ fun configureProvidedPropertiesFromJsr223Context(context: ScriptEvaluationConfig val engineBindings = jsr223context.getBindings(ScriptContext.ENGINE_SCOPE) val globalBindings = jsr223context.getBindings(ScriptContext.GLOBAL_SCOPE) for (prop in knownProperties) { - val v = when { - engineBindings?.containsKey(prop.key) == true -> engineBindings[prop.key] - globalBindings?.containsKey(prop.key) == true -> globalBindings[prop.key] - else -> return ResultWithDiagnostics.Failure("Property ${prop.key} is not found in the bindings".asErrorDiagnostics()) + if (prop.key !in updatedProperties) { + val v = when { + engineBindings?.containsKey(prop.key) == true -> engineBindings[prop.key] + globalBindings?.containsKey(prop.key) == true -> globalBindings[prop.key] + else -> return ResultWithDiagnostics.Failure("Property ${prop.key} is not found in the bindings".asErrorDiagnostics()) + } + updatedProperties[prop.key] = v } - updatedProperties[prop.key] = v } ScriptEvaluationConfiguration(context.evaluationConfiguration) { providedProperties(updatedProperties) diff --git a/libraries/tools/kotlin-main-kts-test/test/org/jetbrains/kotlin/mainKts/test/mainKtsIT.kt b/libraries/tools/kotlin-main-kts-test/test/org/jetbrains/kotlin/mainKts/test/mainKtsIT.kt index 6b04bb8df39..fd6b0dc5a60 100644 --- a/libraries/tools/kotlin-main-kts-test/test/org/jetbrains/kotlin/mainKts/test/mainKtsIT.kt +++ b/libraries/tools/kotlin-main-kts-test/test/org/jetbrains/kotlin/mainKts/test/mainKtsIT.kt @@ -121,6 +121,26 @@ class MainKtsIT { } } + @OptIn(ExperimentalPathApi::class) + @Test + fun testCacheWithFileLocation() { + val scriptPath = File("$TEST_DATA_ROOT/script-file-location-default.main.kts").absolutePath + val cache = createTempDirectory("main.kts.test") + val expectedTestOutput = listOf(Regex.escape(scriptPath)) + + try { + Assert.assertTrue(cache.exists() && cache.listDirectoryEntries("*.jar").isEmpty()) + runWithKotlinRunner(scriptPath, expectedTestOutput, cacheDir = cache) + val cacheFile = cache.listDirectoryEntries("*.jar").firstOrNull() + Assert.assertTrue(cacheFile != null && cacheFile.exists()) + + // this run should use the cached script + runWithKotlinRunner(scriptPath, expectedTestOutput, cacheDir = cache) + } finally { + cache.toFile().deleteRecursively() + } + } + @Test fun testHelloSerialization() { val paths = PathUtil.kotlinPathsForDistDirectory diff --git a/libraries/tools/kotlin-main-kts-test/test/org/jetbrains/kotlin/mainKts/test/mainKtsTest.kt b/libraries/tools/kotlin-main-kts-test/test/org/jetbrains/kotlin/mainKts/test/mainKtsTest.kt index 29bb1988782..b16ab3a32b6 100644 --- a/libraries/tools/kotlin-main-kts-test/test/org/jetbrains/kotlin/mainKts/test/mainKtsTest.kt +++ b/libraries/tools/kotlin-main-kts-test/test/org/jetbrains/kotlin/mainKts/test/mainKtsTest.kt @@ -7,6 +7,7 @@ package org.jetbrains.kotlin.mainKts.test import org.jetbrains.kotlin.mainKts.COMPILED_SCRIPTS_CACHE_DIR_PROPERTY import org.jetbrains.kotlin.mainKts.impl.Directories import org.jetbrains.kotlin.mainKts.MainKtsScript +import org.jetbrains.kotlin.mainKts.SCRIPT_FILE_LOCATION_DEFAULT_VARIABLE_NAME import org.jetbrains.kotlin.scripting.compiler.plugin.assertTrue import org.junit.Assert import org.junit.Assert.assertEquals @@ -145,6 +146,65 @@ class MainKtsTest { Assert.assertEquals(listOf("Hi from sub", "Hi from super", "Hi from random"), out) } + @Test + fun testScriptFileLocationDefaultVariable() { + val resOk = evalFile(File("$TEST_DATA_ROOT/script-file-location-default.main.kts")) + assertSucceeded(resOk) + val resultValue = resOk.valueOrThrow().returnValue + assertTrue(resultValue is ResultValue.Value) { "Result value should be of type Value" } + val value = (resultValue as ResultValue.Value).value!! + assertEquals("String", value::class.simpleName) + val expectedPathSuffix = "libraries/tools/kotlin-main-kts-test/testData/script-file-location-default.main.kts" + val actualPath = (value as String).replace("\\", "/") + assertTrue(actualPath.endsWith(expectedPathSuffix)) { "Script file path does not end with expected path" } + } + + @Test + fun testScriptFileLocationCustomizedVariable() { + val resOk = evalFile(File("$TEST_DATA_ROOT/script-file-location-customized.main.kts")) + assertSucceeded(resOk) + val resultValue = resOk.valueOrThrow().returnValue + assertTrue(resultValue is ResultValue.Value) { "Result value should be of type Value" } + val value = (resultValue as ResultValue.Value).value!! + assertEquals("String", value::class.simpleName) + val expectedPathSuffix = "libraries/tools/kotlin-main-kts-test/testData/script-file-location-customized.main.kts" + val actualPath = (value as String).replace("\\", "/") + assertTrue(actualPath.endsWith(expectedPathSuffix)) { "Script file path does not end with expected path" } + } + + @Test + fun testScriptFileLocationWithImportedScript() { + val resOk = evalFile(File("$TEST_DATA_ROOT/script-file-location-with-imported-file.main.kts")) + assertSucceeded(resOk) + val resultValue = resOk.valueOrThrow().returnValue + assertTrue(resultValue is ResultValue.Value) { "Result value should be of type Value" } + val value = (resultValue as ResultValue.Value).value!! + assertEquals("Array", value::class.simpleName) + val expectedSelfPathSuffix = "libraries/tools/kotlin-main-kts-test/testData/script-file-location-with-imported-file.main.kts" + val expectedImportedPathSuffix = "libraries/tools/kotlin-main-kts-test/testData/script-file-location-helper-imported-file.main.kts" + val actualPathSelf = (value as Array<*>)[0].toString().replace("\\", "/") + val actualPathImported = value[1].toString().replace("\\", "/") + assertTrue(actualPathSelf.endsWith(expectedSelfPathSuffix)) { "Script file path does not end with expected path" } + assertTrue(actualPathImported.endsWith(expectedImportedPathSuffix)) { "Script file path does not end with expected path" } + } + + @Test + fun testScriptFileLocationDefaultVariableNotAvailableIfScriptFileVariableCustomized() { + val resFailed = evalFile(File("$TEST_DATA_ROOT/script-file-location-customized-default-not-available.main.kts")) + assertFailed("Unresolved reference: $SCRIPT_FILE_LOCATION_DEFAULT_VARIABLE_NAME", resFailed) + } + + @Test + fun testScriptFileLocationDefaultVariableRedefinition() { + val resOk = evalFile(File("$TEST_DATA_ROOT/script-file-location-redefine-variable.kts")) + assertSucceeded(resOk) + val resultValue = resOk.valueOrThrow().returnValue + assertTrue(resultValue is ResultValue.Value) { "Result value should be of type Value" } + val value = (resultValue as ResultValue.Value).value!! + assertEquals("String", value::class.simpleName) + assertEquals("success", value) + } + private fun assertIsJava6Bytecode(res: ResultWithDiagnostics) { val scriptClassResource = res.valueOrThrow().returnValue.scriptClass!!.java.run { getResource("$simpleName.class") diff --git a/libraries/tools/kotlin-main-kts-test/testData/script-file-location-customized-default-not-available.main.kts b/libraries/tools/kotlin-main-kts-test/testData/script-file-location-customized-default-not-available.main.kts new file mode 100644 index 00000000000..0c7d2940422 --- /dev/null +++ b/libraries/tools/kotlin-main-kts-test/testData/script-file-location-customized-default-not-available.main.kts @@ -0,0 +1,4 @@ + +@file:ScriptFileLocation("scriptFileLocation") + +__FILE__.absolutePath \ No newline at end of file diff --git a/libraries/tools/kotlin-main-kts-test/testData/script-file-location-customized.main.kts b/libraries/tools/kotlin-main-kts-test/testData/script-file-location-customized.main.kts new file mode 100644 index 00000000000..4d8d446f56f --- /dev/null +++ b/libraries/tools/kotlin-main-kts-test/testData/script-file-location-customized.main.kts @@ -0,0 +1,4 @@ + +@file:ScriptFileLocation("scriptFileLocation") + +scriptFileLocation.absolutePath \ No newline at end of file diff --git a/libraries/tools/kotlin-main-kts-test/testData/script-file-location-default.main.kts b/libraries/tools/kotlin-main-kts-test/testData/script-file-location-default.main.kts new file mode 100644 index 00000000000..899449b2f3c --- /dev/null +++ b/libraries/tools/kotlin-main-kts-test/testData/script-file-location-default.main.kts @@ -0,0 +1,2 @@ + +__FILE__.absolutePath \ No newline at end of file diff --git a/libraries/tools/kotlin-main-kts-test/testData/script-file-location-helper-imported-file.main.kts b/libraries/tools/kotlin-main-kts-test/testData/script-file-location-helper-imported-file.main.kts new file mode 100644 index 00000000000..cc3e9deae87 --- /dev/null +++ b/libraries/tools/kotlin-main-kts-test/testData/script-file-location-helper-imported-file.main.kts @@ -0,0 +1,5 @@ +import java.io.File + +fun getDependentScriptFile(): File { + return __FILE__ +} diff --git a/libraries/tools/kotlin-main-kts-test/testData/script-file-location-redefine-variable.kts b/libraries/tools/kotlin-main-kts-test/testData/script-file-location-redefine-variable.kts new file mode 100644 index 00000000000..b5e86bd4552 --- /dev/null +++ b/libraries/tools/kotlin-main-kts-test/testData/script-file-location-redefine-variable.kts @@ -0,0 +1,3 @@ + +val __FILE__ = "success" +__FILE__ diff --git a/libraries/tools/kotlin-main-kts-test/testData/script-file-location-with-imported-file.main.kts b/libraries/tools/kotlin-main-kts-test/testData/script-file-location-with-imported-file.main.kts new file mode 100644 index 00000000000..ec01c10e687 --- /dev/null +++ b/libraries/tools/kotlin-main-kts-test/testData/script-file-location-with-imported-file.main.kts @@ -0,0 +1,4 @@ +@file:Import("script-file-location-helper-imported-file.main.kts") + +arrayOf(__FILE__.absolutePath, getDependentScriptFile().absolutePath) + diff --git a/libraries/tools/kotlin-main-kts/src/org/jetbrains/kotlin/mainKts/annotations.kt b/libraries/tools/kotlin-main-kts/src/org/jetbrains/kotlin/mainKts/annotations.kt index e7df7b65a8a..8e1d1128f07 100644 --- a/libraries/tools/kotlin-main-kts/src/org/jetbrains/kotlin/mainKts/annotations.kt +++ b/libraries/tools/kotlin-main-kts/src/org/jetbrains/kotlin/mainKts/annotations.kt @@ -33,3 +33,11 @@ annotation class Import(vararg val paths: String) @Repeatable @Retention(AnnotationRetention.SOURCE) annotation class CompilerOptions(vararg val options: String) + +/** + * Option that configures the name of the variable that will hold a file pointing to the script location. + * If not specified, {@link [SCRIPT_FILE_LOCATION_DEFAULT_VARIABLE_NAME]} will be used as the variable name + */ +@Target(AnnotationTarget.FILE) +@Retention(AnnotationRetention.SOURCE) +annotation class ScriptFileLocation(val variable: String) diff --git a/libraries/tools/kotlin-main-kts/src/org/jetbrains/kotlin/mainKts/scriptDef.kt b/libraries/tools/kotlin-main-kts/src/org/jetbrains/kotlin/mainKts/scriptDef.kt index 708eaab10bd..f8da37e8c2e 100644 --- a/libraries/tools/kotlin-main-kts/src/org/jetbrains/kotlin/mainKts/scriptDef.kt +++ b/libraries/tools/kotlin-main-kts/src/org/jetbrains/kotlin/mainKts/scriptDef.kt @@ -40,15 +40,18 @@ abstract class MainKtsScript(val args: Array) const val COMPILED_SCRIPTS_CACHE_DIR_ENV_VAR = "KOTLIN_MAIN_KTS_COMPILED_SCRIPTS_CACHE_DIR" const val COMPILED_SCRIPTS_CACHE_DIR_PROPERTY = "kotlin.main.kts.compiled.scripts.cache.dir" const val COMPILED_SCRIPTS_CACHE_VERSION = 1 +const val SCRIPT_FILE_LOCATION_DEFAULT_VARIABLE_NAME = "__FILE__" class MainKtsScriptDefinition : ScriptCompilationConfiguration( { - defaultImports(DependsOn::class, Repository::class, Import::class, CompilerOptions::class) + defaultImports(DependsOn::class, Repository::class, Import::class, CompilerOptions::class, ScriptFileLocation::class) jvm { dependenciesFromClassContext(MainKtsScriptDefinition::class, "kotlin-main-kts", "kotlin-stdlib", "kotlin-reflect") } refineConfiguration { onAnnotations(DependsOn::class, Repository::class, Import::class, CompilerOptions::class, handler = MainKtsConfigurator()) + onAnnotations(ScriptFileLocation::class, handler = ScriptFileLocationCustomConfigurator()) + beforeCompiling(::configureScriptFileLocationPathVariablesForCompilation) beforeCompiling(::configureProvidedPropertiesFromJsr223Context) } ide { @@ -63,6 +66,7 @@ class MainKtsScriptDefinition : ScriptCompilationConfiguration( object MainKtsEvaluationConfiguration : ScriptEvaluationConfiguration( { scriptsInstancesSharing(true) + refineConfigurationBeforeEvaluate(::configureScriptFileLocationPathVariablesForEvaluation) refineConfigurationBeforeEvaluate(::configureProvidedPropertiesFromJsr223Context) refineConfigurationBeforeEvaluate(::configureConstructorArgsFromMainArgs) } @@ -90,6 +94,48 @@ class MainKtsHostConfiguration : ScriptingHostConfiguration( } ) +fun configureScriptFileLocationPathVariablesForEvaluation(context: ScriptEvaluationConfigurationRefinementContext): ResultWithDiagnostics { + val compilationConfiguration = context.evaluationConfiguration[ScriptEvaluationConfiguration.compilationConfiguration] + ?: throw RuntimeException() + val scriptFileLocation = compilationConfiguration[ScriptCompilationConfiguration.scriptFileLocation] + ?: return context.evaluationConfiguration.asSuccess() + val scriptFileLocationVariable = compilationConfiguration[ScriptCompilationConfiguration.scriptFileLocationVariable] + ?: return context.evaluationConfiguration.asSuccess() + + val res = context.evaluationConfiguration.with { + providedProperties.put(mapOf(scriptFileLocationVariable to scriptFileLocation)) + } + return res.asSuccess() +} + +fun configureScriptFileLocationPathVariablesForCompilation(context: ScriptConfigurationRefinementContext): ResultWithDiagnostics { + val scriptFile = (context.script as? FileBasedScriptSource)?.file ?: return context.compilationConfiguration.asSuccess() + val scriptFileLocationVariableName = context.compilationConfiguration[ScriptCompilationConfiguration.scriptFileLocationVariable] + ?: SCRIPT_FILE_LOCATION_DEFAULT_VARIABLE_NAME + + return ScriptCompilationConfiguration(context.compilationConfiguration) { + providedProperties.put(mapOf(scriptFileLocationVariableName to KotlinType(File::class))) + scriptFileLocation.put(scriptFile) + scriptFileLocationVariable.put(scriptFileLocationVariableName) + }.asSuccess() +} + +class ScriptFileLocationCustomConfigurator : RefineScriptCompilationConfigurationHandler { + + override operator fun invoke(context: ScriptConfigurationRefinementContext): ResultWithDiagnostics { + + val scriptLocationVariable = context.collectedData?.get(ScriptCollectedData.collectedAnnotations) + ?.filterByAnnotationType()?.firstOrNull()?.annotation?.variable + ?: return context.compilationConfiguration.asSuccess() + + val compilationConfiguration = ScriptCompilationConfiguration(context.compilationConfiguration) { + scriptFileLocationVariable.put(scriptLocationVariable) + } + + return compilationConfiguration.asSuccess() + } +} + fun configureConstructorArgsFromMainArgs(context: ScriptEvaluationConfigurationRefinementContext): ResultWithDiagnostics { val mainArgs = context.evaluationConfiguration[ScriptEvaluationConfiguration.jvm.mainArguments] val res = if (context.evaluationConfiguration[ScriptEvaluationConfiguration.constructorArgs] == null && mainArgs != null) { diff --git a/plugins/scripting/scripting-compiler/src/org/jetbrains/kotlin/scripting/compiler/plugin/impl/jvmCompilationUtil.kt b/plugins/scripting/scripting-compiler/src/org/jetbrains/kotlin/scripting/compiler/plugin/impl/jvmCompilationUtil.kt index 318f4eb9c8a..8b0afb6151d 100644 --- a/plugins/scripting/scripting-compiler/src/org/jetbrains/kotlin/scripting/compiler/plugin/impl/jvmCompilationUtil.kt +++ b/plugins/scripting/scripting-compiler/src/org/jetbrains/kotlin/scripting/compiler/plugin/impl/jvmCompilationUtil.kt @@ -141,7 +141,7 @@ internal fun makeCompiledScript( sourceFile.declarations.firstIsInstanceOrNull()?.let { ktScript -> makeOtherScripts(ktScript).onSuccess { otherScripts -> KJvmCompiledScript( - containingKtFile.virtualFilePath, + sourceFile.virtualFilePath, getScriptConfiguration(sourceFile), ktScript.fqName.asString(), null,