UNPKG

9.18 kBJavaScriptView Raw
1
2
3import {ContextualKeyword} from "../parser/tokenizer/keywords";
4import {TokenType as tt} from "../parser/tokenizer/types";
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44/**
45 * Get information about the class fields for this class, given a token processor pointing to the
46 * open-brace at the start of the class.
47 */
48export default function getClassInfo(
49 rootTransformer,
50 tokens,
51 nameManager,
52) {
53 const snapshot = tokens.snapshot();
54
55 const headerInfo = processClassHeader(tokens);
56
57 let constructorInitializerStatements = [];
58 const instanceInitializerNames = [];
59 const staticInitializerNames = [];
60 let constructorInsertPos = null;
61 const fields = [];
62 const rangesToRemove = [];
63
64 const classContextId = tokens.currentToken().contextId;
65 if (classContextId == null) {
66 throw new Error("Expected non-null class context ID on class open-brace.");
67 }
68
69 tokens.nextToken();
70 while (!tokens.matchesContextIdAndLabel(tt.braceR, classContextId)) {
71 if (tokens.matchesContextual(ContextualKeyword._constructor) && !tokens.currentToken().isType) {
72 ({constructorInitializerStatements, constructorInsertPos} = processConstructor(tokens));
73 } else if (tokens.matches1(tt.semi)) {
74 rangesToRemove.push({start: tokens.currentIndex(), end: tokens.currentIndex() + 1});
75 tokens.nextToken();
76 } else if (tokens.currentToken().isType) {
77 tokens.nextToken();
78 } else {
79 // Either a method or a field. Skip to the identifier part.
80 const statementStartIndex = tokens.currentIndex();
81 let isStatic = false;
82 let isESPrivate = false;
83 while (isAccessModifier(tokens.currentToken())) {
84 if (tokens.matches1(tt._static)) {
85 isStatic = true;
86 }
87 if (tokens.matches1(tt.hash)) {
88 isESPrivate = true;
89 }
90 tokens.nextToken();
91 }
92 if (isStatic && tokens.matches1(tt.braceL)) {
93 // This is a static block, so don't process it in any special way.
94 skipToNextClassElement(tokens, classContextId);
95 continue;
96 }
97 if (isESPrivate) {
98 // Sucrase doesn't attempt to transpile private fields; just leave them as-is.
99 skipToNextClassElement(tokens, classContextId);
100 continue;
101 }
102 if (
103 tokens.matchesContextual(ContextualKeyword._constructor) &&
104 !tokens.currentToken().isType
105 ) {
106 ({constructorInitializerStatements, constructorInsertPos} = processConstructor(tokens));
107 continue;
108 }
109
110 const nameStartIndex = tokens.currentIndex();
111 skipFieldName(tokens);
112 if (tokens.matches1(tt.lessThan) || tokens.matches1(tt.parenL)) {
113 // This is a method, so nothing to process.
114 skipToNextClassElement(tokens, classContextId);
115 continue;
116 }
117 // There might be a type annotation that we need to skip.
118 while (tokens.currentToken().isType) {
119 tokens.nextToken();
120 }
121 if (tokens.matches1(tt.eq)) {
122 const equalsIndex = tokens.currentIndex();
123 // This is an initializer, so we need to wrap in an initializer method.
124 const valueEnd = tokens.currentToken().rhsEndIndex;
125 if (valueEnd == null) {
126 throw new Error("Expected rhsEndIndex on class field assignment.");
127 }
128 tokens.nextToken();
129 while (tokens.currentIndex() < valueEnd) {
130 rootTransformer.processToken();
131 }
132 let initializerName;
133 if (isStatic) {
134 initializerName = nameManager.claimFreeName("__initStatic");
135 staticInitializerNames.push(initializerName);
136 } else {
137 initializerName = nameManager.claimFreeName("__init");
138 instanceInitializerNames.push(initializerName);
139 }
140 // Fields start at the name, so `static x = 1;` has a field range of `x = 1;`.
141 fields.push({
142 initializerName,
143 equalsIndex,
144 start: nameStartIndex,
145 end: tokens.currentIndex(),
146 });
147 } else {
148 // This is just a declaration, so doesn't need to produce any code in the output.
149 rangesToRemove.push({start: statementStartIndex, end: tokens.currentIndex()});
150 }
151 }
152 }
153
154 tokens.restoreToSnapshot(snapshot);
155 return {
156 headerInfo,
157 constructorInitializerStatements,
158 instanceInitializerNames,
159 staticInitializerNames,
160 constructorInsertPos,
161 fields,
162 rangesToRemove,
163 };
164}
165
166/**
167 * Move the token processor to the next method/field in the class.
168 *
169 * To do that, we seek forward to the next start of a class name (either an open
170 * bracket or an identifier, or the closing curly brace), then seek backward to
171 * include any access modifiers.
172 */
173function skipToNextClassElement(tokens, classContextId) {
174 tokens.nextToken();
175 while (tokens.currentToken().contextId !== classContextId) {
176 tokens.nextToken();
177 }
178 while (isAccessModifier(tokens.tokenAtRelativeIndex(-1))) {
179 tokens.previousToken();
180 }
181}
182
183function processClassHeader(tokens) {
184 const classToken = tokens.currentToken();
185 const contextId = classToken.contextId;
186 if (contextId == null) {
187 throw new Error("Expected context ID on class token.");
188 }
189 const isExpression = classToken.isExpression;
190 if (isExpression == null) {
191 throw new Error("Expected isExpression on class token.");
192 }
193 let className = null;
194 let hasSuperclass = false;
195 tokens.nextToken();
196 if (tokens.matches1(tt.name)) {
197 className = tokens.identifierName();
198 }
199 while (!tokens.matchesContextIdAndLabel(tt.braceL, contextId)) {
200 // If this has a superclass, there will always be an `extends` token. If it doesn't have a
201 // superclass, only type parameters and `implements` clauses can show up here, all of which
202 // consist only of type tokens. A declaration like `class A<B extends C> {` should *not* count
203 // as having a superclass.
204 if (tokens.matches1(tt._extends) && !tokens.currentToken().isType) {
205 hasSuperclass = true;
206 }
207 tokens.nextToken();
208 }
209 return {isExpression, className, hasSuperclass};
210}
211
212/**
213 * Extract useful information out of a constructor, starting at the "constructor" name.
214 */
215function processConstructor(
216 tokens,
217) {
218 const constructorInitializerStatements = [];
219
220 tokens.nextToken();
221 const constructorContextId = tokens.currentToken().contextId;
222 if (constructorContextId == null) {
223 throw new Error("Expected context ID on open-paren starting constructor params.");
224 }
225 // Advance through parameters looking for access modifiers.
226 while (!tokens.matchesContextIdAndLabel(tt.parenR, constructorContextId)) {
227 if (tokens.currentToken().contextId === constructorContextId) {
228 // Current token is an open paren or comma just before a param, so check
229 // that param for access modifiers.
230 tokens.nextToken();
231 if (isAccessModifier(tokens.currentToken())) {
232 tokens.nextToken();
233 while (isAccessModifier(tokens.currentToken())) {
234 tokens.nextToken();
235 }
236 const token = tokens.currentToken();
237 if (token.type !== tt.name) {
238 throw new Error("Expected identifier after access modifiers in constructor arg.");
239 }
240 const name = tokens.identifierNameForToken(token);
241 constructorInitializerStatements.push(`this.${name} = ${name}`);
242 }
243 } else {
244 tokens.nextToken();
245 }
246 }
247 // )
248 tokens.nextToken();
249 let constructorInsertPos = tokens.currentIndex();
250
251 // Advance through body looking for a super call.
252 let foundSuperCall = false;
253 while (!tokens.matchesContextIdAndLabel(tt.braceR, constructorContextId)) {
254 if (!foundSuperCall && tokens.matches2(tt._super, tt.parenL)) {
255 tokens.nextToken();
256 const superCallContextId = tokens.currentToken().contextId;
257 if (superCallContextId == null) {
258 throw new Error("Expected a context ID on the super call");
259 }
260 while (!tokens.matchesContextIdAndLabel(tt.parenR, superCallContextId)) {
261 tokens.nextToken();
262 }
263 constructorInsertPos = tokens.currentIndex();
264 foundSuperCall = true;
265 }
266 tokens.nextToken();
267 }
268 // }
269 tokens.nextToken();
270
271 return {constructorInitializerStatements, constructorInsertPos};
272}
273
274/**
275 * Determine if this is any token that can go before the name in a method/field.
276 */
277function isAccessModifier(token) {
278 return [
279 tt._async,
280 tt._get,
281 tt._set,
282 tt.plus,
283 tt.minus,
284 tt._readonly,
285 tt._static,
286 tt._public,
287 tt._private,
288 tt._protected,
289 tt._abstract,
290 tt.star,
291 tt._declare,
292 tt.hash,
293 ].includes(token.type);
294}
295
296/**
297 * The next token or set of tokens is either an identifier or an expression in square brackets, for
298 * a method or field name.
299 */
300function skipFieldName(tokens) {
301 if (tokens.matches1(tt.bracketL)) {
302 const startToken = tokens.currentToken();
303 const classContextId = startToken.contextId;
304 if (classContextId == null) {
305 throw new Error("Expected class context ID on computed name open bracket.");
306 }
307 while (!tokens.matchesContextIdAndLabel(tt.bracketR, classContextId)) {
308 tokens.nextToken();
309 }
310 tokens.nextToken();
311 } else {
312 tokens.nextToken();
313 }
314}