diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index ff23a68..1af9e09 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index faf9300..1aa94a4 100755 --- a/gradlew +++ b/gradlew @@ -15,8 +15,6 @@ # See the License for the specific language governing permissions and # limitations under the License. # -# SPDX-License-Identifier: Apache-2.0 -# ############################################################################## # @@ -57,7 +55,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -86,7 +84,7 @@ done # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) -APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -205,7 +203,7 @@ fi DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Collect all arguments for the java command: -# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, # and any embedded shellness will be escaped. # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be # treated as '${Hostname}' itself on the command line. diff --git a/gradlew.bat b/gradlew.bat index 9b42019..6689b85 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -13,8 +13,6 @@ @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem -@rem SPDX-License-Identifier: Apache-2.0 -@rem @if "%DEBUG%"=="" @echo off @rem ########################################################################## @@ -45,11 +43,11 @@ set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if %ERRORLEVEL% equ 0 goto execute -echo. 1>&2 -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 -echo. 1>&2 -echo Please set the JAVA_HOME variable in your environment to match the 1>&2 -echo location of your Java installation. 1>&2 +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. goto fail @@ -59,11 +57,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute -echo. 1>&2 -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 -echo. 1>&2 -echo Please set the JAVA_HOME variable in your environment to match the 1>&2 -echo location of your Java installation. 1>&2 +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. goto fail diff --git a/lib/src/test/kotlin/io/github/json5/kotlin/JSON5ParseErrorsTest.kt b/lib/src/test/kotlin/io/github/json5/kotlin/JSON5ParseErrorsTest.kt index f744f64..0857183 100644 --- a/lib/src/test/kotlin/io/github/json5/kotlin/JSON5ParseErrorsTest.kt +++ b/lib/src/test/kotlin/io/github/json5/kotlin/JSON5ParseErrorsTest.kt @@ -6,9 +6,17 @@ import io.kotest.matchers.string.shouldContain import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test +/** + * Tests for error handling in JSON5 parsing. + * This class verifies that the parser correctly throws [JSON5Exception] for various + * syntax errors and invalid JSON5 constructs. + */ @DisplayName("JSON5.parse errors") class JSON5ParseErrorsTest { + /** + * Tests that parsing an empty document throws an error. + */ @Test fun `should throw on empty documents`() { val exception = shouldThrow { @@ -19,6 +27,10 @@ class JSON5ParseErrorsTest { exception.columnNumber shouldBe 1 } + /** + * Tests that parsing a document containing only comments throws an error, + * as a valid JSON5 document must have a top-level value. + */ @Test fun `should throw on documents with only comments`() { val exception = shouldThrow { @@ -29,6 +41,9 @@ class JSON5ParseErrorsTest { exception.columnNumber shouldBe 3 // After reading "//a", the cursor is at the end of input } + /** + * Tests that an incomplete single-line comment (e.g., `/` not followed by `/`) throws an error. + */ @Test fun `should throw on incomplete single line comments`() { val exception = shouldThrow { @@ -39,6 +54,9 @@ class JSON5ParseErrorsTest { exception.columnNumber shouldBe 1 } + /** + * Tests that an unterminated multi-line comment (e.g., `/*` without `*/`) throws an error. + */ @Test fun `should throw on unterminated multiline comments`() { val exception = shouldThrow { @@ -49,6 +67,9 @@ class JSON5ParseErrorsTest { exception.columnNumber shouldBe 2 // Position of the '*' } + /** + * Tests that an unterminated multi-line comment closing (e.g., `/**` without matching `*/`) throws an error. + */ @Test fun `should throw on unterminated multiline comment closings`() { val exception = shouldThrow { @@ -59,6 +80,9 @@ class JSON5ParseErrorsTest { exception.columnNumber shouldBe 3 // Position of the second '*' } + /** + * Tests that invalid characters appearing where a value is expected throw an error. + */ @Test fun `should throw on invalid characters in values`() { val exception = shouldThrow { @@ -69,6 +93,10 @@ class JSON5ParseErrorsTest { exception.columnNumber shouldBe 1 } + /** + * Tests that invalid characters within an identifier's escape sequence (e.g. `{\a:1}`) throw an error. + * This typically occurs if an escape sequence is not a valid Unicode escape (`\uXXXX`) or a valid single character escape. + */ @Test fun `should throw on invalid characters in identifier start escapes`() { val exception = shouldThrow { @@ -79,6 +107,10 @@ class JSON5ParseErrorsTest { exception.columnNumber shouldBe 3 } + /** + * Tests that an identifier starting with an invalid character (e.g. `{\u0021:1}` which is `{!":"1}`) throws an error. + * `!` is not a valid start for an identifier. + */ @Test fun `should throw on invalid identifier start characters`() { val exception = shouldThrow { @@ -89,6 +121,9 @@ class JSON5ParseErrorsTest { exception.columnNumber shouldBe 2 } + /** + * Tests that an invalid escape sequence within an identifier (not at the start) throws an error. + */ @Test fun `should throw on invalid characters in identifier continue escapes`() { val exception = shouldThrow { @@ -99,6 +134,10 @@ class JSON5ParseErrorsTest { exception.columnNumber shouldBe 3 // Parser reported error at col 3 } + /** + * Tests that an identifier containing an invalid character (e.g. `{a\u0021:1}` which is `{"a!":1}`) throws an error. + * `!` is not a valid continuation character for an identifier. + */ @Test fun `should throw on invalid identifier continue characters`() { val exception = shouldThrow { @@ -109,6 +148,9 @@ class JSON5ParseErrorsTest { exception.columnNumber shouldBe 3 } + /** + * Tests that an invalid character immediately following a sign (`+` or `-`) in a number throws an error. + */ @Test fun `should throw on invalid characters following a sign`() { val exception = shouldThrow { @@ -119,6 +161,9 @@ class JSON5ParseErrorsTest { exception.columnNumber shouldBe 2 } + /** + * Tests that an invalid character immediately following a leading decimal point in a number throws an error. + */ @Test fun `should throw on invalid characters following a leading decimal point`() { val exception = shouldThrow { @@ -129,6 +174,9 @@ class JSON5ParseErrorsTest { exception.columnNumber shouldBe 2 } + /** + * Tests that an invalid character immediately following an exponent indicator (`e` or `E`) in a number throws an error. + */ @Test fun `should throw on invalid characters following an exponent indicator`() { val exception = shouldThrow { @@ -139,6 +187,9 @@ class JSON5ParseErrorsTest { exception.columnNumber shouldBe 3 } + /** + * Tests that an invalid character immediately following an exponent sign (`+` or `-`) in a number throws an error. + */ @Test fun `should throw on invalid characters following an exponent sign`() { val exception = shouldThrow { @@ -149,6 +200,9 @@ class JSON5ParseErrorsTest { exception.columnNumber shouldBe 4 } + /** + * Tests that an invalid character immediately following a hexadecimal indicator (`0x` or `0X`) in a number throws an error. + */ @Test fun `should throw on invalid characters following a hexadecimal indicator`() { val exception = shouldThrow { @@ -159,6 +213,9 @@ class JSON5ParseErrorsTest { exception.columnNumber shouldBe 3 } + /** + * Tests that an unescaped newline character within a string throws an error. + */ @Test fun `should throw on invalid new lines in strings`() { val exception = shouldThrow { @@ -169,6 +226,9 @@ class JSON5ParseErrorsTest { exception.columnNumber shouldBe 1 // In JavaScript, the column resets to 0, but Kotlin uses 1-indexed } + /** + * Tests that an unterminated string (missing closing quote) throws an error. + */ @Test fun `should throw on unterminated strings`() { val exception = shouldThrow { @@ -179,6 +239,9 @@ class JSON5ParseErrorsTest { exception.columnNumber shouldBe 2 } + /** + * Tests that an object property name starting with an invalid identifier character (e.g. `!`) throws an error. + */ @Test fun `should throw on invalid identifier start characters in property names`() { val exception = shouldThrow { @@ -189,6 +252,9 @@ class JSON5ParseErrorsTest { exception.columnNumber shouldBe 2 } + /** + * Tests that an invalid character appearing immediately after a property name (before the colon) throws an error. + */ @Test fun `should throw on invalid characters following a property name`() { val exception = shouldThrow { @@ -199,6 +265,9 @@ class JSON5ParseErrorsTest { exception.columnNumber shouldBe 3 } + /** + * Tests that an invalid character appearing immediately after a property value (before a comma or closing brace) throws an error. + */ @Test fun `should throw on invalid characters following a property value`() { val exception = shouldThrow { @@ -209,6 +278,9 @@ class JSON5ParseErrorsTest { exception.columnNumber shouldBe 5 } + /** + * Tests that an invalid character appearing immediately after an array value (before a comma or closing bracket) throws an error. + */ @Test fun `should throw on invalid characters following an array value`() { val exception = shouldThrow { @@ -219,6 +291,9 @@ class JSON5ParseErrorsTest { exception.columnNumber shouldBe 3 } + /** + * Tests that an invalid character within a literal (e.g., `true`, `false`, `null`, `Infinity`, `NaN`) throws an error. + */ @Test fun `should throw on invalid characters in literals`() { val exception = shouldThrow { @@ -229,6 +304,9 @@ class JSON5ParseErrorsTest { exception.columnNumber shouldBe 4 } + /** + * Tests that an unterminated escape sequence within a string (e.g., `"\` at the end of input) throws an error. + */ @Test fun `should throw on unterminated escapes`() { val exception = shouldThrow { @@ -239,6 +317,9 @@ class JSON5ParseErrorsTest { exception.columnNumber shouldBe 2 // Position of the '\' } + /** + * Tests that an invalid first digit in a hexadecimal escape sequence (e.g., `\xg`) throws an error. + */ @Test fun `should throw on invalid first digits in hexadecimal escapes`() { val exception = shouldThrow { @@ -249,6 +330,9 @@ class JSON5ParseErrorsTest { exception.columnNumber shouldBe 4 } + /** + * Tests that an invalid second digit in a hexadecimal escape sequence (e.g., `\x0g`) throws an error. + */ @Test fun `should throw on invalid second digits in hexadecimal escapes`() { val exception = shouldThrow { @@ -259,6 +343,9 @@ class JSON5ParseErrorsTest { exception.columnNumber shouldBe 5 } + /** + * Tests that an invalid digit in a Unicode escape sequence (e.g., `\u000g`) throws an error. + */ @Test fun `should throw on invalid unicode escapes`() { val exception = shouldThrow { @@ -269,6 +356,15 @@ class JSON5ParseErrorsTest { exception.columnNumber shouldBe 7 } + /** + * Tests that an escaped digit (e.g., `\1`) throws an error. + * **Note on JSON5 Specification Compliance:** + * According to the JSON5 specification (Section 5.1 - Escapes), an escape sequence like `\1` + * is an "unrecognized simple escape" and should be interpreted as the character itself (i.e., the string "1"). + * It should **not** throw an error. + * The current test expectation (throwing an error) is incorrect based on the spec. + * Both this test and the parser's behavior need correction to align with the JSON5 specification. + */ @Test fun `should throw on escaped digits`() { for (i in 1..9) { @@ -281,6 +377,10 @@ class JSON5ParseErrorsTest { } } + /** + * Tests that octal escape sequences (e.g., `\01`) throw an error, as they are not allowed in JSON5. + * Note: `\0` (null character) is a valid escape, but `\0` followed by other digits is an octal escape. + */ @Test fun `should throw on octal escapes`() { val exception = shouldThrow { @@ -291,6 +391,9 @@ class JSON5ParseErrorsTest { exception.columnNumber shouldBe 4 } + /** + * Tests that having multiple top-level values without being part of an array or object throws an error. + */ @Test fun `should throw on multiple values`() { val exception = shouldThrow { @@ -301,6 +404,9 @@ class JSON5ParseErrorsTest { exception.columnNumber shouldBe 3 } + /** + * Tests that error messages correctly escape control characters. + */ @Test fun `should throw with control characters escaped in the message`() { val exception = shouldThrow { @@ -311,6 +417,9 @@ class JSON5ParseErrorsTest { exception.columnNumber shouldBe 1 } + /** + * Tests that an unclosed object (e.g., `{` at the end of input) throws an error. + */ @Test fun `should throw on unclosed objects before property names`() { val exception = shouldThrow { @@ -321,6 +430,9 @@ class JSON5ParseErrorsTest { exception.columnNumber shouldBe 1 // Position of the "{" } + /** + * Tests that an unclosed object after a property name (e.g., `{a` at EOF) throws an error. + */ @Test fun `should throw on unclosed objects after property names`() { val exception = shouldThrow { @@ -331,6 +443,9 @@ class JSON5ParseErrorsTest { exception.columnNumber shouldBe 2 // Position of the "a" } + /** + * Tests that an unclosed object after a property name and colon (e.g., `{a:` at EOF) throws an error. + */ @Test fun `should throw on unclosed objects before property values`() { val exception = shouldThrow { @@ -341,6 +456,9 @@ class JSON5ParseErrorsTest { exception.columnNumber shouldBe 3 // Position of the ":" } + /** + * Tests that an unclosed object after a property value (e.g., `{a:1` at EOF) throws an error. + */ @Test fun `should throw on unclosed objects after property values`() { val exception = shouldThrow { @@ -351,6 +469,9 @@ class JSON5ParseErrorsTest { exception.columnNumber shouldBe 4 // Position of the "1" } + /** + * Tests that an unclosed array (e.g., `[` at EOF) throws an error. + */ @Test fun `should throw on unclosed arrays before values`() { val exception = shouldThrow { @@ -361,6 +482,9 @@ class JSON5ParseErrorsTest { exception.columnNumber shouldBe 1 // Position of the "[" } + /** + * Tests that an unclosed array after a value (e.g., `[1` at EOF) throws an error. + */ @Test fun `should throw on unclosed arrays after values`() { val exception = shouldThrow { @@ -371,6 +495,10 @@ class JSON5ParseErrorsTest { exception.columnNumber shouldBe 2 // Position of the "1" } + /** + * Tests various scenarios where invalid keys in an object should cause an error. + * This includes using literals like `null`, `true`, or numbers as unquoted keys. + */ @Test @DisplayName("Object: should throw for invalid keys") fun `object invalid keys`() { @@ -390,6 +518,9 @@ class JSON5ParseErrorsTest { ex3.columnNumber shouldBe 2 // '1' } + /** + * Tests various scenarios involving misplaced or extra commas in objects that should cause an error. + */ @Test @DisplayName("Object: should throw for comma issues") fun `object comma issues`() { @@ -404,6 +535,10 @@ class JSON5ParseErrorsTest { ex2.columnNumber shouldBe 2 } + /** + * Tests various object structural issues that should cause an error. + * This includes missing commas between properties, missing values for keys, or missing keys. + */ @Test @DisplayName("Object: should throw for structure issues") fun `object structure issues`() { @@ -428,6 +563,10 @@ class JSON5ParseErrorsTest { ex4.columnNumber shouldBe 7 } + /** + * Tests various scenarios involving misplaced or extra commas in arrays that should cause an error. + * JSON5 does not allow elision (e.g. `[1,,2]`) unlike JavaScript. + */ @Test @DisplayName("Array: should throw for comma issues") fun `array comma issues`() { @@ -447,6 +586,9 @@ class JSON5ParseErrorsTest { ex3.columnNumber shouldBe 4 } + /** + * Tests array structural issues, such as missing commas between elements. + */ @Test @DisplayName("Array: should throw for structure issues") fun `array structure issues`() { @@ -456,6 +598,9 @@ class JSON5ParseErrorsTest { ex1.columnNumber shouldBe 4 } + /** + * Tests that unterminated strings (missing closing quote) throw errors. + */ @Test @DisplayName("String: should throw for unterminated strings") fun `string unterminated`() { @@ -471,6 +616,9 @@ class JSON5ParseErrorsTest { // ex2.columnNumber shouldBe 4 } + /** + * Tests that an unescaped newline character within a string throws an error. + */ @Test @DisplayName("String: should throw for invalid unescaped newline") fun `string invalid unescaped newline`() { diff --git a/lib/src/test/kotlin/io/github/json5/kotlin/JSON5ParseTest.kt b/lib/src/test/kotlin/io/github/json5/kotlin/JSON5ParseTest.kt index 8419384..c223348 100644 --- a/lib/src/test/kotlin/io/github/json5/kotlin/JSON5ParseTest.kt +++ b/lib/src/test/kotlin/io/github/json5/kotlin/JSON5ParseTest.kt @@ -11,39 +11,67 @@ import kotlin.math.pow import kotlin.test.Ignore import kotlin.test.assertTrue +/** + * Tests for JSON5 parsing functionality. + * This class covers various aspects of parsing JSON5, including basic data types, + * object and array structures, comments, whitespace, and the use of a reviver function. + * It also includes tests for parsing different number formats (integers, signed, fractional, exponents, hex) + * and string formats (single/double quoted, escaped characters, line terminators). + */ @DisplayName("JSON5.parse") class JSON5ParseTest { + /** + * Tests parsing of an empty JSON5 object. + */ @Test fun `should parse empty object`() { JSON5.parse("{}") shouldBe emptyMap() } + /** + * Tests parsing of a simple JSON5 object with a string value. + */ @Test fun `should parse simple object with string value`() { JSON5.parse("""{"key": "value"}""") shouldBe mapOf("key" to "value") } + /** + * Tests parsing of a simple JSON5 object with a number value. + */ @Test fun `should parse simple object with number value`() { JSON5.parse("""{"key": 42}""") shouldBe mapOf("key" to 42.0) } + /** + * Tests parsing of a simple JSON5 object with a boolean value. + */ @Test fun `should parse simple object with boolean value`() { JSON5.parse("""{"key": true}""") shouldBe mapOf("key" to true) } + /** + * Tests parsing of a simple JSON5 object with a null value. + */ @Test fun `should parse simple object with null value`() { JSON5.parse("""{"key": null}""") shouldBe mapOf("key" to null) } + /** + * Tests parsing of an empty JSON5 array. + */ @Test fun `should parse empty array`() { JSON5.parse("[]") shouldBe emptyList() } + /** + * Tests parsing of a JSON5 array containing various data types. + */ @Test fun `should parse array with values`() { JSON5.parse("""[1, "string", true, null]""") shouldBe listOf(1.0, "string", true, null) @@ -51,21 +79,36 @@ class JSON5ParseTest { // Additional object tests + /** + * Tests parsing of JSON5 object property names enclosed in double quotes. + */ @Test fun `should parse double quoted string property names`() { JSON5.parse("""{"a":1}""") shouldBe mapOf("a" to 1.0) } + /** + * Tests parsing of JSON5 object property names enclosed in single quotes. + */ @Test fun `should parse single quoted string property names`() { JSON5.parse("""{'a':1}""") shouldBe mapOf("a" to 1.0) } + /** + * Tests parsing of unquoted JSON5 object property names. + */ @Test fun `should parse unquoted property names`() { JSON5.parse("""{a:1}""") shouldBe mapOf("a" to 1.0) } + /** + * Tests parsing of JSON5 object property names with special characters. + * This test highlights a deviation from the JSON5 specification (section 2.3) regarding valid identifier characters. + * The JSON5 spec allows `$` and `_` as identifier start/part characters, but this parser currently flags `$` as invalid in this context. + * The original expectation `mapOf("\$_" to 1.0, "_$" to 2.0, "a\u200C" to 3.0)` is commented out. + */ @Test fun `should parse special character property names`() { // Original: JSON5.parse("""{\${"$"}_:1,_\$:2,a\u200C:3}""") shouldBe mapOf("\$_" to 1.0, "_$" to 2.0, "a\u200C" to 3.0) @@ -78,11 +121,21 @@ class JSON5ParseTest { exception.columnNumber shouldBe 3 } + /** + * Tests parsing of JSON5 object property names containing Unicode characters. + */ @Test fun `should parse unicode property names`() { JSON5.parse("""{ùńîċõďë:9}""") shouldBe mapOf("ùńîċõďë" to 9.0) } + /** + * Tests parsing of JSON5 object property names with escaped characters. + * This test highlights a deviation from the JSON5 specification (section 2.3) regarding valid identifier characters. + * The JSON5 spec allows Unicode escape sequences (e.g., `\u0024` for `$`) to form valid identifiers. + * This parser currently flags the backslash of an escape sequence as an invalid character in this context. + * The original expectation `mapOf("ab" to 1.0, "\$_" to 2.0, "_$" to 3.0)` is commented out. + */ @Test fun `should parse escaped property names`() { // Note: The double backslashes in the test string become single backslashes in the actual string @@ -96,17 +149,26 @@ class JSON5ParseTest { exception.columnNumber shouldBe 3 } + /** + * Tests that the `__proto__` property name is preserved during parsing. + */ @Test fun `should preserve __proto__ property names`() { val result = JSON5.parse("""{"__proto__":1}""") as Map<*, *> result["__proto__"] shouldBe 1.0 } + /** + * Tests parsing of JSON5 objects with multiple properties. + */ @Test fun `should parse multiple properties`() { JSON5.parse("""{abc:1,def:2}""") shouldBe mapOf("abc" to 1.0, "def" to 2.0) } + /** + * Tests parsing of nested JSON5 objects. + */ @Test fun `should parse nested objects`() { JSON5.parse("""{a:{b:2}}""") shouldBe mapOf("a" to mapOf("b" to 2.0)) @@ -114,11 +176,17 @@ class JSON5ParseTest { // Additional array tests + /** + * Tests parsing of JSON5 arrays with multiple values. + */ @Test fun `should parse multiple array values`() { JSON5.parse("[1,2]") shouldBe listOf(1.0, 2.0) } + /** + * Tests parsing of nested JSON5 arrays. + */ @Test fun `should parse nested arrays`() { JSON5.parse("[1,[2,3]]") shouldBe listOf(1.0, listOf(2.0, 3.0)) @@ -126,47 +194,74 @@ class JSON5ParseTest { // Number tests + /** + * Tests parsing of numbers with leading zeros (which are valid in JSON5). + */ @Test fun `should parse leading zeroes`() { JSON5.parse("[0,0.,0e0]") shouldBe listOf(0.0, 0.0, 0.0) } + /** + * Tests parsing of integer numbers. + */ @Test fun `should parse integers`() { JSON5.parse("[1,23,456,7890]") shouldBe listOf(1.0, 23.0, 456.0, 7890.0) } + /** + * Tests parsing of signed numbers (positive and negative). + */ @Test fun `should parse signed numbers`() { JSON5.parse("[-1,+2,-.1,-0]") shouldBe listOf(-1.0, 2.0, -0.1, -0.0) } + /** + * Tests parsing of numbers with leading decimal points. + */ @Test fun `should parse leading decimal points`() { JSON5.parse("[.1,.23]") shouldBe listOf(0.1, 0.23) } + /** + * Tests parsing of fractional numbers (numbers with decimal points). + */ @Test fun `should parse fractional numbers`() { JSON5.parse("[1.0,1.23]") shouldBe listOf(1.0, 1.23) } + /** + * Tests parsing of numbers with exponents. + */ @Test fun `should parse exponents`() { JSON5.parse("[1e0,1e1,1e01,1.e0,1.1e0,1e-1,1e+1]") shouldBe listOf(1.0, 10.0, 10.0, 1.0, 1.1, 0.1, 10.0) } + /** + * Tests parsing of hexadecimal numbers. + */ @Test fun `should parse hexadecimal numbers`() { JSON5.parse("[0x1,0x10,0xff,0xFF]") shouldBe listOf(1.0, 16.0, 255.0, 255.0) } + /** + * Tests parsing of `Infinity` and `-Infinity` values. + */ @Test fun `should parse infinity values`() { JSON5.parse("[Infinity,-Infinity]") shouldBe listOf(Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY) } + /** + * Tests parsing of `NaN` (Not a Number). + */ @Test fun `should parse NaN`() { val result = JSON5.parse("NaN") @@ -174,6 +269,9 @@ class JSON5ParseTest { assertTrue((result as Double).isNaN()) } + /** + * Tests parsing of signed `NaN` (e.g., `-NaN`). + */ @Test fun `should parse signed NaN`() { val result = JSON5.parse("-NaN") @@ -181,36 +279,69 @@ class JSON5ParseTest { assertTrue((result as Double).isNaN()) } + /** + * Tests parsing of numbers that appear as bare values (not within an object or array). + */ @Test fun `should parse bare numbers`() { JSON5.parse("1") shouldBe 1.0 JSON5.parse("+1.23e100") shouldBe 1.23e100 } + /** + * Tests parsing of hexadecimal numbers that appear as bare values. + */ @Test fun `should parse bare hexadecimal numbers`() { JSON5.parse("0x1") shouldBe 1.0 - // Adjusted to reflect current parser bug / behavior + // Adjusted to reflect current parser bug / behavior for very large negative hex numbers. + // The parser might lose precision or handle very large negative hex numbers differently than expected. JSON5.parse("-0x0123456789abcdefABCDEF") shouldBe -1.3754889325393114E24 } // String tests + /** + * Tests parsing of strings enclosed in double quotes. + */ @Test fun `should parse double quoted strings`() { JSON5.parse("\"abc\"") shouldBe "abc" } + /** + * Tests parsing of strings enclosed in single quotes. + */ @Test fun `should parse single quoted strings`() { JSON5.parse("'abc'") shouldBe "abc" } + /** + * Tests parsing of strings containing quotes that match the enclosing quotes (e.g., `'"'` or `"'"`). + */ @Test fun `should parse quotes in strings`() { JSON5.parse("""['"',"'"]""") shouldBe listOf("\"", "'") } + /** + * Tests parsing of various escaped characters within strings. + * This test is currently ignored due to known issues with how the parser handles certain escape sequences + * and line continuations, which deviate from the JSON5 specification (section 5.1). + * + * **JSON5 Specification (Section 5.1 - Escapes and Line Continuations):** + * - Standard escapes (`\b`, `\f`, `\n`, `\r`, `\t`, `\v`, `\0`, `\xHH`, `\uHHHH`) should be parsed as their respective characters. + * - Line continuations (`\` followed by a line terminator sequence like `\n`, `\r\n`, `\r`, `\u2028`, `\u2029`) should result in the backslash and the line terminator sequence being ignored, effectively joining the lines. + * - Any other escaped character (e.g., `\a`, `\'`, `\"`) should be interpreted as the character itself (e.g., `a`, `'`, `"`). + * + * **Current Parser Behavior (deviations):** + * - Line continuations like `\\\n` are incorrectly parsed as `\` followed by a newline character, instead of an empty string. + * - Escaped characters not part of the standard set or line continuations, like `\a`, are sometimes misinterpreted (e.g., `\a` becomes BEL `\u0007` instead of `a`). + * + * The expected string in the test reflects the current incorrect output for documentation purposes. + * This test should be updated and unignored once the parser correctly implements section 5.1 of the spec. + */ @Ignore @Test fun `should parse escaped characters`() { @@ -224,6 +355,10 @@ class JSON5ParseTest { "\u0008\u000C\u000A\u000D\u0009\u000B\u0000\u000F\u01FF\\\n\\\r\n\\\r\u2028\u2029\u0007'\"" // Explicit \uXXXX for all initial escapes } + /** + * Tests parsing of strings containing Unicode line separator (`\u2028`) and paragraph separator (`\u2029`) characters. + * These are valid unescaped characters in JSON5 strings. + */ @Test fun `should parse line and paragraph separators`() { JSON5.parse("'\u2028\u2029'") shouldBe "\u2028\u2029" @@ -231,16 +366,25 @@ class JSON5ParseTest { // Comments tests + /** + * Tests that single-line comments (starting with `//`) are correctly ignored. + */ @Test fun `should parse single-line comments`() { JSON5.parse("{//comment\n}") shouldBe emptyMap() } + /** + * Tests that single-line comments at the very end of the input are correctly handled. + */ @Test fun `should parse single-line comments at end of input`() { JSON5.parse("{}//comment") shouldBe emptyMap() } + /** + * Tests that multi-line comments (enclosed in `/* ... */`) are correctly ignored. + */ @Test fun `should parse multi-line comments`() { JSON5.parse("{/*comment\n** */}") shouldBe emptyMap() @@ -248,6 +392,11 @@ class JSON5ParseTest { // Whitespace tests + /** + * Tests that various whitespace characters are correctly ignored by the parser. + * This includes tab, vertical tab, form feed, space, non-breaking space, byte order mark, + * line feed, carriage return, line separator, paragraph separator, and em space. + */ @Test fun `should parse whitespace`() { JSON5.parse("{\t\u000B\u000C \u00A0\uFEFF\n\r\u2028\u2029\u2003}") shouldBe emptyMap() @@ -255,31 +404,51 @@ class JSON5ParseTest { // Reviver tests + /** + * Tests the reviver function's ability to modify property values during parsing. + */ @Test fun `should modify property values using reviver`() { JSON5.parse("{a:1,b:2}") { k, v -> if (k == "a") "revived" else v } shouldBe mapOf("a" to "revived", "b" to 2.0) } + /** + * Tests the reviver function's ability to modify property values within nested objects. + */ @Test fun `should modify nested object property values using reviver`() { JSON5.parse("{a:{b:2}}") { k, v -> if (k == "b") "revived" else v } shouldBe mapOf("a" to mapOf("b" to "revived")) } + /** + * Tests the reviver function's ability to delete property values by returning `null`. + * Note: In Kotlin, `null` returned by the reviver effectively removes the key from the resulting map. + */ @Test fun `should delete property values using reviver`() { JSON5.parse("{a:1,b:2}") { k, v -> if (k == "a") null else v } shouldBe mapOf("b" to 2.0) } + /** + * Tests the reviver function's ability to modify array values during parsing. + */ @Test fun `should modify array values using reviver`() { JSON5.parse("[0,1,2]") { k, v -> if (k == "1") "revived" else v } shouldBe listOf(0.0, "revived", 2.0) } + /** + * Tests the reviver function's ability to modify values within nested arrays. + */ @Test fun `should modify nested array values using reviver`() { JSON5.parse("[0,[1,2,3]]") { k, v -> if (k == "2") "revived" else v } shouldBe listOf(0.0, listOf(1.0, 2.0, "revived")) } + /** + * Tests the reviver function's ability to delete array values by returning `null`. + * Note: In Kotlin, `null` returned by the reviver for an array element results in a `null` value at that index in the list. + */ @Test fun `should delete array values using reviver`() { val result = JSON5.parse("[0,1,2]") { k, v -> if (k == "1") null else v } as List<*> @@ -288,11 +457,18 @@ class JSON5ParseTest { result[2] shouldBe 2.0 } + /** + * Tests the reviver function's ability to modify the root value of the parsed JSON5. + * The key for the root value is an empty string. + */ @Test fun `should modify the root value using reviver`() { JSON5.parse("1") { k, v -> if (k == "") "revived" else v } shouldBe "revived" } + /** + * Tests that parsing invalid JSON5 input throws a [JSON5Exception]. + */ @Test fun `should throw exception for invalid JSON5`() { val exception = shouldThrow { @@ -301,6 +477,11 @@ class JSON5ParseTest { exception.lineNumber shouldBe 1 } + /** + * Tests parsing of various valid identifier formats for object keys, + * including those starting or ending with `_` or `$`, containing Unicode characters, + * and those formed using Unicode escape sequences. + */ @Test @DisplayName("should parse diverse identifiers correctly") fun `parse diverse identifiers`() { @@ -318,6 +499,12 @@ class JSON5ParseTest { JSON5.parse("{ \\u0061\\u0062c: 12 }") shouldBe mapOf("abc" to 12.0) // Starts with escapes, then normal char } + /** + * Tests parsing of large hexadecimal numbers. + * JSON5 supports hexadecimal numbers, which are parsed as [Double] values. + * This test checks precision for very large hex numbers and boundary conditions around `2^53`, + * which is the largest integer that can be exactly represented by a [Double]. + */ @Test @DisplayName("should parse large hexadecimal numbers") fun `parse large hexadecimal numbers`() { @@ -341,6 +528,10 @@ class JSON5ParseTest { // JSON5.parse("0x20000000000001") shouldBe (2.0.pow(53.0) + 2) // Due to rounding for doubles } + /** + * Tests that parsing malformed hexadecimal numbers throws a [JSON5Exception]. + * This includes hex numbers with no digits (e.g., "0x", "-0x") or invalid hex digits (e.g., "0xG"). + */ @Test @DisplayName("should handle invalid hexadecimal numbers") fun `parse invalid hexadecimal numbers`() { @@ -370,6 +561,13 @@ class JSON5ParseTest { ex5.message shouldContain "invalid character 'G' at line 1, column 5" } + /** + * Tests parsing of line continuations in strings. + * A line continuation (`\` followed by a line terminator sequence) should be ignored, + * effectively concatenating the parts of the string. + * This test uses various line terminator sequences: LF (`\n`), CRLF (`\r\n`), CR (`\r`), + * LS (`\u2028`), and PS (`\u2029`). + */ @Test @DisplayName("should parse line continuations correctly") fun `parse line continuations`() { @@ -380,6 +578,17 @@ class JSON5ParseTest { JSON5.parse("'ab\\\u2029cd'") shouldBe "abcd" } + /** + * Tests that unrecognized simple escape sequences are parsed as the character itself. + * For example, `\a` should be parsed as the character `a`, not as a BEL character. + * This behavior is defined in the JSON5 specification (section 5.1). + * + * This test is currently ignored because the parser may not correctly handle all such cases, + * potentially misinterpreting some unrecognized escapes or correctly handling them but + * this test requires confirmation after other escape-related bugs are fixed. + * The expectations in this test are correct according to the JSON5 specification. + * This test should be unignored and verified once the parser's escape handling is fully compliant. + */ @Ignore @Test @DisplayName("should parse unrecognized simple escapes as the character itself") diff --git a/lib/src/test/kotlin/io/github/json5/kotlin/JSON5ParserTestLargeFile.kt b/lib/src/test/kotlin/io/github/json5/kotlin/JSON5ParserTestLargeFile.kt index 9d777e0..a793acd 100644 --- a/lib/src/test/kotlin/io/github/json5/kotlin/JSON5ParserTestLargeFile.kt +++ b/lib/src/test/kotlin/io/github/json5/kotlin/JSON5ParserTestLargeFile.kt @@ -5,7 +5,20 @@ import org.junit.jupiter.api.Test import java.nio.file.Files import java.nio.file.Paths +/** + * Tests the JSON5 parser's ability to handle a large and complex JSON5 file. + * This class focuses on ensuring the parser can correctly process substantial inputs + * that may include a wide variety of JSON5 features, nested structures, and numerous entries. + */ class JSON5ParserTestLargeFile { + /** + * Parses the `runtime_enabled_features.json5` file, a real-world example from Chromium. + * This test performs several assertions to verify the structural integrity and + * content of the parsed data. It checks for the presence of specific keys, + * the types of values (Map, List), and the values of certain properties within + * the parsed configuration. This ensures the parser handles large, complex, + * and potentially deeply nested JSON5 documents correctly. + */ @Test fun testParseSimpleChromiumConfig() { val path = Paths.get("src/test/resources/runtime_enabled_features.json5") diff --git a/lib/src/test/kotlin/io/github/json5/kotlin/JSON5ParserTextExampleFiles.kt b/lib/src/test/kotlin/io/github/json5/kotlin/JSON5ParserTextExampleFiles.kt index 37ed7b5..15db481 100644 --- a/lib/src/test/kotlin/io/github/json5/kotlin/JSON5ParserTextExampleFiles.kt +++ b/lib/src/test/kotlin/io/github/json5/kotlin/JSON5ParserTextExampleFiles.kt @@ -5,7 +5,17 @@ import org.junit.jupiter.api.Test import java.nio.file.Files import java.nio.file.Paths +/** + * Tests the JSON5 parser using various example `.json5` files from the test resources. + * These tests validate the parser's ability to correctly interpret different JSON5 features + * by comparing the parsed output against expected Kotlin data structures (Maps and Lists). + */ class JSON5ParserTextExampleFiles { + /** + * Tests parsing of a basic JSON5 object from the `simple-object.json5` resource file. + * This file includes common data types like strings, numbers, booleans, and a nested object, + * primarily using quoted keys and standard JSON-like syntax. + */ @Test fun testParseSimpleObjectJson5() { val path = Paths.get("src/test/resources/simple-object.json5") @@ -27,6 +37,11 @@ class JSON5ParserTextExampleFiles { assertEquals(expected, result) } + /** + * Tests parsing of a JSON5 array containing various data types from `array-example.json5`. + * This includes numbers, strings, booleans, null, a nested object, a nested array, + * and special numeric values like Infinity and NaN. + */ @Test fun testParseArrayExampleJson5() { val path = Paths.get("src/test/resources/array-example.json5") @@ -60,6 +75,10 @@ class JSON5ParserTextExampleFiles { } } + /** + * Tests parsing of an empty JSON5 object from `empty-json.json5`. + * Verifies that an empty but valid JSON5 object string is parsed into an empty Kotlin Map. + */ @Test fun testParseEmptyJson5() { val path = Paths.get("src/test/resources/empty-json.json5") @@ -69,6 +88,12 @@ class JSON5ParserTextExampleFiles { assertEquals(expected, result) } + /** + * Tests parsing of various numeric formats from `numeric-formats.json5`. + * This includes integers, negative numbers, floats, leading/trailing decimal points, + * explicit positive signs, hexadecimal numbers, scientific notation, + * and special values like Infinity and NaN. + */ @Test fun testParseNumericFormatsJson5() { val path = Paths.get("src/test/resources/numeric-formats.json5") @@ -102,6 +127,12 @@ class JSON5ParserTextExampleFiles { } } + /** + * Tests parsing of various string formats and identifier types from `string-and-identifiers.json5`. + * This covers single and double quoted strings, multi-line strings, strings with line/paragraph separators, + * unquoted identifiers with special characters ($, _), Unicode identifiers, and identifiers with Unicode escapes. + * It also includes empty objects and arrays as values. + */ @Test fun testParseStringAndIdentifiersJson5() { val path = Paths.get("src/test/resources/string-and-identifiers.json5") @@ -132,6 +163,10 @@ class JSON5ParserTextExampleFiles { } } + /** + * Tests parsing of a JSON5 document where the root value is a single string, from `root-string.json5`. + * JSON5 allows any valid JSON5 value as the root of a document, not just objects or arrays. + */ @Test fun testParseRootStringJson5() { val path = Paths.get("src/test/resources/root-string.json5") @@ -141,6 +176,12 @@ class JSON5ParserTextExampleFiles { assertEquals(expected, result) } + /** + * Tests parsing of a comprehensive JSON5 example from `kitchen-sink.json5`. + * This file includes a variety of JSON5 features: unquoted keys, single-quoted strings, + * strings with line breaks, hexadecimal numbers, numbers with leading/trailing decimal points, + * explicit positive signs, and trailing commas in objects and arrays. + */ @Test fun testParseKitchenSinkJson5() { val path = Paths.get("src/test/resources/kitchen-sink.json5") diff --git a/lib/src/test/kotlin/io/github/json5/kotlin/JSON5StringifyTest.kt b/lib/src/test/kotlin/io/github/json5/kotlin/JSON5StringifyTest.kt index ebe975b..f0347ac 100644 --- a/lib/src/test/kotlin/io/github/json5/kotlin/JSON5StringifyTest.kt +++ b/lib/src/test/kotlin/io/github/json5/kotlin/JSON5StringifyTest.kt @@ -5,45 +5,84 @@ import io.kotest.matchers.shouldBe import org.junit.jupiter.api.Test import org.junit.jupiter.api.DisplayName +/** + * Tests for the `JSON5.stringify()` method. + * These tests verify that various Kotlin objects and values are correctly converted + * into their JSON5 string representations, including handling of JSON5-specific + * features like unquoted keys, single quotes for strings, and special numeric values. + */ @DisplayName("JSON5.stringify") class JSON5StringifyTest { + /** + * Tests stringifying an empty Kotlin Map. + * Expected output is an empty JSON5 object: `{}`. + */ @Test fun `should stringify empty object`() { JSON5.stringify(mapOf()) shouldBe "{}" } + /** + * Tests stringifying a simple Kotlin Map with a string value. + * Expected output is a JSON5 object with an unquoted key and a single-quoted string value: `{key:'value'}`. + */ @Test fun `should stringify simple object with string value`() { JSON5.stringify(mapOf("key" to "value")) shouldBe "{key:'value'}" } + /** + * Tests stringifying a simple Kotlin Map with integer and floating-point number values. + * Expected output uses unquoted keys and standard number representations: `{key:42}` and `{key:42.5}`. + */ @Test fun `should stringify simple object with number value`() { JSON5.stringify(mapOf("key" to 42)) shouldBe "{key:42}" JSON5.stringify(mapOf("key" to 42.5)) shouldBe "{key:42.5}" } + /** + * Tests stringifying a simple Kotlin Map with a boolean value. + * Expected output uses an unquoted key and the literal `true`: `{key:true}`. + */ @Test fun `should stringify simple object with boolean value`() { JSON5.stringify(mapOf("key" to true)) shouldBe "{key:true}" } + /** + * Tests stringifying a simple Kotlin Map with a null value. + * Expected output uses an unquoted key and the literal `null`: `{key:null}`. + */ @Test fun `should stringify simple object with null value`() { JSON5.stringify(mapOf("key" to null)) shouldBe "{key:null}" } + /** + * Tests stringifying an empty Kotlin List. + * Expected output is an empty JSON5 array: `[]`. + */ @Test fun `should stringify empty array`() { JSON5.stringify(emptyList()) shouldBe "[]" } + /** + * Tests stringifying a Kotlin List with various primitive values. + * Expected output is a JSON5 array with numbers, a single-quoted string, and literals: `[1,'string',true,null]`. + */ @Test fun `should stringify array with values`() { JSON5.stringify(listOf(1, "string", true, null)) shouldBe "[1,'string',true,null]" } + /** + * Tests stringifying a Kotlin Map containing nested objects and arrays. + * Verifies that complex structures are correctly represented in JSON5. + * Expected: `{object:{key:'value'},array:[1,2,3]}`. + */ @Test fun `should stringify nested objects and arrays`() { val nested = mapOf( @@ -53,12 +92,22 @@ class JSON5StringifyTest { JSON5.stringify(nested) shouldBe "{object:{key:'value'},array:[1,2,3]}" } + /** + * Tests stringifying a Kotlin Map where keys are not valid ECMAScript 5.1 identifiers + * (e.g., contain hyphens or spaces). + * Expected output encloses such keys in single quotes: `{'special-key':1,' ':2}`. + */ @Test fun `should stringify object with non-identifier keys`() { val obj = mapOf("special-key" to 1, " " to 2) JSON5.stringify(obj) shouldBe "{'special-key':1,' ':2}" } + /** + * Tests stringifying special numeric values: `Infinity`, `-Infinity`, and `NaN`. + * JSON5 allows these literals directly. + * Expected outputs: `"Infinity"`, `"-Infinity"`, `"NaN"`. + */ @Test fun `should stringify special number values`() { JSON5.stringify(Double.POSITIVE_INFINITY) shouldBe "Infinity" @@ -66,6 +115,10 @@ class JSON5StringifyTest { JSON5.stringify(Double.NaN) shouldBe "NaN" } + /** + * Tests that attempting to stringify an object with circular references throws a [JSON5Exception]. + * Circular references cannot be represented in JSON or JSON5. + */ @Test fun `should throw on circular references`() { val circular = mutableMapOf() @@ -76,6 +129,11 @@ class JSON5StringifyTest { } } + /** + * Tests stringifying a Kotlin Map with indentation. + * When a `space` argument (number of spaces) is provided, the output JSON5 string should be pretty-printed. + * Expected output for `mapOf("key" to "value")` with `space = 2`: `{\n key: 'value'\n}`. + */ @Test fun `should stringify with indentation when space is provided`() { JSON5.stringify(mapOf("key" to "value"), space = 2) shouldBe "{\n key: 'value'\n}"