diff --git a/src/rules/__tests__/lowercase-name.test.ts b/src/rules/__tests__/lowercase-name.test.ts index 31fbbf54c..271361d9a 100644 --- a/src/rules/__tests__/lowercase-name.test.ts +++ b/src/rules/__tests__/lowercase-name.test.ts @@ -191,7 +191,7 @@ ruleTester.run('lowercase-name', rule, { { messageId: 'unexpectedLowercase', data: { method: TestCaseName.it }, - column: 9, + column: 29, line: 1, }, ], @@ -203,7 +203,7 @@ ruleTester.run('lowercase-name', rule, { { messageId: 'unexpectedLowercase', data: { method: DescribeAlias.describe }, - column: 15, + column: 35, line: 1, }, ], diff --git a/src/rules/__tests__/no-conditional-expect.test.ts b/src/rules/__tests__/no-conditional-expect.test.ts index b611d33fb..5b3637208 100644 --- a/src/rules/__tests__/no-conditional-expect.test.ts +++ b/src/rules/__tests__/no-conditional-expect.test.ts @@ -151,6 +151,14 @@ ruleTester.run('logical conditions', rule, { `, errors: [{ messageId: 'conditionalExpect' }], }, + { + code: ` + it.each()('foo', () => { + something || expect(something).toHaveBeenCalled(); + }) + `, + errors: [{ messageId: 'conditionalExpect' }], + }, { code: ` function getValue() { @@ -218,6 +226,14 @@ ruleTester.run('conditional conditions', rule, { `, errors: [{ messageId: 'conditionalExpect' }], }, + { + code: ` + it.each()('foo', () => { + something ? noop() : expect(something).toHaveBeenCalled(); + }) + `, + errors: [{ messageId: 'conditionalExpect' }], + }, { code: ` function getValue() { @@ -304,6 +320,19 @@ ruleTester.run('switch conditions', rule, { `, errors: [{ messageId: 'conditionalExpect' }], }, + { + code: ` + it.each()('foo', () => { + switch(something) { + case 'value': + expect(something).toHaveBeenCalled(); + default: + break; + } + }) + `, + errors: [{ messageId: 'conditionalExpect' }], + }, { code: ` function getValue() { @@ -384,6 +413,18 @@ ruleTester.run('if conditions', rule, { `, errors: [{ messageId: 'conditionalExpect' }], }, + { + code: ` + it.each()('foo', () => { + if(!doSomething) { + // do nothing + } else { + expect(something).toHaveBeenCalled(); + } + }) + `, + errors: [{ messageId: 'conditionalExpect' }], + }, { code: ` function getValue() { @@ -491,6 +532,18 @@ ruleTester.run('catch conditions', rule, { `, errors: [{ messageId: 'conditionalExpect' }], }, + { + code: ` + it.each()('foo', () => { + try { + + } catch (err) { + expect(err).toMatch('Error'); + } + }) + `, + errors: [{ messageId: 'conditionalExpect' }], + }, { code: ` it.skip.each\`\`('foo', () => { @@ -503,6 +556,18 @@ ruleTester.run('catch conditions', rule, { `, errors: [{ messageId: 'conditionalExpect' }], }, + { + code: ` + it.skip.each()('foo', () => { + try { + + } catch (err) { + expect(err).toMatch('Error'); + } + }) + `, + errors: [{ messageId: 'conditionalExpect' }], + }, { code: ` function getValue() { diff --git a/src/rules/__tests__/no-focused-tests.test.ts b/src/rules/__tests__/no-focused-tests.test.ts index c6229e770..71a60cf1a 100644 --- a/src/rules/__tests__/no-focused-tests.test.ts +++ b/src/rules/__tests__/no-focused-tests.test.ts @@ -46,7 +46,7 @@ ruleTester.run('no-focused-tests', rule, { ], }, { - code: 'describe.only.each()', + code: 'describe.only.each()()', errors: [ { messageId: 'focusedTest', @@ -55,7 +55,7 @@ ruleTester.run('no-focused-tests', rule, { suggestions: [ { messageId: 'suggestRemoveFocus', - output: 'describe.each()', + output: 'describe.each()()', }, ], }, @@ -126,7 +126,7 @@ ruleTester.run('no-focused-tests', rule, { ], }, { - code: 'it.only.each()', + code: 'it.only.each()()', errors: [ { messageId: 'focusedTest', @@ -135,7 +135,7 @@ ruleTester.run('no-focused-tests', rule, { suggestions: [ { messageId: 'suggestRemoveFocus', - output: 'it.each()', + output: 'it.each()()', }, ], }, @@ -206,7 +206,7 @@ ruleTester.run('no-focused-tests', rule, { ], }, { - code: 'test.only.each()', + code: 'test.only.each()()', errors: [ { messageId: 'focusedTest', @@ -215,7 +215,7 @@ ruleTester.run('no-focused-tests', rule, { suggestions: [ { messageId: 'suggestRemoveFocus', - output: 'test.each()', + output: 'test.each()()', }, ], }, @@ -286,7 +286,7 @@ ruleTester.run('no-focused-tests', rule, { ], }, { - code: 'fit.each()', + code: 'fit.each()()', errors: [ { messageId: 'focusedTest', @@ -295,7 +295,7 @@ ruleTester.run('no-focused-tests', rule, { suggestions: [ { messageId: 'suggestRemoveFocus', - output: 'it.each()', + output: 'it.each()()', }, ], }, diff --git a/src/rules/__tests__/no-if.test.ts b/src/rules/__tests__/no-if.test.ts index d6554638e..f890baac7 100644 --- a/src/rules/__tests__/no-if.test.ts +++ b/src/rules/__tests__/no-if.test.ts @@ -27,6 +27,13 @@ ruleTester.run('conditional expressions', rule, { }; }); `, + dedent` + it.each()('foo', function () { + const foo = function (bar) { + return foo ? bar : null; + }; + }); + `, ], invalid: [ { @@ -120,6 +127,11 @@ ruleTester.run('switch statements', rule, { switch('bar') {} }) `, + dedent` + describe.skip.each()('foo', () => { + switch('bar') {} + }) + `, dedent` xdescribe('foo', () => { switch('bar') {} @@ -501,6 +513,13 @@ ruleTester.run('if statements', rule, { }); }) `, + dedent` + describe.each\`\`('foo', () => { + afterEach(() => { + if('bar') {} + }); + }) + `, dedent` describe('foo', () => { beforeEach(() => { @@ -826,6 +845,20 @@ ruleTester.run('if statements', rule, { }, ], }, + { + code: dedent` + it.each()('foo', () => { + callExpression() + if ('bar') {} + }) + `, + errors: [ + { + data: { condition: 'if' }, + messageId: 'conditionalInTest', + }, + ], + }, { code: dedent` it.only.each\`\`('foo', () => { @@ -840,6 +873,20 @@ ruleTester.run('if statements', rule, { }, ], }, + { + code: dedent` + it.only.each()('foo', () => { + callExpression() + if ('bar') {} + }) + `, + errors: [ + { + data: { condition: 'if' }, + messageId: 'conditionalInTest', + }, + ], + }, { code: dedent` describe('valid', () => { diff --git a/src/rules/__tests__/no-test-return-statement.test.ts b/src/rules/__tests__/no-test-return-statement.test.ts index 686c078f4..2d5a886e8 100644 --- a/src/rules/__tests__/no-test-return-statement.test.ts +++ b/src/rules/__tests__/no-test-return-statement.test.ts @@ -68,6 +68,14 @@ ruleTester.run('no-test-return-statement', rule, { `, errors: [{ messageId: 'noReturnValue', column: 3, line: 2 }], }, + { + code: dedent` + it.each()("one", function () { + return expect(1).toBe(1); + }); + `, + errors: [{ messageId: 'noReturnValue', column: 3, line: 2 }], + }, { code: dedent` it.only.each\`\`("one", function () { @@ -76,6 +84,14 @@ ruleTester.run('no-test-return-statement', rule, { `, errors: [{ messageId: 'noReturnValue', column: 3, line: 2 }], }, + { + code: dedent` + it.only.each()("one", function () { + return expect(1).toBe(1); + }); + `, + errors: [{ messageId: 'noReturnValue', column: 3, line: 2 }], + }, { code: dedent` it("one", myTest); diff --git a/src/rules/__tests__/utils.test.ts b/src/rules/__tests__/utils.test.ts index ead3ebfea..4bbb5d927 100644 --- a/src/rules/__tests__/utils.test.ts +++ b/src/rules/__tests__/utils.test.ts @@ -20,6 +20,7 @@ const rule = createRule({ messages: { details: [ 'callType', // + 'numOfArgs', ] .map(data => `${data}: {{ ${data} }}`) .join('\n'), @@ -38,7 +39,10 @@ const rule = createRule({ context.report({ messageId: 'details', node, - data: { callType }, + data: { + callType, + numOfArgs: node.arguments.length, + }, }); } }, @@ -86,7 +90,10 @@ const testUtilsAgainst = ( errors: [ { messageId: 'details' as const, - data: { callType }, + data: { + callType, + numOfArgs: 2, + }, column: 1, line: 1, }, diff --git a/src/rules/consistent-test-it.ts b/src/rules/consistent-test-it.ts index 6675df48e..c990fe5f2 100644 --- a/src/rules/consistent-test-it.ts +++ b/src/rules/consistent-test-it.ts @@ -8,7 +8,6 @@ import { createRule, getNodeName, isDescribeCall, - isEachCall, isTestCaseCall, } from './utils'; @@ -86,6 +85,8 @@ export default createRule< const funcNode = node.callee.type === AST_NODE_TYPES.TaggedTemplateExpression ? node.callee.tag + : node.callee.type === AST_NODE_TYPES.CallExpression + ? node.callee.callee : node.callee; if ( @@ -121,7 +122,7 @@ export default createRule< } }, 'CallExpression:exit'(node) { - if (isDescribeCall(node) && !isEachCall(node)) { + if (isDescribeCall(node)) { describeNestingLevel--; } }, diff --git a/src/rules/lowercase-name.ts b/src/rules/lowercase-name.ts index ce2c78644..8b67cfa59 100644 --- a/src/rules/lowercase-name.ts +++ b/src/rules/lowercase-name.ts @@ -1,17 +1,13 @@ -import { - AST_NODE_TYPES, - TSESLint, - TSESTree, -} from '@typescript-eslint/experimental-utils'; +import { TSESLint, TSESTree } from '@typescript-eslint/experimental-utils'; import { CallExpressionWithSingleArgument, DescribeAlias, StringNode, TestCaseName, createRule, + getNodeName, getStringValue, isDescribeCall, - isEachCall, isStringNode, isTestCaseCall, } from './utils'; @@ -33,25 +29,11 @@ const findNodeNameAndArgument = ( return null; } - if (isEachCall(node)) { - if ( - node.parent.arguments.length > 0 && - isStringNode(node.parent.arguments[0]) - ) { - return [node.callee.object.name, node.parent.arguments[0]]; - } - - return null; - } - - if ( - node.callee.type !== AST_NODE_TYPES.Identifier || - !hasStringAsFirstArgument(node) - ) { + if (!hasStringAsFirstArgument(node)) { return null; } - return [node.callee.name, node.arguments[0]]; + return [getNodeName(node).split('.')[0], node.arguments[0]]; }; export default createRule< diff --git a/src/rules/no-duplicate-hooks.ts b/src/rules/no-duplicate-hooks.ts index d1aaecaf2..e8b02754a 100644 --- a/src/rules/no-duplicate-hooks.ts +++ b/src/rules/no-duplicate-hooks.ts @@ -1,4 +1,4 @@ -import { createRule, isDescribeCall, isEachCall, isHook } from './utils'; +import { createRule, isDescribeCall, isHook } from './utils'; const newHookContext = () => ({ beforeAll: 0, @@ -45,7 +45,7 @@ export default createRule({ } }, 'CallExpression:exit'(node) { - if (isDescribeCall(node) && !isEachCall(node)) { + if (isDescribeCall(node)) { hookContexts.pop(); } }, diff --git a/src/rules/no-focused-tests.ts b/src/rules/no-focused-tests.ts index 89ffb3500..f971ccaad 100644 --- a/src/rules/no-focused-tests.ts +++ b/src/rules/no-focused-tests.ts @@ -1,47 +1,38 @@ +import { AST_NODE_TYPES } from '@typescript-eslint/experimental-utils'; import { - AST_NODE_TYPES, - TSESTree, -} from '@typescript-eslint/experimental-utils'; -import { - DescribeAlias, - TestCaseName, - TestCaseProperty, + AccessorNode, + JestFunctionCallExpression, createRule, + getNodeName, + isDescribeCall, isSupportedAccessor, + isTestCaseCall, } from './utils'; -const validTestCaseNames = [TestCaseName.test, TestCaseName.it]; - -const testFunctions = new Set([ - DescribeAlias.describe, - ...validTestCaseNames, -]); - -interface ConcurrentExpression extends TSESTree.MemberExpressionComputedName { - parent: TSESTree.MemberExpression; -} - -const isConcurrentExpression = ( - expression: TSESTree.MemberExpression, -): expression is ConcurrentExpression => - isSupportedAccessor(expression.property, TestCaseProperty.concurrent) && - !!expression.parent && - expression.parent.type === AST_NODE_TYPES.MemberExpression; - -const matchesTestFunction = (object: TSESTree.LeftHandSideExpression) => - 'name' in object && - typeof object.name === 'string' && - (object.name in TestCaseName || object.name in DescribeAlias); +const findOnlyNode = ( + node: JestFunctionCallExpression, +): AccessorNode<'only'> | null => { + const callee = + node.callee.type === AST_NODE_TYPES.TaggedTemplateExpression + ? node.callee.tag + : node.callee.type === AST_NODE_TYPES.CallExpression + ? node.callee.callee + : node.callee; + + if (callee.type === AST_NODE_TYPES.MemberExpression) { + if (callee.object.type === AST_NODE_TYPES.MemberExpression) { + if (isSupportedAccessor(callee.object.property, 'only')) { + return callee.object.property; + } + } -const isCallToFocusedTestFunction = (object: TSESTree.Identifier) => - object.name.startsWith('f') && testFunctions.has(object.name.substring(1)); + if (isSupportedAccessor(callee.property, 'only')) { + return callee.property; + } + } -const isCallToTestOnlyFunction = (callee: TSESTree.MemberExpression) => - matchesTestFunction(callee.object) && - isSupportedAccessor( - isConcurrentExpression(callee) ? callee.parent.property : callee.property, - 'only', - ); + return null; +}; export default createRule({ name: __filename, @@ -62,113 +53,47 @@ export default createRule({ defaultOptions: [], create: context => ({ CallExpression(node) { - const callee = - node.callee.type === AST_NODE_TYPES.TaggedTemplateExpression - ? node.callee.tag - : node.callee; - - if (callee.type === AST_NODE_TYPES.MemberExpression) { - const calleeObject = callee.object; - - if ( - calleeObject.type === AST_NODE_TYPES.Identifier && - isCallToFocusedTestFunction(calleeObject) - ) { - context.report({ - messageId: 'focusedTest', - node: calleeObject, - suggest: [ - { - messageId: 'suggestRemoveFocus', - fix(fixer) { - return fixer.removeRange([ - calleeObject.range[0], - calleeObject.range[0] + 1, - ]); - }, - }, - ], - }); - - return; - } - - if ( - calleeObject.type === AST_NODE_TYPES.MemberExpression && - isCallToTestOnlyFunction(calleeObject) - ) { - context.report({ - messageId: 'focusedTest', - node: isConcurrentExpression(calleeObject) - ? callee.property - : calleeObject.property, - suggest: [ - { - messageId: 'suggestRemoveFocus', - fix(fixer) { - if ( - calleeObject.property.type === AST_NODE_TYPES.Identifier && - calleeObject.property.name === 'only' - ) { - return fixer.removeRange([ - calleeObject.object.range[1], - calleeObject.range[1], - ]); - } - - return fixer.removeRange([ - calleeObject.range[1], - callee.range[1], - ]); - }, - }, - ], - }); - - return; - } - - if (isCallToTestOnlyFunction(callee)) { - context.report({ - messageId: 'focusedTest', - node: callee.property, - suggest: [ - { - messageId: 'suggestRemoveFocus', - fix(fixer) { - return fixer.removeRange([ - calleeObject.range[1], - callee.range[1], - ]); - }, - }, - ], - }); - - return; - } + if (!isDescribeCall(node) && !isTestCaseCall(node)) { + return; } - if ( - callee.type === AST_NODE_TYPES.Identifier && - isCallToFocusedTestFunction(callee) - ) { + if (getNodeName(node).startsWith('f')) { context.report({ messageId: 'focusedTest', - node: callee, + node, suggest: [ { messageId: 'suggestRemoveFocus', - fix(fixer) { - return fixer.removeRange([ - callee.range[0], - callee.range[0] + 1, - ]); - }, + fix: fixer => + fixer.removeRange([node.range[0], node.range[0] + 1]), }, ], }); + + return; } + + const onlyNode = findOnlyNode(node); + + if (!onlyNode) { + return; + } + + context.report({ + messageId: 'focusedTest', + node: onlyNode, + suggest: [ + { + messageId: 'suggestRemoveFocus', + fix: fixer => + fixer.removeRange([ + onlyNode.range[0] - 1, + onlyNode.range[1] + + Number(onlyNode.type !== AST_NODE_TYPES.Identifier), + ]), + }, + ], + }); }, }), }); diff --git a/src/rules/no-if.ts b/src/rules/no-if.ts index f8ba2f236..0203d4953 100644 --- a/src/rules/no-if.ts +++ b/src/rules/no-if.ts @@ -77,6 +77,10 @@ export default createRule({ CallExpression(node) { if (isTestCaseCall(node)) { stack.push(true); + + if (getNodeName(node).endsWith('each')) { + stack.push(true); + } } }, FunctionExpression(node) { diff --git a/src/rules/no-standalone-expect.ts b/src/rules/no-standalone-expect.ts index 247775673..7f5e02820 100644 --- a/src/rules/no-standalone-expect.ts +++ b/src/rules/no-standalone-expect.ts @@ -4,7 +4,6 @@ import { } from '@typescript-eslint/experimental-utils'; import { DescribeAlias, - TestCaseName, createRule, getNodeName, isDescribeCall, @@ -47,14 +46,6 @@ const getBlockType = ( return null; }; -const isEach = (node: TSESTree.CallExpression): boolean => - node.callee.type === AST_NODE_TYPES.CallExpression && - node.callee.callee.type === AST_NODE_TYPES.MemberExpression && - node.callee.callee.property.type === AST_NODE_TYPES.Identifier && - node.callee.callee.property.name === 'each' && - node.callee.callee.object.type === AST_NODE_TYPES.Identifier && - TestCaseName.hasOwnProperty(node.callee.callee.object.name); - type BlockType = 'test' | 'function' | 'describe' | 'arrow' | 'template'; export default createRule< @@ -121,9 +112,8 @@ export default createRule< if ( (top === 'test' && - (isEach(node) || - (isTestBlock(node) && - node.callee.type !== AST_NODE_TYPES.MemberExpression))) || + isTestBlock(node) && + node.callee.type !== AST_NODE_TYPES.MemberExpression) || (top === 'template' && node.callee.type === AST_NODE_TYPES.TaggedTemplateExpression) ) { diff --git a/src/rules/no-test-prefixes.ts b/src/rules/no-test-prefixes.ts index f708aa4c0..9b30d68c8 100644 --- a/src/rules/no-test-prefixes.ts +++ b/src/rules/no-test-prefixes.ts @@ -37,6 +37,8 @@ export default createRule({ const funcNode = node.callee.type === AST_NODE_TYPES.TaggedTemplateExpression ? node.callee.tag + : node.callee.type === AST_NODE_TYPES.CallExpression + ? node.callee.callee : node.callee; context.report({ diff --git a/src/rules/prefer-expect-assertions.ts b/src/rules/prefer-expect-assertions.ts index 0a3ce8342..fb8eb41ca 100644 --- a/src/rules/prefer-expect-assertions.ts +++ b/src/rules/prefer-expect-assertions.ts @@ -8,7 +8,6 @@ import { createRule, getAccessorValue, hasOnlyOneArgument, - isEachCall, isFunction, isSupportedAccessor, isTestCaseCall, @@ -105,13 +104,11 @@ export default createRule<[RuleOptions], MessageIds>({ return; } - const args = isEachCall(node) ? node.parent.arguments : node.arguments; - - if (args.length < 2) { + if (node.arguments.length < 2) { return; } - const [, testFn] = args; + const [, testFn] = node.arguments; if ( !isFunction(testFn) || diff --git a/src/rules/prefer-todo.ts b/src/rules/prefer-todo.ts index 8bdd8dff5..1dbcca6d6 100644 --- a/src/rules/prefer-todo.ts +++ b/src/rules/prefer-todo.ts @@ -28,7 +28,7 @@ function createTodoFixer( node: JestFunctionCallExpression, fixer: TSESLint.RuleFixer, ) { - const testName = getNodeName(node.callee).split('.').shift(); + const testName = getNodeName(node).split('.').shift(); return fixer.replaceText(node.callee, `${testName}.todo`); } @@ -38,7 +38,7 @@ const isTargetedTestCase = ( ): node is JestFunctionCallExpression => isTestCaseCall(node) && [TestCaseName.it, TestCaseName.test, 'it.skip', 'test.skip'].includes( - getNodeName(node.callee), + getNodeName(node), ); export default createRule({ diff --git a/src/rules/require-top-level-describe.ts b/src/rules/require-top-level-describe.ts index 12cb047aa..73509c0f3 100644 --- a/src/rules/require-top-level-describe.ts +++ b/src/rules/require-top-level-describe.ts @@ -1,11 +1,5 @@ import { TSESTree } from '@typescript-eslint/experimental-utils'; -import { - createRule, - isDescribeCall, - isEachCall, - isHook, - isTestCaseCall, -} from './utils'; +import { createRule, isDescribeCall, isHook, isTestCaseCall } from './utils'; export default createRule({ name: __filename, @@ -50,7 +44,7 @@ export default createRule({ } }, 'CallExpression:exit'(node: TSESTree.CallExpression) { - if (isDescribeCall(node) && !isEachCall(node)) { + if (isDescribeCall(node)) { numberOfDescribeBlocks--; } }, diff --git a/src/rules/utils.ts b/src/rules/utils.ts index efe6e34cf..4cb2f1f92 100644 --- a/src/rules/utils.ts +++ b/src/rules/utils.ts @@ -262,7 +262,7 @@ export const getAccessorValue = ( ? accessor.name : getStringValue(accessor); -type AccessorNode = +export type AccessorNode = | StringNode | KnownIdentifier; @@ -580,16 +580,42 @@ export interface JestFunctionCallExpressionWithIdentifierCallee< callee: JestFunctionIdentifier; } -interface JestFunctionCallExpressionWithTaggedTemplateCallee - extends TSESTree.CallExpression { - callee: TSESTree.TaggedTemplateExpression; +interface JestEachMemberExpression< + TName extends Exclude +> extends KnownMemberExpression<'each'> { + object: + | KnownIdentifier + | (KnownMemberExpression & { object: KnownIdentifier }); +} + +export interface JestCalledEachCallExpression< + TName extends Exclude +> extends TSESTree.CallExpression { + callee: TSESTree.CallExpression & { + callee: JestEachMemberExpression; + }; } +export interface JestTaggedEachCallExpression< + TName extends Exclude +> extends TSESTree.CallExpression { + callee: TSESTree.TaggedTemplateExpression & { + tag: JestEachMemberExpression; + }; +} + +type JestEachCallExpression< + TName extends Exclude +> = JestCalledEachCallExpression | JestTaggedEachCallExpression; + export type JestFunctionCallExpression< - FunctionName extends JestFunctionName = JestFunctionName + FunctionName extends Exclude = Exclude< + JestFunctionName, + HookName + > > = + | JestEachCallExpression | JestFunctionCallExpressionWithMemberExpressionCallee - | JestFunctionCallExpressionWithTaggedTemplateCallee | JestFunctionCallExpressionWithIdentifierCallee; const joinNames = (a: string | null, b: string | null): string | null => @@ -597,6 +623,7 @@ const joinNames = (a: string | null, b: string | null): string | null => export function getNodeName( node: + | JestFunctionCallExpression | JestFunctionMemberExpression | JestFunctionIdentifier | TSESTree.TaggedTemplateExpression, @@ -686,17 +713,19 @@ export const isTestCaseCall = ( const callee = node.callee.type === AST_NODE_TYPES.TaggedTemplateExpression ? node.callee.tag + : node.callee.type === AST_NODE_TYPES.CallExpression + ? node.callee.callee : node.callee; if ( callee.type === AST_NODE_TYPES.MemberExpression && isTestCaseProperty(callee.property) ) { - // if we're an `each()`, ensure we're being called (i.e `.each()()`) + // if we're an `each()`, ensure we're the outer CallExpression (i.e `.each()()`) if ( + getAccessorValue(callee.property) === 'each' && node.callee.type !== AST_NODE_TYPES.TaggedTemplateExpression && - node.parent?.type !== AST_NODE_TYPES.CallExpression && - getAccessorValue(callee.property) === 'each' + node.callee.type !== AST_NODE_TYPES.CallExpression ) { return false; } @@ -740,17 +769,19 @@ export const isDescribeCall = ( const callee = node.callee.type === AST_NODE_TYPES.TaggedTemplateExpression ? node.callee.tag + : node.callee.type === AST_NODE_TYPES.CallExpression + ? node.callee.callee : node.callee; if ( callee.type === AST_NODE_TYPES.MemberExpression && isDescribeProperty(callee.property) ) { - // if we're an `each()`, ensure we're being called (i.e `.each()()`) + // if we're an `each()`, ensure we're the outer CallExpression (i.e `.each()()`) if ( + getAccessorValue(callee.property) === 'each' && node.callee.type !== AST_NODE_TYPES.TaggedTemplateExpression && - node.parent?.type !== AST_NODE_TYPES.CallExpression && - getAccessorValue(callee.property) === 'each' + node.callee.type !== AST_NODE_TYPES.CallExpression ) { return false; } @@ -763,44 +794,6 @@ export const isDescribeCall = ( return false; }; -/** - * Checks if the given node` is a call to `.each(...)()`. - * If `true`, the code must look like `.each(...)()`. - * - * @param {JestFunctionCallExpression} node - * - * @return {node is JestFunctionCallExpressionWithMemberExpressionCallee & {parent: TSESTree.CallExpression}} - */ -export const isEachCall = ( - node: JestFunctionCallExpression, -): node is JestFunctionCallExpressionWithMemberExpressionCallee< - DescribeAlias | TestCaseName, - DescribeProperty.each | TestCaseProperty.each -> & { parent: TSESTree.CallExpression } => - node.parent?.type === AST_NODE_TYPES.CallExpression && - node.callee.type === AST_NODE_TYPES.MemberExpression && - isSupportedAccessor(node.callee.property, DescribeProperty.each); - -/** - * Gets the arguments of the given `JestFunctionCallExpression`. - * - * If the `node` is an `each` call, then the arguments of the actual suite - * are returned, rather then the `each` array argument. - * - * @param {JestFunctionCallExpression} node - * - * @return {Expression[]} - */ -export const getJestFunctionArguments = ( - node: JestFunctionCallExpression, -) => - node.callee.type === AST_NODE_TYPES.MemberExpression && - isSupportedAccessor(node.callee.property, DescribeProperty.each) && - node.parent && - node.parent.type === AST_NODE_TYPES.CallExpression - ? node.parent.arguments - : node.arguments; - const collectReferences = (scope: TSESLint.Scope.Scope) => { const locals = new Set(); const unresolved = new Set(); diff --git a/src/rules/valid-describe.ts b/src/rules/valid-describe.ts index 255f94a12..2c288e3f0 100644 --- a/src/rules/valid-describe.ts +++ b/src/rules/valid-describe.ts @@ -2,13 +2,7 @@ import { AST_NODE_TYPES, TSESTree, } from '@typescript-eslint/experimental-utils'; -import { - createRule, - getJestFunctionArguments, - isDescribeCall, - isEachCall, - isFunction, -} from './utils'; +import { createRule, getNodeName, isDescribeCall, isFunction } from './utils'; const paramsLocation = ( params: TSESTree.Expression[] | TSESTree.Parameter[], @@ -45,28 +39,23 @@ export default createRule({ create(context) { return { CallExpression(node) { - if ( - !isDescribeCall(node) || - node.callee.type === AST_NODE_TYPES.TaggedTemplateExpression - ) { + if (!isDescribeCall(node)) { return; } - const nodeArguments = getJestFunctionArguments(node); - - if (nodeArguments.length < 1) { + if (node.arguments.length < 1) { return context.report({ messageId: 'nameAndCallback', loc: node.loc, }); } - const [, callback] = nodeArguments; + const [, callback] = node.arguments; if (!callback) { context.report({ messageId: 'nameAndCallback', - loc: paramsLocation(nodeArguments), + loc: paramsLocation(node.arguments), }); return; @@ -75,7 +64,7 @@ export default createRule({ if (!isFunction(callback)) { context.report({ messageId: 'secondArgumentMustBeFunction', - loc: paramsLocation(nodeArguments), + loc: paramsLocation(node.arguments), }); return; @@ -88,7 +77,7 @@ export default createRule({ }); } - if (!isEachCall(node) && callback.params.length) { + if (!getNodeName(node).endsWith('each') && callback.params.length) { context.report({ messageId: 'unexpectedDescribeArgument', loc: paramsLocation(callback.params), diff --git a/src/rules/valid-title.ts b/src/rules/valid-title.ts index f92c5e3a1..6902f1262 100644 --- a/src/rules/valid-title.ts +++ b/src/rules/valid-title.ts @@ -7,7 +7,6 @@ import { StringNode, TestCaseName, createRule, - getJestFunctionArguments, getNodeName, getStringValue, isDescribeCall, @@ -165,7 +164,7 @@ export default createRule<[Options], MessageIds>({ return; } - const [argument] = getJestFunctionArguments(node); + const [argument] = node.arguments; if (!argument) { return; @@ -237,7 +236,7 @@ export default createRule<[Options], MessageIds>({ }); } - const nodeName = trimFXprefix(getNodeName(node.callee)); + const nodeName = trimFXprefix(getNodeName(node)); const [firstWord] = title.split(' '); if (firstWord.toLowerCase() === nodeName) {