Refactor: Improve JavaScript parser readability (#9783)
This commit is contained in:
1 parent
adf7718ba4
commit
2c14620ba6
7 files changed
+214
-201
No files matched your search
@@ -368,6 +368,7 @@
|
||||
"TSAs",
|
||||
"tsep",
|
||||
"TSESTree",
|
||||
"TSESTreeOptions",
|
||||
"TSJS",
|
||||
"tslib",
|
||||
"typeahead",
|
||||
|
||||
+91
-108
@@ -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);
|
||||
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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;
|
||||
Reference in new issue
Block a user