Skip to content

Commit

Permalink
Refactor await/yield production parameter tracking (babel#10956)
Browse files Browse the repository at this point in the history
* test: add test fixtures

* refactor: track AWAIT and YIELD in separate handler

* fix flow errors

* add flow type annotation to production-parameter

* address review comments

* refactor: track [Return] parameter
  • Loading branch information
JLHwung authored and rajasekarm committed Feb 17, 2020
1 parent deea971 commit aa13876
Show file tree
Hide file tree
Showing 16 changed files with 650 additions and 73 deletions.
2 changes: 2 additions & 0 deletions packages/babel-parser/src/parser/base.js
Expand Up @@ -5,13 +5,15 @@ import type State from "../tokenizer/state";
import type { PluginsMap } from "./index";
import type ScopeHandler from "../util/scope";
import type ClassScopeHandler from "../util/class-scope";
import type ProductionParameterHandler from "../util/production-parameter";

export default class BaseParser {
// Properties set by constructor in index.js
options: Options;
inModule: boolean;
scope: ScopeHandler<*>;
classScope: ClassScopeHandler;
prodParam: ProductionParameterHandler;
plugins: PluginsMap;
filename: ?string;
sawUnambiguousESM: boolean = false;
Expand Down
62 changes: 41 additions & 21 deletions packages/babel-parser/src/parser/expression.js
Expand Up @@ -33,15 +33,20 @@ import * as charCodes from "charcodes";
import {
BIND_OUTSIDE,
BIND_VAR,
functionFlags,
SCOPE_ARROW,
SCOPE_CLASS,
SCOPE_DIRECT_SUPER,
SCOPE_FUNCTION,
SCOPE_SUPER,
SCOPE_PROGRAM,
SCOPE_ASYNC,
} from "../util/scopeflags";
import { ExpressionErrors } from "./util";
import {
PARAM_AWAIT,
PARAM_RETURN,
PARAM,
functionFlags,
} from "../util/production-parameter";

export default class ExpressionParser extends LValParser {
// Forward-declaration: defined in statement.js
Expand Down Expand Up @@ -108,11 +113,12 @@ export default class ExpressionParser extends LValParser {

// Convenience method to parse an Expression only
getExpression(): N.Expression {
let scopeFlags = SCOPE_PROGRAM;
let paramFlags = PARAM;
if (this.hasPlugin("topLevelAwait") && this.inModule) {
scopeFlags |= SCOPE_ASYNC;
paramFlags |= PARAM_AWAIT;
}
this.scope.enter(scopeFlags);
this.scope.enter(SCOPE_PROGRAM);
this.prodParam.enter(paramFlags);
this.nextToken();
const expr = this.parseExpression();
if (!this.match(tt.eof)) {
Expand All @@ -131,12 +137,18 @@ export default class ExpressionParser extends LValParser {
// and, *if* the syntactic construct they handle is present, wrap
// the AST node that the inner parser gave them in another node.

// Parse a full expression. The optional arguments are used to
// forbid the `in` operator (in for loops initialization expressions)
// and provide reference for storing '=' operator inside shorthand
// property assignment in contexts where both object expression
// and object pattern might appear (so it's possible to raise
// delayed syntax error at correct position).
// Parse a full expression.
// - `noIn`
// is used to forbid the `in` operator (in for loops initialization expressions)
// When `noIn` is true, the production parameter [In] is not present.
// Whenever [?In] appears in the right-hand sides of a production, we pass
// `noIn` to the subroutine calls.

// - `refExpressionErrors `
// provides reference for storing '=' operator inside shorthand
// property assignment in contexts where both object expression
// and object pattern might appear (so it's possible to raise
// delayed syntax error at correct position).

parseExpression(
noIn?: boolean,
Expand Down Expand Up @@ -169,7 +181,7 @@ export default class ExpressionParser extends LValParser {
const startPos = this.state.start;
const startLoc = this.state.startLoc;
if (this.isContextual("yield")) {
if (this.scope.inGenerator) {
if (this.prodParam.hasYield) {
let left = this.parseYield(noIn);
if (afterLeftParse) {
left = afterLeftParse.call(this, left, startPos, startLoc);
Expand Down Expand Up @@ -367,7 +379,7 @@ export default class ExpressionParser extends LValParser {
if (
this.match(tt.name) &&
this.state.value === "await" &&
this.scope.inAsync
this.prodParam.hasAwait
) {
throw this.raise(
this.state.start,
Expand Down Expand Up @@ -1161,7 +1173,7 @@ export default class ExpressionParser extends LValParser {
this.next();
meta = this.createIdentifier(meta, "function");

if (this.scope.inGenerator && this.eat(tt.dot)) {
if (this.prodParam.hasYield && this.eat(tt.dot)) {
return this.parseMetaProperty(node, meta, "sent");
}
return this.parseFunction(node);
Expand Down Expand Up @@ -1833,13 +1845,15 @@ export default class ExpressionParser extends LValParser {
node.generator = !!isGenerator;
const allowModifiers = isConstructor; // For TypeScript parameter properties
this.scope.enter(
functionFlags(isAsync, node.generator) |
SCOPE_FUNCTION |
SCOPE_SUPER |
(inClassScope ? SCOPE_CLASS : 0) |
(allowDirectSuper ? SCOPE_DIRECT_SUPER : 0),
);
this.prodParam.enter(functionFlags(isAsync, node.generator));
this.parseFunctionParams((node: any), allowModifiers);
this.parseFunctionBodyAndFinish(node, type, true);
this.prodParam.exit();
this.scope.exit();

this.state.yieldPos = oldYieldPos;
Expand All @@ -1857,7 +1871,8 @@ export default class ExpressionParser extends LValParser {
isAsync: boolean,
trailingCommaPos: ?number,
): N.ArrowFunctionExpression {
this.scope.enter(functionFlags(isAsync, false) | SCOPE_ARROW);
this.scope.enter(SCOPE_FUNCTION | SCOPE_ARROW);
this.prodParam.enter(functionFlags(isAsync, false));
this.initFunction(node, isAsync);
const oldMaybeInArrowParameters = this.state.maybeInArrowParameters;
const oldYieldPos = this.state.yieldPos;
Expand All @@ -1872,6 +1887,7 @@ export default class ExpressionParser extends LValParser {
this.state.awaitPos = -1;
this.parseFunctionBody(node, true);

this.prodParam.exit();
this.scope.exit();
this.state.maybeInArrowParameters = oldMaybeInArrowParameters;
this.state.yieldPos = oldYieldPos;
Expand Down Expand Up @@ -1949,7 +1965,11 @@ export default class ExpressionParser extends LValParser {
allowExpression,
!oldStrict && useStrict,
);
// FunctionBody[Yield, Await]:
// StatementList[?Yield, ?Await, +Return] opt
this.prodParam.enter(this.prodParam.currentFlags() | PARAM_RETURN);
node.body = this.parseBlock(true, false);
this.prodParam.exit();
this.state.labels = oldLabels;
}

Expand Down Expand Up @@ -2140,7 +2160,7 @@ export default class ExpressionParser extends LValParser {
checkKeywords: boolean,
isBinding: boolean,
): void {
if (this.scope.inGenerator && word === "yield") {
if (this.prodParam.hasYield && word === "yield") {
this.raise(
startLoc,
"Can not use 'yield' as identifier inside a generator",
Expand All @@ -2149,7 +2169,7 @@ export default class ExpressionParser extends LValParser {
}

if (word === "await") {
if (this.scope.inAsync) {
if (this.prodParam.hasAwait) {
this.raise(
startLoc,
"Can not use 'await' as identifier inside an async function",
Expand Down Expand Up @@ -2187,7 +2207,7 @@ export default class ExpressionParser extends LValParser {
: isStrictReservedWord;

if (reservedTest(word, this.inModule)) {
if (!this.scope.inAsync && word === "await") {
if (!this.prodParam.hasAwait && word === "await") {
this.raise(
startLoc,
"Can not use keyword 'await' outside an async function",
Expand All @@ -2199,10 +2219,10 @@ export default class ExpressionParser extends LValParser {
}

isAwaitAllowed(): boolean {
if (this.scope.inFunction) return this.scope.inAsync;
if (this.scope.inFunction) return this.prodParam.hasAwait;
if (this.options.allowAwaitOutsideFunction) return true;
if (this.hasPlugin("topLevelAwait")) {
return this.inModule && this.scope.inAsync;
return this.inModule && this.prodParam.hasAwait;
}
return false;
}
Expand Down
14 changes: 10 additions & 4 deletions packages/babel-parser/src/parser/index.js
Expand Up @@ -5,9 +5,13 @@ import type { File /*::, JSXOpeningElement */ } from "../types";
import type { PluginList } from "../plugin-utils";
import { getOptions } from "../options";
import StatementParser from "./statement";
import { SCOPE_ASYNC, SCOPE_PROGRAM } from "../util/scopeflags";
import { SCOPE_PROGRAM } from "../util/scopeflags";
import ScopeHandler from "../util/scope";
import ClassScopeHandler from "../util/class-scope";
import ProductionParameterHandler, {
PARAM_AWAIT,
PARAM,
} from "../util/production-parameter";

export type PluginsMap = Map<string, { [string]: any }>;

Expand All @@ -28,6 +32,7 @@ export default class Parser extends StatementParser {
this.options = options;
this.inModule = this.options.sourceType === "module";
this.scope = new ScopeHandler(this.raise.bind(this), this.inModule);
this.prodParam = new ProductionParameterHandler();
this.classScope = new ClassScopeHandler(this.raise.bind(this));
this.plugins = pluginsMap(this.options.plugins);
this.filename = options.sourceFilename;
Expand All @@ -39,11 +44,12 @@ export default class Parser extends StatementParser {
}

parse(): File {
let scopeFlags = SCOPE_PROGRAM;
let paramFlags = PARAM;
if (this.hasPlugin("topLevelAwait") && this.inModule) {
scopeFlags |= SCOPE_ASYNC;
paramFlags |= PARAM_AWAIT;
}
this.scope.enter(scopeFlags);
this.scope.enter(SCOPE_PROGRAM);
this.prodParam.enter(paramFlags);
const file = this.startNode();
const program = this.startNode();
this.nextToken();
Expand Down
15 changes: 12 additions & 3 deletions packages/babel-parser/src/parser/statement.js
Expand Up @@ -15,8 +15,8 @@ import {
BIND_LEXICAL,
BIND_VAR,
BIND_FUNCTION,
functionFlags,
SCOPE_CLASS,
SCOPE_FUNCTION,
SCOPE_OTHER,
SCOPE_SIMPLE_CATCH,
SCOPE_SUPER,
Expand All @@ -28,6 +28,7 @@ import {
type BindingTypes,
} from "../util/scopeflags";
import { ExpressionErrors } from "./util";
import { PARAM, functionFlags } from "../util/production-parameter";

const loopLabel = { kind: "loop" },
switchLabel = { kind: "switch" };
Expand Down Expand Up @@ -574,7 +575,7 @@ export default class StatementParser extends ExpressionParser {
}

parseReturnStatement(node: N.ReturnStatement): N.ReturnStatement {
if (!this.scope.inFunction && !this.options.allowReturnOutsideFunction) {
if (!this.prodParam.hasReturn && !this.options.allowReturnOutsideFunction) {
this.raise(this.state.start, "'return' outside of function");
}

Expand Down Expand Up @@ -1059,7 +1060,8 @@ export default class StatementParser extends ExpressionParser {
this.state.maybeInArrowParameters = false;
this.state.yieldPos = -1;
this.state.awaitPos = -1;
this.scope.enter(functionFlags(node.async, node.generator));
this.scope.enter(SCOPE_FUNCTION);
this.prodParam.enter(functionFlags(isAsync, node.generator));

if (!isStatement) {
node.id = this.parseFunctionId();
Expand All @@ -1078,6 +1080,7 @@ export default class StatementParser extends ExpressionParser {
);
});

this.prodParam.exit();
this.scope.exit();

if (isStatement && !isHangingStatement) {
Expand Down Expand Up @@ -1599,9 +1602,12 @@ export default class StatementParser extends ExpressionParser {
node: N.ClassPrivateProperty,
): N.ClassPrivateProperty {
this.scope.enter(SCOPE_CLASS | SCOPE_SUPER);
// [In] production parameter is tracked in parseMaybeAssign
this.prodParam.enter(PARAM);

node.value = this.eat(tt.eq) ? this.parseMaybeAssign() : null;
this.semicolon();
this.prodParam.exit();

this.scope.exit();

Expand All @@ -1614,6 +1620,8 @@ export default class StatementParser extends ExpressionParser {
}

this.scope.enter(SCOPE_CLASS | SCOPE_SUPER);
// [In] production parameter is tracked in parseMaybeAssign
this.prodParam.enter(PARAM);

if (this.match(tt.eq)) {
this.expectPlugin("classProperties");
Expand All @@ -1624,6 +1632,7 @@ export default class StatementParser extends ExpressionParser {
}
this.semicolon();

this.prodParam.exit();
this.scope.exit();

return this.finishNode(node, "ClassProperty");
Expand Down
4 changes: 2 additions & 2 deletions packages/babel-parser/src/plugins/flow.js
Expand Up @@ -12,13 +12,13 @@ import { types as tc } from "../tokenizer/context";
import * as charCodes from "charcodes";
import { isIteratorStart } from "../util/identifier";
import {
functionFlags,
type BindingTypes,
BIND_NONE,
BIND_LEXICAL,
BIND_VAR,
BIND_FUNCTION,
SCOPE_ARROW,
SCOPE_FUNCTION,
SCOPE_OTHER,
} from "../util/scopeflags";
import type { ExpressionErrors } from "../parser/util";
Expand Down Expand Up @@ -1889,7 +1889,7 @@ export default (superClass: Class<Parser>): Class<Parser> =>
node.extra?.trailingComma,
);
// Enter scope, as checkParams defines bindings
this.scope.enter(functionFlags(false, false) | SCOPE_ARROW);
this.scope.enter(SCOPE_FUNCTION | SCOPE_ARROW);
// Use super's method to force the parameters to be checked
super.checkParams(node, false, true);
this.scope.exit();
Expand Down
7 changes: 7 additions & 0 deletions packages/babel-parser/src/plugins/typescript/index.js
Expand Up @@ -26,6 +26,7 @@ import {
import TypeScriptScopeHandler from "./scope";
import * as charCodes from "charcodes";
import type { ExpressionErrors } from "../../parser/util";
import { PARAM } from "../../util/production-parameter";

type TsModifier =
| "readonly"
Expand Down Expand Up @@ -1265,7 +1266,9 @@ export default (superClass: Class<Parser>): Class<Parser> =>
node.body = inner;
} else {
this.scope.enter(SCOPE_TS_MODULE);
this.prodParam.enter(PARAM);
node.body = this.tsParseModuleBlock();
this.prodParam.exit();
this.scope.exit();
}
return this.finishNode(node, "TSModuleDeclaration");
Expand All @@ -1284,7 +1287,9 @@ export default (superClass: Class<Parser>): Class<Parser> =>
}
if (this.match(tt.braceL)) {
this.scope.enter(SCOPE_TS_MODULE);
this.prodParam.enter(PARAM);
node.body = this.tsParseModuleBlock();
this.prodParam.exit();
this.scope.exit();
} else {
this.semicolon();
Expand Down Expand Up @@ -1439,11 +1444,13 @@ export default (superClass: Class<Parser>): Class<Parser> =>
// Would like to use tsParseAmbientExternalModuleDeclaration here, but already ran past "global".
if (this.match(tt.braceL)) {
this.scope.enter(SCOPE_TS_MODULE);
this.prodParam.enter(PARAM);
const mod: N.TsModuleDeclaration = node;
mod.global = true;
mod.id = expr;
mod.body = this.tsParseModuleBlock();
this.scope.exit();
this.prodParam.exit();
return this.finishNode(mod, "TSModuleDeclaration");
}
break;
Expand Down
2 changes: 1 addition & 1 deletion packages/babel-parser/src/tokenizer/context.js
Expand Up @@ -60,7 +60,7 @@ tt.name.updateContext = function(prevType) {
if (prevType !== tt.dot) {
if (
(this.state.value === "of" && !this.state.exprAllowed) ||
(this.state.value === "yield" && this.scope.inGenerator)
(this.state.value === "yield" && this.prodParam.hasYield)
) {
allowed = true;
}
Expand Down

0 comments on commit aa13876

Please sign in to comment.