Skip to content

Commit 3d847b2

Browse files
authored
fix(lowercase-name): support .each methods (#746)
1 parent d763f89 commit 3d847b2

File tree

4 files changed

+98
-58
lines changed

4 files changed

+98
-58
lines changed

src/rules/__tests__/lowercase-name.test.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ const ruleTester = new TSESLint.RuleTester({
1313

1414
ruleTester.run('lowercase-name', rule, {
1515
valid: [
16+
'it.each()',
1617
'randomFunction()',
1718
'foo.bar()',
1819
'it()',
@@ -178,6 +179,30 @@ ruleTester.run('lowercase-name', rule, {
178179
},
179180
],
180181
},
182+
{
183+
code: "it.each(['green', 'black'])('Should return %', () => {})",
184+
output: "it.each(['green', 'black'])('should return %', () => {})",
185+
errors: [
186+
{
187+
messageId: 'unexpectedLowercase',
188+
data: { method: TestCaseName.it },
189+
column: 9,
190+
line: 1,
191+
},
192+
],
193+
},
194+
{
195+
code: "describe.each(['green', 'black'])('Should return %', () => {})",
196+
output: "describe.each(['green', 'black'])('should return %', () => {})",
197+
errors: [
198+
{
199+
messageId: 'unexpectedLowercase',
200+
data: { method: DescribeAlias.describe },
201+
column: 15,
202+
line: 1,
203+
},
204+
],
205+
},
181206
],
182207
});
183208

src/rules/lowercase-name.ts

Lines changed: 62 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@ import {
66
import {
77
CallExpressionWithSingleArgument,
88
DescribeAlias,
9-
JestFunctionCallExpressionWithIdentifierCallee,
109
StringNode,
1110
TestCaseName,
1211
createRule,
1312
getStringValue,
1413
isDescribe,
14+
isEachCall,
1515
isStringNode,
1616
isTestCase,
1717
} from './utils';
@@ -21,38 +21,37 @@ type IgnorableFunctionExpressions =
2121
| TestCaseName.test
2222
| DescribeAlias.describe;
2323

24-
type CallExpressionWithCorrectCalleeAndArguments = JestFunctionCallExpressionWithIdentifierCallee<IgnorableFunctionExpressions> &
25-
CallExpressionWithSingleArgument<StringNode>;
26-
2724
const hasStringAsFirstArgument = (
2825
node: TSESTree.CallExpression,
2926
): node is CallExpressionWithSingleArgument<StringNode> =>
3027
node.arguments[0] && isStringNode(node.arguments[0]);
3128

32-
const isJestFunctionWithLiteralArg = (
29+
const findNodeNameAndArgument = (
3330
node: TSESTree.CallExpression,
34-
): node is CallExpressionWithCorrectCalleeAndArguments =>
35-
(isTestCase(node) || isDescribe(node)) &&
36-
node.callee.type === AST_NODE_TYPES.Identifier &&
37-
hasStringAsFirstArgument(node);
38-
39-
const jestFunctionName = (
40-
node: CallExpressionWithCorrectCalleeAndArguments,
41-
allowedPrefixes: readonly string[],
42-
) => {
43-
const description = getStringValue(node.arguments[0]);
44-
45-
if (allowedPrefixes.some(name => description.startsWith(name))) {
31+
): [name: string, firstArg: StringNode] | null => {
32+
if (!(isTestCase(node) || isDescribe(node))) {
4633
return null;
4734
}
4835

49-
const firstCharacter = description.charAt(0);
36+
if (isEachCall(node)) {
37+
if (
38+
node.parent?.type === AST_NODE_TYPES.CallExpression &&
39+
hasStringAsFirstArgument(node.parent)
40+
) {
41+
return [node.callee.object.name, node.parent.arguments[0]];
42+
}
43+
44+
return null;
45+
}
5046

51-
if (!firstCharacter || firstCharacter === firstCharacter.toLowerCase()) {
47+
if (
48+
node.callee.type !== AST_NODE_TYPES.Identifier ||
49+
!hasStringAsFirstArgument(node)
50+
) {
5251
return null;
5352
}
5453

55-
return node.callee.name;
54+
return [node.callee.name, node.arguments[0]];
5655
};
5756

5857
export default createRule<
@@ -117,10 +116,6 @@ export default createRule<
117116

118117
return {
119118
CallExpression(node: TSESTree.CallExpression) {
120-
if (!isJestFunctionWithLiteralArg(node)) {
121-
return;
122-
}
123-
124119
if (isDescribe(node)) {
125120
numberOfDescribeBlocks++;
126121

@@ -129,32 +124,50 @@ export default createRule<
129124
}
130125
}
131126

132-
const erroneousMethod = jestFunctionName(node, allowedPrefixes);
133-
134-
if (erroneousMethod && !ignore.includes(node.callee.name)) {
135-
context.report({
136-
messageId: 'unexpectedLowercase',
137-
node: node.arguments[0],
138-
data: { method: erroneousMethod },
139-
fix(fixer) {
140-
const [firstArg] = node.arguments;
141-
142-
const description = getStringValue(firstArg);
143-
144-
const rangeIgnoringQuotes: TSESLint.AST.Range = [
145-
firstArg.range[0] + 1,
146-
firstArg.range[1] - 1,
147-
];
148-
const newDescription =
149-
description.substring(0, 1).toLowerCase() +
150-
description.substring(1);
151-
152-
return [
153-
fixer.replaceTextRange(rangeIgnoringQuotes, newDescription),
154-
];
155-
},
156-
});
127+
const results = findNodeNameAndArgument(node);
128+
129+
if (!results) {
130+
return;
157131
}
132+
133+
const [name, firstArg] = results;
134+
135+
const description = getStringValue(firstArg);
136+
137+
if (allowedPrefixes.some(name => description.startsWith(name))) {
138+
return;
139+
}
140+
141+
const firstCharacter = description.charAt(0);
142+
143+
if (
144+
!firstCharacter ||
145+
firstCharacter === firstCharacter.toLowerCase() ||
146+
ignore.includes(name as IgnorableFunctionExpressions)
147+
) {
148+
return;
149+
}
150+
151+
context.report({
152+
messageId: 'unexpectedLowercase',
153+
node: node.arguments[0],
154+
data: { method: name },
155+
fix(fixer) {
156+
const description = getStringValue(firstArg);
157+
158+
const rangeIgnoringQuotes: TSESLint.AST.Range = [
159+
firstArg.range[0] + 1,
160+
firstArg.range[1] - 1,
161+
];
162+
const newDescription =
163+
description.substring(0, 1).toLowerCase() +
164+
description.substring(1);
165+
166+
return [
167+
fixer.replaceTextRange(rangeIgnoringQuotes, newDescription),
168+
];
169+
},
170+
});
158171
},
159172
'CallExpression:exit'(node: TSESTree.CallExpression) {
160173
if (isDescribe(node)) {

src/rules/utils.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -672,16 +672,18 @@ export const isDescribe = (
672672
DescribeProperty.hasOwnProperty(node.callee.property.name));
673673

674674
/**
675-
* Checks if the given `describe` is a call to `describe.each`.
675+
* Checks if the given node` is a call to `<describe|test|it>.each(...)`.
676+
* If `true`, the code must look like `<method>.each(...)`.
676677
*
677-
* @param {JestFunctionCallExpression<DescribeAlias>} node
678-
* @return {node is JestFunctionCallExpression<DescribeAlias, DescribeProperty.each>}
678+
* @param {JestFunctionCallExpression<DescribeAlias | TestCaseName>} node
679+
*
680+
* @return {node is JestFunctionCallExpressionWithMemberExpressionCallee<DescribeAlias | TestCaseName, DescribeProperty.each | TestCaseProperty.each>}
679681
*/
680-
export const isDescribeEach = (
681-
node: JestFunctionCallExpression<DescribeAlias>,
682+
export const isEachCall = (
683+
node: JestFunctionCallExpression<DescribeAlias | TestCaseName>,
682684
): node is JestFunctionCallExpressionWithMemberExpressionCallee<
683-
DescribeAlias,
684-
DescribeProperty.each
685+
DescribeAlias | TestCaseName,
686+
DescribeProperty.each | TestCaseProperty.each
685687
> =>
686688
node.callee.type === AST_NODE_TYPES.MemberExpression &&
687689
isSupportedAccessor(node.callee.property, DescribeProperty.each);

src/rules/valid-describe.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {
66
createRule,
77
getJestFunctionArguments,
88
isDescribe,
9-
isDescribeEach,
9+
isEachCall,
1010
isFunction,
1111
} from './utils';
1212

@@ -85,7 +85,7 @@ export default createRule({
8585
});
8686
}
8787

88-
if (!isDescribeEach(node) && callback.params.length) {
88+
if (!isEachCall(node) && callback.params.length) {
8989
context.report({
9090
messageId: 'unexpectedDescribeArgument',
9191
loc: paramsLocation(callback.params),

0 commit comments

Comments
 (0)