diff options
author | Louis Pilfold <louis@lpil.uk> | 2023-03-02 11:27:16 +0000 |
---|---|---|
committer | Louis Pilfold <louis@lpil.uk> | 2023-03-02 11:28:00 +0000 |
commit | dc0d1c70cdedd46c327bf00d03ac283b0938b16e (patch) | |
tree | 1f8d9b40ef9baf80242cdc31dd5c399c9f676037 /src/gleam_json_ffi.mjs | |
parent | 74df14ca3b9f8cab03cfbc660d875d40f66b8e5e (diff) | |
download | gleam_json-dc0d1c70cdedd46c327bf00d03ac283b0938b16e.tar.gz gleam_json-dc0d1c70cdedd46c327bf00d03ac283b0938b16e.zip |
Update error parsing for latest V8
Diffstat (limited to 'src/gleam_json_ffi.mjs')
-rw-r--r-- | src/gleam_json_ffi.mjs | 221 |
1 files changed, 114 insertions, 107 deletions
diff --git a/src/gleam_json_ffi.mjs b/src/gleam_json_ffi.mjs index b60d848..85a81b0 100644 --- a/src/gleam_json_ffi.mjs +++ b/src/gleam_json_ffi.mjs @@ -1,46 +1,38 @@ -import { Ok, Error } from './gleam.mjs' -import { UnexpectedByte, UnexpectedEndOfInput } from './gleam/json.mjs' - -const Runtime = { - chromium: 'chromium', - spidermonkey: 'spidermonkey', - jscore: 'jscore', -} +import { Ok, Error } from "./gleam.mjs"; +import { UnexpectedByte, UnexpectedEndOfInput } from "./gleam/json.mjs"; export function json_to_string(json) { - return JSON.stringify(json) + return JSON.stringify(json); } export function object(entries) { - return Object.fromEntries(entries) + return Object.fromEntries(entries); } export function identity(x) { - return x + return x; } export function array(list) { - return list.toArray() + return list.toArray(); } export function do_null() { - return null + return null; } export function decode(string) { try { - const result = JSON.parse(string) - return new Ok(result) + const result = JSON.parse(string); + return new Ok(result); } catch (err) { - return new Error(getJsonDecodeError(err, string)) + return new Error(getJsonDecodeError(err, string)); } } export function getJsonDecodeError(stdErr, json) { - if (isUnexpectedEndOfInput(stdErr)) return new UnexpectedEndOfInput() - const unexpectedByteRuntime = getUnexpectedByteRuntime(stdErr) - if (unexpectedByteRuntime) return toUnexpectedByteError(unexpectedByteRuntime, stdErr, json) - return new UnexpectedByte('', 0) + if (isUnexpectedEndOfInput(stdErr)) return new UnexpectedEndOfInput(); + return toUnexpectedByteError(stdErr, json); } /** @@ -48,139 +40,154 @@ export function getJsonDecodeError(stdErr, json) { * - Chromium (edge, chrome, node) * - Spidermonkey (firefox) * - JavascriptCore (safari) - * + * * Note that Spidermonkey and JavascriptCore will both incorrectly report some * UnexpectedByte errors as UnexpectedEndOfInput errors. For example: - * + * * @example * // in JavascriptCore * JSON.parse('{"a"]: "b"}) * // => JSON Parse error: Expected ':' before value - * + * * JSON.parse('{"a"') * // => JSON Parse error: Expected ':' before value - * + * * // in Chromium (correct) * JSON.parse('{"a"]: "b"}) * // => Unexpected token ] in JSON at position 4 - * + * * JSON.parse('{"a"') * // => Unexpected end of JSON input */ -const unexpectedEndOfInputRegex = /((unexpected (end|eof))|(end of data)|(unterminated string)|(json( parse error|\.parse)\: expected '(\:|\}|\])'))/i - function isUnexpectedEndOfInput(err) { - return unexpectedEndOfInputRegex.test(err.message) + const unexpectedEndOfInputRegex = + /((unexpected (end|eof))|(end of data)|(unterminated string)|(json( parse error|\.parse)\: expected '(\:|\}|\])'))/i; + return unexpectedEndOfInputRegex.test(err.message); } +/** + * Converts a SyntaxError to an UnexpectedByte error based on the JS runtime. + * + * For Chromium, the unexpected byte and position are reported by the runtime. + * + * For JavascriptCore, only the unexpected byte is reported by the runtime, so + * there is no way to know which position that character is in unless we then + * parse the string again ourselves. So instead, the position is reported as 0. + * + * For Spidermonkey, the position is reported by the runtime as a line and column number + * and the unexpected byte is found using those coordinates. + * + * @param {'chromium' | 'spidermonkey' | 'jscore'} runtime + * @param {SyntaxError} err + * @param {string} json + * @returns {UnexpectedByte} + */ +function toUnexpectedByteError(err, json) { + let converters = [ + v8UnexpectedByteError, + oldV8UnexpectedByteError, + jsCoreUnexpectedByteError, + spidermonkeyUnexpectedByteError, + ]; + + for (let converter of converters) { + let result = converter(err, json); + if (result) return result; + } + + return new UnexpectedByte("", 0); +} /** * Matches unexpected byte messages in: - * - Chromium (edge, chrome, node) - * - * Matches the character and its position. + * - V8 (edge, chrome, node) + * + * Matches the character but not the position as this is no longer reported by + * V8. Boo! */ -const chromiumUnexpectedByteRegex = /unexpected token (.) in JSON at position (\d+)/i +function v8UnexpectedByteError(err) { + const regex = /unexpected token '(.)', ".+" is not valid JSON/i; + const match = regex.exec(err.message); + if (!match) return null; + const byte = toHex(match[1]); + return new UnexpectedByte(byte, -1); +} /** * Matches unexpected byte messages in: - * - JavascriptCore (safari) - * - * JavascriptCore only reports what the character is and not its position. + * - V8 (edge, chrome, node) + * + * No longer works in current versions of V8. + * + * Matches the character and its position. */ -const jsCoreUnexpectedByteRegex = /unexpected (identifier|token) "(.)"/i +function oldV8UnexpectedByteError(err) { + const regex = /unexpected token (.) in JSON at position (\d+)/i; + const match = regex.exec(err.message); + if (!match) return null; + const byte = toHex(match[1]); + const position = Number(match[2]); + return new UnexpectedByte(byte, position); +} /** * Matches unexpected byte messages in: * - Spidermonkey (firefox) - * + * * Matches the position in a 2d grid only and not the character. */ -const spidermonkeyUnexpectedByteRegex = /(unexpected character|expected .*) at line (\d+) column (\d+)/i - -function getUnexpectedByteRuntime(err) { - if (chromiumUnexpectedByteRegex.test(err.message)) return Runtime.chromium - if (jsCoreUnexpectedByteRegex.test(err.message)) return Runtime.jscore - if (spidermonkeyUnexpectedByteRegex.test(err.message)) return Runtime.spidermonkey - return null +function spidermonkeyUnexpectedByteError(err, json) { + const regex = + /(unexpected character|expected .*) at line (\d+) column (\d+)/i; + const match = regex.exec(err.message); + if (!match) return null; + const line = Number(match[2]); + const column = Number(match[3]); + const position = getPositionFromMultiline(line, column, json); + const byte = toHex(json[position]); + return new UnexpectedByte(byte, position); } /** - * Converts a SyntaxError to an UnexpectedByte error based on the JS runtime. - * - * For Chromium, the unexpected byte and position are reported by the runtime. - * - * For JavascriptCore, only the unexpected byte is reported by the runtime, so - * there is no way to know which position that character is in unless we then - * parse the string again ourselves. So instead, the position is reported as 0. - * - * For Spidermonkey, the position is reported by the runtime as a line and column number - * and the unexpected byte is found using those coordinates. - * - * @param {'chromium' | 'spidermonkey' | 'jscore'} runtime - * @param {SyntaxError} err - * @param {string} json - * @returns {UnexpectedByte} + * Matches unexpected byte messages in: + * - JavascriptCore (safari) + * + * JavascriptCore only reports what the character is and not its position. */ -function toUnexpectedByteError(runtime, err, json) { - switch (runtime) { - case Runtime.chromium: - return toChromiumUnexpectedByteError(err) - case Runtime.spidermonkey: - return toSpidermonkeyUnexpectedByteError(err, json) - case Runtime.jscore: - return toJsCoreUnexpectedByteError(err) - } -} - -function toChromiumUnexpectedByteError(err) { - const match = chromiumUnexpectedByteRegex.exec(err.message) - const byte = toHex(match[1]) - const position = Number(match[2]) - return new UnexpectedByte(byte, position) -} - -function toSpidermonkeyUnexpectedByteError(err, json) { - const match = spidermonkeyUnexpectedByteRegex.exec(err.message) - const line = Number(match[2]) - const column = Number(match[3]) - const position = getPositionFromMultiline(line, column, json) - const byte = toHex(json[position]) - return new UnexpectedByte(byte, position) -} - -function toJsCoreUnexpectedByteError(err) { - const match = jsCoreUnexpectedByteRegex.exec(err.message) - const byte = toHex(match[2]) - return new UnexpectedByte(byte, 0) +function jsCoreUnexpectedByteError(err) { + const regex = /unexpected (identifier|token) "(.)"/i; + const match = regex.exec(err.message); + if (!match) return null; + const byte = toHex(match[2]); + return new UnexpectedByte(byte, 0); } function toHex(char) { - return "0x" + char.charCodeAt(0).toString(16).toUpperCase() + return "0x" + char.charCodeAt(0).toString(16).toUpperCase(); } /** * Gets the position of a character in a flattened (i.e. single line) string - * from a line and column number. Note that the position is 0-indexed and + * from a line and column number. Note that the position is 0-indexed and * the line and column numbers are 1-indexed. - * - * @param {number} line - * @param {number} column - * @param {string} string + * + * @param {number} line + * @param {number} column + * @param {string} string */ function getPositionFromMultiline(line, column, string) { - if (line === 1) return column - 1 + if (line === 1) return column - 1; - let currentLn = 1 - let position = 0 - string.split('').find((char, idx) => { - if (char === '\n') currentLn += 1 + let currentLn = 1; + let position = 0; + string.split("").find((char, idx) => { + if (char === "\n") currentLn += 1; if (currentLn === line) { - position = idx + column - return true + position = idx + column; + return true; } - return false - }) + return false; + }); - return position -}
\ No newline at end of file + return position; +} |