Skip to content

Lint fix hoist jest mock #5181

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

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion eslint/eslint-plugin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@
"scripts": {
"build": "heft build --clean",
"_phase:build": "heft run --only build -- --clean",
"_phase:test": "heft run --only test -- --clean"
"_phase:test": "heft run --only test -- --clean",
"test:file": "heft test --clean --test-path-pattern hoist-jest-mock.test"
},
"dependencies": {
"@rushstack/tree-pattern": "workspace:*",
Expand Down
83 changes: 81 additions & 2 deletions eslint/eslint-plugin/src/hoist-jest-mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const hoistJestMock: TSESLint.RuleModule<MessageIds, Options> = {
defaultOptions: [],
meta: {
type: 'problem',
fixable: 'code',
messages: {
'error-unhoisted-jest-mock':
"Jest's module mocking APIs must be called before regular imports. Move this call so that it precedes" +
Expand Down Expand Up @@ -88,6 +89,8 @@ const hoistJestMock: TSESLint.RuleModule<MessageIds, Options> = {

// This tracks the first require() or import expression that we found in the file.
let firstImportNode: TSESTree.Node | undefined = undefined;
// track if import node has variable declaration
let hasVariableDeclaration: boolean = false;

// Avoid reporting more than one error for a given statement.
// Example: jest.mock('a').mock('b');
Expand All @@ -98,7 +101,26 @@ const hoistJestMock: TSESLint.RuleModule<MessageIds, Options> = {
if (firstImportNode === undefined) {
// EXAMPLE: const x = require('x')
if (hoistJestMockPatterns.requireCallExpression.match(node)) {
firstImportNode = node;
// Check if this require is inside a jest.mock factory function
let currentNode: TSESTree.Node | undefined = node;
let isInJestMockFactory = false;

while (currentNode?.parent) {
if (
currentNode.parent.type === AST_NODE_TYPES.ArrowFunctionExpression &&
currentNode.parent.parent?.type === AST_NODE_TYPES.CallExpression &&
isHoistableJestCall(currentNode.parent.parent)
) {
isInJestMockFactory = true;
break;
}
currentNode = currentNode.parent;
}

// Only set firstImportNode if not in a factory function
if (!isInJestMockFactory) {
firstImportNode = node;
}
}
}

Expand All @@ -111,7 +133,64 @@ const hoistJestMock: TSESLint.RuleModule<MessageIds, Options> = {
context.report({
node,
messageId: 'error-unhoisted-jest-mock',
data: { importLine: firstImportNode.loc.start.line }
data: { importLine: firstImportNode.loc.start.line },
fix: (fixer: TSESLint.RuleFixer) => {
// Ensure firstImportNode is defined before attempting fix
if (!firstImportNode) {
return null;
}

const sourceCode: TSESLint.SourceCode = context.getSourceCode();
const statementText: string = sourceCode.getText(outerStatement);

// Check if this import is inside a jest.mock factory function
let currentNode: TSESTree.Node | undefined = firstImportNode;
let isInJestMockFactory = false;

while (currentNode?.parent) {
if (
currentNode.parent.type === AST_NODE_TYPES.ArrowFunctionExpression &&
currentNode.parent.parent?.type === AST_NODE_TYPES.CallExpression &&
isHoistableJestCall(currentNode.parent.parent)
) {
isInJestMockFactory = true;
break;
}
currentNode = currentNode.parent;
}

// If the import is inside a jest.mock factory, don't consider it for hoisting
if (isInJestMockFactory) {
return null;
}

// Remove the statement from its current position
const removeOriginal: TSESLint.RuleFix = fixer.removeRange([
outerStatement.range[0] - 1, // Include the previous line's newline character
outerStatement.range[1]
]);

const importExpr = firstImportNode;
let nodeToInsertBefore = importExpr;

// Check if the import is part of a variable declaration
if (
importExpr.parent &&
importExpr.parent.type === 'VariableDeclarator' &&
importExpr.parent.parent &&
importExpr.parent.parent.type === 'VariableDeclaration'
) {
nodeToInsertBefore = importExpr.parent.parent;
}

// Insert it before the first import
const addBeforeImport: TSESLint.RuleFix = fixer.insertTextBefore(
nodeToInsertBefore,
statementText + '\n'
);

return [removeOriginal, addBeforeImport];
}
});
}
}
Expand Down
Loading
Loading