diff --git a/docs/rules/README.md b/docs/rules/README.md index 4b71ab1..2bfd26d 100644 --- a/docs/rules/README.md +++ b/docs/rules/README.md @@ -19,3 +19,4 @@ * [prefer-arrow-callback](prefer-arrow-callback.md) - prefer arrow function callbacks (mocha-aware) * [valid-suite-description](valid-suite-description.md) - match suite descriptions against a pre-configured regular expression * [valid-test-description](valid-test-description.md) - match test descriptions against a pre-configured regular expression +* [no-async-describe](no-async-describe.md) - disallow async functions passed to describe diff --git a/docs/rules/no-async-describe.md b/docs/rules/no-async-describe.md new file mode 100644 index 0000000..3a9994f --- /dev/null +++ b/docs/rules/no-async-describe.md @@ -0,0 +1,50 @@ +# Disallow async functions passed to describe (no-async-describe) + +This rule disallows the use of an async function with `describe`. It usually indicates a copy/paste error or that you're trying to use `describe` for setup code, which should happen in `before` or `beforeEach`. Also, it can lead to [the contained `it` blocks not being picked up](https://github.com/mochajs/mocha/issues/2975). + +Example: + +```js +describe('the thing', async function () { + // This work should happen in a beforeEach: + const theThing = await getTheThing(); + + it('should foo', function () { + // ... + }); +}); +``` + +## Rule Details + +The rule supports "describe", "context" and "suite" suite function names and different valid suite name prefixes like "skip" or "only". + +The following patterns are considered problems, whether or not the function uses `await`: + +```js +describe('something', async function () { + it('should work', function () {}); +}); + +describe('something', async () => { + it('should work', function () {}); +}); +``` + +If the `describe` function does not contain `await`, a fix of removing `async` will be suggested. + +The rule won't be able to detect the (hopefully uncommon) cases where the async +function is defined before the `describe` call and passed by reference: + +```js +async function mySuite() { + it('my test', () => {}); +} + +describe('my suite', mySuite); +``` + +## When Not To Use It + +- If you use another library which exposes a similar API as mocha (e.g. `describe.only`), you should turn this rule off because it would raise warnings. +- In environments that have not yet adopted ES6 language features (ES3/5). diff --git a/index.js b/index.js index 111aa09..76cb7c1 100644 --- a/index.js +++ b/index.js @@ -20,7 +20,8 @@ module.exports = { 'max-top-level-suites': require('./lib/rules/max-top-level-suites'), 'no-nested-tests': require('./lib/rules/no-nested-tests'), 'no-setup-in-describe': require('./lib/rules/no-setup-in-describe'), - 'prefer-arrow-callback': require('./lib/rules/prefer-arrow-callback') + 'prefer-arrow-callback': require('./lib/rules/prefer-arrow-callback'), + 'no-async-describe': require('./lib/rules/no-async-describe') }, configs: { recommended: { diff --git a/lib/rules/no-async-describe.js b/lib/rules/no-async-describe.js new file mode 100644 index 0000000..9988d34 --- /dev/null +++ b/lib/rules/no-async-describe.js @@ -0,0 +1,70 @@ +'use strict'; + +/* eslint "complexity": [ "error", 5 ] */ + +/** + * @fileoverview Disallow async functions as arguments to describe + */ + +const astUtils = require('../util/ast'); +const { additionalSuiteNames } = require('../util/settings'); + +module.exports = function (context) { + const sourceCode = context.getSourceCode(); + + function isFunction(node) { + return ( + node.type === 'FunctionExpression' || + node.type === 'FunctionDeclaration' || + node.type === 'ArrowFunctionExpression' + ); + } + + function containsDirectAwait(node) { + if (node.type === 'AwaitExpression') { + return true; + } else if (node.type && !isFunction(node)) { + return Object.keys(node).some(function (key) { + if (Array.isArray(node[key])) { + return node[key].some(containsDirectAwait); + } else if (key !== 'parent' && node[key] && typeof node[key] === 'object') { + return containsDirectAwait(node[key]); + } + return false; + }); + } + return false; + } + + function fixAsyncFunction(fixer, fn) { + if (!containsDirectAwait(fn.body)) { + // Remove the "async" token and all the whitespace before "function": + const [ asyncToken, functionToken ] = sourceCode.getFirstTokens(fn, 2); + return fixer.removeRange([ asyncToken.range[0], functionToken.range[0] ]); + } + return undefined; + } + + function isAsyncFunction(node) { + return node && (node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression') && node.async; + } + + return { + CallExpression(node) { + const name = astUtils.getNodeName(node.callee); + + if (astUtils.isDescribe(node, additionalSuiteNames(context.settings))) { + const fnArg = node.arguments.slice(-1)[0]; + if (isAsyncFunction(fnArg)) { + context.report({ + node: fnArg, + message: `Unexpected async function in ${name}()`, + fix(fixer) { + return fixAsyncFunction(fixer, fnArg); + } + }); + } + } + } + }; +}; diff --git a/test/rules/no-async-describe.js b/test/rules/no-async-describe.js new file mode 100644 index 0000000..d8ef7ab --- /dev/null +++ b/test/rules/no-async-describe.js @@ -0,0 +1,77 @@ +'use strict'; + +const RuleTester = require('eslint').RuleTester; +const rule = require('../../lib/rules/no-async-describe'); +const ruleTester = new RuleTester(); + +ruleTester.run('no-async-describe', rule, { + valid: [ + 'describe()', + 'describe("hello")', + 'describe(function () {})', + 'describe("hello", function () {})', + { code: '() => { a.b }', parserOptions: { ecmaVersion: 6 } }, + { code: 'describe("hello", () => { a.b })', parserOptions: { ecmaVersion: 6 } }, + 'it()', + { code: 'it("hello", async function () {})', parserOptions: { ecmaVersion: 8 } }, + { code: 'it("hello", async () => {})', parserOptions: { ecmaVersion: 8 } } + ], + + invalid: [ + { + code: 'describe("hello", async function () {})', + output: 'describe("hello", function () {})', + parserOptions: { ecmaVersion: 8 }, errors: [ { + message: 'Unexpected async function in describe()', + line: 1, + column: 19 + } ] + }, + { + code: 'foo("hello", async function () {})', + output: 'foo("hello", function () {})', + settings: { + mocha: { + additionalSuiteNames: [ 'foo' ] + } + }, + parserOptions: { ecmaVersion: 8 }, errors: [ { + message: 'Unexpected async function in foo()', + line: 1, + column: 14 + } ] + }, + { + code: 'describe("hello", async () => {})', + output: 'describe("hello", () => {})', + parserOptions: { ecmaVersion: 8 }, + errors: [ { + message: 'Unexpected async function in describe()', + line: 1, + column: 19 + } ] + }, + { + code: 'describe("hello", async () => {await foo;})', + // Do not offer a fix for an async function that contains await + output: null, + parserOptions: { ecmaVersion: 8 }, + errors: [ { + message: 'Unexpected async function in describe()', + line: 1, + column: 19 + } ] + }, + { + code: 'describe("hello", async () => {async function bar() {await foo;}})', + // Do offer a fix despite a nested async function containing await + output: 'describe("hello", () => {async function bar() {await foo;}})', + parserOptions: { ecmaVersion: 8 }, + errors: [ { + message: 'Unexpected async function in describe()', + line: 1, + column: 19 + } ] + } + ] +});