Improve script and REPL result handling:

- implement error result
- refactor other result classes
- implement handling in the script evaluation extension - also restores
  previous script error reporting functionality
- add possibility to customize result fileds in script and REPL
- refactor result calculation in the backend: cleanup, rename (since
  it is not only about REPL now)
This commit is contained in:
Ilya Chernikov
2019-07-04 13:33:15 +02:00
parent dc4370ff08
commit 9ae0ff03fa
23 changed files with 421 additions and 127 deletions
@@ -51,6 +51,8 @@ runtimeJar()
sourcesJar()
javadocJar()
testsJar()
projectTest {
workingDir = rootDir
}
@@ -14,7 +14,7 @@ import org.jetbrains.kotlin.cli.common.arguments.CommonCompilerArguments
import org.jetbrains.kotlin.cli.common.arguments.K2JVMCompilerArguments
import org.jetbrains.kotlin.cli.common.config.addKotlinSourceRoot
import org.jetbrains.kotlin.cli.common.extensions.ScriptEvaluationExtension
import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity.ERROR
import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity
import org.jetbrains.kotlin.cli.jvm.compiler.EnvironmentConfigFiles
import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment
import org.jetbrains.kotlin.config.CompilerConfiguration
@@ -23,9 +23,8 @@ import org.jetbrains.kotlin.scripting.compiler.plugin.impl.ScriptJvmCompilerFrom
import org.jetbrains.kotlin.scripting.configuration.ScriptingConfigurationKeys
import org.jetbrains.kotlin.scripting.definitions.ScriptDefinitionProvider
import java.io.File
import kotlin.script.experimental.api.constructorArgs
import kotlin.script.experimental.api.valueOr
import kotlin.script.experimental.api.with
import java.io.PrintStream
import kotlin.script.experimental.api.*
import kotlin.script.experimental.host.toScriptSource
import kotlin.script.experimental.jvm.BasicJvmScriptEvaluator
@@ -41,7 +40,7 @@ class JvmCliScriptEvaluationExtension : ScriptEvaluationExtension {
val messageCollector = configuration.getNotNull(CLIConfigurationKeys.MESSAGE_COLLECTOR_KEY)
val scriptDefinitionProvider = ScriptDefinitionProvider.getInstance(projectEnvironment.project)
if (scriptDefinitionProvider == null) {
messageCollector.report(ERROR, "Unable to process the script, scripting plugin is not configured")
messageCollector.report(CompilerMessageSeverity.ERROR, "Unable to process the script, scripting plugin is not configured")
return COMPILATION_ERROR
}
val sourcePath = arguments.freeArgs.first()
@@ -56,7 +55,7 @@ class JvmCliScriptEvaluationExtension : ScriptEvaluationExtension {
val extensionHint =
if (configuration.get(ScriptingConfigurationKeys.SCRIPT_DEFINITIONS)?.let { it.size == 1 && it.first().isDefault } == true) " (.kts)"
else ""
messageCollector.report(ERROR, "Specify path to the script file$extensionHint as the first argument")
messageCollector.report(CompilerMessageSeverity.ERROR, "Specify path to the script file$extensionHint as the first argument")
return COMPILATION_ERROR
}
@@ -76,16 +75,56 @@ class JvmCliScriptEvaluationExtension : ScriptEvaluationExtension {
return runBlocking {
val compiledScript =
scriptCompiler.compile(script, scriptCompilationConfiguration)
.valueOr { return@runBlocking COMPILATION_ERROR }
/*val evalResult = */
BasicJvmScriptEvaluator().invoke(compiledScript, evaluationConfiguration)
.valueOr {
for (report in it.reports) {
messageCollector.report(report.severity.toCompilerMessageSeverity(), report.render(withSeverity = false))
}
return@runBlocking COMPILATION_ERROR
}
val evalResult = BasicJvmScriptEvaluator().invoke(compiledScript, evaluationConfiguration)
.valueOr {
for (report in it.reports) {
messageCollector.report(ERROR, report.toString())
messageCollector.report(report.severity.toCompilerMessageSeverity(), report.render(withSeverity = false))
}
return@runBlocking ExitCode.SCRIPT_EXECUTION_ERROR
return@runBlocking ExitCode.INTERNAL_ERROR
}
ExitCode.OK
when (evalResult.returnValue) {
is ResultValue.Value -> {
println((evalResult.returnValue as ResultValue.Value).value)
ExitCode.OK
}
is ResultValue.Error -> {
val errorValue = evalResult.returnValue as ResultValue.Error
errorValue.renderError(System.err)
ExitCode.SCRIPT_EXECUTION_ERROR
}
else -> ExitCode.OK
}
}
}
}
}
private fun ScriptDiagnostic.Severity.toCompilerMessageSeverity(): CompilerMessageSeverity =
when (this) {
ScriptDiagnostic.Severity.FATAL -> CompilerMessageSeverity.EXCEPTION
ScriptDiagnostic.Severity.ERROR -> CompilerMessageSeverity.ERROR
ScriptDiagnostic.Severity.WARNING -> CompilerMessageSeverity.WARNING
ScriptDiagnostic.Severity.INFO -> CompilerMessageSeverity.INFO
ScriptDiagnostic.Severity.DEBUG -> CompilerMessageSeverity.LOGGING
}
private fun ResultValue.Error.renderError(stream: PrintStream) {
val fullTrace = error.stackTrace
if (wrappingException == null || fullTrace.size < wrappingException!!.stackTrace.size) {
error.printStackTrace(stream)
} else {
// subtracting wrapping message stacktrace from error stacktrace to show only user-specific part of it
// TODO: consider more reliable logic, e.g. comparing traces, fallback to full error printing in case of mismatch
// TODO: write tests
stream.println(error)
val scriptTraceSize = fullTrace.size - wrappingException!!.stackTrace.size
for (i in 0 until scriptTraceSize) {
stream.println("\tat " + fullTrace[i])
}
}
}
@@ -13,7 +13,6 @@ import org.jetbrains.kotlin.cli.common.messages.MessageCollectorBasedReporter
import org.jetbrains.kotlin.cli.common.repl.IReplStageHistory
import org.jetbrains.kotlin.cli.common.repl.LineId
import org.jetbrains.kotlin.cli.common.repl.ReplCodeLine
import org.jetbrains.kotlin.cli.common.repl.scriptResultFieldName
import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment
import org.jetbrains.kotlin.codegen.ClassBuilderFactories
import org.jetbrains.kotlin.codegen.CompilationErrorHandler
@@ -112,7 +111,14 @@ class KJvmReplCompilerImpl(val hostConfiguration: ScriptingHostConfiguration) :
is ReplCodeAnalyzer.ReplLineAnalysisResult.WithErrors -> return failure(
messageCollector
)
is ReplCodeAnalyzer.ReplLineAnalysisResult.Successful -> analysisResult.scriptDescriptor
is ReplCodeAnalyzer.ReplLineAnalysisResult.Successful -> {
(analysisResult.scriptDescriptor as? ScriptDescriptor)
?: return failure(
snippet,
messageCollector,
"Unexpected script descriptor type ${analysisResult.scriptDescriptor::class}"
)
}
else -> return failure(
snippet,
messageCollector,
@@ -120,8 +126,6 @@ class KJvmReplCompilerImpl(val hostConfiguration: ScriptingHostConfiguration) :
)
}
val type = (scriptDescriptor as ScriptDescriptor).resultValue?.returnType
val generationState = GenerationState.Builder(
snippetKtFile.project,
ClassBuilderFactories.BINARIES,
@@ -130,9 +134,7 @@ class KJvmReplCompilerImpl(val hostConfiguration: ScriptingHostConfiguration) :
sourceFiles,
compilationState.environment.configuration
).build().apply {
replSpecific.resultType = type
replSpecific.scriptResultFieldName = scriptResultFieldName(codeLine.no)
replSpecific.earlierScriptsForReplInterpreter = history.map { it.item }
scriptSpecific.earlierScriptsForReplInterpreter = history.map { it.item }
beforeCompile()
}
KotlinCodegenFacade.generatePackage(
@@ -147,10 +147,9 @@ internal fun makeCompiledScript(
val module = makeCompiledModule(generationState)
val resultField = with(generationState.replSpecific) {
// TODO: pass it in the configuration instead
if (!hasResult || resultType == null || scriptResultFieldName == null) null
else scriptResultFieldName!! to KotlinType(DescriptorRenderer.FQ_NAMES_IN_TYPES.renderType(resultType!!))
val resultField = with(generationState.scriptSpecific) {
if (resultType == null || resultFieldName == null) null
else resultFieldName!! to KotlinType(DescriptorRenderer.FQ_NAMES_IN_TYPES.renderType(resultType!!))
}
return KJvmCompiledScript(
@@ -0,0 +1,2 @@
10
@@ -0,0 +1,76 @@
/*
* Copyright 2010-2019 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 org.jetbrains.kotlin.scripting.compiler.plugin
import java.io.File
import junit.framework.Assert.*
import org.jetbrains.kotlin.cli.common.CLITool
import org.jetbrains.kotlin.cli.jvm.K2JVMCompiler
import org.junit.Test
import java.io.ByteArrayOutputStream
import java.io.PrintStream
class ScriptingWithCliCompilerTest {
companion object {
const val TEST_DATA_DIR = "plugins/scripting/scripting-compiler/testData"
}
@Test
fun testResultValue() {
runWithK2JVMCompiler("$TEST_DATA_DIR/integration/intResult.kts", listOf("10"))
}
}
fun runWithK2JVMCompiler(
scriptPath: String,
expectedOutPatterns: List<String> = emptyList(),
expectedExitCode: Int = 0
) {
val mainKtsJar = File("dist/kotlinc/lib/kotlin-main-kts.jar")
assertTrue("kotlin-main-kts.jar not found, run dist task: ${mainKtsJar.absolutePath}", mainKtsJar.exists())
val (out, err, ret) = captureOutErrRet {
CLITool.doMainNoExit(
K2JVMCompiler(),
arrayOf("-kotlin-home", "dist/kotlinc", "-cp", mainKtsJar.absolutePath, "-script", scriptPath)
)
}
try {
val outLines = out.lines()
assertEquals(expectedOutPatterns.size, outLines.size)
for ((expectedPattern, actualLine) in expectedOutPatterns.zip(outLines)) {
assertTrue(
"line \"$actualLine\" do not match with expected pattern \"$expectedPattern\"",
Regex(expectedPattern).matches(actualLine)
)
}
assertEquals(expectedExitCode, ret.code)
} catch (e: Throwable) {
println("OUT:\n$out")
println("ERR:\n$err")
throw e
}
}
internal fun <T> captureOutErrRet(body: () -> T): Triple<String, String, T> {
val outStream = ByteArrayOutputStream()
val errStream = ByteArrayOutputStream()
val prevOut = System.out
val prevErr = System.err
System.setOut(PrintStream(outStream))
System.setErr(PrintStream(errStream))
val ret = try {
body()
} finally {
System.out.flush()
System.err.flush()
System.setOut(prevOut)
System.setErr(prevErr)
}
return Triple(outStream.toString().trim(), errStream.toString().trim(), ret)
}