Skip to content

Commit de1846d

Browse files
committed
Initial attempt to fix the error cases
1 parent 9067d75 commit de1846d

File tree

3 files changed

+83
-29
lines changed

3 files changed

+83
-29
lines changed
Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,45 @@
11
package io.github.json5.kotlin
22

33
/**
4-
* Exception thrown when there's an error parsing JSON5 data.
4+
* Exception thrown when there's an error parsing JSON5 input.
55
*/
66
class JSON5Exception(
77
message: String,
8-
val lineNumber: Int = -1,
9-
val columnNumber: Int = -1
10-
) : RuntimeException(message) {
8+
val lineNumber: Int,
9+
val columnNumber: Int
10+
) : RuntimeException("JSON5: $message at line $lineNumber, column $columnNumber") {
1111

12-
override val message: String
13-
get() = if (lineNumber > 0) {
14-
"${super.message} at line $lineNumber, column $columnNumber"
15-
} else {
16-
super.message ?: ""
12+
companion object {
13+
/**
14+
* Format a character for error messages, with special handling for control characters.
15+
*/
16+
fun formatChar(c: Char): String {
17+
return when {
18+
c.isISOControl() -> "\\x" + c.code.toString(16).padStart(2, '0')
19+
else -> c.toString()
20+
}
1721
}
22+
23+
/**
24+
* Create an exception for an invalid character in the input.
25+
*/
26+
fun invalidChar(c: Char, line: Int, col: Int): JSON5Exception {
27+
val charStr = formatChar(c)
28+
return JSON5Exception("invalid character '$charStr'", line, col)
29+
}
30+
31+
/**
32+
* Create an exception for invalid end of input.
33+
*/
34+
fun invalidEndOfInput(line: Int, col: Int): JSON5Exception {
35+
return JSON5Exception("invalid end of input", line, col)
36+
}
37+
38+
/**
39+
* Create an exception for invalid identifier character.
40+
*/
41+
fun invalidIdentifierChar(line: Int, col: Int): JSON5Exception {
42+
return JSON5Exception("invalid identifier character", line, col)
43+
}
44+
}
1845
}

lib/src/main/kotlin/io/github/json5/kotlin/JSON5Lexer.kt

Lines changed: 45 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -67,22 +67,22 @@ class JSON5Lexer(private val source: String) {
6767
advance() // Skip the backslash
6868

6969
if (currentChar != 'u') {
70-
throw JSON5Exception("Expected 'u' after backslash in identifier", line, column)
70+
throw JSON5Exception.invalidChar(currentChar ?: ' ', line, column)
7171
}
7272

73-
advance() // Skip 'u'
73+
advance() // Skip the 'u'
7474
val hexDigits = StringBuilder()
7575
repeat(4) {
7676
if (currentChar == null || !currentChar!!.isHexDigit()) {
77-
throw JSON5Exception("Invalid hex escape sequence in identifier", line, column)
77+
throw JSON5Exception.invalidChar(currentChar ?: ' ', line, column)
7878
}
7979
hexDigits.append(currentChar)
8080
advance()
8181
}
8282

8383
val char = hexDigits.toString().toInt(16).toChar()
8484
if (!isIdentifierStart(char)) {
85-
throw JSON5Exception("Invalid identifier character", line, startColumn)
85+
throw JSON5Exception.invalidIdentifierChar(line, startColumn)
8686
}
8787

8888
val buffer = StringBuilder().append(char)
@@ -92,14 +92,14 @@ class JSON5Lexer(private val source: String) {
9292
if (currentChar == '\\') {
9393
advance() // Skip backslash
9494
if (currentChar != 'u') {
95-
throw JSON5Exception("Expected 'u' after backslash in identifier", line, column)
95+
throw JSON5Exception.invalidChar(currentChar ?: ' ', line, column)
9696
}
9797
advance() // Skip 'u'
9898

9999
val identHexDigits = StringBuilder()
100100
repeat(4) {
101101
if (currentChar == null || !currentChar!!.isHexDigit()) {
102-
throw JSON5Exception("Invalid hex escape sequence in identifier", line, column)
102+
throw JSON5Exception.invalidChar(currentChar ?: ' ', line, column)
103103
}
104104
identHexDigits.append(currentChar)
105105
advance()
@@ -116,11 +116,18 @@ class JSON5Lexer(private val source: String) {
116116

117117
return Token.IdentifierToken(buffer.toString(), line, startColumn)
118118
}
119+
'/' -> {
120+
// Handle incomplete comments
121+
if (peek() == null) {
122+
throw JSON5Exception.invalidChar('/', line, column)
123+
}
124+
throw JSON5Exception.invalidChar(peek() ?: ' ', line, column + 1)
125+
}
119126
else -> {
120127
if (isIdentifierStart(currentChar)) {
121128
readIdentifier()
122129
} else {
123-
throw JSON5Exception("Unexpected character: $currentChar", line, column)
130+
throw JSON5Exception.invalidChar(currentChar ?: ' ', line, column)
124131
}
125132
}
126133
}
@@ -200,6 +207,7 @@ class JSON5Lexer(private val source: String) {
200207

201208
private fun readString(): Token.StringToken {
202209
val startColumn = column
210+
val startLine = line
203211
val quoteChar = currentChar
204212
advance() // Skip the quote character
205213

@@ -216,7 +224,7 @@ class JSON5Lexer(private val source: String) {
216224
advance() // Skip the backslash
217225
buffer.append(readEscapeSequence())
218226
}
219-
'\n', '\r' -> throw JSON5Exception("Unterminated string", line, startColumn)
227+
'\n', '\r' -> throw JSON5Exception.invalidChar(currentChar ?: ' ', line, column)
220228
else -> {
221229
buffer.append(currentChar)
222230
advance()
@@ -225,7 +233,7 @@ class JSON5Lexer(private val source: String) {
225233
}
226234

227235
if (!done) {
228-
throw JSON5Exception("Unterminated string", line, startColumn)
236+
throw JSON5Exception.invalidEndOfInput(startLine, startColumn)
229237
}
230238

231239
return Token.StringToken(buffer.toString(), line, startColumn)
@@ -292,17 +300,33 @@ class JSON5Lexer(private val source: String) {
292300
}
293301
'x' -> {
294302
advance()
295-
return readHexEscape(2)
303+
try {
304+
return readHexEscape(2)
305+
} catch (e: Exception) {
306+
if (currentChar != null) {
307+
throw JSON5Exception.invalidChar(currentChar ?: ' ', line, column)
308+
}
309+
throw JSON5Exception.invalidEndOfInput(line, column)
310+
}
296311
}
297312
'u' -> {
298313
advance()
299-
return readHexEscape(4)
314+
try {
315+
return readHexEscape(4)
316+
} catch (e: Exception) {
317+
if (currentChar != null) {
318+
throw JSON5Exception.invalidChar(currentChar ?: ' ', line, column)
319+
}
320+
throw JSON5Exception.invalidEndOfInput(line, column)
321+
}
300322
}
323+
null -> throw JSON5Exception.invalidEndOfInput(line, column)
324+
in '1'..'9' -> throw JSON5Exception.invalidChar(currentChar ?: ' ', line, column)
301325
else -> {
302326
// Just return the character after the backslash (e.g. for \', \", etc.)
303327
val c = currentChar
304328
advance()
305-
return c ?: throw JSON5Exception("Invalid escape sequence", line, column)
329+
return c ?: throw JSON5Exception.invalidEndOfInput(line, column)
306330
}
307331
}
308332
}
@@ -403,7 +427,7 @@ class JSON5Lexer(private val source: String) {
403427
}
404428

405429
if (!hasDigits) {
406-
throw JSON5Exception("Invalid hexadecimal number", line, column)
430+
throw JSON5Exception.invalidChar(currentChar ?: ' ', line, column)
407431
}
408432

409433
try {
@@ -416,7 +440,7 @@ class JSON5Lexer(private val source: String) {
416440
}
417441
return Token.NumericToken(value, startLine, startColumn)
418442
} catch (e: NumberFormatException) {
419-
throw JSON5Exception("Invalid hexadecimal number: ${buffer}", line, column)
443+
throw JSON5Exception("Invalid hexadecimal number", line, column)
420444
}
421445
}
422446

@@ -464,15 +488,15 @@ class JSON5Lexer(private val source: String) {
464488
}
465489

466490
if (!hasExponentDigits) {
467-
throw JSON5Exception("Invalid exponent in number", line, column)
491+
throw JSON5Exception.invalidChar(currentChar ?: ' ', line, column)
468492
}
469493

470494
hasExponentPart = true
471495
}
472496

473497
// Must have at least one part (integer, fraction, or starts with a decimal point)
474498
if (!(hasIntegerPart || hasFractionPart) || (hasFractionPart && !hasIntegerPart && buffer.length == 1)) {
475-
throw JSON5Exception("Invalid number", line, column)
499+
throw JSON5Exception.invalidChar(currentChar ?: ' ', line, column)
476500
}
477501

478502
val value = buffer.toString().toDouble()
@@ -516,8 +540,11 @@ class JSON5Lexer(private val source: String) {
516540
private fun readHexEscape(digits: Int): Char {
517541
val hexString = StringBuilder()
518542
repeat(digits) {
519-
if (currentChar == null || !currentChar!!.isHexDigit()) {
520-
throw JSON5Exception("Invalid hex escape sequence", line, column)
543+
if (currentChar == null) {
544+
throw JSON5Exception.invalidEndOfInput(line, column)
545+
}
546+
if (!currentChar!!.isHexDigit()) {
547+
throw JSON5Exception.invalidChar(currentChar!!, line, column)
521548
}
522549
hexString.append(currentChar)
523550
advance()

lib/src/main/kotlin/io/github/json5/kotlin/JSON5Serializer.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ internal object JSON5Serializer {
103103

104104
// Check for circular references
105105
if (stack.any { it === obj }) {
106-
throw JSON5Exception("Converting circular structure to JSON5")
106+
throw JSON5Exception("Converting circular structure to JSON5", 0, 0)
107107
}
108108

109109
stack.add(obj)
@@ -175,7 +175,7 @@ internal object JSON5Serializer {
175175

176176
// Check for circular references
177177
if (stack.any { it === array }) {
178-
throw JSON5Exception("Converting circular structure to JSON5")
178+
throw JSON5Exception("Converting circular structure to JSON5", 0, 0)
179179
}
180180

181181
stack.add(array)

0 commit comments

Comments
 (0)