209 lines
		
	
	
		
			5.3 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			209 lines
		
	
	
		
			5.3 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| 'use strict';
 | |
| 
 | |
| /**
 | |
|  * Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
 | |
|  *
 | |
|  * This source code is licensed under the MIT license found in the
 | |
|  * LICENSE file in the root directory of this source tree.
 | |
|  *
 | |
|  */
 | |
| // Only used for types
 | |
| // eslint-disable-next-line
 | |
| // eslint-disable-next-line
 | |
| const invariant = (condition, message) => {
 | |
|   if (!condition) {
 | |
|     throw new Error('babel-plugin-jest-hoist: ' + message);
 | |
|   }
 | |
| }; // We allow `jest`, `expect`, `require`, all default Node.js globals and all
 | |
| // ES2015 built-ins to be used inside of a `jest.mock` factory.
 | |
| // We also allow variables prefixed with `mock` as an escape-hatch.
 | |
| 
 | |
| const WHITELISTED_IDENTIFIERS = new Set([
 | |
|   'Array',
 | |
|   'ArrayBuffer',
 | |
|   'Boolean',
 | |
|   'DataView',
 | |
|   'Date',
 | |
|   'Error',
 | |
|   'EvalError',
 | |
|   'Float32Array',
 | |
|   'Float64Array',
 | |
|   'Function',
 | |
|   'Generator',
 | |
|   'GeneratorFunction',
 | |
|   'Infinity',
 | |
|   'Int16Array',
 | |
|   'Int32Array',
 | |
|   'Int8Array',
 | |
|   'InternalError',
 | |
|   'Intl',
 | |
|   'JSON',
 | |
|   'Map',
 | |
|   'Math',
 | |
|   'NaN',
 | |
|   'Number',
 | |
|   'Object',
 | |
|   'Promise',
 | |
|   'Proxy',
 | |
|   'RangeError',
 | |
|   'ReferenceError',
 | |
|   'Reflect',
 | |
|   'RegExp',
 | |
|   'Set',
 | |
|   'String',
 | |
|   'Symbol',
 | |
|   'SyntaxError',
 | |
|   'TypeError',
 | |
|   'URIError',
 | |
|   'Uint16Array',
 | |
|   'Uint32Array',
 | |
|   'Uint8Array',
 | |
|   'Uint8ClampedArray',
 | |
|   'WeakMap',
 | |
|   'WeakSet',
 | |
|   'arguments',
 | |
|   'console',
 | |
|   'expect',
 | |
|   'isNaN',
 | |
|   'jest',
 | |
|   'parseFloat',
 | |
|   'parseInt',
 | |
|   'require',
 | |
|   'undefined'
 | |
| ]);
 | |
| Object.keys(global).forEach(name => {
 | |
|   WHITELISTED_IDENTIFIERS.add(name);
 | |
| });
 | |
| const JEST_GLOBAL = {
 | |
|   name: 'jest'
 | |
| }; // TODO: Should be Visitor<{ids: Set<NodePath<Identifier>>}>, but `ReferencedIdentifier` doesn't exist
 | |
| 
 | |
| const IDVisitor = {
 | |
|   ReferencedIdentifier(path) {
 | |
|     // @ts-ignore: passed as Visitor State
 | |
|     this.ids.add(path);
 | |
|   },
 | |
| 
 | |
|   blacklist: ['TypeAnnotation', 'TSTypeAnnotation', 'TSTypeReference']
 | |
| };
 | |
| const FUNCTIONS = Object.create(null);
 | |
| 
 | |
| FUNCTIONS.mock = args => {
 | |
|   if (args.length === 1) {
 | |
|     return args[0].isStringLiteral() || args[0].isLiteral();
 | |
|   } else if (args.length === 2 || args.length === 3) {
 | |
|     const moduleFactory = args[1];
 | |
|     invariant(
 | |
|       moduleFactory.isFunction(),
 | |
|       'The second argument of `jest.mock` must be an inline function.'
 | |
|     );
 | |
|     const ids = new Set();
 | |
|     const parentScope = moduleFactory.parentPath.scope; // @ts-ignore: Same as above: ReferencedIdentifier doesn't exist
 | |
| 
 | |
|     moduleFactory.traverse(IDVisitor, {
 | |
|       ids
 | |
|     });
 | |
|     var _iteratorNormalCompletion = true;
 | |
|     var _didIteratorError = false;
 | |
|     var _iteratorError = undefined;
 | |
| 
 | |
|     try {
 | |
|       for (
 | |
|         var _iterator = ids[Symbol.iterator](), _step;
 | |
|         !(_iteratorNormalCompletion = (_step = _iterator.next()).done);
 | |
|         _iteratorNormalCompletion = true
 | |
|       ) {
 | |
|         const id = _step.value;
 | |
|         const name = id.node.name;
 | |
|         let found = false;
 | |
|         let scope = id.scope;
 | |
| 
 | |
|         while (scope !== parentScope) {
 | |
|           if (scope.bindings[name]) {
 | |
|             found = true;
 | |
|             break;
 | |
|           }
 | |
| 
 | |
|           scope = scope.parent;
 | |
|         }
 | |
| 
 | |
|         if (!found) {
 | |
|           invariant(
 | |
|             (scope.hasGlobal(name) && WHITELISTED_IDENTIFIERS.has(name)) ||
 | |
|             /^mock/i.test(name) || // Allow istanbul's coverage variable to pass.
 | |
|               /^(?:__)?cov/.test(name),
 | |
|             'The module factory of `jest.mock()` is not allowed to ' +
 | |
|               'reference any out-of-scope variables.\n' +
 | |
|               'Invalid variable access: ' +
 | |
|               name +
 | |
|               '\n' +
 | |
|               'Whitelisted objects: ' +
 | |
|               Array.from(WHITELISTED_IDENTIFIERS).join(', ') +
 | |
|               '.\n' +
 | |
|               'Note: This is a precaution to guard against uninitialized mock ' +
 | |
|               'variables. If it is ensured that the mock is required lazily, ' +
 | |
|               'variable names prefixed with `mock` (case insensitive) are permitted.'
 | |
|           );
 | |
|         }
 | |
|       }
 | |
|     } catch (err) {
 | |
|       _didIteratorError = true;
 | |
|       _iteratorError = err;
 | |
|     } finally {
 | |
|       try {
 | |
|         if (!_iteratorNormalCompletion && _iterator.return != null) {
 | |
|           _iterator.return();
 | |
|         }
 | |
|       } finally {
 | |
|         if (_didIteratorError) {
 | |
|           throw _iteratorError;
 | |
|         }
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     return true;
 | |
|   }
 | |
| 
 | |
|   return false;
 | |
| };
 | |
| 
 | |
| FUNCTIONS.unmock = args => args.length === 1 && args[0].isStringLiteral();
 | |
| 
 | |
| FUNCTIONS.deepUnmock = args => args.length === 1 && args[0].isStringLiteral();
 | |
| 
 | |
| FUNCTIONS.disableAutomock = FUNCTIONS.enableAutomock = args =>
 | |
|   args.length === 0;
 | |
| 
 | |
| module.exports = () => {
 | |
|   const shouldHoistExpression = expr => {
 | |
|     if (!expr.isCallExpression()) {
 | |
|       return false;
 | |
|     }
 | |
| 
 | |
|     const callee = expr.get('callee');
 | |
|     const expressionArguments = expr.get('arguments'); // TODO: avoid type casts - the types can be arrays (is it possible to ignore that without casting?)
 | |
| 
 | |
|     const object = callee.get('object');
 | |
|     const property = callee.get('property');
 | |
|     return (
 | |
|       property.isIdentifier() &&
 | |
|       FUNCTIONS[property.node.name] &&
 | |
|       (object.isIdentifier(JEST_GLOBAL) ||
 | |
|         (callee.isMemberExpression() && shouldHoistExpression(object))) &&
 | |
|       FUNCTIONS[property.node.name](expressionArguments)
 | |
|     );
 | |
|   };
 | |
| 
 | |
|   const visitor = {
 | |
|     ExpressionStatement(path) {
 | |
|       if (shouldHoistExpression(path.get('expression'))) {
 | |
|         // @ts-ignore: private, magical property
 | |
|         path.node._blockHoist = Infinity;
 | |
|       }
 | |
|     }
 | |
|   };
 | |
|   return {
 | |
|     visitor
 | |
|   };
 | |
| };
 |