Skip to content

Commit be154d8

Browse files
reid-spencerclaude
andcommitted
Fix parse error location for missing syntax at end of line
When trailing syntax is missing (e.g., `type Money is Currency` instead of `Currency(USD)`), fastparse reports the error at the next token's position rather than where the expected token was missing. Added adjustFailureIndex() that walks backward past whitespace and adjusts the location when it crosses a newline boundary, so errors point to the end of the offending line. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 9783208 commit be154d8

2 files changed

Lines changed: 51 additions & 1 deletion

File tree

language/shared/src/main/scala/com/ossuminc/riddl/language/parsing/ParsingErrors.scala

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,31 @@ trait ParsingErrors {
6464
.mkString("(", " | ", ")")
6565
}
6666

67+
/** When a parse failure occurs, the failure index points to where
68+
* fastparse found the unexpected token (the "next" token). For
69+
* better UX, we want to point to where the expected token was
70+
* missing — just after the last successfully consumed token.
71+
* Only adjust if the next token is on a different line (crossing
72+
* a newline), which is the case that confuses users the most.
73+
*/
74+
private def adjustFailureIndex(index: Int, input: RiddlParserInput): Int =
75+
val data = input.data
76+
if index <= 0 || index > data.length then index
77+
else
78+
var i = index - 1
79+
// Walk backward past whitespace
80+
while i >= 0 && data(i).isWhitespace do i -= 1
81+
// Only adjust if we crossed a newline
82+
if i >= 0 && input.lineOf(i) < input.lineOf(index.min(data.length - 1)) then
83+
i + 1 // Just after last non-whitespace char
84+
else
85+
index // Same line — keep original
86+
end if
87+
end adjustFailureIndex
88+
6789
def makeParseFailureError(failure: Failure, input: RiddlParserInput): Unit = {
68-
val location = input.location(failure.index)
90+
val adjustedIndex = adjustFailureIndex(failure.index, input)
91+
val location = input.location(adjustedIndex)
6992
val trace = failure.trace()
7093
val msg = trace.terminals.value.size match {
7194
case 0 => "Expected nothing"

language/shared/src/test/scala/com/ossuminc/riddl/language/parsing/CommonParserTest.scala

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,33 @@ abstract class CommonParserTest(using PlatformContext) extends AbstractParsingTe
133133
}
134134
}
135135
}
136+
"ParseErrorLocation" should {
137+
"report error at end of offending line, not start of next line" in { (td: TestData) =>
138+
// "type Money is Currency" is missing the type arguments e.g. Currency(USD)
139+
// The error should point to line 2 (where "Currency" ends), not line 3
140+
val input = RiddlParserInput(
141+
"domain foo is {\n type Money is Currency\n}\n",
142+
td
143+
)
144+
parseTopLevelDomains(input) match
145+
case Right(_) => fail("Expected parse failure but parsing succeeded")
146+
case Left(messages) =>
147+
val errors = messages.justErrors
148+
errors must not be empty
149+
// The error about the incomplete type should be on line 2
150+
// (where "Currency" is), not line 3 (where "}" is)
151+
val errorOnLine2 = errors.exists(_.loc.line == 2)
152+
val errorOnLine3Only = errors.forall(_.loc.line == 3)
153+
if errorOnLine3Only then
154+
fail(
155+
s"Error incorrectly reported on line 3 instead of line 2: " +
156+
errors.map(m => s"${m.loc.line}:${m.loc.col} ${m.message}").mkString("; ")
157+
)
158+
end if
159+
errorOnLine2 mustBe true
160+
}
161+
}
162+
136163
"NoWhiteSpaceParsers" should {
137164
"handle a URL" in { (td: TestData) =>
138165
val input = RiddlParserInput("https://www.wordnik.com/words/phi", td)

0 commit comments

Comments
 (0)