diff --git a/src/rules/__tests__/prefer-called-with.test.js b/src/rules/__tests__/prefer-called-with.test.ts similarity index 90% rename from src/rules/__tests__/prefer-called-with.test.js rename to src/rules/__tests__/prefer-called-with.test.ts index 5a641a648..78c130d6d 100644 --- a/src/rules/__tests__/prefer-called-with.test.js +++ b/src/rules/__tests__/prefer-called-with.test.ts @@ -1,7 +1,7 @@ -import { RuleTester } from 'eslint'; +import { TSESLint } from '@typescript-eslint/experimental-utils'; import rule from '../prefer-called-with'; -const ruleTester = new RuleTester(); +const ruleTester = new TSESLint.RuleTester(); ruleTester.run('prefer-called-with', rule, { valid: [ diff --git a/src/rules/prefer-called-with.js b/src/rules/prefer-called-with.ts similarity index 50% rename from src/rules/prefer-called-with.js rename to src/rules/prefer-called-with.ts index 0e4f99554..26b7d789e 100644 --- a/src/rules/prefer-called-with.js +++ b/src/rules/prefer-called-with.ts @@ -1,26 +1,30 @@ import { - expectCaseWithParent, - expectNotCase, - getDocsUrl, - method, -} from './util'; + createRule, + isExpectCallWithNot, + isExpectCallWithParent, +} from './tsUtils'; -export default { +export default createRule({ + name: __filename, meta: { docs: { - url: getDocsUrl(__filename), + category: 'Best Practices', + description: 'Suggest using `toBeCalledWith` OR `toHaveBeenCalledWith`', + recommended: false, }, messages: { - preferCalledWith: 'Prefer {{name}}With(/* expected args */)', + preferCalledWith: 'Prefer {{ name }}With(/* expected args */)', }, + type: 'suggestion', schema: [], }, + defaultOptions: [], create(context) { return { CallExpression(node) { // Could check resolves/rejects here but not a likely idiom. - if (expectCaseWithParent(node) && !expectNotCase(node)) { - const methodNode = method(node); + if (isExpectCallWithParent(node) && !isExpectCallWithNot(node)) { + const methodNode = node.parent.property; const { name } = methodNode; if (name === 'toBeCalled' || name === 'toHaveBeenCalled') { context.report({ @@ -33,4 +37,4 @@ export default { }, }; }, -}; +}); diff --git a/src/rules/tsUtils.ts b/src/rules/tsUtils.ts index 675162cfe..663d6df31 100644 --- a/src/rules/tsUtils.ts +++ b/src/rules/tsUtils.ts @@ -15,10 +15,14 @@ export const createRule = ESLintUtils.RuleCreator(name => { return `${REPO_URL}/blob/v${version}/docs/rules/${ruleName}.md`; }); -interface JestExpectIdentifier extends TSESTree.Identifier { - name: 'expect'; +interface JestSpecificIdentifier + extends TSESTree.Identifier { + name: Name; } +type JestExpectIdentifier = JestSpecificIdentifier<'expect'>; +type JestNotIdentifier = JestSpecificIdentifier<'not'>; + /** * Checks if the given `node` is considered a {@link JestExpectIdentifier}. * @@ -67,6 +71,16 @@ interface JestExpectCallWithParent extends JestExpectCallExpression { parent: JestExpectCallMemberExpression; } +interface JestNotNamespaceMemberExpression + extends JestExpectCallMemberExpression { + object: JestExpectCallExpression; + property: JestNotIdentifier; +} + +interface JestExpectCallWithNotParent extends JestExpectCallWithParent { + parent: JestNotNamespaceMemberExpression; +} + export const isExpectCallWithParent = ( node: TSESTree.Node, ): node is JestExpectCallWithParent => @@ -75,6 +89,27 @@ export const isExpectCallWithParent = ( node.parent.type === AST_NODE_TYPES.MemberExpression && node.parent.property.type === AST_NODE_TYPES.Identifier; +/** + * Checks if the given `node` is a {@link JestExpectCallWithNotParent}. + * + * This is any call to `expect` that is followed by the `not` namespace: + * + * @example``` + * expect('a').not.toBe('b'); + * expect().not + * ``` + * + * @param {Node} node + * + * @return {node is JestExpectCallWithNotParent} + */ +export const isExpectCallWithNot = ( + node: TSESTree.Node, +): node is JestExpectCallWithNotParent => + isExpectCallWithParent(node) && + node.parent.object.type === AST_NODE_TYPES.CallExpression && + node.parent.property.name === 'not'; + export enum DescribeAlias { 'describe' = 'describe', 'fdescribe' = 'fdescribe',