Skip to content

Commit

Permalink
chore(prefer-todo): migrate to TS (#335)
Browse files Browse the repository at this point in the history
  • Loading branch information
G-Rath authored and SimenB committed Jul 22, 2019
1 parent ed2a0f6 commit 6cbaa0f
Show file tree
Hide file tree
Showing 5 changed files with 150 additions and 107 deletions.
@@ -1,7 +1,7 @@
import { RuleTester } from 'eslint';
import { TSESLint } from '@typescript-eslint/experimental-utils';
import rule from '../prefer-todo';

const ruleTester = new RuleTester({
const ruleTester = new TSESLint.RuleTester({
parserOptions: { ecmaVersion: 2015 },
});

Expand Down
80 changes: 0 additions & 80 deletions src/rules/prefer-todo.js

This file was deleted.

119 changes: 119 additions & 0 deletions src/rules/prefer-todo.ts
@@ -0,0 +1,119 @@
import {
AST_NODE_TYPES,
TSESLint,
TSESTree,
} from '@typescript-eslint/experimental-utils';
import {
FunctionExpression,
JestFunctionCallExpression,
StringLiteral,
TestCaseName,
createRule,
getNodeName,
isFunction,
isStringNode,
isTestCase,
} from './tsUtils';

function isOnlyTestTitle(node: TSESTree.CallExpression) {
return node.arguments.length === 1;
}

function isFunctionBodyEmpty(node: FunctionExpression) {
/* istanbul ignore next https://github.com/typescript-eslint/typescript-eslint/issues/734 */
if (!node.body) {
throw new Error(
`Unexpected null while performing prefer-todo - please file a github issue at https://github.com/jest-community/eslint-plugin-jest`,
);
}

return (
node.body.type === AST_NODE_TYPES.BlockStatement &&
node.body.body &&
!node.body.body.length
);
}

function isTestBodyEmpty(node: TSESTree.CallExpression) {
const [, fn] = node.arguments;
return fn && isFunction(fn) && isFunctionBodyEmpty(fn);
}

function addTodo(
node: JestFunctionCallExpression<TestCaseName>,
fixer: TSESLint.RuleFixer,
) {
const testName = getNodeName(node.callee)
.split('.')
.shift();
return fixer.replaceText(node.callee, `${testName}.todo`);
}

interface CallExpressionWithStringArgument extends TSESTree.CallExpression {
arguments: [StringLiteral | TSESTree.TemplateLiteral];
}

function isFirstArgString(
node: TSESTree.CallExpression,
): node is CallExpressionWithStringArgument {
return node.arguments[0] && isStringNode(node.arguments[0]);
}

const isTargetedTestCase = (
node: TSESTree.CallExpression,
): node is JestFunctionCallExpression<TestCaseName> =>
isTestCase(node) &&
(['it', 'test', 'it.skip', 'test.skip'] as Array<string | null>).includes(
getNodeName(node.callee),
);

export default createRule({
name: __filename,
meta: {
docs: {
category: 'Best Practices',
description: 'Suggest using `test.todo`',
recommended: false,
},
messages: {
todoOverEmpty: 'Prefer todo test case over empty test case',
todoOverUnimplemented:
'Prefer todo test case over unimplemented test case',
},
fixable: 'code',
schema: [],
type: 'layout',
},
defaultOptions: [],
create(context) {
return {
CallExpression(node) {
if (!isTargetedTestCase(node) || !isFirstArgString(node)) {
return;
}

if (isTestBodyEmpty(node)) {
context.report({
messageId: 'todoOverEmpty',
node,
fix: fixer => [
fixer.removeRange([
node.arguments[0].range[1],
node.arguments[1].range[1],
]),
addTodo(node, fixer),
],
});
}

if (isOnlyTestTitle(node)) {
context.report({
messageId: 'todoOverUnimplemented',
node,
fix: fixer => [addTodo(node, fixer)],
});
}
},
};
},
});
31 changes: 29 additions & 2 deletions src/rules/tsUtils.ts
Expand Up @@ -127,7 +127,13 @@ export type JestFunctionCallExpression<
| JestFunctionCallExpressionWithMemberExpressionCallee<FunctionName>
| JestFunctionCallExpressionWithIdentifierCallee<FunctionName>;

export const getNodeName = (node: TSESTree.Node): string | null => {
export function getNodeName(
node:
| JestFunctionMemberExpression<JestFunctionName>
| JestFunctionIdentifier<JestFunctionName>,
): string;
export function getNodeName(node: TSESTree.Node): string | null;
export function getNodeName(node: TSESTree.Node): string | null {
function joinNames(a?: string | null, b?: string | null): string | null {
return a && b ? `${a}.${b}` : null;
}
Expand All @@ -145,7 +151,7 @@ export const getNodeName = (node: TSESTree.Node): string | null => {
}

return null;
};
}

export type FunctionExpression =
| TSESTree.ArrowFunctionExpression
Expand Down Expand Up @@ -192,6 +198,27 @@ export const isLiteralNode = (node: {
type: AST_NODE_TYPES;
}): node is TSESTree.Literal => node.type === AST_NODE_TYPES.Literal;

export interface StringLiteral extends TSESTree.Literal {
value: string;
}

export type StringNode = StringLiteral | TSESTree.TemplateLiteral;

export const isStringLiteral = (node: TSESTree.Node): node is StringLiteral =>
node.type === AST_NODE_TYPES.Literal && typeof node.value === 'string';

export const isTemplateLiteral = (
node: TSESTree.Node,
): node is TSESTree.TemplateLiteral =>
node && node.type === AST_NODE_TYPES.TemplateLiteral;

export const isStringNode = (node: TSESTree.Node): node is StringNode =>
isStringLiteral(node) || isTemplateLiteral(node);

/* istanbul ignore next we'll need this later */
export const getStringValue = (arg: StringNode): string =>
isTemplateLiteral(arg) ? arg.quasis[0].value.raw : arg.value;

const collectReferences = (scope: TSESLint.Scope.Scope) => {
const locals = new Set();
const unresolved = new Set();
Expand Down
23 changes: 0 additions & 23 deletions src/rules/util.js
Expand Up @@ -101,21 +101,6 @@ const describeAliases = new Set(['describe', 'fdescribe', 'xdescribe']);

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

export const getNodeName = node => {
function joinNames(a, b) {
return a && b ? `${a}.${b}` : null;
}

switch (node && node.type) {
case 'Identifier':
return node.name;
case 'MemberExpression':
return joinNames(getNodeName(node.object), getNodeName(node.property));
}

return null;
};

export const isTestCase = node =>
node &&
node.type === 'CallExpression' &&
Expand Down Expand Up @@ -165,11 +150,3 @@ export const getDocsUrl = filename => {

return `${REPO_URL}/blob/v${version}/docs/rules/${ruleName}.md`;
};

export function composeFixers(node) {
return (...fixers) => {
return fixerApi => {
return fixers.reduce((all, fixer) => [...all, fixer(node, fixerApi)], []);
};
};
}

0 comments on commit 6cbaa0f

Please sign in to comment.