ReactDisplayNameTransformer.js 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160
  1. import {IdentifierRole} from "../parser/tokenizer";
  2. import {TokenType as tt} from "../parser/tokenizer/types";
  3. import Transformer from "./Transformer";
  4. /**
  5. * Implementation of babel-plugin-transform-react-display-name, which adds a
  6. * display name to usages of React.createClass and createReactClass.
  7. */
  8. export default class ReactDisplayNameTransformer extends Transformer {
  9. constructor(
  10. rootTransformer,
  11. tokens,
  12. importProcessor,
  13. options,
  14. ) {
  15. super();this.rootTransformer = rootTransformer;this.tokens = tokens;this.importProcessor = importProcessor;this.options = options;;
  16. }
  17. process() {
  18. const startIndex = this.tokens.currentIndex();
  19. if (this.tokens.identifierName() === "createReactClass") {
  20. const newName =
  21. this.importProcessor && this.importProcessor.getIdentifierReplacement("createReactClass");
  22. if (newName) {
  23. this.tokens.replaceToken(`(0, ${newName})`);
  24. } else {
  25. this.tokens.copyToken();
  26. }
  27. this.tryProcessCreateClassCall(startIndex);
  28. return true;
  29. }
  30. if (
  31. this.tokens.matches3(tt.name, tt.dot, tt.name) &&
  32. this.tokens.identifierName() === "React" &&
  33. this.tokens.identifierNameAtIndex(this.tokens.currentIndex() + 2) === "createClass"
  34. ) {
  35. const newName = this.importProcessor
  36. ? this.importProcessor.getIdentifierReplacement("React") || "React"
  37. : "React";
  38. if (newName) {
  39. this.tokens.replaceToken(newName);
  40. this.tokens.copyToken();
  41. this.tokens.copyToken();
  42. } else {
  43. this.tokens.copyToken();
  44. this.tokens.copyToken();
  45. this.tokens.copyToken();
  46. }
  47. this.tryProcessCreateClassCall(startIndex);
  48. return true;
  49. }
  50. return false;
  51. }
  52. /**
  53. * This is called with the token position at the open-paren.
  54. */
  55. tryProcessCreateClassCall(startIndex) {
  56. const displayName = this.findDisplayName(startIndex);
  57. if (!displayName) {
  58. return;
  59. }
  60. if (this.classNeedsDisplayName()) {
  61. this.tokens.copyExpectedToken(tt.parenL);
  62. this.tokens.copyExpectedToken(tt.braceL);
  63. this.tokens.appendCode(`displayName: '${displayName}',`);
  64. this.rootTransformer.processBalancedCode();
  65. this.tokens.copyExpectedToken(tt.braceR);
  66. this.tokens.copyExpectedToken(tt.parenR);
  67. }
  68. }
  69. findDisplayName(startIndex) {
  70. if (startIndex < 2) {
  71. return null;
  72. }
  73. if (this.tokens.matches2AtIndex(startIndex - 2, tt.name, tt.eq)) {
  74. // This is an assignment (or declaration) and the LHS is either an identifier or a member
  75. // expression ending in an identifier, so use that identifier name.
  76. return this.tokens.identifierNameAtIndex(startIndex - 2);
  77. }
  78. if (
  79. startIndex >= 2 &&
  80. this.tokens.tokens[startIndex - 2].identifierRole === IdentifierRole.ObjectKey
  81. ) {
  82. // This is an object literal value.
  83. return this.tokens.identifierNameAtIndex(startIndex - 2);
  84. }
  85. if (this.tokens.matches2AtIndex(startIndex - 2, tt._export, tt._default)) {
  86. return this.getDisplayNameFromFilename();
  87. }
  88. return null;
  89. }
  90. getDisplayNameFromFilename() {
  91. const filePath = this.options.filePath || "unknown";
  92. const pathSegments = filePath.split("/");
  93. const filename = pathSegments[pathSegments.length - 1];
  94. const dotIndex = filename.lastIndexOf(".");
  95. const baseFilename = dotIndex === -1 ? filename : filename.slice(0, dotIndex);
  96. if (baseFilename === "index" && pathSegments[pathSegments.length - 2]) {
  97. return pathSegments[pathSegments.length - 2];
  98. } else {
  99. return baseFilename;
  100. }
  101. }
  102. /**
  103. * We only want to add a display name when this is a function call containing
  104. * one argument, which is an object literal without `displayName` as an
  105. * existing key.
  106. */
  107. classNeedsDisplayName() {
  108. let index = this.tokens.currentIndex();
  109. if (!this.tokens.matches2(tt.parenL, tt.braceL)) {
  110. return false;
  111. }
  112. // The block starts on the {, and we expect any displayName key to be in
  113. // that context. We need to ignore other other contexts to avoid matching
  114. // nested displayName keys.
  115. const objectStartIndex = index + 1;
  116. const objectContextId = this.tokens.tokens[objectStartIndex].contextId;
  117. if (objectContextId == null) {
  118. throw new Error("Expected non-null context ID on object open-brace.");
  119. }
  120. for (; index < this.tokens.tokens.length; index++) {
  121. const token = this.tokens.tokens[index];
  122. if (token.type === tt.braceR && token.contextId === objectContextId) {
  123. index++;
  124. break;
  125. }
  126. if (
  127. this.tokens.identifierNameAtIndex(index) === "displayName" &&
  128. this.tokens.tokens[index].identifierRole === IdentifierRole.ObjectKey &&
  129. token.contextId === objectContextId
  130. ) {
  131. // We found a displayName key, so bail out.
  132. return false;
  133. }
  134. }
  135. if (index === this.tokens.tokens.length) {
  136. throw new Error("Unexpected end of input when processing React class.");
  137. }
  138. // If we got this far, we know we have createClass with an object with no
  139. // display name, so we want to proceed as long as that was the only argument.
  140. return (
  141. this.tokens.matches1AtIndex(index, tt.parenR) ||
  142. this.tokens.matches2AtIndex(index, tt.comma, tt.parenR)
  143. );
  144. }
  145. }