Skip to content

Commit ec0caef

Browse files
committed
fix: issue 1800
1 parent 8753599 commit ec0caef

File tree

3 files changed

+111
-4
lines changed

3 files changed

+111
-4
lines changed

src/rules/__tests__/unbound-method.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ const validTestCases: string[] = [
8181
'expect(() => Promise.resolve().then(console.log)).not.toThrow();',
8282
...toThrowMatchers.map(matcher => `expect(console.log).not.${matcher}();`),
8383
...toThrowMatchers.map(matcher => `expect(console.log).${matcher}();`),
84-
// Issue #1800: jest.mocked().mock.calls should be allowed
84+
// https://github.com/jest-community/eslint-plugin-jest/issues/1800
8585
...[
8686
'const parameter = jest.mocked(service.method).mock.calls[0][0];',
8787
'const calls = jest.mocked(service.method).mock.calls;',

src/rules/unbound-method.ts

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
findTopMostCallExpression,
99
getAccessorValue,
1010
isIdentifier,
11+
isSupportedAccessor,
1112
parseJestFnCall,
1213
} from './utils';
1314

@@ -73,9 +74,116 @@ export default createRule<Options, MessageIds>({
7374
return {};
7475
}
7576

77+
/**
78+
* Checks if a MemberExpression is an argument to a `jest.mocked()` call.
79+
* This handles cases like `jest.mocked(service.method)` where `service.method`
80+
* should not be flagged as an unbound method.
81+
*/
82+
const isArgumentToJestMocked = (
83+
node: TSESTree.MemberExpression,
84+
): boolean => {
85+
// Check if the immediate parent is a CallExpression
86+
if (node.parent?.type !== AST_NODE_TYPES.CallExpression) {
87+
return false;
88+
}
89+
90+
const parentCall = node.parent;
91+
92+
// Check if this node is an argument to the call
93+
if (!parentCall.arguments.some(arg => arg === node)) {
94+
return false;
95+
}
96+
97+
// Check if the call is jest.mocked() by examining the callee
98+
if (
99+
parentCall.callee.type === AST_NODE_TYPES.MemberExpression &&
100+
isSupportedAccessor(parentCall.callee.object) &&
101+
isSupportedAccessor(parentCall.callee.property)
102+
) {
103+
const objectName = getAccessorValue(parentCall.callee.object);
104+
const propertyName = getAccessorValue(parentCall.callee.property);
105+
106+
if (objectName === 'jest' && propertyName === 'mocked') {
107+
return true;
108+
}
109+
}
110+
111+
// Also try using parseJestFnCall as a fallback
112+
const jestFnCall = parseJestFnCall(
113+
findTopMostCallExpression(parentCall),
114+
context,
115+
);
116+
117+
return (
118+
jestFnCall?.type === 'jest' &&
119+
jestFnCall.members.length >= 1 &&
120+
isIdentifier(jestFnCall.members[0], 'mocked')
121+
);
122+
};
123+
124+
/**
125+
* Checks if a MemberExpression is accessing `.mock` property on a
126+
* `jest.mocked()` result. This handles cases like:
127+
* - `jest.mocked(service.method).mock.calls[0][0]`
128+
* - `jest.mocked(service.method).mock.calls`
129+
*/
130+
const isAccessingMockProperty = (
131+
node: TSESTree.MemberExpression,
132+
): boolean => {
133+
// Check if we're accessing `.mock` property
134+
if (!isSupportedAccessor(node.property)) {
135+
return false;
136+
}
137+
138+
const propertyName = getAccessorValue(node.property);
139+
140+
if (propertyName !== 'mock') {
141+
return false;
142+
}
143+
144+
// Traverse up the chain to find if the object is a jest.mocked() call
145+
let current: TSESTree.Node = node.object;
146+
147+
while (current) {
148+
if (current.type === AST_NODE_TYPES.CallExpression) {
149+
const jestFnCall = parseJestFnCall(
150+
findTopMostCallExpression(current),
151+
context,
152+
);
153+
154+
if (
155+
jestFnCall?.type === 'jest' &&
156+
jestFnCall.members.length >= 1 &&
157+
isIdentifier(jestFnCall.members[0], 'mocked')
158+
) {
159+
return true;
160+
}
161+
}
162+
163+
// Continue traversing up if the current node is part of a member chain
164+
if (current.type === AST_NODE_TYPES.MemberExpression) {
165+
current = current.object;
166+
} else {
167+
break;
168+
}
169+
}
170+
171+
return false;
172+
};
173+
76174
return {
77175
...baseSelectors,
78176
MemberExpression(node: TSESTree.MemberExpression): void {
177+
// Check if this MemberExpression is an argument to jest.mocked()
178+
if (isArgumentToJestMocked(node)) {
179+
return;
180+
}
181+
182+
// Check if accessing .mock property on jest.mocked() result
183+
if (isAccessingMockProperty(node)) {
184+
return;
185+
}
186+
79187
if (node.parent?.type === AST_NODE_TYPES.CallExpression) {
80188
const jestFnCall = parseJestFnCall(
81189
findTopMostCallExpression(node.parent),

src/rules/utils/parseJestFnCall.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -338,9 +338,8 @@ const parseJestFnCallWithReasonInner = (
338338
// parsing e.g. x().y.z(), we'll incorrectly find & parse "x()" even though
339339
// the full chain is not a valid jest function call chain
340340
if (
341-
lastLink !== 'mocked' &&
342-
(node.parent?.type === AST_NODE_TYPES.CallExpression ||
343-
node.parent?.type === AST_NODE_TYPES.MemberExpression)
341+
node.parent?.type === AST_NODE_TYPES.CallExpression ||
342+
node.parent?.type === AST_NODE_TYPES.MemberExpression
344343
) {
345344
return null;
346345
}

0 commit comments

Comments
 (0)