Repository: lo1tuma/eslint-plugin-mocha
Pull request #188 (2019-02-05 11:31:03)

Implement no-async-describe rule
I've seen `describe('my test', async function () { ... })` cause subtle problems, so I think it makes sense to add a rule for it.


Patch:
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);
+                        }
+                    });
+                }
+            }
+        }
+    };
+};


Test patch:
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
+            } ]
+        }
+    ]
+});