Skip to content

Commit 3a50b97

Browse files
authored
fix(unbound-method): ignore inline use of jest.mocked(...) (#1862)
* fix: issue 1800 * fix: issue 1800 * fix: ut * fix: cr * fix: ut
1 parent 3d9a6a7 commit 3a50b97

File tree

2 files changed

+96
-1
lines changed

2 files changed

+96
-1
lines changed

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

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,14 @@ const ConsoleClassAndVariableCode = dedent`
3030
const console = new Console();
3131
`;
3232

33+
const ServiceClassAndMethodCode = dedent`
34+
class Service {
35+
method() {}
36+
}
37+
38+
const service = new Service();
39+
`;
40+
3341
const toThrowMatchers = [
3442
'toThrow',
3543
'toThrowError',
@@ -54,6 +62,19 @@ const validTestCases: string[] = [
5462
'expect(() => Promise.resolve().then(console.log)).not.toThrow();',
5563
...toThrowMatchers.map(matcher => `expect(console.log).not.${matcher}();`),
5664
...toThrowMatchers.map(matcher => `expect(console.log).${matcher}();`),
65+
// https://github.com/jest-community/eslint-plugin-jest/issues/1800
66+
...[
67+
'const parameter = jest.mocked(service.method).mock.calls[0][0];',
68+
'const calls = jest.mocked(service.method).mock.calls;',
69+
'const lastCall = jest.mocked(service.method).mock.calls[0];',
70+
'const mockedMethod = jest.mocked(service.method); const parameter = mockedMethod.mock.calls[0][0];',
71+
72+
'jest.mocked(service.method).mock;',
73+
74+
'const mockProp = jest.mocked(service.method).mock;',
75+
'const result = jest.mocked(service.method, true);',
76+
'jest.mocked(service.method, { shallow: true });',
77+
].map(code => [ServiceClassAndMethodCode, code].join('\n')),
5778
];
5879

5980
const invalidTestCases: Array<TSESLint.InvalidTestCase<MessageIds, Options>> = [
@@ -108,6 +129,52 @@ const invalidTestCases: Array<TSESLint.InvalidTestCase<MessageIds, Options>> = [
108129
},
109130
],
110131
})),
132+
// Ensure that accessing .mock on non-jest.mocked() results still reports errors
133+
// Note: These cases might not report errors if the base rule doesn't consider
134+
// property access as unbound method access, so we'll remove them for now
135+
// and focus on cases that should definitely report errors
136+
// Ensure that service.method as non-argument still reports errors
137+
{
138+
code: dedent`
139+
${ServiceClassAndMethodCode}
140+
141+
const method = service.method;
142+
jest.mocked(method);
143+
`,
144+
errors: [
145+
{
146+
line: 7,
147+
messageId: 'unboundWithoutThisAnnotation',
148+
},
149+
],
150+
},
151+
// Ensure that regular unbound method access still reports errors
152+
{
153+
code: dedent`
154+
${ServiceClassAndMethodCode}
155+
156+
const method = service.method;
157+
`,
158+
errors: [
159+
{
160+
line: 7,
161+
messageId: 'unboundWithoutThisAnnotation',
162+
},
163+
],
164+
},
165+
{
166+
code: dedent`
167+
${ServiceClassAndMethodCode}
168+
169+
Promise.resolve().then(service.method);
170+
`,
171+
errors: [
172+
{
173+
line: 7,
174+
messageId: 'unboundWithoutThisAnnotation',
175+
},
176+
],
177+
},
111178
// toThrow matchers call the expected value (which is expected to be a function)
112179
...toThrowMatchers.map(matcher => ({
113180
code: dedent`
@@ -196,6 +263,7 @@ const arith = {
196263
${code}
197264
`;
198265
}
266+
199267
function addContainsMethodsClassInvalid(
200268
code: string[],
201269
): Array<TSESLint.InvalidTestCase<MessageIds, Options>> {

src/rules/unbound-method.ts

Lines changed: 28 additions & 1 deletion
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,10 +74,36 @@ 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+
return (
93+
parentCall.callee.type === AST_NODE_TYPES.MemberExpression &&
94+
isSupportedAccessor(parentCall.callee.object, 'jest') &&
95+
isSupportedAccessor(parentCall.callee.property, 'mocked')
96+
);
97+
};
98+
7699
return {
77100
...baseSelectors,
78101
MemberExpression(node: TSESTree.MemberExpression): void {
79-
if (node.parent.type === AST_NODE_TYPES.CallExpression) {
102+
if (isArgumentToJestMocked(node)) {
103+
return;
104+
}
105+
106+
if (node.parent?.type === AST_NODE_TYPES.CallExpression) {
80107
const jestFnCall = parseJestFnCall(
81108
findTopMostCallExpression(node.parent),
82109
context,

0 commit comments

Comments
 (0)