Add parser support for placeholders (#9364)

This commit is contained in:
Nicolò Ribaudo
2019-03-05 00:45:42 +01:00
committed by GitHub
parent c60c4dd375
commit d832c0f434
141 changed files with 7630 additions and 36 deletions

View File

@@ -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 {

View File

@@ -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(

View File

@@ -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.

View File

@@ -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,
);

View 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)
);
}
};

View File

@@ -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
// ================