// @flow import * as N from "../types"; import { types as tt, type TokenType } from "../tokenizer/types"; import ExpressionParser from "./expression"; import { Errors } from "./error"; import { isIdentifierChar, isIdentifierStart, keywordRelationalOperator, } from "../util/identifier"; import { lineBreak } from "../util/whitespace"; import * as charCodes from "charcodes"; import { BIND_CLASS, BIND_LEXICAL, BIND_VAR, BIND_FUNCTION, SCOPE_CLASS, SCOPE_FUNCTION, SCOPE_OTHER, SCOPE_SIMPLE_CATCH, SCOPE_SUPER, CLASS_ELEMENT_OTHER, CLASS_ELEMENT_INSTANCE_GETTER, CLASS_ELEMENT_INSTANCE_SETTER, CLASS_ELEMENT_STATIC_GETTER, CLASS_ELEMENT_STATIC_SETTER, type BindingTypes, } from "../util/scopeflags"; import { ExpressionErrors } from "./util"; import { PARAM, functionFlags } from "../util/production-parameter"; const loopLabel = { kind: "loop" }, switchLabel = { kind: "switch" }; const FUNC_NO_FLAGS = 0b000, FUNC_STATEMENT = 0b001, FUNC_HANGING_STATEMENT = 0b010, FUNC_NULLABLE_ID = 0b100; export default class StatementParser extends ExpressionParser { // ### 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. parseTopLevel(file: N.File, program: N.Program): N.File { program.sourceType = this.options.sourceType; program.interpreter = this.parseInterpreterDirective(); this.parseBlockBody(program, true, true, tt.eof); if ( this.inModule && !this.options.allowUndeclaredExports && this.scope.undefinedExports.size > 0 ) { for (const [name] of Array.from(this.scope.undefinedExports)) { const pos = this.scope.undefinedExports.get(name); // $FlowIssue this.raise(pos, Errors.ModuleExportUndefined, name); } } file.program = this.finishNode(program, "Program"); file.comments = this.state.comments; if (this.options.tokens) file.tokens = this.tokens; return this.finishNode(file, "File"); } // TODO stmtToDirective(stmt: N.Statement): N.Directive { const expr = stmt.expression; const directiveLiteral = this.startNodeAt(expr.start, expr.loc.start); const directive = this.startNodeAt(stmt.start, stmt.loc.start); const raw = this.input.slice(expr.start, expr.end); const val = (directiveLiteral.value = raw.slice(1, -1)); // remove quotes this.addExtra(directiveLiteral, "raw", raw); this.addExtra(directiveLiteral, "rawValue", val); directive.value = this.finishNodeAt( directiveLiteral, "DirectiveLiteral", expr.end, expr.loc.end, ); return this.finishNodeAt(directive, "Directive", stmt.end, stmt.loc.end); } parseInterpreterDirective(): N.InterpreterDirective | null { if (!this.match(tt.interpreterDirective)) { return null; } const node = this.startNode(); node.value = this.state.value; this.next(); return this.finishNode(node, "InterpreterDirective"); } isLet(context: ?string): boolean { if (!this.isContextual("let")) { return false; } const next = this.nextTokenStart(); const nextCh = this.input.charCodeAt(next); // For ambiguous cases, determine if a LexicalDeclaration (or only a // Statement) is allowed here. If context is not empty then only a Statement // is allowed. However, `let [` is an explicit negative lookahead for // ExpressionStatement, so special-case it first. if (nextCh === charCodes.leftSquareBracket) return true; if (context) return false; if (nextCh === charCodes.leftCurlyBrace) return true; if (isIdentifierStart(nextCh)) { let pos = next + 1; while (isIdentifierChar(this.input.charCodeAt(pos))) { ++pos; } const ident = this.input.slice(next, pos); if (!keywordRelationalOperator.test(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. // https://tc39.es/ecma262/#prod-Statement // ImportDeclaration and ExportDeclaration are also handled here so we can throw recoverable errors // when they are not at the top level parseStatement(context: ?string, topLevel?: boolean): N.Statement { if (this.match(tt.at)) { this.parseDecorators(true); } return this.parseStatementContent(context, topLevel); } parseStatementContent(context: ?string, topLevel: ?boolean): N.Statement { let starttype = this.state.type; const node = this.startNode(); let kind; if (this.isLet(context)) { 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: // $FlowFixMe 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 (this.lookaheadCharCode() === charCodes.dot) break; if (context) { if (this.state.strict) { this.raise(this.state.start, Errors.StrictFunction); } else if (context !== "if" && context !== "label") { this.raise(this.state.start, Errors.SloppyFunction); } } return this.parseFunctionStatement(node, false, !context); case tt._class: if (context) 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.state.value; if (context && kind !== "var") { this.raise(this.state.start, Errors.UnexpectedLexicalDeclaration); } 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._import: { const nextTokenCharCode = this.lookaheadCharCode(); if ( nextTokenCharCode === charCodes.leftParenthesis || // import() nextTokenCharCode === charCodes.dot // import.meta ) { break; } } // fall through case tt._export: { if (!this.options.allowImportExportEverywhere && !topLevel) { this.raise(this.state.start, Errors.UnexpectedImportExport); } this.next(); // eat `import`/`export` let result; if (starttype === tt._import) { result = this.parseImport(node); if ( result.type === "ImportDeclaration" && (!result.importKind || result.importKind === "value") ) { this.sawUnambiguousESM = true; } } else { result = this.parseExport(node); if ( (result.type === "ExportNamedDeclaration" && (!result.exportKind || result.exportKind === "value")) || (result.type === "ExportAllDeclaration" && (!result.exportKind || result.exportKind === "value")) || result.type === "ExportDefaultDeclaration" ) { this.sawUnambiguousESM = true; } } this.assertModuleNodeAllowed(node); return result; } default: { if (this.isAsyncFunction()) { if (context) { this.raise( this.state.start, Errors.AsyncFunctionInSingleStatementContext, ); } this.next(); return this.parseFunctionStatement(node, true, !context); } } } // 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, context); } else { return this.parseExpressionStatement(node, expr); } } assertModuleNodeAllowed(node: N.Node): void { if (!this.options.allowImportExportEverywhere && !this.inModule) { this.raiseWithData( node.start, { code: "BABEL_PARSER_SOURCETYPE_MODULE_REQUIRED", }, Errors.ImportOutsideModule, ); } } takeDecorators(node: N.HasDecorators): void { const decorators = this.state.decoratorStack[ this.state.decoratorStack.length - 1 ]; if (decorators.length) { node.decorators = decorators; this.resetStartLocationFromNode(node, decorators[0]); this.state.decoratorStack[this.state.decoratorStack.length - 1] = []; } } canHaveLeadingDecorator(): boolean { return this.match(tt._class); } parseDecorators(allowExport?: boolean): void { const currentContextDecorators = this.state.decoratorStack[ this.state.decoratorStack.length - 1 ]; while (this.match(tt.at)) { const decorator = this.parseDecorator(); currentContextDecorators.push(decorator); } if (this.match(tt._export)) { if (!allowExport) { this.unexpected(); } if ( this.hasPlugin("decorators") && !this.getPluginOption("decorators", "decoratorsBeforeExport") ) { this.raise(this.state.start, Errors.DecoratorExportClass); } } else if (!this.canHaveLeadingDecorator()) { throw this.raise(this.state.start, Errors.UnexpectedLeadingDecorator); } } parseDecorator(): N.Decorator { this.expectOnePlugin(["decorators-legacy", "decorators"]); const node = this.startNode(); this.next(); if (this.hasPlugin("decorators")) { // Every time a decorator class expression is evaluated, a new empty array is pushed onto the stack // So that the decorators of any nested class expressions will be dealt with separately this.state.decoratorStack.push([]); const startPos = this.state.start; const startLoc = this.state.startLoc; let expr: N.Expression; if (this.eat(tt.parenL)) { expr = this.parseExpression(); this.expect(tt.parenR); } else { expr = this.parseIdentifier(false); while (this.eat(tt.dot)) { const node = this.startNodeAt(startPos, startLoc); node.object = expr; node.property = this.parseIdentifier(true); node.computed = false; expr = this.finishNode(node, "MemberExpression"); } } node.expression = this.parseMaybeDecoratorArguments(expr); this.state.decoratorStack.pop(); } else { node.expression = this.parseExprSubscripts(); } return this.finishNode(node, "Decorator"); } parseMaybeDecoratorArguments(expr: N.Expression): N.Expression { if (this.eat(tt.parenL)) { const node = this.startNodeAtNode(expr); node.callee = expr; node.arguments = this.parseCallExpressionArguments(tt.parenR, false); this.toReferencedList(node.arguments); return this.finishNode(node, "CallExpression"); } return expr; } parseBreakContinueStatement( node: N.BreakStatement | N.ContinueStatement, keyword: string, ): N.BreakStatement | N.ContinueStatement { const isBreak = keyword === "break"; this.next(); if (this.isLineTerminator()) { node.label = null; } else { node.label = this.parseIdentifier(); this.semicolon(); } this.verifyBreakContinue(node, keyword); return this.finishNode( node, isBreak ? "BreakStatement" : "ContinueStatement", ); } verifyBreakContinue( node: N.BreakStatement | N.ContinueStatement, keyword: string, ) { const isBreak = keyword === "break"; let i; for (i = 0; i < this.state.labels.length; ++i) { const lab = this.state.labels[i]; 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, Errors.IllegalBreakContinue, keyword); } } parseDebuggerStatement(node: N.DebuggerStatement): N.DebuggerStatement { this.next(); this.semicolon(); return this.finishNode(node, "DebuggerStatement"); } parseHeaderExpression(): N.Expression { this.expect(tt.parenL); const val = this.parseExpression(); this.expect(tt.parenR); return val; } parseDoStatement(node: N.DoWhileStatement): N.DoWhileStatement { this.next(); this.state.labels.push(loopLabel); node.body = // For the smartPipelines plugin: Disable topic references from outer // contexts within the loop body. They are permitted in test expressions, // outside of the loop body. this.withTopicForbiddingContext(() => // Parse the loop body's body. this.parseStatement("do"), ); this.state.labels.pop(); this.expect(tt._while); node.test = this.parseHeaderExpression(); 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: N.Node): N.ForLike { this.next(); this.state.labels.push(loopLabel); let awaitAt = -1; if (this.isAwaitAllowed() && this.eatContextual("await")) { awaitAt = this.state.lastTokStart; } this.scope.enter(SCOPE_OTHER); this.expect(tt.parenL); if (this.match(tt.semi)) { if (awaitAt > -1) { this.unexpected(awaitAt); } return this.parseFor(node, null); } const isLet = this.isLet(); if (this.match(tt._var) || this.match(tt._const) || isLet) { const init = this.startNode(); const kind = isLet ? "let" : this.state.value; this.next(); this.parseVar(init, true, kind); this.finishNode(init, "VariableDeclaration"); if ( (this.match(tt._in) || this.isContextual("of")) && init.declarations.length === 1 ) { return this.parseForIn(node, init, awaitAt); } if (awaitAt > -1) { this.unexpected(awaitAt); } return this.parseFor(node, init); } const refExpressionErrors = new ExpressionErrors(); const init = this.parseExpression(true, refExpressionErrors); if (this.match(tt._in) || this.isContextual("of")) { this.toAssignable(init); const description = this.isContextual("of") ? "for-of statement" : "for-in statement"; this.checkLVal(init, undefined, undefined, description); return this.parseForIn(node, init, awaitAt); } else { this.checkExpressionErrors(refExpressionErrors, true); } if (awaitAt > -1) { this.unexpected(awaitAt); } return this.parseFor(node, init); } parseFunctionStatement( node: N.FunctionDeclaration, isAsync?: boolean, declarationPosition?: boolean, ): N.FunctionDeclaration { this.next(); return this.parseFunction( node, FUNC_STATEMENT | (declarationPosition ? 0 : FUNC_HANGING_STATEMENT), isAsync, ); } parseIfStatement(node: N.IfStatement): N.IfStatement { this.next(); node.test = this.parseHeaderExpression(); node.consequent = this.parseStatement("if"); node.alternate = this.eat(tt._else) ? this.parseStatement("if") : null; return this.finishNode(node, "IfStatement"); } parseReturnStatement(node: N.ReturnStatement): N.ReturnStatement { if (!this.prodParam.hasReturn && !this.options.allowReturnOutsideFunction) { this.raise(this.state.start, Errors.IllegalReturn); } 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: N.SwitchStatement): N.SwitchStatement { this.next(); node.discriminant = this.parseHeaderExpression(); const cases = (node.cases = []); this.expect(tt.braceL); this.state.labels.push(switchLabel); this.scope.enter(SCOPE_OTHER); // 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"); cases.push((cur = this.startNode())); cur.consequent = []; this.next(); if (isCase) { cur.test = this.parseExpression(); } else { if (sawDefault) { this.raise( this.state.lastTokStart, Errors.MultipleDefaultsInSwitch, ); } sawDefault = true; cur.test = null; } this.expect(tt.colon); } else { if (cur) { cur.consequent.push(this.parseStatement(null)); } else { this.unexpected(); } } } this.scope.exit(); if (cur) this.finishNode(cur, "SwitchCase"); this.next(); // Closing brace this.state.labels.pop(); return this.finishNode(node, "SwitchStatement"); } parseThrowStatement(node: N.ThrowStatement): N.ThrowStatement { this.next(); if (this.hasPrecedingLineBreak()) { this.raise(this.state.lastTokEnd, Errors.NewlineAfterThrow); } node.argument = this.parseExpression(); this.semicolon(); return this.finishNode(node, "ThrowStatement"); } parseCatchClauseParam(): N.Pattern { const param = this.parseBindingAtom(); const simple = param.type === "Identifier"; this.scope.enter(simple ? SCOPE_SIMPLE_CATCH : 0); this.checkLVal(param, BIND_LEXICAL, null, "catch clause"); return param; } parseTryStatement(node: N.TryStatement): N.TryStatement { this.next(); node.block = this.parseBlock(); node.handler = null; if (this.match(tt._catch)) { const clause = this.startNode(); this.next(); if (this.match(tt.parenL)) { this.expect(tt.parenL); clause.param = this.parseCatchClauseParam(); this.expect(tt.parenR); } else { clause.param = null; this.scope.enter(SCOPE_OTHER); } clause.body = // For the smartPipelines plugin: Disable topic references from outer // contexts within the catch clause's body. this.withTopicForbiddingContext(() => // Parse the catch clause's body. this.parseBlock(false, false), ); this.scope.exit(); node.handler = this.finishNode(clause, "CatchClause"); } node.finalizer = this.eat(tt._finally) ? this.parseBlock() : null; if (!node.handler && !node.finalizer) { this.raise(node.start, Errors.NoCatchOrFinally); } return this.finishNode(node, "TryStatement"); } parseVarStatement( node: N.VariableDeclaration, kind: "var" | "let" | "const", ): N.VariableDeclaration { this.next(); this.parseVar(node, false, kind); this.semicolon(); return this.finishNode(node, "VariableDeclaration"); } parseWhileStatement(node: N.WhileStatement): N.WhileStatement { this.next(); node.test = this.parseHeaderExpression(); this.state.labels.push(loopLabel); node.body = // For the smartPipelines plugin: // Disable topic references from outer contexts within the loop body. // They are permitted in test expressions, outside of the loop body. this.withTopicForbiddingContext(() => // Parse loop body. this.parseStatement("while"), ); this.state.labels.pop(); return this.finishNode(node, "WhileStatement"); } parseWithStatement(node: N.WithStatement): N.WithStatement { if (this.state.strict) { this.raise(this.state.start, Errors.StrictWith); } this.next(); node.object = this.parseHeaderExpression(); node.body = // For the smartPipelines plugin: // Disable topic references from outer contexts within the with statement's body. // They are permitted in function default-parameter expressions, which are // part of the outer context, outside of the with statement's body. this.withTopicForbiddingContext(() => // Parse the statement body. this.parseStatement("with"), ); return this.finishNode(node, "WithStatement"); } parseEmptyStatement(node: N.EmptyStatement): N.EmptyStatement { this.next(); return this.finishNode(node, "EmptyStatement"); } parseLabeledStatement( node: N.LabeledStatement, maybeName: string, expr: N.Identifier, context: ?string, ): N.LabeledStatement { for (const label of this.state.labels) { if (label.name === maybeName) { this.raise(expr.start, Errors.LabelRedeclaration, maybeName); } } 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( context ? context.indexOf("label") === -1 ? context + "label" : context : "label", ); this.state.labels.pop(); node.label = expr; return this.finishNode(node, "LabeledStatement"); } parseExpressionStatement( node: N.ExpressionStatement, expr: N.Expression, ): N.Statement { 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?: boolean = false, createNewLexicalScope?: boolean = true, afterBlockParse?: (hasStrictModeDirective: boolean) => void, ): N.BlockStatement { const node = this.startNode(); this.expect(tt.braceL); if (createNewLexicalScope) { this.scope.enter(SCOPE_OTHER); } this.parseBlockBody( node, allowDirectives, false, tt.braceR, afterBlockParse, ); if (createNewLexicalScope) { this.scope.exit(); } return this.finishNode(node, "BlockStatement"); } isValidDirective(stmt: N.Statement): boolean { return ( stmt.type === "ExpressionStatement" && stmt.expression.type === "StringLiteral" && !stmt.expression.extra.parenthesized ); } parseBlockBody( node: N.BlockStatementLike, allowDirectives: ?boolean, topLevel: boolean, end: TokenType, afterBlockParse?: (hasStrictModeDirective: boolean) => void, ): void { const body = (node.body = []); const directives = (node.directives = []); this.parseBlockOrModuleBlockBody( body, allowDirectives ? directives : undefined, topLevel, end, afterBlockParse, ); } // Undefined directives means that directives are not allowed. // https://tc39.es/ecma262/#prod-Block // https://tc39.es/ecma262/#prod-ModuleBody parseBlockOrModuleBlockBody( body: N.Statement[], directives: ?(N.Directive[]), topLevel: boolean, end: TokenType, afterBlockParse?: (hasStrictModeDirective: boolean) => void, ): void { const octalPositions = []; const oldStrict = this.state.strict; let hasStrictModeDirective = false; let parsedNonDirective = false; while (!this.match(end)) { // Track octal literals that occur before a "use strict" directive. if (!parsedNonDirective && this.state.octalPositions.length) { octalPositions.push(...this.state.octalPositions); } const stmt = this.parseStatement(null, topLevel); if (directives && !parsedNonDirective && this.isValidDirective(stmt)) { const directive = this.stmtToDirective(stmt); directives.push(directive); if (!hasStrictModeDirective && directive.value.value === "use strict") { hasStrictModeDirective = true; this.setStrict(true); } continue; } parsedNonDirective = true; body.push(stmt); } // Throw an error for any octal literals found before a // "use strict" directive. Strict mode will be set at parse // time for any literals that occur after the directive. if (this.state.strict && octalPositions.length) { for (const pos of octalPositions) { this.raise(pos, Errors.StrictOctalLiteral); } } if (afterBlockParse) { afterBlockParse.call(this, hasStrictModeDirective); } if (!oldStrict) { this.setStrict(false); } this.next(); } // Parse a regular `for` loop. The disambiguation code in // `parseStatement` will already have parsed the init statement or // expression. parseFor( node: N.ForStatement, init: ?(N.VariableDeclaration | N.Expression), ): N.ForStatement { 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 = // For the smartPipelines plugin: Disable topic references from outer // contexts within the loop body. They are permitted in test expressions, // outside of the loop body. this.withTopicForbiddingContext(() => // Parse the loop body. this.parseStatement("for"), ); this.scope.exit(); 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: N.ForInOf, init: N.VariableDeclaration | N.AssignmentPattern, awaitAt: number, ): N.ForInOf { const isForIn = this.match(tt._in); this.next(); if (isForIn) { if (awaitAt > -1) this.unexpected(awaitAt); } else { node.await = awaitAt > -1; } if ( init.type === "VariableDeclaration" && init.declarations[0].init != null && (!isForIn || this.state.strict || init.kind !== "var" || init.declarations[0].id.type !== "Identifier") ) { this.raise( init.start, Errors.ForInOfLoopInitializer, isForIn ? "for-in" : "for-of", ); } else if (init.type === "AssignmentPattern") { this.raise(init.start, Errors.InvalidLhs, "for-loop"); } node.left = init; node.right = isForIn ? this.parseExpression() : this.parseMaybeAssignAllowIn(); this.expect(tt.parenR); node.body = // For the smartPipelines plugin: // Disable topic references from outer contexts within the loop body. // They are permitted in test expressions, outside of the loop body. this.withTopicForbiddingContext(() => // Parse loop body. this.parseStatement("for"), ); this.scope.exit(); this.state.labels.pop(); return this.finishNode(node, isForIn ? "ForInStatement" : "ForOfStatement"); } // Parse a list of variable declarations. parseVar( node: N.VariableDeclaration, isFor: boolean, kind: "var" | "let" | "const", ): N.VariableDeclaration { const declarations = (node.declarations = []); const isTypescript = this.hasPlugin("typescript"); node.kind = kind; for (;;) { const decl = this.startNode(); this.parseVarId(decl, kind); if (this.eat(tt.eq)) { decl.init = isFor ? this.parseMaybeAssignDisallowIn() : this.parseMaybeAssignAllowIn(); } else { if ( kind === "const" && !(this.match(tt._in) || this.isContextual("of")) ) { // `const` with no initializer is allowed in TypeScript. // It could be a declaration like `const x: number;`. if (!isTypescript) { this.raise( this.state.lastTokEnd, Errors.DeclarationMissingInitializer, "Const declarations", ); } } else if ( decl.id.type !== "Identifier" && !(isFor && (this.match(tt._in) || this.isContextual("of"))) ) { this.raise( this.state.lastTokEnd, Errors.DeclarationMissingInitializer, "Complex binding patterns", ); } decl.init = null; } declarations.push(this.finishNode(decl, "VariableDeclarator")); if (!this.eat(tt.comma)) break; } return node; } parseVarId(decl: N.VariableDeclarator, kind: "var" | "let" | "const"): void { decl.id = this.parseBindingAtom(); this.checkLVal( decl.id, kind === "var" ? BIND_VAR : BIND_LEXICAL, undefined, "variable declaration", kind !== "var", ); } // Parse a function declaration or literal (depending on the // `isStatement` parameter). parseFunction( node: T, statement?: number = FUNC_NO_FLAGS, isAsync?: boolean = false, ): T { const isStatement = statement & FUNC_STATEMENT; const isHangingStatement = statement & FUNC_HANGING_STATEMENT; const requireId = !!isStatement && !(statement & FUNC_NULLABLE_ID); this.initFunction(node, isAsync); if (this.match(tt.star) && isHangingStatement) { this.raise(this.state.start, Errors.GeneratorInSingleStatementContext); } node.generator = this.eat(tt.star); if (isStatement) { node.id = this.parseFunctionId(requireId); } const oldMaybeInArrowParameters = this.state.maybeInArrowParameters; const oldYieldPos = this.state.yieldPos; const oldAwaitPos = this.state.awaitPos; this.state.maybeInArrowParameters = false; this.state.yieldPos = -1; this.state.awaitPos = -1; this.scope.enter(SCOPE_FUNCTION); this.prodParam.enter(functionFlags(isAsync, node.generator)); if (!isStatement) { node.id = this.parseFunctionId(); } this.parseFunctionParams(node, /* allowModifiers */ false); // For the smartPipelines plugin: Disable topic references from outer // contexts within the function body. They are permitted in function // default-parameter expressions, outside of the function body. this.withTopicForbiddingContext(() => { // Parse the function body. this.parseFunctionBodyAndFinish( node, isStatement ? "FunctionDeclaration" : "FunctionExpression", ); }); this.prodParam.exit(); this.scope.exit(); if (isStatement && !isHangingStatement) { // We need to register this _after_ parsing the function body // because of TypeScript body-less function declarations, // which shouldn't be added to the scope. this.registerFunctionStatementId(node); } this.state.maybeInArrowParameters = oldMaybeInArrowParameters; this.state.yieldPos = oldYieldPos; this.state.awaitPos = oldAwaitPos; return node; } parseFunctionId(requireId?: boolean): ?N.Identifier { return requireId || this.match(tt.name) ? this.parseIdentifier() : null; } parseFunctionParams(node: N.Function, allowModifiers?: boolean): void { const oldInParameters = this.state.inParameters; this.state.inParameters = true; this.expect(tt.parenL); node.params = this.parseBindingList( tt.parenR, charCodes.rightParenthesis, /* allowEmpty */ false, allowModifiers, ); this.state.inParameters = oldInParameters; this.checkYieldAwaitInDefaultParams(); } registerFunctionStatementId(node: N.Function): void { if (!node.id) return; // If it is a regular function declaration in sloppy mode, then it is // subject to Annex B semantics (BIND_FUNCTION). Otherwise, the binding // mode depends on properties of the current scope (see // treatFunctionsAsVar). this.scope.declareName( node.id.name, this.state.strict || node.generator || node.async ? this.scope.treatFunctionsAsVar ? BIND_VAR : BIND_LEXICAL : BIND_FUNCTION, node.id.start, ); } // Parse a class declaration or literal (depending on the // `isStatement` parameter). parseClass( node: T, isStatement: /* T === ClassDeclaration */ boolean, optionalId?: boolean, ): T { this.next(); this.takeDecorators(node); // A class definition is always strict mode code. const oldStrict = this.state.strict; this.state.strict = true; this.parseClassId(node, isStatement, optionalId); this.parseClassSuper(node); // this.state.strict is restored in parseClassBody node.body = this.parseClassBody(!!node.superClass, oldStrict); return this.finishNode( node, isStatement ? "ClassDeclaration" : "ClassExpression", ); } isClassProperty(): boolean { return this.match(tt.eq) || this.match(tt.semi) || this.match(tt.braceR); } isClassMethod(): boolean { return this.match(tt.parenL); } isNonstaticConstructor(method: N.ClassMethod | N.ClassProperty): boolean { return ( !method.computed && !method.static && (method.key.name === "constructor" || // Identifier method.key.value === "constructor") // String literal ); } // https://tc39.es/ecma262/#prod-ClassBody parseClassBody( constructorAllowsSuper: boolean, oldStrict: boolean, ): N.ClassBody { this.classScope.enter(); const state = { hadConstructor: false }; let decorators: N.Decorator[] = []; const classBody: N.ClassBody = this.startNode(); classBody.body = []; this.expect(tt.braceL); // For the smartPipelines plugin: Disable topic references from outer // contexts within the class body. this.withTopicForbiddingContext(() => { while (!this.match(tt.braceR)) { if (this.eat(tt.semi)) { if (decorators.length > 0) { throw this.raise(this.state.lastTokEnd, Errors.DecoratorSemicolon); } continue; } if (this.match(tt.at)) { decorators.push(this.parseDecorator()); continue; } const member = this.startNode(); // steal the decorators if there are any if (decorators.length) { member.decorators = decorators; this.resetStartLocationFromNode(member, decorators[0]); decorators = []; } this.parseClassMember(classBody, member, state, constructorAllowsSuper); if ( member.kind === "constructor" && member.decorators && member.decorators.length > 0 ) { this.raise(member.start, Errors.DecoratorConstructor); } } }); this.state.strict = oldStrict; this.next(); // eat `}` if (decorators.length) { throw this.raise(this.state.start, Errors.TrailingDecorator); } this.classScope.exit(); return this.finishNode(classBody, "ClassBody"); } // returns true if the current identifier is a method/field name, // false if it is a modifier parseClassMemberFromModifier( classBody: N.ClassBody, member: N.ClassMember, ): boolean { const key = this.parseIdentifier(true); // eats the modifier if (this.isClassMethod()) { const method: N.ClassMethod = (member: any); // a method named like the modifier method.kind = "method"; method.computed = false; method.key = key; method.static = false; this.pushClassMethod( classBody, method, false, false, /* isConstructor */ false, false, ); return true; } else if (this.isClassProperty()) { const prop: N.ClassProperty = (member: any); // a property named like the modifier prop.computed = false; prop.key = key; prop.static = false; classBody.body.push(this.parseClassProperty(prop)); return true; } return false; } parseClassMember( classBody: N.ClassBody, member: N.ClassMember, state: { hadConstructor: boolean }, constructorAllowsSuper: boolean, ): void { const isStatic = this.isContextual("static"); if (isStatic && this.parseClassMemberFromModifier(classBody, member)) { // a class element named 'static' return; } this.parseClassMemberWithIsStatic( classBody, member, state, isStatic, constructorAllowsSuper, ); } parseClassMemberWithIsStatic( classBody: N.ClassBody, member: N.ClassMember, state: { hadConstructor: boolean }, isStatic: boolean, constructorAllowsSuper: boolean, ) { const publicMethod: $FlowSubtype = member; const privateMethod: $FlowSubtype = member; const publicProp: $FlowSubtype = member; const privateProp: $FlowSubtype = member; const method: typeof publicMethod | typeof privateMethod = publicMethod; const publicMember: typeof publicMethod | typeof publicProp = publicMethod; member.static = isStatic; if (this.eat(tt.star)) { // a generator method.kind = "method"; this.parseClassElementName(method); if (method.key.type === "PrivateName") { // Private generator method this.pushClassPrivateMethod(classBody, privateMethod, true, false); return; } if (this.isNonstaticConstructor(publicMethod)) { this.raise(publicMethod.key.start, Errors.ConstructorIsGenerator); } this.pushClassMethod( classBody, publicMethod, true, false, /* isConstructor */ false, false, ); return; } const containsEsc = this.state.containsEsc; const key = this.parseClassElementName(member); const isPrivate = key.type === "PrivateName"; // Check the key is not a computed expression or string literal. const isSimple = key.type === "Identifier"; const maybeQuestionTokenStart = this.state.start; this.parsePostMemberNameModifiers(publicMember); if (this.isClassMethod()) { method.kind = "method"; if (isPrivate) { this.pushClassPrivateMethod(classBody, privateMethod, false, false); return; } // a normal method const isConstructor = this.isNonstaticConstructor(publicMethod); let allowsDirectSuper = false; if (isConstructor) { publicMethod.kind = "constructor"; // TypeScript allows multiple overloaded constructor declarations. if (state.hadConstructor && !this.hasPlugin("typescript")) { this.raise(key.start, Errors.DuplicateConstructor); } state.hadConstructor = true; allowsDirectSuper = constructorAllowsSuper; } this.pushClassMethod( classBody, publicMethod, false, false, isConstructor, allowsDirectSuper, ); } else if (this.isClassProperty()) { if (isPrivate) { this.pushClassPrivateProperty(classBody, privateProp); } else { this.pushClassProperty(classBody, publicProp); } } else if ( isSimple && key.name === "async" && !containsEsc && !this.isLineTerminator() ) { // an async method const isGenerator = this.eat(tt.star); if (publicMember.optional) { this.unexpected(maybeQuestionTokenStart); } method.kind = "method"; // The so-called parsed name would have been "async": get the real name. this.parseClassElementName(method); this.parsePostMemberNameModifiers(publicMember); if (method.key.type === "PrivateName") { // private async method this.pushClassPrivateMethod( classBody, privateMethod, isGenerator, true, ); } else { if (this.isNonstaticConstructor(publicMethod)) { this.raise(publicMethod.key.start, Errors.ConstructorIsAsync); } this.pushClassMethod( classBody, publicMethod, isGenerator, true, /* isConstructor */ false, false, ); } } else if ( isSimple && (key.name === "get" || key.name === "set") && !containsEsc && !(this.match(tt.star) && this.isLineTerminator()) ) { // `get\n*` is an uninitialized property named 'get' followed by a generator. // a getter or setter method.kind = key.name; // The so-called parsed name would have been "get/set": get the real name. this.parseClassElementName(publicMethod); if (method.key.type === "PrivateName") { // private getter/setter this.pushClassPrivateMethod(classBody, privateMethod, false, false); } else { if (this.isNonstaticConstructor(publicMethod)) { this.raise(publicMethod.key.start, Errors.ConstructorIsAccessor); } this.pushClassMethod( classBody, publicMethod, false, false, /* isConstructor */ false, false, ); } this.checkGetterSetterParams(publicMethod); } else if (this.isLineTerminator()) { // an uninitialized class property (due to ASI, since we don't otherwise recognize the next token) if (isPrivate) { this.pushClassPrivateProperty(classBody, privateProp); } else { this.pushClassProperty(classBody, publicProp); } } else { this.unexpected(); } } // https://tc39.es/proposal-class-fields/#prod-ClassElementName parseClassElementName(member: N.ClassMember): N.Expression | N.Identifier { const key = this.parsePropertyName(member, /* isPrivateNameAllowed */ true); if ( !member.computed && member.static && ((key: $FlowSubtype).name === "prototype" || (key: $FlowSubtype).value === "prototype") ) { this.raise(key.start, Errors.StaticPrototype); } if (key.type === "PrivateName" && key.id.name === "constructor") { this.raise(key.start, Errors.ConstructorClassPrivateField); } return key; } pushClassProperty(classBody: N.ClassBody, prop: N.ClassProperty) { if ( !prop.computed && (prop.key.name === "constructor" || prop.key.value === "constructor") ) { // Non-computed field, which is either an identifier named "constructor" // or a string literal named "constructor" this.raise(prop.key.start, Errors.ConstructorClassField); } classBody.body.push(this.parseClassProperty(prop)); } pushClassPrivateProperty( classBody: N.ClassBody, prop: N.ClassPrivateProperty, ) { this.expectPlugin("classPrivateProperties", prop.key.start); const node = this.parseClassPrivateProperty(prop); classBody.body.push(node); this.classScope.declarePrivateName( node.key.id.name, CLASS_ELEMENT_OTHER, node.key.start, ); } pushClassMethod( classBody: N.ClassBody, method: N.ClassMethod, isGenerator: boolean, isAsync: boolean, isConstructor: boolean, allowsDirectSuper: boolean, ): void { classBody.body.push( this.parseMethod( method, isGenerator, isAsync, isConstructor, allowsDirectSuper, "ClassMethod", true, ), ); } pushClassPrivateMethod( classBody: N.ClassBody, method: N.ClassPrivateMethod, isGenerator: boolean, isAsync: boolean, ): void { this.expectPlugin("classPrivateMethods", method.key.start); const node = this.parseMethod( method, isGenerator, isAsync, /* isConstructor */ false, false, "ClassPrivateMethod", true, ); classBody.body.push(node); const kind = node.kind === "get" ? node.static ? CLASS_ELEMENT_STATIC_GETTER : CLASS_ELEMENT_INSTANCE_GETTER : node.kind === "set" ? node.static ? CLASS_ELEMENT_STATIC_SETTER : CLASS_ELEMENT_INSTANCE_SETTER : CLASS_ELEMENT_OTHER; this.classScope.declarePrivateName(node.key.id.name, kind, node.key.start); } // Overridden in typescript.js parsePostMemberNameModifiers( // eslint-disable-next-line no-unused-vars methodOrProp: N.ClassMethod | N.ClassProperty, ): void {} parseClassPrivateProperty( node: N.ClassPrivateProperty, ): N.ClassPrivateProperty { this.scope.enter(SCOPE_CLASS | SCOPE_SUPER); this.prodParam.enter(PARAM); node.value = this.eat(tt.eq) ? this.parseMaybeAssignAllowIn() : null; this.semicolon(); this.prodParam.exit(); this.scope.exit(); return this.finishNode(node, "ClassPrivateProperty"); } parseClassProperty(node: N.ClassProperty): N.ClassProperty { if (!node.typeAnnotation) { this.expectPlugin("classProperties"); } this.scope.enter(SCOPE_CLASS | SCOPE_SUPER); this.prodParam.enter(PARAM); if (this.match(tt.eq)) { this.expectPlugin("classProperties"); this.next(); node.value = this.parseMaybeAssignAllowIn(); } else { node.value = null; } this.semicolon(); this.prodParam.exit(); this.scope.exit(); return this.finishNode(node, "ClassProperty"); } parseClassId( node: N.Class, isStatement: boolean, optionalId: ?boolean, bindingType: BindingTypes = BIND_CLASS, ): void { if (this.match(tt.name)) { node.id = this.parseIdentifier(); if (isStatement) { this.checkLVal(node.id, bindingType, undefined, "class name"); } } else { if (optionalId || !isStatement) { node.id = null; } else { this.unexpected(null, Errors.MissingClassName); } } } // https://tc39.es/ecma262/#prod-ClassHeritage parseClassSuper(node: N.Class): void { node.superClass = this.eat(tt._extends) ? this.parseExprSubscripts() : null; } // Parses module export declaration. // https://tc39.es/ecma262/#prod-ExportDeclaration parseExport(node: N.Node): N.AnyExport { const hasDefault = this.maybeParseExportDefaultSpecifier(node); const parseAfterDefault = !hasDefault || this.eat(tt.comma); const hasStar = parseAfterDefault && this.eatExportStar(node); const hasNamespace = hasStar && this.maybeParseExportNamespaceSpecifier(node); const parseAfterNamespace = parseAfterDefault && (!hasNamespace || this.eat(tt.comma)); const isFromRequired = hasDefault || hasStar; if (hasStar && !hasNamespace) { if (hasDefault) this.unexpected(); this.parseExportFrom(node, true); return this.finishNode(node, "ExportAllDeclaration"); } const hasSpecifiers = this.maybeParseExportNamedSpecifiers(node); if ( (hasDefault && parseAfterDefault && !hasStar && !hasSpecifiers) || (hasNamespace && parseAfterNamespace && !hasSpecifiers) ) { throw this.unexpected(null, tt.braceL); } let hasDeclaration; if (isFromRequired || hasSpecifiers) { hasDeclaration = false; this.parseExportFrom(node, isFromRequired); } else { hasDeclaration = this.maybeParseExportDeclaration(node); } if (isFromRequired || hasSpecifiers || hasDeclaration) { this.checkExport(node, true, false, !!node.source); return this.finishNode(node, "ExportNamedDeclaration"); } if (this.eat(tt._default)) { // export default ... node.declaration = this.parseExportDefaultExpression(); this.checkExport(node, true, true); return this.finishNode(node, "ExportDefaultDeclaration"); } throw this.unexpected(null, tt.braceL); } // eslint-disable-next-line no-unused-vars eatExportStar(node: N.Node): boolean { return this.eat(tt.star); } maybeParseExportDefaultSpecifier(node: N.Node): boolean { if (this.isExportDefaultSpecifier()) { // export defaultObj ... this.expectPlugin("exportDefaultFrom"); const specifier = this.startNode(); specifier.exported = this.parseIdentifier(true); node.specifiers = [this.finishNode(specifier, "ExportDefaultSpecifier")]; return true; } return false; } maybeParseExportNamespaceSpecifier(node: N.Node): boolean { if (this.isContextual("as")) { if (!node.specifiers) node.specifiers = []; const specifier = this.startNodeAt( this.state.lastTokStart, this.state.lastTokStartLoc, ); this.next(); specifier.exported = this.parseIdentifier(true); node.specifiers.push( this.finishNode(specifier, "ExportNamespaceSpecifier"), ); return true; } return false; } maybeParseExportNamedSpecifiers(node: N.Node): boolean { if (this.match(tt.braceL)) { if (!node.specifiers) node.specifiers = []; node.specifiers.push(...this.parseExportSpecifiers()); node.source = null; node.declaration = null; return true; } return false; } maybeParseExportDeclaration(node: N.Node): boolean { if (this.shouldParseExportDeclaration()) { node.specifiers = []; node.source = null; node.declaration = this.parseExportDeclaration(node); return true; } return false; } isAsyncFunction(): boolean { if (!this.isContextual("async")) return false; const next = this.nextTokenStart(); return ( !lineBreak.test(this.input.slice(this.state.pos, next)) && this.isUnparsedContextual(next, "function") ); } parseExportDefaultExpression(): N.Expression | N.Declaration { const expr = this.startNode(); const isAsync = this.isAsyncFunction(); if (this.match(tt._function) || isAsync) { this.next(); if (isAsync) { this.next(); } return this.parseFunction( expr, FUNC_STATEMENT | FUNC_NULLABLE_ID, isAsync, ); } else if (this.match(tt._class)) { return this.parseClass(expr, true, true); } else if (this.match(tt.at)) { if ( this.hasPlugin("decorators") && this.getPluginOption("decorators", "decoratorsBeforeExport") ) { this.raise(this.state.start, Errors.DecoratorBeforeExport); } this.parseDecorators(false); return this.parseClass(expr, true, true); } else if (this.match(tt._const) || this.match(tt._var) || this.isLet()) { throw this.raise(this.state.start, Errors.UnsupportedDefaultExport); } else { const res = this.parseMaybeAssignAllowIn(); this.semicolon(); return res; } } // eslint-disable-next-line no-unused-vars parseExportDeclaration(node: N.ExportNamedDeclaration): ?N.Declaration { return this.parseStatement(null); } isExportDefaultSpecifier(): boolean { if (this.match(tt.name)) { const value = this.state.value; if ((value === "async" && !this.state.containsEsc) || value === "let") { return false; } if ( (value === "type" || value === "interface") && !this.state.containsEsc ) { const l = this.lookahead(); // If we see any variable name other than `from` after `type` keyword, // we consider it as flow/typescript type exports // note that this approach may fail on some pedantic cases // export type from = number if ( (l.type === tt.name && l.value !== "from") || l.type === tt.braceL ) { this.expectOnePlugin(["flow", "typescript"]); return false; } } } else if (!this.match(tt._default)) { return false; } const next = this.nextTokenStart(); const hasFrom = this.isUnparsedContextual(next, "from"); if ( this.input.charCodeAt(next) === charCodes.comma || (this.match(tt.name) && hasFrom) ) { return true; } // lookahead again when `export default from` is seen if (this.match(tt._default) && hasFrom) { const nextAfterFrom = this.input.charCodeAt( this.nextTokenStartSince(next + 4), ); return ( nextAfterFrom === charCodes.quotationMark || nextAfterFrom === charCodes.apostrophe ); } return false; } parseExportFrom(node: N.ExportNamedDeclaration, expect?: boolean): void { if (this.eatContextual("from")) { node.source = this.parseImportSource(); this.checkExport(node); } else { if (expect) { this.unexpected(); } else { node.source = null; } } this.semicolon(); } shouldParseExportDeclaration(): boolean { if (this.match(tt.at)) { this.expectOnePlugin(["decorators", "decorators-legacy"]); if (this.hasPlugin("decorators")) { if (this.getPluginOption("decorators", "decoratorsBeforeExport")) { this.unexpected(this.state.start, Errors.DecoratorBeforeExport); } else { return true; } } } return ( this.state.type.keyword === "var" || this.state.type.keyword === "const" || this.state.type.keyword === "function" || this.state.type.keyword === "class" || this.isLet() || this.isAsyncFunction() ); } checkExport( node: N.ExportNamedDeclaration, checkNames?: boolean, isDefault?: boolean, isFrom?: boolean, ): void { if (checkNames) { // Check for duplicate exports if (isDefault) { // Default exports this.checkDuplicateExports(node, "default"); if (this.hasPlugin("exportDefaultFrom")) { const declaration = ((node: any): N.ExportDefaultDeclaration) .declaration; if ( declaration.type === "Identifier" && declaration.name === "from" && declaration.end - declaration.start === 4 && // does not contain escape !declaration.extra?.parenthesized ) { this.raise(declaration.start, Errors.ExportDefaultFromAsIdentifier); } } } else if (node.specifiers && node.specifiers.length) { // Named exports for (const specifier of node.specifiers) { this.checkDuplicateExports(specifier, specifier.exported.name); // $FlowIgnore if (!isFrom && specifier.local) { // check for keywords used as local names this.checkReservedWord( specifier.local.name, specifier.local.start, true, false, ); // check if export is defined // $FlowIgnore this.scope.checkLocalExport(specifier.local); } } } else if (node.declaration) { // Exported declarations if ( node.declaration.type === "FunctionDeclaration" || node.declaration.type === "ClassDeclaration" ) { const id = node.declaration.id; if (!id) throw new Error("Assertion failure"); this.checkDuplicateExports(node, id.name); } else if (node.declaration.type === "VariableDeclaration") { for (const declaration of node.declaration.declarations) { this.checkDeclaration(declaration.id); } } } } const currentContextDecorators = this.state.decoratorStack[ this.state.decoratorStack.length - 1 ]; // If node.declaration is a class, it will take all decorators in the current context. // Thus we should throw if we see non-empty decorators here. if (currentContextDecorators.length) { throw this.raise(node.start, Errors.UnsupportedDecoratorExport); } } checkDeclaration(node: N.Pattern | N.ObjectProperty): void { if (node.type === "Identifier") { this.checkDuplicateExports(node, node.name); } else 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 === "AssignmentPattern") { this.checkDeclaration(node.left); } } checkDuplicateExports( node: | N.Identifier | N.ExportNamedDeclaration | N.ExportSpecifier | N.ExportDefaultSpecifier, name: string, ): void { if (this.state.exportedIdentifiers.indexOf(name) > -1) { this.raise( node.start, name === "default" ? Errors.DuplicateDefaultExport : Errors.DuplicateExport, name, ); } this.state.exportedIdentifiers.push(name); } // Parses a comma-separated list of module exports. parseExportSpecifiers(): Array { const nodes = []; let first = true; // 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 node = this.startNode(); node.local = this.parseIdentifier(true); node.exported = this.eatContextual("as") ? this.parseIdentifier(true) : node.local.__clone(); nodes.push(this.finishNode(node, "ExportSpecifier")); } return nodes; } // Parses import declaration. // https://tc39.es/ecma262/#prod-ImportDeclaration parseImport(node: N.Node): N.AnyImport { // import '...' node.specifiers = []; if (!this.match(tt.string)) { // check if we have a default import like // import React from "react"; const hasDefault = this.maybeParseDefaultImportSpecifier(node); /* we are checking if we do not have a default import, then it is obvious that we need named imports * import { get } from "axios"; * but if we do have a default import * we need to check if we have a comma after that and * that is where this `|| this.eat` condition comes into play */ const parseNext = !hasDefault || this.eat(tt.comma); // if we do have to parse the next set of specifiers, we first check for star imports // import React, * from "react"; const hasStar = parseNext && this.maybeParseStarImportSpecifier(node); // now we check if we need to parse the next imports // but only if they are not importing * (everything) if (parseNext && !hasStar) this.parseNamedImportSpecifiers(node); this.expectContextual("from"); } node.source = this.parseImportSource(); // https://github.com/tc39/proposal-module-attributes // parse module attributes if the next token is `with` or ignore and finish the ImportDeclaration node. const attributes = this.maybeParseModuleAttributes(); if (attributes) { node.attributes = attributes; } this.semicolon(); return this.finishNode(node, "ImportDeclaration"); } parseImportSource(): N.StringLiteral { if (!this.match(tt.string)) this.unexpected(); return this.parseExprAtom(); } // eslint-disable-next-line no-unused-vars shouldParseDefaultImport(node: N.ImportDeclaration): boolean { return this.match(tt.name); } parseImportSpecifierLocal( node: N.ImportDeclaration, specifier: N.Node, type: string, contextDescription: string, ): void { specifier.local = this.parseIdentifier(); this.checkLVal( specifier.local, BIND_LEXICAL, undefined, contextDescription, ); node.specifiers.push(this.finishNode(specifier, type)); } maybeParseModuleAttributes() { if (this.match(tt._with) && !this.hasPrecedingLineBreak()) { this.expectPlugin("moduleAttributes"); this.next(); } else { if (this.hasPlugin("moduleAttributes")) return []; return null; } const attrs = []; const attributes = new Set(); do { // we are trying to parse a node which has the following syntax // with type: "json" // [with -> keyword], [type -> Identifier], [":" -> token for colon], ["json" -> StringLiteral] const node = this.startNode(); node.key = this.parseIdentifier(true); // for now we are only allowing `type` as the only allowed module attribute if (node.key.name !== "type") { this.raise( node.key.start, Errors.ModuleAttributeDifferentFromType, node.key.name, ); } // check if we already have an entry for an attribute // if a duplicate entry is found, throw an error // for now this logic will come into play only when someone declares `type` twice if (attributes.has(node.key.name)) { this.raise( node.key.start, Errors.ModuleAttributesWithDuplicateKeys, node.key.name, ); } attributes.add(node.key.name); this.expect(tt.colon); // check if the value set to the module attribute is a string as we only allow string literals if (!this.match(tt.string)) { throw this.unexpected( this.state.start, Errors.ModuleAttributeInvalidValue, ); } node.value = this.parseLiteral(this.state.value, "StringLiteral"); this.finishNode(node, "ImportAttribute"); attrs.push(node); } while (this.eat(tt.comma)); return attrs; } maybeParseDefaultImportSpecifier(node: N.ImportDeclaration): boolean { if (this.shouldParseDefaultImport(node)) { // import defaultObj, { x, y as z } from '...' this.parseImportSpecifierLocal( node, this.startNode(), "ImportDefaultSpecifier", "default import specifier", ); return true; } return false; } maybeParseStarImportSpecifier(node: N.ImportDeclaration): boolean { if (this.match(tt.star)) { const specifier = this.startNode(); this.next(); this.expectContextual("as"); this.parseImportSpecifierLocal( node, specifier, "ImportNamespaceSpecifier", "import namespace specifier", ); return true; } return false; } parseNamedImportSpecifiers(node: N.ImportDeclaration) { let first = true; 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)) { throw this.raise(this.state.start, Errors.DestructureNamedImport); } this.expect(tt.comma); if (this.eat(tt.braceR)) break; } this.parseImportSpecifier(node); } } // https://tc39.es/ecma262/#prod-ImportSpecifier parseImportSpecifier(node: N.ImportDeclaration): void { 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(); } this.checkLVal( specifier.local, BIND_LEXICAL, undefined, "import specifier", ); node.specifiers.push(this.finishNode(specifier, "ImportSpecifier")); } }