UNPKG

10.3 kBJavaScriptView Raw
1'use strict';
2
3Object.defineProperty(exports, '__esModule', {
4 value: true
5});
6exports.default = void 0;
7
8function _template() {
9 const data = require('@babel/template');
10
11 _template = function () {
12 return data;
13 };
14
15 return data;
16}
17
18function _types() {
19 const data = require('@babel/types');
20
21 _types = function () {
22 return data;
23 };
24
25 return data;
26}
27
28/**
29 * Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
30 *
31 * This source code is licensed under the MIT license found in the
32 * LICENSE file in the root directory of this source tree.
33 *
34 */
35const JEST_GLOBAL_NAME = 'jest';
36const JEST_GLOBALS_MODULE_NAME = '@jest/globals';
37const JEST_GLOBALS_MODULE_JEST_EXPORT_NAME = 'jest';
38const hoistedVariables = new WeakSet(); // We allow `jest`, `expect`, `require`, all default Node.js globals and all
39// ES2015 built-ins to be used inside of a `jest.mock` factory.
40// We also allow variables prefixed with `mock` as an escape-hatch.
41
42const ALLOWED_IDENTIFIERS = new Set(
43 [
44 'Array',
45 'ArrayBuffer',
46 'Boolean',
47 'BigInt',
48 'DataView',
49 'Date',
50 'Error',
51 'EvalError',
52 'Float32Array',
53 'Float64Array',
54 'Function',
55 'Generator',
56 'GeneratorFunction',
57 'Infinity',
58 'Int16Array',
59 'Int32Array',
60 'Int8Array',
61 'InternalError',
62 'Intl',
63 'JSON',
64 'Map',
65 'Math',
66 'NaN',
67 'Number',
68 'Object',
69 'Promise',
70 'Proxy',
71 'RangeError',
72 'ReferenceError',
73 'Reflect',
74 'RegExp',
75 'Set',
76 'String',
77 'Symbol',
78 'SyntaxError',
79 'TypeError',
80 'URIError',
81 'Uint16Array',
82 'Uint32Array',
83 'Uint8Array',
84 'Uint8ClampedArray',
85 'WeakMap',
86 'WeakSet',
87 'arguments',
88 'console',
89 'expect',
90 'isNaN',
91 'jest',
92 'parseFloat',
93 'parseInt',
94 'exports',
95 'require',
96 'module',
97 '__filename',
98 '__dirname',
99 'undefined',
100 ...Object.getOwnPropertyNames(global)
101 ].sort()
102);
103const IDVisitor = {
104 ReferencedIdentifier(path, {ids}) {
105 ids.add(path);
106 },
107
108 blacklist: ['TypeAnnotation', 'TSTypeAnnotation', 'TSTypeReference']
109};
110const FUNCTIONS = Object.create(null);
111
112FUNCTIONS.mock = args => {
113 if (args.length === 1) {
114 return args[0].isStringLiteral() || args[0].isLiteral();
115 } else if (args.length === 2 || args.length === 3) {
116 const moduleFactory = args[1];
117
118 if (!moduleFactory.isFunction()) {
119 throw moduleFactory.buildCodeFrameError(
120 'The second argument of `jest.mock` must be an inline function.\n',
121 TypeError
122 );
123 }
124
125 const ids = new Set();
126 const parentScope = moduleFactory.parentPath.scope; // @ts-expect-error: ReferencedIdentifier and blacklist are not known on visitors
127
128 moduleFactory.traverse(IDVisitor, {
129 ids
130 });
131
132 for (const id of ids) {
133 const {name} = id.node;
134 let found = false;
135 let scope = id.scope;
136
137 while (scope !== parentScope) {
138 if (scope.bindings[name]) {
139 found = true;
140 break;
141 }
142
143 scope = scope.parent;
144 }
145
146 if (!found) {
147 let isAllowedIdentifier =
148 (scope.hasGlobal(name) && ALLOWED_IDENTIFIERS.has(name)) ||
149 /^mock/i.test(name) || // Allow istanbul's coverage variable to pass.
150 /^(?:__)?cov/.test(name);
151
152 if (!isAllowedIdentifier) {
153 const binding = scope.bindings[name];
154
155 if (
156 binding !== null &&
157 binding !== void 0 &&
158 binding.path.isVariableDeclarator()
159 ) {
160 const {node} = binding.path;
161 const initNode = node.init;
162
163 if (initNode && binding.constant && scope.isPure(initNode, true)) {
164 hoistedVariables.add(node);
165 isAllowedIdentifier = true;
166 }
167 }
168 }
169
170 if (!isAllowedIdentifier) {
171 throw id.buildCodeFrameError(
172 'The module factory of `jest.mock()` is not allowed to ' +
173 'reference any out-of-scope variables.\n' +
174 'Invalid variable access: ' +
175 name +
176 '\n' +
177 'Allowed objects: ' +
178 Array.from(ALLOWED_IDENTIFIERS).join(', ') +
179 '.\n' +
180 'Note: This is a precaution to guard against uninitialized mock ' +
181 'variables. If it is ensured that the mock is required lazily, ' +
182 'variable names prefixed with `mock` (case insensitive) are permitted.\n',
183 ReferenceError
184 );
185 }
186 }
187 }
188
189 return true;
190 }
191
192 return false;
193};
194
195FUNCTIONS.unmock = args => args.length === 1 && args[0].isStringLiteral();
196
197FUNCTIONS.deepUnmock = args => args.length === 1 && args[0].isStringLiteral();
198
199FUNCTIONS.disableAutomock = FUNCTIONS.enableAutomock = args =>
200 args.length === 0;
201
202const createJestObjectGetter = (0, _template().statement)`
203function GETTER_NAME() {
204 const { JEST_GLOBALS_MODULE_JEST_EXPORT_NAME } = require("JEST_GLOBALS_MODULE_NAME");
205 GETTER_NAME = () => JEST_GLOBALS_MODULE_JEST_EXPORT_NAME;
206 return JEST_GLOBALS_MODULE_JEST_EXPORT_NAME;
207}
208`;
209
210const isJestObject = expression => {
211 // global
212 if (
213 expression.isIdentifier() &&
214 expression.node.name === JEST_GLOBAL_NAME &&
215 !expression.scope.hasBinding(JEST_GLOBAL_NAME)
216 ) {
217 return true;
218 } // import { jest } from '@jest/globals'
219
220 if (
221 expression.referencesImport(
222 JEST_GLOBALS_MODULE_NAME,
223 JEST_GLOBALS_MODULE_JEST_EXPORT_NAME
224 )
225 ) {
226 return true;
227 } // import * as JestGlobals from '@jest/globals'
228
229 if (
230 expression.isMemberExpression() &&
231 !expression.node.computed &&
232 expression.get('object').referencesImport(JEST_GLOBALS_MODULE_NAME, '*') &&
233 expression.node.property.type === 'Identifier' &&
234 expression.node.property.name === JEST_GLOBALS_MODULE_JEST_EXPORT_NAME
235 ) {
236 return true;
237 }
238
239 return false;
240};
241
242const extractJestObjExprIfHoistable = expr => {
243 var _FUNCTIONS$propertyNa;
244
245 if (!expr.isCallExpression()) {
246 return null;
247 }
248
249 const callee = expr.get('callee');
250 const args = expr.get('arguments');
251
252 if (!callee.isMemberExpression() || callee.node.computed) {
253 return null;
254 }
255
256 const object = callee.get('object');
257 const property = callee.get('property');
258 const propertyName = property.node.name;
259 const jestObjExpr = isJestObject(object)
260 ? object // The Jest object could be returned from another call since the functions are all chainable.
261 : extractJestObjExprIfHoistable(object);
262
263 if (!jestObjExpr) {
264 return null;
265 } // Important: Call the function check last
266 // It might throw an error to display to the user,
267 // which should only happen if we're already sure it's a call on the Jest object.
268
269 const functionLooksHoistable =
270 (_FUNCTIONS$propertyNa = FUNCTIONS[propertyName]) === null ||
271 _FUNCTIONS$propertyNa === void 0
272 ? void 0
273 : _FUNCTIONS$propertyNa.call(FUNCTIONS, args);
274 return functionLooksHoistable ? jestObjExpr : null;
275};
276/* eslint-disable sort-keys */
277
278var _default = () => ({
279 pre({path: program}) {
280 this.declareJestObjGetterIdentifier = () => {
281 if (this.jestObjGetterIdentifier) {
282 return this.jestObjGetterIdentifier;
283 }
284
285 this.jestObjGetterIdentifier =
286 program.scope.generateUidIdentifier('getJestObj');
287 program.unshiftContainer('body', [
288 createJestObjectGetter({
289 GETTER_NAME: this.jestObjGetterIdentifier.name,
290 JEST_GLOBALS_MODULE_JEST_EXPORT_NAME,
291 JEST_GLOBALS_MODULE_NAME
292 })
293 ]);
294 return this.jestObjGetterIdentifier;
295 };
296 },
297
298 visitor: {
299 ExpressionStatement(exprStmt) {
300 const jestObjExpr = extractJestObjExprIfHoistable(
301 exprStmt.get('expression')
302 );
303
304 if (jestObjExpr) {
305 jestObjExpr.replaceWith(
306 (0, _types().callExpression)(
307 this.declareJestObjGetterIdentifier(),
308 []
309 )
310 );
311 }
312 }
313 },
314
315 // in `post` to make sure we come after an import transform and can unshift above the `require`s
316 post({path: program}) {
317 const self = this;
318 visitBlock(program);
319 program.traverse({
320 BlockStatement: visitBlock
321 });
322
323 function visitBlock(block) {
324 // use a temporary empty statement instead of the real first statement, which may itself be hoisted
325 const [varsHoistPoint, callsHoistPoint] = block.unshiftContainer('body', [
326 (0, _types().emptyStatement)(),
327 (0, _types().emptyStatement)()
328 ]);
329 block.traverse({
330 CallExpression: visitCallExpr,
331 VariableDeclarator: visitVariableDeclarator,
332 // do not traverse into nested blocks, or we'll hoist calls in there out to this block
333 blacklist: ['BlockStatement']
334 });
335 callsHoistPoint.remove();
336 varsHoistPoint.remove();
337
338 function visitCallExpr(callExpr) {
339 var _self$jestObjGetterId;
340
341 const {
342 node: {callee}
343 } = callExpr;
344
345 if (
346 (0, _types().isIdentifier)(callee) &&
347 callee.name ===
348 ((_self$jestObjGetterId = self.jestObjGetterIdentifier) === null ||
349 _self$jestObjGetterId === void 0
350 ? void 0
351 : _self$jestObjGetterId.name)
352 ) {
353 const mockStmt = callExpr.getStatementParent();
354
355 if (mockStmt) {
356 const mockStmtParent = mockStmt.parentPath;
357
358 if (mockStmtParent.isBlock()) {
359 const mockStmtNode = mockStmt.node;
360 mockStmt.remove();
361 callsHoistPoint.insertBefore(mockStmtNode);
362 }
363 }
364 }
365 }
366
367 function visitVariableDeclarator(varDecl) {
368 if (hoistedVariables.has(varDecl.node)) {
369 // should be assert function, but it's not. So let's cast below
370 varDecl.parentPath.assertVariableDeclaration();
371 const {kind, declarations} = varDecl.parent;
372
373 if (declarations.length === 1) {
374 varDecl.parentPath.remove();
375 } else {
376 varDecl.remove();
377 }
378
379 varsHoistPoint.insertBefore(
380 (0, _types().variableDeclaration)(kind, [varDecl.node])
381 );
382 }
383 }
384 }
385 }
386});
387/* eslint-enable */
388
389exports.default = _default;