UNPKG

10 kBJavaScriptView Raw
1
2import {TokenType as tt} from "../parser/tokenizer/types";
3
4import isIdentifier from "../util/isIdentifier";
5
6import Transformer from "./Transformer";
7
8export default class TypeScriptTransformer extends Transformer {
9 constructor(
10 rootTransformer,
11 tokens,
12 isImportsTransformEnabled,
13 ) {
14 super();this.rootTransformer = rootTransformer;this.tokens = tokens;this.isImportsTransformEnabled = isImportsTransformEnabled;;
15 }
16
17 process() {
18 if (
19 this.rootTransformer.processPossibleArrowParamEnd() ||
20 this.rootTransformer.processPossibleAsyncArrowWithTypeParams() ||
21 this.rootTransformer.processPossibleTypeRange()
22 ) {
23 return true;
24 }
25 if (
26 this.tokens.matches1(tt._public) ||
27 this.tokens.matches1(tt._protected) ||
28 this.tokens.matches1(tt._private) ||
29 this.tokens.matches1(tt._abstract) ||
30 this.tokens.matches1(tt._readonly) ||
31 this.tokens.matches1(tt._override) ||
32 this.tokens.matches1(tt.nonNullAssertion)
33 ) {
34 this.tokens.removeInitialToken();
35 return true;
36 }
37 if (this.tokens.matches1(tt._enum) || this.tokens.matches2(tt._const, tt._enum)) {
38 this.processEnum();
39 return true;
40 }
41 if (
42 this.tokens.matches2(tt._export, tt._enum) ||
43 this.tokens.matches3(tt._export, tt._const, tt._enum)
44 ) {
45 this.processEnum(true);
46 return true;
47 }
48 return false;
49 }
50
51 processEnum(isExport = false) {
52 // We might have "export const enum", so just remove all relevant tokens.
53 this.tokens.removeInitialToken();
54 while (this.tokens.matches1(tt._const) || this.tokens.matches1(tt._enum)) {
55 this.tokens.removeToken();
56 }
57 const enumName = this.tokens.identifierName();
58 this.tokens.removeToken();
59 if (isExport && !this.isImportsTransformEnabled) {
60 this.tokens.appendCode("export ");
61 }
62 this.tokens.appendCode(`var ${enumName}; (function (${enumName})`);
63 this.tokens.copyExpectedToken(tt.braceL);
64 this.processEnumBody(enumName);
65 this.tokens.copyExpectedToken(tt.braceR);
66 if (isExport && this.isImportsTransformEnabled) {
67 this.tokens.appendCode(`)(${enumName} || (exports.${enumName} = ${enumName} = {}));`);
68 } else {
69 this.tokens.appendCode(`)(${enumName} || (${enumName} = {}));`);
70 }
71 }
72
73 /**
74 * Transform an enum into equivalent JS. This has complexity in a few places:
75 * - TS allows string enums, numeric enums, and a mix of the two styles within an enum.
76 * - Enum keys are allowed to be referenced in later enum values.
77 * - Enum keys are allowed to be strings.
78 * - When enum values are omitted, they should follow an auto-increment behavior.
79 */
80 processEnumBody(enumName) {
81 // Code that can be used to reference the previous enum member, or null if this is the first
82 // enum member.
83 let previousValueCode = null;
84 while (true) {
85 if (this.tokens.matches1(tt.braceR)) {
86 break;
87 }
88 const {nameStringCode, variableName} = this.extractEnumKeyInfo(this.tokens.currentToken());
89 this.tokens.removeInitialToken();
90
91 if (
92 this.tokens.matches3(tt.eq, tt.string, tt.comma) ||
93 this.tokens.matches3(tt.eq, tt.string, tt.braceR)
94 ) {
95 this.processStringLiteralEnumMember(enumName, nameStringCode, variableName);
96 } else if (this.tokens.matches1(tt.eq)) {
97 this.processExplicitValueEnumMember(enumName, nameStringCode, variableName);
98 } else {
99 this.processImplicitValueEnumMember(
100 enumName,
101 nameStringCode,
102 variableName,
103 previousValueCode,
104 );
105 }
106 if (this.tokens.matches1(tt.comma)) {
107 this.tokens.removeToken();
108 }
109
110 if (variableName != null) {
111 previousValueCode = variableName;
112 } else {
113 previousValueCode = `${enumName}[${nameStringCode}]`;
114 }
115 }
116 }
117
118 /**
119 * Detect name information about this enum key, which will be used to determine which code to emit
120 * and whether we should declare a variable as part of this declaration.
121 *
122 * Some cases to keep in mind:
123 * - Enum keys can be implicitly referenced later, e.g. `X = 1, Y = X`. In Sucrase, we implement
124 * this by declaring a variable `X` so that later expressions can use it.
125 * - In addition to the usual identifier key syntax, enum keys are allowed to be string literals,
126 * e.g. `"hello world" = 3,`. Template literal syntax is NOT allowed.
127 * - Even if the enum key is defined as a string literal, it may still be referenced by identifier
128 * later, e.g. `"X" = 1, Y = X`. That means that we need to detect whether or not a string
129 * literal is identifier-like and emit a variable if so, even if the declaration did not use an
130 * identifier.
131 * - Reserved keywords like `break` are valid enum keys, but are not valid to be referenced later
132 * and would be a syntax error if we emitted a variable, so we need to skip the variable
133 * declaration in those cases.
134 *
135 * The variableName return value captures these nuances: if non-null, we can and must emit a
136 * variable declaration, and if null, we can't and shouldn't.
137 */
138 extractEnumKeyInfo(nameToken) {
139 if (nameToken.type === tt.name) {
140 const name = this.tokens.identifierNameForToken(nameToken);
141 return {
142 nameStringCode: `"${name}"`,
143 variableName: isIdentifier(name) ? name : null,
144 };
145 } else if (nameToken.type === tt.string) {
146 const name = this.tokens.stringValueForToken(nameToken);
147 return {
148 nameStringCode: this.tokens.code.slice(nameToken.start, nameToken.end),
149 variableName: isIdentifier(name) ? name : null,
150 };
151 } else {
152 throw new Error("Expected name or string at beginning of enum element.");
153 }
154 }
155
156 /**
157 * Handle an enum member where the RHS is just a string literal (not omitted, not a number, and
158 * not a complex expression). This is the typical form for TS string enums, and in this case, we
159 * do *not* create a reverse mapping.
160 *
161 * This is called after deleting the key token, when the token processor is at the equals sign.
162 *
163 * Example 1:
164 * someKey = "some value"
165 * ->
166 * const someKey = "some value"; MyEnum["someKey"] = someKey;
167 *
168 * Example 2:
169 * "some key" = "some value"
170 * ->
171 * MyEnum["some key"] = "some value";
172 */
173 processStringLiteralEnumMember(
174 enumName,
175 nameStringCode,
176 variableName,
177 ) {
178 if (variableName != null) {
179 this.tokens.appendCode(`const ${variableName}`);
180 // =
181 this.tokens.copyToken();
182 // value string
183 this.tokens.copyToken();
184 this.tokens.appendCode(`; ${enumName}[${nameStringCode}] = ${variableName};`);
185 } else {
186 this.tokens.appendCode(`${enumName}[${nameStringCode}]`);
187 // =
188 this.tokens.copyToken();
189 // value string
190 this.tokens.copyToken();
191 this.tokens.appendCode(";");
192 }
193 }
194
195 /**
196 * Handle an enum member initialized with an expression on the right-hand side (other than a
197 * string literal). In these cases, we should transform the expression and emit code that sets up
198 * a reverse mapping.
199 *
200 * The TypeScript implementation of this operation distinguishes between expressions that can be
201 * "constant folded" at compile time (i.e. consist of number literals and simple math operations
202 * on those numbers) and ones that are dynamic. For constant expressions, it emits the resolved
203 * numeric value, and auto-incrementing is only allowed in that case. Evaluating expressions at
204 * compile time would add significant complexity to Sucrase, so Sucrase instead leaves the
205 * expression as-is, and will later emit something like `MyEnum["previousKey"] + 1` to implement
206 * auto-incrementing.
207 *
208 * This is called after deleting the key token, when the token processor is at the equals sign.
209 *
210 * Example 1:
211 * someKey = 1 + 1
212 * ->
213 * const someKey = 1 + 1; MyEnum[MyEnum["someKey"] = someKey] = "someKey";
214 *
215 * Example 2:
216 * "some key" = 1 + 1
217 * ->
218 * MyEnum[MyEnum["some key"] = 1 + 1] = "some key";
219 */
220 processExplicitValueEnumMember(
221 enumName,
222 nameStringCode,
223 variableName,
224 ) {
225 const rhsEndIndex = this.tokens.currentToken().rhsEndIndex;
226 if (rhsEndIndex == null) {
227 throw new Error("Expected rhsEndIndex on enum assign.");
228 }
229
230 if (variableName != null) {
231 this.tokens.appendCode(`const ${variableName}`);
232 this.tokens.copyToken();
233 while (this.tokens.currentIndex() < rhsEndIndex) {
234 this.rootTransformer.processToken();
235 }
236 this.tokens.appendCode(
237 `; ${enumName}[${enumName}[${nameStringCode}] = ${variableName}] = ${nameStringCode};`,
238 );
239 } else {
240 this.tokens.appendCode(`${enumName}[${enumName}[${nameStringCode}]`);
241 this.tokens.copyToken();
242 while (this.tokens.currentIndex() < rhsEndIndex) {
243 this.rootTransformer.processToken();
244 }
245 this.tokens.appendCode(`] = ${nameStringCode};`);
246 }
247 }
248
249 /**
250 * Handle an enum member with no right-hand side expression. In this case, the value is the
251 * previous value plus 1, or 0 if there was no previous value. We should also always emit a
252 * reverse mapping.
253 *
254 * Example 1:
255 * someKey2
256 * ->
257 * const someKey2 = someKey1 + 1; MyEnum[MyEnum["someKey2"] = someKey2] = "someKey2";
258 *
259 * Example 2:
260 * "some key 2"
261 * ->
262 * MyEnum[MyEnum["some key 2"] = someKey1 + 1] = "some key 2";
263 */
264 processImplicitValueEnumMember(
265 enumName,
266 nameStringCode,
267 variableName,
268 previousValueCode,
269 ) {
270 let valueCode = previousValueCode != null ? `${previousValueCode} + 1` : "0";
271 if (variableName != null) {
272 this.tokens.appendCode(`const ${variableName} = ${valueCode}; `);
273 valueCode = variableName;
274 }
275 this.tokens.appendCode(
276 `${enumName}[${enumName}[${nameStringCode}] = ${valueCode}] = ${nameStringCode};`,
277 );
278 }
279}