diff --git a/src/rules/__tests__/expect-expect.test.ts b/src/rules/__tests__/expect-expect.test.ts index 3ef7ed9a8..78b9ebbc8 100644 --- a/src/rules/__tests__/expect-expect.test.ts +++ b/src/rules/__tests__/expect-expect.test.ts @@ -17,6 +17,7 @@ ruleTester.run('expect-expect', rule, { 'it("should pass", () => expect(true).toBeDefined())', 'test("should pass", () => expect(true).toBeDefined())', 'it("should pass", () => somePromise().then(() => expect(true).toBeDefined()))', + 'it("should pass", myTest); function myTest() { expect(true).toBeDefined() }', { code: 'test("should pass", () => { expect(true).toBeDefined(); foo(true).toBe(true); })', @@ -50,6 +51,15 @@ ruleTester.run('expect-expect', rule, { }, ], }, + { + code: 'it("should fail", myTest); function myTest() {}', + errors: [ + { + messageId: 'noAssertions', + type: AST_NODE_TYPES.CallExpression, + }, + ], + }, { code: 'test("should fail", () => {});', errors: [ diff --git a/src/rules/__tests__/no-if.test.ts b/src/rules/__tests__/no-if.test.ts index 11a33f729..51585fcc0 100644 --- a/src/rules/__tests__/no-if.test.ts +++ b/src/rules/__tests__/no-if.test.ts @@ -17,6 +17,9 @@ ruleTester.run('no-if', rule, { { code: `it('foo', () => {})`, }, + { + code: `it('foo', () => {}); function myTest() { if('bar') {} }`, + }, { code: `foo('bar', () => { if(baz) {} @@ -272,6 +275,14 @@ ruleTester.run('no-if', rule, { }, ], }, + { + code: `it('foo', myTest); function myTest() { if ('bar') {} }`, + errors: [ + { + messageId: 'noIf', + }, + ], + }, { code: `describe('foo', () => { it('bar', () => { diff --git a/src/rules/__tests__/no-test-return-statement.test.ts b/src/rules/__tests__/no-test-return-statement.test.ts index 7e10d132e..3a939e525 100644 --- a/src/rules/__tests__/no-test-return-statement.test.ts +++ b/src/rules/__tests__/no-test-return-statement.test.ts @@ -23,6 +23,12 @@ ruleTester.run('no-test-prefixes', rule, { expect(1).toBe(1); }); `, + ` + it("one", myTest); + function myTest() { + expect(1).toBe(1); + } + `, ], invalid: [ { @@ -41,5 +47,14 @@ ruleTester.run('no-test-prefixes', rule, { `, errors: [{ messageId: 'noReturnValue', column: 9, line: 3 }], }, + { + code: ` + it("one", myTest); + function myTest () { + return expect(1).toBe(1); + } + `, + errors: [{ messageId: 'noReturnValue', column: 11, line: 4 }], + }, ], }); diff --git a/src/rules/__tests__/no-try-expect.test.ts b/src/rules/__tests__/no-try-expect.test.ts index d6e358d08..14329bae9 100644 --- a/src/rules/__tests__/no-try-expect.test.ts +++ b/src/rules/__tests__/no-try-expect.test.ts @@ -14,17 +14,21 @@ ruleTester.run('no-try-catch', rule, { `it('foo', () => { expect('foo').toEqual('foo'); })`, + `it('foo', () => {}) + function myTest() { + try { + } catch { + } + }`, `it('foo', () => { expect('bar').toEqual('bar'); }); try { - } catch { expect('foo').toEqual('foo'); }`, `it.skip('foo'); try { - } catch { expect('foo').toEqual('foo'); }`, @@ -44,6 +48,22 @@ ruleTester.run('no-try-catch', rule, { }, ], }, + { + code: `it('foo', myTest) + function myTest() { + try { + + } catch (err) { + expect(err).toMatch('Error'); + } + } + `, + errors: [ + { + messageId: 'noTryExpect', + }, + ], + }, { code: `it('foo', async () => { await wrapper('production', async () => { diff --git a/src/rules/expect-expect.ts b/src/rules/expect-expect.ts index 80669947e..17803a39b 100644 --- a/src/rules/expect-expect.ts +++ b/src/rules/expect-expect.ts @@ -7,7 +7,12 @@ import { AST_NODE_TYPES, TSESTree, } from '@typescript-eslint/experimental-utils'; -import { TestCaseName, createRule, getNodeName } from './utils'; +import { + TestCaseName, + createRule, + getFunctionDeclarationTestCallExpressions, + getNodeName, +} from './utils'; export default createRule< [Partial<{ assertFunctionNames: readonly string[] }>], @@ -39,7 +44,31 @@ export default createRule< }, defaultOptions: [{ assertFunctionNames: ['expect'] }], create(context, [{ assertFunctionNames = ['expect'] }]) { - const unchecked: TSESTree.CallExpression[] = []; + const unchecked: Array< + TSESTree.CallExpression | TSESTree.FunctionDeclaration + > = []; + + function checkCallExpressionUsed(nodes: TSESTree.Node[]) { + for (const node of nodes) { + const index = + node.type === AST_NODE_TYPES.CallExpression + ? unchecked.indexOf(node) + : -1; + + if (node.type === AST_NODE_TYPES.FunctionDeclaration) { + const nodes = getFunctionDeclarationTestCallExpressions( + context, + node, + ); + checkCallExpressionUsed(nodes); + } + + if (index !== -1) { + unchecked.splice(index, 1); + break; + } + } + } return { CallExpression(node) { @@ -48,17 +77,7 @@ export default createRule< unchecked.push(node); } else if (name && assertFunctionNames.includes(name)) { // Return early in case of nested `it` statements. - for (const ancestor of context.getAncestors()) { - const index = - ancestor.type === AST_NODE_TYPES.CallExpression - ? unchecked.indexOf(ancestor) - : -1; - - if (index !== -1) { - unchecked.splice(index, 1); - break; - } - } + checkCallExpressionUsed(context.getAncestors()); } }, 'Program:exit'() { diff --git a/src/rules/no-if.ts b/src/rules/no-if.ts index ce8ccc145..bb51b315e 100644 --- a/src/rules/no-if.ts +++ b/src/rules/no-if.ts @@ -1,4 +1,10 @@ -import { TestCaseName, createRule, getNodeName, isTestCase } from './utils'; +import { + TestCaseName, + createRule, + getNodeName, + isFunctionDeclarationTestCase, + isTestCase, +} from './utils'; import { AST_NODE_TYPES, TSESTree, @@ -63,8 +69,8 @@ export default createRule({ FunctionExpression() { stack.push(false); }, - FunctionDeclaration() { - stack.push(false); + FunctionDeclaration(node) { + stack.push(isFunctionDeclarationTestCase(context, node)); }, ArrowFunctionExpression(node) { stack.push(isTestArrowFunction(node)); diff --git a/src/rules/no-test-return-statement.ts b/src/rules/no-test-return-statement.ts index 0548f3085..57ebcf06b 100644 --- a/src/rules/no-test-return-statement.ts +++ b/src/rules/no-test-return-statement.ts @@ -1,4 +1,9 @@ -import { createRule, isFunction, isTestCase } from './utils'; +import { + createRule, + isFunction, + isFunctionDeclarationTestCase, + isTestCase, +} from './utils'; import { TSESTree } from '@typescript-eslint/experimental-utils'; const RETURN_STATEMENT = 'ReturnStatement'; @@ -41,6 +46,16 @@ export default createRule({ const returnStmt = body.find(t => t.type === RETURN_STATEMENT); if (!returnStmt) return; + context.report({ messageId: 'noReturnValue', node: returnStmt }); + }, + FunctionDeclaration(node) { + if (!isFunctionDeclarationTestCase(context, node)) return; + + const returnStmt = node.body.body.find( + t => t.type === RETURN_STATEMENT, + ); + if (!returnStmt) return; + context.report({ messageId: 'noReturnValue', node: returnStmt }); }, }; diff --git a/src/rules/no-try-expect.ts b/src/rules/no-try-expect.ts index efe85d328..caa48d34e 100644 --- a/src/rules/no-try-expect.ts +++ b/src/rules/no-try-expect.ts @@ -1,5 +1,10 @@ import { TSESTree } from '@typescript-eslint/experimental-utils'; -import { createRule, isExpectCall, isTestCase } from './utils'; +import { + createRule, + isExpectCall, + isFunctionDeclarationTestCase, + isTestCase, +} from './utils'; export default createRule({ name: __filename, @@ -39,6 +44,11 @@ export default createRule({ }); } }, + FunctionDeclaration(node) { + if (isFunctionDeclarationTestCase(context, node)) { + isTest = true; + } + }, CatchClause() { if (isTest) { ++catchDepth; @@ -54,6 +64,11 @@ export default createRule({ isTest = false; } }, + 'FunctionDeclaration:exit'(node) { + if (isFunctionDeclarationTestCase(context, node)) { + isTest = false; + } + }, }; }, }); diff --git a/src/rules/utils.ts b/src/rules/utils.ts index 2422010e1..726fb673b 100644 --- a/src/rules/utils.ts +++ b/src/rules/utils.ts @@ -631,6 +631,37 @@ export const isHook = ( ); }; +export const isFunctionDeclarationTestCase = ( + context: TSESLint.RuleContext, + node: TSESTree.FunctionDeclaration, +): boolean => { + return getFunctionDeclarationTestCallExpressions(context, node).length > 0; +}; + +export const getFunctionDeclarationTestCallExpressions = ( + context: TSESLint.RuleContext, + node: TSESTree.FunctionDeclaration, +): Array> => { + const variables = context.getDeclaredVariables(node); + return variables.reduce>>( + (acc, { references }) => { + const callExpressions = references + .map(({ identifier }) => identifier.parent) + .filter(isCallExpression) + .filter(isTestCase); + + return [...acc, ...callExpressions]; + }, + [], + ); +}; + +const isCallExpression = ( + node?: TSESTree.Node, +): node is TSESTree.CallExpression => { + return !!node && node.type === AST_NODE_TYPES.CallExpression; +}; + export const isTestCase = ( node: TSESTree.CallExpression, ): node is JestFunctionCallExpression => {