Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Hoist static variables before hoisted mocks #653

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -6,6 +6,8 @@ jest.mock('./App', () => () => <div>Hello world</div>);

↓ ↓ ↓ ↓ ↓ ↓

var _jsxFileName = "/root/project/src/file.js";

_getJestObj().mock("./App", () => () =>
/*#__PURE__*/ _jsxDEV(
"div",
Expand All @@ -15,7 +17,7 @@ _getJestObj().mock("./App", () => () =>
void 0,
false,
{
fileName: _hoistedMockFactoryVariable(),
fileName: _jsxFileName,
lineNumber: 1,
columnNumber: 32
},
Expand All @@ -24,7 +26,6 @@ _getJestObj().mock("./App", () => () =>
);

import { jsxDEV as _jsxDEV } from "react/jsx-dev-runtime";
var _jsxFileName = "/root/project/src/file.js";

function _getJestObj() {
const { jest } = require("@jest/globals");
Expand All @@ -34,10 +35,6 @@ function _getJestObj() {
return jest;
}

function _hoistedMockFactoryVariable() {
return "/root/project/src/file.js";
}


`;

Expand Down
53 changes: 28 additions & 25 deletions packages/babel-plugin-jest-hoist/src/index.ts
Expand Up @@ -16,15 +16,20 @@ import {
Identifier,
Node,
Program,
VariableDeclaration,
VariableDeclarator,
callExpression,
emptyStatement,
isIdentifier,
variableDeclaration,
} from '@babel/types';

const JEST_GLOBAL_NAME = 'jest';
const JEST_GLOBALS_MODULE_NAME = '@jest/globals';
const JEST_GLOBALS_MODULE_JEST_EXPORT_NAME = 'jest';

const hoistedVariables = new WeakSet<VariableDeclarator>();

// 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.
Expand Down Expand Up @@ -146,20 +151,7 @@ FUNCTIONS.mock = args => {
const initNode = binding.path.node.init;

if (initNode && binding.constant && scope.isPure(initNode, true)) {
const identifier = scope.generateUidIdentifier(
'hoistedMockFactoryVariable',
);

binding.path.parentPath.insertAfter(
createHoistedVariable({
HOISTED_NAME: identifier,
HOISTED_VALUE: binding.path.node.init,
}),
);

// replace reference with a call to the new function
id.replaceWith(callExpression(identifier, []));

hoistedVariables.add(binding.path.node);
isAllowedIdentifier = true;
}
}
Expand Down Expand Up @@ -202,12 +194,6 @@ function GETTER_NAME() {
}
`;

const createHoistedVariable = statement`
function HOISTED_NAME() {
return HOISTED_VALUE;
}
`;

const isJestObject = (expression: NodePath<Expression>): boolean => {
// global
if (
Expand Down Expand Up @@ -317,24 +303,27 @@ export default (): PluginObj<{
// in `post` to make sure we come after an import transform and can unshift above the `require`s
post({path: program}) {
const self = this;

visitBlock(program);
program.traverse({
BlockStatement: visitBlock,
});

function visitBlock(block: NodePath<BlockStatement> | NodePath<Program>) {
// use a temporary empty statement instead of the real first statement, which may itself be hoisted
const [firstNonHoistedStatementOfBlock] = block.unshiftContainer(
'body',
const [varsHoistPoint, callsHoistPoint] = block.unshiftContainer('body', [
emptyStatement(),
);
emptyStatement(),
]);
block.traverse({
CallExpression: visitCallExpr,
VariableDeclarator: visitVariableDeclarator,
// do not traverse into nested blocks, or we'll hoist calls in there out to this block
// @ts-expect-error blacklist is not known
blacklist: ['BlockStatement'],
});
firstNonHoistedStatementOfBlock.remove();
callsHoistPoint.remove();
varsHoistPoint.remove();

function visitCallExpr(callExpr: NodePath<CallExpression>) {
const {
Expand All @@ -351,11 +340,25 @@ export default (): PluginObj<{
const mockStmtParent = mockStmt.parentPath;
if (mockStmtParent.isBlock()) {
mockStmt.remove();
firstNonHoistedStatementOfBlock.insertBefore(mockStmtNode);
callsHoistPoint.insertBefore(mockStmtNode);
}
}
}
}

function visitVariableDeclarator(varDecl: NodePath<VariableDeclarator>) {
if (hoistedVariables.has(varDecl.node)) {
const {kind, declarations} = varDecl.parent as VariableDeclaration;
if (declarations.length === 1) {
varDecl.parentPath.remove();
} else {
varDecl.remove();
}
varsHoistPoint.insertBefore(
variableDeclaration(kind, [varDecl.node]),
);
}
}
}
},
});
Expand Down