diff --git a/.eslintrc.yml b/.eslintrc.yml index 0c335e97d..639e2c15f 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -137,3 +137,4 @@ overrides: - "src/language-js/parser-babel.js" - "src/language-js/parser-meriyah.js" - "src/language-js/pragma.js" + - "src/language-js/parser/json.js" diff --git a/src/language-js/parser-babel.js b/src/language-js/parser-babel.js index d7a9ac327..ea4f5f107 100644 --- a/src/language-js/parser-babel.js +++ b/src/language-js/parser-babel.js @@ -1,7 +1,6 @@ "use strict"; const flatten = require("lodash/flatten"); -const createError = require("../common/parser-create-error"); const tryCombinations = require("../utils/try-combinations"); const { getNextNonSpaceNonCommentCharacterIndexWithStartIndex, @@ -9,6 +8,8 @@ const { } = require("../common/util"); const postprocess = require("./parse-postprocess"); const createParser = require("./parser/create-parser"); +const createBabelParseError = require("./parser/create-babel-parse-error"); +const jsonParsers = require("./parser/json"); const parseOptions = { sourceType: "module", @@ -87,19 +88,6 @@ function parseWithOptions(parseMethod, text, options) { return ast; } -function createParseError(error) { - // babel error prints (l:c) with cols that are zero indexed - // so we need our custom error - const { message, loc } = error; - - return createError(message.replace(/ \(.*\)/, ""), { - start: { - line: loc ? loc.line : 0, - column: loc ? loc.column + 1 : 0, - }, - }); -} - function createParse(parseMethod, ...pluginCombinations) { const commonPlugins = parseOptions.plugins; pluginCombinations = @@ -135,7 +123,7 @@ function createParse(parseMethod, ...pluginCombinations) { ); if (!ast) { - throw createParseError(error); + throw createBabelParseError(error); } return postprocess(ast, { ...opts, originalText: text }); @@ -153,7 +141,6 @@ const parseTypeScript = createParse( ["typescript"] ); const parseExpression = createParse("parseExpression", ["jsx"]); -const parseJson = createJsonParse(); const messagesShouldThrow = new Set([ // TSErrors.UnexpectedTypeAnnotation @@ -182,96 +169,6 @@ function shouldRethrowRecoveredError(error) { return messagesShouldThrow.has(message); } -function createJsonParse(options = {}) { - const { allowComments = true } = options; - - return function parse(text /*, parsers, options*/) { - let ast; - try { - ast = require("@babel/parser").parseExpression(text, { - tokens: true, - ranges: true, - }); - } catch (error) { - throw createParseError(error); - } - - if (!allowComments) { - // @ts-ignore - for (const comment of ast.comments) { - assertJsonNode(comment); - } - } - - assertJsonNode(ast); - - return ast; - }; -} - -function assertJsonNode(node, parent) { - switch (node.type) { - case "ArrayExpression": - for (const element of node.elements) { - assertJsonChildNode(element); - } - return; - case "ObjectExpression": - for (const property of node.properties) { - assertJsonChildNode(property); - } - return; - case "ObjectProperty": - if (node.computed) { - throw createJsonError("computed"); - } - - if (node.shorthand) { - throw createJsonError("shorthand"); - } - - assertJsonChildNode(node.key); - assertJsonChildNode(node.value); - return; - case "UnaryExpression": - switch (node.operator) { - case "+": - case "-": - return assertJsonChildNode(node.argument); - default: - throw createJsonError("operator"); - } - case "Identifier": - if (parent && parent.type === "ObjectProperty" && parent.key === node) { - return; - } - throw createJsonError(); - case "NullLiteral": - case "BooleanLiteral": - case "NumericLiteral": - case "StringLiteral": - return; - default: - throw createJsonError(); - } - - function assertJsonChildNode(child) { - return assertJsonNode(child, node); - } - - function createJsonError(attribute) { - const name = !attribute - ? node.type - : `${node.type} with ${attribute}=${JSON.stringify(node[attribute])}`; - return createError(`${name} is not allowed in JSON.`, { - start: { - line: node.loc.start.line, - column: node.loc.start.column + 1, - }, - }); - } -} - const babel = createParser(parse); const babelExpression = createParser(parseExpression); @@ -281,17 +178,7 @@ module.exports = { babel, "babel-flow": createParser(parseFlow), "babel-ts": createParser(parseTypeScript), - json: createParser({ - parse: parseJson, - hasPragma() { - return true; - }, - }), - json5: createParser(parseJson), - "json-stringify": createParser({ - parse: createJsonParse({ allowComments: false }), - astFormat: "estree-json", - }), + ...jsonParsers, /** @internal */ __js_expression: babelExpression, /** for vue filter */ diff --git a/src/language-js/parser/create-babel-parse-error.js b/src/language-js/parser/create-babel-parse-error.js new file mode 100644 index 000000000..7d14c0634 --- /dev/null +++ b/src/language-js/parser/create-babel-parse-error.js @@ -0,0 +1,18 @@ +"use strict"; + +const createError = require("../../common/parser-create-error"); + +function createBabelParseError(error) { + // babel error prints (l:c) with cols that are zero indexed + // so we need our custom error + const { message, loc } = error; + + return createError(message.replace(/ \(.*\)/, ""), { + start: { + line: loc ? loc.line : 0, + column: loc ? loc.column + 1 : 0, + }, + }); +} + +module.exports = createBabelParseError; diff --git a/src/language-js/parser/json.js b/src/language-js/parser/json.js new file mode 100644 index 000000000..c1717f5b8 --- /dev/null +++ b/src/language-js/parser/json.js @@ -0,0 +1,113 @@ +"use strict"; + +const createError = require("../../common/parser-create-error"); +const createParser = require("./create-parser"); +const createBabelParseError = require("./create-babel-parse-error"); + +function createJsonParse(options = {}) { + const { allowComments = true } = options; + + return function parse(text /*, parsers, options*/) { + let ast; + try { + ast = require("@babel/parser").parseExpression(text, { + tokens: true, + ranges: true, + }); + } catch (error) { + throw createBabelParseError(error); + } + + if (!allowComments) { + // @ts-ignore + for (const comment of ast.comments) { + assertJsonNode(comment); + } + } + + assertJsonNode(ast); + + return ast; + }; +} + +function assertJsonNode(node, parent) { + switch (node.type) { + case "ArrayExpression": + for (const element of node.elements) { + assertJsonChildNode(element); + } + return; + case "ObjectExpression": + for (const property of node.properties) { + assertJsonChildNode(property); + } + return; + case "ObjectProperty": + if (node.computed) { + throw createJsonError("computed"); + } + + if (node.shorthand) { + throw createJsonError("shorthand"); + } + + assertJsonChildNode(node.key); + assertJsonChildNode(node.value); + return; + case "UnaryExpression": + switch (node.operator) { + case "+": + case "-": + return assertJsonChildNode(node.argument); + default: + throw createJsonError("operator"); + } + case "Identifier": + if (parent && parent.type === "ObjectProperty" && parent.key === node) { + return; + } + throw createJsonError(); + case "NullLiteral": + case "BooleanLiteral": + case "NumericLiteral": + case "StringLiteral": + return; + default: + throw createJsonError(); + } + + function assertJsonChildNode(child) { + return assertJsonNode(child, node); + } + + function createJsonError(attribute) { + const name = !attribute + ? node.type + : `${node.type} with ${attribute}=${JSON.stringify(node[attribute])}`; + return createError(`${name} is not allowed in JSON.`, { + start: { + line: node.loc.start.line, + column: node.loc.start.column + 1, + }, + }); + } +} + +const parseJson = createJsonParse(); + +const jsonParsers = { + json: createParser({ + parse: parseJson, + hasPragma() { + return true; + }, + }), + json5: createParser(parseJson), + "json-stringify": createParser({ + parse: createJsonParse({ allowComments: false }), + astFormat: "estree-json", + }), +}; + +module.exports = jsonParsers;