Skip to content

Commit 57b7254

Browse files
G-RathSimenB
authored andcommitted
chore(prefer-spy-on): convert to typescript (#326)
1 parent 9934c2d commit 57b7254

File tree

4 files changed

+153
-86
lines changed

4 files changed

+153
-86
lines changed

src/rules/__tests__/prefer-spy-on.test.js renamed to src/rules/__tests__/prefer-spy-on.test.ts

Lines changed: 53 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1-
import { RuleTester } from 'eslint';
1+
import {
2+
AST_NODE_TYPES,
3+
TSESLint,
4+
} from '@typescript-eslint/experimental-utils';
25
import rule from '../prefer-spy-on';
36

4-
const ruleTester = new RuleTester({
7+
const ruleTester = new TSESLint.RuleTester({
58
parserOptions: {
69
ecmaVersion: 6,
710
},
@@ -22,44 +25,84 @@ ruleTester.run('prefer-spy-on', rule, {
2225
invalid: [
2326
{
2427
code: 'obj.a = jest.fn(); const test = 10;',
25-
errors: [{ messageId: 'useJestSpyOn', type: 'AssignmentExpression' }],
28+
errors: [
29+
{
30+
messageId: 'useJestSpyOn',
31+
type: AST_NODE_TYPES.AssignmentExpression,
32+
},
33+
],
2634
output: "jest.spyOn(obj, 'a'); const test = 10;",
2735
},
2836
{
2937
code: "Date['now'] = jest['fn']()",
30-
errors: [{ messageId: 'useJestSpyOn', type: 'AssignmentExpression' }],
38+
errors: [
39+
{
40+
messageId: 'useJestSpyOn',
41+
type: AST_NODE_TYPES.AssignmentExpression,
42+
},
43+
],
3144
output: "jest.spyOn(Date, 'now')",
3245
},
3346
{
3447
code: 'window[`${name}`] = jest[`fn`]()',
35-
errors: [{ messageId: 'useJestSpyOn', type: 'AssignmentExpression' }],
48+
errors: [
49+
{
50+
messageId: 'useJestSpyOn',
51+
type: AST_NODE_TYPES.AssignmentExpression,
52+
},
53+
],
3654
output: 'jest.spyOn(window, `${name}`)',
3755
},
3856
{
3957
code: "obj['prop' + 1] = jest['fn']()",
40-
errors: [{ messageId: 'useJestSpyOn', type: 'AssignmentExpression' }],
58+
errors: [
59+
{
60+
messageId: 'useJestSpyOn',
61+
type: AST_NODE_TYPES.AssignmentExpression,
62+
},
63+
],
4164
output: "jest.spyOn(obj, 'prop' + 1)",
4265
},
4366
{
4467
code: 'obj.one.two = jest.fn(); const test = 10;',
45-
errors: [{ messageId: 'useJestSpyOn', type: 'AssignmentExpression' }],
68+
errors: [
69+
{
70+
messageId: 'useJestSpyOn',
71+
type: AST_NODE_TYPES.AssignmentExpression,
72+
},
73+
],
4674
output: "jest.spyOn(obj.one, 'two'); const test = 10;",
4775
},
4876
{
4977
code: 'obj.a = jest.fn(() => 10)',
50-
errors: [{ messageId: 'useJestSpyOn', type: 'AssignmentExpression' }],
78+
errors: [
79+
{
80+
messageId: 'useJestSpyOn',
81+
type: AST_NODE_TYPES.AssignmentExpression,
82+
},
83+
],
5184
output: "jest.spyOn(obj, 'a').mockImplementation(() => 10)",
5285
},
5386
{
5487
code:
5588
"obj.a.b = jest.fn(() => ({})).mockReturnValue('default').mockReturnValueOnce('first call'); test();",
56-
errors: [{ messageId: 'useJestSpyOn', type: 'AssignmentExpression' }],
89+
errors: [
90+
{
91+
messageId: 'useJestSpyOn',
92+
type: AST_NODE_TYPES.AssignmentExpression,
93+
},
94+
],
5795
output:
5896
"jest.spyOn(obj.a, 'b').mockImplementation(() => ({})).mockReturnValue('default').mockReturnValueOnce('first call'); test();",
5997
},
6098
{
6199
code: 'window.fetch = jest.fn(() => ({})).one.two().three().four',
62-
errors: [{ messageId: 'useJestSpyOn', type: 'AssignmentExpression' }],
100+
errors: [
101+
{
102+
messageId: 'useJestSpyOn',
103+
type: AST_NODE_TYPES.AssignmentExpression,
104+
},
105+
],
63106
output:
64107
"jest.spyOn(window, 'fetch').mockImplementation(() => ({})).one.two().three().four",
65108
},

src/rules/prefer-spy-on.js

Lines changed: 0 additions & 71 deletions
This file was deleted.

src/rules/prefer-spy-on.ts

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import {
2+
AST_NODE_TYPES,
3+
TSESTree,
4+
} from '@typescript-eslint/experimental-utils';
5+
import { createRule, getNodeName } from './tsUtils';
6+
7+
const findNodeObject = (
8+
node: TSESTree.CallExpression | TSESTree.MemberExpression,
9+
): TSESTree.LeftHandSideExpression | null => {
10+
if ('object' in node) {
11+
return node.object;
12+
}
13+
14+
if (node.callee.type === AST_NODE_TYPES.MemberExpression) {
15+
return node.callee.object;
16+
}
17+
18+
return null;
19+
};
20+
21+
const getJestFnCall = (node: TSESTree.Node): TSESTree.CallExpression | null => {
22+
if (
23+
node.type !== AST_NODE_TYPES.CallExpression &&
24+
node.type !== AST_NODE_TYPES.MemberExpression
25+
) {
26+
return null;
27+
}
28+
29+
const obj = findNodeObject(node);
30+
31+
if (!obj) {
32+
return null;
33+
}
34+
35+
if (obj.type === AST_NODE_TYPES.Identifier) {
36+
return node.type === AST_NODE_TYPES.CallExpression &&
37+
getNodeName(node.callee) === 'jest.fn'
38+
? node
39+
: null;
40+
}
41+
42+
return getJestFnCall(obj);
43+
};
44+
45+
export default createRule({
46+
name: __filename,
47+
meta: {
48+
docs: {
49+
category: 'Best Practices',
50+
description: 'Suggest using `jest.spyOn()`',
51+
recommended: false,
52+
},
53+
messages: {
54+
useJestSpyOn: 'Use jest.spyOn() instead.',
55+
},
56+
fixable: 'code',
57+
schema: [],
58+
type: 'suggestion',
59+
},
60+
defaultOptions: [],
61+
create(context) {
62+
return {
63+
AssignmentExpression(node) {
64+
const { left, right } = node;
65+
66+
if (left.type !== AST_NODE_TYPES.MemberExpression) return;
67+
68+
const jestFnCall = getJestFnCall(right);
69+
70+
if (!jestFnCall) return;
71+
72+
context.report({
73+
node,
74+
messageId: 'useJestSpyOn',
75+
fix(fixer) {
76+
const leftPropQuote =
77+
left.property.type === AST_NODE_TYPES.Identifier ? "'" : '';
78+
const [arg] = jestFnCall.arguments;
79+
const argSource = arg && context.getSourceCode().getText(arg);
80+
const mockImplementation = argSource
81+
? `.mockImplementation(${argSource})`
82+
: '';
83+
84+
return [
85+
fixer.insertTextBefore(left, `jest.spyOn(`),
86+
fixer.replaceTextRange(
87+
[left.object.range[1], left.property.range[0]],
88+
`, ${leftPropQuote}`,
89+
),
90+
fixer.replaceTextRange(
91+
[left.property.range[1], jestFnCall.range[1]],
92+
`${leftPropQuote})${mockImplementation}`,
93+
),
94+
];
95+
},
96+
});
97+
},
98+
};
99+
},
100+
});

src/rules/util.js

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -109,11 +109,6 @@ export const getNodeName = node => {
109109
switch (node && node.type) {
110110
case 'Identifier':
111111
return node.name;
112-
case 'Literal':
113-
return node.value;
114-
case 'TemplateLiteral':
115-
if (node.expressions.length === 0) return node.quasis[0].value.cooked;
116-
break;
117112
case 'MemberExpression':
118113
return joinNames(getNodeName(node.object), getNodeName(node.property));
119114
}

0 commit comments

Comments
 (0)