From fce68102bd64711c1ade83e6cf417b037b052b0c Mon Sep 17 00:00:00 2001 From: Ilya Chernikov Date: Fri, 7 Dec 2018 21:37:37 +0100 Subject: [PATCH] Implement imported scripts evaluation and construction --- .../cli/jvm/compiler/KotlinCoreEnvironment.kt | 63 ++++++++---- .../experimental/api/scriptCompilation.kt | 3 + .../jvmhost/impl/KJvmCompiledScript.kt | 3 +- .../jvmhost/impl/KJvmCompilerImpl.kt | 96 ++++++++++++++++--- .../jvmhost/jvmScriptEvaluation.kt | 32 +++++-- ...KotlinScriptDefinitionAdapterFromNewAPI.kt | 4 +- 6 files changed, 163 insertions(+), 38 deletions(-) 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 22714b2399c..de5b0d82fa3 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 @@ -242,7 +242,9 @@ class KotlinCoreEnvironment private constructor( sourceFiles += createKtFiles(project) if (scriptDefinitionProvider != null) { - sourceFiles += collectRequiredSourcesFromDependencies(configuration, project, sourceFiles) + val (classpath, newSources, _) = collectScriptsCompilationDependencies(configuration, project, sourceFiles) + configuration.addJvmClasspathRoots(classpath) + sourceFiles += newSources } sourceFiles.sortBy { it.virtualFile.path } @@ -703,29 +705,58 @@ private fun createSourceFilesFromSourceRoots( return result } -fun collectRequiredSourcesFromDependencies( +data class ScriptsCompilationDependencies( + val classpath: List, + val sources: List, + val sourceDependencies: List +) { + data class SourceDependencies( + val script: KtFile, + val sourceDependencies: List + ) +} + +// recursively collect dependencies from initial and imported scripts +fun collectScriptsCompilationDependencies( configuration: CompilerConfiguration, project: Project, initialSources: Iterable -): List { +): ScriptsCompilationDependencies{ + val collectedClassPath = ArrayList() val collectedSources = ArrayList() - val importsProvider = ScriptDependenciesProvider.getInstance(project) - var remainingSources = initialSources.sortedBy { it.virtualFile.path } + val collectedSourceDependencies = ArrayList() + var remainingSources = initialSources val knownSourcePaths = HashSet() + val importsProvider = ScriptDependenciesProvider.getInstance(project) while (true) { - val dependencies = remainingSources.mapNotNull(importsProvider::getScriptDependencies) - configuration.addJvmClasspathRoots(dependencies.flatMap { it.classpath }.distinctBy { it.absolutePath }) - val addedSourceRoots = dependencies.flatMap { it.scripts.map { KotlinSourceRoot(it.path, false) } } - val addedSources = createSourceFilesFromSourceRoots(configuration, project, addedSourceRoots) - val newSources = if (addedSources.isEmpty()) emptyList() else { - remainingSources.forEach { knownSourcePaths.add(it.virtualFile.path) } - addedSources.filterNot { knownSourcePaths.contains(it.virtualFile.path) } + val newRemainingSources = ArrayList() + for (source in remainingSources) { + val dependencies = importsProvider.getScriptDependencies(source) + if (dependencies != null) { + collectedClassPath.addAll(dependencies.classpath) + + val sourceDependenciesRoots = dependencies.scripts.map { KotlinSourceRoot(it.path, false) } + val sourceDependencies = createSourceFilesFromSourceRoots(configuration, project, sourceDependenciesRoots) + if (sourceDependencies.isNotEmpty()) { + collectedSourceDependencies.add(ScriptsCompilationDependencies.SourceDependencies(source, sourceDependencies)) + + val newSources = sourceDependencies.filterNot { knownSourcePaths.contains(it.virtualFile.path) } + for (newSource in newSources) { + collectedSources.add(newSource) + newRemainingSources.add(newSource) + knownSourcePaths.add(newSource.virtualFile.path) + } + } + } } - if (newSources.isEmpty()) break + if (newRemainingSources.isEmpty()) break else { - collectedSources += newSources - remainingSources = newSources + remainingSources = newRemainingSources } } - return collectedSources + return ScriptsCompilationDependencies( + collectedClassPath.distinctBy { it.absolutePath }, + collectedSources, + collectedSourceDependencies + ) } 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 18c96a3c8bf..3b4355ab033 100644 --- a/libraries/scripting/common/src/kotlin/script/experimental/api/scriptCompilation.kt +++ b/libraries/scripting/common/src/kotlin/script/experimental/api/scriptCompilation.kt @@ -233,4 +233,7 @@ interface CompiledScript { * @return result wrapper, if successful - with loaded KClass */ suspend fun getClass(scriptEvaluationConfiguration: ScriptEvaluationConfiguration?): ResultWithDiagnostics> + + val importedScripts: List>? + get() = null } diff --git a/libraries/scripting/jvm-host/src/kotlin/script/experimental/jvmhost/impl/KJvmCompiledScript.kt b/libraries/scripting/jvm-host/src/kotlin/script/experimental/jvmhost/impl/KJvmCompiledScript.kt index 01ada5be731..3f4f10826ad 100644 --- a/libraries/scripting/jvm-host/src/kotlin/script/experimental/jvmhost/impl/KJvmCompiledScript.kt +++ b/libraries/scripting/jvm-host/src/kotlin/script/experimental/jvmhost/impl/KJvmCompiledScript.kt @@ -19,7 +19,8 @@ import kotlin.script.experimental.jvmhost.baseClassLoader class KJvmCompiledScript( compilationConfiguration: ScriptCompilationConfiguration, generationState: GenerationState, - private var scriptClassFQName: String + private var scriptClassFQName: String, + override val importedScripts: List>? = null ) : CompiledScript, Serializable { private var _compilationConfiguration: ScriptCompilationConfiguration? = compilationConfiguration diff --git a/libraries/scripting/jvm-host/src/kotlin/script/experimental/jvmhost/impl/KJvmCompilerImpl.kt b/libraries/scripting/jvm-host/src/kotlin/script/experimental/jvmhost/impl/KJvmCompilerImpl.kt index fc15c833ada..6821229f2cd 100644 --- a/libraries/scripting/jvm-host/src/kotlin/script/experimental/jvmhost/impl/KJvmCompilerImpl.kt +++ b/libraries/scripting/jvm-host/src/kotlin/script/experimental/jvmhost/impl/KJvmCompilerImpl.kt @@ -4,6 +4,7 @@ */ package kotlin.script.experimental.jvmhost.impl +import com.intellij.openapi.fileTypes.LanguageFileType import com.intellij.openapi.util.Disposer import com.intellij.openapi.util.text.StringUtil import com.intellij.openapi.vfs.CharsetToolkit @@ -26,12 +27,20 @@ import org.jetbrains.kotlin.codegen.CompilationErrorHandler import org.jetbrains.kotlin.codegen.KotlinCodegenFacade import org.jetbrains.kotlin.codegen.state.GenerationState import org.jetbrains.kotlin.config.* +import org.jetbrains.kotlin.idea.KotlinFileType import org.jetbrains.kotlin.idea.KotlinLanguage +import org.jetbrains.kotlin.name.Name +import org.jetbrains.kotlin.name.NameUtils import org.jetbrains.kotlin.parsing.KotlinParserDefinition import org.jetbrains.kotlin.psi.KtFile +import org.jetbrains.kotlin.psi.KtScript import org.jetbrains.kotlin.script.KotlinScriptDefinition import org.jetbrains.kotlin.script.util.KotlinJars +import org.jetbrains.kotlin.utils.addToStdlib.firstIsInstanceOrNull import java.io.File +import kotlin.reflect.KClass +import kotlin.reflect.KType +import kotlin.reflect.full.starProjectedType import kotlin.script.experimental.api.* import kotlin.script.experimental.dependencies.DependenciesResolver import kotlin.script.experimental.host.ScriptingHostConfiguration @@ -131,9 +140,14 @@ class KJvmCompilerImpl(val hostConfiguration: ScriptingHostConfiguration) : KJvm val psiFile: KtFile = psiFileFactory.trySetupPsiForFile(virtualFile, KotlinLanguage.INSTANCE, true, false) as KtFile? ?: return failure("Unable to make PSI file from script".asErrorDiagnostics()) - val sourceFiles = arrayListOf(psiFile).also { - it.addAll(collectRequiredSourcesFromDependencies(kotlinCompilerConfiguration, environment.project, it)) - } + val ktScript = psiFile.declarations.firstIsInstanceOrNull() + ?: return failure("Not a script file".asErrorDiagnostics()) + + val sourceFiles = arrayListOf(psiFile) + val (classpath, newSources, sourceDependencies) = + collectScriptsCompilationDependencies(kotlinCompilerConfiguration, environment.project, sourceFiles) + kotlinCompilerConfiguration.addJvmClasspathRoots(classpath) + sourceFiles.addAll(newSources) analyzerWithCompilerReport.analyzeAndReport(sourceFiles) { val project = environment.project @@ -160,7 +174,16 @@ class KJvmCompilerImpl(val hostConfiguration: ScriptingHostConfiguration) : KJvm ).build() KotlinCodegenFacade.compileCorrectFiles(generationState, CompilationErrorHandler.THROW_EXCEPTION) - val res = KJvmCompiledScript(updatedConfiguration, generationState, scriptFileName.capitalize()) + fun makeCompiledScript(script: KtScript): KJvmCompiledScript<*> { + + val importedScripts = sourceDependencies.find { it.script == script }?.sourceDependencies?.mapNotNull { sourceFile -> + sourceFile.declarations.firstIsInstanceOrNull()?.let { makeCompiledScript(it) } + } ?: emptyList() + + return KJvmCompiledScript(updatedConfiguration, generationState, script.fqName.asString(), importedScripts) + } + + val res = makeCompiledScript(ktScript) return ResultWithDiagnostics.Success(res, messageCollector.diagnostics) } catch (ex: Throwable) { @@ -203,17 +226,37 @@ internal class ScriptDiagnosticsMessageCollector : MessageCollector { } // A bridge to the current scripting - +// mostly copies functionality from KotlinScriptDefinitionAdapterFromNewAPI[Base] +// reusing it requires structural changes that doesn't seem justified now, since the internals of the scripting should be reworked soon anyway +// TODO: either finish refactoring of the scripting internals or reuse KotlinScriptDefinitionAdapterFromNewAPI[BAse] here internal class BridgeScriptDefinition( - scriptCompilationConfiguration: ScriptCompilationConfiguration, - hostConfiguration: ScriptingHostConfiguration, + val scriptCompilationConfiguration: ScriptCompilationConfiguration, + val hostConfiguration: ScriptingHostConfiguration, updateClasspath: (List) -> Unit -) : KotlinScriptDefinition( - hostConfiguration.getScriptingClass( - scriptCompilationConfiguration.getOrError(ScriptCompilationConfiguration.baseClass), - BridgeScriptDefinition::class - ) -) { +) : KotlinScriptDefinition(Any::class) { + + val baseClass: KClass<*> by lazy(LazyThreadSafetyMode.PUBLICATION) { + getScriptingClass(scriptCompilationConfiguration.getOrError(ScriptCompilationConfiguration.baseClass)) + } + + override val template: KClass<*> get() = baseClass + + override val name: String + get() = scriptCompilationConfiguration[ScriptCompilationConfiguration.displayName] ?: "Kotlin Script" + + override val fileType: LanguageFileType = KotlinFileType.INSTANCE + + override fun isScript(fileName: String): Boolean = + fileName.endsWith(".$fileExtension") + + override fun getScriptName(script: KtScript): Name { + val fileBasedName = NameUtils.getScriptNameForFile(script.containingKtFile.name) + return Name.identifier(fileBasedName.identifier.removeSuffix(".$fileExtension")) + } + + override val fileExtension: String + get() = scriptCompilationConfiguration[ScriptCompilationConfiguration.fileExtension] ?: super.fileExtension + override val acceptedAnnotations = run { val cl = this::class.java.classLoader scriptCompilationConfiguration[ScriptCompilationConfiguration.refineConfigurationOnAnnotations]?.annotations @@ -221,6 +264,33 @@ internal class BridgeScriptDefinition( ?: emptyList() } + override val implicitReceivers: List by lazy(LazyThreadSafetyMode.PUBLICATION) { + scriptCompilationConfiguration[ScriptCompilationConfiguration.implicitReceivers] + .orEmpty() + .map { getScriptingClass(it).starProjectedType } + } + + override val providedProperties: List> by lazy(LazyThreadSafetyMode.PUBLICATION) { + scriptCompilationConfiguration[ScriptCompilationConfiguration.providedProperties] + ?.map { (k, v) -> k to getScriptingClass(v).starProjectedType }.orEmpty() + } + + override val additionalCompilerArguments: List + get() = scriptCompilationConfiguration[ScriptCompilationConfiguration.compilerOptions] + .orEmpty() + override val dependencyResolver: DependenciesResolver = BridgeDependenciesResolver(scriptCompilationConfiguration, updateClasspath) + + private val scriptingClassGetter by lazy(LazyThreadSafetyMode.PUBLICATION) { + hostConfiguration[ScriptingHostConfiguration.getScriptingClass] + ?: throw IllegalArgumentException("Expecting 'getScriptingClass' property in the scripting environment") + } + + private fun getScriptingClass(type: KotlinType) = + scriptingClassGetter( + type, + KotlinScriptDefinition::class, // Assuming that the KotlinScriptDefinition class is loaded in the proper classloader + hostConfiguration + ) } diff --git a/libraries/scripting/jvm-host/src/kotlin/script/experimental/jvmhost/jvmScriptEvaluation.kt b/libraries/scripting/jvm-host/src/kotlin/script/experimental/jvmhost/jvmScriptEvaluation.kt index addb6e34591..43b09afcb48 100644 --- a/libraries/scripting/jvm-host/src/kotlin/script/experimental/jvmhost/jvmScriptEvaluation.kt +++ b/libraries/scripting/jvm-host/src/kotlin/script/experimental/jvmhost/jvmScriptEvaluation.kt @@ -41,14 +41,34 @@ open class BasicJvmScriptEvaluator : ScriptEvaluator { scriptEvaluationConfiguration?.get(ScriptEvaluationConfiguration.implicitReceivers)?.let { args.addAll(it) } - scriptEvaluationConfiguration?.get(ScriptEvaluationConfiguration.constructorArgs)?.let { - args.addAll(it) + val importedScriptsReports = ArrayList() + var importedScriptsLoadingFailed = false + compiledScript.importedScripts?.forEach { + val importedScriptEvalRes = invoke(it, scriptEvaluationConfiguration) + importedScriptsReports.addAll(importedScriptEvalRes.reports) + when (importedScriptEvalRes) { + is ResultWithDiagnostics.Success -> { + args.add(importedScriptEvalRes.value) + } + else -> { + importedScriptsLoadingFailed = true + return@forEach + } + } } - val ctor = scriptClass.java.constructors.single() - val instance = ctor.newInstance(*args.toArray()) + if (importedScriptsLoadingFailed) { + ResultWithDiagnostics.Failure(importedScriptsReports) + } else { - // TODO: fix result value - ResultWithDiagnostics.Success(EvaluationResult(ResultValue.Value("", instance, ""), scriptEvaluationConfiguration)) + scriptEvaluationConfiguration?.get(ScriptEvaluationConfiguration.constructorArgs)?.let { + args.addAll(it) + } + val ctor = scriptClass.java.constructors.single() + val instance = ctor.newInstance(*args.toArray()) + + // TODO: fix result value + ResultWithDiagnostics.Success(EvaluationResult(ResultValue.Value("", instance, ""), scriptEvaluationConfiguration)) + } } } } catch (e: Throwable) { diff --git a/plugins/scripting/scripting-cli/src/org/jetbrains/kotlin/scripting/compiler/plugin/KotlinScriptDefinitionAdapterFromNewAPI.kt b/plugins/scripting/scripting-cli/src/org/jetbrains/kotlin/scripting/compiler/plugin/KotlinScriptDefinitionAdapterFromNewAPI.kt index fc0e2094eb7..d63c50b7ab1 100644 --- a/plugins/scripting/scripting-cli/src/org/jetbrains/kotlin/scripting/compiler/plugin/KotlinScriptDefinitionAdapterFromNewAPI.kt +++ b/plugins/scripting/scripting-cli/src/org/jetbrains/kotlin/scripting/compiler/plugin/KotlinScriptDefinitionAdapterFromNewAPI.kt @@ -25,9 +25,9 @@ import kotlin.script.experimental.util.getOrError // temporary trick with passing Any as a template and overwriting it below, TODO: fix after introducing new script definitions hierarchy abstract class KotlinScriptDefinitionAdapterFromNewAPIBase : KotlinScriptDefinition(Any::class) { - protected abstract val scriptCompilationConfiguration: ScriptCompilationConfiguration + abstract val scriptCompilationConfiguration: ScriptCompilationConfiguration - protected abstract val hostConfiguration: ScriptingHostConfiguration + abstract val hostConfiguration: ScriptingHostConfiguration open val baseClass: KClass<*> by lazy(LazyThreadSafetyMode.PUBLICATION) { getScriptingClass(scriptCompilationConfiguration.getOrError(ScriptCompilationConfiguration.baseClass))