Refactor: Improve JavaScript parser readability (#9783)

This commit is contained in:
fisker Cheung authored and GitHub committed 2020-12-15 14:09:44 +08:00
1 parent adf7718ba4
commit 2c14620ba6
7 files changed
+214 -201

No files matched your search

+1
View File
@@ -368,6 +368,7 @@
"TSAs",
"tsep",
"TSESTree",
"TSESTreeOptions",
"TSJS",
"tslib",
"typeahead",
+91 -108
View File
@@ -1,5 +1,6 @@
"use strict";
const flatten = require("lodash/flatten");
const createError = require("../common/parser-create-error");
const {
getNextNonSpaceNonCommentCharacterIndexWithStartIndex,
@@ -8,61 +9,46 @@ const {
const { hasPragma } = require("./pragma");
const { locStart, locEnd } = require("./loc");
const postprocess = require("./parse-postprocess");
const tryCombinations = require("./parser/try-combinations");
function babelOptions({ sourceType, extraPlugins = [] }) {
return {
sourceType,
allowAwaitOutsideFunction: true,
allowImportExportEverywhere: true,
allowReturnOutsideFunction: true,
allowSuperOutsideMethod: true,
allowUndeclaredExports: true,
errorRecovery: true,
createParenthesizedExpressions: true,
plugins: [
// When adding a plugin, please add a test in `tests/js/babel-plugins`,
// To remove plugins, remove it here and run `yarn test tests/js/babel-plugins` to verify
"doExpressions",
"classProperties",
"exportDefaultFrom",
"functionBind",
"functionSent",
"classPrivateProperties",
"throwExpressions",
"classPrivateMethods",
"v8intrinsic",
"partialApplication",
["decorators", { decoratorsBeforeExport: false }],
"privateIn",
"importAssertions",
["recordAndTuple", { syntaxType: "hash" }],
"decimal",
"moduleStringNames",
"classStaticBlock",
...extraPlugins,
],
tokens: true,
ranges: true,
};
}
function resolvePluginsConflict(
condition,
pluginCombinations,
conflictPlugins
) {
if (!condition) {
return pluginCombinations;
}
const combinations = [];
for (const combination of pluginCombinations) {
for (const plugin of conflictPlugins) {
combinations.push([...combination, plugin]);
}
}
return combinations;
}
const parseOptions = {
sourceType: "module",
allowAwaitOutsideFunction: true,
allowImportExportEverywhere: true,
allowReturnOutsideFunction: true,
allowSuperOutsideMethod: true,
allowUndeclaredExports: true,
errorRecovery: true,
createParenthesizedExpressions: true,
plugins: [
// When adding a plugin, please add a test in `tests/js/babel-plugins`,
// To remove plugins, remove it here and run `yarn test tests/js/babel-plugins` to verify
"doExpressions",
"classProperties",
"exportDefaultFrom",
"functionBind",
"functionSent",
"classPrivateProperties",
"throwExpressions",
"classPrivateMethods",
"v8intrinsic",
"partialApplication",
["decorators", { decoratorsBeforeExport: false }],
"privateIn",
"importAssertions",
["recordAndTuple", { syntaxType: "hash" }],
"decimal",
"moduleStringNames",
"classStaticBlock",
],
tokens: true,
ranges: true,
};
const pipelineOperatorPlugins = [
["pipelineOperator", { proposal: "smart" }],
["pipelineOperator", { proposal: "minimal" }],
["pipelineOperator", { proposal: "fsharp" }],
];
// Similar to babel
// https://github.com/babel/babel/pull/7934/files#diff-a739835084910b0ee3ea649df5a4d223R67
@@ -89,47 +75,67 @@ function isFlowFile(text, options) {
return FLOW_PRAGMA_REGEX.test(text);
}
function parseWithOptions(parseMethod, text, options) {
// Inline the require to avoid loading all the JS if we don't use it
/** @type {import("@babel/parser").parse | import("@babel/parser").parseExpression} */
const parse = require("@babel/parser")[parseMethod];
const ast = parse(text, options);
// @ts-ignore
const error = ast.errors.find((error) => shouldRethrowRecoveredError(error));
if (error) {
throw error;
}
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;
throw createError(message.replace(/ \(.*\)/, ""), {
start: {
line: loc ? loc.line : 0,
column: loc ? loc.column + 1 : 0,
},
});
}
function createParse(parseMethod, ...pluginCombinations) {
const commonPlugins = parseOptions.plugins;
pluginCombinations = pluginCombinations.length
? pluginCombinations.map((plugins) => [...commonPlugins, ...plugins])
: [commonPlugins];
return (text, parsers, opts = {}) => {
if (opts.parser === "babel" && isFlowFile(text, opts)) {
opts.parser = "babel-flow";
return parseFlow(text, parsers, opts);
}
// Inline the require to avoid loading all the JS if we don't use it
const babel = require("@babel/parser");
let combinations = pluginCombinations;
if (text.includes("|>")) {
combinations = flatten(
pipelineOperatorPlugins.map((pipelineOperatorPlugin) =>
combinations.map((plugins) => [...plugins, pipelineOperatorPlugin])
)
);
}
const sourceType =
opts.__babelSourceType === "script" ? "script" : "module";
const { result: ast, error } = tryCombinations(
...combinations.map((plugins) => () =>
parseWithOptions(parseMethod, text, {
...parseOptions,
sourceType,
plugins,
})
)
);
let ast;
try {
const combinations = resolvePluginsConflict(
text.includes("|>"),
pluginCombinations,
[
["pipelineOperator", { proposal: "smart" }],
["pipelineOperator", { proposal: "minimal" }],
["pipelineOperator", { proposal: "fsharp" }],
]
);
ast = tryCombinations(
(options) => babel[parseMethod](text, options),
combinations.map((extraPlugins) =>
babelOptions({ sourceType, extraPlugins })
)
);
} catch (error) {
// babel error prints (l:c) with cols that are zero indexed
// so we need our custom error
const { message, loc } = error;
throw createError(message.replace(/ \(.*\)/, ""), {
start: {
line: loc ? loc.line : 0,
column: loc ? loc.column + 1 : 0,
},
});
if (!ast) {
throw createParseError(error);
}
return postprocess(ast, { ...opts, originalText: text });
@@ -148,20 +154,6 @@ const parseTypeScript = createParse(
);
const parseExpression = createParse("parseExpression", ["jsx"]);
function tryCombinations(fn, combinations) {
let error;
for (let i = 0; i < combinations.length; i++) {
try {
return rethrowSomeRecoveredErrors(fn(combinations[i]));
} catch (_error) {
if (!error) {
error = _error;
}
}
}
throw error;
}
const messagesShouldThrow = new Set([
// TSErrors.UnexpectedTypeAnnotation
// https://github.com/babel/babel/blob/008fe25ae22e78288fbc637d41069bb4a1040987/packages/babel-parser/src/plugins/typescript/index.js#L95
@@ -174,21 +166,12 @@ const messagesShouldThrow = new Set([
"Type parameters must come after the async keyword, e.g. instead of `<T> async () => {}`, use `async <T>() => {}`",
]);
function shouldRethrow(error) {
function shouldRethrowRecoveredError(error) {
const [, message] = error.message.match(/(.*?)\s*\(\d+:\d+\)/);
// Only works for literal message
return messagesShouldThrow.has(message);
}
function rethrowSomeRecoveredErrors(ast) {
const error = ast.errors.find((error) => shouldRethrow(error));
if (error) {
throw error;
}
return ast;
}
function parseJson(text, parsers, opts) {
const ast = parseExpression(text, parsers, opts);
+21 -19
View File
@@ -4,8 +4,9 @@ const createError = require("../common/parser-create-error");
const { hasPragma } = require("./pragma");
const { locStart, locEnd } = require("./loc");
const postprocess = require("./parse-postprocess");
const tryCombinations = require("./parser/try-combinations");
const espreeOptions = {
const parseOptions = {
range: true,
loc: true,
comment: true,
@@ -18,9 +19,20 @@ const espreeOptions = {
},
};
function createParseError(error) {
const { message, lineNumber, column } = error;
/* istanbul ignore next */
if (typeof lineNumber !== "number") {
throw error;
}
throw createError(message, { start: { line: lineNumber, column } });
}
function parse(originalText, parsers, options) {
const { parse, latestEcmaVersion } = require("espree");
espreeOptions.ecmaVersion = latestEcmaVersion;
parseOptions.ecmaVersion = latestEcmaVersion;
// Replace shebang with space
const shebang = getShebang(originalText);
@@ -28,24 +40,14 @@ function parse(originalText, parsers, options) {
? " ".repeat(shebang.length) + originalText.slice(shebang.length)
: originalText;
let ast;
const { result: ast, error: moduleParseError } = tryCombinations(
() => parse(text, { ...parseOptions, sourceType: "module" }),
() => parse(text, { ...parseOptions, sourceType: "script" })
);
try {
ast = parse(text, espreeOptions);
} catch (moduleError) {
try {
ast = parse(text, { ...espreeOptions, sourceType: "script" });
} catch (_) {
// throw the error for `module` parsing
const { message, lineNumber, column } = moduleError;
/* istanbul ignore next */
if (typeof lineNumber !== "number") {
throw moduleError;
}
throw createError(message, { start: { line: lineNumber, column } });
}
if (!ast) {
// throw the error for `module` parsing
throw createParseError(moduleParseError);
}
return postprocess(ast, { ...options, originalText });
+26 -22
View File
@@ -5,32 +5,36 @@ const { hasPragma } = require("./pragma");
const { locStart, locEnd } = require("./loc");
const postprocess = require("./parse-postprocess");
const parseOptions = {
enums: true,
esproposal_decorators: true,
esproposal_class_instance_fields: true,
esproposal_class_static_fields: true,
esproposal_export_star_as: true,
esproposal_optional_chaining: true,
esproposal_nullish_coalescing: true,
tokens: true,
};
function createParseError(error) {
const {
message,
loc: { start, end },
} = error;
return createError(message, {
start: { line: start.line, column: start.column + 1 },
end: { line: end.line, column: end.column + 1 },
});
}
function parse(text, parsers, opts) {
// 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, {
enums: true,
esproposal_decorators: true,
esproposal_class_instance_fields: true,
esproposal_class_static_fields: true,
esproposal_export_star_as: true,
esproposal_optional_chaining: true,
esproposal_nullish_coalescing: true,
tokens: true,
});
const { parse } = require("flow-parser");
const ast = parse(text, parseOptions);
const [error] = ast.errors;
if (error) {
const {
message,
loc: { start, end },
} = error;
throw createError(message, {
start: { line: start.line, column: start.column + 1 },
end: { line: end.line, column: end.column + 1 },
});
throw createParseError(error);
}
return postprocess(ast, { ...opts, originalText: text });
+20 -17
View File
@@ -4,6 +4,7 @@ const createError = require("../common/parser-create-error");
const { hasPragma } = require("./pragma");
const { locStart, locEnd } = require("./loc");
const postprocess = require("./parse-postprocess");
const tryCombinations = require("./parser/try-combinations");
// https://github.com/meriyah/meriyah/blob/4676f60b6c149d7082bde2c9147f9ae2359c8075/src/parser.ts#L185
const parseOptions = {
@@ -59,25 +60,27 @@ function parseWithOptions(text, module) {
return ast;
}
function createParseError(error) {
// throw the error for `module` parsing
const { message, line, column } = error;
/* istanbul ignore next */
if (typeof line !== "number") {
return error;
}
return createError(message, { start: { line, column } });
}
function parse(text, parsers, options) {
let ast;
const { result: ast, error: moduleParseError } = tryCombinations(
() => parseWithOptions(text, /* module */ true),
() => parseWithOptions(text, /* module */ false)
);
try {
ast = parseWithOptions(text /* module */, true);
} catch (moduleError) {
try {
ast = parseWithOptions(text, /* module */ false);
} catch (_) {
// throw the error for `module` parsing
const { message, line, column } = moduleError;
/* istanbul ignore next */
if (typeof line !== "number") {
throw moduleError;
}
throw createError(message, { start: { line, column } });
}
if (!ast) {
// Throw the error for `module` parsing
throw createParseError(moduleParseError);
}
return postprocess(ast, { ...options, originalText: text });
+38 -35
View File
@@ -4,30 +4,49 @@ const createError = require("../common/parser-create-error");
const { hasPragma } = require("./pragma");
const { locStart, locEnd } = require("./loc");
const postprocess = require("./parse-postprocess");
const tryCombinations = require("./parser/try-combinations");
/** @type {import("@typescript-eslint/typescript-estree").TSESTreeOptions} */
const parseOptions = {
// `jest@<=26.4.2` rely on `loc`
// https://github.com/facebook/jest/issues/10444
loc: true,
range: true,
comment: true,
useJSXTextNode: true,
jsx: true,
tokens: true,
loggerFn: false,
project: [],
};
function createParseError(error) {
const { message, lineNumber, column } = error;
/* istanbul ignore next */
if (typeof lineNumber !== "number") {
return error;
}
return createError(message, {
start: { line: lineNumber, column: column + 1 },
});
}
function parse(text, parsers, opts) {
const jsx = isProbablyJsx(text);
let result;
try {
const { parseWithNodeMaps } = require("@typescript-eslint/typescript-estree");
const { result, error: firstError } = tryCombinations(
// Try passing with our best guess first.
result = tryParseTypeScript(text, jsx);
} catch (firstError) {
try {
// But if we get it wrong, try the opposite.
result = tryParseTypeScript(text, !jsx);
} catch (secondError) {
// Suppose our guess is correct, throw the first error
const { message, lineNumber, column } = firstError;
() => parseWithNodeMaps(text, { ...parseOptions, jsx }),
// But if we get it wrong, try the opposite.
() => parseWithNodeMaps(text, { ...parseOptions, jsx: !jsx })
);
/* istanbul ignore next */
if (typeof lineNumber !== "number") {
throw firstError;
}
throw createError(message, {
start: { line: lineNumber, column: column + 1 },
});
}
if (!result) {
// Suppose our guess is correct, throw the first error
throw createParseError(firstError);
}
return postprocess(result.ast, {
@@ -37,22 +56,6 @@ function parse(text, parsers, opts) {
});
}
function tryParseTypeScript(text, jsx) {
const { parseWithNodeMaps } = require("@typescript-eslint/typescript-estree");
return parseWithNodeMaps(text, {
// `jest@<=26.4.2` rely on `loc`
// https://github.com/facebook/jest/issues/10444
loc: true,
range: true,
comment: true,
useJSXTextNode: true,
jsx,
tokens: true,
loggerFn: false,
project: [],
});
}
/**
* Use a naive regular expression to detect JSX
*/
@@ -0,0 +1,17 @@
"use strict";
function tryCombinations(...combinations) {
let firstError;
for (const [index, fn] of combinations.entries()) {
try {
return { result: fn() };
} catch (error) {
if (index === 0) {
firstError = error;
}
}
}
return { error: firstError };
}
module.exports = tryCombinations;