diff --git a/src/rules/__tests__/valid-describe.test.js b/src/rules/__tests__/valid-describe.test.ts similarity index 97% rename from src/rules/__tests__/valid-describe.test.js rename to src/rules/__tests__/valid-describe.test.ts index 211d1537b..adfbad592 100644 --- a/src/rules/__tests__/valid-describe.test.js +++ b/src/rules/__tests__/valid-describe.test.ts @@ -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, }, diff --git a/src/rules/tsUtils.ts b/src/rules/tsUtils.ts index ca3891053..11eb3ecb8 100644 --- a/src/rules/tsUtils.ts +++ b/src/rules/tsUtils.ts @@ -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'; @@ -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) + ); +}; diff --git a/src/rules/valid-describe.js b/src/rules/valid-describe.js deleted file mode 100644 index 0fa9c7cdd..000000000 --- a/src/rules/valid-describe.js +++ /dev/null @@ -1,103 +0,0 @@ -import { getDocsUrl, isDescribe, isFunction } from './util'; - -const isAsync = node => node.async; - -const isString = node => - (node.type === 'Literal' && typeof node.value === 'string') || - node.type === 'TemplateLiteral'; - -const hasParams = node => node.params.length > 0; - -const paramsLocation = params => { - const [first] = params; - const last = params[params.length - 1]; - return { - start: { - line: first.loc.start.line, - column: first.loc.start.column, - }, - end: { - line: last.loc.end.line, - column: last.loc.end.column, - }, - }; -}; - -export default { - meta: { - docs: { - url: getDocsUrl(__filename), - }, - 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: [], - }, - 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 === undefined) { - context.report({ - messageId: 'nameAndCallback', - loc: paramsLocation(node.arguments), - }); - - return; - } - if (!isFunction(callbackFunction)) { - context.report({ - messageId: 'secondArgumentMustBeFunction', - loc: paramsLocation(node.arguments), - }); - - return; - } - if (isAsync(callbackFunction)) { - context.report({ - messageId: 'noAsyncDescribeCallback', - node: callbackFunction, - }); - } - if (hasParams(callbackFunction)) { - context.report({ - messageId: 'unexpectedDescribeArgument', - loc: paramsLocation(callbackFunction.params), - }); - } - if (callbackFunction.body.type === 'BlockStatement') { - callbackFunction.body.body.forEach(node => { - if (node.type === 'ReturnStatement') { - context.report({ - messageId: 'unexpectedReturnInDescribe', - node, - }); - } - }); - } - } - }, - }; - }, -}; diff --git a/src/rules/valid-describe.ts b/src/rules/valid-describe.ts new file mode 100644 index 000000000..f4eafa3a5 --- /dev/null +++ b/src/rules/valid-describe.ts @@ -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; + } + } + }, + }; + }, +});