Skip to content

Commit 31231c3

Browse files
committed
Merge branch 'error-handling'
2 parents 749217b + dd8d137 commit 31231c3

File tree

7 files changed

+313
-62
lines changed

7 files changed

+313
-62
lines changed

src/main/kotlin/Error.kt

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,29 @@
11
package net.quickwrite
22

3-
abstract class JSONParseException(content: String) : Exception(content)
3+
import net.quickwrite.lexer.JSONPositionData
44

5-
class JSONLexerException(content: String) : JSONParseException(content)
5+
abstract class JSONParseException(message: String, private val position: JSONPositionData?) : Exception(message) {
6+
override fun getLocalizedMessage(): String {
7+
if (position == null) {
8+
return "error: " + message!!
9+
}
610

7-
class JSONParserException(content: String) : JSONParseException(content)
11+
val line = position.getLine()
12+
val lineNumber = position.lineNumber().toString()
13+
val linePosition = position.linePosition()
14+
15+
val length = position.getLength()
16+
17+
return """
18+
error: $lineNumber:$linePosition $message
19+
$lineNumber | $line
20+
${" ".repeat(lineNumber.length)} | ${" ".repeat(linePosition)}${"^".repeat(length)}
21+
""".trimIndent()
22+
}
23+
}
24+
25+
class JSONLexerException(message: String, position: JSONPositionData)
26+
: JSONParseException(message, position)
27+
28+
class JSONParserException(message: String, position: JSONPositionData?)
29+
: JSONParseException(message, position)

src/main/kotlin/JSONParser.kt

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,15 @@ import net.quickwrite.lexer.JSONLexemeType.*
55
import net.quickwrite.lexer.StringJSONLexer
66
import java.math.BigDecimal
77
import java.util.Stack
8+
import kotlin.jvm.Throws
89

910
private enum class State {
1011
START,
1112
OBJECT,
1213
ARRAY,
1314
}
1415

16+
@Throws(JSONParseException::class)
1517
fun jsonParse(input: String): Any? {
1618
val lexer = StringJSONLexer(input)
1719

@@ -47,7 +49,7 @@ fun jsonParse(input: String): Any? {
4749
continue
4850
}
4951
EOF -> error("Unreachable")
50-
else -> throw JSONParserException("Invalid Token") // TODO: Better error reporting
52+
else -> throw JSONParserException("Invalid Token", lexer.getPosition(token.position, token.length))
5153
}
5254
break
5355
}
@@ -82,22 +84,22 @@ fun jsonParse(input: String): Any? {
8284

8385
if(map.isNotEmpty()) {
8486
if (token.type != COMMA) {
85-
throw JSONParserException("Expected a ',' or a '}', but got ${token.type}") // TODO: Better error reporting
87+
throw JSONParserException("Expected a ',' or a '}', but got ${token.type}", lexer.getPosition(token.position, token.length))
8688
}
8789

8890
token = lexer.getNext()
8991
}
9092

9193
if (token.type != STRING) {
92-
throw JSONParserException("Expected an entry or a '}', but got ${token.type}") // TODO: Better error reporting
94+
throw JSONParserException("Expected an entry or a '}', but got ${token.type}", lexer.getPosition(token.position, token.length))
9395
}
9496

9597
val identifier = token.content!!
9698

9799
token = lexer.getNext()
98100

99101
if (token.type != COLON) {
100-
throw JSONParserException("Expected ':' (COLON), but got ${token.type}") // TODO: Better error reporting
102+
throw JSONParserException("Expected ':' (COLON), but got ${token.type}", lexer.getPosition(token.position, token.length))
101103
}
102104

103105
token = lexer.getNext()
@@ -121,7 +123,10 @@ fun jsonParse(input: String): Any? {
121123
continue
122124
}
123125
EOF -> break
124-
else -> throw JSONParserException("Invalid Token") // TODO: Better error reporting
126+
COMMA -> throw JSONParserException("Expected a value, but got a ','", lexer.getPosition(token.position))
127+
COLON -> throw JSONParserException("Expected a value, but got a ','", lexer.getPosition(token.position))
128+
SQUARE_CLOSE -> throw JSONParserException("Object got closed too early. Expected a value, but got '}'", lexer.getPosition(token.position))
129+
else -> throw JSONParserException("Invalid Token", lexer.getPosition(token.position, token.length))
125130
}
126131
}
127132
State.ARRAY -> {
@@ -155,7 +160,7 @@ fun jsonParse(input: String): Any? {
155160

156161
if (array.isNotEmpty()) {
157162
if (token.type != COMMA) {
158-
throw JSONParserException("Expected a ',' or a ']', but got ${token.type}") // TODO: Better error reporting
163+
throw JSONParserException("Expected a ',' or a ']', but got ${token.type}", lexer.getPosition(token.position, token.length))
159164
}
160165

161166
token = lexer.getNext()
@@ -178,26 +183,30 @@ fun jsonParse(input: String): Any? {
178183
continue
179184
}
180185
EOF -> break
181-
else -> throw JSONParserException("Invalid Token") // TODO: Better error reporting
186+
COMMA -> throw JSONParserException("Expected a value, but got a ','", lexer.getPosition(token.position))
187+
COLON -> throw JSONParserException("Expected a value, but got a ','", lexer.getPosition(token.position))
188+
SQUARE_CLOSE -> throw JSONParserException("List got closed too early. Expected a value, but got ']'", lexer.getPosition(token.position))
189+
else -> throw JSONParserException("Invalid Token", lexer.getPosition(token.position, token.length))
182190
}
183191
}
184192
}
185193
}
186194

187195
if (stack.empty()) {
188-
throw JSONParserException("JSON cannot be empty")
196+
throw JSONParserException("JSON cannot be empty", null)
189197
}
190198

199+
token = lexer.getNext()
191200
// Too much JSON
192-
if (lexer.getNext().type != EOF) {
193-
throw JSONParserException("Invalid JSON") // TODO: Better error handling
201+
if (token.type != EOF) {
202+
throw JSONParserException("Invalid JSON", lexer.getPosition(token.position, token.length))
194203
}
195204

196205
val result = stack.pop()
197206

198207
// Too little JSON
199208
if (!stack.empty()) {
200-
throw JSONParserException("Invalid JSON") // TODO: Better error handling
209+
throw JSONParserException("JSON ended before it could be completely parsed", null)
201210
}
202211

203212
return result

src/main/kotlin/lexer/JSONLexer.kt

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
package net.quickwrite.lexer
22

3+
import net.quickwrite.JSONLexerException
4+
import kotlin.jvm.Throws
5+
36
enum class JSONLexemeType
47
{
58
NULL,
@@ -16,8 +19,23 @@ enum class JSONLexemeType
1619
EOF
1720
}
1821

19-
data class JSONLexeme(val type: JSONLexemeType, val content: String? = null)
22+
data class JSONLexeme(
23+
val type: JSONLexemeType,
24+
val position: Int,
25+
val length: Int = 1,
26+
val content: String? = null
27+
)
2028

2129
interface JSONLexer {
30+
@Throws(JSONLexerException::class)
2231
fun getNext(): JSONLexeme
32+
33+
fun getPosition(position: Int, length: Int = 1): JSONPositionData
34+
}
35+
36+
interface JSONPositionData {
37+
fun lineNumber(): Int
38+
fun linePosition(): Int
39+
fun getLength(): Int
40+
fun getLine(): String
2341
}

src/main/kotlin/lexer/StringJSONLexer.kt

Lines changed: 94 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,20 @@
11
package net.quickwrite.lexer
22

33
import net.quickwrite.JSONLexerException
4+
import kotlin.jvm.Throws
45

56
class StringJSONLexer(private val content: CharSequence) : JSONLexer {
67
private var position = 0
78

9+
@Throws(JSONLexerException::class)
810
override fun getNext(): JSONLexeme {
911
skipWhitespace()
1012

1113
if(position >= content.length) {
12-
return JSONLexeme(JSONLexemeType.EOF)
14+
return JSONLexeme(JSONLexemeType.EOF, position)
1315
}
1416

15-
return when(content.getChar(position)) {
17+
return when(val char = content.getChar(position)) {
1618
'{' -> charLexeme(JSONLexemeType.CURLY_OPEN)
1719
'}' -> charLexeme(JSONLexemeType.CURLY_CLOSE)
1820
'[' -> charLexeme(JSONLexemeType.SQUARE_OPEN)
@@ -28,26 +30,45 @@ class StringJSONLexer(private val content: CharSequence) : JSONLexer {
2830
't' -> parseLiteral("true", JSONLexemeType.TRUE)
2931
'f' -> parseLiteral("false", JSONLexemeType.FALSE)
3032
'n' -> parseLiteral("null", JSONLexemeType.NULL)
31-
else -> throw JSONLexerException("Invalid token")
33+
else -> throw JSONLexerException("Did not recognize '$char' as a valid token character", getPosition(position))
3234
}
3335
}
3436

3537
private fun parseLiteral(input: String, type: JSONLexemeType): JSONLexeme {
36-
for (i in input) {
37-
if(content.getChar(position) != i) {
38-
throw JSONLexerException("Expected '$input', but got malformed input")
38+
fun captureRestToken() {
39+
// Try to capture the rest of the token
40+
while(!content.getChar(position).isWhitespace() && content.getChar(position) != 0.toChar()) {
41+
position++
42+
}
43+
}
44+
45+
for (i in 0..< input.length) {
46+
if(content.getChar(position) != input[i]) {
47+
val start = position - i
48+
captureRestToken()
49+
50+
throw JSONLexerException("Expected '$input', but got malformed input", getPosition(start, position - start))
3951
}
4052
position++
4153
}
42-
return JSONLexeme(type)
54+
55+
if (content.getChar(position).isLetter()) {
56+
val start = position - input.length
57+
captureRestToken()
58+
59+
throw JSONLexerException("Expected '$input', but got malformed input", getPosition(start, position - start))
60+
}
61+
62+
return JSONLexeme(type, position - input.length, input.length)
4363
}
4464

4565
private fun charLexeme(type: JSONLexemeType): JSONLexeme {
4666
position++
47-
return JSONLexeme(type)
67+
return JSONLexeme(type, position - 1)
4868
}
4969

5070
private fun parseString(): JSONLexeme {
71+
val start = position
5172
position++
5273

5374
val builder = StringBuilder()
@@ -65,7 +86,8 @@ class StringJSONLexer(private val content: CharSequence) : JSONLexer {
6586
'r' -> '\r'
6687
't' -> '\t'
6788
'u' -> parseUDigitNumber()
68-
else -> throw JSONLexerException("The character $char cannot be escaped") // TODO: Better error handling
89+
0.toChar() -> throw JSONLexerException("String wasn't correctly terminated", getPosition(content.length - 1))
90+
else -> throw JSONLexerException("The character $char cannot be escaped", getPosition(position - 1, 2))
6991
})
7092
position++
7193
continue
@@ -76,16 +98,17 @@ class StringJSONLexer(private val content: CharSequence) : JSONLexer {
7698
}
7799

78100
if (position >= content.length) {
79-
throw JSONLexerException("String wasn't terminated") // TODO: Better error handling
101+
throw JSONLexerException("String wasn't correctly terminated", getPosition(content.length - 1))
80102
}
81103

82104
position++
83105

84-
return JSONLexeme(JSONLexemeType.STRING, builder.toString())
106+
return JSONLexeme(JSONLexemeType.STRING, start, position - start, builder.toString())
85107
}
86108

87109
private fun parseUDigitNumber(): Char {
88110
var number = 0
111+
val start = position
89112

90113
for (i in 3 downTo 0) {
91114
position++
@@ -109,7 +132,10 @@ class StringJSONLexer(private val content: CharSequence) : JSONLexer {
109132
continue
110133
}
111134

112-
throw JSONLexerException("An invalid hex digit has been provided. Expected 0-9A-Fa-f, but got '$char'") // TODO: Better error handling
135+
throw JSONLexerException(
136+
"An invalid hex digit has been provided. Expected 0-9A-Fa-f, but got '$char'",
137+
getPosition(start - 1, position - start + 1)
138+
)
113139
}
114140

115141
return number.toChar()
@@ -122,10 +148,13 @@ class StringJSONLexer(private val content: CharSequence) : JSONLexer {
122148

123149
if (content.getChar(position) == '.') {
124150
position++
125-
val start = position
151+
val pStart = position
126152
parseNumber()
127-
if (start == position)
128-
throw JSONLexerException("Trailing dot is not allowed") // TODO: Better error handling
153+
if (pStart == position)
154+
throw JSONLexerException(
155+
"Trailing dot is not allowed",
156+
getPosition(start, position - start)
157+
)
129158
}
130159

131160
if (content.getChar(position) == 'e' || content.getChar(position) == 'E') {
@@ -134,13 +163,21 @@ class StringJSONLexer(private val content: CharSequence) : JSONLexer {
134163
if (content.getChar(position) == '-' || content.getChar(position) == '+')
135164
position++
136165

137-
val start = position
166+
val pStart = position
138167
parseNumber()
139-
if (start == position)
140-
throw JSONLexerException("Trailing '${content.getChar(position)}' is not allowed") // TODO: Better error handling
168+
if (pStart == position)
169+
throw JSONLexerException(
170+
"Trailing '${content.getChar(position - 1)}' is not allowed",
171+
getPosition(start, position - start)
172+
)
141173
}
142174

143-
return JSONLexeme(JSONLexemeType.NUMBER, content.substring(if(negative) start - 1 else start, position))
175+
return JSONLexeme(
176+
JSONLexemeType.NUMBER,
177+
start,
178+
position - start,
179+
content.substring(if(negative) start - 1 else start, position)
180+
)
144181
}
145182

146183
private fun parseFirstPart() {
@@ -153,7 +190,7 @@ class StringJSONLexer(private val content: CharSequence) : JSONLexer {
153190
position++
154191

155192
if (content.getChar(position) in '0' .. '9') {
156-
throw JSONLexerException("A number cannot start with a zero") // TODO: Better error handling
193+
throw JSONLexerException("A number cannot start with a zero", getPosition(position - 1, 2))
157194
}
158195
return
159196
}
@@ -172,6 +209,42 @@ class StringJSONLexer(private val content: CharSequence) : JSONLexer {
172209
}
173210
}
174211

212+
override fun getPosition(position: Int, length: Int): JSONPositionData {
213+
var pos = position
214+
215+
while (pos - 1 >= 0 && content[pos - 1] != '\n' && content[pos - 1] != '\r') {
216+
pos--
217+
}
218+
219+
val startLine = pos
220+
221+
pos = position
222+
while (pos < content.length && content[pos] != '\n' && content[pos] != '\r') {
223+
pos++
224+
}
225+
226+
class Position : JSONPositionData {
227+
override fun lineNumber(): Int {
228+
return content.slice(0..position).lines().size
229+
}
230+
231+
override fun linePosition(): Int {
232+
return position - startLine
233+
}
234+
235+
override fun getLength(): Int {
236+
return length
237+
}
238+
239+
override fun getLine(): String {
240+
return content.substring(startLine, pos)
241+
}
242+
243+
}
244+
245+
return Position()
246+
}
247+
175248
private fun CharSequence.getChar(index: Int): Char {
176249
if (index >= this.length) {
177250
return 0.toChar()
@@ -183,5 +256,5 @@ class StringJSONLexer(private val content: CharSequence) : JSONLexer {
183256
private fun Char.isWhitespace(): Boolean {
184257
return this == ' ' || this == '\r' || this == '\n' || this == '\t'
185258
}
259+
return this[index]
186260
}
187-

0 commit comments

Comments
 (0)