From c4fb3fe742c61ee2dd0ab1b1197a254d3c9f6a55 Mon Sep 17 00:00:00 2001 From: Andy Date: Sun, 23 Apr 2017 15:40:49 -0700 Subject: [PATCH] Convert each file with parser methods to a class in an inheritance chain (#481) --- src/parser/base.js | 24 + src/parser/comments.js | 222 ++--- src/parser/expression.js | 1928 ++++++++++++++++++------------------ src/parser/index.js | 31 +- src/parser/location.js | 22 +- src/parser/lval.js | 475 ++++----- src/parser/node.js | 73 +- src/parser/statement.js | 2031 +++++++++++++++++++------------------- src/parser/util.js | 154 +-- src/tokenizer/index.js | 10 +- 10 files changed, 2487 insertions(+), 2483 deletions(-) create mode 100644 src/parser/base.js diff --git a/src/parser/base.js b/src/parser/base.js new file mode 100644 index 0000000000..fa1fc13539 --- /dev/null +++ b/src/parser/base.js @@ -0,0 +1,24 @@ +// @flow + +import type { Options } from "../options"; +import { reservedWords } from "../util/identifier"; + +export default class BaseParser { + // Properties set by constructor in index.js + options: Options; + inModule: boolean; + plugins: { [key: string]: boolean }; + filename: ?string; + + isReservedWord(word: string): boolean { + if (word === "await") { + return this.inModule; + } else { + return reservedWords[6](word); + } + } + + hasPlugin(name: string): boolean { + return !!this.plugins[name]; + } +} diff --git a/src/parser/comments.js b/src/parser/comments.js index c032e85e07..a634491848 100644 --- a/src/parser/comments.js +++ b/src/parser/comments.js @@ -24,133 +24,133 @@ * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -import Parser from "./index"; +import BaseParser from "./base"; function last(stack) { return stack[stack.length - 1]; } -const pp = Parser.prototype; - -pp.addComment = function (comment) { - if (this.filename) comment.loc.filename = this.filename; - this.state.trailingComments.push(comment); - this.state.leadingComments.push(comment); -}; - -pp.processComment = function (node) { - if (node.type === "Program" && node.body.length > 0) return; - - const stack = this.state.commentStack; - - let lastChild, trailingComments, i, j; - - if (this.state.trailingComments.length > 0) { - // If the first comment in trailingComments comes after the - // current node, then we're good - all comments in the array will - // come after the node and so it's safe to add them as official - // trailingComments. - if (this.state.trailingComments[0].start >= node.end) { - trailingComments = this.state.trailingComments; - this.state.trailingComments = []; - } else { - // Otherwise, if the first comment doesn't come after the - // current node, that means we have a mix of leading and trailing - // comments in the array and that leadingComments contains the - // same items as trailingComments. Reset trailingComments to - // zero items and we'll handle this by evaluating leadingComments - // later. - this.state.trailingComments.length = 0; - } - } else { - const lastInStack = last(stack); - if (stack.length > 0 && lastInStack.trailingComments && lastInStack.trailingComments[0].start >= node.end) { - trailingComments = lastInStack.trailingComments; - lastInStack.trailingComments = null; - } +export default class CommentsParser extends BaseParser { + addComment(comment) { + if (this.filename) comment.loc.filename = this.filename; + this.state.trailingComments.push(comment); + this.state.leadingComments.push(comment); } - // Eating the stack. - while (stack.length > 0 && last(stack).start >= node.start) { - lastChild = stack.pop(); - } + processComment(node) { + if (node.type === "Program" && node.body.length > 0) return; - if (lastChild) { - if (lastChild.leadingComments) { - if (lastChild !== node && last(lastChild.leadingComments).end <= node.start) { - node.leadingComments = lastChild.leadingComments; - lastChild.leadingComments = null; + const stack = this.state.commentStack; + + let lastChild, trailingComments, i, j; + + if (this.state.trailingComments.length > 0) { + // If the first comment in trailingComments comes after the + // current node, then we're good - all comments in the array will + // come after the node and so it's safe to add them as official + // trailingComments. + if (this.state.trailingComments[0].start >= node.end) { + trailingComments = this.state.trailingComments; + this.state.trailingComments = []; } else { - // A leading comment for an anonymous class had been stolen by its first ClassMethod, - // so this takes back the leading comment. - // See also: https://github.com/eslint/espree/issues/158 - for (i = lastChild.leadingComments.length - 2; i >= 0; --i) { - if (lastChild.leadingComments[i].end <= node.start) { - node.leadingComments = lastChild.leadingComments.splice(0, i + 1); + // Otherwise, if the first comment doesn't come after the + // current node, that means we have a mix of leading and trailing + // comments in the array and that leadingComments contains the + // same items as trailingComments. Reset trailingComments to + // zero items and we'll handle this by evaluating leadingComments + // later. + this.state.trailingComments.length = 0; + } + } else { + const lastInStack = last(stack); + if (stack.length > 0 && lastInStack.trailingComments && lastInStack.trailingComments[0].start >= node.end) { + trailingComments = lastInStack.trailingComments; + lastInStack.trailingComments = null; + } + } + + // Eating the stack. + while (stack.length > 0 && last(stack).start >= node.start) { + lastChild = stack.pop(); + } + + if (lastChild) { + if (lastChild.leadingComments) { + if (lastChild !== node && last(lastChild.leadingComments).end <= node.start) { + node.leadingComments = lastChild.leadingComments; + lastChild.leadingComments = null; + } else { + // A leading comment for an anonymous class had been stolen by its first ClassMethod, + // so this takes back the leading comment. + // See also: https://github.com/eslint/espree/issues/158 + for (i = lastChild.leadingComments.length - 2; i >= 0; --i) { + if (lastChild.leadingComments[i].end <= node.start) { + node.leadingComments = lastChild.leadingComments.splice(0, i + 1); + break; + } + } + } + } + } else if (this.state.leadingComments.length > 0) { + if (last(this.state.leadingComments).end <= node.start) { + if (this.state.commentPreviousNode) { + for (j = 0; j < this.state.leadingComments.length; j++) { + if (this.state.leadingComments[j].end < this.state.commentPreviousNode.end) { + this.state.leadingComments.splice(j, 1); + j--; + } + } + } + if (this.state.leadingComments.length > 0) { + node.leadingComments = this.state.leadingComments; + this.state.leadingComments = []; + } + } else { + // https://github.com/eslint/espree/issues/2 + // + // In special cases, such as return (without a value) and + // debugger, all comments will end up as leadingComments and + // will otherwise be eliminated. This step runs when the + // commentStack is empty and there are comments left + // in leadingComments. + // + // This loop figures out the stopping point between the actual + // leading and trailing comments by finding the location of the + // first comment that comes after the given node. + for (i = 0; i < this.state.leadingComments.length; i++) { + if (this.state.leadingComments[i].end > node.start) { break; } } - } - } - } else if (this.state.leadingComments.length > 0) { - if (last(this.state.leadingComments).end <= node.start) { - if (this.state.commentPreviousNode) { - for (j = 0; j < this.state.leadingComments.length; j++) { - if (this.state.leadingComments[j].end < this.state.commentPreviousNode.end) { - this.state.leadingComments.splice(j, 1); - j--; - } - } - } - if (this.state.leadingComments.length > 0) { - node.leadingComments = this.state.leadingComments; - this.state.leadingComments = []; - } - } else { - // https://github.com/eslint/espree/issues/2 - // - // In special cases, such as return (without a value) and - // debugger, all comments will end up as leadingComments and - // will otherwise be eliminated. This step runs when the - // commentStack is empty and there are comments left - // in leadingComments. - // - // This loop figures out the stopping point between the actual - // leading and trailing comments by finding the location of the - // first comment that comes after the given node. - for (i = 0; i < this.state.leadingComments.length; i++) { - if (this.state.leadingComments[i].end > node.start) { - break; + + // Split the array based on the location of the first comment + // that comes after the node. Keep in mind that this could + // result in an empty array, and if so, the array must be + // deleted. + node.leadingComments = this.state.leadingComments.slice(0, i); + if ((node.leadingComments: Array).length === 0) { + node.leadingComments = null; + } + + // Similarly, trailing comments are attached later. The variable + // must be reset to null if there are no trailing comments. + trailingComments = this.state.leadingComments.slice(i); + if (trailingComments.length === 0) { + trailingComments = null; } } + } - // Split the array based on the location of the first comment - // that comes after the node. Keep in mind that this could - // result in an empty array, and if so, the array must be - // deleted. - node.leadingComments = this.state.leadingComments.slice(0, i); - if ((node.leadingComments: Array).length === 0) { - node.leadingComments = null; - } + this.state.commentPreviousNode = node; - // Similarly, trailing comments are attached later. The variable - // must be reset to null if there are no trailing comments. - trailingComments = this.state.leadingComments.slice(i); - if (trailingComments.length === 0) { - trailingComments = null; + if (trailingComments) { + if (trailingComments.length && trailingComments[0].start >= node.start && last(trailingComments).end <= node.end) { + node.innerComments = trailingComments; + } else { + node.trailingComments = trailingComments; } } + + stack.push(node); } - - this.state.commentPreviousNode = node; - - if (trailingComments) { - if (trailingComments.length && trailingComments[0].start >= node.start && last(trailingComments).end <= node.end) { - node.innerComments = trailingComments; - } else { - node.trailingComments = trailingComments; - } - } - - stack.push(node); -}; +} diff --git a/src/parser/expression.js b/src/parser/expression.js index e813460314..168e595415 100644 --- a/src/parser/expression.js +++ b/src/parser/expression.js @@ -19,1104 +19,1104 @@ // [opp]: http://en.wikipedia.org/wiki/Operator-precedence_parser import { types as tt } from "../tokenizer/types"; -import Parser from "./index"; +import LValParser from "./lval"; import { reservedWords } from "../util/identifier"; -const pp = Parser.prototype; +export default class ExpressionParser extends LValParser { + // Check if property name clashes with already added. + // Object/class getters and setters are not allowed to clash — + // either with each other or with an init property — and in + // strict mode, init properties are also not allowed to be repeated. -// Check if property name clashes with already added. -// Object/class getters and setters are not allowed to clash — -// either with each other or with an init property — and in -// strict mode, init properties are also not allowed to be repeated. + checkPropClash(prop, propHash) { + if (prop.computed || prop.kind) return; -pp.checkPropClash = function (prop, propHash) { - if (prop.computed || prop.kind) return; + const key = prop.key; + // It is either an Identifier or a String/NumericLiteral + const name = key.type === "Identifier" ? key.name : String(key.value); - const key = prop.key; - // It is either an Identifier or a String/NumericLiteral - const name = key.type === "Identifier" ? key.name : String(key.value); - - if (name === "__proto__") { - if (propHash.proto) this.raise(key.start, "Redefinition of __proto__ property"); - propHash.proto = true; - } -}; - -// Convenience method to parse an Expression only -pp.getExpression = function() { - this.nextToken(); - const expr = this.parseExpression(); - if (!this.match(tt.eof)) { - this.unexpected(); - } - return expr; -}; - -// ### 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). - -pp.parseExpression = function (noIn, refShorthandDefaultPos) { - const startPos = this.state.start; - const startLoc = this.state.startLoc; - const expr = this.parseMaybeAssign(noIn, refShorthandDefaultPos); - if (this.match(tt.comma)) { - const node = this.startNodeAt(startPos, startLoc); - node.expressions = [expr]; - while (this.eat(tt.comma)) { - node.expressions.push(this.parseMaybeAssign(noIn, refShorthandDefaultPos)); + if (name === "__proto__") { + if (propHash.proto) this.raise(key.start, "Redefinition of __proto__ property"); + propHash.proto = true; } - this.toReferencedList(node.expressions); - return this.finishNode(node, "SequenceExpression"); - } - return expr; -}; - -// Parse an assignment expression. This includes applications of -// operators like `+=`. - -pp.parseMaybeAssign = function (noIn, refShorthandDefaultPos, afterLeftParse, refNeedsArrowPos) { - const startPos = this.state.start; - const startLoc = this.state.startLoc; - - if (this.match(tt._yield) && this.state.inGenerator) { - let left = this.parseYield(); - if (afterLeftParse) left = afterLeftParse.call(this, left, startPos, startLoc); - return left; } - let failOnShorthandAssign; - if (refShorthandDefaultPos) { - failOnShorthandAssign = false; - } else { - refShorthandDefaultPos = { start: 0 }; - failOnShorthandAssign = true; - } - - if (this.match(tt.parenL) || this.match(tt.name)) { - this.state.potentialArrowAt = this.state.start; - } - - let left = this.parseMaybeConditional(noIn, refShorthandDefaultPos, refNeedsArrowPos); - if (afterLeftParse) left = afterLeftParse.call(this, left, startPos, startLoc); - if (this.state.type.isAssign) { - const node = this.startNodeAt(startPos, startLoc); - node.operator = this.state.value; - node.left = this.match(tt.eq) ? this.toAssignable(left, undefined, "assignment expression") : left; - refShorthandDefaultPos.start = 0; // reset because shorthand default was used correctly - - this.checkLVal(left, undefined, undefined, "assignment expression"); - - if (left.extra && left.extra.parenthesized) { - let errorMsg; - if (left.type === "ObjectPattern") { - errorMsg = "`({a}) = 0` use `({a} = 0)`"; - } else if (left.type === "ArrayPattern") { - errorMsg = "`([a]) = 0` use `([a] = 0)`"; - } - if (errorMsg) { - this.raise(left.start, `You're trying to assign to a parenthesized expression, eg. instead of ${errorMsg}`); - } + // Convenience method to parse an Expression only + getExpression() { + this.nextToken(); + const expr = this.parseExpression(); + if (!this.match(tt.eof)) { + this.unexpected(); } - - this.next(); - node.right = this.parseMaybeAssign(noIn); - return this.finishNode(node, "AssignmentExpression"); - } else if (failOnShorthandAssign && refShorthandDefaultPos.start) { - this.unexpected(refShorthandDefaultPos.start); - } - - return left; -}; - -// Parse a ternary conditional (`?:`) operator. - -pp.parseMaybeConditional = function (noIn, refShorthandDefaultPos, refNeedsArrowPos) { - const startPos = this.state.start; - const startLoc = this.state.startLoc; - const expr = this.parseExprOps(noIn, refShorthandDefaultPos); - if (refShorthandDefaultPos && refShorthandDefaultPos.start) return expr; - - return this.parseConditional(expr, noIn, startPos, startLoc, refNeedsArrowPos); -}; - -pp.parseConditional = function (expr, noIn, startPos, startLoc) { - if (this.eat(tt.question)) { - const node = this.startNodeAt(startPos, startLoc); - node.test = expr; - node.consequent = this.parseMaybeAssign(); - this.expect(tt.colon); - node.alternate = this.parseMaybeAssign(noIn); - return this.finishNode(node, "ConditionalExpression"); - } - return expr; -}; - -// Start the precedence parser. - -pp.parseExprOps = function (noIn, refShorthandDefaultPos) { - const startPos = this.state.start; - const startLoc = this.state.startLoc; - const expr = this.parseMaybeUnary(refShorthandDefaultPos); - if (refShorthandDefaultPos && refShorthandDefaultPos.start) { return expr; - } else { - return this.parseExprOp(expr, startPos, startLoc, -1, noIn); } -}; -// 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. + // ### Expression parsing -pp.parseExprOp = function(left, leftStartPos, leftStartLoc, minPrec, noIn) { - const prec = this.state.type.binop; - if (prec != null && (!noIn || !this.match(tt._in))) { - if (prec > minPrec) { - const node = this.startNodeAt(leftStartPos, leftStartLoc); - node.left = left; + // 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, refShorthandDefaultPos) { + const startPos = this.state.start; + const startLoc = this.state.startLoc; + const expr = this.parseMaybeAssign(noIn, refShorthandDefaultPos); + if (this.match(tt.comma)) { + const node = this.startNodeAt(startPos, startLoc); + node.expressions = [expr]; + while (this.eat(tt.comma)) { + node.expressions.push(this.parseMaybeAssign(noIn, refShorthandDefaultPos)); + } + this.toReferencedList(node.expressions); + return this.finishNode(node, "SequenceExpression"); + } + return expr; + } + + // Parse an assignment expression. This includes applications of + // operators like `+=`. + + parseMaybeAssign(noIn, refShorthandDefaultPos, afterLeftParse, refNeedsArrowPos) { + const startPos = this.state.start; + const startLoc = this.state.startLoc; + + if (this.match(tt._yield) && this.state.inGenerator) { + let left = this.parseYield(); + if (afterLeftParse) left = afterLeftParse.call(this, left, startPos, startLoc); + return left; + } + + let failOnShorthandAssign; + if (refShorthandDefaultPos) { + failOnShorthandAssign = false; + } else { + refShorthandDefaultPos = { start: 0 }; + failOnShorthandAssign = true; + } + + if (this.match(tt.parenL) || this.match(tt.name)) { + this.state.potentialArrowAt = this.state.start; + } + + let left = this.parseMaybeConditional(noIn, refShorthandDefaultPos, refNeedsArrowPos); + if (afterLeftParse) left = afterLeftParse.call(this, left, startPos, startLoc); + if (this.state.type.isAssign) { + const node = this.startNodeAt(startPos, startLoc); node.operator = this.state.value; + node.left = this.match(tt.eq) ? this.toAssignable(left, undefined, "assignment expression") : left; + refShorthandDefaultPos.start = 0; // reset because shorthand default was used correctly - if ( - node.operator === "**" && - left.type === "UnaryExpression" && - left.extra && - !left.extra.parenthesizedArgument && - !left.extra.parenthesized - ) { - this.raise(left.argument.start, "Illegal expression. Wrap left hand side or entire exponentiation in parentheses."); + this.checkLVal(left, undefined, undefined, "assignment expression"); + + if (left.extra && left.extra.parenthesized) { + let errorMsg; + if (left.type === "ObjectPattern") { + errorMsg = "`({a}) = 0` use `({a} = 0)`"; + } else if (left.type === "ArrayPattern") { + errorMsg = "`([a]) = 0` use `([a] = 0)`"; + } + if (errorMsg) { + this.raise(left.start, `You're trying to assign to a parenthesized expression, eg. instead of ${errorMsg}`); + } } - const op = this.state.type; this.next(); - - const startPos = this.state.start; - const startLoc = this.state.startLoc; - node.right = this.parseExprOp(this.parseMaybeUnary(), startPos, startLoc, op.rightAssociative ? prec - 1 : prec, noIn); - - this.finishNode(node, (op === tt.logicalOR || op === tt.logicalAND) ? "LogicalExpression" : "BinaryExpression"); - return this.parseExprOp(node, leftStartPos, leftStartLoc, minPrec, noIn); - } - } - return left; -}; - -// Parse unary operators, both prefix and postfix. - -pp.parseMaybeUnary = function (refShorthandDefaultPos) { - if (this.state.type.prefix) { - const node = this.startNode(); - const update = this.match(tt.incDec); - node.operator = this.state.value; - node.prefix = true; - this.next(); - - const argType = this.state.type; - node.argument = this.parseMaybeUnary(); - - this.addExtra(node, "parenthesizedArgument", argType === tt.parenL && (!node.argument.extra || !node.argument.extra.parenthesized)); - - if (refShorthandDefaultPos && refShorthandDefaultPos.start) { + node.right = this.parseMaybeAssign(noIn); + return this.finishNode(node, "AssignmentExpression"); + } else if (failOnShorthandAssign && refShorthandDefaultPos.start) { this.unexpected(refShorthandDefaultPos.start); } - if (update) { - this.checkLVal(node.argument, undefined, undefined, "prefix operation"); - } else if (this.state.strict && node.operator === "delete" && node.argument.type === "Identifier") { - this.raise(node.start, "Deleting local variable in strict mode"); + return left; + } + + // Parse a ternary conditional (`?:`) operator. + + parseMaybeConditional(noIn, refShorthandDefaultPos, refNeedsArrowPos) { + const startPos = this.state.start; + const startLoc = this.state.startLoc; + const expr = this.parseExprOps(noIn, refShorthandDefaultPos); + if (refShorthandDefaultPos && refShorthandDefaultPos.start) return expr; + + return this.parseConditional(expr, noIn, startPos, startLoc, refNeedsArrowPos); + } + + parseConditional(expr, noIn, startPos, startLoc) { + if (this.eat(tt.question)) { + const node = this.startNodeAt(startPos, startLoc); + node.test = expr; + node.consequent = this.parseMaybeAssign(); + this.expect(tt.colon); + node.alternate = this.parseMaybeAssign(noIn); + return this.finishNode(node, "ConditionalExpression"); } - - return this.finishNode(node, update ? "UpdateExpression" : "UnaryExpression"); - } - - const startPos = this.state.start; - const startLoc = this.state.startLoc; - let expr = this.parseExprSubscripts(refShorthandDefaultPos); - if (refShorthandDefaultPos && refShorthandDefaultPos.start) return expr; - while (this.state.type.postfix && !this.canInsertSemicolon()) { - const node = this.startNodeAt(startPos, startLoc); - node.operator = this.state.value; - node.prefix = false; - node.argument = expr; - this.checkLVal(expr, undefined, undefined, "postfix operation"); - this.next(); - expr = this.finishNode(node, "UpdateExpression"); - } - return expr; -}; - -// Parse call, dot, and `[]`-subscript expressions. - -pp.parseExprSubscripts = function (refShorthandDefaultPos) { - const startPos = this.state.start; - const startLoc = this.state.startLoc; - const potentialArrowAt = this.state.potentialArrowAt; - const expr = this.parseExprAtom(refShorthandDefaultPos); - - if (expr.type === "ArrowFunctionExpression" && expr.start === potentialArrowAt) { return expr; } - if (refShorthandDefaultPos && refShorthandDefaultPos.start) { - return expr; - } + // Start the precedence parser. - return this.parseSubscripts(expr, startPos, startLoc); -}; - -pp.parseSubscripts = function (base, startPos, startLoc, noCalls) { - for (;;) { - if (!noCalls && this.eat(tt.doubleColon)) { - const node = this.startNodeAt(startPos, startLoc); - node.object = base; - node.callee = this.parseNoCallExpr(); - return this.parseSubscripts(this.finishNode(node, "BindExpression"), startPos, startLoc, noCalls); - } else if (this.eat(tt.dot)) { - const node = this.startNodeAt(startPos, startLoc); - node.object = base; - node.property = this.parseIdentifier(true); - node.computed = false; - base = this.finishNode(node, "MemberExpression"); - } else if (this.eat(tt.bracketL)) { - const node = this.startNodeAt(startPos, startLoc); - node.object = base; - node.property = this.parseExpression(); - node.computed = true; - this.expect(tt.bracketR); - base = this.finishNode(node, "MemberExpression"); - } else if (!noCalls && this.match(tt.parenL)) { - const possibleAsync = this.state.potentialArrowAt === base.start && base.type === "Identifier" && base.name === "async" && !this.canInsertSemicolon(); - this.next(); - - const node = this.startNodeAt(startPos, startLoc); - node.callee = base; - node.arguments = this.parseCallExpressionArguments(tt.parenR, possibleAsync); - if (node.callee.type === "Import" && node.arguments.length !== 1) { - this.raise(node.start, "import() requires exactly one argument"); - } - base = this.finishNode(node, "CallExpression"); - - if (possibleAsync && this.shouldParseAsyncArrow()) { - return this.parseAsyncArrowFromCallExpression(this.startNodeAt(startPos, startLoc), node); - } else { - this.toReferencedList(node.arguments); - } - } else if (this.match(tt.backQuote)) { - const node = this.startNodeAt(startPos, startLoc); - node.tag = base; - node.quasi = this.parseTemplate(true); - base = this.finishNode(node, "TaggedTemplateExpression"); + parseExprOps(noIn, refShorthandDefaultPos) { + const startPos = this.state.start; + const startLoc = this.state.startLoc; + const expr = this.parseMaybeUnary(refShorthandDefaultPos); + if (refShorthandDefaultPos && refShorthandDefaultPos.start) { + return expr; } else { - return base; + return this.parseExprOp(expr, startPos, startLoc, -1, noIn); } } -}; -pp.parseCallExpressionArguments = function (close, possibleAsyncArrow) { - const elts = []; - let innerParenStart; - let first = true; + // 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. - while (!this.eat(close)) { - if (first) { - first = false; - } else { - this.expect(tt.comma); - if (this.eat(close)) break; - } + parseExprOp(left, leftStartPos, leftStartLoc, minPrec, noIn) { + const prec = this.state.type.binop; + if (prec != null && (!noIn || !this.match(tt._in))) { + if (prec > minPrec) { + const node = this.startNodeAt(leftStartPos, leftStartLoc); + node.left = left; + node.operator = this.state.value; - // we need to make sure that if this is an async arrow functions, that we don't allow inner parens inside the params - if (this.match(tt.parenL) && !innerParenStart) { - innerParenStart = this.state.start; - } - - elts.push(this.parseExprListItem(false, possibleAsyncArrow ? { start: 0 } : undefined, possibleAsyncArrow ? { start: 0 } : undefined)); - } - - // we found an async arrow function so let's not allow any inner parens - if (possibleAsyncArrow && innerParenStart && this.shouldParseAsyncArrow()) { - this.unexpected(); - } - - return elts; -}; - -pp.shouldParseAsyncArrow = function () { - return this.match(tt.arrow); -}; - -pp.parseAsyncArrowFromCallExpression = function (node, call) { - this.expect(tt.arrow); - return this.parseArrowExpression(node, call.arguments, true); -}; - -// Parse a no-call expression (like argument of `new` or `::` operators). - -pp.parseNoCallExpr = function () { - const startPos = this.state.start; - const startLoc = this.state.startLoc; - return this.parseSubscripts(this.parseExprAtom(), startPos, startLoc, 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 `{}`. - -pp.parseExprAtom = function (refShorthandDefaultPos) { - const canBeArrow = this.state.potentialArrowAt === this.state.start; - let node; - - switch (this.state.type) { - case tt._super: - if (!this.state.inMethod && !this.options.allowSuperOutsideMethod) { - this.raise(this.state.start, "'super' outside of function or class"); - } - - node = this.startNode(); - this.next(); - if (!this.match(tt.parenL) && !this.match(tt.bracketL) && !this.match(tt.dot)) { - this.unexpected(); - } - if (this.match(tt.parenL) && this.state.inMethod !== "constructor" && !this.options.allowSuperOutsideMethod) { - this.raise(node.start, "super() is only valid inside a class constructor. Make sure the method name is spelled exactly as 'constructor'."); - } - return this.finishNode(node, "Super"); - - case tt._import: - if (!this.hasPlugin("dynamicImport")) this.unexpected(); - - node = this.startNode(); - this.next(); - if (!this.match(tt.parenL)) { - this.unexpected(null, tt.parenL); - } - return this.finishNode(node, "Import"); - - case tt._this: - node = this.startNode(); - this.next(); - return this.finishNode(node, "ThisExpression"); - - case tt._yield: - if (this.state.inGenerator) this.unexpected(); - - case tt.name: - node = this.startNode(); - const allowAwait = this.state.value === "await" && this.state.inAsync; - const allowYield = this.shouldAllowYieldIdentifier(); - const id = this.parseIdentifier(allowAwait || allowYield); - - if (id.name === "await") { - if (this.state.inAsync || this.inModule) { - return this.parseAwait(node); + if ( + node.operator === "**" && + left.type === "UnaryExpression" && + left.extra && + !left.extra.parenthesizedArgument && + !left.extra.parenthesized + ) { + this.raise(left.argument.start, "Illegal expression. Wrap left hand side or entire exponentiation in parentheses."); } - } else if (id.name === "async" && this.match(tt._function) && !this.canInsertSemicolon()) { + + const op = this.state.type; this.next(); - return this.parseFunction(node, false, false, true); - } else if (canBeArrow && id.name === "async" && this.match(tt.name)) { - const params = [this.parseIdentifier()]; - this.expect(tt.arrow); - // let foo = bar => {}; - return this.parseArrowExpression(node, params, true); + + const startPos = this.state.start; + const startLoc = this.state.startLoc; + node.right = this.parseExprOp(this.parseMaybeUnary(), startPos, startLoc, op.rightAssociative ? prec - 1 : prec, noIn); + + this.finishNode(node, (op === tt.logicalOR || op === tt.logicalAND) ? "LogicalExpression" : "BinaryExpression"); + return this.parseExprOp(node, leftStartPos, leftStartLoc, minPrec, noIn); + } + } + return left; + } + + // Parse unary operators, both prefix and postfix. + + parseMaybeUnary(refShorthandDefaultPos) { + if (this.state.type.prefix) { + const node = this.startNode(); + const update = this.match(tt.incDec); + node.operator = this.state.value; + node.prefix = true; + this.next(); + + const argType = this.state.type; + node.argument = this.parseMaybeUnary(); + + this.addExtra(node, "parenthesizedArgument", argType === tt.parenL && (!node.argument.extra || !node.argument.extra.parenthesized)); + + if (refShorthandDefaultPos && refShorthandDefaultPos.start) { + this.unexpected(refShorthandDefaultPos.start); } - if (canBeArrow && !this.canInsertSemicolon() && this.eat(tt.arrow)) { - return this.parseArrowExpression(node, [id]); + if (update) { + this.checkLVal(node.argument, undefined, undefined, "prefix operation"); + } else if (this.state.strict && node.operator === "delete" && node.argument.type === "Identifier") { + this.raise(node.start, "Deleting local variable in strict mode"); } - return id; + return this.finishNode(node, update ? "UpdateExpression" : "UnaryExpression"); + } - case tt._do: - if (this.hasPlugin("doExpressions")) { - const node = this.startNode(); + const startPos = this.state.start; + const startLoc = this.state.startLoc; + let expr = this.parseExprSubscripts(refShorthandDefaultPos); + if (refShorthandDefaultPos && refShorthandDefaultPos.start) return expr; + while (this.state.type.postfix && !this.canInsertSemicolon()) { + const node = this.startNodeAt(startPos, startLoc); + node.operator = this.state.value; + node.prefix = false; + node.argument = expr; + this.checkLVal(expr, undefined, undefined, "postfix operation"); + this.next(); + expr = this.finishNode(node, "UpdateExpression"); + } + return expr; + } + + // Parse call, dot, and `[]`-subscript expressions. + + parseExprSubscripts(refShorthandDefaultPos) { + const startPos = this.state.start; + const startLoc = this.state.startLoc; + const potentialArrowAt = this.state.potentialArrowAt; + const expr = this.parseExprAtom(refShorthandDefaultPos); + + if (expr.type === "ArrowFunctionExpression" && expr.start === potentialArrowAt) { + return expr; + } + + if (refShorthandDefaultPos && refShorthandDefaultPos.start) { + return expr; + } + + return this.parseSubscripts(expr, startPos, startLoc); + } + + parseSubscripts(base, startPos, startLoc, noCalls) { + for (;;) { + if (!noCalls && this.eat(tt.doubleColon)) { + const node = this.startNodeAt(startPos, startLoc); + node.object = base; + node.callee = this.parseNoCallExpr(); + return this.parseSubscripts(this.finishNode(node, "BindExpression"), startPos, startLoc, noCalls); + } else if (this.eat(tt.dot)) { + const node = this.startNodeAt(startPos, startLoc); + node.object = base; + node.property = this.parseIdentifier(true); + node.computed = false; + base = this.finishNode(node, "MemberExpression"); + } else if (this.eat(tt.bracketL)) { + const node = this.startNodeAt(startPos, startLoc); + node.object = base; + node.property = this.parseExpression(); + node.computed = true; + this.expect(tt.bracketR); + base = this.finishNode(node, "MemberExpression"); + } else if (!noCalls && this.match(tt.parenL)) { + const possibleAsync = this.state.potentialArrowAt === base.start && base.type === "Identifier" && base.name === "async" && !this.canInsertSemicolon(); this.next(); - const oldInFunction = this.state.inFunction; - const oldLabels = this.state.labels; - this.state.labels = []; - this.state.inFunction = false; - node.body = this.parseBlock(false, true); - this.state.inFunction = oldInFunction; - this.state.labels = oldLabels; - return this.finishNode(node, "DoExpression"); - } - case tt.regexp: - const value = this.state.value; - node = this.parseLiteral(value.value, "RegExpLiteral"); - node.pattern = value.pattern; - node.flags = value.flags; - return node; + const node = this.startNodeAt(startPos, startLoc); + node.callee = base; + node.arguments = this.parseCallExpressionArguments(tt.parenR, possibleAsync); + if (node.callee.type === "Import" && node.arguments.length !== 1) { + this.raise(node.start, "import() requires exactly one argument"); + } + base = this.finishNode(node, "CallExpression"); - case tt.num: - return this.parseLiteral(this.state.value, "NumericLiteral"); - - case tt.string: - return this.parseLiteral(this.state.value, "StringLiteral"); - - case tt._null: - node = this.startNode(); - this.next(); - return this.finishNode(node, "NullLiteral"); - - case tt._true: case tt._false: - node = this.startNode(); - node.value = this.match(tt._true); - this.next(); - return this.finishNode(node, "BooleanLiteral"); - - case tt.parenL: - return this.parseParenAndDistinguishExpression(null, null, canBeArrow); - - case tt.bracketL: - node = this.startNode(); - this.next(); - node.elements = this.parseExprList(tt.bracketR, true, refShorthandDefaultPos); - this.toReferencedList(node.elements); - return this.finishNode(node, "ArrayExpression"); - - case tt.braceL: - return this.parseObj(false, refShorthandDefaultPos); - - case tt._function: - return this.parseFunctionExpression(); - - case tt.at: - this.parseDecorators(); - - case tt._class: - node = this.startNode(); - this.takeDecorators(node); - return this.parseClass(node, false); - - case tt._new: - return this.parseNew(); - - case tt.backQuote: - return this.parseTemplate(false); - - case tt.doubleColon: - node = this.startNode(); - this.next(); - node.object = null; - const callee = node.callee = this.parseNoCallExpr(); - if (callee.type === "MemberExpression") { - return this.finishNode(node, "BindExpression"); + if (possibleAsync && this.shouldParseAsyncArrow()) { + return this.parseAsyncArrowFromCallExpression(this.startNodeAt(startPos, startLoc), node); + } else { + this.toReferencedList(node.arguments); + } + } else if (this.match(tt.backQuote)) { + const node = this.startNodeAt(startPos, startLoc); + node.tag = base; + node.quasi = this.parseTemplate(true); + base = this.finishNode(node, "TaggedTemplateExpression"); } else { - this.raise(callee.start, "Binding should be performed on object property."); + return base; + } + } + } + + parseCallExpressionArguments(close, possibleAsyncArrow) { + const elts = []; + let innerParenStart; + let first = true; + + while (!this.eat(close)) { + if (first) { + first = false; + } else { + this.expect(tt.comma); + if (this.eat(close)) break; } - default: + // we need to make sure that if this is an async arrow functions, that we don't allow inner parens inside the params + if (this.match(tt.parenL) && !innerParenStart) { + innerParenStart = this.state.start; + } + + elts.push(this.parseExprListItem(false, possibleAsyncArrow ? { start: 0 } : undefined, possibleAsyncArrow ? { start: 0 } : undefined)); + } + + // we found an async arrow function so let's not allow any inner parens + if (possibleAsyncArrow && innerParenStart && this.shouldParseAsyncArrow()) { this.unexpected(); - } -}; + } -pp.parseFunctionExpression = function () { - const node = this.startNode(); - const meta = this.parseIdentifier(true); - if (this.state.inGenerator && this.eat(tt.dot) && this.hasPlugin("functionSent")) { - return this.parseMetaProperty(node, meta, "sent"); - } else { - return this.parseFunction(node, false); - } -}; - -pp.parseMetaProperty = function (node, meta, propertyName) { - node.meta = meta; - node.property = this.parseIdentifier(true); - - if (node.property.name !== propertyName) { - this.raise(node.property.start, `The only valid meta property for new is ${meta.name}.${propertyName}`); + return elts; } - return this.finishNode(node, "MetaProperty"); -}; + shouldParseAsyncArrow() { + return this.match(tt.arrow); + } -pp.parseLiteral = function (value, type, startPos, startLoc) { - startPos = startPos || this.state.start; - startLoc = startLoc || this.state.startLoc; + parseAsyncArrowFromCallExpression(node, call) { + this.expect(tt.arrow); + return this.parseArrowExpression(node, call.arguments, true); + } - const node = this.startNodeAt(startPos, startLoc); - this.addExtra(node, "rawValue", value); - this.addExtra(node, "raw", this.input.slice(startPos, this.state.end)); - node.value = value; - this.next(); - return this.finishNode(node, type); -}; + // Parse a no-call expression (like argument of `new` or `::` operators). -pp.parseParenExpression = function () { - this.expect(tt.parenL); - const val = this.parseExpression(); - this.expect(tt.parenR); - return val; -}; + parseNoCallExpr() { + const startPos = this.state.start; + const startLoc = this.state.startLoc; + return this.parseSubscripts(this.parseExprAtom(), startPos, startLoc, true); + } -pp.parseParenAndDistinguishExpression = function (startPos, startLoc, canBeArrow) { - startPos = startPos || this.state.start; - startLoc = startLoc || this.state.startLoc; + // 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 `{}`. - let val; - this.expect(tt.parenL); + parseExprAtom(refShorthandDefaultPos) { + const canBeArrow = this.state.potentialArrowAt === this.state.start; + let node; - const innerStartPos = this.state.start; - const innerStartLoc = this.state.startLoc; - const exprList = []; - const refShorthandDefaultPos = { start: 0 }; - const refNeedsArrowPos = { start: 0 }; - let first = true; - let spreadStart; - let optionalCommaStart; + switch (this.state.type) { + case tt._super: + if (!this.state.inMethod && !this.options.allowSuperOutsideMethod) { + this.raise(this.state.start, "'super' outside of function or class"); + } - while (!this.match(tt.parenR)) { - if (first) { - first = false; + node = this.startNode(); + this.next(); + if (!this.match(tt.parenL) && !this.match(tt.bracketL) && !this.match(tt.dot)) { + this.unexpected(); + } + if (this.match(tt.parenL) && this.state.inMethod !== "constructor" && !this.options.allowSuperOutsideMethod) { + this.raise(node.start, "super() is only valid inside a class constructor. Make sure the method name is spelled exactly as 'constructor'."); + } + return this.finishNode(node, "Super"); + + case tt._import: + if (!this.hasPlugin("dynamicImport")) this.unexpected(); + + node = this.startNode(); + this.next(); + if (!this.match(tt.parenL)) { + this.unexpected(null, tt.parenL); + } + return this.finishNode(node, "Import"); + + case tt._this: + node = this.startNode(); + this.next(); + return this.finishNode(node, "ThisExpression"); + + case tt._yield: + if (this.state.inGenerator) this.unexpected(); + + case tt.name: + node = this.startNode(); + const allowAwait = this.state.value === "await" && this.state.inAsync; + const allowYield = this.shouldAllowYieldIdentifier(); + const id = this.parseIdentifier(allowAwait || allowYield); + + if (id.name === "await") { + if (this.state.inAsync || this.inModule) { + return this.parseAwait(node); + } + } else if (id.name === "async" && this.match(tt._function) && !this.canInsertSemicolon()) { + this.next(); + return this.parseFunction(node, false, false, true); + } else if (canBeArrow && id.name === "async" && this.match(tt.name)) { + const params = [this.parseIdentifier()]; + this.expect(tt.arrow); + // let foo = bar => {}; + return this.parseArrowExpression(node, params, true); + } + + if (canBeArrow && !this.canInsertSemicolon() && this.eat(tt.arrow)) { + return this.parseArrowExpression(node, [id]); + } + + return id; + + case tt._do: + if (this.hasPlugin("doExpressions")) { + const node = this.startNode(); + this.next(); + const oldInFunction = this.state.inFunction; + const oldLabels = this.state.labels; + this.state.labels = []; + this.state.inFunction = false; + node.body = this.parseBlock(false, true); + this.state.inFunction = oldInFunction; + this.state.labels = oldLabels; + return this.finishNode(node, "DoExpression"); + } + + case tt.regexp: + const value = this.state.value; + node = this.parseLiteral(value.value, "RegExpLiteral"); + node.pattern = value.pattern; + node.flags = value.flags; + return node; + + case tt.num: + return this.parseLiteral(this.state.value, "NumericLiteral"); + + case tt.string: + return this.parseLiteral(this.state.value, "StringLiteral"); + + case tt._null: + node = this.startNode(); + this.next(); + return this.finishNode(node, "NullLiteral"); + + case tt._true: case tt._false: + node = this.startNode(); + node.value = this.match(tt._true); + this.next(); + return this.finishNode(node, "BooleanLiteral"); + + case tt.parenL: + return this.parseParenAndDistinguishExpression(null, null, canBeArrow); + + case tt.bracketL: + node = this.startNode(); + this.next(); + node.elements = this.parseExprList(tt.bracketR, true, refShorthandDefaultPos); + this.toReferencedList(node.elements); + return this.finishNode(node, "ArrayExpression"); + + case tt.braceL: + return this.parseObj(false, refShorthandDefaultPos); + + case tt._function: + return this.parseFunctionExpression(); + + case tt.at: + this.parseDecorators(); + + case tt._class: + node = this.startNode(); + this.takeDecorators(node); + return this.parseClass(node, false); + + case tt._new: + return this.parseNew(); + + case tt.backQuote: + return this.parseTemplate(false); + + case tt.doubleColon: + node = this.startNode(); + this.next(); + node.object = null; + const callee = node.callee = this.parseNoCallExpr(); + if (callee.type === "MemberExpression") { + return this.finishNode(node, "BindExpression"); + } else { + this.raise(callee.start, "Binding should be performed on object property."); + } + + default: + this.unexpected(); + } + } + + parseFunctionExpression() { + const node = this.startNode(); + const meta = this.parseIdentifier(true); + if (this.state.inGenerator && this.eat(tt.dot) && this.hasPlugin("functionSent")) { + return this.parseMetaProperty(node, meta, "sent"); } else { - this.expect(tt.comma, refNeedsArrowPos.start || null); - if (this.match(tt.parenR)) { - optionalCommaStart = this.state.start; + return this.parseFunction(node, false); + } + } + + parseMetaProperty(node, meta, propertyName) { + node.meta = meta; + node.property = this.parseIdentifier(true); + + if (node.property.name !== propertyName) { + this.raise(node.property.start, `The only valid meta property for new is ${meta.name}.${propertyName}`); + } + + return this.finishNode(node, "MetaProperty"); + } + + parseLiteral(value, type, startPos, startLoc) { + startPos = startPos || this.state.start; + startLoc = startLoc || this.state.startLoc; + + const node = this.startNodeAt(startPos, startLoc); + this.addExtra(node, "rawValue", value); + this.addExtra(node, "raw", this.input.slice(startPos, this.state.end)); + node.value = value; + this.next(); + return this.finishNode(node, type); + } + + parseParenExpression() { + this.expect(tt.parenL); + const val = this.parseExpression(); + this.expect(tt.parenR); + return val; + } + + parseParenAndDistinguishExpression(startPos, startLoc, canBeArrow) { + startPos = startPos || this.state.start; + startLoc = startLoc || this.state.startLoc; + + let val; + this.expect(tt.parenL); + + const innerStartPos = this.state.start; + const innerStartLoc = this.state.startLoc; + const exprList = []; + const refShorthandDefaultPos = { start: 0 }; + const refNeedsArrowPos = { start: 0 }; + let first = true; + let spreadStart; + let optionalCommaStart; + + while (!this.match(tt.parenR)) { + if (first) { + first = false; + } else { + this.expect(tt.comma, refNeedsArrowPos.start || null); + if (this.match(tt.parenR)) { + optionalCommaStart = this.state.start; + break; + } + } + + if (this.match(tt.ellipsis)) { + const spreadNodeStartPos = this.state.start; + const spreadNodeStartLoc = this.state.startLoc; + spreadStart = this.state.start; + exprList.push(this.parseParenItem(this.parseRest(), spreadNodeStartPos, spreadNodeStartLoc)); break; + } else { + exprList.push(this.parseMaybeAssign(false, refShorthandDefaultPos, this.parseParenItem, refNeedsArrowPos)); } } - if (this.match(tt.ellipsis)) { - const spreadNodeStartPos = this.state.start; - const spreadNodeStartLoc = this.state.startLoc; - spreadStart = this.state.start; - exprList.push(this.parseParenItem(this.parseRest(), spreadNodeStartPos, spreadNodeStartLoc)); - break; + const innerEndPos = this.state.start; + const innerEndLoc = this.state.startLoc; + this.expect(tt.parenR); + + let arrowNode = this.startNodeAt(startPos, startLoc); + if (canBeArrow && this.shouldParseArrow() && (arrowNode = this.parseArrow(arrowNode))) { + for (const param of exprList) { + if (param.extra && param.extra.parenthesized) this.unexpected(param.extra.parenStart); + } + + return this.parseArrowExpression(arrowNode, exprList); + } + + if (!exprList.length) { + this.unexpected(this.state.lastTokStart); + } + if (optionalCommaStart) this.unexpected(optionalCommaStart); + if (spreadStart) this.unexpected(spreadStart); + if (refShorthandDefaultPos.start) this.unexpected(refShorthandDefaultPos.start); + if (refNeedsArrowPos.start) this.unexpected(refNeedsArrowPos.start); + + if (exprList.length > 1) { + val = this.startNodeAt(innerStartPos, innerStartLoc); + val.expressions = exprList; + this.toReferencedList(val.expressions); + this.finishNodeAt(val, "SequenceExpression", innerEndPos, innerEndLoc); } else { - exprList.push(this.parseMaybeAssign(false, refShorthandDefaultPos, this.parseParenItem, refNeedsArrowPos)); + val = exprList[0]; + } + + + this.addExtra(val, "parenthesized", true); + this.addExtra(val, "parenStart", startPos); + + return val; + } + + shouldParseArrow() { + return !this.canInsertSemicolon(); + } + + parseArrow(node) { + if (this.eat(tt.arrow)) { + return node; } } - const innerEndPos = this.state.start; - const innerEndLoc = this.state.startLoc; - this.expect(tt.parenR); - - let arrowNode = this.startNodeAt(startPos, startLoc); - if (canBeArrow && this.shouldParseArrow() && (arrowNode = this.parseArrow(arrowNode))) { - for (const param of exprList) { - if (param.extra && param.extra.parenthesized) this.unexpected(param.extra.parenStart); - } - - return this.parseArrowExpression(arrowNode, exprList); - } - - if (!exprList.length) { - this.unexpected(this.state.lastTokStart); - } - if (optionalCommaStart) this.unexpected(optionalCommaStart); - if (spreadStart) this.unexpected(spreadStart); - if (refShorthandDefaultPos.start) this.unexpected(refShorthandDefaultPos.start); - if (refNeedsArrowPos.start) this.unexpected(refNeedsArrowPos.start); - - if (exprList.length > 1) { - val = this.startNodeAt(innerStartPos, innerStartLoc); - val.expressions = exprList; - this.toReferencedList(val.expressions); - this.finishNodeAt(val, "SequenceExpression", innerEndPos, innerEndLoc); - } else { - val = exprList[0]; - } - - - this.addExtra(val, "parenthesized", true); - this.addExtra(val, "parenStart", startPos); - - return val; -}; - -pp.shouldParseArrow = function () { - return !this.canInsertSemicolon(); -}; - -pp.parseArrow = function (node) { - if (this.eat(tt.arrow)) { + parseParenItem(node) { return node; } -}; -pp.parseParenItem = function (node) { - return node; -}; + // 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 -// 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 + parseNew() { + const node = this.startNode(); + const meta = this.parseIdentifier(true); -pp.parseNew = function () { - const node = this.startNode(); - const meta = this.parseIdentifier(true); + if (this.eat(tt.dot)) { + const metaProp = this.parseMetaProperty(node, meta, "target"); - if (this.eat(tt.dot)) { - const metaProp = this.parseMetaProperty(node, meta, "target"); + if (!this.state.inFunction) { + this.raise(metaProp.property.start, "new.target can only be used in functions"); + } - if (!this.state.inFunction) { - this.raise(metaProp.property.start, "new.target can only be used in functions"); + return metaProp; } - return metaProp; - } + node.callee = this.parseNoCallExpr(); - node.callee = this.parseNoCallExpr(); - - if (this.eat(tt.parenL)) { - node.arguments = this.parseExprList(tt.parenR); - this.toReferencedList(node.arguments); - } else { - node.arguments = []; - } - - return this.finishNode(node, "NewExpression"); -}; - -// Parse template expression. - -pp.parseTemplateElement = function (isTagged) { - const elem = this.startNode(); - if (this.state.value === null) { - if (!isTagged) { - this.raise(this.state.invalidTemplateEscapePosition, "Invalid escape sequence in template"); + if (this.eat(tt.parenL)) { + node.arguments = this.parseExprList(tt.parenR); + this.toReferencedList(node.arguments); } else { - this.state.invalidTemplateEscapePosition = null; + node.arguments = []; } + + return this.finishNode(node, "NewExpression"); } - elem.value = { - raw: this.input.slice(this.state.start, this.state.end).replace(/\r\n?/g, "\n"), - cooked: this.state.value - }; - this.next(); - elem.tail = this.match(tt.backQuote); - return this.finishNode(elem, "TemplateElement"); -}; -pp.parseTemplate = function (isTagged) { - const node = this.startNode(); - this.next(); - node.expressions = []; - let curElt = this.parseTemplateElement(isTagged); - node.quasis = [curElt]; - while (!curElt.tail) { - this.expect(tt.dollarBraceL); - node.expressions.push(this.parseExpression()); - this.expect(tt.braceR); - node.quasis.push(curElt = this.parseTemplateElement(isTagged)); + // Parse template expression. + + parseTemplateElement(isTagged) { + const elem = this.startNode(); + if (this.state.value === null) { + if (!isTagged) { + this.raise(this.state.invalidTemplateEscapePosition, "Invalid escape sequence in template"); + } else { + this.state.invalidTemplateEscapePosition = null; + } + } + elem.value = { + raw: this.input.slice(this.state.start, this.state.end).replace(/\r\n?/g, "\n"), + cooked: this.state.value + }; + this.next(); + elem.tail = this.match(tt.backQuote); + return this.finishNode(elem, "TemplateElement"); } - this.next(); - return this.finishNode(node, "TemplateLiteral"); -}; -// Parse an object literal or binding pattern. - -pp.parseObj = function (isPattern, refShorthandDefaultPos) { - let decorators = []; - const propHash = Object.create(null); - let first = true; - const node = this.startNode(); - - node.properties = []; - this.next(); - - let firstRestLocation = null; - - while (!this.eat(tt.braceR)) { - if (first) { - first = false; - } else { - this.expect(tt.comma); - if (this.eat(tt.braceR)) break; + parseTemplate(isTagged) { + const node = this.startNode(); + this.next(); + node.expressions = []; + let curElt = this.parseTemplateElement(isTagged); + node.quasis = [curElt]; + while (!curElt.tail) { + this.expect(tt.dollarBraceL); + node.expressions.push(this.parseExpression()); + this.expect(tt.braceR); + node.quasis.push(curElt = this.parseTemplateElement(isTagged)); } + this.next(); + return this.finishNode(node, "TemplateLiteral"); + } - while (this.match(tt.at)) { - decorators.push(this.parseDecorator()); - } + // Parse an object literal or binding pattern. - let prop = this.startNode(), isGenerator = false, isAsync = false, startPos, startLoc; - if (decorators.length) { - prop.decorators = decorators; - decorators = []; - } + parseObj(isPattern, refShorthandDefaultPos) { + let decorators = []; + const propHash = Object.create(null); + let first = true; + const node = this.startNode(); - if (this.hasPlugin("objectRestSpread") && this.match(tt.ellipsis)) { - prop = this.parseSpread(isPattern ? { start: 0 } : undefined); - prop.type = isPattern ? "RestElement" : "SpreadElement"; - if (isPattern) this.toAssignable(prop.argument, true, "object pattern"); - node.properties.push(prop); - if (isPattern) { - const position = this.state.start; - if (firstRestLocation !== null) { - this.unexpected(firstRestLocation, "Cannot have multiple rest elements when destructuring"); - } else if (this.eat(tt.braceR)) { - break; - } else if (this.match(tt.comma) && this.lookahead().type === tt.braceR) { - this.unexpected(position, "A trailing comma is not permitted after the rest element"); + node.properties = []; + this.next(); + + let firstRestLocation = null; + + while (!this.eat(tt.braceR)) { + if (first) { + first = false; + } else { + this.expect(tt.comma); + if (this.eat(tt.braceR)) break; + } + + while (this.match(tt.at)) { + decorators.push(this.parseDecorator()); + } + + let prop = this.startNode(), isGenerator = false, isAsync = false, startPos, startLoc; + if (decorators.length) { + prop.decorators = decorators; + decorators = []; + } + + if (this.hasPlugin("objectRestSpread") && this.match(tt.ellipsis)) { + prop = this.parseSpread(isPattern ? { start: 0 } : undefined); + prop.type = isPattern ? "RestElement" : "SpreadElement"; + if (isPattern) this.toAssignable(prop.argument, true, "object pattern"); + node.properties.push(prop); + if (isPattern) { + const position = this.state.start; + if (firstRestLocation !== null) { + this.unexpected(firstRestLocation, "Cannot have multiple rest elements when destructuring"); + } else if (this.eat(tt.braceR)) { + break; + } else if (this.match(tt.comma) && this.lookahead().type === tt.braceR) { + this.unexpected(position, "A trailing comma is not permitted after the rest element"); + } else { + firstRestLocation = position; + continue; + } } else { - firstRestLocation = position; continue; } - } else { - continue; } - } - prop.method = false; - prop.shorthand = false; + prop.method = false; + prop.shorthand = false; - if (isPattern || refShorthandDefaultPos) { - startPos = this.state.start; - startLoc = this.state.startLoc; - } + if (isPattern || refShorthandDefaultPos) { + startPos = this.state.start; + startLoc = this.state.startLoc; + } - if (!isPattern) { - isGenerator = this.eat(tt.star); - } + if (!isPattern) { + isGenerator = this.eat(tt.star); + } - if (!isPattern && this.isContextual("async")) { - if (isGenerator) this.unexpected(); + if (!isPattern && this.isContextual("async")) { + if (isGenerator) this.unexpected(); - const asyncId = this.parseIdentifier(); - if (this.match(tt.colon) || this.match(tt.parenL) || this.match(tt.braceR) || this.match(tt.eq) || this.match(tt.comma)) { - prop.key = asyncId; - prop.computed = false; + const asyncId = this.parseIdentifier(); + if (this.match(tt.colon) || this.match(tt.parenL) || this.match(tt.braceR) || this.match(tt.eq) || this.match(tt.comma)) { + prop.key = asyncId; + prop.computed = false; + } else { + isAsync = true; + if (this.hasPlugin("asyncGenerators")) isGenerator = this.eat(tt.star); + this.parsePropertyName(prop); + } } else { - isAsync = true; - if (this.hasPlugin("asyncGenerators")) isGenerator = this.eat(tt.star); this.parsePropertyName(prop); } - } else { + + this.parseObjPropValue(prop, startPos, startLoc, isGenerator, isAsync, isPattern, refShorthandDefaultPos); + this.checkPropClash(prop, propHash); + + if (prop.shorthand) { + this.addExtra(prop, "shorthand", true); + } + + node.properties.push(prop); + } + + if (firstRestLocation !== null) { + this.unexpected(firstRestLocation, "The rest element has to be the last element when destructuring"); + } + + if (decorators.length) { + this.raise(this.state.start, "You have trailing decorators with no property"); + } + + return this.finishNode(node, isPattern ? "ObjectPattern" : "ObjectExpression"); + } + + isGetterOrSetterMethod(prop, isPattern) { + return !isPattern && + !prop.computed && + prop.key.type === "Identifier" && + (prop.key.name === "get" || prop.key.name === "set") && + ( + this.match(tt.string) || // get "string"() {} + this.match(tt.num) || // get 1() {} + this.match(tt.bracketL) || // get ["string"]() {} + this.match(tt.name) || // get foo() {} + this.state.type.keyword // get debugger() {} + ); + } + + // get methods aren't allowed to have any parameters + // set methods must have exactly 1 parameter + checkGetterSetterParamCount(method) { + const paramCount = method.kind === "get" ? 0 : 1; + if (method.params.length !== paramCount) { + const start = method.start; + if (method.kind === "get") { + this.raise(start, "getter should have no params"); + } else { + this.raise(start, "setter should have exactly one param"); + } + } + } + + parseObjectMethod(prop, isGenerator, isAsync, isPattern) { + if (isAsync || isGenerator || this.match(tt.parenL)) { + if (isPattern) this.unexpected(); + prop.kind = "method"; + prop.method = true; + this.parseMethod(prop, isGenerator, isAsync); + + return this.finishNode(prop, "ObjectMethod"); + } + + if (this.isGetterOrSetterMethod(prop, isPattern)) { + if (isGenerator || isAsync) this.unexpected(); + prop.kind = prop.key.name; this.parsePropertyName(prop); - } + this.parseMethod(prop); + this.checkGetterSetterParamCount(prop); - this.parseObjPropValue(prop, startPos, startLoc, isGenerator, isAsync, isPattern, refShorthandDefaultPos); - this.checkPropClash(prop, propHash); - - if (prop.shorthand) { - this.addExtra(prop, "shorthand", true); - } - - node.properties.push(prop); - } - - if (firstRestLocation !== null) { - this.unexpected(firstRestLocation, "The rest element has to be the last element when destructuring"); - } - - if (decorators.length) { - this.raise(this.state.start, "You have trailing decorators with no property"); - } - - return this.finishNode(node, isPattern ? "ObjectPattern" : "ObjectExpression"); -}; - -pp.isGetterOrSetterMethod = function (prop, isPattern) { - return !isPattern && - !prop.computed && - prop.key.type === "Identifier" && - (prop.key.name === "get" || prop.key.name === "set") && - ( - this.match(tt.string) || // get "string"() {} - this.match(tt.num) || // get 1() {} - this.match(tt.bracketL) || // get ["string"]() {} - this.match(tt.name) || // get foo() {} - this.state.type.keyword // get debugger() {} - ); -}; - -// get methods aren't allowed to have any parameters -// set methods must have exactly 1 parameter -pp.checkGetterSetterParamCount = function (method) { - const paramCount = method.kind === "get" ? 0 : 1; - if (method.params.length !== paramCount) { - const start = method.start; - if (method.kind === "get") { - this.raise(start, "getter should have no params"); - } else { - this.raise(start, "setter should have exactly one param"); + return this.finishNode(prop, "ObjectMethod"); } } -}; -pp.parseObjectMethod = function (prop, isGenerator, isAsync, isPattern) { - if (isAsync || isGenerator || this.match(tt.parenL)) { - if (isPattern) this.unexpected(); - prop.kind = "method"; - prop.method = true; - this.parseMethod(prop, isGenerator, isAsync); + parseObjectProperty(prop, startPos, startLoc, isPattern, refShorthandDefaultPos) { + if (this.eat(tt.colon)) { + prop.value = isPattern ? this.parseMaybeDefault(this.state.start, this.state.startLoc) : this.parseMaybeAssign(false, refShorthandDefaultPos); - return this.finishNode(prop, "ObjectMethod"); - } + return this.finishNode(prop, "ObjectProperty"); + } - if (this.isGetterOrSetterMethod(prop, isPattern)) { - if (isGenerator || isAsync) this.unexpected(); - prop.kind = prop.key.name; - this.parsePropertyName(prop); - this.parseMethod(prop); - this.checkGetterSetterParamCount(prop); + if (!prop.computed && prop.key.type === "Identifier") { + this.checkReservedWord(prop.key.name, prop.key.start, true, true); - return this.finishNode(prop, "ObjectMethod"); - } -}; - -pp.parseObjectProperty = function (prop, startPos, startLoc, isPattern, refShorthandDefaultPos) { - if (this.eat(tt.colon)) { - prop.value = isPattern ? this.parseMaybeDefault(this.state.start, this.state.startLoc) : this.parseMaybeAssign(false, refShorthandDefaultPos); - - return this.finishNode(prop, "ObjectProperty"); - } - - if (!prop.computed && prop.key.type === "Identifier") { - this.checkReservedWord(prop.key.name, prop.key.start, true, true); - - if (isPattern) { - prop.value = this.parseMaybeDefault(startPos, startLoc, prop.key.__clone()); - } else if (this.match(tt.eq) && refShorthandDefaultPos) { - if (!refShorthandDefaultPos.start) { - refShorthandDefaultPos.start = this.state.start; + if (isPattern) { + prop.value = this.parseMaybeDefault(startPos, startLoc, prop.key.__clone()); + } else if (this.match(tt.eq) && refShorthandDefaultPos) { + if (!refShorthandDefaultPos.start) { + refShorthandDefaultPos.start = this.state.start; + } + prop.value = this.parseMaybeDefault(startPos, startLoc, prop.key.__clone()); + } else { + prop.value = prop.key.__clone(); } - prop.value = this.parseMaybeDefault(startPos, startLoc, prop.key.__clone()); + prop.shorthand = true; + + return this.finishNode(prop, "ObjectProperty"); + } + } + + parseObjPropValue(prop, startPos, startLoc, isGenerator, isAsync, isPattern, refShorthandDefaultPos) { + const node = + this.parseObjectMethod(prop, isGenerator, isAsync, isPattern) || + this.parseObjectProperty(prop, startPos, startLoc, isPattern, refShorthandDefaultPos); + + if (!node) this.unexpected(); + + return node; + } + + parsePropertyName(prop) { + if (this.eat(tt.bracketL)) { + prop.computed = true; + prop.key = this.parseMaybeAssign(); + this.expect(tt.bracketR); } else { - prop.value = prop.key.__clone(); - } - prop.shorthand = true; - - return this.finishNode(prop, "ObjectProperty"); - } -}; - -pp.parseObjPropValue = function (prop, startPos, startLoc, isGenerator, isAsync, isPattern, refShorthandDefaultPos) { - const node = - this.parseObjectMethod(prop, isGenerator, isAsync, isPattern) || - this.parseObjectProperty(prop, startPos, startLoc, isPattern, refShorthandDefaultPos); - - if (!node) this.unexpected(); - - return node; -}; - -pp.parsePropertyName = function (prop) { - if (this.eat(tt.bracketL)) { - prop.computed = true; - prop.key = this.parseMaybeAssign(); - this.expect(tt.bracketR); - } else { - prop.computed = false; - const oldInPropertyName = this.state.inPropertyName; - this.state.inPropertyName = true; - prop.key = (this.match(tt.num) || this.match(tt.string)) ? this.parseExprAtom() : this.parseIdentifier(true); - this.state.inPropertyName = oldInPropertyName; - } - return prop.key; -}; - -// Initialize empty function node. - -pp.initFunction = function (node, isAsync) { - node.id = null; - node.generator = false; - node.expression = false; - node.async = !!isAsync; -}; - -// Parse object or class method. - -pp.parseMethod = function (node, isGenerator, isAsync) { - const oldInMethod = this.state.inMethod; - this.state.inMethod = node.kind || true; - this.initFunction(node, isAsync); - this.expect(tt.parenL); - node.params = this.parseBindingList(tt.parenR); - node.generator = !!isGenerator; - this.parseFunctionBody(node); - this.state.inMethod = oldInMethod; - return node; -}; - -// Parse arrow function expression with given parameters. - -pp.parseArrowExpression = function (node, params, isAsync) { - this.initFunction(node, isAsync); - node.params = this.toAssignableList(params, true, "arrow function parameters"); - this.parseFunctionBody(node, true); - return this.finishNode(node, "ArrowFunctionExpression"); -}; - -pp.isStrictBody = function (node, isExpression) { - if (!isExpression && node.body.directives.length) { - for (const directive of (node.body.directives: Array)) { - if (directive.value.value === "use strict") { - return true; - } + prop.computed = false; + const oldInPropertyName = this.state.inPropertyName; + this.state.inPropertyName = true; + prop.key = (this.match(tt.num) || this.match(tt.string)) ? this.parseExprAtom() : this.parseIdentifier(true); + this.state.inPropertyName = oldInPropertyName; } + return prop.key; } - return false; -}; + // Initialize empty function node. -// Parse function body and check parameters. -pp.parseFunctionBody = function (node, allowExpression) { - const isExpression = allowExpression && !this.match(tt.braceL); - - const oldInAsync = this.state.inAsync; - this.state.inAsync = node.async; - if (isExpression) { - node.body = this.parseMaybeAssign(); - node.expression = true; - } else { - // Start a new scope with regard to labels and the `inFunction` - // flag (restore them to their old value afterwards). - const oldInFunc = this.state.inFunction; - const oldInGen = this.state.inGenerator; - const oldLabels = this.state.labels; - this.state.inFunction = true; this.state.inGenerator = node.generator; this.state.labels = []; - node.body = this.parseBlock(true); + initFunction(node, isAsync) { + node.id = null; + node.generator = false; node.expression = false; - this.state.inFunction = oldInFunc; this.state.inGenerator = oldInGen; this.state.labels = oldLabels; - } - this.state.inAsync = oldInAsync; - - // If this is a strict mode function, verify that argument names - // are not repeated, and it does not try to bind the words `eval` - // or `arguments`. - const isStrict = this.isStrictBody(node, isExpression); - // Also check when allowExpression === true for arrow functions - const checkLVal = this.state.strict || allowExpression || isStrict; - - if (isStrict && node.id && node.id.type === "Identifier" && node.id.name === "yield") { - this.raise(node.id.start, "Binding yield in strict mode"); + node.async = !!isAsync; } - if (checkLVal) { - const nameHash = Object.create(null); - const oldStrict = this.state.strict; - if (isStrict) this.state.strict = true; - if (node.id) { - this.checkLVal(node.id, true, undefined, "function name"); - } - for (const param of (node.params: Array)) { - if (isStrict && param.type !== "Identifier") { - this.raise(param.start, "Non-simple parameter in strict mode"); + // Parse object or class method. + + parseMethod(node, isGenerator, isAsync) { + const oldInMethod = this.state.inMethod; + this.state.inMethod = node.kind || true; + this.initFunction(node, isAsync); + this.expect(tt.parenL); + node.params = this.parseBindingList(tt.parenR); + node.generator = !!isGenerator; + this.parseFunctionBody(node); + this.state.inMethod = oldInMethod; + return node; + } + + // Parse arrow function expression with given parameters. + + parseArrowExpression(node, params, isAsync) { + this.initFunction(node, isAsync); + node.params = this.toAssignableList(params, true, "arrow function parameters"); + this.parseFunctionBody(node, true); + return this.finishNode(node, "ArrowFunctionExpression"); + } + + isStrictBody(node, isExpression) { + if (!isExpression && node.body.directives.length) { + for (const directive of (node.body.directives: Array)) { + if (directive.value.value === "use strict") { + return true; + } } - this.checkLVal(param, true, nameHash, "function parameter list"); } - this.state.strict = oldStrict; + + return false; } -}; -// 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). + // Parse function body and check parameters. + parseFunctionBody(node, allowExpression) { + const isExpression = allowExpression && !this.match(tt.braceL); -pp.parseExprList = function (close, allowEmpty, refShorthandDefaultPos) { - const elts = []; - let first = true; - - while (!this.eat(close)) { - if (first) { - first = false; + const oldInAsync = this.state.inAsync; + this.state.inAsync = node.async; + if (isExpression) { + node.body = this.parseMaybeAssign(); + node.expression = true; } else { - this.expect(tt.comma); - if (this.eat(close)) break; + // Start a new scope with regard to labels and the `inFunction` + // flag (restore them to their old value afterwards). + const oldInFunc = this.state.inFunction; + const oldInGen = this.state.inGenerator; + const oldLabels = this.state.labels; + this.state.inFunction = true; this.state.inGenerator = node.generator; this.state.labels = []; + node.body = this.parseBlock(true); + node.expression = false; + this.state.inFunction = oldInFunc; this.state.inGenerator = oldInGen; this.state.labels = oldLabels; + } + this.state.inAsync = oldInAsync; + + // If this is a strict mode function, verify that argument names + // are not repeated, and it does not try to bind the words `eval` + // or `arguments`. + const isStrict = this.isStrictBody(node, isExpression); + // Also check when allowExpression === true for arrow functions + const checkLVal = this.state.strict || allowExpression || isStrict; + + if (isStrict && node.id && node.id.type === "Identifier" && node.id.name === "yield") { + this.raise(node.id.start, "Binding yield in strict mode"); } - elts.push(this.parseExprListItem(allowEmpty, refShorthandDefaultPos)); - } - return elts; -}; - -pp.parseExprListItem = function (allowEmpty, refShorthandDefaultPos, refNeedsArrowPos) { - let elt; - if (allowEmpty && this.match(tt.comma)) { - elt = null; - } else if (this.match(tt.ellipsis)) { - elt = this.parseSpread(refShorthandDefaultPos); - } else { - elt = this.parseMaybeAssign(false, refShorthandDefaultPos, this.parseParenItem, refNeedsArrowPos); - } - return elt; -}; - -// Parse the next token as an identifier. If `liberal` is true (used -// when parsing properties), it will also convert keywords into -// identifiers. - -pp.parseIdentifier = function (liberal) { - const node = this.startNode(); - if (!liberal) { - this.checkReservedWord(this.state.value, this.state.start, !!this.state.type.keyword, false); + if (checkLVal) { + const nameHash = Object.create(null); + const oldStrict = this.state.strict; + if (isStrict) this.state.strict = true; + if (node.id) { + this.checkLVal(node.id, true, undefined, "function name"); + } + for (const param of (node.params: Array)) { + if (isStrict && param.type !== "Identifier") { + this.raise(param.start, "Non-simple parameter in strict mode"); + } + this.checkLVal(param, true, nameHash, "function parameter list"); + } + this.state.strict = oldStrict; + } } - if (this.match(tt.name)) { - node.name = this.state.value; - } else if (this.state.type.keyword) { - node.name = this.state.type.keyword; - } else { - this.unexpected(); + // 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, refShorthandDefaultPos) { + const elts = []; + let first = true; + + while (!this.eat(close)) { + if (first) { + first = false; + } else { + this.expect(tt.comma); + if (this.eat(close)) break; + } + + elts.push(this.parseExprListItem(allowEmpty, refShorthandDefaultPos)); + } + return elts; } - if (!liberal && node.name === "await" && this.state.inAsync) { - this.raise(node.start, "invalid use of await inside of an async function"); + parseExprListItem(allowEmpty, refShorthandDefaultPos, refNeedsArrowPos) { + let elt; + if (allowEmpty && this.match(tt.comma)) { + elt = null; + } else if (this.match(tt.ellipsis)) { + elt = this.parseSpread(refShorthandDefaultPos); + } else { + elt = this.parseMaybeAssign(false, refShorthandDefaultPos, this.parseParenItem, refNeedsArrowPos); + } + return elt; } - node.loc.identifierName = node.name; + // Parse the next token as an identifier. If `liberal` is true (used + // when parsing properties), it will also convert keywords into + // identifiers. - this.next(); - return this.finishNode(node, "Identifier"); -}; + parseIdentifier(liberal) { + const node = this.startNode(); + if (!liberal) { + this.checkReservedWord(this.state.value, this.state.start, !!this.state.type.keyword, false); + } -pp.checkReservedWord = function (word, startLoc, checkKeywords, isBinding) { - if (this.isReservedWord(word) || (checkKeywords && this.isKeyword(word))) { - this.raise(startLoc, word + " is a reserved word"); + if (this.match(tt.name)) { + node.name = this.state.value; + } else if (this.state.type.keyword) { + node.name = this.state.type.keyword; + } else { + this.unexpected(); + } + + if (!liberal && node.name === "await" && this.state.inAsync) { + this.raise(node.start, "invalid use of await inside of an async function"); + } + + node.loc.identifierName = node.name; + + this.next(); + return this.finishNode(node, "Identifier"); } - if (this.state.strict && (reservedWords.strict(word) || (isBinding && reservedWords.strictBind(word)))) { - this.raise(startLoc, word + " is a reserved word in strict mode"); - } -}; + checkReservedWord(word, startLoc, checkKeywords, isBinding) { + if (this.isReservedWord(word) || (checkKeywords && this.isKeyword(word))) { + this.raise(startLoc, word + " is a reserved word"); + } -// Parses await expression inside async function. - -pp.parseAwait = function (node) { - // istanbul ignore next: this condition is checked at the call site so won't be hit here - if (!this.state.inAsync) { - this.unexpected(); + if (this.state.strict && (reservedWords.strict(word) || (isBinding && reservedWords.strictBind(word)))) { + this.raise(startLoc, word + " is a reserved word in strict mode"); + } } - if (this.match(tt.star)) { - this.raise(node.start, "await* has been removed from the async functions proposal. Use Promise.all() instead."); - } - node.argument = this.parseMaybeUnary(); - return this.finishNode(node, "AwaitExpression"); -}; -// Parses yield expression inside generator. + // Parses await expression inside async function. -pp.parseYield = function () { - const node = this.startNode(); - this.next(); - if ( - this.match(tt.semi) || - this.canInsertSemicolon() || - (!this.match(tt.star) && !this.state.type.startsExpr) - ) { - node.delegate = false; - node.argument = null; - } else { - node.delegate = this.eat(tt.star); - node.argument = this.parseMaybeAssign(); + parseAwait(node) { + // istanbul ignore next: this condition is checked at the call site so won't be hit here + if (!this.state.inAsync) { + this.unexpected(); + } + if (this.match(tt.star)) { + this.raise(node.start, "await* has been removed from the async functions proposal. Use Promise.all() instead."); + } + node.argument = this.parseMaybeUnary(); + return this.finishNode(node, "AwaitExpression"); } - return this.finishNode(node, "YieldExpression"); -}; + + // Parses yield expression inside generator. + + parseYield() { + const node = this.startNode(); + this.next(); + if ( + this.match(tt.semi) || + this.canInsertSemicolon() || + (!this.match(tt.star) && !this.state.type.startsExpr) + ) { + node.delegate = false; + node.argument = null; + } else { + node.delegate = this.eat(tt.star); + node.argument = this.parseMaybeAssign(); + } + return this.finishNode(node, "YieldExpression"); + } +} diff --git a/src/parser/index.js b/src/parser/index.js index 9600040c5f..41ce90fde2 100644 --- a/src/parser/index.js +++ b/src/parser/index.js @@ -1,11 +1,14 @@ -import { reservedWords } from "../util/identifier"; +// @flow + +import type { Options } from "../options"; +import type { File } from "../types"; import { getOptions } from "../options"; -import Tokenizer from "../tokenizer"; +import StatementParser from "./statement"; export const plugins = {}; -export default class Parser extends Tokenizer { - constructor(options: Object, input: string) { +export default class Parser extends StatementParser { + constructor(options: Options, input: string) { options = getOptions(options); super(options, input); @@ -21,25 +24,7 @@ export default class Parser extends Tokenizer { } } - isReservedWord(word: string): boolean { - if (word === "await") { - return this.inModule; - } else { - return reservedWords[6](word); - } - } - - hasPlugin(name: string): boolean { - return !!this.plugins[name]; - } - - parse(): { - type: "File", - program: { - type: "Program", - body: Array - } - } { + parse(): File { const file = this.startNode(); const program = this.startNode(); this.nextToken(); diff --git a/src/parser/location.js b/src/parser/location.js index 2a408a5100..1562f52fb6 100644 --- a/src/parser/location.js +++ b/src/parser/location.js @@ -1,7 +1,5 @@ import { getLineInfo } from "../util/location"; -import Parser from "./index"; - -const pp = Parser.prototype; +import CommentsParser from "./comments"; // This function is used to raise exceptions on parse errors. It // takes an offset integer (into the current `input`) to indicate @@ -9,11 +7,13 @@ const pp = Parser.prototype; // of the error message, and then raises a `SyntaxError` with that // message. -pp.raise = function (pos, message) { - const loc = getLineInfo(this.input, pos); - message += ` (${loc.line}:${loc.column})`; - const err = new SyntaxError(message); - err.pos = pos; - err.loc = loc; - throw err; -}; +export default class LocationParser extends CommentsParser { + raise(pos, message) { + const loc = getLineInfo(this.input, pos); + message += ` (${loc.line}:${loc.column})`; + const err = new SyntaxError(message); + err.pos = pos; + err.loc = loc; + throw err; + } +} diff --git a/src/parser/lval.js b/src/parser/lval.js index 9f0f7566c7..b3d33baf3d 100644 --- a/src/parser/lval.js +++ b/src/parser/lval.js @@ -1,259 +1,260 @@ import { types as tt } from "../tokenizer/types"; -import Parser from "./index"; +import { NodeUtils } from "./node"; -const pp = Parser.prototype; +export default class LValParser extends NodeUtils { + // Convert existing expression atom to assignable pattern + // if possible. -// Convert existing expression atom to assignable pattern -// if possible. + toAssignable(node, isBinding, contextDescription) { + if (node) { + switch (node.type) { + case "Identifier": + case "ObjectPattern": + case "ArrayPattern": + case "AssignmentPattern": + break; -pp.toAssignable = function (node, isBinding, contextDescription) { - if (node) { - switch (node.type) { - case "Identifier": - case "ObjectPattern": - case "ArrayPattern": - case "AssignmentPattern": - break; - - case "ObjectExpression": - node.type = "ObjectPattern"; - for (const prop of (node.properties: Array)) { - if (prop.type === "ObjectMethod") { - if (prop.kind === "get" || prop.kind === "set") { - this.raise(prop.key.start, "Object pattern can't contain getter or setter"); + case "ObjectExpression": + node.type = "ObjectPattern"; + for (const prop of (node.properties: Array)) { + if (prop.type === "ObjectMethod") { + if (prop.kind === "get" || prop.kind === "set") { + this.raise(prop.key.start, "Object pattern can't contain getter or setter"); + } else { + this.raise(prop.key.start, "Object pattern can't contain methods"); + } } else { - this.raise(prop.key.start, "Object pattern can't contain methods"); + this.toAssignable(prop, isBinding, "object destructuring pattern"); } + } + break; + + case "ObjectProperty": + this.toAssignable(node.value, isBinding, contextDescription); + break; + + case "SpreadElement": + node.type = "RestElement"; + break; + + case "ArrayExpression": + node.type = "ArrayPattern"; + this.toAssignableList(node.elements, isBinding, contextDescription); + break; + + case "AssignmentExpression": + if (node.operator === "=") { + node.type = "AssignmentPattern"; + delete node.operator; } else { - this.toAssignable(prop, isBinding, "object destructuring pattern"); + this.raise(node.left.end, "Only '=' operator can be used for specifying default value."); + } + break; + + case "MemberExpression": + if (!isBinding) break; + + default: { + const message = "Invalid left-hand side" + + (contextDescription ? " in " + contextDescription : /* istanbul ignore next */ "expression"); + this.raise(node.start, message); + } + } + } + return node; + } + + // Convert list of expression atoms to binding list. + + toAssignableList(exprList, isBinding, contextDescription) { + let end = exprList.length; + if (end) { + const last = exprList[end - 1]; + if (last && last.type === "RestElement") { + --end; + } else if (last && last.type === "SpreadElement") { + last.type = "RestElement"; + const arg = last.argument; + this.toAssignable(arg, isBinding, contextDescription); + if (arg.type !== "Identifier" && arg.type !== "MemberExpression" && arg.type !== "ArrayPattern") { + this.unexpected(arg.start); + } + --end; + } + } + for (let i = 0; i < end; i++) { + const elt = exprList[i]; + if (elt && elt.type === "SpreadElement") + this.raise(elt.start, "The rest element has to be the last element when destructuring"); + if (elt) this.toAssignable(elt, isBinding, contextDescription); + } + return exprList; + } + + // Convert list of expression atoms to a list of + + toReferencedList(exprList) { + return exprList; + } + + // Parses spread element. + + parseSpread(refShorthandDefaultPos) { + const node = this.startNode(); + this.next(); + node.argument = this.parseMaybeAssign(false, refShorthandDefaultPos); + return this.finishNode(node, "SpreadElement"); + } + + parseRest() { + const node = this.startNode(); + this.next(); + node.argument = this.parseBindingAtom(); + return this.finishNode(node, "RestElement"); + } + + shouldAllowYieldIdentifier() { + return this.match(tt._yield) && !this.state.strict && !this.state.inGenerator; + } + + parseBindingIdentifier() { + return this.parseIdentifier(this.shouldAllowYieldIdentifier()); + } + + // Parses lvalue (assignable) atom. + parseBindingAtom() { + switch (this.state.type) { + case tt._yield: + case tt.name: + return this.parseBindingIdentifier(); + + case tt.bracketL: + const node = this.startNode(); + this.next(); + node.elements = this.parseBindingList(tt.bracketR, true); + return this.finishNode(node, "ArrayPattern"); + + case tt.braceL: + return this.parseObj(true); + + default: + this.unexpected(); + } + } + + parseBindingList(close, allowEmpty) { + const elts = []; + let first = true; + while (!this.eat(close)) { + if (first) { + first = false; + } else { + this.expect(tt.comma); + } + if (allowEmpty && this.match(tt.comma)) { + elts.push(null); + } else if (this.eat(close)) { + break; + } else if (this.match(tt.ellipsis)) { + elts.push(this.parseAssignableListItemTypes(this.parseRest())); + this.expect(close); + break; + } else { + const decorators = []; + while (this.match(tt.at)) { + decorators.push(this.parseDecorator()); + } + const left = this.parseMaybeDefault(); + if (decorators.length) { + left.decorators = decorators; + } + this.parseAssignableListItemTypes(left); + elts.push(this.parseMaybeDefault(left.start, left.loc.start, left)); + } + } + return elts; + } + + parseAssignableListItemTypes(param) { + return param; + } + + // Parses assignment pattern around given atom if possible. + + parseMaybeDefault(startPos, startLoc, left) { + startLoc = startLoc || this.state.startLoc; + startPos = startPos || this.state.start; + left = left || this.parseBindingAtom(); + if (!this.eat(tt.eq)) return left; + + const node = this.startNodeAt(startPos, startLoc); + node.left = left; + node.right = this.parseMaybeAssign(); + return this.finishNode(node, "AssignmentPattern"); + } + + // Verify that a node is an lval — something that can be assigned + // to. + + checkLVal(expr, isBinding, checkClashes, contextDescription) { + switch (expr.type) { + case "Identifier": + this.checkReservedWord(expr.name, expr.start, false, true); + + if (checkClashes) { + // we need to prefix this with an underscore for the cases where we have a key of + // `__proto__`. there's a bug in old V8 where the following wouldn't work: + // + // > var obj = Object.create(null); + // undefined + // > obj.__proto__ + // null + // > obj.__proto__ = true; + // true + // > obj.__proto__ + // null + const key = `_${expr.name}`; + + if (checkClashes[key]) { + this.raise(expr.start, "Argument name clash in strict mode"); + } else { + checkClashes[key] = true; } } break; - case "ObjectProperty": - this.toAssignable(node.value, isBinding, contextDescription); + case "MemberExpression": + if (isBinding) + this.raise(expr.start, (isBinding ? "Binding" : "Assigning to") + " member expression"); break; - case "SpreadElement": - node.type = "RestElement"; - break; - - case "ArrayExpression": - node.type = "ArrayPattern"; - this.toAssignableList(node.elements, isBinding, contextDescription); - break; - - case "AssignmentExpression": - if (node.operator === "=") { - node.type = "AssignmentPattern"; - delete node.operator; - } else { - this.raise(node.left.end, "Only '=' operator can be used for specifying default value."); + case "ObjectPattern": + for (let prop of (expr.properties: Array)) { + if (prop.type === "ObjectProperty") prop = prop.value; + this.checkLVal(prop, isBinding, checkClashes, "object destructuring pattern"); } break; - case "MemberExpression": - if (!isBinding) break; + case "ArrayPattern": + for (const elem of (expr.elements: Array)) { + if (elem) this.checkLVal(elem, isBinding, checkClashes, "array destructuring pattern"); + } + break; + + case "AssignmentPattern": + this.checkLVal(expr.left, isBinding, checkClashes, "assignment pattern"); + break; + + case "RestElement": + this.checkLVal(expr.argument, isBinding, checkClashes, "rest element"); + break; default: { - const message = "Invalid left-hand side" + + const message = (isBinding ? /* istanbul ignore next */ "Binding invalid" : "Invalid") + + " left-hand side" + (contextDescription ? " in " + contextDescription : /* istanbul ignore next */ "expression"); - this.raise(node.start, message); + this.raise(expr.start, message); } } } - return node; -}; - -// Convert list of expression atoms to binding list. - -pp.toAssignableList = function (exprList, isBinding, contextDescription) { - let end = exprList.length; - if (end) { - const last = exprList[end - 1]; - if (last && last.type === "RestElement") { - --end; - } else if (last && last.type === "SpreadElement") { - last.type = "RestElement"; - const arg = last.argument; - this.toAssignable(arg, isBinding, contextDescription); - if (arg.type !== "Identifier" && arg.type !== "MemberExpression" && arg.type !== "ArrayPattern") { - this.unexpected(arg.start); - } - --end; - } - } - for (let i = 0; i < end; i++) { - const elt = exprList[i]; - if (elt && elt.type === "SpreadElement") - this.raise(elt.start, "The rest element has to be the last element when destructuring"); - if (elt) this.toAssignable(elt, isBinding, contextDescription); - } - return exprList; -}; - -// Convert list of expression atoms to a list of - -pp.toReferencedList = function (exprList) { - return exprList; -}; - -// Parses spread element. - -pp.parseSpread = function (refShorthandDefaultPos) { - const node = this.startNode(); - this.next(); - node.argument = this.parseMaybeAssign(false, refShorthandDefaultPos); - return this.finishNode(node, "SpreadElement"); -}; - -pp.parseRest = function () { - const node = this.startNode(); - this.next(); - node.argument = this.parseBindingAtom(); - return this.finishNode(node, "RestElement"); -}; - -pp.shouldAllowYieldIdentifier = function () { - return this.match(tt._yield) && !this.state.strict && !this.state.inGenerator; -}; - -pp.parseBindingIdentifier = function () { - return this.parseIdentifier(this.shouldAllowYieldIdentifier()); -}; - -// Parses lvalue (assignable) atom. -pp.parseBindingAtom = function () { - switch (this.state.type) { - case tt._yield: - case tt.name: - return this.parseBindingIdentifier(); - - case tt.bracketL: - const node = this.startNode(); - this.next(); - node.elements = this.parseBindingList(tt.bracketR, true); - return this.finishNode(node, "ArrayPattern"); - - case tt.braceL: - return this.parseObj(true); - - default: - this.unexpected(); - } -}; - -pp.parseBindingList = function (close, allowEmpty) { - const elts = []; - let first = true; - while (!this.eat(close)) { - if (first) { - first = false; - } else { - this.expect(tt.comma); - } - if (allowEmpty && this.match(tt.comma)) { - elts.push(null); - } else if (this.eat(close)) { - break; - } else if (this.match(tt.ellipsis)) { - elts.push(this.parseAssignableListItemTypes(this.parseRest())); - this.expect(close); - break; - } else { - const decorators = []; - while (this.match(tt.at)) { - decorators.push(this.parseDecorator()); - } - const left = this.parseMaybeDefault(); - if (decorators.length) { - left.decorators = decorators; - } - this.parseAssignableListItemTypes(left); - elts.push(this.parseMaybeDefault(left.start, left.loc.start, left)); - } - } - return elts; -}; - -pp.parseAssignableListItemTypes = function (param) { - return param; -}; - -// Parses assignment pattern around given atom if possible. - -pp.parseMaybeDefault = function (startPos, startLoc, left) { - startLoc = startLoc || this.state.startLoc; - startPos = startPos || this.state.start; - left = left || this.parseBindingAtom(); - if (!this.eat(tt.eq)) return left; - - const node = this.startNodeAt(startPos, startLoc); - node.left = left; - node.right = this.parseMaybeAssign(); - return this.finishNode(node, "AssignmentPattern"); -}; - -// Verify that a node is an lval — something that can be assigned -// to. - -pp.checkLVal = function (expr, isBinding, checkClashes, contextDescription) { - switch (expr.type) { - case "Identifier": - this.checkReservedWord(expr.name, expr.start, false, true); - - if (checkClashes) { - // we need to prefix this with an underscore for the cases where we have a key of - // `__proto__`. there's a bug in old V8 where the following wouldn't work: - // - // > var obj = Object.create(null); - // undefined - // > obj.__proto__ - // null - // > obj.__proto__ = true; - // true - // > obj.__proto__ - // null - const key = `_${expr.name}`; - - if (checkClashes[key]) { - this.raise(expr.start, "Argument name clash in strict mode"); - } else { - checkClashes[key] = true; - } - } - break; - - case "MemberExpression": - if (isBinding) this.raise(expr.start, (isBinding ? "Binding" : "Assigning to") + " member expression"); - break; - - case "ObjectPattern": - for (let prop of (expr.properties: Array)) { - if (prop.type === "ObjectProperty") prop = prop.value; - this.checkLVal(prop, isBinding, checkClashes, "object destructuring pattern"); - } - break; - - case "ArrayPattern": - for (const elem of (expr.elements: Array)) { - if (elem) this.checkLVal(elem, isBinding, checkClashes, "array destructuring pattern"); - } - break; - - case "AssignmentPattern": - this.checkLVal(expr.left, isBinding, checkClashes, "assignment pattern"); - break; - - case "RestElement": - this.checkLVal(expr.argument, isBinding, checkClashes, "rest element"); - break; - - default: { - const message = (isBinding ? /* istanbul ignore next */ "Binding invalid" : "Invalid") + - " left-hand side" + - (contextDescription ? " in " + contextDescription : /* istanbul ignore next */ "expression"); - this.raise(expr.start, message); - } - } -}; +} diff --git a/src/parser/node.js b/src/parser/node.js index 364323ec7f..dafe10af9b 100644 --- a/src/parser/node.js +++ b/src/parser/node.js @@ -1,9 +1,9 @@ import Parser from "./index"; +import UtilParser from "./util"; import { SourceLocation, type Position } from "../util/location"; // Start an AST node, attaching a start offset. -const pp = Parser.prototype; const commentKeys = ["leadingComments", "trailingComments", "innerComments"]; class Node { @@ -34,43 +34,40 @@ class Node { } } -pp.startNode = function () { - return new Node(this, this.state.start, this.state.startLoc); -}; +export class NodeUtils extends UtilParser { + startNode() { + return new Node(this, this.state.start, this.state.startLoc); + } -pp.startNodeAt = function (pos, loc) { - return new Node(this, pos, loc); -}; + startNodeAt(pos, loc) { + return new Node(this, pos, loc); + } -function finishNodeAt(node, type, pos, loc) { - node.type = type; - node.end = pos; - node.loc.end = loc; - if (this.options.ranges) node.range[1] = pos; - this.processComment(node); - return node; + // Finish an AST node, adding `type` and `end` properties. + + finishNode(node, type) { + return this.finishNodeAt(node, type, this.state.lastTokEnd, this.state.lastTokEndLoc); + } + + // Finish node at given position + + finishNodeAt(node, type, pos, loc) { + node.type = type; + node.end = pos; + node.loc.end = loc; + if (this.options.ranges) node.range[1] = pos; + this.processComment(node); + return node; + } + + /** + * Reset the start location of node to the start location of locationNode + */ + resetStartLocationFromNode(node, locationNode) { + node.start = locationNode.start; + node.loc.start = locationNode.loc.start; + if (this.options.ranges) node.range[0] = locationNode.range[0]; + + return node; + } } - -// Finish an AST node, adding `type` and `end` properties. - -pp.finishNode = function (node, type) { - return finishNodeAt.call(this, node, type, this.state.lastTokEnd, this.state.lastTokEndLoc); -}; - -// Finish node at given position - -pp.finishNodeAt = function (node, type, pos, loc) { - return finishNodeAt.call(this, node, type, pos, loc); -}; - - -/** - * Reset the start location of node to the start location of locationNode - */ -pp.resetStartLocationFromNode = function (node, locationNode) { - node.start = locationNode.start; - node.loc.start = locationNode.loc.start; - if (this.options.ranges) node.range[0] = locationNode.range[0]; - - return node; -}; diff --git a/src/parser/statement.js b/src/parser/statement.js index f4bd38fb2f..376c685147 100644 --- a/src/parser/statement.js +++ b/src/parser/statement.js @@ -1,1105 +1,1106 @@ /* eslint max-len: 0 */ import { types as tt } from "../tokenizer/types"; -import Parser from "./index"; +import ExpressionParser from "./expression"; import { lineBreak } from "../util/whitespace"; -const pp = Parser.prototype; +// Reused empty array added for node fields that are always empty. -// ### Statement parsing - -// Parse a program. Initializes the parser, reads any number of -// statements, and wraps them in a Program node. Optionally takes a -// `program` argument. If present, the statements will be appended -// to its body instead of creating a new node. - -pp.parseTopLevel = function (file, program) { - program.sourceType = this.options.sourceType; - - this.parseBlockBody(program, true, true, tt.eof); - - file.program = this.finishNode(program, "Program"); - file.comments = this.state.comments; - file.tokens = this.state.tokens; - - return this.finishNode(file, "File"); -}; +const empty = []; const loopLabel = { kind: "loop" }, switchLabel = { kind: "switch" }; -// TODO +export default class StatementParser extends ExpressionParser { -pp.stmtToDirective = function (stmt) { - const expr = stmt.expression; + // ### Statement parsing - const directiveLiteral = this.startNodeAt(expr.start, expr.loc.start); - const directive = this.startNodeAt(stmt.start, stmt.loc.start); + // Parse a program. Initializes the parser, reads any number of + // statements, and wraps them in a Program node. Optionally takes a + // `program` argument. If present, the statements will be appended + // to its body instead of creating a new node. - const raw = this.input.slice(expr.start, expr.end); - const val = directiveLiteral.value = raw.slice(1, -1); // remove quotes + parseTopLevel(file, program) { + program.sourceType = this.options.sourceType; - this.addExtra(directiveLiteral, "raw", raw); - this.addExtra(directiveLiteral, "rawValue", val); + this.parseBlockBody(program, true, true, tt.eof); - directive.value = this.finishNodeAt(directiveLiteral, "DirectiveLiteral", expr.end, expr.loc.end); + file.program = this.finishNode(program, "Program"); + file.comments = this.state.comments; + file.tokens = this.state.tokens; - return this.finishNodeAt(directive, "Directive", stmt.end, stmt.loc.end); -}; - -// Parse a single statement. -// -// If expecting a statement and finding a slash operator, parse a -// regular expression literal. This is to handle cases like -// `if (foo) /blah/.exec(foo)`, where looking at the previous token -// does not help. - -pp.parseStatement = function (declaration, topLevel) { - if (this.match(tt.at)) { - this.parseDecorators(true); + return this.finishNode(file, "File"); } - const starttype = this.state.type; - const node = this.startNode(); + // TODO - // Most types of statements are recognized by the keyword they - // start with. Many are trivial to parse, some require a bit of - // complexity. + stmtToDirective(stmt) { + const expr = stmt.expression; - switch (starttype) { - case tt._break: case tt._continue: return this.parseBreakContinueStatement(node, starttype.keyword); - case tt._debugger: return this.parseDebuggerStatement(node); - case tt._do: return this.parseDoStatement(node); - case tt._for: return this.parseForStatement(node); - case tt._function: - if (!declaration) this.unexpected(); - return this.parseFunctionStatement(node); + const directiveLiteral = this.startNodeAt(expr.start, expr.loc.start); + const directive = this.startNodeAt(stmt.start, stmt.loc.start); - case tt._class: - if (!declaration) this.unexpected(); - return this.parseClass(node, true); + const raw = this.input.slice(expr.start, expr.end); + const val = directiveLiteral.value = raw.slice(1, -1); // remove quotes - case tt._if: return this.parseIfStatement(node); - case tt._return: return this.parseReturnStatement(node); - case tt._switch: return this.parseSwitchStatement(node); - case tt._throw: return this.parseThrowStatement(node); - case tt._try: return this.parseTryStatement(node); + this.addExtra(directiveLiteral, "raw", raw); + this.addExtra(directiveLiteral, "rawValue", val); - case tt._let: - case tt._const: - if (!declaration) this.unexpected(); // NOTE: falls through to _var + directive.value = this.finishNodeAt(directiveLiteral, "DirectiveLiteral", expr.end, expr.loc.end); - case tt._var: - return this.parseVarStatement(node, starttype); + return this.finishNodeAt(directive, "Directive", stmt.end, stmt.loc.end); + } - case tt._while: return this.parseWhileStatement(node); - case tt._with: return this.parseWithStatement(node); - case tt.braceL: return this.parseBlock(); - case tt.semi: return this.parseEmptyStatement(node); - case tt._export: - case tt._import: - if (this.hasPlugin("dynamicImport") && this.lookahead().type === tt.parenL) break; + // Parse a single statement. + // + // If expecting a statement and finding a slash operator, parse a + // regular expression literal. This is to handle cases like + // `if (foo) /blah/.exec(foo)`, where looking at the previous token + // does not help. - if (!this.options.allowImportExportEverywhere) { - if (!topLevel) { - this.raise(this.state.start, "'import' and 'export' may only appear at the top level"); + parseStatement(declaration, topLevel) { + if (this.match(tt.at)) { + this.parseDecorators(true); + } + + const starttype = this.state.type; + const node = this.startNode(); + + // Most types of statements are recognized by the keyword they + // start with. Many are trivial to parse, some require a bit of + // complexity. + + switch (starttype) { + case tt._break: case tt._continue: return this.parseBreakContinueStatement(node, starttype.keyword); + case tt._debugger: return this.parseDebuggerStatement(node); + case tt._do: return this.parseDoStatement(node); + case tt._for: return this.parseForStatement(node); + case tt._function: + if (!declaration) this.unexpected(); + return this.parseFunctionStatement(node); + + case tt._class: + if (!declaration) this.unexpected(); + return this.parseClass(node, true); + + case tt._if: return this.parseIfStatement(node); + case tt._return: return this.parseReturnStatement(node); + case tt._switch: return this.parseSwitchStatement(node); + case tt._throw: return this.parseThrowStatement(node); + case tt._try: return this.parseTryStatement(node); + + case tt._let: + case tt._const: + if (!declaration) this.unexpected(); // NOTE: falls through to _var + + case tt._var: + return this.parseVarStatement(node, starttype); + + case tt._while: return this.parseWhileStatement(node); + case tt._with: return this.parseWithStatement(node); + case tt.braceL: return this.parseBlock(); + case tt.semi: return this.parseEmptyStatement(node); + case tt._export: + case tt._import: + if (this.hasPlugin("dynamicImport") && this.lookahead().type === tt.parenL) break; + + if (!this.options.allowImportExportEverywhere) { + if (!topLevel) { + this.raise(this.state.start, "'import' and 'export' may only appear at the top level"); + } + + if (!this.inModule) { + this.raise(this.state.start, "'import' and 'export' may appear only with 'sourceType: module'"); + } } + return starttype === tt._import ? this.parseImport(node) : this.parseExport(node); - if (!this.inModule) { - this.raise(this.state.start, "'import' and 'export' may appear only with 'sourceType: module'"); + case tt.name: + if (this.state.value === "async") { + // peek ahead and see if next token is a function + const state = this.state.clone(); + this.next(); + if (this.match(tt._function) && !this.canInsertSemicolon()) { + this.expect(tt._function); + return this.parseFunction(node, true, false, true); + } else { + this.state = state; + } } - } - return starttype === tt._import ? this.parseImport(node) : this.parseExport(node); + } - case tt.name: - if (this.state.value === "async") { - // peek ahead and see if next token is a function - const state = this.state.clone(); - this.next(); - if (this.match(tt._function) && !this.canInsertSemicolon()) { - this.expect(tt._function); - return this.parseFunction(node, true, false, true); - } else { - this.state = state; - } - } - } + // If the statement does not start with a statement keyword or a + // brace, it's an ExpressionStatement or LabeledStatement. We + // simply start parsing an expression, and afterwards, if the + // next token is a colon and the expression was a simple + // Identifier node, we switch to interpreting it as a label. + const maybeName = this.state.value; + const expr = this.parseExpression(); - // If the statement does not start with a statement keyword or a - // brace, it's an ExpressionStatement or LabeledStatement. We - // simply start parsing an expression, and afterwards, if the - // next token is a colon and the expression was a simple - // Identifier node, we switch to interpreting it as a label. - const maybeName = this.state.value; - const expr = this.parseExpression(); - - if (starttype === tt.name && expr.type === "Identifier" && this.eat(tt.colon)) { - return this.parseLabeledStatement(node, maybeName, expr); - } else { - return this.parseExpressionStatement(node, expr); - } -}; - -pp.takeDecorators = function (node) { - if (this.state.decorators.length) { - node.decorators = this.state.decorators; - this.state.decorators = []; - } -}; - -pp.parseDecorators = function (allowExport) { - while (this.match(tt.at)) { - const decorator = this.parseDecorator(); - this.state.decorators.push(decorator); - } - - if (allowExport && this.match(tt._export)) { - return; - } - - if (!this.match(tt._class)) { - this.raise(this.state.start, "Leading decorators must be attached to a class declaration"); - } -}; - -pp.parseDecorator = function () { - if (!this.hasPlugin("decorators")) { - this.unexpected(); - } - const node = this.startNode(); - this.next(); - node.expression = this.parseMaybeAssign(); - return this.finishNode(node, "Decorator"); -}; - -pp.parseBreakContinueStatement = function (node, keyword) { - const isBreak = keyword === "break"; - this.next(); - - 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. - let i; - for (i = 0; i < this.state.labels.length; ++i) { - const lab = this.state.labels[i]; - if (node.label == null || lab.name === node.label.name) { - if (lab.kind != null && (isBreak || lab.kind === "loop")) break; - if (node.label && isBreak) break; + if (starttype === tt.name && expr.type === "Identifier" && this.eat(tt.colon)) { + return this.parseLabeledStatement(node, maybeName, expr); + } else { + return this.parseExpressionStatement(node, expr); } } - if (i === this.state.labels.length) this.raise(node.start, "Unsyntactic " + keyword); - return this.finishNode(node, isBreak ? "BreakStatement" : "ContinueStatement"); -}; -pp.parseDebuggerStatement = function (node) { - this.next(); - this.semicolon(); - return this.finishNode(node, "DebuggerStatement"); -}; - -pp.parseDoStatement = function (node) { - this.next(); - this.state.labels.push(loopLabel); - node.body = this.parseStatement(false); - this.state.labels.pop(); - this.expect(tt._while); - node.test = this.parseParenExpression(); - this.eat(tt.semi); - return this.finishNode(node, "DoWhileStatement"); -}; - -// Disambiguating between a `for` and a `for`/`in` or `for`/`of` -// loop is non-trivial. Basically, we have to parse the init `var` -// statement or expression, disallowing the `in` operator (see -// the second parameter to `parseExpression`), and then check -// whether the next token is `in` or `of`. When there is no init -// part (semicolon immediately after the opening parenthesis), it -// is a regular `for` loop. - -pp.parseForStatement = function (node) { - this.next(); - this.state.labels.push(loopLabel); - - let forAwait = false; - if (this.hasPlugin("asyncGenerators") && this.state.inAsync && this.isContextual("await")) { - forAwait = true; - this.next(); + takeDecorators(node) { + if (this.state.decorators.length) { + node.decorators = this.state.decorators; + this.state.decorators = []; + } } - this.expect(tt.parenL); - if (this.match(tt.semi)) { - if (forAwait) { + parseDecorators(allowExport) { + while (this.match(tt.at)) { + const decorator = this.parseDecorator(); + this.state.decorators.push(decorator); + } + + if (allowExport && this.match(tt._export)) { + return; + } + + if (!this.match(tt._class)) { + this.raise(this.state.start, "Leading decorators must be attached to a class declaration"); + } + } + + parseDecorator() { + if (!this.hasPlugin("decorators")) { this.unexpected(); } - return this.parseFor(node, null); + const node = this.startNode(); + this.next(); + node.expression = this.parseMaybeAssign(); + return this.finishNode(node, "Decorator"); } - if (this.match(tt._var) || this.match(tt._let) || this.match(tt._const)) { - const init = this.startNode(); - const varKind = this.state.type; + parseBreakContinueStatement(node, keyword) { + const isBreak = keyword === "break"; this.next(); - this.parseVar(init, true, varKind); - this.finishNode(init, "VariableDeclaration"); - if (this.match(tt._in) || this.isContextual("of")) { - if (init.declarations.length === 1 && !init.declarations[0].init) { - return this.parseForIn(node, init, forAwait); + 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. + let i; + for (i = 0; i < this.state.labels.length; ++i) { + const lab = this.state.labels[i]; + if (node.label == null || lab.name === node.label.name) { + if (lab.kind != null && (isBreak || lab.kind === "loop")) break; + if (node.label && isBreak) break; } } + if (i === this.state.labels.length) this.raise(node.start, "Unsyntactic " + keyword); + return this.finishNode(node, isBreak ? "BreakStatement" : "ContinueStatement"); + } + + parseDebuggerStatement(node) { + this.next(); + this.semicolon(); + return this.finishNode(node, "DebuggerStatement"); + } + + parseDoStatement(node) { + this.next(); + this.state.labels.push(loopLabel); + node.body = this.parseStatement(false); + this.state.labels.pop(); + this.expect(tt._while); + node.test = this.parseParenExpression(); + this.eat(tt.semi); + return this.finishNode(node, "DoWhileStatement"); + } + + // Disambiguating between a `for` and a `for`/`in` or `for`/`of` + // loop is non-trivial. Basically, we have to parse the init `var` + // statement or expression, disallowing the `in` operator (see + // the second parameter to `parseExpression`), and then check + // whether the next token is `in` or `of`. When there is no init + // part (semicolon immediately after the opening parenthesis), it + // is a regular `for` loop. + + parseForStatement(node) { + this.next(); + this.state.labels.push(loopLabel); + + let forAwait = false; + if (this.hasPlugin("asyncGenerators") && this.state.inAsync && this.isContextual("await")) { + forAwait = true; + this.next(); + } + this.expect(tt.parenL); + + if (this.match(tt.semi)) { + if (forAwait) { + this.unexpected(); + } + return this.parseFor(node, null); + } + + if (this.match(tt._var) || this.match(tt._let) || this.match(tt._const)) { + const init = this.startNode(); + const varKind = this.state.type; + this.next(); + this.parseVar(init, true, varKind); + this.finishNode(init, "VariableDeclaration"); + + if (this.match(tt._in) || this.isContextual("of")) { + if (init.declarations.length === 1 && !init.declarations[0].init) { + return this.parseForIn(node, init, forAwait); + } + } + if (forAwait) { + this.unexpected(); + } + return this.parseFor(node, init); + } + + const refShorthandDefaultPos = { start: 0 }; + const init = this.parseExpression(true, refShorthandDefaultPos); + if (this.match(tt._in) || this.isContextual("of")) { + const description = this.isContextual("of") ? "for-of statement" : "for-in statement"; + this.toAssignable(init, undefined, description); + this.checkLVal(init, undefined, undefined, description); + return this.parseForIn(node, init, forAwait); + } else if (refShorthandDefaultPos.start) { + this.unexpected(refShorthandDefaultPos.start); + } if (forAwait) { this.unexpected(); } return this.parseFor(node, init); } - const refShorthandDefaultPos = { start: 0 }; - const init = this.parseExpression(true, refShorthandDefaultPos); - if (this.match(tt._in) || this.isContextual("of")) { - const description = this.isContextual("of") ? "for-of statement" : "for-in statement"; - this.toAssignable(init, undefined, description); - this.checkLVal(init, undefined, undefined, description); - return this.parseForIn(node, init, forAwait); - } else if (refShorthandDefaultPos.start) { - this.unexpected(refShorthandDefaultPos.start); - } - if (forAwait) { - this.unexpected(); - } - return this.parseFor(node, init); -}; - -pp.parseFunctionStatement = function (node) { - this.next(); - return this.parseFunction(node, true); -}; - -pp.parseIfStatement = function (node) { - this.next(); - node.test = this.parseParenExpression(); - node.consequent = this.parseStatement(false); - node.alternate = this.eat(tt._else) ? this.parseStatement(false) : null; - return this.finishNode(node, "IfStatement"); -}; - -pp.parseReturnStatement = function (node) { - if (!this.state.inFunction && !this.options.allowReturnOutsideFunction) { - this.raise(this.state.start, "'return' outside of function"); + parseFunctionStatement(node) { + this.next(); + return this.parseFunction(node, true); } - this.next(); + parseIfStatement(node) { + this.next(); + node.test = this.parseParenExpression(); + node.consequent = this.parseStatement(false); + node.alternate = this.eat(tt._else) ? this.parseStatement(false) : null; + return this.finishNode(node, "IfStatement"); + } - // In `return` (and `break`/`continue`), the keywords with - // optional arguments, we eagerly look for a semicolon or the - // possibility to insert one. + parseReturnStatement(node) { + if (!this.state.inFunction && !this.options.allowReturnOutsideFunction) { + this.raise(this.state.start, "'return' outside of function"); + } - if (this.isLineTerminator()) { - node.argument = null; - } else { + this.next(); + + // In `return` (and `break`/`continue`), the keywords with + // optional arguments, we eagerly look for a semicolon or the + // possibility to insert one. + + if (this.isLineTerminator()) { + node.argument = null; + } else { + node.argument = this.parseExpression(); + this.semicolon(); + } + + return this.finishNode(node, "ReturnStatement"); + } + + parseSwitchStatement(node) { + this.next(); + node.discriminant = this.parseParenExpression(); + node.cases = []; + this.expect(tt.braceL); + this.state.labels.push(switchLabel); + + // Statements under must be grouped (by label) in SwitchCase + // nodes. `cur` is used to keep the node that we are currently + // adding statements to. + + let cur; + for (let sawDefault; !this.match(tt.braceR); ) { + if (this.match(tt._case) || this.match(tt._default)) { + const isCase = this.match(tt._case); + if (cur) this.finishNode(cur, "SwitchCase"); + node.cases.push(cur = this.startNode()); + cur.consequent = []; + this.next(); + if (isCase) { + cur.test = this.parseExpression(); + } else { + if (sawDefault) this.raise(this.state.lastTokStart, "Multiple default clauses"); + sawDefault = true; + cur.test = null; + } + this.expect(tt.colon); + } else { + if (cur) { + cur.consequent.push(this.parseStatement(true)); + } else { + this.unexpected(); + } + } + } + if (cur) this.finishNode(cur, "SwitchCase"); + this.next(); // Closing brace + this.state.labels.pop(); + return this.finishNode(node, "SwitchStatement"); + } + + parseThrowStatement(node) { + this.next(); + if (lineBreak.test(this.input.slice(this.state.lastTokEnd, this.state.start))) + this.raise(this.state.lastTokEnd, "Illegal newline after throw"); node.argument = this.parseExpression(); this.semicolon(); + return this.finishNode(node, "ThrowStatement"); + } + + parseTryStatement(node) { + this.next(); + + node.block = this.parseBlock(); + node.handler = null; + + if (this.match(tt._catch)) { + const clause = this.startNode(); + this.next(); + + this.expect(tt.parenL); + clause.param = this.parseBindingAtom(); + this.checkLVal(clause.param, true, Object.create(null), "catch clause"); + this.expect(tt.parenR); + + clause.body = this.parseBlock(); + node.handler = this.finishNode(clause, "CatchClause"); + } + + node.guardedHandlers = empty; + node.finalizer = this.eat(tt._finally) ? this.parseBlock() : null; + + if (!node.handler && !node.finalizer) { + this.raise(node.start, "Missing catch or finally clause"); + } + + return this.finishNode(node, "TryStatement"); + } + + parseVarStatement(node, kind) { + this.next(); + this.parseVar(node, false, kind); + this.semicolon(); + return this.finishNode(node, "VariableDeclaration"); + } + + parseWhileStatement(node) { + this.next(); + node.test = this.parseParenExpression(); + this.state.labels.push(loopLabel); + node.body = this.parseStatement(false); + this.state.labels.pop(); + return this.finishNode(node, "WhileStatement"); + } + + parseWithStatement(node) { + if (this.state.strict) this.raise(this.state.start, "'with' in strict mode"); + this.next(); + node.object = this.parseParenExpression(); + node.body = this.parseStatement(false); + return this.finishNode(node, "WithStatement"); + } + + parseEmptyStatement(node) { + this.next(); + return this.finishNode(node, "EmptyStatement"); + } + + parseLabeledStatement(node, maybeName, expr) { + for (const label of (this.state.labels: Array)) { + if (label.name === maybeName) { + this.raise(expr.start, `Label '${maybeName}' is already declared`); + } + } + + const kind = this.state.type.isLoop ? "loop" : this.match(tt._switch) ? "switch" : null; + for (let i = this.state.labels.length - 1; i >= 0; i--) { + const label = this.state.labels[i]; + if (label.statementStart === node.start) { + label.statementStart = this.state.start; + label.kind = kind; + } else { + break; + } + } + + this.state.labels.push({ name: maybeName, kind: kind, statementStart: this.state.start }); + node.body = this.parseStatement(true); + this.state.labels.pop(); + node.label = expr; + return this.finishNode(node, "LabeledStatement"); + } + + parseExpressionStatement(node, expr) { + node.expression = expr; + this.semicolon(); + return this.finishNode(node, "ExpressionStatement"); + } + + // Parse a semicolon-enclosed block of statements, handling `"use + // strict"` declarations when `allowStrict` is true (used for + // function bodies). + + parseBlock(allowDirectives?) { + const node = this.startNode(); + this.expect(tt.braceL); + this.parseBlockBody(node, allowDirectives, false, tt.braceR); + return this.finishNode(node, "BlockStatement"); + } + + isValidDirective(stmt) { + return stmt.type === "ExpressionStatement" && + stmt.expression.type === "StringLiteral" && + !stmt.expression.extra.parenthesized; + } + + parseBlockBody(node, allowDirectives, topLevel, end) { + node.body = []; + node.directives = []; + + let parsedNonDirective = false; + let oldStrict; + let octalPosition; + + while (!this.eat(end)) { + if (!parsedNonDirective && this.state.containsOctal && !octalPosition) { + octalPosition = this.state.octalPosition; + } + + const stmt = this.parseStatement(true, topLevel); + + if (allowDirectives && !parsedNonDirective && this.isValidDirective(stmt)) { + const directive = this.stmtToDirective(stmt); + node.directives.push(directive); + + if (oldStrict === undefined && directive.value.value === "use strict") { + oldStrict = this.state.strict; + this.setStrict(true); + + if (octalPosition) { + this.raise(octalPosition, "Octal literal in strict mode"); + } + } + + continue; + } + + parsedNonDirective = true; + node.body.push(stmt); + } + + if (oldStrict === false) { + this.setStrict(false); + } + } + + // Parse a regular `for` loop. The disambiguation code in + // `parseStatement` will already have parsed the init statement or + // expression. + + parseFor(node, init) { + node.init = init; + this.expect(tt.semi); + node.test = this.match(tt.semi) ? null : this.parseExpression(); + this.expect(tt.semi); + node.update = this.match(tt.parenR) ? null : this.parseExpression(); + this.expect(tt.parenR); + node.body = this.parseStatement(false); + this.state.labels.pop(); + return this.finishNode(node, "ForStatement"); + } + + // Parse a `for`/`in` and `for`/`of` loop, which are almost + // same from parser's perspective. + + parseForIn(node, init, forAwait) { + const type = this.match(tt._in) ? "ForInStatement" : "ForOfStatement"; + if (forAwait) { + this.eatContextual("of"); + } else { + this.next(); + } + node.await = !!forAwait; + node.left = init; + node.right = this.parseExpression(); + this.expect(tt.parenR); + node.body = this.parseStatement(false); + this.state.labels.pop(); + return this.finishNode(node, type); + } + + // Parse a list of variable declarations. + + parseVar(node, isFor, kind) { + node.declarations = []; + node.kind = kind.keyword; + for (;;) { + const decl = this.startNode(); + this.parseVarHead(decl); + if (this.eat(tt.eq)) { + decl.init = this.parseMaybeAssign(isFor); + } else if (kind === tt._const && !(this.match(tt._in) || this.isContextual("of"))) { + this.unexpected(); + } else if (decl.id.type !== "Identifier" && !(isFor && (this.match(tt._in) || this.isContextual("of")))) { + this.raise(this.state.lastTokEnd, "Complex binding patterns require an initialization value"); + } else { + decl.init = null; + } + node.declarations.push(this.finishNode(decl, "VariableDeclarator")); + if (!this.eat(tt.comma)) break; + } + return node; + } + + parseVarHead(decl) { + decl.id = this.parseBindingAtom(); + this.checkLVal(decl.id, true, undefined, "variable declaration"); + } + + // Parse a function declaration or literal (depending on the + // `isStatement` parameter). + + parseFunction(node, isStatement, allowExpressionBody, isAsync, optionalId) { + const oldInMethod = this.state.inMethod; + this.state.inMethod = false; + + this.initFunction(node, isAsync); + + if (this.match(tt.star)) { + if (node.async && !this.hasPlugin("asyncGenerators")) { + this.unexpected(); + } else { + node.generator = true; + this.next(); + } + } + + if (isStatement && !optionalId && !this.match(tt.name) && !this.match(tt._yield)) { + this.unexpected(); + } + + if (this.match(tt.name) || this.match(tt._yield)) { + node.id = this.parseBindingIdentifier(); + } + + this.parseFunctionParams(node); + this.parseFunctionBody(node, allowExpressionBody); + + this.state.inMethod = oldInMethod; + + return this.finishNode(node, isStatement ? "FunctionDeclaration" : "FunctionExpression"); + } + + parseFunctionParams(node) { + this.expect(tt.parenL); + node.params = this.parseBindingList(tt.parenR); + } + + // Parse a class declaration or literal (depending on the + // `isStatement` parameter). + + parseClass(node, isStatement, optionalId) { + this.next(); + this.takeDecorators(node); + this.parseClassId(node, isStatement, optionalId); + this.parseClassSuper(node); + this.parseClassBody(node); + return this.finishNode(node, isStatement ? "ClassDeclaration" : "ClassExpression"); + } + + isClassProperty() { + return this.match(tt.eq) || this.match(tt.semi) || this.match(tt.braceR); + } + + isClassMethod() { + return this.match(tt.parenL); + } + + isNonstaticConstructor(method) { + return !method.computed && !method.static && ( + (method.key.name === "constructor") || // Identifier + (method.key.value === "constructor") // Literal + ); + } + + parseClassBody(node) { + // class bodies are implicitly strict + const oldStrict = this.state.strict; + this.state.strict = true; + + let hadConstructor = false; + let decorators = []; + const classBody = this.startNode(); + + classBody.body = []; + + this.expect(tt.braceL); + + while (!this.eat(tt.braceR)) { + if (this.eat(tt.semi)) { + if (decorators.length > 0) { + this.raise(this.state.lastTokEnd, "Decorators must not be followed by a semicolon"); + } + continue; + } + + if (this.match(tt.at)) { + decorators.push(this.parseDecorator()); + continue; + } + + const method = this.startNode(); + + // steal the decorators if there are any + if (decorators.length) { + method.decorators = decorators; + decorators = []; + } + + method.static = false; + if (this.match(tt.name) && this.state.value === "static") { + const key = this.parseIdentifier(true); // eats 'static' + if (this.isClassMethod()) { + // a method named 'static' + method.kind = "method"; + method.computed = false; + method.key = key; + this.parseClassMethod(classBody, method, false, false); + continue; + } else if (this.isClassProperty()) { + // a property named 'static' + method.computed = false; + method.key = key; + classBody.body.push(this.parseClassProperty(method)); + continue; + } + // otherwise something static + method.static = true; + } + + if (this.eat(tt.star)) { + // a generator + method.kind = "method"; + this.parsePropertyName(method); + if (this.isNonstaticConstructor(method)) { + this.raise(method.key.start, "Constructor can't be a generator"); + } + if (!method.computed && method.static && (method.key.name === "prototype" || method.key.value === "prototype")) { + this.raise(method.key.start, "Classes may not have static property named prototype"); + } + this.parseClassMethod(classBody, method, true, false); + } else { + const isSimple = this.match(tt.name); + const key = this.parsePropertyName(method); + if (!method.computed && method.static && (method.key.name === "prototype" || method.key.value === "prototype")) { + this.raise(method.key.start, "Classes may not have static property named prototype"); + } + if (this.isClassMethod()) { + // a normal method + if (this.isNonstaticConstructor(method)) { + if (hadConstructor) { + this.raise(key.start, "Duplicate constructor in the same class"); + } else if (method.decorators) { + this.raise(method.start, "You can't attach decorators to a class constructor"); + } + hadConstructor = true; + method.kind = "constructor"; + } else { + method.kind = "method"; + } + this.parseClassMethod(classBody, method, false, false); + } else if (this.isClassProperty()) { + // a normal property + if (this.isNonstaticConstructor(method)) { + this.raise(method.key.start, "Classes may not have a non-static field named 'constructor'"); + } + classBody.body.push(this.parseClassProperty(method)); + } else if (isSimple && key.name === "async" && !this.isLineTerminator()) { + // an async method + const isGenerator = this.hasPlugin("asyncGenerators") && this.eat(tt.star); + method.kind = "method"; + this.parsePropertyName(method); + if (this.isNonstaticConstructor(method)) { + this.raise(method.key.start, "Constructor can't be an async function"); + } + this.parseClassMethod(classBody, method, isGenerator, true); + } else if (isSimple && (key.name === "get" || key.name === "set") && !(this.isLineTerminator() && this.match(tt.star))) { // `get\n*` is an uninitialized property named 'get' followed by a generator. + // a getter or setter + method.kind = key.name; + this.parsePropertyName(method); + if (this.isNonstaticConstructor(method)) { + this.raise(method.key.start, "Constructor can't have get/set modifier"); + } + this.parseClassMethod(classBody, method, false, false); + this.checkGetterSetterParamCount(method); + } else if (this.isLineTerminator()) { + // an uninitialized class property (due to ASI, since we don't otherwise recognize the next token) + if (this.isNonstaticConstructor(method)) { + this.raise(method.key.start, "Classes may not have a non-static field named 'constructor'"); + } + classBody.body.push(this.parseClassProperty(method)); + } else { + this.unexpected(); + } + } + } + + if (decorators.length) { + this.raise(this.state.start, "You have trailing decorators with no method"); + } + + node.body = this.finishNode(classBody, "ClassBody"); + + this.state.strict = oldStrict; + } + + parseClassProperty(node) { + const noPluginMsg = "You can only use Class Properties when the 'classProperties' plugin is enabled."; + if (!node.typeAnnotation && !this.hasPlugin("classProperties")) { + this.raise(node.start, noPluginMsg); + } + + if (this.match(tt.eq)) { + if (!this.hasPlugin("classProperties")) this.raise(this.state.start, noPluginMsg); + this.next(); + node.value = this.parseMaybeAssign(); + } else { + node.value = null; + } + this.semicolon(); + return this.finishNode(node, "ClassProperty"); + } + + parseClassMethod(classBody, method, isGenerator, isAsync) { + this.parseMethod(method, isGenerator, isAsync); + classBody.body.push(this.finishNode(method, "ClassMethod")); + } + + parseClassId(node, isStatement, optionalId) { + if (this.match(tt.name)) { + node.id = this.parseIdentifier(); + } else { + if (optionalId || !isStatement) { + node.id = null; + } else { + this.unexpected(); + } + } + } + + parseClassSuper(node) { + node.superClass = this.eat(tt._extends) ? this.parseExprSubscripts() : null; + } + + // Parses module export declaration. + + parseExport(node) { + this.eat(tt._export); + + // export * from '...' + if (this.match(tt.star)) { + const specifier = this.startNode(); + this.next(); + if (this.hasPlugin("exportExtensions") && this.eatContextual("as")) { + specifier.exported = this.parseIdentifier(true); + node.specifiers = [this.finishNode(specifier, "ExportNamespaceSpecifier")]; + this.parseExportSpecifiersMaybe(node); + this.parseExportFrom(node, true); + } else { + this.parseExportFrom(node, true); + return this.finishNode(node, "ExportAllDeclaration"); + } + } else if (this.hasPlugin("exportExtensions") && this.isExportDefaultSpecifier()) { + const specifier = this.startNode(); + specifier.exported = this.parseIdentifier(true); + node.specifiers = [this.finishNode(specifier, "ExportDefaultSpecifier")]; + if (this.match(tt.comma) && this.lookahead().type === tt.star) { + this.expect(tt.comma); + const specifier = this.startNode(); + this.expect(tt.star); + this.expectContextual("as"); + specifier.exported = this.parseIdentifier(); + node.specifiers.push(this.finishNode(specifier, "ExportNamespaceSpecifier")); + } else { + this.parseExportSpecifiersMaybe(node); + } + this.parseExportFrom(node, true); + } else if (this.eat(tt._default)) { // export default ... + let expr = this.startNode(); + let needsSemi = false; + if (this.eat(tt._function)) { + expr = this.parseFunction(expr, true, false, false, true); + } else if ( + this.isContextual("async") && + this.lookahead().type === tt._function + ) { // async function declaration + this.eatContextual("async"); + this.eat(tt._function); + expr = this.parseFunction(expr, true, false, true, true); + } else if (this.match(tt._class)) { + expr = this.parseClass(expr, true, true); + } else { + needsSemi = true; + expr = this.parseMaybeAssign(); + } + node.declaration = expr; + if (needsSemi) this.semicolon(); + this.checkExport(node, true, true); + return this.finishNode(node, "ExportDefaultDeclaration"); + } else if (this.shouldParseExportDeclaration()) { + node.specifiers = []; + node.source = null; + node.declaration = this.parseExportDeclaration(node); + } else { // export { x, y as z } [from '...'] + node.declaration = null; + node.specifiers = this.parseExportSpecifiers(); + this.parseExportFrom(node); + } + this.checkExport(node, true); + return this.finishNode(node, "ExportNamedDeclaration"); + } + + parseExportDeclaration() { + return this.parseStatement(true); + } + + isExportDefaultSpecifier() { + if (this.match(tt.name)) { + return this.state.value !== "type" + && this.state.value !== "async" + && this.state.value !== "interface"; + } + + if (!this.match(tt._default)) { + return false; + } + + const lookahead = this.lookahead(); + return lookahead.type === tt.comma || (lookahead.type === tt.name && lookahead.value === "from"); + } + + parseExportSpecifiersMaybe(node) { + if (this.eat(tt.comma)) { + node.specifiers = node.specifiers.concat(this.parseExportSpecifiers()); + } + } + + parseExportFrom(node, expect?) { + if (this.eatContextual("from")) { + node.source = this.match(tt.string) ? this.parseExprAtom() : this.unexpected(); + this.checkExport(node); + } else { + if (expect) { + this.unexpected(); + } else { + node.source = null; + } + } + + this.semicolon(); } - return this.finishNode(node, "ReturnStatement"); -}; - -pp.parseSwitchStatement = function (node) { - this.next(); - node.discriminant = this.parseParenExpression(); - node.cases = []; - this.expect(tt.braceL); - this.state.labels.push(switchLabel); - - // Statements under must be grouped (by label) in SwitchCase - // nodes. `cur` is used to keep the node that we are currently - // adding statements to. - - let cur; - for (let sawDefault; !this.match(tt.braceR); ) { - if (this.match(tt._case) || this.match(tt._default)) { - const isCase = this.match(tt._case); - if (cur) this.finishNode(cur, "SwitchCase"); - node.cases.push(cur = this.startNode()); - cur.consequent = []; - this.next(); - if (isCase) { - cur.test = this.parseExpression(); - } else { - if (sawDefault) this.raise(this.state.lastTokStart, "Multiple default clauses"); - sawDefault = true; - cur.test = null; - } - this.expect(tt.colon); - } else { - if (cur) { - cur.consequent.push(this.parseStatement(true)); - } else { - this.unexpected(); - } - } - } - if (cur) this.finishNode(cur, "SwitchCase"); - this.next(); // Closing brace - this.state.labels.pop(); - return this.finishNode(node, "SwitchStatement"); -}; - -pp.parseThrowStatement = function (node) { - this.next(); - if (lineBreak.test(this.input.slice(this.state.lastTokEnd, this.state.start))) - this.raise(this.state.lastTokEnd, "Illegal newline after throw"); - node.argument = this.parseExpression(); - this.semicolon(); - return this.finishNode(node, "ThrowStatement"); -}; - -// Reused empty array added for node fields that are always empty. - -const empty = []; - -pp.parseTryStatement = function (node) { - this.next(); - - node.block = this.parseBlock(); - node.handler = null; - - if (this.match(tt._catch)) { - const clause = this.startNode(); - this.next(); - - this.expect(tt.parenL); - clause.param = this.parseBindingAtom(); - this.checkLVal(clause.param, true, Object.create(null), "catch clause"); - this.expect(tt.parenR); - - clause.body = this.parseBlock(); - node.handler = this.finishNode(clause, "CatchClause"); + shouldParseExportDeclaration(): boolean { + return this.state.type.keyword === "var" + || this.state.type.keyword === "const" + || this.state.type.keyword === "let" + || this.state.type.keyword === "function" + || this.state.type.keyword === "class" + || this.isContextual("async"); } - node.guardedHandlers = empty; - node.finalizer = this.eat(tt._finally) ? this.parseBlock() : null; - - if (!node.handler && !node.finalizer) { - this.raise(node.start, "Missing catch or finally clause"); - } - - return this.finishNode(node, "TryStatement"); -}; - -pp.parseVarStatement = function (node, kind) { - this.next(); - this.parseVar(node, false, kind); - this.semicolon(); - return this.finishNode(node, "VariableDeclaration"); -}; - -pp.parseWhileStatement = function (node) { - this.next(); - node.test = this.parseParenExpression(); - this.state.labels.push(loopLabel); - node.body = this.parseStatement(false); - this.state.labels.pop(); - return this.finishNode(node, "WhileStatement"); -}; - -pp.parseWithStatement = function (node) { - if (this.state.strict) this.raise(this.state.start, "'with' in strict mode"); - this.next(); - node.object = this.parseParenExpression(); - node.body = this.parseStatement(false); - return this.finishNode(node, "WithStatement"); -}; - -pp.parseEmptyStatement = function (node) { - this.next(); - return this.finishNode(node, "EmptyStatement"); -}; - -pp.parseLabeledStatement = function (node, maybeName, expr) { - for (const label of (this.state.labels: Array)) { - if (label.name === maybeName) { - this.raise(expr.start, `Label '${maybeName}' is already declared`); - } - } - - const kind = this.state.type.isLoop ? "loop" : this.match(tt._switch) ? "switch" : null; - for (let i = this.state.labels.length - 1; i >= 0; i--) { - const label = this.state.labels[i]; - if (label.statementStart === node.start) { - label.statementStart = this.state.start; - label.kind = kind; - } else { - break; - } - } - - this.state.labels.push({ name: maybeName, kind: kind, statementStart: this.state.start }); - node.body = this.parseStatement(true); - this.state.labels.pop(); - node.label = expr; - return this.finishNode(node, "LabeledStatement"); -}; - -pp.parseExpressionStatement = function (node, expr) { - node.expression = expr; - this.semicolon(); - return this.finishNode(node, "ExpressionStatement"); -}; - -// Parse a semicolon-enclosed block of statements, handling `"use -// strict"` declarations when `allowStrict` is true (used for -// function bodies). - -pp.parseBlock = function (allowDirectives?) { - const node = this.startNode(); - this.expect(tt.braceL); - this.parseBlockBody(node, allowDirectives, false, tt.braceR); - return this.finishNode(node, "BlockStatement"); -}; - -pp.isValidDirective = function (stmt) { - return stmt.type === "ExpressionStatement" && - stmt.expression.type === "StringLiteral" && - !stmt.expression.extra.parenthesized; -}; - -pp.parseBlockBody = function (node, allowDirectives, topLevel, end) { - node.body = []; - node.directives = []; - - let parsedNonDirective = false; - let oldStrict; - let octalPosition; - - while (!this.eat(end)) { - if (!parsedNonDirective && this.state.containsOctal && !octalPosition) { - octalPosition = this.state.octalPosition; - } - - const stmt = this.parseStatement(true, topLevel); - - if (allowDirectives && !parsedNonDirective && this.isValidDirective(stmt)) { - const directive = this.stmtToDirective(stmt); - node.directives.push(directive); - - if (oldStrict === undefined && directive.value.value === "use strict") { - oldStrict = this.state.strict; - this.setStrict(true); - - if (octalPosition) { - this.raise(octalPosition, "Octal literal in strict mode"); + checkExport(node, checkNames, isDefault) { + if (checkNames) { + // Check for duplicate exports + if (isDefault) { + // Default exports + this.checkDuplicateExports(node, "default"); + } else if (node.specifiers && node.specifiers.length) { + // Named exports + for (const specifier of node.specifiers) { + this.checkDuplicateExports(specifier, specifier.exported.name); } - } - - continue; - } - - parsedNonDirective = true; - node.body.push(stmt); - } - - if (oldStrict === false) { - this.setStrict(false); - } -}; - -// Parse a regular `for` loop. The disambiguation code in -// `parseStatement` will already have parsed the init statement or -// expression. - -pp.parseFor = function (node, init) { - node.init = init; - this.expect(tt.semi); - node.test = this.match(tt.semi) ? null : this.parseExpression(); - this.expect(tt.semi); - node.update = this.match(tt.parenR) ? null : this.parseExpression(); - this.expect(tt.parenR); - node.body = this.parseStatement(false); - this.state.labels.pop(); - return this.finishNode(node, "ForStatement"); -}; - -// Parse a `for`/`in` and `for`/`of` loop, which are almost -// same from parser's perspective. - -pp.parseForIn = function (node, init, forAwait) { - const type = this.match(tt._in) ? "ForInStatement" : "ForOfStatement"; - if (forAwait) { - this.eatContextual("of"); - } else { - this.next(); - } - node.await = !!forAwait; - node.left = init; - node.right = this.parseExpression(); - this.expect(tt.parenR); - node.body = this.parseStatement(false); - this.state.labels.pop(); - return this.finishNode(node, type); -}; - -// Parse a list of variable declarations. - -pp.parseVar = function (node, isFor, kind) { - node.declarations = []; - node.kind = kind.keyword; - for (;;) { - const decl = this.startNode(); - this.parseVarHead(decl); - if (this.eat(tt.eq)) { - decl.init = this.parseMaybeAssign(isFor); - } else if (kind === tt._const && !(this.match(tt._in) || this.isContextual("of"))) { - this.unexpected(); - } else if (decl.id.type !== "Identifier" && !(isFor && (this.match(tt._in) || this.isContextual("of")))) { - this.raise(this.state.lastTokEnd, "Complex binding patterns require an initialization value"); - } else { - decl.init = null; - } - node.declarations.push(this.finishNode(decl, "VariableDeclarator")); - if (!this.eat(tt.comma)) break; - } - return node; -}; - -pp.parseVarHead = function (decl) { - decl.id = this.parseBindingAtom(); - this.checkLVal(decl.id, true, undefined, "variable declaration"); -}; - -// Parse a function declaration or literal (depending on the -// `isStatement` parameter). - -pp.parseFunction = function (node, isStatement, allowExpressionBody, isAsync, optionalId) { - const oldInMethod = this.state.inMethod; - this.state.inMethod = false; - - this.initFunction(node, isAsync); - - if (this.match(tt.star)) { - if (node.async && !this.hasPlugin("asyncGenerators")) { - this.unexpected(); - } else { - node.generator = true; - this.next(); - } - } - - if (isStatement && !optionalId && !this.match(tt.name) && !this.match(tt._yield)) { - this.unexpected(); - } - - if (this.match(tt.name) || this.match(tt._yield)) { - node.id = this.parseBindingIdentifier(); - } - - this.parseFunctionParams(node); - this.parseFunctionBody(node, allowExpressionBody); - - this.state.inMethod = oldInMethod; - - return this.finishNode(node, isStatement ? "FunctionDeclaration" : "FunctionExpression"); -}; - -pp.parseFunctionParams = function (node) { - this.expect(tt.parenL); - node.params = this.parseBindingList(tt.parenR); -}; - -// Parse a class declaration or literal (depending on the -// `isStatement` parameter). - -pp.parseClass = function (node, isStatement, optionalId) { - this.next(); - this.takeDecorators(node); - this.parseClassId(node, isStatement, optionalId); - this.parseClassSuper(node); - this.parseClassBody(node); - return this.finishNode(node, isStatement ? "ClassDeclaration" : "ClassExpression"); -}; - -pp.isClassProperty = function () { - return this.match(tt.eq) || this.match(tt.semi) || this.match(tt.braceR); -}; - -pp.isClassMethod = function () { - return this.match(tt.parenL); -}; - -pp.isNonstaticConstructor = function (method) { - return !method.computed && !method.static && ( - (method.key.name === "constructor") || // Identifier - (method.key.value === "constructor") // Literal - ); -}; - -pp.parseClassBody = function (node) { - // class bodies are implicitly strict - const oldStrict = this.state.strict; - this.state.strict = true; - - let hadConstructor = false; - let decorators = []; - const classBody = this.startNode(); - - classBody.body = []; - - this.expect(tt.braceL); - - while (!this.eat(tt.braceR)) { - if (this.eat(tt.semi)) { - if (decorators.length > 0) { - this.raise(this.state.lastTokEnd, "Decorators must not be followed by a semicolon"); - } - continue; - } - - if (this.match(tt.at)) { - decorators.push(this.parseDecorator()); - continue; - } - - const method = this.startNode(); - - // steal the decorators if there are any - if (decorators.length) { - method.decorators = decorators; - decorators = []; - } - - method.static = false; - if (this.match(tt.name) && this.state.value === "static") { - const key = this.parseIdentifier(true); // eats 'static' - if (this.isClassMethod()) { - // a method named 'static' - method.kind = "method"; - method.computed = false; - method.key = key; - this.parseClassMethod(classBody, method, false, false); - continue; - } else if (this.isClassProperty()) { - // a property named 'static' - method.computed = false; - method.key = key; - classBody.body.push(this.parseClassProperty(method)); - continue; - } - // otherwise something static - method.static = true; - } - - if (this.eat(tt.star)) { - // a generator - method.kind = "method"; - this.parsePropertyName(method); - if (this.isNonstaticConstructor(method)) { - this.raise(method.key.start, "Constructor can't be a generator"); - } - if (!method.computed && method.static && (method.key.name === "prototype" || method.key.value === "prototype")) { - this.raise(method.key.start, "Classes may not have static property named prototype"); - } - this.parseClassMethod(classBody, method, true, false); - } else { - const isSimple = this.match(tt.name); - const key = this.parsePropertyName(method); - if (!method.computed && method.static && (method.key.name === "prototype" || method.key.value === "prototype")) { - this.raise(method.key.start, "Classes may not have static property named prototype"); - } - if (this.isClassMethod()) { - // a normal method - if (this.isNonstaticConstructor(method)) { - if (hadConstructor) { - this.raise(key.start, "Duplicate constructor in the same class"); - } else if (method.decorators) { - this.raise(method.start, "You can't attach decorators to a class constructor"); + } else if (node.declaration) { + // Exported declarations + if (node.declaration.type === "FunctionDeclaration" || node.declaration.type === "ClassDeclaration") { + this.checkDuplicateExports(node, node.declaration.id.name); + } else if (node.declaration.type === "VariableDeclaration") { + for (const declaration of node.declaration.declarations) { + this.checkDeclaration(declaration.id); } - hadConstructor = true; - method.kind = "constructor"; - } else { - method.kind = "method"; } - this.parseClassMethod(classBody, method, false, false); - } else if (this.isClassProperty()) { - // a normal property - if (this.isNonstaticConstructor(method)) { - this.raise(method.key.start, "Classes may not have a non-static field named 'constructor'"); + } + } + + if (this.state.decorators.length) { + const isClass = node.declaration && (node.declaration.type === "ClassDeclaration" || node.declaration.type === "ClassExpression"); + if (!node.declaration || !isClass) { + this.raise(node.start, "You can only use decorators on an export when exporting a class"); + } + this.takeDecorators(node.declaration); + } + } + + checkDeclaration(node) { + if (node.type === "ObjectPattern") { + for (const prop of node.properties) { + this.checkDeclaration(prop); + } + } else if (node.type === "ArrayPattern") { + for (const elem of node.elements) { + if (elem) { + this.checkDeclaration(elem); } - classBody.body.push(this.parseClassProperty(method)); - } else if (isSimple && key.name === "async" && !this.isLineTerminator()) { - // an async method - const isGenerator = this.hasPlugin("asyncGenerators") && this.eat(tt.star); - method.kind = "method"; - this.parsePropertyName(method); - if (this.isNonstaticConstructor(method)) { - this.raise(method.key.start, "Constructor can't be an async function"); - } - this.parseClassMethod(classBody, method, isGenerator, true); - } else if (isSimple && (key.name === "get" || key.name === "set") && !(this.isLineTerminator() && this.match(tt.star))) { // `get\n*` is an uninitialized property named 'get' followed by a generator. - // a getter or setter - method.kind = key.name; - this.parsePropertyName(method); - if (this.isNonstaticConstructor(method)) { - this.raise(method.key.start, "Constructor can't have get/set modifier"); - } - this.parseClassMethod(classBody, method, false, false); - this.checkGetterSetterParamCount(method); - } else if (this.isLineTerminator()) { - // an uninitialized class property (due to ASI, since we don't otherwise recognize the next token) - if (this.isNonstaticConstructor(method)) { - this.raise(method.key.start, "Classes may not have a non-static field named 'constructor'"); - } - classBody.body.push(this.parseClassProperty(method)); + } + } else if (node.type === "ObjectProperty") { + this.checkDeclaration(node.value); + } else if (node.type === "RestElement") { + this.checkDeclaration(node.argument); + } else if (node.type === "Identifier") { + this.checkDuplicateExports(node, node.name); + } + } + + checkDuplicateExports(node, name) { + if (this.state.exportedIdentifiers.indexOf(name) > -1) { + this.raiseDuplicateExportError(node, name); + } + this.state.exportedIdentifiers.push(name); + } + + raiseDuplicateExportError(node, name) { + this.raise(node.start, name === "default" ? + "Only one default export allowed per module." : + `\`${name}\` has already been exported. Exported identifiers must be unique.` + ); + } + + // Parses a comma-separated list of module exports. + + parseExportSpecifiers() { + const nodes = []; + let first = true; + let needsFrom; + + // export { x, y as z } [from '...'] + this.expect(tt.braceL); + + while (!this.eat(tt.braceR)) { + if (first) { + first = false; } else { - this.unexpected(); + this.expect(tt.comma); + if (this.eat(tt.braceR)) break; } + + const isDefault = this.match(tt._default); + if (isDefault && !needsFrom) needsFrom = true; + + const node = this.startNode(); + node.local = this.parseIdentifier(isDefault); + node.exported = this.eatContextual("as") ? this.parseIdentifier(true) : node.local.__clone(); + nodes.push(this.finishNode(node, "ExportSpecifier")); } - } - if (decorators.length) { - this.raise(this.state.start, "You have trailing decorators with no method"); - } - - node.body = this.finishNode(classBody, "ClassBody"); - - this.state.strict = oldStrict; -}; - -pp.parseClassProperty = function (node) { - const noPluginMsg = "You can only use Class Properties when the 'classProperties' plugin is enabled."; - if (!node.typeAnnotation && !this.hasPlugin("classProperties")) { - this.raise(node.start, noPluginMsg); - } - - if (this.match(tt.eq)) { - if (!this.hasPlugin("classProperties")) this.raise(this.state.start, noPluginMsg); - this.next(); - node.value = this.parseMaybeAssign(); - } else { - node.value = null; - } - this.semicolon(); - return this.finishNode(node, "ClassProperty"); -}; - -pp.parseClassMethod = function (classBody, method, isGenerator, isAsync) { - this.parseMethod(method, isGenerator, isAsync); - classBody.body.push(this.finishNode(method, "ClassMethod")); -}; - -pp.parseClassId = function (node, isStatement, optionalId) { - if (this.match(tt.name)) { - node.id = this.parseIdentifier(); - } else { - if (optionalId || !isStatement) { - node.id = null; - } else { + // https://github.com/ember-cli/ember-cli/pull/3739 + if (needsFrom && !this.isContextual("from")) { this.unexpected(); } + + return nodes; } -}; -pp.parseClassSuper = function (node) { - node.superClass = this.eat(tt._extends) ? this.parseExprSubscripts() : null; -}; + // Parses import declaration. -// Parses module export declaration. + parseImport(node) { + this.eat(tt._import); -pp.parseExport = function (node) { - this.eat(tt._export); - - // export * from '...' - if (this.match(tt.star)) { - const specifier = this.startNode(); - this.next(); - if (this.hasPlugin("exportExtensions") && this.eatContextual("as")) { - specifier.exported = this.parseIdentifier(true); - node.specifiers = [this.finishNode(specifier, "ExportNamespaceSpecifier")]; - this.parseExportSpecifiersMaybe(node); - this.parseExportFrom(node, true); + // import '...' + if (this.match(tt.string)) { + node.specifiers = []; + node.source = this.parseExprAtom(); } else { - this.parseExportFrom(node, true); - return this.finishNode(node, "ExportAllDeclaration"); + node.specifiers = []; + this.parseImportSpecifiers(node); + this.expectContextual("from"); + node.source = this.match(tt.string) ? this.parseExprAtom() : this.unexpected(); } - } else if (this.hasPlugin("exportExtensions") && this.isExportDefaultSpecifier()) { - const specifier = this.startNode(); - specifier.exported = this.parseIdentifier(true); - node.specifiers = [this.finishNode(specifier, "ExportDefaultSpecifier")]; - if (this.match(tt.comma) && this.lookahead().type === tt.star) { - this.expect(tt.comma); + this.semicolon(); + return this.finishNode(node, "ImportDeclaration"); + } + + // Parses a comma-separated list of module imports. + + parseImportSpecifiers(node) { + let first = true; + if (this.match(tt.name)) { + // import defaultObj, { x, y as z } from '...' + const startPos = this.state.start; + const startLoc = this.state.startLoc; + node.specifiers.push(this.parseImportSpecifierDefault(this.parseIdentifier(), startPos, startLoc)); + if (!this.eat(tt.comma)) return; + } + + if (this.match(tt.star)) { const specifier = this.startNode(); - this.expect(tt.star); + this.next(); this.expectContextual("as"); - specifier.exported = this.parseIdentifier(); - node.specifiers.push(this.finishNode(specifier, "ExportNamespaceSpecifier")); - } else { - this.parseExportSpecifiersMaybe(node); + specifier.local = this.parseIdentifier(); + this.checkLVal(specifier.local, true, undefined, "import namespace specifier"); + node.specifiers.push(this.finishNode(specifier, "ImportNamespaceSpecifier")); + return; } - this.parseExportFrom(node, true); - } else if (this.eat(tt._default)) { // export default ... - let expr = this.startNode(); - let needsSemi = false; - if (this.eat(tt._function)) { - expr = this.parseFunction(expr, true, false, false, true); - } else if ( - this.isContextual("async") && - this.lookahead().type === tt._function - ) { // async function declaration - this.eatContextual("async"); - this.eat(tt._function); - expr = this.parseFunction(expr, true, false, true, true); - } else if (this.match(tt._class)) { - expr = this.parseClass(expr, true, true); - } else { - needsSemi = true; - expr = this.parseMaybeAssign(); - } - node.declaration = expr; - if (needsSemi) this.semicolon(); - this.checkExport(node, true, true); - return this.finishNode(node, "ExportDefaultDeclaration"); - } else if (this.shouldParseExportDeclaration()) { - node.specifiers = []; - node.source = null; - node.declaration = this.parseExportDeclaration(node); - } else { // export { x, y as z } [from '...'] - node.declaration = null; - node.specifiers = this.parseExportSpecifiers(); - this.parseExportFrom(node); - } - this.checkExport(node, true); - return this.finishNode(node, "ExportNamedDeclaration"); -}; -pp.parseExportDeclaration = function () { - return this.parseStatement(true); -}; - -pp.isExportDefaultSpecifier = function () { - if (this.match(tt.name)) { - return this.state.value !== "type" - && this.state.value !== "async" - && this.state.value !== "interface"; - } - - if (!this.match(tt._default)) { - return false; - } - - const lookahead = this.lookahead(); - return lookahead.type === tt.comma || (lookahead.type === tt.name && lookahead.value === "from"); -}; - -pp.parseExportSpecifiersMaybe = function (node) { - if (this.eat(tt.comma)) { - node.specifiers = node.specifiers.concat(this.parseExportSpecifiers()); - } -}; - -pp.parseExportFrom = function (node, expect?) { - if (this.eatContextual("from")) { - node.source = this.match(tt.string) ? this.parseExprAtom() : this.unexpected(); - this.checkExport(node); - } else { - if (expect) { - this.unexpected(); - } else { - node.source = null; - } - } - - this.semicolon(); -}; - -pp.shouldParseExportDeclaration = function (): boolean { - return this.state.type.keyword === "var" - || this.state.type.keyword === "const" - || this.state.type.keyword === "let" - || this.state.type.keyword === "function" - || this.state.type.keyword === "class" - || this.isContextual("async"); -}; - -pp.checkExport = function (node, checkNames, isDefault) { - if (checkNames) { - // Check for duplicate exports - if (isDefault) { - // Default exports - this.checkDuplicateExports(node, "default"); - } else if (node.specifiers && node.specifiers.length) { - // Named exports - for (const specifier of node.specifiers) { - this.checkDuplicateExports(specifier, specifier.exported.name); - } - } else if (node.declaration) { - // Exported declarations - if (node.declaration.type === "FunctionDeclaration" || node.declaration.type === "ClassDeclaration") { - this.checkDuplicateExports(node, node.declaration.id.name); - } else if (node.declaration.type === "VariableDeclaration") { - for (const declaration of node.declaration.declarations) { - this.checkDeclaration(declaration.id); + this.expect(tt.braceL); + while (!this.eat(tt.braceR)) { + if (first) { + first = false; + } else { + // Detect an attempt to deep destructure + if (this.eat(tt.colon)) { + this.unexpected(null, "ES2015 named imports do not destructure. Use another statement for destructuring after the import."); } + + this.expect(tt.comma); + if (this.eat(tt.braceR)) break; } + + this.parseImportSpecifier(node); } } - if (this.state.decorators.length) { - const isClass = node.declaration && (node.declaration.type === "ClassDeclaration" || node.declaration.type === "ClassExpression"); - if (!node.declaration || !isClass) { - this.raise(node.start, "You can only use decorators on an export when exporting a class"); - } - this.takeDecorators(node.declaration); - } -}; - -pp.checkDeclaration = function(node) { - if (node.type === "ObjectPattern") { - for (const prop of node.properties) { - this.checkDeclaration(prop); - } - } else if (node.type === "ArrayPattern") { - for (const elem of node.elements) { - if (elem) { - this.checkDeclaration(elem); - } - } - } else if (node.type === "ObjectProperty") { - this.checkDeclaration(node.value); - } else if (node.type === "RestElement") { - this.checkDeclaration(node.argument); - } else if (node.type === "Identifier") { - this.checkDuplicateExports(node, node.name); - } -}; - -pp.checkDuplicateExports = function(node, name) { - if (this.state.exportedIdentifiers.indexOf(name) > -1) { - this.raiseDuplicateExportError(node, name); - } - this.state.exportedIdentifiers.push(name); -}; - -pp.raiseDuplicateExportError = function(node, name) { - this.raise(node.start, name === "default" ? - "Only one default export allowed per module." : - `\`${name}\` has already been exported. Exported identifiers must be unique.` - ); -}; - -// Parses a comma-separated list of module exports. - -pp.parseExportSpecifiers = function () { - const nodes = []; - let first = true; - let needsFrom; - - // export { x, y as z } [from '...'] - this.expect(tt.braceL); - - while (!this.eat(tt.braceR)) { - if (first) { - first = false; - } else { - this.expect(tt.comma); - if (this.eat(tt.braceR)) break; - } - - const isDefault = this.match(tt._default); - if (isDefault && !needsFrom) needsFrom = true; - - const node = this.startNode(); - node.local = this.parseIdentifier(isDefault); - node.exported = this.eatContextual("as") ? this.parseIdentifier(true) : node.local.__clone(); - nodes.push(this.finishNode(node, "ExportSpecifier")); - } - - // https://github.com/ember-cli/ember-cli/pull/3739 - if (needsFrom && !this.isContextual("from")) { - this.unexpected(); - } - - return nodes; -}; - -// Parses import declaration. - -pp.parseImport = function (node) { - this.eat(tt._import); - - // import '...' - if (this.match(tt.string)) { - node.specifiers = []; - node.source = this.parseExprAtom(); - } else { - node.specifiers = []; - this.parseImportSpecifiers(node); - this.expectContextual("from"); - node.source = this.match(tt.string) ? this.parseExprAtom() : this.unexpected(); - } - this.semicolon(); - return this.finishNode(node, "ImportDeclaration"); -}; - -// Parses a comma-separated list of module imports. - -pp.parseImportSpecifiers = function (node) { - let first = true; - if (this.match(tt.name)) { - // import defaultObj, { x, y as z } from '...' - const startPos = this.state.start; - const startLoc = this.state.startLoc; - node.specifiers.push(this.parseImportSpecifierDefault(this.parseIdentifier(), startPos, startLoc)); - if (!this.eat(tt.comma)) return; - } - - if (this.match(tt.star)) { + parseImportSpecifier(node) { const specifier = this.startNode(); - this.next(); - this.expectContextual("as"); - specifier.local = this.parseIdentifier(); - this.checkLVal(specifier.local, true, undefined, "import namespace specifier"); - node.specifiers.push(this.finishNode(specifier, "ImportNamespaceSpecifier")); - return; - } - - this.expect(tt.braceL); - while (!this.eat(tt.braceR)) { - if (first) { - first = false; + specifier.imported = this.parseIdentifier(true); + if (this.eatContextual("as")) { + specifier.local = this.parseIdentifier(); } else { - // Detect an attempt to deep destructure - if (this.eat(tt.colon)) { - this.unexpected(null, "ES2015 named imports do not destructure. Use another statement for destructuring after the import."); - } - - this.expect(tt.comma); - if (this.eat(tt.braceR)) break; + this.checkReservedWord(specifier.imported.name, specifier.start, true, true); + specifier.local = specifier.imported.__clone(); } - - this.parseImportSpecifier(node); + this.checkLVal(specifier.local, true, undefined, "import specifier"); + node.specifiers.push(this.finishNode(specifier, "ImportSpecifier")); } -}; -pp.parseImportSpecifier = function (node) { - const specifier = this.startNode(); - specifier.imported = this.parseIdentifier(true); - if (this.eatContextual("as")) { - specifier.local = this.parseIdentifier(); - } else { - this.checkReservedWord(specifier.imported.name, specifier.start, true, true); - specifier.local = specifier.imported.__clone(); + parseImportSpecifierDefault(id, startPos, startLoc) { + const node = this.startNodeAt(startPos, startLoc); + node.local = id; + this.checkLVal(node.local, true, undefined, "default import specifier"); + return this.finishNode(node, "ImportDefaultSpecifier"); } - this.checkLVal(specifier.local, true, undefined, "import specifier"); - node.specifiers.push(this.finishNode(specifier, "ImportSpecifier")); -}; - -pp.parseImportSpecifierDefault = function (id, startPos, startLoc) { - const node = this.startNodeAt(startPos, startLoc); - node.local = id; - this.checkLVal(node.local, true, undefined, "default import specifier"); - return this.finishNode(node, "ImportDefaultSpecifier"); -}; +} diff --git a/src/parser/util.js b/src/parser/util.js index 86724e67c6..532e3fe7a4 100644 --- a/src/parser/util.js +++ b/src/parser/util.js @@ -1,88 +1,88 @@ import { types as tt } from "../tokenizer/types"; -import Parser from "./index"; +import Tokenizer from "../tokenizer"; import { lineBreak } from "../util/whitespace"; -const pp = Parser.prototype; - // ## Parser utilities -// TODO +export default class UtilParser extends Tokenizer { + // TODO -pp.addExtra = function (node, key, val) { - if (!node) return; + addExtra(node, key, val) { + if (!node) return; - const extra = node.extra = node.extra || {}; - extra[key] = val; -}; - -// TODO - -pp.isRelational = function (op) { - return this.match(tt.relational) && this.state.value === op; -}; - -// TODO - -pp.expectRelational = function (op) { - if (this.isRelational(op)) { - this.next(); - } else { - this.unexpected(null, tt.relational); + const extra = node.extra = node.extra || {}; + extra[key] = val; } -}; -// Tests whether parsed token is a contextual keyword. + // TODO -pp.isContextual = function (name) { - return this.match(tt.name) && this.state.value === name; -}; - -// Consumes contextual keyword if possible. - -pp.eatContextual = function (name) { - return this.state.value === name && this.eat(tt.name); -}; - -// Asserts that following token is given contextual keyword. - -pp.expectContextual = function (name, message) { - if (!this.eatContextual(name)) this.unexpected(null, message); -}; - -// Test whether a semicolon can be inserted at the current position. - -pp.canInsertSemicolon = function () { - return this.match(tt.eof) || - this.match(tt.braceR) || - lineBreak.test(this.input.slice(this.state.lastTokEnd, this.state.start)); -}; - -// TODO - -pp.isLineTerminator = function () { - return this.eat(tt.semi) || this.canInsertSemicolon(); -}; - -// Consume a semicolon, or, failing that, see if we are allowed to -// pretend that there is a semicolon at this position. - -pp.semicolon = function () { - if (!this.isLineTerminator()) this.unexpected(null, tt.semi); -}; - -// Expect a token of a given type. If found, consume it, otherwise, -// raise an unexpected token error at given pos. - -pp.expect = function (type, pos) { - return this.eat(type) || this.unexpected(pos, type); -}; - -// Raise an unexpected token error. Can take the expected token type -// instead of a message string. - -pp.unexpected = function (pos, messageOrType = "Unexpected token") { - if (messageOrType && typeof messageOrType === "object" && messageOrType.label) { - messageOrType = `Unexpected token, expected ${messageOrType.label}`; + isRelational(op) { + return this.match(tt.relational) && this.state.value === op; } - this.raise(pos != null ? pos : this.state.start, messageOrType); -}; + + // TODO + + expectRelational(op) { + if (this.isRelational(op)) { + this.next(); + } else { + this.unexpected(null, tt.relational); + } + } + + // Tests whether parsed token is a contextual keyword. + + isContextual(name) { + return this.match(tt.name) && this.state.value === name; + } + + // Consumes contextual keyword if possible. + + eatContextual(name) { + return this.state.value === name && this.eat(tt.name); + } + + // Asserts that following token is given contextual keyword. + + expectContextual(name, message) { + if (!this.eatContextual(name)) this.unexpected(null, message); + } + + // Test whether a semicolon can be inserted at the current position. + + canInsertSemicolon() { + return this.match(tt.eof) || + this.match(tt.braceR) || + lineBreak.test(this.input.slice(this.state.lastTokEnd, this.state.start)); + } + + // TODO + + isLineTerminator() { + return this.eat(tt.semi) || this.canInsertSemicolon(); + } + + // Consume a semicolon, or, failing that, see if we are allowed to + // pretend that there is a semicolon at this position. + + semicolon() { + if (!this.isLineTerminator()) this.unexpected(null, tt.semi); + } + + // Expect a token of a given type. If found, consume it, otherwise, + // raise an unexpected token error at given pos. + + expect(type, pos) { + return this.eat(type) || this.unexpected(pos, type); + } + + // Raise an unexpected token error. Can take the expected token type + // instead of a message string. + + unexpected(pos, messageOrType = "Unexpected token") { + if (messageOrType && typeof messageOrType === "object" && messageOrType.label) { + messageOrType = `Unexpected token, expected ${messageOrType.label}`; + } + this.raise(pos != null ? pos : this.state.start, messageOrType); + } +} diff --git a/src/tokenizer/index.js b/src/tokenizer/index.js index 74f03f3088..2303175c18 100644 --- a/src/tokenizer/index.js +++ b/src/tokenizer/index.js @@ -8,6 +8,7 @@ import type { Position } from "../util/location"; import { isIdentifierStart, isIdentifierChar, isKeyword } from "../util/identifier"; import { types as tt, keywords as keywordTypes } from "./types"; import { type TokContext, types as ct } from "./context"; +import LocationParser from "../parser/location"; import { SourceLocation } from "../util/location"; import { lineBreak, lineBreakG, isNewLine, nonASCIIwhitespace } from "../util/whitespace"; import State from "./state"; @@ -43,14 +44,8 @@ function codePointToString(code: number): string { } } -export default class Tokenizer { +export default class Tokenizer extends LocationParser { // Forward-declarations - // location.js - +raise: (pos: number, message: string) => empty; - // comments.js (TODO: Better type for the parameter) - +addComment: (comment: Object) => void; - // parser/index.js - +hasPlugin: (name: string) => boolean; // parser/util.js +unexpected: (pos?: ?number, messageOrType?: string | TokenType) => empty; @@ -59,6 +54,7 @@ export default class Tokenizer { input: string; constructor(options: Options, input: string) { + super(); this.state = new State; this.state.init(options, input); }