packages/babel-parser/src/plugins/placeholders.js
// @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> {
const isFinished = !!(node.expectedNode && node.type === "Placeholder");
node.expectedNode = expectedNode;
return isFinished ? node : this.finishNode(node, "Placeholder");
}
/* ============================================================ *
* tokenizer/index.js *
* ============================================================ */
getTokenFromCode(code: number) {
if (
code === charCodes.percentSign &&
this.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);
}
isExportDefaultSpecifier(): boolean {
if (this.match(tt._default)) {
const next = this.nextTokenStart();
if (this.isUnparsedContextual(next, "from")) {
if (
this.input.startsWith(
tt.placeholder.label,
this.nextTokenStartSince(next + 4),
)
) {
return true;
}
}
}
return super.isExportDefaultSpecifier();
}
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?.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)
);
}
};