diff --git a/lib/detect-testing-library-utils.ts b/lib/detect-testing-library-utils.ts index 9413c881..7559ae8f 100644 --- a/lib/detect-testing-library-utils.ts +++ b/lib/detect-testing-library-utils.ts @@ -63,7 +63,10 @@ type IsSyncQueryFn = (node: TSESTree.Identifier) => boolean; type IsAsyncQueryFn = (node: TSESTree.Identifier) => boolean; type IsQueryFn = (node: TSESTree.Identifier) => boolean; type IsCustomQueryFn = (node: TSESTree.Identifier) => boolean; -type IsAsyncUtilFn = (node: TSESTree.Identifier) => boolean; +type IsAsyncUtilFn = ( + node: TSESTree.Identifier, + validNames?: readonly typeof ASYNC_UTILS[number][] +) => boolean; type IsFireEventMethodFn = (node: TSESTree.Identifier) => boolean; type IsRenderUtilFn = (node: TSESTree.Identifier) => boolean; type IsPresenceAssertFn = (node: TSESTree.MemberExpression) => boolean; @@ -298,9 +301,15 @@ export function detectTestingLibraryUtils< * Otherwise, it means `custom-module` has been set up, so only those nodes * coming from Testing Library will be considered as valid. */ - const isAsyncUtil: IsAsyncUtilFn = (node) => { - return isTestingLibraryUtil(node, (identifierNodeName) => - ASYNC_UTILS.includes(identifierNodeName) + const isAsyncUtil: IsAsyncUtilFn = (node, validNames = ASYNC_UTILS) => { + return isTestingLibraryUtil( + node, + (identifierNodeName, originalNodeName) => { + return ( + (validNames as string[]).includes(identifierNodeName) || + (validNames as string[]).includes(originalNodeName) + ); + } ); }; diff --git a/lib/rules/no-wait-for-empty-callback.ts b/lib/rules/no-wait-for-empty-callback.ts index 7ff27dd9..1b88fc98 100644 --- a/lib/rules/no-wait-for-empty-callback.ts +++ b/lib/rules/no-wait-for-empty-callback.ts @@ -1,19 +1,16 @@ +import { ASTUtils, TSESTree } from '@typescript-eslint/experimental-utils'; import { - ESLintUtils, - TSESTree, - ASTUtils, -} from '@typescript-eslint/experimental-utils'; -import { getDocsUrl } from '../utils'; -import { isBlockStatement, isCallExpression } from '../node-utils'; + getPropertyIdentifierNode, + isBlockStatement, + isCallExpression, +} from '../node-utils'; +import { createTestingLibraryRule } from '../create-testing-library-rule'; export const RULE_NAME = 'no-wait-for-empty-callback'; export type MessageIds = 'noWaitForEmptyCallback'; type Options = []; -const WAIT_EXPRESSION_QUERY = - 'CallExpression[callee.name=/^(waitFor|waitForElementToBeRemoved)$/]'; - -export default ESLintUtils.RuleCreator(getDocsUrl)({ +export default createTestingLibraryRule({ name: RULE_NAME, meta: { type: 'suggestion', @@ -33,11 +30,24 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ defaultOptions: [], // trimmed down implementation of https://github.com/eslint/eslint/blob/master/lib/rules/no-empty-function.js - // TODO: var referencing any of previously mentioned? - create: function (context) { + create(context, _, helpers) { + function isValidWaitFor(node: TSESTree.Node): boolean { + const parentCallExpression = node.parent as TSESTree.CallExpression; + const parentIdentifier = getPropertyIdentifierNode(parentCallExpression); + + return helpers.isAsyncUtil(parentIdentifier, [ + 'waitFor', + 'waitForElementToBeRemoved', + ]); + } + function reportIfEmpty( node: TSESTree.ArrowFunctionExpression | TSESTree.FunctionExpression ) { + if (!isValidWaitFor(node)) { + return; + } + if ( isBlockStatement(node.body) && node.body.body.length === 0 && @@ -56,17 +66,27 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ } function reportNoop(node: TSESTree.Identifier) { + if (!isValidWaitFor(node)) { + return; + } + context.report({ node, loc: node.loc.start, messageId: 'noWaitForEmptyCallback', + data: { + methodName: + isCallExpression(node.parent) && + ASTUtils.isIdentifier(node.parent.callee) && + node.parent.callee.name, + }, }); } return { - [`${WAIT_EXPRESSION_QUERY} > ArrowFunctionExpression`]: reportIfEmpty, - [`${WAIT_EXPRESSION_QUERY} > FunctionExpression`]: reportIfEmpty, - [`${WAIT_EXPRESSION_QUERY} > Identifier[name="noop"]`]: reportNoop, + 'CallExpression > ArrowFunctionExpression': reportIfEmpty, + 'CallExpression > FunctionExpression': reportIfEmpty, + 'CallExpression > Identifier[name="noop"]': reportNoop, }; }, }); diff --git a/lib/utils.ts b/lib/utils.ts index b7c6725d..a464a517 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -70,7 +70,7 @@ const ASYNC_UTILS = [ 'wait', 'waitForElement', 'waitForDomChange', -]; +] as const; const SYNC_EVENTS = ['fireEvent', 'userEvent']; diff --git a/tests/lib/rules/no-wait-for-empty-callback.test.ts b/tests/lib/rules/no-wait-for-empty-callback.test.ts index 17d8de07..76e57789 100644 --- a/tests/lib/rules/no-wait-for-empty-callback.test.ts +++ b/tests/lib/rules/no-wait-for-empty-callback.test.ts @@ -29,6 +29,24 @@ ruleTester.run(RULE_NAME, rule, { { code: `wait(() => {})`, }, + { + code: `wait(noop)`, + }, + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` + import { waitFor } from 'somewhere-else' + waitFor(() => {}) + `, + }, + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` + import { waitFor as renamedWaitFor } from '@testing-library/react' + import { waitFor } from 'somewhere-else' + waitFor(() => {}) + `, + }, ], invalid: [ @@ -36,7 +54,46 @@ ruleTester.run(RULE_NAME, rule, { code: `${m}(() => {})`, errors: [ { + line: 1, + column: 8 + m.length, + messageId: 'noWaitForEmptyCallback', + data: { + methodName: m, + }, + }, + ], + })), + ...ALL_WAIT_METHODS.map((m) => ({ + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` + import { ${m} } from 'test-utils'; + ${m}(() => {}); + `, + errors: [ + { + line: 3, + column: 16 + m.length, + messageId: 'noWaitForEmptyCallback', + data: { + methodName: m, + }, + }, + ], + })), + ...ALL_WAIT_METHODS.map((m) => ({ + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` + import { ${m} as renamedAsyncUtil } from 'test-utils'; + renamedAsyncUtil(() => {}); + `, + errors: [ + { + line: 3, + column: 32, messageId: 'noWaitForEmptyCallback', + data: { + methodName: 'renamedAsyncUtil', + }, }, ], })), @@ -44,7 +101,12 @@ ruleTester.run(RULE_NAME, rule, { code: `${m}((a, b) => {})`, errors: [ { + line: 1, + column: 12 + m.length, messageId: 'noWaitForEmptyCallback', + data: { + methodName: m, + }, }, ], })), @@ -52,7 +114,12 @@ ruleTester.run(RULE_NAME, rule, { code: `${m}(() => { /* I'm empty anyway */ })`, errors: [ { + line: 1, + column: 8 + m.length, messageId: 'noWaitForEmptyCallback', + data: { + methodName: m, + }, }, ], })), @@ -63,7 +130,12 @@ ruleTester.run(RULE_NAME, rule, { })`, errors: [ { + line: 1, + column: 13 + m.length, messageId: 'noWaitForEmptyCallback', + data: { + methodName: m, + }, }, ], })), @@ -73,7 +145,12 @@ ruleTester.run(RULE_NAME, rule, { })`, errors: [ { + line: 1, + column: 14 + m.length, messageId: 'noWaitForEmptyCallback', + data: { + methodName: m, + }, }, ], })), @@ -83,7 +160,12 @@ ruleTester.run(RULE_NAME, rule, { })`, errors: [ { + line: 1, + column: 13 + m.length, messageId: 'noWaitForEmptyCallback', + data: { + methodName: m, + }, }, ], })), @@ -92,7 +174,12 @@ ruleTester.run(RULE_NAME, rule, { code: `${m}(noop)`, errors: [ { + line: 1, + column: 2 + m.length, messageId: 'noWaitForEmptyCallback', + data: { + methodName: m, + }, }, ], })),