import {types as tt} from "./tokentype" import {Parser} from "./state" import {lineBreak, skipWhiteSpace} from "./whitespace" import {isIdentifierStart, isIdentifierChar} from "./identifier" import {DestructuringErrors} from "./parseutil" const pp = Parser.prototype // ### 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(node) { let first = true if (!node.body) node.body = [] while (this.type !== tt.eof) { let stmt = this.parseStatement(true, true) node.body.push(stmt) if (first) { if (this.isUseStrict(stmt)) this.setStrict(true) first = false } } this.next() if (this.options.ecmaVersion >= 6) { node.sourceType = this.options.sourceType } return this.finishNode(node, "Program") } const loopLabel = {kind: "loop"}, switchLabel = {kind: "switch"} pp.isLet = function() { if (this.type !== tt.name || this.options.ecmaVersion < 6 || this.value != "let") return false skipWhiteSpace.lastIndex = this.pos let skip = skipWhiteSpace.exec(this.input) let next = this.pos + skip[0].length, nextCh = this.input.charCodeAt(next) if (nextCh === 91 || nextCh == 123) return true // '{' and '[' if (isIdentifierStart(nextCh, true)) { for (var pos = next + 1; isIdentifierChar(this.input.charCodeAt(pos), true); ++pos) {} let ident = this.input.slice(next, pos) if (!this.isKeyword(ident)) return true } return false } // 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) { let starttype = this.type, node = this.startNode(), kind if (this.isLet()) { starttype = tt._var kind = "let" } // 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.options.ecmaVersion >= 6) 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._const: case tt._var: kind = kind || this.value if (!declaration && kind != "var") this.unexpected() return this.parseVarStatement(node, kind) 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.options.allowImportExportEverywhere) { if (!topLevel) this.raise(this.start, "'import' and 'export' may only appear at the top level") if (!this.inModule) this.raise(this.start, "'import' and 'export' may appear only with 'sourceType: module'") } return starttype === tt._import ? this.parseImport(node) : this.parseExport(node) // 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. default: let maybeName = this.value, 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.parseBreakContinueStatement = function(node, keyword) { let isBreak = keyword == "break" this.next() if (this.eat(tt.semi) || this.insertSemicolon()) node.label = null else if (this.type !== tt.name) this.unexpected() else { node.label = this.parseIdent() this.semicolon() } // Verify that there is an actual destination to break or // continue to. for (var i = 0; i < this.labels.length; ++i) { let lab = this.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.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.labels.push(loopLabel) node.body = this.parseStatement(false) this.labels.pop() this.expect(tt._while) node.test = this.parseParenExpression() if (this.options.ecmaVersion >= 6) this.eat(tt.semi) else this.semicolon() 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.labels.push(loopLabel) this.expect(tt.parenL) if (this.type === tt.semi) return this.parseFor(node, null) let isLet = this.isLet() if (this.type === tt._var || this.type === tt._const || isLet) { let init = this.startNode(), kind = isLet ? "let" : this.value this.next() this.parseVar(init, true, kind) this.finishNode(init, "VariableDeclaration") if ((this.type === tt._in || (this.options.ecmaVersion >= 6 && this.isContextual("of"))) && init.declarations.length === 1 && !(kind !== "var" && init.declarations[0].init)) return this.parseForIn(node, init) return this.parseFor(node, init) } let refDestructuringErrors = new DestructuringErrors let init = this.parseExpression(true, refDestructuringErrors) if (this.type === tt._in || (this.options.ecmaVersion >= 6 && this.isContextual("of"))) { this.checkPatternErrors(refDestructuringErrors, true) this.toAssignable(init) this.checkLVal(init) return this.parseForIn(node, init) } else { this.checkExpressionErrors(refDestructuringErrors, true) } 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.inFunction && !this.options.allowReturnOutsideFunction) this.raise(this.start, "'return' outside of function") 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.eat(tt.semi) || this.insertSemicolon()) node.argument = null else { node.argument = this.parseExpression(); this.semicolon() } return this.finishNode(node, "ReturnStatement") } pp.parseSwitchStatement = function(node) { this.next() node.discriminant = this.parseParenExpression() node.cases = [] this.expect(tt.braceL) this.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. for (var cur, sawDefault = false; this.type != tt.braceR;) { if (this.type === tt._case || this.type === tt._default) { let isCase = this.type === 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.raiseRecoverable(this.lastTokStart, "Multiple default clauses") sawDefault = true cur.test = null } this.expect(tt.colon) } else { if (!cur) this.unexpected() cur.consequent.push(this.parseStatement(true)) } } if (cur) this.finishNode(cur, "SwitchCase") this.next() // Closing brace this.labels.pop() return this.finishNode(node, "SwitchStatement") } pp.parseThrowStatement = function(node) { this.next() if (lineBreak.test(this.input.slice(this.lastTokEnd, this.start))) this.raise(this.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.type === tt._catch) { let clause = this.startNode() this.next() this.expect(tt.parenL) clause.param = this.parseBindingAtom() this.checkLVal(clause.param, true) this.expect(tt.parenR) clause.body = this.parseBlock() node.handler = this.finishNode(clause, "CatchClause") } 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.labels.push(loopLabel) node.body = this.parseStatement(false) this.labels.pop() return this.finishNode(node, "WhileStatement") } pp.parseWithStatement = function(node) { if (this.strict) this.raise(this.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 (let i = 0; i < this.labels.length; ++i) if (this.labels[i].name === maybeName) this.raise(expr.start, "Label '" + maybeName + "' is already declared") let kind = this.type.isLoop ? "loop" : this.type === tt._switch ? "switch" : null for (let i = this.labels.length - 1; i >= 0; i--) { let label = this.labels[i] if (label.statementStart == node.start) { label.statementStart = this.start label.kind = kind } else break } this.labels.push({name: maybeName, kind: kind, statementStart: this.start}) node.body = this.parseStatement(true) this.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(allowStrict) { let node = this.startNode(), first = true, oldStrict node.body = [] this.expect(tt.braceL) while (!this.eat(tt.braceR)) { let stmt = this.parseStatement(true) node.body.push(stmt) if (first && allowStrict && this.isUseStrict(stmt)) { oldStrict = this.strict this.setStrict(this.strict = true) } first = false } if (oldStrict === false) this.setStrict(false) return this.finishNode(node, "BlockStatement") } // 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.type === tt.semi ? null : this.parseExpression() this.expect(tt.semi) node.update = this.type === tt.parenR ? null : this.parseExpression() this.expect(tt.parenR) node.body = this.parseStatement(false) this.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) { let type = this.type === tt._in ? "ForInStatement" : "ForOfStatement" this.next() node.left = init node.right = this.parseExpression() this.expect(tt.parenR) node.body = this.parseStatement(false) this.labels.pop() return this.finishNode(node, type) } // Parse a list of variable declarations. pp.parseVar = function(node, isFor, kind) { node.declarations = [] node.kind = kind for (;;) { let decl = this.startNode() this.parseVarId(decl) if (this.eat(tt.eq)) { decl.init = this.parseMaybeAssign(isFor) } else if (kind === "const" && !(this.type === tt._in || (this.options.ecmaVersion >= 6 && this.isContextual("of")))) { this.unexpected() } else if (decl.id.type != "Identifier" && !(isFor && (this.type === tt._in || this.isContextual("of")))) { this.raise(this.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.parseVarId = function(decl) { decl.id = this.parseBindingAtom() this.checkLVal(decl.id, true) } // Parse a function declaration or literal (depending on the // `isStatement` parameter). pp.parseFunction = function(node, isStatement, allowExpressionBody) { this.initFunction(node) if (this.options.ecmaVersion >= 6) node.generator = this.eat(tt.star) var oldInGen = this.inGenerator this.inGenerator = node.generator if (isStatement || this.type === tt.name) node.id = this.parseIdent() this.parseFunctionParams(node) this.parseFunctionBody(node, allowExpressionBody) this.inGenerator = oldInGen return this.finishNode(node, isStatement ? "FunctionDeclaration" : "FunctionExpression") } pp.parseFunctionParams = function(node) { this.expect(tt.parenL) node.params = this.parseBindingList(tt.parenR, false, false, true) } // Parse a class declaration or literal (depending on the // `isStatement` parameter). pp.parseClass = function(node, isStatement) { this.next() this.parseClassId(node, isStatement) this.parseClassSuper(node) let classBody = this.startNode() let hadConstructor = false classBody.body = [] this.expect(tt.braceL) while (!this.eat(tt.braceR)) { if (this.eat(tt.semi)) continue let method = this.startNode() let isGenerator = this.eat(tt.star) let isMaybeStatic = this.type === tt.name && this.value === "static" this.parsePropertyName(method) method.static = isMaybeStatic && this.type !== tt.parenL if (method.static) { if (isGenerator) this.unexpected() isGenerator = this.eat(tt.star) this.parsePropertyName(method) } method.kind = "method" let isGetSet = false if (!method.computed) { let {key} = method if (!isGenerator && key.type === "Identifier" && this.type !== tt.parenL && (key.name === "get" || key.name === "set")) { isGetSet = true method.kind = key.name key = this.parsePropertyName(method) } if (!method.static && (key.type === "Identifier" && key.name === "constructor" || key.type === "Literal" && key.value === "constructor")) { if (hadConstructor) this.raise(key.start, "Duplicate constructor in the same class") if (isGetSet) this.raise(key.start, "Constructor can't have get/set modifier") if (isGenerator) this.raise(key.start, "Constructor can't be a generator") method.kind = "constructor" hadConstructor = true } } this.parseClassMethod(classBody, method, isGenerator) if (isGetSet) { let paramCount = method.kind === "get" ? 0 : 1 if (method.value.params.length !== paramCount) { let start = method.value.start if (method.kind === "get") this.raiseRecoverable(start, "getter should have no params") else this.raiseRecoverable(start, "setter should have exactly one param") } if (method.kind === "set" && method.value.params[0].type === "RestElement") this.raise(method.value.params[0].start, "Setter cannot use rest params") } } node.body = this.finishNode(classBody, "ClassBody") return this.finishNode(node, isStatement ? "ClassDeclaration" : "ClassExpression") } pp.parseClassMethod = function(classBody, method, isGenerator) { method.value = this.parseMethod(isGenerator) classBody.body.push(this.finishNode(method, "MethodDefinition")) } pp.parseClassId = function(node, isStatement) { node.id = this.type === tt.name ? this.parseIdent() : isStatement ? this.unexpected() : null } pp.parseClassSuper = function(node) { node.superClass = this.eat(tt._extends) ? this.parseExprSubscripts() : null } // Parses module export declaration. pp.parseExport = function(node) { this.next() // export * from '...' if (this.eat(tt.star)) { this.expectContextual("from") node.source = this.type === tt.string ? this.parseExprAtom() : this.unexpected() this.semicolon() return this.finishNode(node, "ExportAllDeclaration") } if (this.eat(tt._default)) { // export default ... let parens = this.type == tt.parenL let expr = this.parseMaybeAssign() let needsSemi = true if (!parens && (expr.type == "FunctionExpression" || expr.type == "ClassExpression")) { needsSemi = false if (expr.id) { expr.type = expr.type == "FunctionExpression" ? "FunctionDeclaration" : "ClassDeclaration" } } node.declaration = expr if (needsSemi) this.semicolon() return this.finishNode(node, "ExportDefaultDeclaration") } // export var|const|let|function|class ... if (this.shouldParseExportStatement()) { node.declaration = this.parseStatement(true) node.specifiers = [] node.source = null } else { // export { x, y as z } [from '...'] node.declaration = null node.specifiers = this.parseExportSpecifiers() if (this.eatContextual("from")) { node.source = this.type === tt.string ? this.parseExprAtom() : this.unexpected() } else { // check for keywords used as local names for (let i = 0; i < node.specifiers.length; i++) { if (this.keywords.test(node.specifiers[i].local.name) || this.reservedWords.test(node.specifiers[i].local.name)) { this.unexpected(node.specifiers[i].local.start) } } node.source = null } this.semicolon() } return this.finishNode(node, "ExportNamedDeclaration") } pp.shouldParseExportStatement = function() { return this.type.keyword || this.isLet() } // Parses a comma-separated list of module exports. pp.parseExportSpecifiers = function() { let nodes = [], first = true // export { x, y as z } [from '...'] this.expect(tt.braceL) while (!this.eat(tt.braceR)) { if (!first) { this.expect(tt.comma) if (this.afterTrailingComma(tt.braceR)) break } else first = false let node = this.startNode() node.local = this.parseIdent(this.type === tt._default) node.exported = this.eatContextual("as") ? this.parseIdent(true) : node.local nodes.push(this.finishNode(node, "ExportSpecifier")) } return nodes } // Parses import declaration. pp.parseImport = function(node) { this.next() // import '...' if (this.type === tt.string) { node.specifiers = empty node.source = this.parseExprAtom() } else { node.specifiers = this.parseImportSpecifiers() this.expectContextual("from") node.source = this.type === tt.string ? this.parseExprAtom() : this.unexpected() } this.semicolon() return this.finishNode(node, "ImportDeclaration") } // Parses a comma-separated list of module imports. pp.parseImportSpecifiers = function() { let nodes = [], first = true if (this.type === tt.name) { // import defaultObj, { x, y as z } from '...' let node = this.startNode() node.local = this.parseIdent() this.checkLVal(node.local, true) nodes.push(this.finishNode(node, "ImportDefaultSpecifier")) if (!this.eat(tt.comma)) return nodes } if (this.type === tt.star) { let node = this.startNode() this.next() this.expectContextual("as") node.local = this.parseIdent() this.checkLVal(node.local, true) nodes.push(this.finishNode(node, "ImportNamespaceSpecifier")) return nodes } this.expect(tt.braceL) while (!this.eat(tt.braceR)) { if (!first) { this.expect(tt.comma) if (this.afterTrailingComma(tt.braceR)) break } else first = false let node = this.startNode() node.imported = this.parseIdent(true) if (this.eatContextual("as")) { node.local = this.parseIdent() } else { node.local = node.imported if (this.isKeyword(node.local.name)) this.unexpected(node.local.start) if (this.reservedWordsStrict.test(node.local.name)) this.raise(node.local.start, "The keyword '" + node.local.name + "' is reserved") } this.checkLVal(node.local, true) nodes.push(this.finishNode(node, "ImportSpecifier")) } return nodes }