aoc/node_modules/babel-plugin-jest-hoist/build/index.js

369 lines
11 KiB
JavaScript
Raw Normal View History

2022-12-03 22:11:42 +00:00
'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) 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.
*
*/
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 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);
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.
let functionLooksHoistableOrInHoistable = FUNCTIONS[propertyName]?.(args);
for (
let path = expr;
path && !functionLooksHoistableOrInHoistable;
path = path.parentPath
) {
functionLooksHoistableOrInHoistable = hoistedJestExpressions.has(
// @ts-expect-error: it's ok if path.node is not an Expression, .has will
// just return false.
path.node
);
}
if (functionLooksHoistableOrInHoistable) {
hoistedJestExpressions.add(expr.node);
return 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 jestObjExpr = extractJestObjExprIfHoistable(
exprStmt.get('expression')
);
if (jestObjExpr) {
jestObjExpr.replaceWith(
(0, _types().callExpression)(
this.declareJestObjGetterIdentifier(),
[]
)
);
}
}
},
// in `post` to make sure we come after an import transform and can unshift above the `require`s
post({path: program}) {
// eslint-disable-next-line @typescript-eslint/no-this-alias
const self = this;
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) {
const {
node: {callee}
} = callExpr;
if (
(0, _types().isIdentifier)(callee) &&
callee.name === self.jestObjGetterIdentifier?.name
) {
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 */