From 71a0df0bef307afb09361b992d035cafd824265b Mon Sep 17 00:00:00 2001 From: Andrey Nelyubin Date: Sat, 13 Nov 2021 11:40:58 +0300 Subject: [PATCH] fix(require-hook): added optional settings --- docs/rules/require-hook.md | 58 ++++++++++++++++++++++++ src/rules/__tests__/require-hook.test.ts | 31 +++++++++++++ src/rules/require-hook.ts | 51 ++++++++++++++++++--- 3 files changed, 133 insertions(+), 7 deletions(-) diff --git a/docs/rules/require-hook.md b/docs/rules/require-hook.md index 350f5b5ef..7a361053f 100644 --- a/docs/rules/require-hook.md +++ b/docs/rules/require-hook.md @@ -148,3 +148,61 @@ afterEach(() => { clearCityDatabase(); }); ``` + +## Options + +If there are methods that you want to call outside of hooks and tests, you can +mark them as allowed using the `allowedFunctionCalls` option. + +```json +{ + "jest/require-hook": [ + "error", + { + "allowedFunctionCalls": ["enableAutoDestroy"] + } + ] +} +``` + +Examples of **incorrect** code for the +`{ "allowedFunctionCalls": ["enableAutoDestroy"] }` option: + +```js +/* eslint jest/require-hook: ["error", { "allowedFunctionCalls": ["enableAutoDestroy"] }] */ + +import { enableAutoDestroy, mount } from '@vue/test-utils'; +import { initDatabase, tearDownDatabase } from './databaseUtils'; + +enableAutoDestroy(afterEach); + +initDatabase(); // this will throw a linting error +tearDownDatabase(); // this will too + +describe('Foo', () => { + test('always returns 42', () => { + expect(global.getAnswer()).toBe(42); + }); +}); +``` + +Examples of **correct** code for the +`{ "allowedFunctionCalls": ["enableAutoDestroy"] }` option: + +```js +/* eslint jest/require-hook: ["error", { "allowedFunctionCalls": ["enableAutoDestroy"] }] */ + +import { enableAutoDestroy, mount } from '@vue/test-utils'; +import { initDatabase, tearDownDatabase } from './databaseUtils'; + +enableAutoDestroy(afterEach); + +beforeEach(initDatabase); +afterEach(tearDownDatabase); + +describe('Foo', () => { + test('always returns 42', () => { + expect(global.getAnswer()).toBe(42); + }); +}); +``` diff --git a/src/rules/__tests__/require-hook.test.ts b/src/rules/__tests__/require-hook.test.ts index 5c6f83ec3..4f48cb0f2 100644 --- a/src/rules/__tests__/require-hook.test.ts +++ b/src/rules/__tests__/require-hook.test.ts @@ -152,6 +152,18 @@ ruleTester.run('require-hook', rule, { }); }); `, + { + code: dedent` + enableAutoDestroy(afterEach); + + describe('some tests', () => { + it('is false', () => { + expect(true).toBe(true); + }); + }); + `, + options: [{ allowedFunctionCalls: ['enableAutoDestroy'] }], + }, ], invalid: [ { @@ -374,6 +386,25 @@ ruleTester.run('require-hook', rule, { }, ], }, + { + code: dedent` + enableAutoDestroy(afterEach); + + describe('some tests', () => { + it('is false', () => { + expect(true).toBe(true); + }); + }); + `, + options: [{ allowedFunctionCalls: ['someOtherName'] }], + errors: [ + { + messageId: 'useHook', + line: 1, + column: 1, + }, + ], + }, ], }); diff --git a/src/rules/require-hook.ts b/src/rules/require-hook.ts index 179e554a8..fcde9beff 100644 --- a/src/rules/require-hook.ts +++ b/src/rules/require-hook.ts @@ -27,12 +27,24 @@ const isNullOrUndefined = (node: TSESTree.Expression): boolean => { ); }; -const shouldBeInHook = (node: TSESTree.Node): boolean => { +const isExcludedFnCall = ( + node: TSESTree.CallExpression, + allowedFunctionCalls: string[], +): boolean => { + return allowedFunctionCalls.includes(getNodeName(node) as string); +}; + +const shouldBeInHook = ( + node: TSESTree.Node, + allowedFunctionCalls: string[], +): boolean => { switch (node.type) { case AST_NODE_TYPES.ExpressionStatement: - return shouldBeInHook(node.expression); + return shouldBeInHook(node.expression, allowedFunctionCalls); case AST_NODE_TYPES.CallExpression: - return !isJestFnCall(node); + return !( + isJestFnCall(node) || isExcludedFnCall(node, allowedFunctionCalls) + ); case AST_NODE_TYPES.VariableDeclaration: { if (node.kind === 'const') { return false; @@ -48,7 +60,10 @@ const shouldBeInHook = (node: TSESTree.Node): boolean => { } }; -export default createRule({ +export default createRule< + [Partial<{ allowedFunctionCalls?: readonly string[] }>], + 'useHook' +>({ name: __filename, meta: { docs: { @@ -60,13 +75,35 @@ export default createRule({ useHook: 'This should be done within a hook', }, type: 'suggestion', - schema: [], + schema: [ + { + type: 'object', + properties: { + allowedFunctionCalls: { + type: 'array', + items: { type: 'string' }, + }, + }, + additionalProperties: false, + }, + ], }, - defaultOptions: [], + defaultOptions: [ + { + allowedFunctionCalls: [], + }, + ], create(context) { + const { allowedFunctionCalls } = context.options[0] ?? {}; + const checkBlockBody = (body: TSESTree.BlockStatement['body']) => { for (const statement of body) { - if (shouldBeInHook(statement)) { + if ( + shouldBeInHook( + statement, + Array.isArray(allowedFunctionCalls) ? allowedFunctionCalls : [], + ) + ) { context.report({ node: statement, messageId: 'useHook',