Add parser support for placeholders (#9364)
This commit is contained in:
315
packages/babel-parser/src/plugins/placeholders.js
Normal file
315
packages/babel-parser/src/plugins/placeholders.js
Normal file
@@ -0,0 +1,315 @@
|
||||
// @flow
|
||||
|
||||
import * as charCodes from "charcodes";
|
||||
|
||||
import { types as tt, TokenType } from "../tokenizer/types";
|
||||
import type Parser from "../parser";
|
||||
import * as N from "../types";
|
||||
|
||||
tt.placeholder = new TokenType("%%", { startsExpr: true });
|
||||
|
||||
export type PlaceholderTypes =
|
||||
| "Identifier"
|
||||
| "StringLiteral"
|
||||
| "Expression"
|
||||
| "Statement"
|
||||
| "Declaration"
|
||||
| "BlockStatement"
|
||||
| "ClassBody"
|
||||
| "Pattern";
|
||||
|
||||
// $PropertyType doesn't support enums. Use a fake "switch" (GetPlaceholderNode)
|
||||
//type MaybePlaceholder<T: PlaceholderTypes> = $PropertyType<N, T> | N.Placeholder<T>;
|
||||
|
||||
type _Switch<Value, Cases, Index> = $Call<
|
||||
(
|
||||
$ElementType<$ElementType<Cases, Index>, 0>,
|
||||
) => $ElementType<$ElementType<Cases, Index>, 1>,
|
||||
Value,
|
||||
>;
|
||||
type $Switch<Value, Cases> = _Switch<Value, Cases, *>;
|
||||
|
||||
type NodeOf<T: PlaceholderTypes> = $Switch<
|
||||
T,
|
||||
[
|
||||
["Identifier", N.Identifier],
|
||||
["StringLiteral", N.StringLiteral],
|
||||
["Expression", N.Expression],
|
||||
["Statement", N.Statement],
|
||||
["Declaration", N.Declaration],
|
||||
["BlockStatement", N.BlockStatement],
|
||||
["ClassBody", N.ClassBody],
|
||||
["Pattern", N.Pattern],
|
||||
],
|
||||
>;
|
||||
|
||||
// Placeholder<T> breaks everything, because its type is incompatible with
|
||||
// the substituted nodes.
|
||||
type MaybePlaceholder<T: PlaceholderTypes> = NodeOf<T>; // | Placeholder<T>
|
||||
|
||||
export default (superClass: Class<Parser>): Class<Parser> =>
|
||||
class extends superClass {
|
||||
parsePlaceholder<T: PlaceholderTypes>(
|
||||
expectedNode: T,
|
||||
): /*?N.Placeholder<T>*/ ?MaybePlaceholder<T> {
|
||||
if (this.match(tt.placeholder)) {
|
||||
const node = this.startNode();
|
||||
this.next();
|
||||
this.assertNoSpace("Unexpected space in placeholder.");
|
||||
|
||||
// We can't use this.parseIdentifier because
|
||||
// we don't want nested placeholders.
|
||||
node.name = super.parseIdentifier(/* liberal */ true);
|
||||
|
||||
this.assertNoSpace("Unexpected space in placeholder.");
|
||||
this.expect(tt.placeholder);
|
||||
return this.finishPlaceholder(node, expectedNode);
|
||||
}
|
||||
}
|
||||
|
||||
finishPlaceholder<T: PlaceholderTypes>(
|
||||
node: N.Node,
|
||||
expectedNode: T,
|
||||
): /*N.Placeholder<T>*/ MaybePlaceholder<T> {
|
||||
node.expectedNode = expectedNode;
|
||||
return this.finishNode(node, "Placeholder");
|
||||
}
|
||||
|
||||
/* ============================================================ *
|
||||
* tokenizer/index.js *
|
||||
* ============================================================ */
|
||||
|
||||
getTokenFromCode(code: number) {
|
||||
if (
|
||||
code === charCodes.percentSign &&
|
||||
this.state.input.charCodeAt(this.state.pos + 1) ===
|
||||
charCodes.percentSign
|
||||
) {
|
||||
return this.finishOp(tt.placeholder, 2);
|
||||
}
|
||||
|
||||
return super.getTokenFromCode(...arguments);
|
||||
}
|
||||
|
||||
/* ============================================================ *
|
||||
* parser/expression.js *
|
||||
* ============================================================ */
|
||||
|
||||
parseExprAtom(): MaybePlaceholder<"Expression"> {
|
||||
return (
|
||||
this.parsePlaceholder("Expression") || super.parseExprAtom(...arguments)
|
||||
);
|
||||
}
|
||||
|
||||
parseIdentifier(): MaybePlaceholder<"Identifier"> {
|
||||
// NOTE: This function only handles identifiers outside of
|
||||
// expressions and binding patterns, since they are already
|
||||
// handled by the parseExprAtom and parseBindingAtom functions.
|
||||
// This is needed, for example, to parse "class %%NAME%% {}".
|
||||
return (
|
||||
this.parsePlaceholder("Identifier") ||
|
||||
super.parseIdentifier(...arguments)
|
||||
);
|
||||
}
|
||||
|
||||
checkReservedWord(word: string): void {
|
||||
// Sometimes we call #checkReservedWord(node.name), expecting
|
||||
// that node is an Identifier. If it is a Placeholder, name
|
||||
// will be undefined.
|
||||
if (word !== undefined) super.checkReservedWord(...arguments);
|
||||
}
|
||||
|
||||
/* ============================================================ *
|
||||
* parser/lval.js *
|
||||
* ============================================================ */
|
||||
|
||||
parseBindingAtom(): MaybePlaceholder<"Pattern"> {
|
||||
return (
|
||||
this.parsePlaceholder("Pattern") || super.parseBindingAtom(...arguments)
|
||||
);
|
||||
}
|
||||
|
||||
checkLVal(expr: N.Expression): void {
|
||||
if (expr.type !== "Placeholder") super.checkLVal(...arguments);
|
||||
}
|
||||
|
||||
toAssignable(node: N.Node): N.Node {
|
||||
if (
|
||||
node &&
|
||||
node.type === "Placeholder" &&
|
||||
node.expectedNode === "Expression"
|
||||
) {
|
||||
node.expectedNode = "Pattern";
|
||||
return node;
|
||||
}
|
||||
return super.toAssignable(...arguments);
|
||||
}
|
||||
|
||||
/* ============================================================ *
|
||||
* parser/statement.js *
|
||||
* ============================================================ */
|
||||
|
||||
verifyBreakContinue(node: N.BreakStatement | N.ContinueStatement) {
|
||||
if (node.label && node.label.type === "Placeholder") return;
|
||||
super.verifyBreakContinue(...arguments);
|
||||
}
|
||||
|
||||
parseExpressionStatement(
|
||||
node: MaybePlaceholder<"Statement">,
|
||||
expr: N.Expression,
|
||||
): MaybePlaceholder<"Statement"> {
|
||||
if (
|
||||
expr.type !== "Placeholder" ||
|
||||
(expr.extra && expr.extra.parenthesized)
|
||||
) {
|
||||
return super.parseExpressionStatement(...arguments);
|
||||
}
|
||||
|
||||
if (this.match(tt.colon)) {
|
||||
const stmt: N.LabeledStatement = node;
|
||||
stmt.label = this.finishPlaceholder(expr, "Identifier");
|
||||
this.next();
|
||||
stmt.body = this.parseStatement("label");
|
||||
return this.finishNode(stmt, "LabeledStatement");
|
||||
}
|
||||
|
||||
this.semicolon();
|
||||
|
||||
node.name = expr.name;
|
||||
return this.finishPlaceholder(node, "Statement");
|
||||
}
|
||||
|
||||
parseBlock(): MaybePlaceholder<"BlockStatement"> {
|
||||
return (
|
||||
this.parsePlaceholder("BlockStatement") ||
|
||||
super.parseBlock(...arguments)
|
||||
);
|
||||
}
|
||||
|
||||
parseFunctionId(): ?MaybePlaceholder<"Identifier"> {
|
||||
return (
|
||||
this.parsePlaceholder("Identifier") ||
|
||||
super.parseFunctionId(...arguments)
|
||||
);
|
||||
}
|
||||
|
||||
parseClass<T: N.Class>(
|
||||
node: T,
|
||||
isStatement: /* T === ClassDeclaration */ boolean,
|
||||
optionalId?: boolean,
|
||||
): T {
|
||||
const type = isStatement ? "ClassDeclaration" : "ClassExpression";
|
||||
|
||||
this.next();
|
||||
this.takeDecorators(node);
|
||||
|
||||
const placeholder = this.parsePlaceholder("Identifier");
|
||||
if (placeholder) {
|
||||
if (
|
||||
this.match(tt._extends) ||
|
||||
this.match(tt.placeholder) ||
|
||||
this.match(tt.braceL)
|
||||
) {
|
||||
node.id = placeholder;
|
||||
} else if (optionalId || !isStatement) {
|
||||
node.id = null;
|
||||
node.body = this.finishPlaceholder(placeholder, "ClassBody");
|
||||
return this.finishNode(node, type);
|
||||
} else {
|
||||
this.unexpected(null, "A class name is required");
|
||||
}
|
||||
} else {
|
||||
this.parseClassId(node, isStatement, optionalId);
|
||||
}
|
||||
|
||||
this.parseClassSuper(node);
|
||||
node.body =
|
||||
this.parsePlaceholder("ClassBody") ||
|
||||
this.parseClassBody(!!node.superClass);
|
||||
return this.finishNode(node, type);
|
||||
}
|
||||
|
||||
parseExport(node: N.Node): N.Node {
|
||||
const placeholder = this.parsePlaceholder("Identifier");
|
||||
if (!placeholder) return super.parseExport(...arguments);
|
||||
|
||||
if (!this.isContextual("from") && !this.match(tt.comma)) {
|
||||
// export %%DECL%%;
|
||||
node.specifiers = [];
|
||||
node.source = null;
|
||||
node.declaration = this.finishPlaceholder(placeholder, "Declaration");
|
||||
return this.finishNode(node, "ExportNamedDeclaration");
|
||||
}
|
||||
|
||||
// export %%NAME%% from "foo";
|
||||
this.expectPlugin("exportDefaultFrom");
|
||||
const specifier = this.startNode();
|
||||
specifier.exported = placeholder;
|
||||
node.specifiers = [this.finishNode(specifier, "ExportDefaultSpecifier")];
|
||||
|
||||
return super.parseExport(node);
|
||||
}
|
||||
|
||||
maybeParseExportDefaultSpecifier(node: N.Node): boolean {
|
||||
if (node.specifiers && node.specifiers.length > 0) {
|
||||
// "export %%NAME%%" has already been parsed by #parseExport.
|
||||
return true;
|
||||
}
|
||||
return super.maybeParseExportDefaultSpecifier(...arguments);
|
||||
}
|
||||
|
||||
checkExport(node: N.ExportNamedDeclaration): void {
|
||||
const { specifiers } = node;
|
||||
if (specifiers && specifiers.length) {
|
||||
node.specifiers = specifiers.filter(
|
||||
node => node.exported.type === "Placeholder",
|
||||
);
|
||||
}
|
||||
super.checkExport(node);
|
||||
node.specifiers = specifiers;
|
||||
}
|
||||
|
||||
parseImport(
|
||||
node: N.Node,
|
||||
): N.ImportDeclaration | N.TsImportEqualsDeclaration {
|
||||
const placeholder = this.parsePlaceholder("Identifier");
|
||||
if (!placeholder) return super.parseImport(...arguments);
|
||||
|
||||
node.specifiers = [];
|
||||
|
||||
if (!this.isContextual("from") && !this.match(tt.comma)) {
|
||||
// import %%STRING%%;
|
||||
node.source = this.finishPlaceholder(placeholder, "StringLiteral");
|
||||
this.semicolon();
|
||||
return this.finishNode(node, "ImportDeclaration");
|
||||
}
|
||||
|
||||
// import %%DEFAULT%% ...
|
||||
const specifier = this.startNodeAtNode(placeholder);
|
||||
specifier.local = placeholder;
|
||||
this.finishNode(specifier, "ImportDefaultSpecifier");
|
||||
node.specifiers.push(specifier);
|
||||
|
||||
if (this.eat(tt.comma)) {
|
||||
// import %%DEFAULT%%, * as ...
|
||||
const hasStarImport = this.maybeParseStarImportSpecifier(node);
|
||||
|
||||
// import %%DEFAULT%%, { ...
|
||||
if (!hasStarImport) this.parseNamedImportSpecifiers(node);
|
||||
}
|
||||
|
||||
this.expectContextual("from");
|
||||
node.source = this.parseImportSource();
|
||||
this.semicolon();
|
||||
return this.finishNode(node, "ImportDeclaration");
|
||||
}
|
||||
|
||||
parseImportSource(): MaybePlaceholder<"StringLiteral"> {
|
||||
// import ... from %%STRING%%;
|
||||
|
||||
return (
|
||||
this.parsePlaceholder("StringLiteral") ||
|
||||
super.parseImportSource(...arguments)
|
||||
);
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user