FlowTransformer.js 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182
  1. import {ContextualKeyword} from "../parser/tokenizer/keywords";
  2. import {TokenType as tt} from "../parser/tokenizer/types";
  3. import Transformer from "./Transformer";
  4. export default class FlowTransformer extends Transformer {
  5. constructor(
  6. rootTransformer,
  7. tokens,
  8. isImportsTransformEnabled,
  9. ) {
  10. super();this.rootTransformer = rootTransformer;this.tokens = tokens;this.isImportsTransformEnabled = isImportsTransformEnabled;;
  11. }
  12. process() {
  13. if (
  14. this.rootTransformer.processPossibleArrowParamEnd() ||
  15. this.rootTransformer.processPossibleAsyncArrowWithTypeParams() ||
  16. this.rootTransformer.processPossibleTypeRange()
  17. ) {
  18. return true;
  19. }
  20. if (this.tokens.matches1(tt._enum)) {
  21. this.processEnum();
  22. return true;
  23. }
  24. if (this.tokens.matches2(tt._export, tt._enum)) {
  25. this.processNamedExportEnum();
  26. return true;
  27. }
  28. if (this.tokens.matches3(tt._export, tt._default, tt._enum)) {
  29. this.processDefaultExportEnum();
  30. return true;
  31. }
  32. return false;
  33. }
  34. /**
  35. * Handle a declaration like:
  36. * export enum E ...
  37. *
  38. * With this imports transform, this becomes:
  39. * const E = [[enum]]; exports.E = E;
  40. *
  41. * otherwise, it becomes:
  42. * export const E = [[enum]];
  43. */
  44. processNamedExportEnum() {
  45. if (this.isImportsTransformEnabled) {
  46. // export
  47. this.tokens.removeInitialToken();
  48. const enumName = this.tokens.identifierNameAtRelativeIndex(1);
  49. this.processEnum();
  50. this.tokens.appendCode(` exports.${enumName} = ${enumName};`);
  51. } else {
  52. this.tokens.copyToken();
  53. this.processEnum();
  54. }
  55. }
  56. /**
  57. * Handle a declaration like:
  58. * export default enum E
  59. *
  60. * With the imports transform, this becomes:
  61. * const E = [[enum]]; exports.default = E;
  62. *
  63. * otherwise, it becomes:
  64. * const E = [[enum]]; export default E;
  65. */
  66. processDefaultExportEnum() {
  67. // export
  68. this.tokens.removeInitialToken();
  69. // default
  70. this.tokens.removeToken();
  71. const enumName = this.tokens.identifierNameAtRelativeIndex(1);
  72. this.processEnum();
  73. if (this.isImportsTransformEnabled) {
  74. this.tokens.appendCode(` exports.default = ${enumName};`);
  75. } else {
  76. this.tokens.appendCode(` export default ${enumName};`);
  77. }
  78. }
  79. /**
  80. * Transpile flow enums to invoke the "flow-enums-runtime" library.
  81. *
  82. * Currently, the transpiled code always uses `require("flow-enums-runtime")`,
  83. * but if future flexibility is needed, we could expose a config option for
  84. * this string (similar to configurable JSX). Even when targeting ESM, the
  85. * default behavior of babel-plugin-transform-flow-enums is to use require
  86. * rather than injecting an import.
  87. *
  88. * Flow enums are quite a bit simpler than TS enums and have some convenient
  89. * constraints:
  90. * - Element initializers must be either always present or always absent. That
  91. * means that we can use fixed lookahead on the first element (if any) and
  92. * assume that all elements are like that.
  93. * - The right-hand side of an element initializer must be a literal value,
  94. * not a complex expression and not referencing other elements. That means
  95. * we can simply copy a single token.
  96. *
  97. * Enums can be broken up into three basic cases:
  98. *
  99. * Mirrored enums:
  100. * enum E {A, B}
  101. * ->
  102. * const E = require("flow-enums-runtime").Mirrored(["A", "B"]);
  103. *
  104. * Initializer enums:
  105. * enum E {A = 1, B = 2}
  106. * ->
  107. * const E = require("flow-enums-runtime")({A: 1, B: 2});
  108. *
  109. * Symbol enums:
  110. * enum E of symbol {A, B}
  111. * ->
  112. * const E = require("flow-enums-runtime")({A: Symbol("A"), B: Symbol("B")});
  113. *
  114. * We can statically detect which of the three cases this is by looking at the
  115. * "of" declaration (if any) and seeing if the first element has an initializer.
  116. * Since the other transform details are so similar between the three cases, we
  117. * use a single implementation and vary the transform within processEnumElement
  118. * based on case.
  119. */
  120. processEnum() {
  121. // enum E -> const E
  122. this.tokens.replaceToken("const");
  123. this.tokens.copyExpectedToken(tt.name);
  124. let isSymbolEnum = false;
  125. if (this.tokens.matchesContextual(ContextualKeyword._of)) {
  126. this.tokens.removeToken();
  127. isSymbolEnum = this.tokens.matchesContextual(ContextualKeyword._symbol);
  128. this.tokens.removeToken();
  129. }
  130. const hasInitializers = this.tokens.matches3(tt.braceL, tt.name, tt.eq);
  131. this.tokens.appendCode(' = require("flow-enums-runtime")');
  132. const isMirrored = !isSymbolEnum && !hasInitializers;
  133. this.tokens.replaceTokenTrimmingLeftWhitespace(isMirrored ? ".Mirrored([" : "({");
  134. while (!this.tokens.matches1(tt.braceR)) {
  135. // ... is allowed at the end and has no runtime behavior.
  136. if (this.tokens.matches1(tt.ellipsis)) {
  137. this.tokens.removeToken();
  138. break;
  139. }
  140. this.processEnumElement(isSymbolEnum, hasInitializers);
  141. if (this.tokens.matches1(tt.comma)) {
  142. this.tokens.copyToken();
  143. }
  144. }
  145. this.tokens.replaceToken(isMirrored ? "]);" : "});");
  146. }
  147. /**
  148. * Process an individual enum element, producing either an array element or an
  149. * object element based on what type of enum this is.
  150. */
  151. processEnumElement(isSymbolEnum, hasInitializers) {
  152. if (isSymbolEnum) {
  153. // Symbol enums never have initializers and are expanded to object elements.
  154. // A, -> A: Symbol("A"),
  155. const elementName = this.tokens.identifierName();
  156. this.tokens.copyToken();
  157. this.tokens.appendCode(`: Symbol("${elementName}")`);
  158. } else if (hasInitializers) {
  159. // Initializers are expanded to object elements.
  160. // A = 1, -> A: 1,
  161. this.tokens.copyToken();
  162. this.tokens.replaceTokenTrimmingLeftWhitespace(":");
  163. this.tokens.copyToken();
  164. } else {
  165. // Enum elements without initializers become string literal array elements.
  166. // A, -> "A",
  167. this.tokens.replaceToken(`"${this.tokens.identifierName()}"`);
  168. }
  169. }
  170. }