From 3d847b2164425a2afb754569dbfff52411c95610 Mon Sep 17 00:00:00 2001 From: Gareth Jones Date: Wed, 17 Feb 2021 12:13:14 +1300 Subject: [PATCH] fix(lowercase-name): support `.each` methods (#746) --- src/rules/__tests__/lowercase-name.test.ts | 25 +++++ src/rules/lowercase-name.ts | 111 ++++++++++++--------- src/rules/utils.ts | 16 +-- src/rules/valid-describe.ts | 4 +- 4 files changed, 98 insertions(+), 58 deletions(-) diff --git a/src/rules/__tests__/lowercase-name.test.ts b/src/rules/__tests__/lowercase-name.test.ts index bb4d45677..6510d6b41 100644 --- a/src/rules/__tests__/lowercase-name.test.ts +++ b/src/rules/__tests__/lowercase-name.test.ts @@ -13,6 +13,7 @@ const ruleTester = new TSESLint.RuleTester({ ruleTester.run('lowercase-name', rule, { valid: [ + 'it.each()', 'randomFunction()', 'foo.bar()', 'it()', @@ -178,6 +179,30 @@ ruleTester.run('lowercase-name', rule, { }, ], }, + { + code: "it.each(['green', 'black'])('Should return %', () => {})", + output: "it.each(['green', 'black'])('should return %', () => {})", + errors: [ + { + messageId: 'unexpectedLowercase', + data: { method: TestCaseName.it }, + column: 9, + line: 1, + }, + ], + }, + { + code: "describe.each(['green', 'black'])('Should return %', () => {})", + output: "describe.each(['green', 'black'])('should return %', () => {})", + errors: [ + { + messageId: 'unexpectedLowercase', + data: { method: DescribeAlias.describe }, + column: 15, + line: 1, + }, + ], + }, ], }); diff --git a/src/rules/lowercase-name.ts b/src/rules/lowercase-name.ts index 4dedcd665..041fc2531 100644 --- a/src/rules/lowercase-name.ts +++ b/src/rules/lowercase-name.ts @@ -6,12 +6,12 @@ import { import { CallExpressionWithSingleArgument, DescribeAlias, - JestFunctionCallExpressionWithIdentifierCallee, StringNode, TestCaseName, createRule, getStringValue, isDescribe, + isEachCall, isStringNode, isTestCase, } from './utils'; @@ -21,38 +21,37 @@ type IgnorableFunctionExpressions = | TestCaseName.test | DescribeAlias.describe; -type CallExpressionWithCorrectCalleeAndArguments = JestFunctionCallExpressionWithIdentifierCallee & - CallExpressionWithSingleArgument; - const hasStringAsFirstArgument = ( node: TSESTree.CallExpression, ): node is CallExpressionWithSingleArgument => node.arguments[0] && isStringNode(node.arguments[0]); -const isJestFunctionWithLiteralArg = ( +const findNodeNameAndArgument = ( node: TSESTree.CallExpression, -): node is CallExpressionWithCorrectCalleeAndArguments => - (isTestCase(node) || isDescribe(node)) && - node.callee.type === AST_NODE_TYPES.Identifier && - hasStringAsFirstArgument(node); - -const jestFunctionName = ( - node: CallExpressionWithCorrectCalleeAndArguments, - allowedPrefixes: readonly string[], -) => { - const description = getStringValue(node.arguments[0]); - - if (allowedPrefixes.some(name => description.startsWith(name))) { +): [name: string, firstArg: StringNode] | null => { + if (!(isTestCase(node) || isDescribe(node))) { return null; } - const firstCharacter = description.charAt(0); + if (isEachCall(node)) { + if ( + node.parent?.type === AST_NODE_TYPES.CallExpression && + hasStringAsFirstArgument(node.parent) + ) { + return [node.callee.object.name, node.parent.arguments[0]]; + } + + return null; + } - if (!firstCharacter || firstCharacter === firstCharacter.toLowerCase()) { + if ( + node.callee.type !== AST_NODE_TYPES.Identifier || + !hasStringAsFirstArgument(node) + ) { return null; } - return node.callee.name; + return [node.callee.name, node.arguments[0]]; }; export default createRule< @@ -117,10 +116,6 @@ export default createRule< return { CallExpression(node: TSESTree.CallExpression) { - if (!isJestFunctionWithLiteralArg(node)) { - return; - } - if (isDescribe(node)) { numberOfDescribeBlocks++; @@ -129,32 +124,50 @@ export default createRule< } } - const erroneousMethod = jestFunctionName(node, allowedPrefixes); - - if (erroneousMethod && !ignore.includes(node.callee.name)) { - context.report({ - messageId: 'unexpectedLowercase', - node: node.arguments[0], - data: { method: erroneousMethod }, - fix(fixer) { - const [firstArg] = node.arguments; - - const description = getStringValue(firstArg); - - const rangeIgnoringQuotes: TSESLint.AST.Range = [ - firstArg.range[0] + 1, - firstArg.range[1] - 1, - ]; - const newDescription = - description.substring(0, 1).toLowerCase() + - description.substring(1); - - return [ - fixer.replaceTextRange(rangeIgnoringQuotes, newDescription), - ]; - }, - }); + const results = findNodeNameAndArgument(node); + + if (!results) { + return; } + + const [name, firstArg] = results; + + const description = getStringValue(firstArg); + + if (allowedPrefixes.some(name => description.startsWith(name))) { + return; + } + + const firstCharacter = description.charAt(0); + + if ( + !firstCharacter || + firstCharacter === firstCharacter.toLowerCase() || + ignore.includes(name as IgnorableFunctionExpressions) + ) { + return; + } + + context.report({ + messageId: 'unexpectedLowercase', + node: node.arguments[0], + data: { method: name }, + fix(fixer) { + const description = getStringValue(firstArg); + + const rangeIgnoringQuotes: TSESLint.AST.Range = [ + firstArg.range[0] + 1, + firstArg.range[1] - 1, + ]; + const newDescription = + description.substring(0, 1).toLowerCase() + + description.substring(1); + + return [ + fixer.replaceTextRange(rangeIgnoringQuotes, newDescription), + ]; + }, + }); }, 'CallExpression:exit'(node: TSESTree.CallExpression) { if (isDescribe(node)) { diff --git a/src/rules/utils.ts b/src/rules/utils.ts index 107675d81..b0326ab03 100644 --- a/src/rules/utils.ts +++ b/src/rules/utils.ts @@ -672,16 +672,18 @@ export const isDescribe = ( DescribeProperty.hasOwnProperty(node.callee.property.name)); /** - * Checks if the given `describe` is a call to `describe.each`. + * Checks if the given node` is a call to `.each(...)`. + * If `true`, the code must look like `.each(...)`. * - * @param {JestFunctionCallExpression} node - * @return {node is JestFunctionCallExpression} + * @param {JestFunctionCallExpression} node + * + * @return {node is JestFunctionCallExpressionWithMemberExpressionCallee} */ -export const isDescribeEach = ( - node: JestFunctionCallExpression, +export const isEachCall = ( + node: JestFunctionCallExpression, ): node is JestFunctionCallExpressionWithMemberExpressionCallee< - DescribeAlias, - DescribeProperty.each + DescribeAlias | TestCaseName, + DescribeProperty.each | TestCaseProperty.each > => node.callee.type === AST_NODE_TYPES.MemberExpression && isSupportedAccessor(node.callee.property, DescribeProperty.each); diff --git a/src/rules/valid-describe.ts b/src/rules/valid-describe.ts index a4e706ee9..84bdf65d3 100644 --- a/src/rules/valid-describe.ts +++ b/src/rules/valid-describe.ts @@ -6,7 +6,7 @@ import { createRule, getJestFunctionArguments, isDescribe, - isDescribeEach, + isEachCall, isFunction, } from './utils'; @@ -85,7 +85,7 @@ export default createRule({ }); } - if (!isDescribeEach(node) && callback.params.length) { + if (!isEachCall(node) && callback.params.length) { context.report({ messageId: 'unexpectedDescribeArgument', loc: paramsLocation(callback.params),