From c7d0809431a19f2a4850f75aa371004cf2caab47 Mon Sep 17 00:00:00 2001 From: fisker Date: Thu, 20 Mar 2025 22:17:40 +0800 Subject: [PATCH 1/6] Add JSONError.cause --- index.js | 71 +++++++++++++++++++++++++++++++------------------------- test.js | 19 +++++++++++++++ 2 files changed, 59 insertions(+), 31 deletions(-) diff --git a/index.js b/index.js index 771ccd4..ceb5189 100644 --- a/index.js +++ b/index.js @@ -6,31 +6,56 @@ const getCodePoint = character => `\\u{${character.codePointAt(0).toString(16)}} export class JSONError extends Error { name = 'JSONError'; fileName; - codeFrame; - rawCodeFrame; + #input; + #jsonParseError; #message; + #codeFrame; + #rawCodeFrame; - constructor(message) { + constructor({jsonParseError, fileName, input}) { // We cannot pass message to `super()`, otherwise the message accessor will be overridden. // https://262.ecma-international.org/14.0/#sec-error-message - super(); + super(undefined, {cause: jsonParseError}); + + this.#input = input; + this.#jsonParseError = jsonParseError; + this.fileName = fileName; - this.#message = message; Error.captureStackTrace?.(this, JSONError); } get message() { - const {fileName, codeFrame} = this; - return `${this.#message}${fileName ? ` in ${fileName}` : ''}${codeFrame ? `\n\n${codeFrame}\n` : ''}`; + this.#message ??= `${addCodePointToUnexpectedToken(this.#jsonParseError.message)}${this.#input === '' ? ' while parsing empty string' : ''}`; + + const {codeFrame} = this; + return `${this.#message}${this.fileName ? ` in ${this.fileName}` : ''}${codeFrame ? `\n\n${codeFrame}\n` : ''}`; } set message(message) { this.#message = message; } -} -const generateCodeFrame = (string, location, highlightCode = true) => - codeFrameColumns(string, {start: location}, {highlightCode}); + #getCodeFrame(highlightCode) { + const input = this.#input; + + const location = getErrorLocation(input, this.#jsonParseError.message); + if (!location) { + return; + } + + return codeFrameColumns(input, {start: location}, {highlightCode}); + } + + get codeFrame() { + this.#codeFrame ??= this.#getCodeFrame(/* highlightCode */ true); + return this.#codeFrame; + } + + get rawCodeFrame() { + this.#rawCodeFrame ??= this.#getCodeFrame(/* highlightCode */ false); + return this.#rawCodeFrame; + } +} const getErrorLocation = (string, message) => { const match = message.match(/in JSON at position (?\d+)(?: \(line (?\d+) column (?\d+)\))?$/); @@ -68,29 +93,13 @@ export default function parseJson(string, reviver, fileName) { reviver = undefined; } - let message; try { return JSON.parse(string, reviver); } catch (error) { - message = error.message; + throw new JSONError({ + jsonParseError: error, + fileName, + input: string, + }); } - - let location; - if (string) { - location = getErrorLocation(string, message); - message = addCodePointToUnexpectedToken(message); - } else { - message += ' while parsing empty string'; - } - - const jsonError = new JSONError(message); - - jsonError.fileName = fileName; - - if (location) { - jsonError.codeFrame = generateCodeFrame(string, location); - jsonError.rawCodeFrame = generateCodeFrame(string, location, /* highlightCode */ false); - } - - throw jsonError; } diff --git a/test.js b/test.js index 68cde9d..950c878 100644 --- a/test.js +++ b/test.js @@ -67,6 +67,25 @@ test('main', t => { }, { message: errorMessageRegexWithFileName, }); + + { + let nativeJsonParseError; + try { + JSON.parse(INVALID_JSON_STRING); + } catch (error) { + nativeJsonParseError = error; + } + + let jsonError; + try { + parseJson(INVALID_JSON_STRING); + } catch (error) { + jsonError = error; + } + + t.is(nativeJsonParseError.name, 'SyntaxError'); + t.deepEqual(nativeJsonParseError, jsonError.cause); + } }); test('throws exported error error', t => { From 2d46be2a382d28183b89e7299add239f7cca5a71 Mon Sep 17 00:00:00 2001 From: fisker Date: Thu, 20 Mar 2025 22:23:13 +0800 Subject: [PATCH 2/6] Update readme.md --- readme.md | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/readme.md b/readme.md index a8919ff..be57c75 100644 --- a/readme.md +++ b/readme.md @@ -18,16 +18,13 @@ const json = '{\n\t"foo": true,\n}'; JSON.parse(json); /* -undefined:3 -} -^ -SyntaxError: Unexpected token } +SyntaxError: Expected double-quoted property name in JSON at position 16 (line 3 column 1) */ parseJson(json); /* -JSONError: Unexpected token } in JSON at position 16 while parsing near '{ "foo": true,}' +JSONError: Expected double-quoted property name in JSON at position 16 (line 3 column 1) 1 | { 2 | "foo": true, @@ -38,12 +35,16 @@ JSONError: Unexpected token } in JSON at position 16 while parsing near '{ parseJson(json, 'foo.json'); /* -JSONError: Unexpected token } in JSON at position 16 while parsing near '{ "foo": true,}' in foo.json +JSONError: Expected double-quoted property name in JSON at position 16 (line 3 column 1) in foo.json 1 | { 2 | "foo": true, > 3 | } | ^ + fileName: 'foo.json', + [cause]: SyntaxError: Expected double-quoted property name in JSON at position 16 (line 3 column 1) + at JSON.parse () + at ... */ @@ -58,12 +59,17 @@ try { throw error; } /* -JSONError: Unexpected token } in JSON at position 16 while parsing near '{ "foo": true,}' in foo.json +JSONError: Expected double-quoted property name in JSON at position 16 (line 3 column 1) in foo.json 1 | { 2 | "foo": true, > 3 | } | ^ + + fileName: 'foo.json', + [cause]: SyntaxError: Expected double-quoted property name in JSON at position 16 (line 3 column 1) + at JSON.parse () + at ... */ ``` From df9eb29ea211fb30436d79df8de78ca191df9b53 Mon Sep 17 00:00:00 2001 From: fisker Date: Thu, 20 Mar 2025 22:26:14 +0800 Subject: [PATCH 3/6] Update example --- index.d.ts | 42 ++++++++++++++++++++++++++++++------------ 1 file changed, 30 insertions(+), 12 deletions(-) diff --git a/index.d.ts b/index.d.ts index 5cb3bed..683d141 100644 --- a/index.d.ts +++ b/index.d.ts @@ -40,20 +40,24 @@ import parseJson, {JSONError} from 'parse-json'; const json = '{\n\t"foo": true,\n}'; parseJson(json); -// JSONError: Unexpected token } in JSON at position 16 while parsing near '{ "foo": true,}' -// +// JSONError: Expected double-quoted property name in JSON at position 16 (line 3 column 1) + // 1 | { // 2 | "foo": true, // > 3 | } // | ^ parseJson(json, 'foo.json'); -// JSONError: Unexpected token } in JSON at position 16 while parsing near '{ "foo": true,}' in foo.json -// +// JSONError: Expected double-quoted property name in JSON at position 16 (line 3 column 1) in foo.json + // 1 | { // 2 | "foo": true, // > 3 | } // | ^ +// fileName: 'foo.json', +// [cause]: SyntaxError: Expected double-quoted property name in JSON at position 16 (line 3 column 1) +// at JSON.parse () +// at ... // You can also add the filename at a later point try { @@ -65,12 +69,17 @@ try { throw error; } -// JSONError: Unexpected token } in JSON at position 16 while parsing near '{ "foo": true,}' in foo.json -// +// JSONError: Expected double-quoted property name in JSON at position 16 (line 3 column 1) in foo.json + // 1 | { // 2 | "foo": true, // > 3 | } // | ^ + +// fileName: 'foo.json', +// [cause]: SyntaxError: Expected double-quoted property name in JSON at position 16 (line 3 column 1) +// at JSON.parse () +// at ... ``` */ export default function parseJson(string: string, reviver?: Reviver, filename?: string): JsonObject; @@ -90,20 +99,24 @@ import parseJson, {JSONError} from 'parse-json'; const json = '{\n\t"foo": true,\n}'; parseJson(json); -// JSONError: Unexpected token } in JSON at position 16 while parsing near '{ "foo": true,}' -// +// JSONError: Expected double-quoted property name in JSON at position 16 (line 3 column 1) + // 1 | { // 2 | "foo": true, // > 3 | } // | ^ parseJson(json, 'foo.json'); -// JSONError: Unexpected token } in JSON at position 16 while parsing near '{ "foo": true,}' in foo.json -// +// JSONError: Expected double-quoted property name in JSON at position 16 (line 3 column 1) in foo.json + // 1 | { // 2 | "foo": true, // > 3 | } // | ^ +// fileName: 'foo.json', +// [cause]: SyntaxError: Expected double-quoted property name in JSON at position 16 (line 3 column 1) +// at JSON.parse () +// at ... // You can also add the filename at a later point try { @@ -115,12 +128,17 @@ try { throw error; } -// JSONError: Unexpected token } in JSON at position 16 while parsing near '{ "foo": true,}' in foo.json -// +// JSONError: Expected double-quoted property name in JSON at position 16 (line 3 column 1) in foo.json + // 1 | { // 2 | "foo": true, // > 3 | } // | ^ + +// fileName: 'foo.json', +// [cause]: SyntaxError: Expected double-quoted property name in JSON at position 16 (line 3 column 1) +// at JSON.parse () +// at ... ``` */ export default function parseJson(string: string, filename?: string): JsonObject; From e97eca42eac75626cce4c37b0be2bc9a567394d2 Mon Sep 17 00:00:00 2001 From: fisker Cheung Date: Fri, 21 Mar 2025 02:45:28 +0800 Subject: [PATCH 4/6] Update test.js --- test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test.js b/test.js index 33e5724..8f5930e 100644 --- a/test.js +++ b/test.js @@ -78,7 +78,7 @@ test('main', t => { jsonError.message = 'custom error message'; t.true(jsonError.message.startsWith('custom error message in foo.json')); - // Still have code from in message. + // Still have code frame in message. t.true(stripAnsi(jsonError.message).includes('> 3 | }')); } From 04c3224d144e69367ad0d67e1c6403dda7cd6b4e Mon Sep 17 00:00:00 2001 From: fisker Date: Fri, 21 Mar 2025 14:30:13 +0800 Subject: [PATCH 5/6] Make JSONError backwards compatible --- index.js | 29 +++++++++++++++++++++-------- test.js | 21 +++++++++++++++++++++ 2 files changed, 42 insertions(+), 8 deletions(-) diff --git a/index.js b/index.js index ceb5189..87852ea 100644 --- a/index.js +++ b/index.js @@ -12,14 +12,22 @@ export class JSONError extends Error { #codeFrame; #rawCodeFrame; - constructor({jsonParseError, fileName, input}) { - // We cannot pass message to `super()`, otherwise the message accessor will be overridden. - // https://262.ecma-international.org/14.0/#sec-error-message - super(undefined, {cause: jsonParseError}); - - this.#input = input; - this.#jsonParseError = jsonParseError; - this.fileName = fileName; + constructor(messageOrOptions) { + // JSONError constructor used accept string + // TODO[>=9]: Remove this on next major version + if (typeof messageOrOptions === 'string') { + super(); + this.#message = messageOrOptions; + } else { + const {jsonParseError, fileName, input} = messageOrOptions; + // We cannot pass message to `super()`, otherwise the message accessor will be overridden. + // https://262.ecma-international.org/14.0/#sec-error-message + super(undefined, {cause: jsonParseError}); + + this.#input = input; + this.#jsonParseError = jsonParseError; + this.fileName = fileName; + } Error.captureStackTrace?.(this, JSONError); } @@ -36,6 +44,11 @@ export class JSONError extends Error { } #getCodeFrame(highlightCode) { + // TODO[>=9]: Remove this on next major version + if (!this.#jsonParseError) { + return; + } + const input = this.#input; const location = getErrorLocation(input, this.#jsonParseError.message); diff --git a/test.js b/test.js index 8f5930e..c2dde6a 100644 --- a/test.js +++ b/test.js @@ -162,3 +162,24 @@ test('Unexpected tokens', t => { } } }); + +test('JSONError legacy interface', t => { + { + const error = new JSONError('Error message'); + t.is(error.message, 'Error message'); + } + + { + const error = new JSONError('Error message'); + error.message = 'New error message'; + t.is(error.message, 'New error message'); + } + + { + const error = new JSONError('Error message'); + error.fileName = 'foo.json'; + t.is(error.message, 'Error message in foo.json'); + error.message = 'New error message'; + t.is(error.message, 'New error message in foo.json'); + } +}); From adce299f99cba25d36b944a926463643df253dc4 Mon Sep 17 00:00:00 2001 From: fisker Date: Fri, 21 Mar 2025 14:42:15 +0800 Subject: [PATCH 6/6] Update comment --- index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/index.js b/index.js index 87852ea..6a4c980 100644 --- a/index.js +++ b/index.js @@ -14,7 +14,7 @@ export class JSONError extends Error { constructor(messageOrOptions) { // JSONError constructor used accept string - // TODO[>=9]: Remove this on next major version + // TODO[>=9]: Remove this `if` on next major version if (typeof messageOrOptions === 'string') { super(); this.#message = messageOrOptions; @@ -44,7 +44,7 @@ export class JSONError extends Error { } #getCodeFrame(highlightCode) { - // TODO[>=9]: Remove this on next major version + // TODO[>=9]: Remove this `if` on next major version if (!this.#jsonParseError) { return; }