import {TokenType as tt} from "../parser/tokenizer/types"; import Transformer from "./Transformer"; /** * Transformer supporting the optional chaining and nullish coalescing operators. * * Tech plan here: * https://github.com/alangpierce/sucrase/wiki/Sucrase-Optional-Chaining-and-Nullish-Coalescing-Technical-Plan * * The prefix and suffix code snippets are handled by TokenProcessor, and this transformer handles * the operators themselves. */ export default class OptionalChainingNullishTransformer extends Transformer { constructor( tokens, nameManager) { super();this.tokens = tokens;this.nameManager = nameManager;; } process() { if (this.tokens.matches1(tt.nullishCoalescing)) { const token = this.tokens.currentToken(); if (this.tokens.tokens[token.nullishStartIndex].isAsyncOperation) { this.tokens.replaceTokenTrimmingLeftWhitespace(", async () => ("); } else { this.tokens.replaceTokenTrimmingLeftWhitespace(", () => ("); } return true; } if (this.tokens.matches1(tt._delete)) { const nextToken = this.tokens.tokenAtRelativeIndex(1); if (nextToken.isOptionalChainStart) { this.tokens.removeInitialToken(); return true; } } const token = this.tokens.currentToken(); const chainStart = token.subscriptStartIndex; if ( chainStart != null && this.tokens.tokens[chainStart].isOptionalChainStart && // Super subscripts can't be optional (since super is never null/undefined), and the syntax // relies on the subscript being intact, so leave this token alone. this.tokens.tokenAtRelativeIndex(-1).type !== tt._super ) { const param = this.nameManager.claimFreeName("_"); let arrowStartSnippet; if ( chainStart > 0 && this.tokens.matches1AtIndex(chainStart - 1, tt._delete) && this.isLastSubscriptInChain() ) { // Delete operations are special: we already removed the delete keyword, and to still // perform a delete, we need to insert a delete in the very last part of the chain, which // in correct code will always be a property access. arrowStartSnippet = `${param} => delete ${param}`; } else { arrowStartSnippet = `${param} => ${param}`; } if (this.tokens.tokens[chainStart].isAsyncOperation) { arrowStartSnippet = `async ${arrowStartSnippet}`; } if ( this.tokens.matches2(tt.questionDot, tt.parenL) || this.tokens.matches2(tt.questionDot, tt.lessThan) ) { if (this.justSkippedSuper()) { this.tokens.appendCode(".bind(this)"); } this.tokens.replaceTokenTrimmingLeftWhitespace(`, 'optionalCall', ${arrowStartSnippet}`); } else if (this.tokens.matches2(tt.questionDot, tt.bracketL)) { this.tokens.replaceTokenTrimmingLeftWhitespace(`, 'optionalAccess', ${arrowStartSnippet}`); } else if (this.tokens.matches1(tt.questionDot)) { this.tokens.replaceTokenTrimmingLeftWhitespace(`, 'optionalAccess', ${arrowStartSnippet}.`); } else if (this.tokens.matches1(tt.dot)) { this.tokens.replaceTokenTrimmingLeftWhitespace(`, 'access', ${arrowStartSnippet}.`); } else if (this.tokens.matches1(tt.bracketL)) { this.tokens.replaceTokenTrimmingLeftWhitespace(`, 'access', ${arrowStartSnippet}[`); } else if (this.tokens.matches1(tt.parenL)) { if (this.justSkippedSuper()) { this.tokens.appendCode(".bind(this)"); } this.tokens.replaceTokenTrimmingLeftWhitespace(`, 'call', ${arrowStartSnippet}(`); } else { throw new Error("Unexpected subscript operator in optional chain."); } return true; } return false; } /** * Determine if the current token is the last of its chain, so that we know whether it's eligible * to have a delete op inserted. * * We can do this by walking forward until we determine one way or another. Each * isOptionalChainStart token must be paired with exactly one isOptionalChainEnd token after it in * a nesting way, so we can track depth and walk to the end of the chain (the point where the * depth goes negative) and see if any other subscript token is after us in the chain. */ isLastSubscriptInChain() { let depth = 0; for (let i = this.tokens.currentIndex() + 1; ; i++) { if (i >= this.tokens.tokens.length) { throw new Error("Reached the end of the code while finding the end of the access chain."); } if (this.tokens.tokens[i].isOptionalChainStart) { depth++; } else if (this.tokens.tokens[i].isOptionalChainEnd) { depth--; } if (depth < 0) { return true; } // This subscript token is a later one in the same chain. if (depth === 0 && this.tokens.tokens[i].subscriptStartIndex != null) { return false; } } } /** * Determine if we are the open-paren in an expression like super.a()?.b. * * We can do this by walking backward to find the previous subscript. If that subscript was * preceded by a super, then we must be the subscript after it, so if this is a call expression, * we'll need to attach the right context. */ justSkippedSuper() { let depth = 0; let index = this.tokens.currentIndex() - 1; while (true) { if (index < 0) { throw new Error( "Reached the start of the code while finding the start of the access chain.", ); } if (this.tokens.tokens[index].isOptionalChainStart) { depth--; } else if (this.tokens.tokens[index].isOptionalChainEnd) { depth++; } if (depth < 0) { return false; } // This subscript token is a later one in the same chain. if (depth === 0 && this.tokens.tokens[index].subscriptStartIndex != null) { return this.tokens.tokens[index - 1].type === tt._super; } index--; } } }