OptionalChainingNullishTransformer.js 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155
  1. import {TokenType as tt} from "../parser/tokenizer/types";
  2. import Transformer from "./Transformer";
  3. /**
  4. * Transformer supporting the optional chaining and nullish coalescing operators.
  5. *
  6. * Tech plan here:
  7. * https://github.com/alangpierce/sucrase/wiki/Sucrase-Optional-Chaining-and-Nullish-Coalescing-Technical-Plan
  8. *
  9. * The prefix and suffix code snippets are handled by TokenProcessor, and this transformer handles
  10. * the operators themselves.
  11. */
  12. export default class OptionalChainingNullishTransformer extends Transformer {
  13. constructor( tokens, nameManager) {
  14. super();this.tokens = tokens;this.nameManager = nameManager;;
  15. }
  16. process() {
  17. if (this.tokens.matches1(tt.nullishCoalescing)) {
  18. const token = this.tokens.currentToken();
  19. if (this.tokens.tokens[token.nullishStartIndex].isAsyncOperation) {
  20. this.tokens.replaceTokenTrimmingLeftWhitespace(", async () => (");
  21. } else {
  22. this.tokens.replaceTokenTrimmingLeftWhitespace(", () => (");
  23. }
  24. return true;
  25. }
  26. if (this.tokens.matches1(tt._delete)) {
  27. const nextToken = this.tokens.tokenAtRelativeIndex(1);
  28. if (nextToken.isOptionalChainStart) {
  29. this.tokens.removeInitialToken();
  30. return true;
  31. }
  32. }
  33. const token = this.tokens.currentToken();
  34. const chainStart = token.subscriptStartIndex;
  35. if (
  36. chainStart != null &&
  37. this.tokens.tokens[chainStart].isOptionalChainStart &&
  38. // Super subscripts can't be optional (since super is never null/undefined), and the syntax
  39. // relies on the subscript being intact, so leave this token alone.
  40. this.tokens.tokenAtRelativeIndex(-1).type !== tt._super
  41. ) {
  42. const param = this.nameManager.claimFreeName("_");
  43. let arrowStartSnippet;
  44. if (
  45. chainStart > 0 &&
  46. this.tokens.matches1AtIndex(chainStart - 1, tt._delete) &&
  47. this.isLastSubscriptInChain()
  48. ) {
  49. // Delete operations are special: we already removed the delete keyword, and to still
  50. // perform a delete, we need to insert a delete in the very last part of the chain, which
  51. // in correct code will always be a property access.
  52. arrowStartSnippet = `${param} => delete ${param}`;
  53. } else {
  54. arrowStartSnippet = `${param} => ${param}`;
  55. }
  56. if (this.tokens.tokens[chainStart].isAsyncOperation) {
  57. arrowStartSnippet = `async ${arrowStartSnippet}`;
  58. }
  59. if (
  60. this.tokens.matches2(tt.questionDot, tt.parenL) ||
  61. this.tokens.matches2(tt.questionDot, tt.lessThan)
  62. ) {
  63. if (this.justSkippedSuper()) {
  64. this.tokens.appendCode(".bind(this)");
  65. }
  66. this.tokens.replaceTokenTrimmingLeftWhitespace(`, 'optionalCall', ${arrowStartSnippet}`);
  67. } else if (this.tokens.matches2(tt.questionDot, tt.bracketL)) {
  68. this.tokens.replaceTokenTrimmingLeftWhitespace(`, 'optionalAccess', ${arrowStartSnippet}`);
  69. } else if (this.tokens.matches1(tt.questionDot)) {
  70. this.tokens.replaceTokenTrimmingLeftWhitespace(`, 'optionalAccess', ${arrowStartSnippet}.`);
  71. } else if (this.tokens.matches1(tt.dot)) {
  72. this.tokens.replaceTokenTrimmingLeftWhitespace(`, 'access', ${arrowStartSnippet}.`);
  73. } else if (this.tokens.matches1(tt.bracketL)) {
  74. this.tokens.replaceTokenTrimmingLeftWhitespace(`, 'access', ${arrowStartSnippet}[`);
  75. } else if (this.tokens.matches1(tt.parenL)) {
  76. if (this.justSkippedSuper()) {
  77. this.tokens.appendCode(".bind(this)");
  78. }
  79. this.tokens.replaceTokenTrimmingLeftWhitespace(`, 'call', ${arrowStartSnippet}(`);
  80. } else {
  81. throw new Error("Unexpected subscript operator in optional chain.");
  82. }
  83. return true;
  84. }
  85. return false;
  86. }
  87. /**
  88. * Determine if the current token is the last of its chain, so that we know whether it's eligible
  89. * to have a delete op inserted.
  90. *
  91. * We can do this by walking forward until we determine one way or another. Each
  92. * isOptionalChainStart token must be paired with exactly one isOptionalChainEnd token after it in
  93. * a nesting way, so we can track depth and walk to the end of the chain (the point where the
  94. * depth goes negative) and see if any other subscript token is after us in the chain.
  95. */
  96. isLastSubscriptInChain() {
  97. let depth = 0;
  98. for (let i = this.tokens.currentIndex() + 1; ; i++) {
  99. if (i >= this.tokens.tokens.length) {
  100. throw new Error("Reached the end of the code while finding the end of the access chain.");
  101. }
  102. if (this.tokens.tokens[i].isOptionalChainStart) {
  103. depth++;
  104. } else if (this.tokens.tokens[i].isOptionalChainEnd) {
  105. depth--;
  106. }
  107. if (depth < 0) {
  108. return true;
  109. }
  110. // This subscript token is a later one in the same chain.
  111. if (depth === 0 && this.tokens.tokens[i].subscriptStartIndex != null) {
  112. return false;
  113. }
  114. }
  115. }
  116. /**
  117. * Determine if we are the open-paren in an expression like super.a()?.b.
  118. *
  119. * We can do this by walking backward to find the previous subscript. If that subscript was
  120. * preceded by a super, then we must be the subscript after it, so if this is a call expression,
  121. * we'll need to attach the right context.
  122. */
  123. justSkippedSuper() {
  124. let depth = 0;
  125. let index = this.tokens.currentIndex() - 1;
  126. while (true) {
  127. if (index < 0) {
  128. throw new Error(
  129. "Reached the start of the code while finding the start of the access chain.",
  130. );
  131. }
  132. if (this.tokens.tokens[index].isOptionalChainStart) {
  133. depth--;
  134. } else if (this.tokens.tokens[index].isOptionalChainEnd) {
  135. depth++;
  136. }
  137. if (depth < 0) {
  138. return false;
  139. }
  140. // This subscript token is a later one in the same chain.
  141. if (depth === 0 && this.tokens.tokens[index].subscriptStartIndex != null) {
  142. return this.tokens.tokens[index - 1].type === tt._super;
  143. }
  144. index--;
  145. }
  146. }
  147. }