Add parser support for placeholders (#9364)
This commit is contained in:
@@ -1048,19 +1048,8 @@ export default class ExpressionParser extends LValParser {
|
||||
if (isPrivate) {
|
||||
this.expectOnePlugin(["classPrivateProperties", "classPrivateMethods"]);
|
||||
const node = this.startNode();
|
||||
const columnHashEnd = this.state.end;
|
||||
this.next();
|
||||
const columnIdentifierStart = this.state.start;
|
||||
|
||||
const spacesBetweenHashAndIdentifier =
|
||||
columnIdentifierStart - columnHashEnd;
|
||||
if (spacesBetweenHashAndIdentifier != 0) {
|
||||
this.raise(
|
||||
columnIdentifierStart,
|
||||
"Unexpected space between # and identifier",
|
||||
);
|
||||
}
|
||||
|
||||
this.assertNoSpace("Unexpected space between # and identifier");
|
||||
node.id = this.parseIdentifier(true);
|
||||
return this.finishNode(node, "PrivateName");
|
||||
} else {
|
||||
|
||||
@@ -416,15 +416,24 @@ export default class StatementParser extends ExpressionParser {
|
||||
|
||||
if (this.isLineTerminator()) {
|
||||
node.label = null;
|
||||
} else if (!this.match(tt.name)) {
|
||||
this.unexpected();
|
||||
} else {
|
||||
node.label = this.parseIdentifier();
|
||||
this.semicolon();
|
||||
}
|
||||
|
||||
// Verify that there is an actual destination to break or
|
||||
// continue to.
|
||||
this.verifyBreakContinue(node, keyword);
|
||||
|
||||
return this.finishNode(
|
||||
node,
|
||||
isBreak ? "BreakStatement" : "ContinueStatement",
|
||||
);
|
||||
}
|
||||
|
||||
verifyBreakContinue(
|
||||
node: N.BreakStatement | N.ContinueStatement,
|
||||
keyword: string,
|
||||
) {
|
||||
const isBreak = keyword === "break";
|
||||
let i;
|
||||
for (i = 0; i < this.state.labels.length; ++i) {
|
||||
const lab = this.state.labels[i];
|
||||
@@ -436,10 +445,6 @@ export default class StatementParser extends ExpressionParser {
|
||||
if (i === this.state.labels.length) {
|
||||
this.raise(node.start, "Unsyntactic " + keyword);
|
||||
}
|
||||
return this.finishNode(
|
||||
node,
|
||||
isBreak ? "BreakStatement" : "ContinueStatement",
|
||||
);
|
||||
}
|
||||
|
||||
parseDebuggerStatement(node: N.DebuggerStatement): N.DebuggerStatement {
|
||||
@@ -800,7 +805,7 @@ export default class StatementParser extends ExpressionParser {
|
||||
parseExpressionStatement(
|
||||
node: N.ExpressionStatement,
|
||||
expr: N.Expression,
|
||||
): N.ExpressionStatement {
|
||||
): N.Statement {
|
||||
node.expression = expr;
|
||||
this.semicolon();
|
||||
return this.finishNode(node, "ExpressionStatement");
|
||||
@@ -1024,6 +1029,7 @@ export default class StatementParser extends ExpressionParser {
|
||||
): T {
|
||||
const isStatement = statement & FUNC_STATEMENT;
|
||||
const isHangingStatement = statement & FUNC_HANGING_STATEMENT;
|
||||
const requireId = !!isStatement && !(statement & FUNC_NULLABLE_ID);
|
||||
|
||||
this.initFunction(node, isAsync);
|
||||
|
||||
@@ -1036,10 +1042,7 @@ export default class StatementParser extends ExpressionParser {
|
||||
node.generator = this.eat(tt.star);
|
||||
|
||||
if (isStatement) {
|
||||
node.id =
|
||||
statement & FUNC_NULLABLE_ID && !this.match(tt.name)
|
||||
? null
|
||||
: this.parseIdentifier();
|
||||
node.id = this.parseFunctionId(requireId);
|
||||
if (node.id && !isHangingStatement) {
|
||||
// If it is a regular function declaration in sloppy mode, then it is
|
||||
// subject to Annex B semantics (BIND_FUNCTION). Otherwise, the binding
|
||||
@@ -1067,7 +1070,7 @@ export default class StatementParser extends ExpressionParser {
|
||||
this.scope.enter(functionFlags(node.async, node.generator));
|
||||
|
||||
if (!isStatement) {
|
||||
node.id = this.match(tt.name) ? this.parseIdentifier() : null;
|
||||
node.id = this.parseFunctionId();
|
||||
}
|
||||
|
||||
this.parseFunctionParams(node);
|
||||
@@ -1090,6 +1093,10 @@ export default class StatementParser extends ExpressionParser {
|
||||
return node;
|
||||
}
|
||||
|
||||
parseFunctionId(requireId?: boolean): ?N.Identifier {
|
||||
return requireId || this.match(tt.name) ? this.parseIdentifier() : null;
|
||||
}
|
||||
|
||||
parseFunctionParams(node: N.Function, allowModifiers?: boolean): void {
|
||||
const oldInParameters = this.state.inParameters;
|
||||
this.state.inParameters = true;
|
||||
@@ -1122,7 +1129,7 @@ export default class StatementParser extends ExpressionParser {
|
||||
|
||||
this.parseClassId(node, isStatement, optionalId);
|
||||
this.parseClassSuper(node);
|
||||
this.parseClassBody(node);
|
||||
node.body = this.parseClassBody(!!node.superClass);
|
||||
|
||||
this.state.strict = oldStrict;
|
||||
|
||||
@@ -1149,7 +1156,7 @@ export default class StatementParser extends ExpressionParser {
|
||||
);
|
||||
}
|
||||
|
||||
parseClassBody(node: N.Class): void {
|
||||
parseClassBody(constructorAllowsSuper: boolean): N.ClassBody {
|
||||
this.state.classLevel++;
|
||||
|
||||
const state = { hadConstructor: false };
|
||||
@@ -1159,8 +1166,6 @@ export default class StatementParser extends ExpressionParser {
|
||||
|
||||
this.expect(tt.braceL);
|
||||
|
||||
const constructorAllowsSuper = node.superClass !== null;
|
||||
|
||||
// For the smartPipelines plugin: Disable topic references from outer
|
||||
// contexts within the class body. They are permitted in test expressions,
|
||||
// outside of the class body.
|
||||
@@ -1212,9 +1217,9 @@ export default class StatementParser extends ExpressionParser {
|
||||
);
|
||||
}
|
||||
|
||||
node.body = this.finishNode(classBody, "ClassBody");
|
||||
|
||||
this.state.classLevel--;
|
||||
|
||||
return this.finishNode(classBody, "ClassBody");
|
||||
}
|
||||
|
||||
parseClassMember(
|
||||
|
||||
@@ -113,6 +113,13 @@ export default class UtilParser extends Tokenizer {
|
||||
this.eat(type) || this.unexpected(pos, type);
|
||||
}
|
||||
|
||||
// Throws if the current token and the prev one are separated by a space.
|
||||
assertNoSpace(message: string = "Unexpected space."): void {
|
||||
if (this.state.start > this.state.lastTokEnd) {
|
||||
this.raise(this.state.lastTokEnd, message);
|
||||
}
|
||||
}
|
||||
|
||||
// Raise an unexpected token error. Can take the expected token type
|
||||
// instead of a message string.
|
||||
|
||||
|
||||
@@ -88,12 +88,17 @@ import estree from "./plugins/estree";
|
||||
import flow from "./plugins/flow";
|
||||
import jsx from "./plugins/jsx";
|
||||
import typescript from "./plugins/typescript";
|
||||
import placeholders from "./plugins/placeholders";
|
||||
|
||||
// NOTE: estree must load first; flow and typescript must load last.
|
||||
export const mixinPluginNames = ["estree", "jsx", "flow", "typescript"];
|
||||
// NOTE: order is important. estree must come first; placeholders must come last.
|
||||
export const mixinPlugins: { [name: string]: MixinPlugin } = {
|
||||
estree,
|
||||
jsx,
|
||||
flow,
|
||||
typescript,
|
||||
placeholders,
|
||||
};
|
||||
|
||||
export const mixinPluginNames: $ReadOnlyArray<string> = Object.keys(
|
||||
mixinPlugins,
|
||||
);
|
||||
|
||||
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)
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -3,6 +3,7 @@
|
||||
import type { SourceType } from "./options";
|
||||
import type { Token } from "./tokenizer";
|
||||
import type { SourceLocation } from "./util/location";
|
||||
import type { PlaceholderTypes } from "./plugins/placeholders";
|
||||
|
||||
/*
|
||||
* If making any changes to the AST, update:
|
||||
@@ -45,6 +46,7 @@ export type Pattern =
|
||||
| ArrayPattern
|
||||
| RestElement
|
||||
| AssignmentPattern;
|
||||
//| Placeholder<"Pattern">;
|
||||
export type Declaration =
|
||||
| VariableDeclaration
|
||||
| ClassDeclaration
|
||||
@@ -53,6 +55,8 @@ export type Declaration =
|
||||
| TsTypeAliasDeclaration
|
||||
| TsEnumDeclaration
|
||||
| TsModuleDeclaration;
|
||||
// | Placeholder<"Declaration">;
|
||||
|
||||
export type DeclarationBase = NodeBase & {
|
||||
// TypeScript allows declarations to be prefixed by `declare`.
|
||||
//TODO: a FunctionDeclaration is never "declare", because it's a TSDeclareFunction instead.
|
||||
@@ -78,6 +82,7 @@ export type Identifier = PatternBase & {
|
||||
// TypeScript only. Used in case of an optional parameter.
|
||||
optional?: ?true,
|
||||
};
|
||||
// | Placeholder<"Identifier">;
|
||||
|
||||
export type PrivateName = NodeBase & {
|
||||
type: "PrivateName",
|
||||
@@ -188,6 +193,7 @@ export type BlockStatement = NodeBase & {
|
||||
body: Array<Statement>, // TODO: $ReadOnlyArray
|
||||
directives: $ReadOnlyArray<Directive>,
|
||||
};
|
||||
// | Placeholder<"BlockStatement">;
|
||||
|
||||
export type EmptyStatement = NodeBase & {
|
||||
type: "EmptyStatement",
|
||||
@@ -682,6 +688,7 @@ export type ClassBody = NodeBase & {
|
||||
type: "ClassBody",
|
||||
body: Array<ClassMember | TsIndexSignature>, // TODO: $ReadOnlyArray
|
||||
};
|
||||
// | Placeholder<"ClassBody">;
|
||||
|
||||
export type ClassMemberBase = NodeBase &
|
||||
HasDecorators & {
|
||||
@@ -1421,6 +1428,16 @@ export type TsNonNullExpression = NodeBase & {
|
||||
expression: Expression,
|
||||
};
|
||||
|
||||
// ================
|
||||
// Babel placeholders %%foo%%
|
||||
// ================
|
||||
|
||||
export type Placeholder<N: PlaceholderTypes> = NodeBase & {
|
||||
type: "Placeholder",
|
||||
id: Identifier,
|
||||
expectedNode: N,
|
||||
};
|
||||
|
||||
// ================
|
||||
// Other
|
||||
// ================
|
||||
|
||||
Reference in New Issue
Block a user