Skip to content

Commit

Permalink
fix(lowercase-name): support .each methods (#746)
Browse files Browse the repository at this point in the history
  • Loading branch information
G-Rath committed Feb 16, 2021
1 parent d763f89 commit 3d847b2
Show file tree
Hide file tree
Showing 4 changed files with 98 additions and 58 deletions.
25 changes: 25 additions & 0 deletions src/rules/__tests__/lowercase-name.test.ts
Expand Up @@ -13,6 +13,7 @@ const ruleTester = new TSESLint.RuleTester({

ruleTester.run('lowercase-name', rule, {
valid: [
'it.each()',
'randomFunction()',
'foo.bar()',
'it()',
Expand Down Expand Up @@ -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,
},
],
},
],
});

Expand Down
111 changes: 62 additions & 49 deletions src/rules/lowercase-name.ts
Expand Up @@ -6,12 +6,12 @@ import {
import {
CallExpressionWithSingleArgument,
DescribeAlias,
JestFunctionCallExpressionWithIdentifierCallee,
StringNode,
TestCaseName,
createRule,
getStringValue,
isDescribe,
isEachCall,
isStringNode,
isTestCase,
} from './utils';
Expand All @@ -21,38 +21,37 @@ type IgnorableFunctionExpressions =
| TestCaseName.test
| DescribeAlias.describe;

type CallExpressionWithCorrectCalleeAndArguments = JestFunctionCallExpressionWithIdentifierCallee<IgnorableFunctionExpressions> &
CallExpressionWithSingleArgument<StringNode>;

const hasStringAsFirstArgument = (
node: TSESTree.CallExpression,
): node is CallExpressionWithSingleArgument<StringNode> =>
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<
Expand Down Expand Up @@ -117,10 +116,6 @@ export default createRule<

return {
CallExpression(node: TSESTree.CallExpression) {
if (!isJestFunctionWithLiteralArg(node)) {
return;
}

if (isDescribe(node)) {
numberOfDescribeBlocks++;

Expand All @@ -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)) {
Expand Down
16 changes: 9 additions & 7 deletions src/rules/utils.ts
Expand Up @@ -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 `<describe|test|it>.each(...)`.
* If `true`, the code must look like `<method>.each(...)`.
*
* @param {JestFunctionCallExpression<DescribeAlias>} node
* @return {node is JestFunctionCallExpression<DescribeAlias, DescribeProperty.each>}
* @param {JestFunctionCallExpression<DescribeAlias | TestCaseName>} node
*
* @return {node is JestFunctionCallExpressionWithMemberExpressionCallee<DescribeAlias | TestCaseName, DescribeProperty.each | TestCaseProperty.each>}
*/
export const isDescribeEach = (
node: JestFunctionCallExpression<DescribeAlias>,
export const isEachCall = (
node: JestFunctionCallExpression<DescribeAlias | TestCaseName>,
): 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);
Expand Down
4 changes: 2 additions & 2 deletions src/rules/valid-describe.ts
Expand Up @@ -6,7 +6,7 @@ import {
createRule,
getJestFunctionArguments,
isDescribe,
isDescribeEach,
isEachCall,
isFunction,
} from './utils';

Expand Down Expand Up @@ -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),
Expand Down

0 comments on commit 3d847b2

Please sign in to comment.