sucrase
Version:
Super-fast alternative to Babel for when you can target modern JS runtimes
732 lines (731 loc) • 30.2 kB
JavaScript
"use strict";
/* eslint max-len: 0 */
Object.defineProperty(exports, "__esModule", { value: true });
// A recursive descent parser operates by defining functions for all
// syntactic elements, and recursively calling those, each function
// advancing the input stream and returning an AST node. Precedence
// of constructs (for example, the fact that `!x[1]` means `!(x[1])`
// instead of `(!x)[1]` is handled by the fact that the parser
// function that parses unary prefix operators is called first, and
// in turn calls the function that parses `[]` subscripts — that
// way, it'll receive the node for `x[1]` already parsed, and wraps
// *that* in the unary operator node.
//
// Acorn uses an [operator precedence parser][opp] to handle binary
// operator precedence, because it is much more compact than using
// the technique outlined above, which uses different, nesting
// functions to specify precedence, for all of the ten binary
// precedence levels that JavaScript defines.
//
// [opp]: http://en.wikipedia.org/wiki/Operator-precedence_parser
const tokenizer_1 = require("../tokenizer");
const types_1 = require("../tokenizer/types");
const lval_1 = require("./lval");
class ExpressionParser extends lval_1.default {
// ### Expression parsing
// These nest, from the most general expression type at the top to
// 'atomic', nondivisible expression types at the bottom. Most of
// the functions will simply let the function (s) below them parse,
// 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).
parseExpression(noIn) {
this.parseMaybeAssign(noIn);
if (this.match(types_1.types.comma)) {
while (this.eat(types_1.types.comma)) {
this.parseMaybeAssign(noIn);
}
}
}
// Parse an assignment expression. This includes applications of
// operators like `+=`.
// Returns true if the expression was an arrow function.
parseMaybeAssign(noIn = null, afterLeftParse) {
if (this.match(types_1.types._yield) && this.state.inGenerator) {
this.parseYield();
if (afterLeftParse) {
afterLeftParse.call(this);
}
return false;
}
if (this.match(types_1.types.parenL) || this.match(types_1.types.name) || this.match(types_1.types._yield)) {
this.state.potentialArrowAt = this.state.start;
}
const wasArrow = this.parseMaybeConditional(noIn);
if (afterLeftParse) {
afterLeftParse.call(this);
}
if (this.state.type.isAssign) {
this.next();
this.parseMaybeAssign(noIn);
return false;
}
return wasArrow;
}
// Parse a ternary conditional (`?:`) operator.
// Returns true if the expression was an arrow function.
parseMaybeConditional(noIn) {
const startPos = this.state.start;
const wasArrow = this.parseExprOps(noIn);
if (wasArrow) {
return true;
}
this.parseConditional(noIn, startPos);
return false;
}
parseConditional(noIn, startPos) {
if (this.eat(types_1.types.question)) {
this.parseMaybeAssign();
this.expect(types_1.types.colon);
this.parseMaybeAssign(noIn);
}
}
// Start the precedence parser.
// Returns true if this was an arrow function
parseExprOps(noIn) {
const wasArrow = this.parseMaybeUnary();
if (wasArrow) {
return true;
}
this.parseExprOp(-1, noIn);
return false;
}
// Parse binary operators with the operator precedence parsing
// algorithm. `left` is the left-hand side of the operator.
// `minPrec` provides context that allows the function to stop and
// defer further parser to one of its callers when it encounters an
// operator that has a lower precedence than the set it is parsing.
parseExprOp(minPrec, noIn) {
const prec = this.state.type.binop;
if (prec != null && (!noIn || !this.match(types_1.types._in))) {
if (prec > minPrec) {
const operator = this.state.value;
const op = this.state.type;
this.next();
if (operator === "|>") {
// Support syntax such as 10 |> x => x + 1
this.state.potentialArrowAt = this.state.start;
}
this.parseMaybeUnary();
this.parseExprOp(op.rightAssociative ? prec - 1 : prec, noIn);
this.parseExprOp(minPrec, noIn);
}
}
}
// Parse unary operators, both prefix and postfix.
// Returns true if this was an arrow function.
parseMaybeUnary() {
if (this.state.type.prefix) {
this.next();
this.parseMaybeUnary();
return false;
}
const wasArrow = this.parseExprSubscripts();
if (wasArrow) {
return true;
}
while (this.state.type.postfix && !this.canInsertSemicolon()) {
this.next();
}
return false;
}
// Parse call, dot, and `[]`-subscript expressions.
// Returns true if this was an arrow function.
parseExprSubscripts() {
const startPos = this.state.start;
const wasArrow = this.parseExprAtom();
if (wasArrow) {
return true;
}
this.parseSubscripts(startPos);
return false;
}
parseSubscripts(startPos, noCalls = null) {
const state = { stop: false };
do {
this.parseSubscript(startPos, noCalls, state);
} while (!state.stop);
}
/** Set 'state.stop = true' to indicate that we should stop parsing subscripts. */
parseSubscript(startPos, noCalls, state) {
if (!noCalls && this.eat(types_1.types.doubleColon)) {
this.parseNoCallExpr();
state.stop = true;
this.parseSubscripts(startPos, noCalls);
}
else if (this.match(types_1.types.questionDot)) {
if (noCalls && this.lookaheadType() === types_1.types.parenL) {
state.stop = true;
return;
}
this.next();
if (this.eat(types_1.types.bracketL)) {
this.parseExpression();
this.expect(types_1.types.bracketR);
}
else if (this.eat(types_1.types.parenL)) {
this.parseCallExpressionArguments(types_1.types.parenR);
}
else {
this.parseIdentifier();
}
}
else if (this.eat(types_1.types.dot)) {
this.parseMaybePrivateName();
}
else if (this.eat(types_1.types.bracketL)) {
this.parseExpression();
this.expect(types_1.types.bracketR);
}
else if (!noCalls && this.match(types_1.types.parenL)) {
const possibleAsync = this.atPossibleAsync();
// We see "async", but it's possible it's a usage of the name "async". Parse as if it's a
// function call, and if we see an arrow later, backtrack and re-parse as a parameter list.
const snapshotForAsyncArrow = possibleAsync ? this.state.snapshot() : null;
const startTokenIndex = this.state.tokens.length;
this.next();
const callContextId = this.nextContextId++;
this.state.tokens[this.state.tokens.length - 1].contextId = callContextId;
this.parseCallExpressionArguments(types_1.types.parenR);
this.state.tokens[this.state.tokens.length - 1].contextId = callContextId;
if (possibleAsync && this.shouldParseAsyncArrow()) {
// We hit an arrow, so backtrack and start again parsing function parameters.
this.state.restoreFromSnapshot(snapshotForAsyncArrow);
state.stop = true;
this.parseFunctionParams();
this.parseAsyncArrowFromCallExpression(startPos, startTokenIndex);
}
}
else if (this.match(types_1.types.backQuote)) {
// Tagged template expression.
this.parseTemplate(true);
}
else {
state.stop = true;
}
}
atPossibleAsync() {
// This was made less strict than the original version to avoid passing around nodes, but it
// should be safe to have rare false positives here.
return (this.state.tokens[this.state.tokens.length - 1].value === "async" &&
!this.canInsertSemicolon());
}
parseCallExpressionArguments(close) {
let first = true;
while (!this.eat(close)) {
if (first) {
first = false;
}
else {
this.expect(types_1.types.comma);
if (this.eat(close))
break;
}
this.parseExprListItem(false);
}
}
shouldParseAsyncArrow() {
return this.match(types_1.types.arrow);
}
parseAsyncArrowFromCallExpression(functionStart, startTokenIndex) {
this.expect(types_1.types.arrow);
this.parseArrowExpression(functionStart, startTokenIndex);
}
// Parse a no-call expression (like argument of `new` or `::` operators).
parseNoCallExpr() {
const startPos = this.state.start;
this.parseExprAtom();
this.parseSubscripts(startPos, true);
}
// Parse an atomic expression — either a single token that is an
// expression, an expression started by a keyword like `function` or
// `new`, or an expression wrapped in punctuation like `()`, `[]`,
// or `{}`.
// Returns true if the parsed expression was an arrow function.
parseExprAtom() {
const canBeArrow = this.state.potentialArrowAt === this.state.start;
switch (this.state.type) {
case types_1.types._super:
case types_1.types._this:
case types_1.types.regexp:
case types_1.types.num:
case types_1.types.bigint:
case types_1.types.string:
case types_1.types._null:
case types_1.types._true:
case types_1.types._false:
this.next();
return false;
case types_1.types._import:
if (this.lookaheadType() === types_1.types.dot) {
this.parseImportMetaProperty();
return false;
}
this.next();
return false;
case types_1.types._yield:
if (this.state.inGenerator)
this.unexpected();
case types_1.types.name: {
const startTokenIndex = this.state.tokens.length;
const functionStart = this.state.start;
const name = this.state.value;
this.parseIdentifier();
if (name === "await") {
this.parseAwait();
return false;
}
else if (name === "async" && this.match(types_1.types._function) && !this.canInsertSemicolon()) {
this.next();
this.parseFunction(functionStart, false, false);
return false;
}
else if (canBeArrow && name === "async" && this.match(types_1.types.name)) {
this.parseIdentifier();
this.expect(types_1.types.arrow);
// let foo = bar => {};
this.parseArrowExpression(functionStart, startTokenIndex);
return true;
}
if (canBeArrow && !this.canInsertSemicolon() && this.eat(types_1.types.arrow)) {
this.parseArrowExpression(functionStart, startTokenIndex);
return true;
}
this.state.tokens[this.state.tokens.length - 1].identifierRole = tokenizer_1.IdentifierRole.Access;
return false;
}
case types_1.types._do: {
this.next();
this.parseBlock(false);
return false;
}
case types_1.types.parenL: {
const wasArrow = this.parseParenAndDistinguishExpression(canBeArrow);
return wasArrow;
}
case types_1.types.bracketL:
this.next();
this.parseExprList(types_1.types.bracketR, true);
return false;
case types_1.types.braceL:
this.parseObj(false, false);
return false;
case types_1.types._function:
this.parseFunctionExpression();
return false;
case types_1.types.at:
this.parseDecorators();
// Fall through.
case types_1.types._class:
this.parseClass(false);
return false;
case types_1.types._new:
this.parseNew();
return false;
case types_1.types.backQuote:
this.parseTemplate(false);
return false;
case types_1.types.doubleColon: {
this.next();
this.parseNoCallExpr();
return false;
}
default:
throw this.unexpected();
}
}
parseMaybePrivateName() {
this.eat(types_1.types.hash);
this.parseIdentifier();
}
parseFunctionExpression() {
const functionStart = this.state.start;
this.parseIdentifier();
if (this.state.inGenerator && this.eat(types_1.types.dot)) {
// function.sent
this.parseMetaProperty();
}
this.parseFunction(functionStart, false);
}
parseMetaProperty() {
this.parseIdentifier();
}
parseImportMetaProperty() {
this.parseIdentifier();
this.expect(types_1.types.dot);
// import.meta
this.parseMetaProperty();
}
parseLiteral() {
this.next();
}
parseParenExpression() {
this.expect(types_1.types.parenL);
this.parseExpression();
this.expect(types_1.types.parenR);
}
// Returns true if this was an arrow expression.
parseParenAndDistinguishExpression(canBeArrow) {
// Assume this is a normal parenthesized expression, but if we see an arrow, we'll bail and
// start over as a parameter list.
const snapshot = this.state.snapshot();
const startTokenIndex = this.state.tokens.length;
this.expect(types_1.types.parenL);
const exprList = [];
let first = true;
let spreadStart;
let optionalCommaStart;
while (!this.match(types_1.types.parenR)) {
if (first) {
first = false;
}
else {
this.expect(types_1.types.comma);
if (this.match(types_1.types.parenR)) {
optionalCommaStart = this.state.start;
break;
}
}
if (this.match(types_1.types.ellipsis)) {
spreadStart = this.state.start;
this.parseRest(false /* isBlockScope */);
this.parseParenItem();
if (this.match(types_1.types.comma) && this.lookaheadType() === types_1.types.parenR) {
this.raise(this.state.start, "A trailing comma is not permitted after the rest element");
}
break;
}
else {
exprList.push(this.parseMaybeAssign(false, this.parseParenItem));
}
}
this.expect(types_1.types.parenR);
if (canBeArrow && this.shouldParseArrow()) {
const wasArrow = this.parseArrow();
if (wasArrow) {
// It was an arrow function this whole time, so start over and parse it as params so that we
// get proper token annotations.
this.state.restoreFromSnapshot(snapshot);
// We don't need to worry about functionStart for arrow functions, so just use something.
const functionStart = this.state.start;
// Don't specify a context ID because arrow function don't need a context ID.
this.parseFunctionParams();
this.parseArrow();
this.parseArrowExpression(functionStart, startTokenIndex);
return true;
}
}
if (optionalCommaStart)
this.unexpected(optionalCommaStart);
if (spreadStart)
this.unexpected(spreadStart);
return false;
}
shouldParseArrow() {
return !this.canInsertSemicolon();
}
// Returns whether there was an arrow token.
parseArrow() {
if (this.eat(types_1.types.arrow)) {
return true;
}
return false;
}
parseParenItem() { }
// New's precedence is slightly tricky. It must allow its argument to
// be a `[]` or dot subscript expression, but not a call — at least,
// not without wrapping it in parentheses. Thus, it uses the noCalls
// argument to parseSubscripts to prevent it from consuming the
// argument list.
parseNew() {
this.parseIdentifier();
if (this.eat(types_1.types.dot)) {
// new.target
this.parseMetaProperty();
return;
}
this.parseNoCallExpr();
this.eat(types_1.types.questionDot);
this.parseNewArguments();
}
parseNewArguments() {
if (this.eat(types_1.types.parenL)) {
this.parseExprList(types_1.types.parenR);
}
}
// Parse template expression.
parseTemplateElement(isTagged) {
if (this.state.value === null) {
if (!isTagged) {
// TODO: fix this
this.raise(this.state.pos, "Invalid escape sequence in template");
}
}
this.next();
}
parseTemplate(isTagged) {
this.next();
this.parseTemplateElement(isTagged);
while (!this.match(types_1.types.backQuote)) {
this.expect(types_1.types.dollarBraceL);
this.parseExpression();
this.expect(types_1.types.braceR);
this.parseTemplateElement(isTagged);
}
this.next();
}
// Parse an object literal or binding pattern.
parseObj(isPattern, isBlockScope) {
// Attach a context ID to the object open and close brace and each object key.
const contextId = this.nextContextId++;
let first = true;
this.next();
this.state.tokens[this.state.tokens.length - 1].contextId = contextId;
let firstRestLocation = null;
while (!this.eat(types_1.types.braceR)) {
if (first) {
first = false;
}
else {
this.expect(types_1.types.comma);
if (this.eat(types_1.types.braceR)) {
break;
}
}
let isGenerator = false;
if (this.match(types_1.types.ellipsis)) {
// Note that this is labeled as an access on the token even though it might be an
// assignment.
this.parseSpread();
if (isPattern) {
const position = this.state.start;
if (firstRestLocation !== null) {
this.unexpected(firstRestLocation, "Cannot have multiple rest elements when destructuring");
}
else if (this.eat(types_1.types.braceR)) {
break;
}
else if (this.match(types_1.types.comma) && this.lookaheadType() === types_1.types.braceR) {
this.unexpected(position, "A trailing comma is not permitted after the rest element");
}
else {
firstRestLocation = position;
continue;
}
}
else {
continue;
}
}
if (!isPattern) {
isGenerator = this.eat(types_1.types.star);
}
if (!isPattern && this.isContextual("async")) {
if (isGenerator)
this.unexpected();
this.parseIdentifier();
if (this.match(types_1.types.colon) ||
this.match(types_1.types.parenL) ||
this.match(types_1.types.braceR) ||
this.match(types_1.types.eq) ||
this.match(types_1.types.comma)) {
// This is a key called "async" rather than an async function.
}
else {
if (this.match(types_1.types.star)) {
this.next();
isGenerator = true;
}
this.parsePropertyName(contextId);
}
}
else {
this.parsePropertyName(contextId);
}
this.parseObjPropValue(isGenerator, isPattern, isBlockScope, contextId);
}
this.state.tokens[this.state.tokens.length - 1].contextId = contextId;
}
isGetterOrSetterMethod(isPattern) {
// We go off of the next and don't bother checking if the node key is actually "get" or "set".
// This lets us avoid generating a node, and should only make the validation worse.
return (!isPattern &&
(this.match(types_1.types.string) || // get "string"() {}
this.match(types_1.types.num) || // get 1() {}
this.match(types_1.types.bracketL) || // get ["string"]() {}
this.match(types_1.types.name) || // get foo() {}
!!this.state.type.keyword) // get debugger() {}
);
}
// Returns true if this was a method.
parseObjectMethod(isGenerator, isPattern, objectContextId) {
// We don't need to worry about modifiers because object methods can't have optional bodies, so
// the start will never be used.
const functionStart = this.state.start;
if (this.match(types_1.types.parenL)) {
if (isPattern)
this.unexpected();
this.parseMethod(functionStart, isGenerator, /* isConstructor */ false);
return true;
}
if (this.isGetterOrSetterMethod(isPattern)) {
this.parsePropertyName(objectContextId);
this.parseMethod(functionStart, /* isGenerator */ false, /* isConstructor */ false);
return true;
}
return false;
}
parseObjectProperty(isPattern, isBlockScope) {
if (this.eat(types_1.types.colon)) {
if (isPattern) {
this.parseMaybeDefault(isBlockScope);
}
else {
this.parseMaybeAssign(false);
}
return;
}
// Since there's no colon, we assume this is an object shorthand.
// If we're in a destructuring, we've now discovered that the key was actually an assignee, so
// we need to tag it as a declaration with the appropriate scope. Otherwise, we might need to
// transform it on access, so mark it as an object shorthand.
if (isPattern) {
this.state.tokens[this.state.tokens.length - 1].identifierRole = isBlockScope
? tokenizer_1.IdentifierRole.BlockScopedDeclaration
: tokenizer_1.IdentifierRole.FunctionScopedDeclaration;
}
else {
this.state.tokens[this.state.tokens.length - 1].identifierRole =
tokenizer_1.IdentifierRole.ObjectShorthand;
}
// Regardless of whether we know this to be a pattern or if we're in an ambiguous context, allow
// parsing as if there's a default value.
this.parseMaybeDefault(isBlockScope, true);
}
parseObjPropValue(isGenerator, isPattern, isBlockScope, objectContextId) {
const wasMethod = this.parseObjectMethod(isGenerator, isPattern, objectContextId);
if (!wasMethod) {
this.parseObjectProperty(isPattern, isBlockScope);
}
}
parsePropertyName(objectContextId) {
if (this.eat(types_1.types.bracketL)) {
this.state.tokens[this.state.tokens.length - 1].contextId = objectContextId;
this.parseMaybeAssign();
this.expect(types_1.types.bracketR);
this.state.tokens[this.state.tokens.length - 1].contextId = objectContextId;
}
else {
const oldInPropertyName = this.state.inPropertyName;
this.state.inPropertyName = true;
if (this.match(types_1.types.num) || this.match(types_1.types.string)) {
this.parseExprAtom();
}
else {
this.parseMaybePrivateName();
}
this.state.tokens[this.state.tokens.length - 1].identifierRole = tokenizer_1.IdentifierRole.ObjectKey;
this.state.tokens[this.state.tokens.length - 1].contextId = objectContextId;
this.state.inPropertyName = oldInPropertyName;
}
}
// Parse object or class method.
parseMethod(functionStart, isGenerator, isConstructor) {
const oldInGenerator = this.state.inGenerator;
this.state.inGenerator = isGenerator;
const funcContextId = this.nextContextId++;
const startTokenIndex = this.state.tokens.length;
const allowModifiers = isConstructor; // For TypeScript parameter properties
this.parseFunctionParams(allowModifiers, funcContextId);
this.parseFunctionBodyAndFinish(functionStart, isGenerator, null /* allowExpressionBody */, funcContextId);
const endTokenIndex = this.state.tokens.length;
this.state.scopes.push({ startTokenIndex, endTokenIndex, isFunctionScope: true });
this.state.inGenerator = oldInGenerator;
}
// Parse arrow function expression.
// If the parameters are provided, they will be converted to an
// assignable list.
parseArrowExpression(functionStart, startTokenIndex) {
const oldInGenerator = this.state.inGenerator;
this.state.inGenerator = false;
this.parseFunctionBody(functionStart, false /* isGenerator */, true);
this.state.inGenerator = oldInGenerator;
const endTokenIndex = this.state.tokens.length;
this.state.scopes.push({ startTokenIndex, endTokenIndex, isFunctionScope: true });
}
parseFunctionBodyAndFinish(functionStart, isGenerator, allowExpressionBody = null, funcContextId) {
this.parseFunctionBody(functionStart, isGenerator, allowExpressionBody, funcContextId);
}
// Parse function body and check parameters.
parseFunctionBody(functionStart, isGenerator, allowExpression, funcContextId) {
const isExpression = allowExpression && !this.match(types_1.types.braceL);
if (isExpression) {
this.parseMaybeAssign();
}
else {
// Start a new scope with regard to labels and the `inGenerator`
// flag (restore them to their old value afterwards).
const oldInGen = this.state.inGenerator;
this.state.inGenerator = isGenerator;
this.parseBlock(true /* allowDirectives */, true /* isFunctionScope */, funcContextId);
this.state.inGenerator = oldInGen;
}
}
// Parses a comma-separated list of expressions, and returns them as
// an array. `close` is the token type that ends the list, and
// `allowEmpty` can be turned on to allow subsequent commas with
// nothing in between them to be parsed as `null` (which is needed
// for array literals).
parseExprList(close, allowEmpty = null) {
let first = true;
while (!this.eat(close)) {
if (first) {
first = false;
}
else {
this.expect(types_1.types.comma);
if (this.eat(close))
break;
}
this.parseExprListItem(allowEmpty);
}
}
parseExprListItem(allowEmpty) {
if (allowEmpty && this.match(types_1.types.comma)) {
// Empty item; nothing more to parse for this item.
}
else if (this.match(types_1.types.ellipsis)) {
this.parseSpread();
}
else {
this.parseMaybeAssign(false, this.parseParenItem);
}
}
// Parse the next token as an identifier. If `liberal` is true (used
// when parsing properties), it will also convert keywords into
// identifiers.
parseIdentifier() {
this.next();
this.state.tokens[this.state.tokens.length - 1].type = types_1.types.name;
}
// Parses await expression inside async function.
parseAwait() {
this.parseMaybeUnary();
}
// Parses yield expression inside generator.
parseYield() {
this.next();
if (!this.match(types_1.types.semi) &&
!this.canInsertSemicolon() &&
(this.match(types_1.types.star) || this.state.type.startsExpr)) {
this.eat(types_1.types.star);
this.parseMaybeAssign();
}
}
}
exports.default = ExpressionParser;