123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155 |
- 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--;
- }
- }
- }
|