diff --git a/src/parser-babylon.js b/src/parser-babylon.js new file mode 100644 index 00000000..899910b3 --- /dev/null +++ b/src/parser-babylon.js @@ -0,0 +1,43 @@ +"use strict"; + +function parse(text) { + // Inline the require to avoid loading all the JS if we don't use it + const babylon = require("babylon"); + + const babylonOptions = { + sourceType: "module", + allowImportExportEverywhere: false, + allowReturnOutsideFunction: true, + plugins: [ + "jsx", + "flow", + "doExpressions", + "objectRestSpread", + "decorators", + "classProperties", + "exportExtensions", + "asyncGenerators", + "functionBind", + "functionSent", + "dynamicImport" + ] + }; + + let ast; + try { + ast = babylon.parse(text, babylonOptions); + } catch (originalError) { + try { + return babylon.parse( + text, + Object.assign({}, babylonOptions, { strictMode: false }) + ); + } catch (nonStrictError) { + throw originalError; + } + } + delete ast.tokens; + return ast; +} + +module.exports = parse; diff --git a/src/parser-create-error.js b/src/parser-create-error.js new file mode 100644 index 00000000..1fbb7949 --- /dev/null +++ b/src/parser-create-error.js @@ -0,0 +1,10 @@ +"use strict"; + +function createError(message, line, column) { + // Construct an error similar to the ones thrown by Babylon. + const error = new SyntaxError(message + " (" + line + ":" + column + ")"); + error.loc = { line, column }; + return error; +} + +module.exports = createError; diff --git a/src/parser-flow.js b/src/parser-flow.js new file mode 100644 index 00000000..4d02f431 --- /dev/null +++ b/src/parser-flow.js @@ -0,0 +1,26 @@ +"use strict"; + +const createError = require("./parser-create-error"); + +function parse(text) { + // Inline the require to avoid loading all the JS if we don't use it + const flowParser = require("flow-parser"); + + const ast = flowParser.parse(text, { + esproposal_class_instance_fields: true, + esproposal_class_static_fields: true, + esproposal_export_star_as: true + }); + + if (ast.errors.length > 0) { + throw createError( + ast.errors[0].message, + ast.errors[0].loc.start.line, + ast.errors[0].loc.start.column + ); + } + + return ast; +} + +module.exports = parse; diff --git a/src/parser-postcss.js b/src/parser-postcss.js new file mode 100644 index 00000000..a9d85a82 --- /dev/null +++ b/src/parser-postcss.js @@ -0,0 +1,229 @@ +"use strict"; + +const createError = require("./parser-create-error"); + +function parseSelector(selector) { + const r = require; + const selectorParser = r("postcss-selector-parser"); + let result; + selectorParser(result_ => { + result = result_; + }).process(selector); + return addTypePrefix(result, "selector-"); +} + +function parseValueNodes(nodes) { + let parenGroup = { + open: null, + close: null, + groups: [], + type: "paren_group" + }; + const parenGroupStack = [parenGroup]; + const rootParenGroup = parenGroup; + let commaGroup = { + groups: [], + type: "comma_group" + }; + const commaGroupStack = [commaGroup]; + + for (let i = 0; i < nodes.length; ++i) { + if (nodes[i].type === "paren" && nodes[i].value === "(") { + parenGroup = { + open: nodes[i], + close: null, + groups: [], + type: "paren_group" + }; + parenGroupStack.push(parenGroup); + + commaGroup = { + groups: [], + type: "comma_group" + }; + commaGroupStack.push(commaGroup); + } else if (nodes[i].type === "paren" && nodes[i].value === ")") { + if (commaGroup.groups.length) { + parenGroup.groups.push(commaGroup); + } + parenGroup.close = nodes[i]; + + if (commaGroupStack.length === 1) { + throw new Error("Unbalanced parenthesis"); + } + + commaGroupStack.pop(); + commaGroup = commaGroupStack[commaGroupStack.length - 1]; + commaGroup.groups.push(parenGroup); + + parenGroupStack.pop(); + parenGroup = parenGroupStack[parenGroupStack.length - 1]; + } else if (nodes[i].type === "comma") { + parenGroup.groups.push(commaGroup); + commaGroup = { + groups: [], + type: "comma_group" + }; + commaGroupStack[commaGroupStack.length - 1] = commaGroup; + } else { + commaGroup.groups.push(nodes[i]); + } + } + if (commaGroup.groups.length > 0) { + parenGroup.groups.push(commaGroup); + } + return rootParenGroup; +} + +function flattenGroups(node) { + if ( + node.type === "paren_group" && + !node.open && + !node.close && + node.groups.length === 1 + ) { + return flattenGroups(node.groups[0]); + } + + if (node.type === "comma_group" && node.groups.length === 1) { + return flattenGroups(node.groups[0]); + } + + if (node.type === "paren_group" || node.type === "comma_group") { + return Object.assign({}, node, { groups: node.groups.map(flattenGroups) }); + } + + return node; +} + +function addTypePrefix(node, prefix) { + if (node && typeof node === "object") { + delete node.parent; + for (const key in node) { + addTypePrefix(node[key], prefix); + if (key === "type" && typeof node[key] === "string") { + if (!node[key].startsWith(prefix)) { + node[key] = prefix + node[key]; + } + } + } + } + return node; +} + +function addMissingType(node) { + if (node && typeof node === "object") { + delete node.parent; + for (const key in node) { + addMissingType(node[key]); + } + if (!Array.isArray(node) && node.value && !node.type) { + node.type = "unknown"; + } + } + return node; +} + +function parseNestedValue(node) { + if (node && typeof node === "object") { + delete node.parent; + for (const key in node) { + parseNestedValue(node[key]); + if (key === "nodes") { + node.group = flattenGroups(parseValueNodes(node[key])); + delete node[key]; + } + } + } + return node; +} + +function parseValue(value) { + const r = require; + const valueParser = r("postcss-values-parser"); + const result = valueParser(value, { loose: true }).parse(); + const parsedResult = parseNestedValue(result); + return addTypePrefix(parsedResult, "value-"); +} + +function parseMediaQuery(value) { + const r = require; + const mediaParser = r("postcss-media-query-parser").default; + const result = addMissingType(mediaParser(value)); + return addTypePrefix(result, "media-"); +} + +function parseNestedCSS(node) { + if (node && typeof node === "object") { + delete node.parent; + for (const key in node) { + parseNestedCSS(node[key]); + } + if (typeof node.selector === "string") { + const selector = node.raws.selector + ? node.raws.selector.raw + : node.selector; + + try { + node.selector = parseSelector(selector); + } catch (e) { + // Fail silently. It's better to print it as is than to try and parse it + node.selector = selector; + } + } + if (node.type && typeof node.value === "string") { + try { + node.value = parseValue(node.value); + } catch (e) { + const line = +(e.toString().match(/line: ([0-9]+)/) || [1, 1])[1]; + const column = +(e.toString().match(/column ([0-9]+)/) || [0, 0])[1]; + throw createError( + "(postcss-values-parser) " + e.toString(), + node.source.start.line + line - 1, + node.source.start.column + column + node.prop.length + ); + } + } + if (node.type === "css-atrule" && typeof node.params === "string") { + node.params = parseMediaQuery(node.params); + } + } + return node; +} + +function parseWithParser(parser, text) { + let result; + try { + result = parser.parse(text); + } catch (e) { + if (typeof e.line !== "number") { + throw e; + } + throw createError("(postcss) " + e.name + " " + e.reason, e.line, e.column); + } + const prefixedResult = addTypePrefix(result, "css-"); + const parsedResult = parseNestedCSS(prefixedResult); + return parsedResult; +} + +function parse(text) { + const r = require; + const isLikelySCSS = !!text.match(/(\w\s*: [^}:]+|#){/); + try { + return parseWithParser( + r(isLikelySCSS ? "postcss-scss" : "postcss-less"), + text + ); + } catch (e) { + try { + return parseWithParser( + r(isLikelySCSS ? "postcss-less" : "postcss-scss"), + text + ); + } catch (e2) { + throw e; + } + } +} + +module.exports = parse; diff --git a/src/parser-typescript.js b/src/parser-typescript.js new file mode 100644 index 00000000..db2b82c7 --- /dev/null +++ b/src/parser-typescript.js @@ -0,0 +1,54 @@ +"use strict"; + +const createError = require("./parser-create-error"); + +function parse(text) { + const jsx = isProbablyJsx(text); + let ast; + try { + try { + // Try passing with our best guess first. + ast = tryParseTypeScript(text, jsx); + } catch (e) { + // But if we get it wrong, try the opposite. + ast = tryParseTypeScript(text, !jsx); + } + } catch (e) { + throw createError(e.message, e.lineNumber, e.column); + } + + delete ast.tokens; + return ast; +} + +function tryParseTypeScript(text, jsx) { + // While we are working on typescript, we are putting it in devDependencies + // so it shouldn't be picked up by static analysis + const r = require; + const parser = r("typescript-eslint-parser"); + return parser.parse(text, { + loc: true, + range: true, + tokens: true, + comment: true, + useJSXTextNode: true, + ecmaFeatures: { jsx } + }); +} + +/** + * Use a naive regular expression until we address + * https://github.com/prettier/prettier/issues/1538 + */ +function isProbablyJsx(text) { + return new RegExp( + [ + "(^[^\"'`]*)" // Contains "/>" on line not starting with "//" + ].join(""), + "m" + ).test(text); +} + +module.exports = parse; diff --git a/src/parser.js b/src/parser.js index bbafe85b..76055170 100644 --- a/src/parser.js +++ b/src/parser.js @@ -1,23 +1,18 @@ "use strict"; -function createError(message, line, column) { - // Construct an error similar to the ones thrown by Babylon. - const error = new SyntaxError(message + " (" + line + ":" + column + ")"); - error.loc = { line, column }; - return error; -} - function parse(text, opts) { let parseFunction; if (opts.parser === "flow") { - parseFunction = parseWithFlow; + parseFunction = require("./parser-flow"); } else if (opts.parser === "typescript") { - parseFunction = parseWithTypeScript; + const r = require; + parseFunction = r("./parser-typescript"); } else if (opts.parser === "postcss") { - parseFunction = parseWithPostCSS; + const r = require; + parseFunction = r("./parser-postcss"); } else { - parseFunction = parseWithBabylon; + parseFunction = require("./parser-babylon"); } try { @@ -38,338 +33,4 @@ function parse(text, opts) { } } -function parseWithFlow(text) { - // Inline the require to avoid loading all the JS if we don't use it - const flowParser = require("flow-parser"); - - const ast = flowParser.parse(text, { - esproposal_class_instance_fields: true, - esproposal_class_static_fields: true, - esproposal_export_star_as: true - }); - - if (ast.errors.length > 0) { - throw createError( - ast.errors[0].message, - ast.errors[0].loc.start.line, - ast.errors[0].loc.start.column - ); - } - - return ast; -} - -function parseWithBabylon(text) { - // Inline the require to avoid loading all the JS if we don't use it - const babylon = require("babylon"); - - const babylonOptions = { - sourceType: "module", - allowImportExportEverywhere: false, - allowReturnOutsideFunction: true, - plugins: [ - "jsx", - "flow", - "doExpressions", - "objectRestSpread", - "decorators", - "classProperties", - "exportExtensions", - "asyncGenerators", - "functionBind", - "functionSent", - "dynamicImport" - ] - }; - - let ast; - try { - ast = babylon.parse(text, babylonOptions); - } catch (originalError) { - try { - return babylon.parse( - text, - Object.assign({}, babylonOptions, { strictMode: false }) - ); - } catch (nonStrictError) { - throw originalError; - } - } - delete ast.tokens; - return ast; -} - -function parseWithTypeScript(text) { - const jsx = isProbablyJsx(text); - let ast; - try { - try { - // Try passing with our best guess first. - ast = tryParseTypeScript(text, jsx); - } catch (e) { - // But if we get it wrong, try the opposite. - ast = tryParseTypeScript(text, !jsx); - } - } catch (e) { - throw createError(e.message, e.lineNumber, e.column); - } - - delete ast.tokens; - return ast; -} - -function tryParseTypeScript(text, jsx) { - // While we are working on typescript, we are putting it in devDependencies - // so it shouldn't be picked up by static analysis - const r = require; - const parser = r("typescript-eslint-parser"); - return parser.parse(text, { - loc: true, - range: true, - tokens: true, - comment: true, - useJSXTextNode: true, - ecmaFeatures: { jsx } - }); -} - -/** - * Use a naive regular expression until we address - * https://github.com/prettier/prettier/issues/1538 - */ -function isProbablyJsx(text) { - return new RegExp( - [ - "(^[^\"'`]*)" // Contains "/>" on line not starting with "//" - ].join(""), - "m" - ).test(text); -} - -function parseSelector(selector) { - const r = require; - const selectorParser = r("postcss-selector-parser"); - let result; - selectorParser(result_ => { - result = result_; - }).process(selector); - return addTypePrefix(result, "selector-"); -} - -function parseValueNodes(nodes) { - let parenGroup = { - open: null, - close: null, - groups: [], - type: "paren_group" - }; - const parenGroupStack = [parenGroup]; - const rootParenGroup = parenGroup; - let commaGroup = { - groups: [], - type: "comma_group" - }; - const commaGroupStack = [commaGroup]; - - for (let i = 0; i < nodes.length; ++i) { - if (nodes[i].type === "paren" && nodes[i].value === "(") { - parenGroup = { - open: nodes[i], - close: null, - groups: [], - type: "paren_group" - }; - parenGroupStack.push(parenGroup); - - commaGroup = { - groups: [], - type: "comma_group" - }; - commaGroupStack.push(commaGroup); - } else if (nodes[i].type === "paren" && nodes[i].value === ")") { - if (commaGroup.groups.length) { - parenGroup.groups.push(commaGroup); - } - parenGroup.close = nodes[i]; - - if (commaGroupStack.length === 1) { - throw new Error("Unbalanced parenthesis"); - } - - commaGroupStack.pop(); - commaGroup = commaGroupStack[commaGroupStack.length - 1]; - commaGroup.groups.push(parenGroup); - - parenGroupStack.pop(); - parenGroup = parenGroupStack[parenGroupStack.length - 1]; - } else if (nodes[i].type === "comma") { - parenGroup.groups.push(commaGroup); - commaGroup = { - groups: [], - type: "comma_group" - }; - commaGroupStack[commaGroupStack.length - 1] = commaGroup; - } else { - commaGroup.groups.push(nodes[i]); - } - } - if (commaGroup.groups.length > 0) { - parenGroup.groups.push(commaGroup); - } - return rootParenGroup; -} - -function flattenGroups(node) { - if ( - node.type === "paren_group" && - !node.open && - !node.close && - node.groups.length === 1 - ) { - return flattenGroups(node.groups[0]); - } - - if (node.type === "comma_group" && node.groups.length === 1) { - return flattenGroups(node.groups[0]); - } - - if (node.type === "paren_group" || node.type === "comma_group") { - return Object.assign({}, node, { groups: node.groups.map(flattenGroups) }); - } - - return node; -} - -function addTypePrefix(node, prefix) { - if (node && typeof node === "object") { - delete node.parent; - for (const key in node) { - addTypePrefix(node[key], prefix); - if (key === "type" && typeof node[key] === "string") { - if (!node[key].startsWith(prefix)) { - node[key] = prefix + node[key]; - } - } - } - } - return node; -} - -function addMissingType(node) { - if (node && typeof node === "object") { - delete node.parent; - for (const key in node) { - addMissingType(node[key]); - } - if (!Array.isArray(node) && node.value && !node.type) { - node.type = "unknown"; - } - } - return node; -} - -function parseNestedValue(node) { - if (node && typeof node === "object") { - delete node.parent; - for (const key in node) { - parseNestedValue(node[key]); - if (key === "nodes") { - node.group = flattenGroups(parseValueNodes(node[key])); - delete node[key]; - } - } - } - return node; -} - -function parseValue(value) { - const r = require; - const valueParser = r("postcss-values-parser"); - const result = valueParser(value, { loose: true }).parse(); - const parsedResult = parseNestedValue(result); - return addTypePrefix(parsedResult, "value-"); -} - -function parseMediaQuery(value) { - const r = require; - const mediaParser = r("postcss-media-query-parser").default; - const result = addMissingType(mediaParser(value)); - return addTypePrefix(result, "media-"); -} - -function parseNestedCSS(node) { - if (node && typeof node === "object") { - delete node.parent; - for (const key in node) { - parseNestedCSS(node[key]); - } - if (typeof node.selector === "string") { - const selector = node.raws.selector - ? node.raws.selector.raw - : node.selector; - - try { - node.selector = parseSelector(selector); - } catch (e) { - // Fail silently. It's better to print it as is than to try and parse it - node.selector = selector; - } - } - if (node.type && typeof node.value === "string") { - try { - node.value = parseValue(node.value); - } catch (e) { - const line = +(e.toString().match(/line: ([0-9]+)/) || [1, 1])[1]; - const column = +(e.toString().match(/column ([0-9]+)/) || [0, 0])[1]; - throw createError( - "(postcss-values-parser) " + e.toString(), - node.source.start.line + line - 1, - node.source.start.column + column + node.prop.length - ); - } - } - if (node.type === "css-atrule" && typeof node.params === "string") { - node.params = parseMediaQuery(node.params); - } - } - return node; -} - -function parseWithPostCSSParser(parser, text) { - let result; - try { - result = parser.parse(text); - } catch (e) { - if (typeof e.line !== "number") { - throw e; - } - throw createError("(postcss) " + e.name + " " + e.reason, e.line, e.column); - } - const prefixedResult = addTypePrefix(result, "css-"); - const parsedResult = parseNestedCSS(prefixedResult); - return parsedResult; -} - -function parseWithPostCSS(text) { - const r = require; - const isLikelySCSS = !!text.match(/(\w\s*: [^}:]+|#){/); - try { - return parseWithPostCSSParser( - r(isLikelySCSS ? "postcss-scss" : "postcss-less"), - text - ); - } catch (e) { - try { - return parseWithPostCSSParser( - r(isLikelySCSS ? "postcss-less" : "postcss-scss"), - text - ); - } catch (e2) { - throw e; - } - } -} - module.exports = { parse };