From 72c6dc2bde769740b55ac4e645e18e2bc71fff6d Mon Sep 17 00:00:00 2001 From: sergeych Date: Fri, 21 Nov 2025 17:18:59 +0100 Subject: [PATCH] fixed wrong line report on throw statement --- .../kotlin/net/sergeych/lyng/Compiler.kt | 31 ++++++--- lynglib/src/commonTest/kotlin/ScriptTest.kt | 17 +++++ .../jvmTest/kotlin/ThrowSourcePosJvmTest.kt | 66 +++++++++++++++++++ 3 files changed, 104 insertions(+), 10 deletions(-) create mode 100644 lynglib/src/jvmTest/kotlin/ThrowSourcePosJvmTest.kt diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt index 59fa4d0..6b3bb19 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt @@ -389,7 +389,7 @@ class Compiler( } "throw" -> { - val s = parseThrowStatement() + val s = parseThrowStatement(t.pos) operand = StatementRef(s) } @@ -916,7 +916,7 @@ class Compiler( "class" -> parseClassDeclaration() "enum" -> parseEnumDeclaration() "try" -> parseTryStatement() - "throw" -> parseThrowStatement() + "throw" -> parseThrowStatement(id.pos) "when" -> parseWhenStatement() else -> { // triples @@ -1080,15 +1080,26 @@ class Compiler( } } - private suspend fun parseThrowStatement(): Statement { + private suspend fun parseThrowStatement(start: Pos): Statement { val throwStatement = parseStatement() ?: throw ScriptError(cc.currentPos(), "throw object expected") - return statement { - var errorObject = throwStatement.execute(this) - if (errorObject is ObjString) - errorObject = ObjException(this, errorObject.value) - if (errorObject is ObjException) - raiseError(errorObject) - else raiseError("this is not an exception object: $errorObject") + // Important: bind the created statement to the position of the `throw` keyword so that + // any raised error reports the correct source location. + return statement(start) { sc -> + var errorObject = throwStatement.execute(sc) + // Rebind error scope to the throw-site position so ScriptError.pos is accurate + val throwScope = sc.createChildScope(pos = start) + errorObject = when (errorObject) { + is ObjString -> ObjException(throwScope, errorObject.value) + is ObjException -> ObjException( + errorObject.exceptionClass, + throwScope, + errorObject.message, + errorObject.extraData, + errorObject.useStackTrace + ) + else -> throwScope.raiseError("this is not an exception object: $errorObject") + } + throwScope.raiseError(errorObject as ObjException) } } diff --git a/lynglib/src/commonTest/kotlin/ScriptTest.kt b/lynglib/src/commonTest/kotlin/ScriptTest.kt index 6a9e50f..a7892dd 100644 --- a/lynglib/src/commonTest/kotlin/ScriptTest.kt +++ b/lynglib/src/commonTest/kotlin/ScriptTest.kt @@ -3519,4 +3519,21 @@ class ScriptTest { """.trimIndent()).toString()) } + @Test + fun testThrowReportsSource() = runTest { + try { + eval( + """ + // line 1 + // line 2 + throw "the test" + """.trimIndent() + ) + } catch (se: ScriptError) { + println(se.message) + // Pos.line is zero-based + assertEquals(2, se.pos.line) + } + } + } diff --git a/lynglib/src/jvmTest/kotlin/ThrowSourcePosJvmTest.kt b/lynglib/src/jvmTest/kotlin/ThrowSourcePosJvmTest.kt new file mode 100644 index 0000000..b166618 --- /dev/null +++ b/lynglib/src/jvmTest/kotlin/ThrowSourcePosJvmTest.kt @@ -0,0 +1,66 @@ +/* + * Copyright 2025 Sergey S. Chernov + */ + +import kotlinx.coroutines.runBlocking +import net.sergeych.lyng.Scope +import net.sergeych.lyng.ScriptError +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.fail + +class ThrowSourcePosJvmTest { + + private fun assertThrowLine(code: String, expectedLine: Int) { + try { + runBlocking { Scope().eval(code) } + fail("Expected ScriptError to be thrown, but nothing was thrown") + } catch (se: ScriptError) { + println(se.message) + assertEquals(expectedLine, se.pos.line) + } + } + + @Test + fun simpleThrow_afterComments_reportsCorrectLine() { + val code = """ + // line 1 + // line 2 + throw "simple" + """.trimIndent() + // zero-based line index + assertThrowLine(code, 2) + } + + @Test + fun inlineThrow_withLeadingSpaces_reportsCorrectLine() { + val code = """ + val x = 1 + throw "boom" + """.trimIndent() + // throw is on the 2nd line (zero-based index 1) + assertThrowLine(code, 1) + } + + @Test + fun throwInsideBlock_reportsCorrectLine() { + val code = """ + if( true ) { + // comment + throw "boom" + } + """.trimIndent() + // throw is on the 3rd line of the snippet (zero-based index 2) + assertThrowLine(code, 2) + } + + @Test + fun throwAsExpression_reportsCorrectLine() { + val code = """ + val x = null + val y = x ?: throw "npe-like" + """.trimIndent() + // throw is on the 2nd line (zero-based index 1) + assertThrowLine(code, 1) + } +}