'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); exports.default = jestHoist; function _template() { const data = require('@babel/template'); _template = function () { return data; }; return data; } function _types() { const data = require('@babel/types'); _types = function () { return data; }; return data; } /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * */ const JEST_GLOBAL_NAME = 'jest'; const JEST_GLOBALS_MODULE_NAME = '@jest/globals'; const JEST_GLOBALS_MODULE_JEST_EXPORT_NAME = 'jest'; const hoistedVariables = new WeakSet(); const hoistedJestGetters = new WeakSet(); const hoistedJestExpressions = new WeakSet(); // 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 ALLOWED_IDENTIFIERS = new Set( [ 'Array', 'ArrayBuffer', 'Boolean', 'BigInt', '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', 'exports', 'require', 'module', '__filename', '__dirname', 'undefined', ...Object.getOwnPropertyNames(globalThis) ].sort() ); const IDVisitor = { ReferencedIdentifier(path, {ids}) { ids.add(path); }, blacklist: [ 'TypeAnnotation', 'TSTypeAnnotation', 'TSTypeQuery', '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]; if (!moduleFactory.isFunction()) { throw moduleFactory.buildCodeFrameError( 'The second argument of `jest.mock` must be an inline function.\n', TypeError ); } const ids = new Set(); const parentScope = moduleFactory.parentPath.scope; // @ts-expect-error: ReferencedIdentifier and blacklist are not known on visitors moduleFactory.traverse(IDVisitor, { ids }); for (const id of ids) { const {name} = id.node; let found = false; let scope = id.scope; while (scope !== parentScope) { if (scope.bindings[name] != null) { found = true; break; } scope = scope.parent; } if (!found) { let isAllowedIdentifier = (scope.hasGlobal(name) && ALLOWED_IDENTIFIERS.has(name)) || /^mock/i.test(name) || // Allow istanbul's coverage variable to pass. /^(?:__)?cov/.test(name); if (!isAllowedIdentifier) { const binding = scope.bindings[name]; if (binding?.path.isVariableDeclarator()) { const {node} = binding.path; const initNode = node.init; if (initNode && binding.constant && scope.isPure(initNode, true)) { hoistedVariables.add(node); isAllowedIdentifier = true; } } else if (binding?.path.isImportSpecifier()) { const importDecl = binding.path.parentPath; const imported = binding.path.node.imported; if ( importDecl.node.source.value === JEST_GLOBALS_MODULE_NAME && ((0, _types().isIdentifier)(imported) ? imported.name : imported.value) === JEST_GLOBALS_MODULE_JEST_EXPORT_NAME ) { isAllowedIdentifier = true; // Imports are already hoisted, so we don't need to add it // to hoistedVariables. } } } if (!isAllowedIdentifier) { throw id.buildCodeFrameError( 'The module factory of `jest.mock()` is not allowed to ' + 'reference any out-of-scope variables.\n' + `Invalid variable access: ${name}\n` + `Allowed objects: ${Array.from(ALLOWED_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.\n', ReferenceError ); } } } 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; const createJestObjectGetter = (0, _template().statement)` function GETTER_NAME() { const { JEST_GLOBALS_MODULE_JEST_EXPORT_NAME } = require("JEST_GLOBALS_MODULE_NAME"); GETTER_NAME = () => JEST_GLOBALS_MODULE_JEST_EXPORT_NAME; return JEST_GLOBALS_MODULE_JEST_EXPORT_NAME; } `; const isJestObject = expression => { // global if ( expression.isIdentifier() && expression.node.name === JEST_GLOBAL_NAME && !expression.scope.hasBinding(JEST_GLOBAL_NAME) ) { return true; } // import { jest } from '@jest/globals' if ( expression.referencesImport( JEST_GLOBALS_MODULE_NAME, JEST_GLOBALS_MODULE_JEST_EXPORT_NAME ) ) { return true; } // import * as JestGlobals from '@jest/globals' if ( expression.isMemberExpression() && !expression.node.computed && expression.get('object').referencesImport(JEST_GLOBALS_MODULE_NAME, '*') && expression.node.property.type === 'Identifier' && expression.node.property.name === JEST_GLOBALS_MODULE_JEST_EXPORT_NAME ) { return true; } return false; }; const extractJestObjExprIfHoistable = expr => { if (!expr.isCallExpression()) { return null; } const callee = expr.get('callee'); const args = expr.get('arguments'); if (!callee.isMemberExpression() || callee.node.computed) { return null; } const object = callee.get('object'); const property = callee.get('property'); const propertyName = property.node.name; const jestObjExpr = isJestObject(object) ? object : // The Jest object could be returned from another call since the functions are all chainable. extractJestObjExprIfHoistable(object)?.path; if (!jestObjExpr) { return null; } // Important: Call the function check last // It might throw an error to display to the user, // which should only happen if we're already sure it's a call on the Jest object. const functionIsHoistable = FUNCTIONS[propertyName]?.(args) ?? false; let functionHasHoistableScope = functionIsHoistable; for ( let path = expr; path && !functionHasHoistableScope; path = path.parentPath ) { functionHasHoistableScope = hoistedJestExpressions.has( // @ts-expect-error: it's ok if path.node is not an Expression, .has will // just return false. path.node ); } if (functionHasHoistableScope) { hoistedJestExpressions.add(expr.node); return { hoist: functionIsHoistable, path: jestObjExpr }; } return null; }; /* eslint-disable sort-keys */ function jestHoist() { return { pre({path: program}) { this.declareJestObjGetterIdentifier = () => { if (this.jestObjGetterIdentifier) { return this.jestObjGetterIdentifier; } this.jestObjGetterIdentifier = program.scope.generateUidIdentifier('getJestObj'); program.unshiftContainer('body', [ createJestObjectGetter({ GETTER_NAME: this.jestObjGetterIdentifier.name, JEST_GLOBALS_MODULE_JEST_EXPORT_NAME, JEST_GLOBALS_MODULE_NAME }) ]); return this.jestObjGetterIdentifier; }; }, visitor: { ExpressionStatement(exprStmt) { const jestObjInfo = extractJestObjExprIfHoistable( exprStmt.get('expression') ); if (jestObjInfo) { const jestCallExpr = (0, _types().callExpression)( this.declareJestObjGetterIdentifier(), [] ); jestObjInfo.path.replaceWith(jestCallExpr); if (jestObjInfo.hoist) { hoistedJestGetters.add(jestCallExpr); } } } }, // in `post` to make sure we come after an import transform and can unshift above the `require`s post({path: program}) { visitBlock(program); program.traverse({ BlockStatement: visitBlock }); function visitBlock(block) { // use a temporary empty statement instead of the real first statement, which may itself be hoisted const [varsHoistPoint, callsHoistPoint] = block.unshiftContainer( 'body', [(0, _types().emptyStatement)(), (0, _types().emptyStatement)()] ); block.traverse({ CallExpression: visitCallExpr, VariableDeclarator: visitVariableDeclarator, // do not traverse into nested blocks, or we'll hoist calls in there out to this block blacklist: ['BlockStatement'] }); callsHoistPoint.remove(); varsHoistPoint.remove(); function visitCallExpr(callExpr) { if (hoistedJestGetters.has(callExpr.node)) { const mockStmt = callExpr.getStatementParent(); if (mockStmt) { const mockStmtParent = mockStmt.parentPath; if (mockStmtParent.isBlock()) { const mockStmtNode = mockStmt.node; mockStmt.remove(); callsHoistPoint.insertBefore(mockStmtNode); } } } } function visitVariableDeclarator(varDecl) { if (hoistedVariables.has(varDecl.node)) { // should be assert function, but it's not. So let's cast below varDecl.parentPath.assertVariableDeclaration(); const {kind, declarations} = varDecl.parent; if (declarations.length === 1) { varDecl.parentPath.remove(); } else { varDecl.remove(); } varsHoistPoint.insertBefore( (0, _types().variableDeclaration)(kind, [varDecl.node]) ); } } } } }; } /* eslint-enable */