diff --git a/lib/__tests__/json-utils.test.ts b/lib/__tests__/json-utils.test.ts index 0ca5d19..1635d3c 100644 --- a/lib/__tests__/json-utils.test.ts +++ b/lib/__tests__/json-utils.test.ts @@ -11,10 +11,68 @@ import { escapeForCode, parseJson5Compat, diffLines, + validate, } from "../json-utils"; const BIG = '{"id":12345678901234567890,"pi":3.141592653589793238,"n":42}'; +describe("JSON Formatter Error Locations", () => { + it("extracts correct location for unquoted values", () => { + const result = validate('{"a": x}'); + expect(result.ok).toBe(false); + if (!result.ok) { + expect(result.error.line).toBe(1); + expect(result.error.column).toBe(7); + } + }); + + it("extracts correct location for multi-line JSON", () => { + const input = '{"a": 1,\n"b": tru\n}'; + const result = validate(input); + expect(result.ok).toBe(false); + if (!result.ok) { + expect(result.error.line).toBe(2); + expect(result.error.column).toBe(6); + } + }); + + it("keeps original location for duplicate keys with trailing comma", () => { + const result = validate('{"a":1,"a":2,}'); + expect(result.ok).toBe(false); + if (!result.ok) { + expect(result.error.line).toBe(1); + expect(result.error.column).toBe(14); + } + }); + + it("extracts correct location for unexpected end of input", () => { + const result = validate('{"a": '); + expect(result.ok).toBe(false); + if (!result.ok) { + expect(result.error.line).toBe(1); + expect(result.error.column).toBe(7); + } + }); + + it("extracts correct location for undefined in array", () => { + const result = validate('[1, 2, undefined]'); + expect(result.ok).toBe(false); + if (!result.ok) { + expect(result.error.line).toBe(1); + expect(result.error.column).toBe(8); + } + }); + + it("keeps existing correct locations intact", () => { + const result = validate('{"a":1,}'); + expect(result.ok).toBe(false); + if (!result.ok) { + expect(result.error.line).toBe(1); + expect(result.error.column).toBe(8); + } + }); +}); + describe("lossless numbers", () => { it("preserves large integers and long decimals when formatting", () => { const r = format(BIG, 2); diff --git a/lib/json-utils.ts b/lib/json-utils.ts index 5b9db4f..5c8e9fe 100644 --- a/lib/json-utils.ts +++ b/lib/json-utils.ts @@ -35,9 +35,11 @@ function parseSyntaxError(raw: string, err: unknown): JsonError { const msg = err instanceof Error ? err.message : String(err); const posMatch = msg.match(/position (\d+)/i); const lineMatch = msg.match(/line (\d+) column (\d+)/i); + let line = 1; let column = 1; let position = 0; + if (posMatch) { position = parseInt(posMatch[1], 10); const lc = computeLineCol(raw, position); @@ -46,11 +48,26 @@ function parseSyntaxError(raw: string, err: unknown): JsonError { } else if (lineMatch) { line = parseInt(lineMatch[1], 10); column = parseInt(lineMatch[2], 10); + } else { + try { + llParse(raw); + } catch (llError) { + if (llError instanceof Error) { + const llPosMatch = llError.message.match(/position (\d+)/i); + if (llPosMatch) { + position = parseInt(llPosMatch[1], 10); + const lc = computeLineCol(raw, position); + line = lc.line; + column = lc.column; + } + } + } } + const suggestion = suggestFix(raw); return { message: msg.replace(/in JSON.*$/, "").trim() || "Invalid JSON", line, column, position, suggestion }; } - + export function validate(raw: string): Result { if (!raw.trim()) { return {