OptionalChainingNullishTransformer.js 6.3 KB

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