JestHoistTransformer.js 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111
  1. function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }
  2. import {TokenType as tt} from "../parser/tokenizer/types";
  3. import Transformer from "./Transformer";
  4. const JEST_GLOBAL_NAME = "jest";
  5. const HOISTED_METHODS = ["mock", "unmock", "enableAutomock", "disableAutomock"];
  6. /**
  7. * Implementation of babel-plugin-jest-hoist, which hoists up some jest method
  8. * calls above the imports to allow them to override other imports.
  9. *
  10. * To preserve line numbers, rather than directly moving the jest.mock code, we
  11. * wrap each invocation in a function statement and then call the function from
  12. * the top of the file.
  13. */
  14. export default class JestHoistTransformer extends Transformer {
  15. __init() {this.hoistedFunctionNames = []}
  16. constructor(
  17. rootTransformer,
  18. tokens,
  19. nameManager,
  20. importProcessor,
  21. ) {
  22. super();this.rootTransformer = rootTransformer;this.tokens = tokens;this.nameManager = nameManager;this.importProcessor = importProcessor;JestHoistTransformer.prototype.__init.call(this);;
  23. }
  24. process() {
  25. if (
  26. this.tokens.currentToken().scopeDepth === 0 &&
  27. this.tokens.matches4(tt.name, tt.dot, tt.name, tt.parenL) &&
  28. this.tokens.identifierName() === JEST_GLOBAL_NAME
  29. ) {
  30. // TODO: This only works if imports transform is active, which it will be for jest.
  31. // But if jest adds module support and we no longer need the import transform, this needs fixing.
  32. if (_optionalChain([this, 'access', _ => _.importProcessor, 'optionalAccess', _2 => _2.getGlobalNames, 'call', _3 => _3(), 'optionalAccess', _4 => _4.has, 'call', _5 => _5(JEST_GLOBAL_NAME)])) {
  33. return false;
  34. }
  35. return this.extractHoistedCalls();
  36. }
  37. return false;
  38. }
  39. getHoistedCode() {
  40. if (this.hoistedFunctionNames.length > 0) {
  41. // This will be placed before module interop code, but that's fine since
  42. // imports aren't allowed in module mock factories.
  43. return this.hoistedFunctionNames.map((name) => `${name}();`).join("");
  44. }
  45. return "";
  46. }
  47. /**
  48. * Extracts any methods calls on the jest-object that should be hoisted.
  49. *
  50. * According to the jest docs, https://jestjs.io/docs/en/jest-object#jestmockmodulename-factory-options,
  51. * mock, unmock, enableAutomock, disableAutomock, are the methods that should be hoisted.
  52. *
  53. * We do not apply the same checks of the arguments as babel-plugin-jest-hoist does.
  54. */
  55. extractHoistedCalls() {
  56. // We're handling a chain of calls where `jest` may or may not need to be inserted for each call
  57. // in the chain, so remove the initial `jest` to make the loop implementation cleaner.
  58. this.tokens.removeToken();
  59. // Track some state so that multiple non-hoisted chained calls in a row keep their chaining
  60. // syntax.
  61. let followsNonHoistedJestCall = false;
  62. // Iterate through all chained calls on the jest object.
  63. while (this.tokens.matches3(tt.dot, tt.name, tt.parenL)) {
  64. const methodName = this.tokens.identifierNameAtIndex(this.tokens.currentIndex() + 1);
  65. const shouldHoist = HOISTED_METHODS.includes(methodName);
  66. if (shouldHoist) {
  67. // We've matched e.g. `.mock(...)` or similar call.
  68. // Replace the initial `.` with `function __jestHoist(){jest.`
  69. const hoistedFunctionName = this.nameManager.claimFreeName("__jestHoist");
  70. this.hoistedFunctionNames.push(hoistedFunctionName);
  71. this.tokens.replaceToken(`function ${hoistedFunctionName}(){${JEST_GLOBAL_NAME}.`);
  72. this.tokens.copyToken();
  73. this.tokens.copyToken();
  74. this.rootTransformer.processBalancedCode();
  75. this.tokens.copyExpectedToken(tt.parenR);
  76. this.tokens.appendCode(";}");
  77. followsNonHoistedJestCall = false;
  78. } else {
  79. // This is a non-hoisted method, so just transform the code as usual.
  80. if (followsNonHoistedJestCall) {
  81. // If we didn't hoist the previous call, we can leave the code as-is to chain off of the
  82. // previous method call. It's important to preserve the code here because we don't know
  83. // for sure that the method actually returned the jest object for chaining.
  84. this.tokens.copyToken();
  85. } else {
  86. // If we hoisted the previous call, we know it returns the jest object back, so we insert
  87. // the identifier `jest` to continue the chain.
  88. this.tokens.replaceToken(`${JEST_GLOBAL_NAME}.`);
  89. }
  90. this.tokens.copyToken();
  91. this.tokens.copyToken();
  92. this.rootTransformer.processBalancedCode();
  93. this.tokens.copyExpectedToken(tt.parenR);
  94. followsNonHoistedJestCall = true;
  95. }
  96. }
  97. return true;
  98. }
  99. }