Files
test-repo/src/language-js/parse/babel.js
T

256 lines
7.7 KiB
JavaScript

import { parse as babelParse, parseExpression } from "@babel/parser";
import getNextNonSpaceNonCommentCharacterIndex from "../../utils/get-next-non-space-non-comment-character-index.js";
import tryCombinations from "../../utils/try-combinations.js";
import getShebang from "../utils/get-shebang.js";
import postprocess from "./postprocess/index.js";
import createBabelParseError from "./utils/create-babel-parse-error.js";
import createParser from "./utils/create-parser.js";
import getSourceType from "./utils/get-source-type.js";
import wrapBabelExpression from "./utils/wrap-babel-expression.js";
const createBabelParser = (options) => createParser(createParse(options));
/**
* @typedef {import("@babel/parser").parse | import("@babel/parser").parseExpression} Parse
* @typedef {import("@babel/parser").ParserOptions} ParserOptions
* @typedef {import("@babel/parser").ParserPlugin} ParserPlugin
*/
/** @type {ParserOptions} */
const parseOptions = {
sourceType: "module",
allowImportExportEverywhere: true,
allowReturnOutsideFunction: true,
allowNewTargetOutsideFunction: true,
allowSuperOutsideMethod: true,
allowUndeclaredExports: true,
errorRecovery: true,
createParenthesizedExpressions: true,
createImportExpressions: true,
plugins: [
// When adding a plugin, please add a test in `tests/format/js/babel-plugins`,
// To remove plugins, remove it here and run `yarn test tests/format/js/babel-plugins` to verify
"doExpressions",
"exportDefaultFrom",
"functionBind",
"functionSent",
"throwExpressions",
"partialApplication",
"decorators",
"moduleBlocks",
"asyncDoExpressions",
"destructuringPrivate",
"decoratorAutoAccessors",
"importReflection",
"explicitResourceManagement",
["importAttributes", { deprecatedAssertSyntax: true }],
"sourcePhaseImports",
"deferredImportEvaluation",
["optionalChainingAssign", { version: "2023-07" }],
"recordAndTuple",
],
tokens: true,
ranges: true,
};
/** @type {ParserPlugin} */
const v8intrinsicPlugin = "v8intrinsic";
/** @type {Array<ParserPlugin>} */
const pipelineOperatorPlugins = [
["pipelineOperator", { proposal: "hack", topicToken: "%" }],
["pipelineOperator", { proposal: "fsharp" }],
];
const appendPlugins = (plugins, options = parseOptions) => ({
...options,
plugins: [...options.plugins, ...plugins],
});
// Similar to babel
// https://github.com/babel/babel/pull/7934/files#diff-a739835084910b0ee3ea649df5a4d223R67
const FLOW_PRAGMA_REGEX = /@(?:no)?flow\b/u;
function isFlowFile(text, options) {
if (options.filepath?.endsWith(".js.flow")) {
return true;
}
const shebang = getShebang(text);
if (shebang) {
text = text.slice(shebang.length);
}
const firstNonSpaceNonCommentCharacterIndex =
getNextNonSpaceNonCommentCharacterIndex(text, 0);
if (firstNonSpaceNonCommentCharacterIndex !== false) {
text = text.slice(0, firstNonSpaceNonCommentCharacterIndex);
}
return FLOW_PRAGMA_REGEX.test(text);
}
function parseWithOptions(parse, text, options) {
const ast = parse(text, options);
const error = ast.errors.find(
(error) => !allowedReasonCodes.has(error.reasonCode),
);
if (error) {
throw error;
}
return ast;
}
function createParse({ isExpression = false, optionsCombinations }) {
return (text, options = {}) => {
if (
(options.parser === "babel" || options.parser === "__babel_estree") &&
isFlowFile(text, options)
) {
options.parser = "babel-flow";
return babelFlow.parse(text, options);
}
let combinations = optionsCombinations;
const sourceType = options.__babelSourceType ?? getSourceType(options);
if (sourceType === "script") {
combinations = combinations.map((options) => ({
...options,
sourceType: "script",
}));
}
const shouldEnableV8intrinsicPlugin = /%[A-Z]/u.test(text);
if (text.includes("|>")) {
const conflictsPlugins = shouldEnableV8intrinsicPlugin
? [...pipelineOperatorPlugins, v8intrinsicPlugin]
: pipelineOperatorPlugins;
combinations = conflictsPlugins.flatMap((pipelineOperatorPlugin) =>
combinations.map((options) =>
appendPlugins([pipelineOperatorPlugin], options),
),
);
} else if (shouldEnableV8intrinsicPlugin) {
combinations = combinations.map((options) =>
appendPlugins([v8intrinsicPlugin], options),
);
}
/** @type {Parse} */
const parseFunction = isExpression ? parseExpression : babelParse;
let ast;
try {
ast = tryCombinations(
combinations.map(
(options) => () => parseWithOptions(parseFunction, text, options),
),
);
} catch (/** @type {any} */ { errors: [error] }) {
throw createBabelParseError(error);
}
if (isExpression) {
ast = wrapBabelExpression(ast, { text, rootMarker: options.rootMarker });
}
return postprocess(ast, { parser: "babel", text });
};
}
// Error codes are defined in
// - https://github.com/babel/babel/tree/v7.23.6/packages/babel-parser/src/parse-error
// - https://github.com/babel/babel/blob/v7.23.6/packages/babel-parser/src/plugins/typescript/index.ts#L73-L223
// - https://github.com/babel/babel/blob/v7.23.6/packages/babel-parser/src/plugins/flow/index.ts#L47-L224
// - https://github.com/babel/babel/blob/v7.23.6/packages/babel-parser/src/plugins/jsx/index.ts#L23-L44
const allowedReasonCodes = new Set([
"StrictNumericEscape",
"StrictWith",
"StrictOctalLiteral",
"StrictDelete",
"StrictEvalArguments",
"StrictEvalArgumentsBinding",
"StrictFunction",
"ForInOfLoopInitializer",
"EmptyTypeArguments",
"EmptyTypeParameters",
"ConstructorHasTypeParameters",
"UnsupportedParameterPropertyKind",
"DecoratorExportClass",
"ParamDupe",
"InvalidDecimal",
"RestTrailingComma",
"UnsupportedParameterDecorator",
"UnterminatedJsxContent",
"UnexpectedReservedWord",
"ModuleAttributesWithDuplicateKeys",
"LineTerminatorBeforeArrow",
"InvalidEscapeSequenceTemplate",
"NonAbstractClassHasAbstractMethod",
"OptionalTypeBeforeRequired",
"PatternIsOptional",
"OptionalBindingPattern",
"DeclareClassFieldHasInitializer",
"TypeImportCannotSpecifyDefaultAndNamed",
"ConstructorClassField",
"VarRedeclaration",
"InvalidPrivateFieldResolution",
"DuplicateExport",
]);
const babelParserOptionsCombinations = [appendPlugins(["jsx"])];
const babel = createBabelParser({
optionsCombinations: babelParserOptionsCombinations,
});
const babelTs = createBabelParser({
optionsCombinations: [
appendPlugins(["jsx", "typescript"]),
appendPlugins(["typescript"]),
],
});
const babelExpression = createBabelParser({
isExpression: true,
optionsCombinations: [appendPlugins(["jsx"])],
});
const babelTSExpression = createBabelParser({
isExpression: true,
optionsCombinations: [appendPlugins(["typescript"])],
});
const babelFlow = createBabelParser({
optionsCombinations: [
appendPlugins([
"jsx",
["flow", { all: true, enums: true }],
"flowComments",
]),
],
});
const babelEstree = createBabelParser({
optionsCombinations: babelParserOptionsCombinations.map((options) =>
appendPlugins(["estree"], options),
),
});
export default {
babel,
"babel-flow": babelFlow,
"babel-ts": babelTs,
/** @internal */
__js_expression: babelExpression,
__ts_expression: babelTSExpression,
/** for vue filter */
__vue_expression: babelExpression,
/** for vue filter written in TS */
__vue_ts_expression: babelTSExpression,
/** for vue event binding to handle semicolon */
__vue_event_binding: babel,
/** for vue event binding written in TS to handle semicolon */
__vue_ts_event_binding: babelTs,
/** verify that we can print this AST */
__babel_estree: babelEstree,
};