Files
scriptc/packages/runtime/test/json.test.ts
T
Malte UblandClaude Opus 5.5 d037c66e8c Report JSON.parse syntax errors with V8's messages and positions
JSON.parse's SyntaxError messages were shaped like V8's but differed from
Node's in several ways:
- Every positional message now ends with V8's "(line L column C)", where
  \n, \r and \r\n end lines.
- Positions are UTF-16 offsets, as in Node, not byte offsets.
- An object or array cut off after '{', a property name, a value or an
  array element reports the token V8 expected ("Expected property name or
  '}'", "Expected ':' after property name", "Expected ',' or '}' after
  property value", "Expected ',' or ']' after array element") instead of
  "Unexpected end of JSON input".
- After a comma in an object the message is "Expected double-quoted
  property name".
- An unterminated string reports the end of the input, and a truncated
  \u escape is a "Bad Unicode escape" there.
- A leading zero followed by a digit is an "Unexpected number".
- A misspelled literal names the first differing character, or the end of
  the input.
- An unexpected character is named with the source as V8 does: whole when
  it is under 21 units, otherwise ten units of context on each side; the
  sources "NaN", "Infinity", "undefined" and "[object Object]" name only
  themselves; a quote or a number there is an "Unexpected string" or
  "Unexpected number".

The new corpus program 7993-json-parse-error-messages pins 76 inputs
against Node, including non-ASCII text before the error, line breaks, and
long sources with split surrogate pairs at the context edges.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 09:55:05 -07:00

87 lines
4.1 KiB
TypeScript

import { execFile } from "node:child_process";
import { mkdir } from "node:fs/promises";
import { join } from "node:path";
import { promisify } from "node:util";
import { beforeAll, expect, test } from "vitest";
const execFileAsync = promisify(execFile);
const testDir = import.meta.dirname;
const bin = join(testDir, "build", "test_json");
// Built with ASan + the RC audit: a clean exit also proves the checked-dynamic tree's
// recursive ownership — including trees abandoned mid-parse by syntax
// errors — leaks nothing and frees nothing twice.
beforeAll(async () => {
await mkdir(join(testDir, "build"), { recursive: true });
await execFileAsync("clang", [
"-std=c11",
"-O1",
"-Wall",
"-Wextra",
"-fsanitize=address",
"-DSCR_RC_AUDIT",
"-I",
join(testDir, "../src"),
"-o",
bin,
join(testDir, "test_json.c"),
join(testDir, "../src/scr_json.c"),
join(testDir, "../src/scr_bigint.c"),
join(testDir, "../src/scr_string.c"),
join(testDir, "../src/scr_array.c"),
join(testDir, "../src/scr_map.c"),
join(testDir, "../src/scr_exception.c"),
join(testDir, "../src/scr_error.c"),
join(testDir, "../src/scr_number.c"),
join(testDir, "../src/scr_console.c"),
join(testDir, "../src/scr_closure.c"),
join(testDir, "../src/scr_object.c"),
join(testDir, "../src/scr_union.c"),
join(testDir, "../src/scr_cycle.c"),
join(testDir, "../src/scr_alloc.c"),
join(testDir, "../src/scr_lib.c"),
join(testDir, "../src/scr_hash.c"),
join(testDir, "../src/scr_url.c"),
join(testDir, "../src/scr_url_params.c"),
join(testDir, "../src/scr_path.c"),
join(testDir, "../src/scr_bytes.c"),
join(testDir, "../src/scr_shared.c"),
...(process.platform === "linux" ? ["-D_GNU_SOURCE", "-lm"] : []),
]);
});
// The C side parses/inspects dyn values as checks and routes each expected
// failure through scr_exc_print_uncaught. Parse failures are SyntaxError
// INSTANCES (the depth cap a RangeError, dynCheck failures TypeErrors), so
// every uncaught line renders "name: message"; the JSON syntax error
// messages are Node's (V8's wording, positions and line/column).
test("json runtime: parser, dyn, dynCheck failure path, stringify buffer", async () => {
const { stdout, stderr } = await execFileAsync(bin);
expect(stdout).toMatch(/^(\d+)\/\1 checks passed\n$/);
expect(stderr).toBe(
[
`Uncaught SyntaxError: Unexpected end of JSON input`,
`Uncaught SyntaxError: Unexpected end of JSON input`,
`Uncaught SyntaxError: Expected property name or '}' in JSON at position 1 (line 1 column 2)`,
`Uncaught SyntaxError: Unexpected end of JSON input`,
`Uncaught SyntaxError: Expected ',' or ']' after array element in JSON at position 3 (line 1 column 4)`,
`Uncaught SyntaxError: Expected ':' after property name in JSON at position 4 (line 1 column 5)`,
`Uncaught SyntaxError: Expected double-quoted property name in JSON at position 7 (line 1 column 8)`,
`Uncaught SyntaxError: Expected property name or '}' in JSON at position 1 (line 1 column 2)`,
`Uncaught SyntaxError: Unterminated string in JSON at position 13 (line 1 column 14)`,
`Uncaught SyntaxError: Bad escaped character in JSON at position 6 (line 1 column 7)`,
`Uncaught SyntaxError: Bad control character in string literal in JSON at position 6 (line 1 column 7)`,
`Uncaught SyntaxError: Bad Unicode escape in JSON at position 5 (line 1 column 6)`,
`Uncaught SyntaxError: No number after minus sign in JSON at position 1 (line 1 column 2)`,
`Uncaught SyntaxError: Unterminated fractional number in JSON at position 2 (line 1 column 3)`,
`Uncaught SyntaxError: Exponent part is missing a number in JSON at position 3 (line 1 column 4)`,
`Uncaught SyntaxError: Unexpected non-whitespace character after JSON at position 2 (line 1 column 3)`,
`Uncaught SyntaxError: Unexpected end of JSON input`,
`Uncaught RangeError: Maximum call stack size exceeded`,
`Uncaught TypeError: expected number at $.items[2].price, got string`,
`Uncaught TypeError: expected object at $, got undefined`,
``,
].join("\n"),
);
});