getClassInfo.js 11 KB

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