Skip to content

Commit 6cbaa0f

Browse files
G-RathSimenB
authored andcommitted
chore(prefer-todo): migrate to TS (#335)
1 parent ed2a0f6 commit 6cbaa0f

File tree

5 files changed

+150
-107
lines changed

5 files changed

+150
-107
lines changed

src/rules/__tests__/prefer-todo.test.js renamed to src/rules/__tests__/prefer-todo.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import { RuleTester } from 'eslint';
1+
import { TSESLint } from '@typescript-eslint/experimental-utils';
22
import rule from '../prefer-todo';
33

4-
const ruleTester = new RuleTester({
4+
const ruleTester = new TSESLint.RuleTester({
55
parserOptions: { ecmaVersion: 2015 },
66
});
77

src/rules/prefer-todo.js

Lines changed: 0 additions & 80 deletions
This file was deleted.

src/rules/prefer-todo.ts

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import {
2+
AST_NODE_TYPES,
3+
TSESLint,
4+
TSESTree,
5+
} from '@typescript-eslint/experimental-utils';
6+
import {
7+
FunctionExpression,
8+
JestFunctionCallExpression,
9+
StringLiteral,
10+
TestCaseName,
11+
createRule,
12+
getNodeName,
13+
isFunction,
14+
isStringNode,
15+
isTestCase,
16+
} from './tsUtils';
17+
18+
function isOnlyTestTitle(node: TSESTree.CallExpression) {
19+
return node.arguments.length === 1;
20+
}
21+
22+
function isFunctionBodyEmpty(node: FunctionExpression) {
23+
/* istanbul ignore next https://github.com/typescript-eslint/typescript-eslint/issues/734 */
24+
if (!node.body) {
25+
throw new Error(
26+
`Unexpected null while performing prefer-todo - please file a github issue at https://github.com/jest-community/eslint-plugin-jest`,
27+
);
28+
}
29+
30+
return (
31+
node.body.type === AST_NODE_TYPES.BlockStatement &&
32+
node.body.body &&
33+
!node.body.body.length
34+
);
35+
}
36+
37+
function isTestBodyEmpty(node: TSESTree.CallExpression) {
38+
const [, fn] = node.arguments;
39+
return fn && isFunction(fn) && isFunctionBodyEmpty(fn);
40+
}
41+
42+
function addTodo(
43+
node: JestFunctionCallExpression<TestCaseName>,
44+
fixer: TSESLint.RuleFixer,
45+
) {
46+
const testName = getNodeName(node.callee)
47+
.split('.')
48+
.shift();
49+
return fixer.replaceText(node.callee, `${testName}.todo`);
50+
}
51+
52+
interface CallExpressionWithStringArgument extends TSESTree.CallExpression {
53+
arguments: [StringLiteral | TSESTree.TemplateLiteral];
54+
}
55+
56+
function isFirstArgString(
57+
node: TSESTree.CallExpression,
58+
): node is CallExpressionWithStringArgument {
59+
return node.arguments[0] && isStringNode(node.arguments[0]);
60+
}
61+
62+
const isTargetedTestCase = (
63+
node: TSESTree.CallExpression,
64+
): node is JestFunctionCallExpression<TestCaseName> =>
65+
isTestCase(node) &&
66+
(['it', 'test', 'it.skip', 'test.skip'] as Array<string | null>).includes(
67+
getNodeName(node.callee),
68+
);
69+
70+
export default createRule({
71+
name: __filename,
72+
meta: {
73+
docs: {
74+
category: 'Best Practices',
75+
description: 'Suggest using `test.todo`',
76+
recommended: false,
77+
},
78+
messages: {
79+
todoOverEmpty: 'Prefer todo test case over empty test case',
80+
todoOverUnimplemented:
81+
'Prefer todo test case over unimplemented test case',
82+
},
83+
fixable: 'code',
84+
schema: [],
85+
type: 'layout',
86+
},
87+
defaultOptions: [],
88+
create(context) {
89+
return {
90+
CallExpression(node) {
91+
if (!isTargetedTestCase(node) || !isFirstArgString(node)) {
92+
return;
93+
}
94+
95+
if (isTestBodyEmpty(node)) {
96+
context.report({
97+
messageId: 'todoOverEmpty',
98+
node,
99+
fix: fixer => [
100+
fixer.removeRange([
101+
node.arguments[0].range[1],
102+
node.arguments[1].range[1],
103+
]),
104+
addTodo(node, fixer),
105+
],
106+
});
107+
}
108+
109+
if (isOnlyTestTitle(node)) {
110+
context.report({
111+
messageId: 'todoOverUnimplemented',
112+
node,
113+
fix: fixer => [addTodo(node, fixer)],
114+
});
115+
}
116+
},
117+
};
118+
},
119+
});

src/rules/tsUtils.ts

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,13 @@ export type JestFunctionCallExpression<
127127
| JestFunctionCallExpressionWithMemberExpressionCallee<FunctionName>
128128
| JestFunctionCallExpressionWithIdentifierCallee<FunctionName>;
129129

130-
export const getNodeName = (node: TSESTree.Node): string | null => {
130+
export function getNodeName(
131+
node:
132+
| JestFunctionMemberExpression<JestFunctionName>
133+
| JestFunctionIdentifier<JestFunctionName>,
134+
): string;
135+
export function getNodeName(node: TSESTree.Node): string | null;
136+
export function getNodeName(node: TSESTree.Node): string | null {
131137
function joinNames(a?: string | null, b?: string | null): string | null {
132138
return a && b ? `${a}.${b}` : null;
133139
}
@@ -145,7 +151,7 @@ export const getNodeName = (node: TSESTree.Node): string | null => {
145151
}
146152

147153
return null;
148-
};
154+
}
149155

150156
export type FunctionExpression =
151157
| TSESTree.ArrowFunctionExpression
@@ -192,6 +198,27 @@ export const isLiteralNode = (node: {
192198
type: AST_NODE_TYPES;
193199
}): node is TSESTree.Literal => node.type === AST_NODE_TYPES.Literal;
194200

201+
export interface StringLiteral extends TSESTree.Literal {
202+
value: string;
203+
}
204+
205+
export type StringNode = StringLiteral | TSESTree.TemplateLiteral;
206+
207+
export const isStringLiteral = (node: TSESTree.Node): node is StringLiteral =>
208+
node.type === AST_NODE_TYPES.Literal && typeof node.value === 'string';
209+
210+
export const isTemplateLiteral = (
211+
node: TSESTree.Node,
212+
): node is TSESTree.TemplateLiteral =>
213+
node && node.type === AST_NODE_TYPES.TemplateLiteral;
214+
215+
export const isStringNode = (node: TSESTree.Node): node is StringNode =>
216+
isStringLiteral(node) || isTemplateLiteral(node);
217+
218+
/* istanbul ignore next we'll need this later */
219+
export const getStringValue = (arg: StringNode): string =>
220+
isTemplateLiteral(arg) ? arg.quasis[0].value.raw : arg.value;
221+
195222
const collectReferences = (scope: TSESLint.Scope.Scope) => {
196223
const locals = new Set();
197224
const unresolved = new Set();

src/rules/util.js

Lines changed: 0 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -101,21 +101,6 @@ const describeAliases = new Set(['describe', 'fdescribe', 'xdescribe']);
101101

102102
const testCaseNames = new Set(['fit', 'it', 'test', 'xit', 'xtest']);
103103

104-
export const getNodeName = node => {
105-
function joinNames(a, b) {
106-
return a && b ? `${a}.${b}` : null;
107-
}
108-
109-
switch (node && node.type) {
110-
case 'Identifier':
111-
return node.name;
112-
case 'MemberExpression':
113-
return joinNames(getNodeName(node.object), getNodeName(node.property));
114-
}
115-
116-
return null;
117-
};
118-
119104
export const isTestCase = node =>
120105
node &&
121106
node.type === 'CallExpression' &&
@@ -165,11 +150,3 @@ export const getDocsUrl = filename => {
165150

166151
return `${REPO_URL}/blob/v${version}/docs/rules/${ruleName}.md`;
167152
};
168-
169-
export function composeFixers(node) {
170-
return (...fixers) => {
171-
return fixerApi => {
172-
return fixers.reduce((all, fixer) => [...all, fixer(node, fixerApi)], []);
173-
};
174-
};
175-
}

0 commit comments

Comments
 (0)