diff --git a/src/rules/__tests__/prefer-strict-equal.test.js b/src/rules/__tests__/prefer-strict-equal.test.ts similarity index 79% rename from src/rules/__tests__/prefer-strict-equal.test.js rename to src/rules/__tests__/prefer-strict-equal.test.ts index bd4624e28..4b6bb4b1e 100644 --- a/src/rules/__tests__/prefer-strict-equal.test.js +++ b/src/rules/__tests__/prefer-strict-equal.test.ts @@ -1,7 +1,7 @@ -import { RuleTester } from 'eslint'; +import { TSESLint } from '@typescript-eslint/experimental-utils'; import rule from '../prefer-strict-equal'; -const ruleTester = new RuleTester(); +const ruleTester = new TSESLint.RuleTester(); ruleTester.run('prefer-strict-equal', rule, { valid: [ diff --git a/src/rules/prefer-strict-equal.js b/src/rules/prefer-strict-equal.js deleted file mode 100644 index 6aaa23ea6..000000000 --- a/src/rules/prefer-strict-equal.js +++ /dev/null @@ -1,35 +0,0 @@ -import { expectCase, getDocsUrl, method } from './util'; - -export default { - meta: { - docs: { - url: getDocsUrl(__filename), - }, - messages: { - useToStrictEqual: 'Use toStrictEqual() instead', - }, - fixable: 'code', - schema: [], - }, - create(context) { - return { - CallExpression(node) { - if (!expectCase(node)) { - return; - } - - const propertyName = method(node) && method(node).name; - - if (propertyName === 'toEqual') { - context.report({ - fix(fixer) { - return [fixer.replaceText(method(node), 'toStrictEqual')]; - }, - messageId: 'useToStrictEqual', - node: method(node), - }); - } - }, - }; - }, -}; diff --git a/src/rules/prefer-strict-equal.ts b/src/rules/prefer-strict-equal.ts new file mode 100644 index 000000000..c5d115102 --- /dev/null +++ b/src/rules/prefer-strict-equal.ts @@ -0,0 +1,40 @@ +import { createRule, isExpectCallWithParent } from './tsUtils'; + +export default createRule({ + name: __filename, + meta: { + docs: { + category: 'Best Practices', + description: 'Suggest using toStrictEqual()', + recommended: false, + }, + messages: { + useToStrictEqual: 'Use toStrictEqual() instead', + }, + fixable: 'code', + schema: [], + type: 'suggestion', + }, + defaultOptions: [], + create(context) { + return { + CallExpression(node) { + if (!isExpectCallWithParent(node)) { + return; + } + + const methodNode = node.parent.property; + + if (methodNode && methodNode.name === 'toEqual') { + context.report({ + fix(fixer) { + return [fixer.replaceText(methodNode, 'toStrictEqual')]; + }, + messageId: 'useToStrictEqual', + node: methodNode, + }); + } + }, + }; + }, +}); diff --git a/src/rules/tsUtils.ts b/src/rules/tsUtils.ts index eef02d37a..01ff96164 100644 --- a/src/rules/tsUtils.ts +++ b/src/rules/tsUtils.ts @@ -15,6 +15,66 @@ export const createRule = ESLintUtils.RuleCreator(name => { return `${REPO_URL}/blob/v${version}/docs/rules/${ruleName}.md`; }); +interface JestExpectIdentifier extends TSESTree.Identifier { + name: 'expect'; +} + +/** + * Checks if the given `node` is considered a {@link JestExpectIdentifier}. + * + * A `node` is considered to be as such if it is of type `Identifier`, + * and `name`d `"expect"`. + * + * @param {Node} node + * + * @return {node is JestExpectIdentifier} + */ +const isExpectIdentifier = ( + node: TSESTree.Node, +): node is JestExpectIdentifier => + node.type === AST_NODE_TYPES.Identifier && node.name === 'expect'; + +// represents "expect()" specifically +interface JestExpectCallExpression extends TSESTree.CallExpression { + callee: JestExpectIdentifier; +} + +// represents expect usage like "expect().toBe" & "expect().not.toBe" +interface JestExpectCallMemberExpression extends TSESTree.MemberExpression { + object: JestExpectCallMemberExpression | JestExpectCallExpression; + property: TSESTree.Identifier; +} + +// represents expect usage like "expect.anything" & "expect.hasAssertions" +interface JestExpectNamespaceMemberExpression + extends TSESTree.MemberExpression { + object: JestExpectIdentifier; + property: TSESTree.Identifier; +} + +/** + * Checks if the given `node` is a {@link JestExpectCallExpression}. + * + * @param {Node} node + * + * @return {node is JestExpectCallExpression} + */ +const isExpectCall = (node: TSESTree.Node): node is JestExpectCallExpression => + node.type === AST_NODE_TYPES.CallExpression && + isExpectIdentifier(node.callee); + +interface JestExpectCallWithParent extends JestExpectCallExpression { + parent: JestExpectCallMemberExpression; +} + +export const isExpectCallWithParent = ( + node: TSESTree.Node, +): node is JestExpectCallWithParent => + isExpectCall(node) && + node.parent !== undefined && + node.parent.type === AST_NODE_TYPES.MemberExpression && + node.parent.property.type === AST_NODE_TYPES.Identifier; + export enum DescribeAlias { 'describe' = 'describe', 'fdescribe' = 'fdescribe',