Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { RuleTester } from 'eslint';
import { TSESLint } from '@typescript-eslint/experimental-utils';
import rule from '../valid-describe';

const ruleTester = new RuleTester({
const ruleTester = new TSESLint.RuleTester({
parserOptions: {
ecmaVersion: 8,
},
Expand Down
30 changes: 29 additions & 1 deletion src/rules/tsUtils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
// TODO: rename to utils.ts when TS migration is complete
import { basename } from 'path';
import { ESLintUtils } from '@typescript-eslint/experimental-utils';
import {
AST_NODE_TYPES,
ESLintUtils,
TSESTree,
} from '@typescript-eslint/experimental-utils';
import { version } from '../../package.json';

const REPO_URL = 'https://github.com/jest-community/eslint-plugin-jest';
Expand All @@ -9,3 +13,27 @@ export const createRule = ESLintUtils.RuleCreator(name => {
const ruleName = basename(name, '.ts');
return `${REPO_URL}/blob/v${version}/docs/rules/${ruleName}.md`;
});

enum DescribeAlias {
'describe',
'fdescribe',
'xdescribe',
}

export type FunctionExpression =
| TSESTree.ArrowFunctionExpression
| TSESTree.FunctionExpression;

export const isFunction = (node: TSESTree.Node): node is FunctionExpression =>
node.type === AST_NODE_TYPES.FunctionExpression ||
node.type === AST_NODE_TYPES.ArrowFunctionExpression;

export const isDescribe = (node: TSESTree.CallExpression): boolean => {
return (
(node.callee.type === AST_NODE_TYPES.Identifier &&
node.callee.name in DescribeAlias) ||
(node.callee.type === AST_NODE_TYPES.MemberExpression &&
node.callee.object.type === AST_NODE_TYPES.Identifier &&
node.callee.object.name in DescribeAlias)
);
};
103 changes: 0 additions & 103 deletions src/rules/valid-describe.js

This file was deleted.

114 changes: 114 additions & 0 deletions src/rules/valid-describe.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { AST_NODE_TYPES, TSESTree } from '@typescript-eslint/typescript-estree';
import {
FunctionExpression,
createRule,
isDescribe,
isFunction,
} from './tsUtils';

const isAsync = (node: FunctionExpression): boolean => node.async;

const isString = (node: TSESTree.Node): boolean =>
(node.type === AST_NODE_TYPES.Literal && typeof node.value === 'string') ||
node.type === AST_NODE_TYPES.TemplateLiteral;

const hasParams = (node: FunctionExpression): boolean => node.params.length > 0;

const paramsLocation = (
params: TSESTree.Expression[] | TSESTree.Parameter[],
) => {
const [first] = params;
const last = params[params.length - 1];
return {
start: first.loc.start,
end: last.loc.end,
}
};

export default createRule({
name: __filename,
meta: {
type: 'problem',
docs: {
description:
'Using an improper `describe()` callback function can lead to unexpected test errors.',
category: 'Possible Errors',
recommended: 'warn',
},
messages: {
nameAndCallback: 'Describe requires name and callback arguments',
firstArgumentMustBeName: 'First argument must be name',
secondArgumentMustBeFunction: 'Second argument must be function',
noAsyncDescribeCallback: 'No async describe callback',
unexpectedDescribeArgument: 'Unexpected argument(s) in describe callback',
unexpectedReturnInDescribe:
'Unexpected return statement in describe callback',
},
schema: [],
} as const,
defaultOptions: [],
create(context) {
return {
CallExpression(node) {
if (isDescribe(node)) {
if (node.arguments.length === 0) {
return context.report({
messageId: 'nameAndCallback',
loc: node.loc,
});
}

const [name] = node.arguments;
const [, callbackFunction] = node.arguments;
if (!isString(name)) {
context.report({
messageId: 'firstArgumentMustBeName',
loc: paramsLocation(node.arguments),
});
}
if (!callbackFunction) {
context.report({
messageId: 'nameAndCallback',
loc: paramsLocation(node.arguments),
});

return;
}
if (isFunction(callbackFunction)) {
if (isAsync(callbackFunction)) {
context.report({
messageId: 'noAsyncDescribeCallback',
node: callbackFunction,
});
}
if (hasParams(callbackFunction)) {
context.report({
messageId: 'unexpectedDescribeArgument',
loc: paramsLocation(callbackFunction.params),
});
}
if (
callbackFunction.body &&
callbackFunction.body.type === AST_NODE_TYPES.BlockStatement
) {
callbackFunction.body.body.forEach(node => {
if (node.type === 'ReturnStatement') {
context.report({
messageId: 'unexpectedReturnInDescribe',
node,
});
}
});
}
} else {
context.report({
messageId: 'secondArgumentMustBeFunction',
loc: paramsLocation(node.arguments),
});
return;
}
}
},
};
},
});