Skip to content

Commit 37133f6

Browse files
committed
Ported tests from #3
1 parent 3a896c5 commit 37133f6

File tree

2 files changed

+204
-0
lines changed

2 files changed

+204
-0
lines changed

lib/src/test/kotlin/io/github/json5/kotlin/JSON5ParseErrorsTest.kt

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -370,4 +370,117 @@ class JSON5ParseErrorsTest {
370370
exception.lineNumber shouldBe 1
371371
exception.columnNumber shouldBe 2 // Position of the "1"
372372
}
373+
374+
@Test
375+
@DisplayName("Object: should throw for invalid keys")
376+
fun `object invalid keys`() {
377+
val ex1 = shouldThrow<JSON5Exception> { JSON5.parse("{null: 1}") }
378+
ex1.message shouldContain "Expected property name or '}'" // Lexer sees 'n', expects identifier or string key
379+
ex1.lineNumber shouldBe 1
380+
ex1.columnNumber shouldBe 2 // 'n'
381+
382+
val ex2 = shouldThrow<JSON5Exception> { JSON5.parse("{true: 1}") }
383+
ex2.message shouldContain "Expected property name or '}'"
384+
ex2.lineNumber shouldBe 1
385+
ex2.columnNumber shouldBe 2 // 't'
386+
387+
val ex3 = shouldThrow<JSON5Exception> { JSON5.parse("{123: 1}") }
388+
ex3.message shouldContain "Expected property name or '}'"
389+
ex3.lineNumber shouldBe 1
390+
ex3.columnNumber shouldBe 2 // '1'
391+
}
392+
393+
@Test
394+
@DisplayName("Object: should throw for comma issues")
395+
fun `object comma issues`() {
396+
val ex1 = shouldThrow<JSON5Exception> { JSON5.parse("{a:1,,}") }
397+
ex1.message shouldContain "Expected property name or '}'" // Expects a key after comma
398+
ex1.lineNumber shouldBe 1
399+
ex1.columnNumber shouldBe 6
400+
401+
val ex2 = shouldThrow<JSON5Exception> { JSON5.parse("{,a:1}") }
402+
ex2.message shouldContain "Expected property name or '}'" // Cannot start with comma
403+
ex2.lineNumber shouldBe 1
404+
ex2.columnNumber shouldBe 2
405+
}
406+
407+
@Test
408+
@DisplayName("Object: should throw for structure issues")
409+
fun `object structure issues`() {
410+
val ex1 = shouldThrow<JSON5Exception> { JSON5.parse("{a:1 b:2}") } // Missing comma
411+
ex1.message shouldContain "Expected ',' or '}'" // Expects comma or }
412+
ex1.lineNumber shouldBe 1
413+
ex1.columnNumber shouldBe 6
414+
415+
val ex2 = shouldThrow<JSON5Exception> { JSON5.parse("{\"a\":1 \"b\":2}") } // Missing comma, string keys
416+
ex2.message shouldContain "Expected ',' or '}'" // Expects comma or }
417+
ex2.lineNumber shouldBe 1
418+
ex2.columnNumber shouldBe 8
419+
420+
val ex3 = shouldThrow<JSON5Exception> { JSON5.parse("{a:}") } // Missing value
421+
ex3.message shouldContain "Unexpected punctuator" // Expects a value
422+
ex3.lineNumber shouldBe 1
423+
ex3.columnNumber shouldBe 4
424+
425+
val ex4 = shouldThrow<JSON5Exception> { JSON5.parse("{a:1, :2}") } // Missing key
426+
ex4.message shouldContain "Expected property name or '}'" // Expects a key
427+
ex4.lineNumber shouldBe 1
428+
ex4.columnNumber shouldBe 7
429+
}
430+
431+
@Test
432+
@DisplayName("Array: should throw for comma issues")
433+
fun `array comma issues`() {
434+
val ex1 = shouldThrow<JSON5Exception> { JSON5.parse("[1,2,,]") }
435+
ex1.message shouldContain "Unexpected punctuator" // Expects a value after comma
436+
ex1.lineNumber shouldBe 1
437+
ex1.columnNumber shouldBe 6
438+
439+
val ex2 = shouldThrow<JSON5Exception> { JSON5.parse("[,1,2]") }
440+
ex2.message shouldContain "Unexpected punctuator" // Cannot start with comma
441+
ex2.lineNumber shouldBe 1
442+
ex2.columnNumber shouldBe 2
443+
444+
val ex3 = shouldThrow<JSON5Exception> { JSON5.parse("[1,,2]") } // Elision not allowed by spec
445+
ex3.message shouldContain "Unexpected punctuator" // Expects value, finds comma
446+
ex3.lineNumber shouldBe 1
447+
ex3.columnNumber shouldBe 4
448+
}
449+
450+
@Test
451+
@DisplayName("Array: should throw for structure issues")
452+
fun `array structure issues`() {
453+
val ex1 = shouldThrow<JSON5Exception> { JSON5.parse("[1 2]") } // Missing comma
454+
ex1.message shouldContain "Expected ',' or ']'" // Expects comma or ]
455+
ex1.lineNumber shouldBe 1
456+
ex1.columnNumber shouldBe 4
457+
}
458+
459+
@Test
460+
@DisplayName("String: should throw for unterminated strings")
461+
fun `string unterminated`() {
462+
val ex1 = shouldThrow<JSON5Exception> { JSON5.parse("\"abc") }
463+
ex1.message shouldContain "invalid end of input"
464+
ex1.lineNumber shouldBe 1 // The line where the string started
465+
// Column could be end of line or where EOF is effectively seen
466+
// ex1.columnNumber shouldBe 4
467+
468+
val ex2 = shouldThrow<JSON5Exception> { JSON5.parse("'abc") }
469+
ex2.message shouldContain "invalid end of input"
470+
ex2.lineNumber shouldBe 1
471+
// ex2.columnNumber shouldBe 4
472+
}
473+
474+
@Test
475+
@DisplayName("String: should throw for invalid unescaped newline")
476+
fun `string invalid unescaped newline`() {
477+
val jsonStringWithUnescapedLF = "'abc\ndef'" // Kotlin makes this a literal LF
478+
val exception = shouldThrow<JSON5Exception> {
479+
JSON5.parse(jsonStringWithUnescapedLF)
480+
}
481+
exception.message shouldContain "invalid character '\\x0a'" // LF
482+
exception.lineNumber shouldBe 2 // Error is on the first line where string starts
483+
exception.columnNumber shouldBe 1 // After 'abc'
484+
}
485+
373486
}

lib/src/test/kotlin/io/github/json5/kotlin/JSON5ParseTest.kt

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import io.kotest.matchers.types.shouldBeInstanceOf
77
import org.junit.jupiter.api.Test
88
import org.junit.jupiter.api.DisplayName
99
import kotlin.Double.Companion.NaN
10+
import kotlin.math.pow
1011
import kotlin.test.Ignore
1112
import kotlin.test.assertTrue
1213

@@ -299,4 +300,94 @@ class JSON5ParseTest {
299300
}
300301
exception.lineNumber shouldBe 1
301302
}
303+
304+
@Test
305+
@DisplayName("should parse diverse identifiers correctly")
306+
fun `parse diverse identifiers`() {
307+
JSON5.parse("{ _: 1 }") shouldBe mapOf("_" to 1.0)
308+
JSON5.parse("{ \$: 2 }") shouldBe mapOf("\$" to 2.0)
309+
JSON5.parse("{ _ident: 3 }") shouldBe mapOf("_ident" to 3.0)
310+
JSON5.parse("{ \$ident: 4 }") shouldBe mapOf("\$ident" to 4.0)
311+
JSON5.parse("{ ident_: 5 }") shouldBe mapOf("ident_" to 5.0)
312+
JSON5.parse("{ ident\$: 6 }") shouldBe mapOf("ident\$" to 6.0)
313+
JSON5.parse("{ üñîçødé: 7 }") shouldBe mapOf("üñîçødé" to 7.0) // Already covered but good to have
314+
JSON5.parse("{ \\u0061b\\u0063: 8 }") shouldBe mapOf("abc" to 8.0) // Multiple consecutive escapes
315+
JSON5.parse("{ id\\u0024ent: 9 }") shouldBe mapOf("id\$ent" to 9.0) // Escape resolves to $
316+
JSON5.parse("{ \\u005fid\\u005f: 10 }") shouldBe mapOf("_id_" to 10.0) // Escape resolves to _
317+
JSON5.parse("{ \\u0061: 11 }") shouldBe mapOf("a" to 11.0) // Identifier is a single escape
318+
JSON5.parse("{ \\u0061\\u0062c: 12 }") shouldBe mapOf("abc" to 12.0) // Starts with escapes, then normal char
319+
}
320+
321+
@Test
322+
@DisplayName("should parse large hexadecimal numbers")
323+
fun `parse large hexadecimal numbers`() {
324+
// Max Long as hex is 7fffffffffffffff
325+
// 0x1fffffffffffffff in decimal is 2305843009213693951
326+
JSON5.parse("0x1fffffffffffffff") shouldBe 2.305843009213694E18 // Might lose some precision
327+
// 0x2000000000000000 in decimal is 2305843009213693952
328+
JSON5.parse("0x2000000000000000") shouldBe 2.305843009213694E18 // Might be same as above due to double precision
329+
// A very large hex number
330+
// JSON5.parse("0x123456789abcdef123456789abcdef123456789abcdef") shouldBe 3.777995208190904E49
331+
// JSON5.parse("-0x123456789abcdef123456789abcdef123456789abcdef") shouldBe -3.777995208190904E49
332+
333+
// Hex representation of Double.MAX_VALUE (0x1.fffffffffffffp+1023)
334+
// This is tricky because JSON5 hex are integers.
335+
// The largest exact integer a double can represent is 2^53.
336+
// 0x1FFFFFFFFFFFFF is 2^53 - 1
337+
JSON5.parse("0x1FFFFFFFFFFFFF") shouldBe (2.0.pow(53.0) - 1)
338+
// 0x20000000000000 is 2^53
339+
JSON5.parse("0x20000000000000") shouldBe 2.0.pow(53.0)
340+
// One larger than 2^53 will not be exact
341+
// JSON5.parse("0x20000000000001") shouldBe (2.0.pow(53.0) + 2) // Due to rounding for doubles
342+
}
343+
344+
@Test
345+
@DisplayName("should handle invalid hexadecimal numbers")
346+
fun `parse invalid hexadecimal numbers`() {
347+
val ex1 = shouldThrow<JSON5Exception> {
348+
JSON5.parse("0x")
349+
}
350+
ex1.message shouldContain "invalid character ' ' at line 1, column 2"
351+
352+
val ex2 = shouldThrow<JSON5Exception> {
353+
JSON5.parse("-0x")
354+
}
355+
ex2.message shouldContain "invalid character ' ' at line 1, column 3"
356+
357+
val ex3 = shouldThrow<JSON5Exception> {
358+
JSON5.parse("0xG")
359+
}
360+
ex3.message shouldContain "invalid character 'G' at line 1, column 3"
361+
362+
val ex4 = shouldThrow<JSON5Exception> {
363+
JSON5.parse("+0xG")
364+
}
365+
ex4.message shouldContain "invalid character 'G' at line 1, column 4"
366+
367+
val ex5 = shouldThrow<JSON5Exception> {
368+
JSON5.parse("0x12G")
369+
}
370+
ex5.message shouldContain "invalid character 'G' at line 1, column 5"
371+
}
372+
373+
@Test
374+
@DisplayName("should parse line continuations correctly")
375+
fun `parse line continuations`() {
376+
JSON5.parse("'ab\\\ncd'") shouldBe "abcd"
377+
JSON5.parse("'ab\\\r\ncd'") shouldBe "abcd"
378+
JSON5.parse("'ab\\\rcd'") shouldBe "abcd" // \r is also a line terminator
379+
JSON5.parse("'ab\\\u2028cd'") shouldBe "abcd"
380+
JSON5.parse("'ab\\\u2029cd'") shouldBe "abcd"
381+
}
382+
383+
@Ignore
384+
@Test
385+
@DisplayName("should parse unrecognized simple escapes as the character itself")
386+
fun `parse unrecognized simple escapes`() {
387+
JSON5.parse("'\\a'") shouldBe "a"
388+
JSON5.parse("'\\c'") shouldBe "c"
389+
JSON5.parse("'\\/'") shouldBe "/"
390+
JSON5.parse("'\\1'") shouldBe "1" // \1 is not an octal escape in JSON5
391+
JSON5.parse("'\\ '") shouldBe " " // \ followed by space
392+
}
302393
}

0 commit comments

Comments
 (0)