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

266 lines
8.1 KiB
JavaScript

import tryCombinations from "../../utils/try-combinations.js";
import getShebang from "../utils/get-shebang.js";
import getNextNonSpaceNonCommentCharacterIndexWithStartIndex from "../../utils/text/get-next-non-space-non-comment-character-index-with-start-index.js";
// JSON parsers are bundled here so we can reduce package size
import jsonParsers from "../../language-json/parser-json.js";
import createParser from "./utils/create-parser.js";
import createBabelParseError from "./utils/create-babel-parse-error.js";
import postprocess from "./postprocess/index.js";
import getSourceType from "./utils/get-source-type.js";
/**
* @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,
allowSuperOutsideMethod: true,
allowUndeclaredExports: true,
errorRecovery: true,
createParenthesizedExpressions: 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", { decoratorsBeforeExport: false }],
"importAssertions",
"decimal",
"moduleBlocks",
"asyncDoExpressions",
"regexpUnicodeSets",
"destructuringPrivate",
"decoratorAutoAccessors",
"importReflection",
"explicitResourceManagement",
],
tokens: true,
ranges: true,
};
/** @type {ParserPlugin} */
const recordAndTuplePlugin = ["recordAndTuple", { syntaxType: "hash" }];
/** @type {ParserPlugin} */
const v8intrinsicPlugin = "v8intrinsic";
/** @type {Array<ParserPlugin>} */
const pipelineOperatorPlugins = [
["pipelineOperator", { proposal: "hack", topicToken: "%" }],
["pipelineOperator", { proposal: "minimal" }],
["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/;
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 =
getNextNonSpaceNonCommentCharacterIndexWithStartIndex(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) => !allowedMessageCodes.has(error.reasonCode)
);
if (error) {
throw error;
}
return ast;
}
function createParse(parseMethod, ...optionsCombinations) {
return async (text, opts = {}) => {
if (
(opts.parser === "babel" || opts.parser === "__babel_estree") &&
isFlowFile(text, opts)
) {
opts.parser = "babel-flow";
return parseFlow(text, opts);
}
let combinations = optionsCombinations;
const sourceType = opts.__babelSourceType ?? getSourceType(opts);
if (sourceType === "script") {
combinations = combinations.map((options) => ({
...options,
sourceType: "script",
}));
}
if (/#[[{]/.test(text)) {
combinations = combinations.map((options) =>
appendPlugins([recordAndTuplePlugin], options)
);
}
const shouldEnableV8intrinsicPlugin = /%[A-Z]/.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)
);
}
// Inline `import()` to avoid loading all the JS if we don't use it
const { parse: babelParse, parseExpression } = await import(
"@babel/parser"
);
/** @type {Parse} */
const parseFunction =
parseMethod === "parseExpression" ? parseExpression : babelParse;
const { result: ast, error } = tryCombinations(
combinations.map(
(options) => () => parseWithOptions(parseFunction, text, options)
)
);
if (!ast) {
throw createBabelParseError(error);
}
opts.originalText = text;
return postprocess(ast, opts);
};
}
const parse = createParse("parse", appendPlugins(["jsx", "flow"]));
const parseFlow = createParse(
"parse",
appendPlugins(["jsx", ["flow", { all: true, enums: true }], "flowComments"])
);
const parseTypeScript = createParse(
"parse",
appendPlugins(["jsx", "typescript"]),
appendPlugins(["typescript"])
);
const parseEstree = createParse(
"parse",
appendPlugins(["jsx", "flow", "estree"])
);
const parseExpression = createParse("parseExpression", appendPlugins(["jsx"]));
const parseTSExpression = createParse(
"parseExpression",
appendPlugins(["typescript"])
);
// Error codes are defined in
// - https://github.com/babel/babel/blob/v7.14.0/packages/babel-parser/src/parser/error-message.js
// - https://github.com/babel/babel/blob/v7.14.0/packages/babel-parser/src/plugins/typescript/index.js#L69-L153
// - https://github.com/babel/babel/blob/v7.14.0/packages/babel-parser/src/plugins/flow/index.js#L51-L140
// - https://github.com/babel/babel/blob/v7.14.0/packages/babel-parser/src/plugins/jsx/index.js#L23-L39
const allowedMessageCodes = new Set([
"StrictNumericEscape",
"StrictWith",
"StrictOctalLiteral",
"StrictDelete",
"StrictEvalArguments",
"StrictEvalArgumentsBinding",
"StrictFunction",
"EmptyTypeArguments",
"EmptyTypeParameters",
"ConstructorHasTypeParameters",
"UnsupportedParameterPropertyKind",
"UnexpectedParameterModifier",
"MixedLabeledAndUnlabeledElements",
"InvalidTupleMemberLabel",
"NonClassMethodPropertyHasAbstractModifer",
"DuplicateAccessibilityModifier",
"IndexSignatureHasDeclare",
"DecoratorExportClass",
"ParamDupe",
"InvalidDecimal",
"RestTrailingComma",
"UnsupportedParameterDecorator",
"UnterminatedJsxContent",
"UnexpectedReservedWord",
"ModuleAttributesWithDuplicateKeys",
"LineTerminatorBeforeArrow",
"InvalidEscapeSequenceTemplate",
"NonAbstractClassHasAbstractMethod",
"UnsupportedPropertyDecorator",
"OptionalTypeBeforeRequired",
"PatternIsOptional",
"OptionalBindingPattern",
"DeclareClassFieldHasInitializer",
"TypeImportCannotSpecifyDefaultAndNamed",
"DeclareFunctionHasImplementation",
"ConstructorClassField",
"VarRedeclaration",
"InvalidPrivateFieldResolution",
"DuplicateExport",
]);
const babel = createParser(parse);
const babelTs = createParser(parseTypeScript);
const babelExpression = createParser(parseExpression);
const babelTSExpression = createParser(parseTSExpression);
// Export as a plugin so we can reuse the same bundle for UMD loading
const parser = {
parsers: {
babel,
"babel-flow": createParser(parseFlow),
"babel-ts": babelTs,
...jsonParsers,
/** @internal */
__js_expression: babelExpression,
/** 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: createParser(parseEstree),
},
};
export default parser;