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:
@@ -364,23 +364,27 @@ public class ExpressionCodegen extends KtVisitor<StackValue, StackValue> impleme
|
||||
|
||||
private void putStackValue(@Nullable KtElement expr, @NotNull Type type, @Nullable KotlinType kotlinType, @NotNull StackValue value) {
|
||||
// for repl store the result of the last line into special field
|
||||
if (value.type != Type.VOID_TYPE && state.getReplSpecific().getShouldGenerateScriptResultValue()) {
|
||||
if (value.type != Type.VOID_TYPE) {
|
||||
ScriptContext context = getScriptContext();
|
||||
if (expr == context.getLastStatement()) {
|
||||
StackValue.Field resultValue = StackValue.field(context.getResultFieldInfo(), StackValue.LOCAL_0);
|
||||
resultValue.store(value, v);
|
||||
state.getReplSpecific().setHasResult(true);
|
||||
return;
|
||||
if (context != null && expr == context.getLastStatement()) {
|
||||
FieldInfo resultFieldInfo = context.getResultFieldInfo();
|
||||
if (resultFieldInfo != null) {
|
||||
StackValue.Field resultValue = StackValue.field(resultFieldInfo, StackValue.LOCAL_0);
|
||||
resultValue.store(value, v);
|
||||
state.getScriptSpecific().setResultType(resultFieldInfo.getFieldKotlinType());
|
||||
state.getScriptSpecific().setResultFieldName(resultFieldInfo.getFieldName());
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
value.put(type, kotlinType, v);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Nullable
|
||||
private ScriptContext getScriptContext() {
|
||||
CodegenContext context = getContext();
|
||||
while (!(context instanceof ScriptContext)) {
|
||||
while (context != null && !(context instanceof ScriptContext)) {
|
||||
context = context.getParentContext();
|
||||
}
|
||||
return (ScriptContext) context;
|
||||
|
||||
@@ -23,7 +23,6 @@ import org.jetbrains.kotlin.resolve.descriptorUtil.getSuperClassOrAny
|
||||
import org.jetbrains.kotlin.resolve.descriptorUtil.getSuperInterfaces
|
||||
import org.jetbrains.kotlin.resolve.jvm.AsmTypes
|
||||
import org.jetbrains.kotlin.resolve.jvm.AsmTypes.OBJECT_TYPE
|
||||
import org.jetbrains.kotlin.resolve.jvm.diagnostics.JvmDeclarationOrigin
|
||||
import org.jetbrains.kotlin.resolve.jvm.diagnostics.JvmDeclarationOrigin.Companion.NO_ORIGIN
|
||||
import org.jetbrains.kotlin.resolve.jvm.diagnostics.OtherOrigin
|
||||
import org.jetbrains.kotlin.serialization.DescriptorSerializer
|
||||
@@ -86,14 +85,14 @@ class ScriptCodegen private constructor(
|
||||
)
|
||||
val asmMethod = jvmSignature.asmMethod
|
||||
|
||||
if (state.replSpecific.shouldGenerateScriptResultValue) {
|
||||
val resultFieldInfo = scriptContext.resultFieldInfo
|
||||
scriptContext.resultFieldInfo?.let { resultFieldInfo ->
|
||||
classBuilder.newField(
|
||||
JvmDeclarationOrigin.NO_ORIGIN,
|
||||
ACC_PUBLIC or ACC_FINAL,
|
||||
resultFieldInfo.fieldName,
|
||||
resultFieldInfo.fieldType.descriptor,
|
||||
null, null)
|
||||
NO_ORIGIN,
|
||||
ACC_PUBLIC or ACC_FINAL,
|
||||
resultFieldInfo.fieldName,
|
||||
resultFieldInfo.fieldType.descriptor,
|
||||
null, null
|
||||
)
|
||||
}
|
||||
|
||||
val mv = classBuilder.newMethod(
|
||||
@@ -277,7 +276,7 @@ class ScriptCodegen private constructor(
|
||||
val builder = state.factory.newVisitor(
|
||||
OtherOrigin(declaration, scriptDescriptor), classType, declaration.containingFile)
|
||||
|
||||
val earlierScripts = state.replSpecific.earlierScriptsForReplInterpreter
|
||||
val earlierScripts = state.scriptSpecific.earlierScriptsForReplInterpreter
|
||||
|
||||
val scriptContext = parentContext.intoScript(
|
||||
scriptDescriptor,
|
||||
|
||||
@@ -42,15 +42,15 @@ class ScriptContext(
|
||||
) : ScriptLikeContext(typeMapper, contextDescriptor, parentContext) {
|
||||
val lastStatement: KtExpression?
|
||||
|
||||
val resultFieldInfo: FieldInfo
|
||||
val resultFieldInfo: FieldInfo?
|
||||
get() {
|
||||
assert(state.replSpecific.shouldGenerateScriptResultValue) { "Should not be called unless 'resultFieldName' is set" }
|
||||
val scriptResultFieldName = state.replSpecific.scriptResultFieldName!!
|
||||
val fieldType = state.replSpecific.resultType?.let { state.typeMapper.mapType(it) } ?: AsmTypes.OBJECT_TYPE
|
||||
val resultValue = scriptDescriptor.resultValue ?: return null
|
||||
val scriptResultFieldName = resultValue.name.identifier
|
||||
val fieldType = resultValue.returnType?.let { state.typeMapper.mapType(it) } ?: AsmTypes.OBJECT_TYPE
|
||||
return FieldInfo.createForHiddenField(
|
||||
state.typeMapper.mapClass(scriptDescriptor),
|
||||
fieldType,
|
||||
state.replSpecific.resultType,
|
||||
resultValue.returnType,
|
||||
scriptResultFieldName
|
||||
)
|
||||
}
|
||||
|
||||
@@ -391,7 +391,7 @@ class PsiSourceCompilerForInline(private val codegen: ExpressionCodegen, overrid
|
||||
|
||||
return when (descriptor) {
|
||||
is ScriptDescriptor -> {
|
||||
val earlierScripts = state.replSpecific.earlierScriptsForReplInterpreter
|
||||
val earlierScripts = state.scriptSpecific.earlierScriptsForReplInterpreter
|
||||
containerContext.intoScript(
|
||||
descriptor,
|
||||
earlierScripts ?: emptyList(),
|
||||
|
||||
@@ -214,15 +214,15 @@ class GenerationState private constructor(
|
||||
val factory: ClassFileFactory
|
||||
private lateinit var duplicateSignatureFactory: BuilderFactoryForDuplicateSignatureDiagnostics
|
||||
|
||||
val replSpecific = ForRepl()
|
||||
val scriptSpecific = ForScript()
|
||||
|
||||
//TODO: should be refactored out
|
||||
class ForRepl {
|
||||
// TODO: review usages and consider replace mutability with explicit passing of input and output
|
||||
class ForScript {
|
||||
// quite a mess, this one is an input from repl interpreter
|
||||
var earlierScriptsForReplInterpreter: List<ScriptDescriptor>? = null
|
||||
var scriptResultFieldName: String? = null
|
||||
val shouldGenerateScriptResultValue: Boolean get() = scriptResultFieldName != null
|
||||
// and the rest is an output from the codegen
|
||||
var resultFieldName: String? = null
|
||||
var resultType: KotlinType? = null
|
||||
var hasResult: Boolean = false
|
||||
}
|
||||
|
||||
val isCallAssertionsDisabled: Boolean = configuration.getBoolean(JVMConfigurationKeys.DISABLE_CALL_ASSERTIONS)
|
||||
|
||||
+9
-5
@@ -108,12 +108,16 @@ open class GenericReplEvaluator(
|
||||
|
||||
historyActor.addFinal(compileResult.lineId, EvalClassWithInstanceAndLoader(scriptClass.kotlin, scriptInstance, classLoader, invokeWrapper))
|
||||
|
||||
val resultFieldName = scriptResultFieldName(compileResult.lineId.no)
|
||||
val resultField = scriptClass.getDeclaredField(resultFieldName).apply { isAccessible = true }
|
||||
val resultValue: Any? = resultField.get(scriptInstance)
|
||||
return if (compileResult.hasResult) {
|
||||
val resultFieldName = scriptResultFieldName(compileResult.lineId.no)
|
||||
val resultField = scriptClass.declaredFields.find { it.name == resultFieldName }?.apply { isAccessible = true }
|
||||
assert(resultField != null) { "compileResult.hasResult == true but resultField is null" }
|
||||
val resultValue: Any? = resultField!!.get(scriptInstance)
|
||||
|
||||
return if (compileResult.hasResult) ReplEvalResult.ValueResult(resultFieldName, resultValue, compileResult.type)
|
||||
else ReplEvalResult.UnitResult()
|
||||
ReplEvalResult.ValueResult(resultFieldName, resultValue, compileResult.type)
|
||||
} else {
|
||||
ReplEvalResult.UnitResult()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,9 @@
|
||||
|
||||
package kotlin.script.experimental.api
|
||||
|
||||
import com.sun.xml.internal.messaging.saaj.util.ByteOutputStream
|
||||
import java.io.File
|
||||
import java.io.PrintStream
|
||||
|
||||
/**
|
||||
* The single script diagnostic report
|
||||
@@ -28,11 +30,28 @@ data class ScriptDiagnostic(
|
||||
*/
|
||||
enum class Severity { FATAL, ERROR, WARNING, INFO, DEBUG }
|
||||
|
||||
override fun toString(): String = buildString {
|
||||
append(severity.name)
|
||||
append(' ')
|
||||
override fun toString(): String = render()
|
||||
|
||||
/**
|
||||
* Render diagnostics message as a string in a form:
|
||||
* "[SEVERITY ]message[ (file:line:column)][: exception message[\n exception stacktrace]]"
|
||||
* @param withSeverity add severity prefix, true by default
|
||||
* @param withLocation add error location in the compiled script, if present, true by default
|
||||
* @param withException add exception message, if present, true by default
|
||||
* @param withStackTrace add exception stacktrace, if exception is present and [withException] is true, false by default
|
||||
*/
|
||||
fun render(
|
||||
withSeverity: Boolean = true,
|
||||
withLocation: Boolean = true,
|
||||
withException: Boolean = true,
|
||||
withStackTrace: Boolean = false
|
||||
): String = buildString {
|
||||
if (withSeverity) {
|
||||
append(severity.name)
|
||||
append(' ')
|
||||
}
|
||||
append(message)
|
||||
if (sourcePath != null || location != null) {
|
||||
if (withLocation && (sourcePath != null || location != null)) {
|
||||
append(" (")
|
||||
sourcePath?.let { append(it.substringAfterLast(File.separatorChar)) }
|
||||
location?.let {
|
||||
@@ -43,9 +62,18 @@ data class ScriptDiagnostic(
|
||||
}
|
||||
append(')')
|
||||
}
|
||||
if (exception != null) {
|
||||
if (withException && exception != null) {
|
||||
append(": ")
|
||||
append(exception)
|
||||
if (withStackTrace) {
|
||||
ByteOutputStream().use { os ->
|
||||
val ps = PrintStream(os)
|
||||
exception.printStackTrace(ps)
|
||||
ps.flush()
|
||||
append("\n")
|
||||
append(os.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
package kotlin.script.experimental.api
|
||||
|
||||
import java.io.Serializable
|
||||
import kotlin.script.experimental.util.PropertiesCollection
|
||||
|
||||
const val REPL_SNIPPET_FIRST_NO = 1
|
||||
const val REPL_SNIPPET_FIRST_GEN = 1
|
||||
@@ -29,3 +30,21 @@ data class ReplSnippetIdImpl(override val no: Int, override val generation: Int,
|
||||
private val serialVersionUID: Long = 1L
|
||||
}
|
||||
}
|
||||
|
||||
interface ReplScriptCompilationConfigurationKeys
|
||||
|
||||
open class ReplScriptCompilationConfigurationBuilder : PropertiesCollection.Builder(),
|
||||
ReplScriptCompilationConfigurationKeys {
|
||||
companion object : ReplScriptCompilationConfigurationKeys
|
||||
}
|
||||
|
||||
val ScriptCompilationConfigurationKeys.repl
|
||||
get() = ReplScriptCompilationConfigurationBuilder()
|
||||
|
||||
|
||||
/**
|
||||
* The prefix of the name of the generated script class field to assign the snipped results to, empty means disabled
|
||||
* see also ScriptCompilationConfigurationKeys.resultField
|
||||
*/
|
||||
val ReplScriptCompilationConfigurationKeys.resultFieldPrefix by PropertiesCollection.key<String>("res")
|
||||
|
||||
|
||||
@@ -99,6 +99,12 @@ val ScriptCompilationConfigurationKeys.defaultImports by PropertiesCollection.ke
|
||||
*/
|
||||
val ScriptCompilationConfigurationKeys.importScripts by PropertiesCollection.key<List<SourceCode>>()
|
||||
|
||||
/**
|
||||
* The name of the generated script class field to assign the script results to, empty means disabled
|
||||
* see also ReplScriptCompilationConfigurationKeys.resultFieldPrefix
|
||||
*/
|
||||
val ScriptCompilationConfigurationKeys.resultField by PropertiesCollection.key<String>("\$\$result")
|
||||
|
||||
/**
|
||||
* The list of script dependencies - platform specific
|
||||
*/
|
||||
|
||||
@@ -121,17 +121,40 @@ data class RefineEvaluationConfigurationData(
|
||||
/**
|
||||
* The script evaluation result value
|
||||
*/
|
||||
sealed class ResultValue {
|
||||
class Value(val name: String, val value: Any?, val type: String, val scriptInstance: Any) : ResultValue() {
|
||||
sealed class ResultValue(val scriptInstance: Any? = null) {
|
||||
|
||||
/**
|
||||
* The result value representing a script return value - the value of the last expression in the script
|
||||
* @param name assigned name of the result field - used e.g. in REPL
|
||||
* @param value actual result value
|
||||
* @param type name of the result type
|
||||
* @param scriptInstance instance of the script class
|
||||
*/
|
||||
class Value(val name: String, val value: Any?, val type: String, scriptInstance: Any) : ResultValue(scriptInstance) {
|
||||
override fun toString(): String = "$name: $type = $value"
|
||||
}
|
||||
|
||||
class UnitValue(val scriptInstance: Any) : ResultValue() {
|
||||
/**
|
||||
* The result value representing unit result, e.g. when the script ends with a statement
|
||||
* @param scriptInstance instance of the script class
|
||||
*/
|
||||
class Unit(scriptInstance: Any) : ResultValue(scriptInstance) {
|
||||
override fun toString(): String = "Unit"
|
||||
}
|
||||
|
||||
// TODO: obsolete it, use differently named value in the saving evaluators
|
||||
object Unit : ResultValue()
|
||||
/**
|
||||
* The result value representing an exception from script itself
|
||||
* @param error the actual exception thrown on script evaluation
|
||||
* @param wrappingException the wrapping exception e.g. InvocationTargetException, sometimes useful for calculating the relevant stacktrace
|
||||
*/
|
||||
class Error(val error: Throwable, val wrappingException: Throwable? = null) : ResultValue() {
|
||||
override fun toString(): String = error.toString()
|
||||
}
|
||||
|
||||
/**
|
||||
* The result value used in non-evaluating "evaluators"
|
||||
*/
|
||||
object NotEvaluated : ResultValue()
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+47
-16
@@ -27,7 +27,7 @@ class ReplTest : TestCase() {
|
||||
@Test
|
||||
fun testCompileAndEval() {
|
||||
val out = captureOut {
|
||||
chechEvaluateInReplNoErrors(
|
||||
chechEvaluateInRepl(
|
||||
simpleScriptompilationConfiguration,
|
||||
simpleScriptEvaluationConfiguration,
|
||||
sequenceOf(
|
||||
@@ -41,6 +41,32 @@ class ReplTest : TestCase() {
|
||||
Assert.assertEquals("x = 3", out)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testEvalWithResult() {
|
||||
chechEvaluateInRepl(
|
||||
simpleScriptompilationConfiguration,
|
||||
simpleScriptEvaluationConfiguration,
|
||||
sequenceOf(
|
||||
"val x = 5",
|
||||
"x + 6",
|
||||
"res1 * 2"
|
||||
),
|
||||
sequenceOf(null, 11, 22)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testEvalWithError() {
|
||||
chechEvaluateInRepl(
|
||||
simpleScriptompilationConfiguration,
|
||||
simpleScriptEvaluationConfiguration,
|
||||
sequenceOf(
|
||||
"throw RuntimeException(\"abc\")"
|
||||
),
|
||||
sequenceOf(RuntimeException("abc"))
|
||||
)
|
||||
}
|
||||
|
||||
fun evaluateInRepl(
|
||||
compilationConfiguration: ScriptCompilationConfiguration,
|
||||
evaluationConfiguration: ScriptEvaluationConfiguration,
|
||||
@@ -62,15 +88,12 @@ class ReplTest : TestCase() {
|
||||
}
|
||||
}
|
||||
.onSuccess {
|
||||
val snippetInstance = when (val retVal = it.returnValue) {
|
||||
is ResultValue.Value -> retVal.scriptInstance
|
||||
is ResultValue.UnitValue -> retVal.scriptInstance
|
||||
else -> throw IllegalStateException("Expecting value with script instance, got $it")
|
||||
}
|
||||
currentEvalConfig = ScriptEvaluationConfiguration(currentEvalConfig) {
|
||||
previousSnippets.append(snippetInstance)
|
||||
jvm {
|
||||
baseClassLoader(snippetInstance::class.java.classLoader)
|
||||
it.returnValue.scriptInstance?.let { snippetInstance ->
|
||||
currentEvalConfig = ScriptEvaluationConfiguration(currentEvalConfig) {
|
||||
previousSnippets.append(snippetInstance)
|
||||
jvm {
|
||||
baseClassLoader(snippetInstance::class.java.classLoader)
|
||||
}
|
||||
}
|
||||
}
|
||||
it.asSuccess()
|
||||
@@ -78,7 +101,7 @@ class ReplTest : TestCase() {
|
||||
}
|
||||
}
|
||||
|
||||
fun chechEvaluateInReplNoErrors(
|
||||
fun chechEvaluateInRepl(
|
||||
compilationConfiguration: ScriptCompilationConfiguration,
|
||||
evaluationConfiguration: ScriptEvaluationConfiguration,
|
||||
snippets: Sequence<String>,
|
||||
@@ -90,11 +113,19 @@ class ReplTest : TestCase() {
|
||||
is ResultWithDiagnostics.Failure -> Assert.fail("#$index: Expected result, got $res")
|
||||
is ResultWithDiagnostics.Success -> {
|
||||
val expectedVal = expectedIter.next()
|
||||
val resVal = res.value.returnValue
|
||||
if (resVal is ResultValue.Value && resVal.type.isNotBlank()) // TODO: the latter check is temporary while the result is used to return the instance too
|
||||
Assert.assertEquals("#$index: Expected $expectedVal, got $resVal", expectedVal, resVal.value)
|
||||
else
|
||||
Assert.assertTrue("#$index: Expected $expectedVal, got Unit", expectedVal == null)
|
||||
when (val resVal = res.value.returnValue) {
|
||||
is ResultValue.Value -> Assert.assertEquals(
|
||||
"#$index: Expected $expectedVal, got $resVal",
|
||||
expectedVal,
|
||||
resVal.value
|
||||
)
|
||||
is ResultValue.Unit -> Assert.assertTrue("#$index: Expected $expectedVal, got Unit", expectedVal == null)
|
||||
is ResultValue.Error -> Assert.assertTrue(
|
||||
"#$index: Expected $expectedVal, got Error: ${resVal.error}",
|
||||
expectedVal is Throwable && expectedVal.message == resVal.error.message
|
||||
)
|
||||
else -> Assert.assertTrue("#$index: Expected $expectedVal, got unknown result $resVal", expectedVal == null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+45
-1
@@ -15,6 +15,7 @@ import org.jetbrains.org.objectweb.asm.Opcodes
|
||||
import org.junit.Assert
|
||||
import org.junit.Test
|
||||
import java.io.*
|
||||
import java.lang.RuntimeException
|
||||
import java.net.URLClassLoader
|
||||
import java.nio.file.Files
|
||||
import java.security.MessageDigest
|
||||
@@ -51,6 +52,41 @@ class ScriptingHostTest : TestCase() {
|
||||
Assert.assertEquals(greeting, output2)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testValueResult() {
|
||||
val resVal = evalScriptWithResult("42") as ResultValue.Value
|
||||
Assert.assertEquals(42, resVal.value)
|
||||
Assert.assertEquals("\$\$result", resVal.name)
|
||||
Assert.assertEquals("kotlin.Int", resVal.type)
|
||||
val resField = resVal.scriptInstance!!::class.java.getDeclaredField("\$\$result")
|
||||
Assert.assertEquals(42, resField.get(resVal.scriptInstance!!))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testUnitResult() {
|
||||
val resVal = evalScriptWithResult("val x = 42")
|
||||
Assert.assertTrue(resVal is ResultValue.Unit)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testErrorResult() {
|
||||
val resVal = evalScriptWithResult("throw RuntimeException(\"abc\")")
|
||||
Assert.assertTrue(resVal is ResultValue.Error)
|
||||
val resValError = (resVal as ResultValue.Error).error
|
||||
Assert.assertTrue(resValError is RuntimeException)
|
||||
Assert.assertEquals("abc", resValError.message)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testCustomResultField() {
|
||||
val resVal = evalScriptWithResult("42") {
|
||||
resultField("outcome")
|
||||
} as ResultValue.Value
|
||||
Assert.assertEquals("outcome", resVal.name)
|
||||
val resField = resVal.scriptInstance!!::class.java.getDeclaredField("outcome")
|
||||
Assert.assertEquals(42, resField.get(resVal.scriptInstance!!))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSaveToClasses() {
|
||||
val greeting = "Hello from script classes!"
|
||||
@@ -366,7 +402,8 @@ class ScriptingHostTest : TestCase() {
|
||||
val evaluator = BasicJvmScriptEvaluator()
|
||||
val host = BasicJvmScriptingHost(compiler = compiler, evaluator = evaluator)
|
||||
|
||||
val scriptCompilationConfiguration = createJvmCompilationConfigurationFromTemplate<SimpleScriptTemplate>(body = configurationBuilder)
|
||||
val scriptCompilationConfiguration =
|
||||
createJvmCompilationConfigurationFromTemplate<SimpleScriptTemplate>(body = configurationBuilder)
|
||||
|
||||
Assert.assertEquals(0, cache.storedScripts)
|
||||
var compiledScript: CompiledScript<*>? = null
|
||||
@@ -453,6 +490,13 @@ fun <T> ResultWithDiagnostics<T>.throwOnFailure(): ResultWithDiagnostics<T> = ap
|
||||
private fun evalScript(script: String, host: BasicScriptingHost = BasicJvmScriptingHost()): ResultWithDiagnostics<*> =
|
||||
evalScriptWithConfiguration(script, host)
|
||||
|
||||
private fun evalScriptWithResult(
|
||||
script: String,
|
||||
host: BasicScriptingHost = BasicJvmScriptingHost(),
|
||||
body: ScriptCompilationConfiguration.Builder.() -> Unit = {}
|
||||
): ResultValue =
|
||||
evalScriptWithConfiguration(script, host, body).throwOnFailure().valueOrNull()!!.returnValue
|
||||
|
||||
private fun evalScriptWithConfiguration(
|
||||
script: String,
|
||||
host: BasicScriptingHost = BasicJvmScriptingHost(),
|
||||
|
||||
+2
-2
@@ -41,7 +41,7 @@ open class BasicJvmScriptClassFilesGenerator(val outputDir: File) : ScriptEvalua
|
||||
writeBytes(bytes)
|
||||
}
|
||||
}
|
||||
return ResultWithDiagnostics.Success(EvaluationResult(ResultValue.Unit, scriptEvaluationConfiguration))
|
||||
return ResultWithDiagnostics.Success(EvaluationResult(ResultValue.NotEvaluated, scriptEvaluationConfiguration))
|
||||
} catch (e: Throwable) {
|
||||
return ResultWithDiagnostics.Failure(
|
||||
e.asDiagnostics("Cannot generate script classes: ${e.message}", path = compiledScript.sourceLocationId)
|
||||
@@ -96,7 +96,7 @@ open class BasicJvmScriptJarGenerator(val outputJar: File) : ScriptEvaluator {
|
||||
if (compiledScript !is KJvmCompiledScript<*>)
|
||||
return failure("Cannot generate jar: unsupported compiled script type $compiledScript")
|
||||
compiledScript.saveToJar(outputJar)
|
||||
return ResultWithDiagnostics.Success(EvaluationResult(ResultValue.Unit, scriptEvaluationConfiguration))
|
||||
return ResultWithDiagnostics.Success(EvaluationResult(ResultValue.NotEvaluated, scriptEvaluationConfiguration))
|
||||
} catch (e: Throwable) {
|
||||
return ResultWithDiagnostics.Failure(
|
||||
e.asDiagnostics("Cannot generate script jar: ${e.message}", path = compiledScript.sourceLocationId)
|
||||
|
||||
+15
-11
@@ -56,20 +56,24 @@ class JvmReplEvaluator(
|
||||
val res = runBlocking { scriptEvaluator(compiledScript, currentConfiguration) }
|
||||
|
||||
when (res) {
|
||||
is ResultWithDiagnostics.Success -> when (val retVal = res.value.returnValue) {
|
||||
is ResultValue.Value -> {
|
||||
history.replaceOrPush(compileResult.lineId, retVal.scriptInstance)
|
||||
// TODO: the latter check is temporary while the result is used to return the instance too
|
||||
if (retVal.type.isNotBlank())
|
||||
is ResultWithDiagnostics.Success -> {
|
||||
when (val retVal = res.value.returnValue) {
|
||||
is ResultValue.Error -> {
|
||||
ReplEvalResult.Error.Runtime(
|
||||
retVal.error.message ?: "unknown error",
|
||||
(retVal.error as? Exception) ?: (retVal.wrappingException as? Exception)
|
||||
)
|
||||
}
|
||||
is ResultValue.Value -> {
|
||||
history.replaceOrPush(compileResult.lineId, retVal.scriptInstance!!)
|
||||
ReplEvalResult.ValueResult(retVal.name, retVal.value, retVal.type)
|
||||
else
|
||||
}
|
||||
is ResultValue.Unit -> {
|
||||
history.replaceOrPush(compileResult.lineId, retVal.scriptInstance!!)
|
||||
ReplEvalResult.UnitResult()
|
||||
}
|
||||
else -> throw IllegalStateException("Unexpected snippet result value $retVal")
|
||||
}
|
||||
is ResultValue.UnitValue -> {
|
||||
history.replaceOrPush(compileResult.lineId, retVal.scriptInstance)
|
||||
ReplEvalResult.UnitResult()
|
||||
}
|
||||
else -> throw IllegalStateException("Expecting value with script instance, got $retVal")
|
||||
}
|
||||
else ->
|
||||
ReplEvalResult.Error.Runtime(
|
||||
|
||||
+15
-13
@@ -5,6 +5,7 @@
|
||||
|
||||
package kotlin.script.experimental.jvm
|
||||
|
||||
import java.lang.reflect.InvocationTargetException
|
||||
import kotlin.reflect.KClass
|
||||
import kotlin.script.experimental.api.*
|
||||
import kotlin.script.experimental.jvm.impl.getConfigurationWithClassloader
|
||||
@@ -43,13 +44,18 @@ open class BasicJvmScriptEvaluator : ScriptEvaluator {
|
||||
?.valueOrNull()
|
||||
?: configuration
|
||||
|
||||
val instance =
|
||||
scriptClass.evalWithConfigAndOtherScriptsResults(refinedEvalConfiguration, importedScriptsEvalResults)
|
||||
val resultValue = try {
|
||||
val instance =
|
||||
scriptClass.evalWithConfigAndOtherScriptsResults(refinedEvalConfiguration, importedScriptsEvalResults)
|
||||
|
||||
val resultValue = compiledScript.resultField?.let { (resultFieldName, resultType) ->
|
||||
val resultField = scriptClass.java.getDeclaredField(resultFieldName).apply { isAccessible = true }
|
||||
ResultValue.Value(resultFieldName, resultField.get(instance), resultType.typeName, instance)
|
||||
} ?: ResultValue.Value("", instance, "", instance)
|
||||
compiledScript.resultField?.let { (resultFieldName, resultType) ->
|
||||
val resultField = scriptClass.java.getDeclaredField(resultFieldName).apply { isAccessible = true }
|
||||
ResultValue.Value(resultFieldName, resultField.get(instance), resultType.typeName, instance)
|
||||
} ?: ResultValue.Unit(instance)
|
||||
|
||||
} catch (e: InvocationTargetException) {
|
||||
ResultValue.Error(e.targetException ?: e, e)
|
||||
}
|
||||
|
||||
EvaluationResult(resultValue, refinedEvalConfiguration).let {
|
||||
sharedScripts?.put(scriptClass, it)
|
||||
@@ -59,10 +65,7 @@ open class BasicJvmScriptEvaluator : ScriptEvaluator {
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
ResultWithDiagnostics.Failure(
|
||||
e.asDiagnostics(
|
||||
"Error evaluating script",
|
||||
path = compiledScript.sourceLocationId
|
||||
)
|
||||
e.asDiagnostics(path = compiledScript.sourceLocationId)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -89,13 +92,12 @@ open class BasicJvmScriptEvaluator : ScriptEvaluator {
|
||||
}
|
||||
|
||||
importedScriptsEvalResults.forEach {
|
||||
args.add((it.returnValue as ResultValue.Value).scriptInstance)
|
||||
args.add(it.returnValue.scriptInstance)
|
||||
}
|
||||
|
||||
val ctor = java.constructors.single()
|
||||
val instance = ctor.newInstance(*args.toArray())
|
||||
|
||||
return instance
|
||||
return ctor.newInstance(*args.toArray())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+10
-9
@@ -77,13 +77,16 @@ open class GenericReplCompiler(
|
||||
val analysisResult = compilerState.analyzerEngine.analyzeReplLine(psiFile, codeLine)
|
||||
AnalyzerWithCompilerReport.reportDiagnostics(analysisResult.diagnostics, errorHolder)
|
||||
val scriptDescriptor = when (analysisResult) {
|
||||
is ReplCodeAnalyzer.ReplLineAnalysisResult.WithErrors -> return ReplCompileResult.Error(errorHolder.renderMessage())
|
||||
is ReplCodeAnalyzer.ReplLineAnalysisResult.Successful -> analysisResult.scriptDescriptor
|
||||
is ReplCodeAnalyzer.ReplLineAnalysisResult.WithErrors -> {
|
||||
return ReplCompileResult.Error(errorHolder.renderMessage())
|
||||
}
|
||||
is ReplCodeAnalyzer.ReplLineAnalysisResult.Successful -> {
|
||||
(analysisResult.scriptDescriptor as? ScriptDescriptor)
|
||||
?: error("Unexpected script descriptor type ${analysisResult.scriptDescriptor::class}")
|
||||
}
|
||||
else -> error("Unexpected result ${analysisResult::class.java}")
|
||||
}
|
||||
|
||||
val type = (scriptDescriptor as ScriptDescriptor).resultValue?.returnType
|
||||
|
||||
val generationState = GenerationState.Builder(
|
||||
psiFile.project,
|
||||
ClassBuilderFactories.BINARIES,
|
||||
@@ -93,9 +96,7 @@ open class GenericReplCompiler(
|
||||
compilerConfiguration
|
||||
).build()
|
||||
|
||||
generationState.replSpecific.resultType = type
|
||||
generationState.replSpecific.scriptResultFieldName = scriptResultFieldName(codeLine.no)
|
||||
generationState.replSpecific.earlierScriptsForReplInterpreter = compilerState.history.map { it.item }
|
||||
generationState.scriptSpecific.earlierScriptsForReplInterpreter = compilerState.history.map { it.item }
|
||||
generationState.beforeCompile()
|
||||
KotlinCodegenFacade.generatePackage(
|
||||
generationState,
|
||||
@@ -114,9 +115,9 @@ open class GenericReplCompiler(
|
||||
compilerState.history.map { it.id },
|
||||
generatedClassname,
|
||||
classes,
|
||||
generationState.replSpecific.hasResult,
|
||||
generationState.scriptSpecific.resultFieldName != null,
|
||||
classpathAddendum ?: emptyList(),
|
||||
generationState.replSpecific.resultType?.let {
|
||||
generationState.scriptSpecific.resultType?.let {
|
||||
DescriptorRenderer.FQ_NAMES_IN_TYPES.renderType(it)
|
||||
},
|
||||
null
|
||||
|
||||
+16
-7
@@ -90,14 +90,23 @@ class LazyScriptDescriptor(
|
||||
}
|
||||
|
||||
fun resultFieldName(): String? {
|
||||
val scriptPriority = scriptInfo.script.getUserData(ScriptPriorities.PRIORITY_KEY)
|
||||
if (scriptPriority != null) {
|
||||
return "res$scriptPriority"
|
||||
// TODO: implement robust REPL/script selection
|
||||
val replSnippetId =
|
||||
scriptInfo.script.getUserData(ScriptPriorities.PRIORITY_KEY)?.toString()
|
||||
?: run {
|
||||
val scriptName = name.asString()
|
||||
if (scriptName.startsWith("Line_"))
|
||||
scriptName.split("_")[1]
|
||||
else null
|
||||
}
|
||||
return if (replSnippetId != null) {
|
||||
// assuming repl
|
||||
scriptCompilationConfiguration()[ScriptCompilationConfiguration.repl.resultFieldPrefix]?.takeIf { it.isNotBlank() }?.let {
|
||||
"$it$replSnippetId"
|
||||
}
|
||||
} else {
|
||||
scriptCompilationConfiguration()[ScriptCompilationConfiguration.resultField]?.takeIf { it.isNotBlank() }
|
||||
}
|
||||
val scriptName = name.asString()
|
||||
return if (scriptName.startsWith("Line_")) {
|
||||
"res${scriptName.split("_")[1]}"
|
||||
} else "\$\$result"
|
||||
}
|
||||
|
||||
private val sourceElement = scriptInfo.script.toSourceElement()
|
||||
|
||||
@@ -51,6 +51,8 @@ runtimeJar()
|
||||
sourcesJar()
|
||||
javadocJar()
|
||||
|
||||
testsJar()
|
||||
|
||||
projectTest {
|
||||
workingDir = rootDir
|
||||
}
|
||||
|
||||
+52
-13
@@ -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])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+9
-7
@@ -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(
|
||||
|
||||
+3
-4
@@ -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
|
||||
+76
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user