UNPKG

sucrase

Version:

Super-fast alternative to Babel for when you can target modern JS runtimes

801 lines (800 loc) 29.3 kB
/* eslint max-len: 0 */ import { IdentifierRole } from "../tokenizer"; import { types as tt } from "../tokenizer/types"; import ExpressionParser from "./expression"; export default class StatementParser extends ExpressionParser { // ### Statement parsing parseTopLevel() { this.parseBlockBody(true, tt.eof); this.state.scopes.push({ startTokenIndex: 0, endTokenIndex: this.state.tokens.length, isFunctionScope: true, }); return { tokens: this.state.tokens, scopes: this.state.scopes, }; } // 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. parseStatement(declaration, topLevel = false) { if (this.match(tt.at)) { this.parseDecorators(); } this.parseStatementContent(declaration, topLevel); } parseStatementContent(declaration, topLevel) { const starttype = this.state.type; // 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: this.parseBreakContinueStatement(); return; case tt._debugger: this.parseDebuggerStatement(); return; case tt._do: this.parseDoStatement(); return; case tt._for: this.parseForStatement(); return; case tt._function: if (this.lookaheadType() === tt.dot) break; if (!declaration) this.unexpected(); this.parseFunctionStatement(); return; case tt._class: if (!declaration) this.unexpected(); this.parseClass(true); return; case tt._if: this.parseIfStatement(); return; case tt._return: this.parseReturnStatement(); return; case tt._switch: this.parseSwitchStatement(); return; case tt._throw: this.parseThrowStatement(); return; case tt._try: this.parseTryStatement(); return; case tt._let: case tt._const: if (!declaration) this.unexpected(); // NOTE: falls through to _var case tt._var: this.parseVarStatement(starttype); return; case tt._while: this.parseWhileStatement(); return; case tt.braceL: this.parseBlock(); return; case tt.semi: this.parseEmptyStatement(); return; case tt._export: case tt._import: { const nextType = this.lookaheadType(); if (nextType === tt.parenL || nextType === tt.dot) { break; } this.next(); if (starttype === tt._import) { this.parseImport(); } else { this.parseExport(); } return; } case tt.name: if (this.state.value === "async") { const functionStart = this.state.start; // peek ahead and see if next token is a function const snapshot = this.state.snapshot(); this.next(); if (this.match(tt._function) && !this.canInsertSemicolon()) { this.expect(tt._function); this.parseFunction(functionStart, true, false); return; } else { this.state.restoreFromSnapshot(snapshot); } } default: // Do nothing. break; } // 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 initialTokensLength = this.state.tokens.length; this.parseExpression(); let simpleName = null; if (this.state.tokens.length === initialTokensLength + 1) { const token = this.state.tokens[this.state.tokens.length - 1]; if (token.type === tt.name) { simpleName = token.value; } } if (simpleName == null) { this.semicolon(); return; } if (this.eat(tt.colon)) { this.parseLabeledStatement(); } else { // This was an identifier, so we might want to handle flow/typescript-specific cases. this.parseIdentifierStatement(simpleName); } } parseDecorators() { while (this.match(tt.at)) { this.parseDecorator(); } } parseDecorator() { this.next(); this.parseIdentifier(); while (this.eat(tt.dot)) { this.parseIdentifier(); } if (this.eat(tt.parenL)) { this.parseCallExpressionArguments(tt.parenR); } } parseBreakContinueStatement() { this.next(); if (!this.isLineTerminator()) { this.parseIdentifier(); this.semicolon(); } } parseDebuggerStatement() { this.next(); this.semicolon(); } parseDoStatement() { this.next(); this.parseStatement(false); this.expect(tt._while); this.parseParenExpression(); this.eat(tt.semi); } parseForStatement() { const startTokenIndex = this.state.tokens.length; this.parseAmbiguousForStatement(); const endTokenIndex = this.state.tokens.length; this.state.scopes.push({ startTokenIndex, endTokenIndex, isFunctionScope: false }); } // 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. parseAmbiguousForStatement() { this.next(); let forAwait = false; if (this.isContextual("await")) { forAwait = true; this.next(); } this.expect(tt.parenL); if (this.match(tt.semi)) { if (forAwait) { this.unexpected(); } this.parseFor(); return; } if (this.match(tt._var) || this.match(tt._let) || this.match(tt._const)) { const varKind = this.state.type; this.next(); this.parseVar(true, varKind); if (this.match(tt._in) || this.isContextual("of")) { this.parseForIn(forAwait); return; } this.parseFor(); return; } this.parseExpression(true); if (this.match(tt._in) || this.isContextual("of")) { this.parseForIn(forAwait); return; } if (forAwait) { this.unexpected(); } this.parseFor(); } parseFunctionStatement() { const functionStart = this.state.start; this.next(); this.parseFunction(functionStart, true); } parseIfStatement() { this.next(); this.parseParenExpression(); this.parseStatement(false); if (this.eat(tt._else)) { this.parseStatement(false); } } parseReturnStatement() { 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()) { this.parseExpression(); this.semicolon(); } } parseSwitchStatement() { this.next(); this.parseParenExpression(); const startTokenIndex = this.state.tokens.length; this.expect(tt.braceL); // Don't bother validation; just go through any sequence of cases, defaults, and statements. while (!this.match(tt.braceR)) { if (this.match(tt._case) || this.match(tt._default)) { const isCase = this.match(tt._case); this.next(); if (isCase) { this.parseExpression(); } this.expect(tt.colon); } else { this.parseStatement(true); } } this.next(); // Closing brace const endTokenIndex = this.state.tokens.length; this.state.scopes.push({ startTokenIndex, endTokenIndex, isFunctionScope: false }); } parseThrowStatement() { this.next(); this.parseExpression(); this.semicolon(); } parseTryStatement() { this.next(); this.parseBlock(); if (this.match(tt._catch)) { this.next(); let catchBindingStartTokenIndex = null; if (this.match(tt.parenL)) { catchBindingStartTokenIndex = this.state.tokens.length; this.expect(tt.parenL); this.parseBindingAtom(true /* isBlockScope */); this.expect(tt.parenR); } this.parseBlock(); if (catchBindingStartTokenIndex != null) { // We need a special scope for the catch binding which includes the binding itself and the // catch block. const endTokenIndex = this.state.tokens.length; this.state.scopes.push({ startTokenIndex: catchBindingStartTokenIndex, endTokenIndex, isFunctionScope: false, }); } } if (this.eat(tt._finally)) { this.parseBlock(); } } parseVarStatement(kind) { this.next(); this.parseVar(false, kind); this.semicolon(); } parseWhileStatement() { this.next(); this.parseParenExpression(); this.parseStatement(false); } parseEmptyStatement() { this.next(); } parseLabeledStatement() { this.parseStatement(true); } /** * Parse a statement starting with an identifier of the given name. Subclasses match on the name * to handle statements like "declare". */ parseIdentifierStatement(name) { this.semicolon(); } // Parse a semicolon-enclosed block of statements, handling `"use // strict"` declarations when `allowStrict` is true (used for // function bodies). parseBlock(allowDirectives = false, isFunctionScope = false, contextId) { const startTokenIndex = this.state.tokens.length; this.expect(tt.braceL); this.state.tokens[this.state.tokens.length - 1].contextId = contextId; this.parseBlockBody(false, tt.braceR); this.state.tokens[this.state.tokens.length - 1].contextId = contextId; const endTokenIndex = this.state.tokens.length; this.state.scopes.push({ startTokenIndex, endTokenIndex, isFunctionScope }); } parseBlockBody(topLevel, end) { while (!this.eat(end)) { this.parseStatement(true, topLevel); } } // Parse a regular `for` loop. The disambiguation code in // `parseStatement` will already have parsed the init statement or // expression. parseFor() { this.expect(tt.semi); if (!this.match(tt.semi)) { this.parseExpression(); } this.expect(tt.semi); if (!this.match(tt.parenR)) { this.parseExpression(); } this.expect(tt.parenR); this.parseStatement(false); } // Parse a `for`/`in` and `for`/`of` loop, which are almost // same from parser's perspective. parseForIn(forAwait) { if (forAwait) { this.eatContextual("of"); } else { this.next(); } this.parseExpression(); this.expect(tt.parenR); this.parseStatement(false); } // Parse a list of variable declarations. parseVar(isFor, kind) { while (true) { const isBlockScope = kind === tt._const || kind === tt._let; this.parseVarHead(isBlockScope); if (this.eat(tt.eq)) { this.parseMaybeAssign(isFor); } if (!this.eat(tt.comma)) break; } } parseVarHead(isBlockScope) { this.parseBindingAtom(isBlockScope); } // Parse a function declaration or literal (depending on the // `isStatement` parameter). parseFunction(functionStart, isStatement, allowExpressionBody, optionalId) { const oldInGenerator = this.state.inGenerator; let isGenerator = false; if (this.match(tt.star)) { isGenerator = true; this.next(); } if (isStatement && !optionalId && !this.match(tt.name) && !this.match(tt._yield)) { this.unexpected(); } let nameScopeStartTokenIndex = null; // When parsing function expression, the binding identifier is parsed // according to the rules inside the function. // e.g. (function* yield() {}) is invalid because "yield" is disallowed in // generators. // This isn't the case with function declarations: function* yield() {} is // valid because yield is parsed as if it was outside the generator. // Therefore, this.state.inGenerator is set before or after parsing the // function id according to the "isStatement" parameter. if (!isStatement) this.state.inGenerator = isGenerator; if (this.match(tt.name) || this.match(tt._yield)) { // Expression-style functions should limit their name's scope to the function body, so we make // a new function scope to enforce that. if (!isStatement) { nameScopeStartTokenIndex = this.state.tokens.length; } this.parseBindingIdentifier(); this.state.tokens[this.state.tokens.length - 1].identifierRole = IdentifierRole.FunctionScopedDeclaration; } if (isStatement) this.state.inGenerator = isGenerator; const startTokenIndex = this.state.tokens.length; this.parseFunctionParams(); this.parseFunctionBodyAndFinish(functionStart, isGenerator, allowExpressionBody); const endTokenIndex = this.state.tokens.length; // In addition to the block scope of the function body, we need a separate function-style scope // that includes the params. this.state.scopes.push({ startTokenIndex, endTokenIndex, isFunctionScope: true }); if (nameScopeStartTokenIndex !== null) { this.state.scopes.push({ startTokenIndex: nameScopeStartTokenIndex, endTokenIndex, isFunctionScope: true, }); } this.state.inGenerator = oldInGenerator; } parseFunctionParams(allowModifiers, funcContextId) { this.expect(tt.parenL); this.state.tokens[this.state.tokens.length - 1].contextId = funcContextId; this.parseBindingList(tt.parenR, false /* isBlockScope */, false /* allowEmpty */, allowModifiers); this.state.tokens[this.state.tokens.length - 1].contextId = funcContextId; } // Parse a class declaration or literal (depending on the // `isStatement` parameter). parseClass(isStatement, optionalId = false) { // Put a context ID on the class keyword, the open-brace, and the close-brace, so that later // code can easily navigate to meaningful points on the class. const contextId = this.nextContextId++; this.next(); this.state.tokens[this.state.tokens.length - 1].contextId = contextId; this.state.tokens[this.state.tokens.length - 1].isExpression = !isStatement; // Like with functions, we declare a special "name scope" from the start of the name to the end // of the class, but only with expression-style classes, to represent the fact that the name is // available to the body of the class but not an outer declaration. let nameScopeStartTokenIndex = null; if (!isStatement) { nameScopeStartTokenIndex = this.state.tokens.length; } this.parseClassId(isStatement, optionalId); this.parseClassSuper(); const openBraceIndex = this.state.tokens.length; this.parseClassBody(contextId); this.state.tokens[openBraceIndex].contextId = contextId; this.state.tokens[this.state.tokens.length - 1].contextId = contextId; if (nameScopeStartTokenIndex !== null) { const endTokenIndex = this.state.tokens.length; this.state.scopes.push({ startTokenIndex: nameScopeStartTokenIndex, endTokenIndex, isFunctionScope: false, }); } } isClassProperty() { return this.match(tt.eq) || this.match(tt.semi) || this.match(tt.braceR); } isClassMethod() { return this.match(tt.parenL); } parseClassBody(classContextId) { this.expect(tt.braceL); while (!this.eat(tt.braceR)) { if (this.eat(tt.semi)) { continue; } if (this.match(tt.at)) { this.parseDecorator(); continue; } const memberStart = this.state.start; this.parseClassMember(memberStart, classContextId); } } parseClassMember(memberStart, classContextId) { let isStatic = false; if (this.match(tt.name) && this.state.value === "static") { this.parseIdentifier(); // eats 'static' if (this.isClassMethod()) { this.parseClassMethod(memberStart, false, /* isConstructor */ false); return; } else if (this.isClassProperty()) { this.parseClassProperty(); return; } // otherwise something static this.state.tokens[this.state.tokens.length - 1].type = tt._static; isStatic = true; } this.parseClassMemberWithIsStatic(memberStart, isStatic, classContextId); } parseClassMemberWithIsStatic(memberStart, isStatic, classContextId) { if (this.eat(tt.star)) { // a generator this.parseClassPropertyName(classContextId); this.parseClassMethod(memberStart, true, /* isConstructor */ false); return; } // Get the identifier name so we can tell if it's actually a keyword like "async", "get", or // "set". this.parseClassPropertyName(classContextId); let simpleName = null; let isConstructor = false; const token = this.state.tokens[this.state.tokens.length - 1]; if (token.type === tt.name) { simpleName = token.value; } // We allow "constructor" as either an identifier or a string. if (token.value === "constructor") { isConstructor = true; } this.parsePostMemberNameModifiers(); if (this.isClassMethod()) { this.parseClassMethod(memberStart, false, isConstructor); } else if (this.isClassProperty()) { this.parseClassProperty(); } else if (simpleName === "async" && !this.isLineTerminator()) { this.state.tokens[this.state.tokens.length - 1].type = tt._async; // an async method const isGenerator = this.match(tt.star); if (isGenerator) { this.next(); } // The so-called parsed name would have been "async": get the real name. this.parseClassPropertyName(classContextId); this.parseClassMethod(memberStart, isGenerator, false /* isConstructor */); } else if ((simpleName === "get" || simpleName === "set") && !(this.isLineTerminator() && this.match(tt.star))) { if (simpleName === "get") { this.state.tokens[this.state.tokens.length - 1].type = tt._get; } else { this.state.tokens[this.state.tokens.length - 1].type = tt._set; } // `get\n*` is an uninitialized property named 'get' followed by a generator. // a getter or setter // The so-called parsed name would have been "get/set": get the real name. this.parseClassPropertyName(classContextId); this.parseClassMethod(memberStart, false, /* isConstructor */ false); } else if (this.isLineTerminator()) { // an uninitialized class property (due to ASI, since we don't otherwise recognize the next token) this.parseClassProperty(); } else { this.unexpected(); } } parseClassMethod(functionStart, isGenerator, isConstructor) { this.parseMethod(functionStart, isGenerator, isConstructor); } // Return the name of the class property, if it is a simple identifier. parseClassPropertyName(classContextId) { this.parsePropertyName(classContextId); } // Overridden in typescript.js parsePostMemberNameModifiers() { } parseClassProperty() { if (this.match(tt.eq)) { const equalsTokenIndex = this.state.tokens.length; this.next(); this.parseMaybeAssign(); this.state.tokens[equalsTokenIndex].rhsEndIndex = this.state.tokens.length; } this.semicolon(); } parseClassId(isStatement, optionalId = false) { if (this.match(tt.name)) { this.parseIdentifier(); this.state.tokens[this.state.tokens.length - 1].identifierRole = IdentifierRole.BlockScopedDeclaration; } } // Returns true if there was a superclass. parseClassSuper() { if (this.eat(tt._extends)) { this.parseExprSubscripts(); return true; } return false; } // Parses module export declaration. parseExport() { // export * from '...' if (this.shouldParseExportStar()) { this.parseExportStar(); } else if (this.isExportDefaultSpecifier()) { // export default from this.parseIdentifier(); if (this.match(tt.comma) && this.lookaheadType() === tt.star) { this.expect(tt.comma); this.expect(tt.star); this.expectContextual("as"); this.parseIdentifier(); } else { this.parseExportSpecifiersMaybe(); } this.parseExportFrom(); } else if (this.eat(tt._default)) { // export default ... this.parseExportDefaultExpression(); } else if (this.shouldParseExportDeclaration()) { this.parseExportDeclaration(); } else { // export { x, y as z } [from '...'] this.parseExportSpecifiers(); this.parseExportFrom(); } } parseExportDefaultExpression() { const functionStart = this.state.start; if (this.eat(tt._function)) { this.parseFunction(functionStart, true, false, true); } else if (this.isContextual("async") && this.lookaheadType() === tt._function) { // async function declaration this.eatContextual("async"); this.eat(tt._function); this.parseFunction(functionStart, true, false, true); } else if (this.match(tt._class)) { this.parseClass(true, true); } else { this.parseMaybeAssign(); this.semicolon(); } } // eslint-disable-next-line no-unused-vars parseExportDeclaration() { this.parseStatement(true); } isExportDefaultSpecifier() { if (this.match(tt.name)) { return this.state.value !== "async"; } if (!this.match(tt._default)) { return false; } const lookahead = this.lookaheadTypeAndValue(); return (lookahead.type === tt.comma || (lookahead.type === tt.name && lookahead.value === "from")); } parseExportSpecifiersMaybe() { if (this.eat(tt.comma)) { this.parseExportSpecifiers(); } } parseExportFrom() { if (this.eatContextual("from")) { this.parseExprAtom(); } this.semicolon(); } shouldParseExportStar() { return this.match(tt.star); } parseExportStar() { this.expect(tt.star); if (this.isContextual("as")) { this.parseExportNamespace(); } else { this.parseExportFrom(); } } parseExportNamespace() { this.next(); this.state.tokens[this.state.tokens.length - 1].type = tt._as; this.parseIdentifier(); this.parseExportSpecifiersMaybe(); this.parseExportFrom(); } shouldParseExportDeclaration() { 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") || this.match(tt.at)); } // Parses a comma-separated list of module exports. parseExportSpecifiers() { 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; } this.parseIdentifier(); this.state.tokens[this.state.tokens.length - 1].identifierRole = IdentifierRole.ExportAccess; if (this.eatContextual("as")) { this.parseIdentifier(); } } } // Parses import declaration. parseImport() { // import '...' if (this.match(tt.string)) { this.parseExprAtom(); } else { this.parseImportSpecifiers(); this.expectContextual("from"); this.parseExprAtom(); } this.semicolon(); } // eslint-disable-next-line no-unused-vars shouldParseDefaultImport() { return this.match(tt.name); } parseImportSpecifierLocal() { this.parseIdentifier(); } // Parses a comma-separated list of module imports. parseImportSpecifiers() { let first = true; if (this.shouldParseDefaultImport()) { // import defaultObj, { x, y as z } from '...' this.parseImportSpecifierLocal(); if (!this.eat(tt.comma)) return; } if (this.match(tt.star)) { this.next(); this.expectContextual("as"); this.parseImportSpecifierLocal(); return; } 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(); } } parseImportSpecifier() { this.parseIdentifier(); if (this.eatContextual("as")) { this.parseIdentifier(); } } }