diff --git a/src/rules/__tests__/prefer-spy-on.test.js b/src/rules/__tests__/prefer-spy-on.test.ts similarity index 57% rename from src/rules/__tests__/prefer-spy-on.test.js rename to src/rules/__tests__/prefer-spy-on.test.ts index 44692e335..2bf82f79e 100644 --- a/src/rules/__tests__/prefer-spy-on.test.js +++ b/src/rules/__tests__/prefer-spy-on.test.ts @@ -1,7 +1,10 @@ -import { RuleTester } from 'eslint'; +import { + AST_NODE_TYPES, + TSESLint, +} from '@typescript-eslint/experimental-utils'; import rule from '../prefer-spy-on'; -const ruleTester = new RuleTester({ +const ruleTester = new TSESLint.RuleTester({ parserOptions: { ecmaVersion: 6, }, @@ -22,44 +25,84 @@ ruleTester.run('prefer-spy-on', rule, { invalid: [ { code: 'obj.a = jest.fn(); const test = 10;', - errors: [{ messageId: 'useJestSpyOn', type: 'AssignmentExpression' }], + errors: [ + { + messageId: 'useJestSpyOn', + type: AST_NODE_TYPES.AssignmentExpression, + }, + ], output: "jest.spyOn(obj, 'a'); const test = 10;", }, { code: "Date['now'] = jest['fn']()", - errors: [{ messageId: 'useJestSpyOn', type: 'AssignmentExpression' }], + errors: [ + { + messageId: 'useJestSpyOn', + type: AST_NODE_TYPES.AssignmentExpression, + }, + ], output: "jest.spyOn(Date, 'now')", }, { code: 'window[`${name}`] = jest[`fn`]()', - errors: [{ messageId: 'useJestSpyOn', type: 'AssignmentExpression' }], + errors: [ + { + messageId: 'useJestSpyOn', + type: AST_NODE_TYPES.AssignmentExpression, + }, + ], output: 'jest.spyOn(window, `${name}`)', }, { code: "obj['prop' + 1] = jest['fn']()", - errors: [{ messageId: 'useJestSpyOn', type: 'AssignmentExpression' }], + errors: [ + { + messageId: 'useJestSpyOn', + type: AST_NODE_TYPES.AssignmentExpression, + }, + ], output: "jest.spyOn(obj, 'prop' + 1)", }, { code: 'obj.one.two = jest.fn(); const test = 10;', - errors: [{ messageId: 'useJestSpyOn', type: 'AssignmentExpression' }], + errors: [ + { + messageId: 'useJestSpyOn', + type: AST_NODE_TYPES.AssignmentExpression, + }, + ], output: "jest.spyOn(obj.one, 'two'); const test = 10;", }, { code: 'obj.a = jest.fn(() => 10)', - errors: [{ messageId: 'useJestSpyOn', type: 'AssignmentExpression' }], + errors: [ + { + messageId: 'useJestSpyOn', + type: AST_NODE_TYPES.AssignmentExpression, + }, + ], output: "jest.spyOn(obj, 'a').mockImplementation(() => 10)", }, { code: "obj.a.b = jest.fn(() => ({})).mockReturnValue('default').mockReturnValueOnce('first call'); test();", - errors: [{ messageId: 'useJestSpyOn', type: 'AssignmentExpression' }], + errors: [ + { + messageId: 'useJestSpyOn', + type: AST_NODE_TYPES.AssignmentExpression, + }, + ], output: "jest.spyOn(obj.a, 'b').mockImplementation(() => ({})).mockReturnValue('default').mockReturnValueOnce('first call'); test();", }, { code: 'window.fetch = jest.fn(() => ({})).one.two().three().four', - errors: [{ messageId: 'useJestSpyOn', type: 'AssignmentExpression' }], + errors: [ + { + messageId: 'useJestSpyOn', + type: AST_NODE_TYPES.AssignmentExpression, + }, + ], output: "jest.spyOn(window, 'fetch').mockImplementation(() => ({})).one.two().three().four", }, diff --git a/src/rules/prefer-spy-on.js b/src/rules/prefer-spy-on.js deleted file mode 100644 index 0b5c2f23b..000000000 --- a/src/rules/prefer-spy-on.js +++ /dev/null @@ -1,71 +0,0 @@ -import { getDocsUrl, getNodeName } from './util'; - -const getJestFnCall = node => { - if ( - (node.type !== 'CallExpression' && node.type !== 'MemberExpression') || - (node.callee && node.callee.type !== 'MemberExpression') - ) { - return null; - } - - const obj = node.callee ? node.callee.object : node.object; - - if (obj.type === 'Identifier') { - return node.type === 'CallExpression' && - getNodeName(node.callee) === 'jest.fn' - ? node - : null; - } - - return getJestFnCall(obj); -}; - -export default { - meta: { - docs: { - url: getDocsUrl(__filename), - }, - messages: { - useJestSpyOn: 'Use jest.spyOn() instead.', - }, - fixable: 'code', - schema: [], - }, - create(context) { - return { - AssignmentExpression(node) { - if (node.left.type !== 'MemberExpression') return; - - const jestFnCall = getJestFnCall(node.right); - - if (!jestFnCall) return; - - context.report({ - node, - messageId: 'useJestSpyOn', - fix(fixer) { - const leftPropQuote = - node.left.property.type === 'Identifier' ? "'" : ''; - const [arg] = jestFnCall.arguments; - const argSource = arg && context.getSourceCode().getText(arg); - const mockImplementation = argSource - ? `.mockImplementation(${argSource})` - : ''; - - return [ - fixer.insertTextBefore(node.left, `jest.spyOn(`), - fixer.replaceTextRange( - [node.left.object.range[1], node.left.property.range[0]], - `, ${leftPropQuote}`, - ), - fixer.replaceTextRange( - [node.left.property.range[1], jestFnCall.range[1]], - `${leftPropQuote})${mockImplementation}`, - ), - ]; - }, - }); - }, - }; - }, -}; diff --git a/src/rules/prefer-spy-on.ts b/src/rules/prefer-spy-on.ts new file mode 100644 index 000000000..5a6fff6a4 --- /dev/null +++ b/src/rules/prefer-spy-on.ts @@ -0,0 +1,100 @@ +import { + AST_NODE_TYPES, + TSESTree, +} from '@typescript-eslint/experimental-utils'; +import { createRule, getNodeName } from './tsUtils'; + +const findNodeObject = ( + node: TSESTree.CallExpression | TSESTree.MemberExpression, +): TSESTree.LeftHandSideExpression | null => { + if ('object' in node) { + return node.object; + } + + if (node.callee.type === AST_NODE_TYPES.MemberExpression) { + return node.callee.object; + } + + return null; +}; + +const getJestFnCall = (node: TSESTree.Node): TSESTree.CallExpression | null => { + if ( + node.type !== AST_NODE_TYPES.CallExpression && + node.type !== AST_NODE_TYPES.MemberExpression + ) { + return null; + } + + const obj = findNodeObject(node); + + if (!obj) { + return null; + } + + if (obj.type === AST_NODE_TYPES.Identifier) { + return node.type === AST_NODE_TYPES.CallExpression && + getNodeName(node.callee) === 'jest.fn' + ? node + : null; + } + + return getJestFnCall(obj); +}; + +export default createRule({ + name: __filename, + meta: { + docs: { + category: 'Best Practices', + description: 'Suggest using `jest.spyOn()`', + recommended: false, + }, + messages: { + useJestSpyOn: 'Use jest.spyOn() instead.', + }, + fixable: 'code', + schema: [], + type: 'suggestion', + }, + defaultOptions: [], + create(context) { + return { + AssignmentExpression(node) { + const { left, right } = node; + + if (left.type !== AST_NODE_TYPES.MemberExpression) return; + + const jestFnCall = getJestFnCall(right); + + if (!jestFnCall) return; + + context.report({ + node, + messageId: 'useJestSpyOn', + fix(fixer) { + const leftPropQuote = + left.property.type === AST_NODE_TYPES.Identifier ? "'" : ''; + const [arg] = jestFnCall.arguments; + const argSource = arg && context.getSourceCode().getText(arg); + const mockImplementation = argSource + ? `.mockImplementation(${argSource})` + : ''; + + return [ + fixer.insertTextBefore(left, `jest.spyOn(`), + fixer.replaceTextRange( + [left.object.range[1], left.property.range[0]], + `, ${leftPropQuote}`, + ), + fixer.replaceTextRange( + [left.property.range[1], jestFnCall.range[1]], + `${leftPropQuote})${mockImplementation}`, + ), + ]; + }, + }); + }, + }; + }, +}); diff --git a/src/rules/util.js b/src/rules/util.js index 8fe5ae664..33bbce217 100644 --- a/src/rules/util.js +++ b/src/rules/util.js @@ -109,11 +109,6 @@ export const getNodeName = node => { switch (node && node.type) { case 'Identifier': return node.name; - case 'Literal': - return node.value; - case 'TemplateLiteral': - if (node.expressions.length === 0) return node.quasis[0].value.cooked; - break; case 'MemberExpression': return joinNames(getNodeName(node.object), getNodeName(node.property)); }