Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit f132161

Browse files
author
s.v.zaytsev
committedMay 30, 2024·
feat(prefer-mocked): add new rule (#1470)
1 parent 70c8c5e commit f132161

File tree

6 files changed

+412
-1
lines changed

6 files changed

+412
-1
lines changed
 

‎README.md

+1
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,7 @@ set to warn in.\
352352
| [prefer-importing-jest-globals](docs/rules/prefer-importing-jest-globals.md) | Prefer importing Jest globals | | | 🔧 | |
353353
| [prefer-lowercase-title](docs/rules/prefer-lowercase-title.md) | Enforce lowercase test names | | | 🔧 | |
354354
| [prefer-mock-promise-shorthand](docs/rules/prefer-mock-promise-shorthand.md) | Prefer mock resolved/rejected shorthands for promises | | | 🔧 | |
355+
| [prefer-mocked](docs/rules/prefer-mocked.md) | Prefer jest.mocked() over (fn as jest.Mock) | | | 🔧 | |
355356
| [prefer-snapshot-hint](docs/rules/prefer-snapshot-hint.md) | Prefer including a hint with external snapshots | | | | |
356357
| [prefer-spy-on](docs/rules/prefer-spy-on.md) | Suggest using `jest.spyOn()` | | | 🔧 | |
357358
| [prefer-strict-equal](docs/rules/prefer-strict-equal.md) | Suggest using `toStrictEqual()` | | | | 💡 |

‎docs/rules/prefer-mocked.md

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# Prefer jest.mocked() over (fn as jest.Mock) (`prefer-mocked`)
2+
3+
🔧 This rule is automatically fixable by the
4+
[`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix).
5+
6+
<!-- end auto-generated rule header -->
7+
8+
When working with mocks of functions using Jest, it's recommended to use the
9+
jest.mocked helper function to properly type the mocked functions. This rule
10+
enforces the use of jest.mocked for better type safety and readability.
11+
12+
## Rule details
13+
14+
The following patterns are warnings:
15+
16+
```typescript
17+
(foo as jest.Mock).mockReturnValue(1);
18+
const mock = (foo as jest.Mock).mockReturnValue(1);
19+
(foo as unknown as jest.Mock).mockReturnValue(1);
20+
(Obj.foo as jest.Mock).mockReturnValue(1);
21+
([].foo as jest.Mock).mockReturnValue(1);
22+
```
23+
24+
The following patterns are not warnings:
25+
26+
```js
27+
jest.mocked(foo).mockReturnValue(1);
28+
const mock = jest.mocked(foo).mockReturnValue(1);
29+
jest.mocked(Obj.foo).mockReturnValue(1);
30+
jest.mocked([].foo).mockReturnValue(1);
31+
```

‎src/__tests__/__snapshots__/rules.test.ts.snap

+2
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ exports[`rules should export configs that refer to actual rules 1`] = `
4848
"jest/prefer-importing-jest-globals": "error",
4949
"jest/prefer-lowercase-title": "error",
5050
"jest/prefer-mock-promise-shorthand": "error",
51+
"jest/prefer-mocked": "error",
5152
"jest/prefer-snapshot-hint": "error",
5253
"jest/prefer-spy-on": "error",
5354
"jest/prefer-strict-equal": "error",
@@ -130,6 +131,7 @@ exports[`rules should export configs that refer to actual rules 1`] = `
130131
"jest/prefer-importing-jest-globals": "error",
131132
"jest/prefer-lowercase-title": "error",
132133
"jest/prefer-mock-promise-shorthand": "error",
134+
"jest/prefer-mocked": "error",
133135
"jest/prefer-snapshot-hint": "error",
134136
"jest/prefer-spy-on": "error",
135137
"jest/prefer-strict-equal": "error",

‎src/__tests__/rules.test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { existsSync } from 'fs';
22
import { resolve } from 'path';
33
import plugin from '../';
44

5-
const numberOfRules = 53;
5+
const numberOfRules = 54;
66
const ruleNames = Object.keys(plugin.rules);
77
const deprecatedRules = Object.entries(plugin.rules)
88
.filter(([, rule]) => rule.meta.deprecated)
+321
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,321 @@
1+
import path from 'path';
2+
import dedent from 'dedent';
3+
import rule from '../prefer-mocked';
4+
import { FlatCompatRuleTester as RuleTester } from './test-utils';
5+
6+
function getFixturesRootDir(): string {
7+
return path.join(__dirname, 'fixtures');
8+
}
9+
10+
const rootPath = getFixturesRootDir();
11+
12+
const ruleTester = new RuleTester({
13+
parser: require.resolve('@typescript-eslint/parser'),
14+
parserOptions: {
15+
sourceType: 'module',
16+
tsconfigRootDir: rootPath,
17+
project: './tsconfig.json',
18+
},
19+
});
20+
21+
ruleTester.run('prefer-mocked', rule, {
22+
valid: [
23+
dedent`
24+
import { foo } from './foo';
25+
foo();
26+
`,
27+
28+
dedent`
29+
import { foo } from './foo';
30+
jest.mocked(foo).mockReturnValue(1);
31+
`,
32+
33+
dedent`
34+
import { bar } from './bar';
35+
bar.mockReturnValue(1);
36+
`,
37+
38+
dedent`
39+
import { foo } from './foo';
40+
sinon.stub(foo).returns(1);
41+
`,
42+
43+
dedent`
44+
import { foo } from './foo';
45+
foo.mockImplementation(() => 1);
46+
`,
47+
48+
dedent`
49+
const obj = { foo() {} };
50+
obj.foo();
51+
`,
52+
53+
dedent`
54+
const mockFn = jest.fn();
55+
mockFn.mockReturnValue(1);
56+
`,
57+
58+
dedent`
59+
const arr = [() => {}];
60+
arr[0]();
61+
`,
62+
63+
dedent`
64+
const obj = { foo() {} };
65+
obj.foo.mockReturnValue(1);
66+
`,
67+
68+
dedent`
69+
const obj = { foo() {} };
70+
jest.spyOn(obj, 'foo').mockReturnValue(1);
71+
`,
72+
73+
dedent`
74+
type MockType = jest.Mock;
75+
const mockFn = jest.fn();
76+
(mockFn as MockType).mockReturnValue(1);
77+
`,
78+
],
79+
invalid: [
80+
{
81+
code: dedent`
82+
import { foo } from './foo';
83+
84+
(foo as jest.Mock).mockReturnValue(1);
85+
`,
86+
output: dedent`
87+
import { foo } from './foo';
88+
89+
(jest.mocked(foo)).mockReturnValue(1);
90+
`,
91+
options: [],
92+
errors: [
93+
{
94+
messageId: 'useJestMocked',
95+
column: 2,
96+
line: 3,
97+
},
98+
],
99+
},
100+
{
101+
code: dedent`
102+
import { foo } from './foo';
103+
104+
(foo as jest.Mock).mockImplementation(1);
105+
`,
106+
output: dedent`
107+
import { foo } from './foo';
108+
109+
(jest.mocked(foo)).mockImplementation(1);
110+
`,
111+
options: [],
112+
errors: [
113+
{
114+
messageId: 'useJestMocked',
115+
column: 2,
116+
line: 3,
117+
},
118+
],
119+
},
120+
{
121+
code: dedent`
122+
import { foo } from './foo';
123+
124+
(foo as unknown as jest.Mock).mockReturnValue(1);
125+
`,
126+
output: dedent`
127+
import { foo } from './foo';
128+
129+
(jest.mocked(foo)).mockReturnValue(1);
130+
`,
131+
options: [],
132+
errors: [
133+
{
134+
messageId: 'useJestMocked',
135+
column: 2,
136+
line: 3,
137+
},
138+
],
139+
},
140+
{
141+
code: dedent`
142+
import { Obj } from './foo';
143+
144+
(Obj.foo as jest.Mock).mockReturnValue(1);
145+
`,
146+
output: dedent`
147+
import { Obj } from './foo';
148+
149+
(jest.mocked(Obj.foo)).mockReturnValue(1);
150+
`,
151+
options: [],
152+
errors: [
153+
{
154+
messageId: 'useJestMocked',
155+
column: 2,
156+
line: 3,
157+
},
158+
],
159+
},
160+
{
161+
code: dedent`
162+
([].foo as jest.Mock).mockReturnValue(1);
163+
`,
164+
output: dedent`
165+
(jest.mocked([].foo)).mockReturnValue(1);
166+
`,
167+
options: [],
168+
errors: [
169+
{
170+
messageId: 'useJestMocked',
171+
column: 2,
172+
line: 1,
173+
},
174+
],
175+
},
176+
{
177+
code: dedent`
178+
import { foo } from './foo';
179+
180+
(foo as jest.MockedFunction).mockReturnValue(1);
181+
`,
182+
output: dedent`
183+
import { foo } from './foo';
184+
185+
(jest.mocked(foo)).mockReturnValue(1);
186+
`,
187+
options: [],
188+
errors: [
189+
{
190+
messageId: 'useJestMocked',
191+
column: 2,
192+
line: 3,
193+
},
194+
],
195+
},
196+
{
197+
code: dedent`
198+
import { foo } from './foo';
199+
200+
(foo as jest.MockedFunction).mockImplementation(1);
201+
`,
202+
output: dedent`
203+
import { foo } from './foo';
204+
205+
(jest.mocked(foo)).mockImplementation(1);
206+
`,
207+
options: [],
208+
errors: [
209+
{
210+
messageId: 'useJestMocked',
211+
column: 2,
212+
line: 3,
213+
},
214+
],
215+
},
216+
{
217+
code: dedent`
218+
import { foo } from './foo';
219+
220+
(foo as unknown as jest.MockedFunction).mockReturnValue(1);
221+
`,
222+
output: dedent`
223+
import { foo } from './foo';
224+
225+
(jest.mocked(foo)).mockReturnValue(1);
226+
`,
227+
options: [],
228+
errors: [
229+
{
230+
messageId: 'useJestMocked',
231+
column: 2,
232+
line: 3,
233+
},
234+
],
235+
},
236+
{
237+
code: dedent`
238+
import { Obj } from './foo';
239+
240+
(Obj.foo as jest.MockedFunction).mockReturnValue(1);
241+
`,
242+
output: dedent`
243+
import { Obj } from './foo';
244+
245+
(jest.mocked(Obj.foo)).mockReturnValue(1);
246+
`,
247+
options: [],
248+
errors: [
249+
{
250+
messageId: 'useJestMocked',
251+
column: 2,
252+
line: 3,
253+
},
254+
],
255+
},
256+
{
257+
code: dedent`
258+
(new Array(0).fill(null).foo as jest.MockedFunction).mockReturnValue(1);
259+
`,
260+
output: dedent`
261+
(jest.mocked(new Array(0).fill(null).foo)).mockReturnValue(1);
262+
`,
263+
options: [],
264+
errors: [
265+
{
266+
messageId: 'useJestMocked',
267+
column: 2,
268+
line: 1,
269+
},
270+
],
271+
},
272+
{
273+
code: dedent`
274+
(jest.fn(() => foo) as jest.MockedFunction).mockReturnValue(1);
275+
`,
276+
output: dedent`
277+
(jest.mocked(jest.fn(() => foo))).mockReturnValue(1);
278+
`,
279+
options: [],
280+
errors: [
281+
{
282+
messageId: 'useJestMocked',
283+
column: 2,
284+
line: 1,
285+
},
286+
],
287+
},
288+
{
289+
code: dedent`
290+
const mockedUseFocused = useFocused as jest.MockedFunction<typeof useFocused>;
291+
`,
292+
output: dedent`
293+
const mockedUseFocused = jest.mocked(useFocused);
294+
`,
295+
options: [],
296+
errors: [
297+
{
298+
messageId: 'useJestMocked',
299+
column: 26,
300+
line: 1,
301+
},
302+
],
303+
},
304+
{
305+
code: dedent`
306+
const filter = (MessageService.getMessage as jest.Mock).mock.calls[0][0];
307+
`,
308+
output: dedent`
309+
const filter = (jest.mocked(MessageService.getMessage)).mock.calls[0][0];
310+
`,
311+
options: [],
312+
errors: [
313+
{
314+
messageId: 'useJestMocked',
315+
column: 17,
316+
line: 1,
317+
},
318+
],
319+
},
320+
],
321+
});

‎src/rules/prefer-mocked.ts

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { AST_NODE_TYPES, type TSESTree } from '@typescript-eslint/utils';
2+
import { createRule } from './utils';
3+
4+
type ValidatedTsAsExpression = TSESTree.TSAsExpression & {
5+
typeAnnotation: TSESTree.TSTypeReference & {
6+
typeName: TSESTree.TSQualifiedName & {
7+
left: TSESTree.Identifier;
8+
right: TSESTree.Identifier;
9+
};
10+
};
11+
};
12+
13+
function getFnName(node: TSESTree.Expression, sourceCode: string): string {
14+
if (node.type === AST_NODE_TYPES.TSAsExpression) {
15+
// case: `myFn as unknown as jest.Mock`
16+
return getFnName(node.expression, sourceCode);
17+
}
18+
19+
return sourceCode.slice(...node.range);
20+
}
21+
22+
export default createRule({
23+
name: __filename,
24+
meta: {
25+
docs: {
26+
description: 'Prefer jest.mocked() over (fn as jest.Mock)',
27+
},
28+
messages: {
29+
useJestMocked: 'Prefer jest.mocked({{ replacement }})',
30+
},
31+
schema: [],
32+
type: 'suggestion',
33+
fixable: 'code',
34+
},
35+
defaultOptions: [],
36+
create(context) {
37+
return {
38+
'TSAsExpression:has(TSTypeReference > TSQualifiedName:has(Identifier.left[name="jest"]):has(Identifier.right[name="Mock"],Identifier.right[name="MockedFunction"]))'(
39+
node: ValidatedTsAsExpression,
40+
) {
41+
const fnName = getFnName(node.expression, context.sourceCode.text);
42+
43+
context.report({
44+
node,
45+
messageId: 'useJestMocked',
46+
data: {
47+
replacement: '',
48+
},
49+
fix(fixer) {
50+
return fixer.replaceText(node, `jest.mocked(${fnName})`);
51+
},
52+
});
53+
},
54+
};
55+
},
56+
});

0 commit comments

Comments
 (0)
Please sign in to comment.