UNPKG

6.02 kBJavaScriptView Raw
1
2import {TokenType as tt} from "../parser/tokenizer/types";
3
4import Transformer from "./Transformer";
5
6/**
7 * Transformer supporting the optional chaining and nullish coalescing operators.
8 *
9 * Tech plan here:
10 * https://github.com/alangpierce/sucrase/wiki/Sucrase-Optional-Chaining-and-Nullish-Coalescing-Technical-Plan
11 *
12 * The prefix and suffix code snippets are handled by TokenProcessor, and this transformer handles
13 * the operators themselves.
14 */
15export default class OptionalChainingNullishTransformer extends Transformer {
16 constructor( tokens, nameManager) {
17 super();this.tokens = tokens;this.nameManager = nameManager;;
18 }
19
20 process() {
21 if (this.tokens.matches1(tt.nullishCoalescing)) {
22 const token = this.tokens.currentToken();
23 if (this.tokens.tokens[token.nullishStartIndex].isAsyncOperation) {
24 this.tokens.replaceTokenTrimmingLeftWhitespace(", async () => (");
25 } else {
26 this.tokens.replaceTokenTrimmingLeftWhitespace(", () => (");
27 }
28 return true;
29 }
30 if (this.tokens.matches1(tt._delete)) {
31 const nextToken = this.tokens.tokenAtRelativeIndex(1);
32 if (nextToken.isOptionalChainStart) {
33 this.tokens.removeInitialToken();
34 return true;
35 }
36 }
37 const token = this.tokens.currentToken();
38 const chainStart = token.subscriptStartIndex;
39 if (
40 chainStart != null &&
41 this.tokens.tokens[chainStart].isOptionalChainStart &&
42 // Super subscripts can't be optional (since super is never null/undefined), and the syntax
43 // relies on the subscript being intact, so leave this token alone.
44 this.tokens.tokenAtRelativeIndex(-1).type !== tt._super
45 ) {
46 const param = this.nameManager.claimFreeName("_");
47 let arrowStartSnippet;
48 if (
49 chainStart > 0 &&
50 this.tokens.matches1AtIndex(chainStart - 1, tt._delete) &&
51 this.isLastSubscriptInChain()
52 ) {
53 // Delete operations are special: we already removed the delete keyword, and to still
54 // perform a delete, we need to insert a delete in the very last part of the chain, which
55 // in correct code will always be a property access.
56 arrowStartSnippet = `${param} => delete ${param}`;
57 } else {
58 arrowStartSnippet = `${param} => ${param}`;
59 }
60 if (this.tokens.tokens[chainStart].isAsyncOperation) {
61 arrowStartSnippet = `async ${arrowStartSnippet}`;
62 }
63 if (
64 this.tokens.matches2(tt.questionDot, tt.parenL) ||
65 this.tokens.matches2(tt.questionDot, tt.lessThan)
66 ) {
67 if (this.justSkippedSuper()) {
68 this.tokens.appendCode(".bind(this)");
69 }
70 this.tokens.replaceTokenTrimmingLeftWhitespace(`, 'optionalCall', ${arrowStartSnippet}`);
71 } else if (this.tokens.matches2(tt.questionDot, tt.bracketL)) {
72 this.tokens.replaceTokenTrimmingLeftWhitespace(`, 'optionalAccess', ${arrowStartSnippet}`);
73 } else if (this.tokens.matches1(tt.questionDot)) {
74 this.tokens.replaceTokenTrimmingLeftWhitespace(`, 'optionalAccess', ${arrowStartSnippet}.`);
75 } else if (this.tokens.matches1(tt.dot)) {
76 this.tokens.replaceTokenTrimmingLeftWhitespace(`, 'access', ${arrowStartSnippet}.`);
77 } else if (this.tokens.matches1(tt.bracketL)) {
78 this.tokens.replaceTokenTrimmingLeftWhitespace(`, 'access', ${arrowStartSnippet}[`);
79 } else if (this.tokens.matches1(tt.parenL)) {
80 if (this.justSkippedSuper()) {
81 this.tokens.appendCode(".bind(this)");
82 }
83 this.tokens.replaceTokenTrimmingLeftWhitespace(`, 'call', ${arrowStartSnippet}(`);
84 } else {
85 throw new Error("Unexpected subscript operator in optional chain.");
86 }
87 return true;
88 }
89 return false;
90 }
91
92 /**
93 * Determine if the current token is the last of its chain, so that we know whether it's eligible
94 * to have a delete op inserted.
95 *
96 * We can do this by walking forward until we determine one way or another. Each
97 * isOptionalChainStart token must be paired with exactly one isOptionalChainEnd token after it in
98 * a nesting way, so we can track depth and walk to the end of the chain (the point where the
99 * depth goes negative) and see if any other subscript token is after us in the chain.
100 */
101 isLastSubscriptInChain() {
102 let depth = 0;
103 for (let i = this.tokens.currentIndex() + 1; ; i++) {
104 if (i >= this.tokens.tokens.length) {
105 throw new Error("Reached the end of the code while finding the end of the access chain.");
106 }
107 if (this.tokens.tokens[i].isOptionalChainStart) {
108 depth++;
109 } else if (this.tokens.tokens[i].isOptionalChainEnd) {
110 depth--;
111 }
112 if (depth < 0) {
113 return true;
114 }
115
116 // This subscript token is a later one in the same chain.
117 if (depth === 0 && this.tokens.tokens[i].subscriptStartIndex != null) {
118 return false;
119 }
120 }
121 }
122
123 /**
124 * Determine if we are the open-paren in an expression like super.a()?.b.
125 *
126 * We can do this by walking backward to find the previous subscript. If that subscript was
127 * preceded by a super, then we must be the subscript after it, so if this is a call expression,
128 * we'll need to attach the right context.
129 */
130 justSkippedSuper() {
131 let depth = 0;
132 let index = this.tokens.currentIndex() - 1;
133 while (true) {
134 if (index < 0) {
135 throw new Error(
136 "Reached the start of the code while finding the start of the access chain.",
137 );
138 }
139 if (this.tokens.tokens[index].isOptionalChainStart) {
140 depth--;
141 } else if (this.tokens.tokens[index].isOptionalChainEnd) {
142 depth++;
143 }
144 if (depth < 0) {
145 return false;
146 }
147
148 // This subscript token is a later one in the same chain.
149 if (depth === 0 && this.tokens.tokens[index].subscriptStartIndex != null) {
150 return this.tokens.tokens[index - 1].type === tt._super;
151 }
152 index--;
153 }
154 }
155}