Repository: lo1tuma/eslint-plugin-mocha (ts)

Issue:
Rule proposal: no-empty-description
Hi, that's would be awesome to catch empty suites/tests. 

```js
test('', function () {
    assert.equal(2 + 2, 4);
});
```

Of course, I can use `valid-suite-description`/ `valid-test-description` rules to enforce descriptions format, but I don't have a specific convention, and a lightweight `no-empty` rule will be useful for such a case.

I'm ready to provide help with implementation, if needed.



Pull request description:
Add `no-empty-description` rule. closes #302
This rule enforces specifying the suite/test descriptions for each test. 

The following patterns are considered problems:

```js
it();
suite("");
test(function() { })
test.only(" ", function() { })
```

also, I added this rule to the recommendations list.


Patch:
diff --git a/.eslintrc.json b/.eslintrc.json
index 902c911..333cb76 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -14,7 +14,7 @@
 
     "parserOptions": {
         "sourceType": "script",
-        "ecmaVersion": 2017,
+        "ecmaVersion": 2019,
         "ecmaFeatures": {
             "jsx": false,
             "globalReturn": false,
diff --git a/docs/rules/README.md b/docs/rules/README.md
index 29c1d83..beab601 100644
--- a/docs/rules/README.md
+++ b/docs/rules/README.md
@@ -24,3 +24,4 @@
 ||:wrench:| [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
+|:heavy_check_mark:|| [no-empty-description](no-empty-description.md) | Disallow empty test descriptions
diff --git a/docs/rules/no-empty-description.md b/docs/rules/no-empty-description.md
new file mode 100644
index 0000000..8eba62d
--- /dev/null
+++ b/docs/rules/no-empty-description.md
@@ -0,0 +1,45 @@
+# Disallow empty test descriptions (no-empty-description)
+
+This rule enforces you to specify the suite/test descriptions for each test.
+
+## Rule Details
+
+This rule checks each mocha test function to have a non-empty description.
+
+The following patterns are considered problems:
+
+```js
+it();
+
+suite("");
+
+test(function() { })
+
+test.only(" ", function() { })
+
+```
+
+These patterns would not be considered problems:
+
+```js
+describe('foo', function () {
+    it('bar');
+});
+
+suite('foo', function () {
+    test('bar');
+});
+```
+
+## Options
+
+Example of a custom rule configuration:
+
+```js
+   rules: {
+       "mocha/no-empty-description": [ "warn", {
+           testNames: ["it", "specify", "test", "mytestname"],
+           message: 'custom error message'
+       } ]
+   }
+```
\ No newline at end of file
diff --git a/index.js b/index.js
index 6d6aac7..05a0e9f 100644
--- a/index.js
+++ b/index.js
@@ -23,7 +23,8 @@ module.exports = {
         'no-top-level-hooks': require('./lib/rules/no-top-level-hooks'),
         'prefer-arrow-callback': require('./lib/rules/prefer-arrow-callback'),
         'valid-suite-description': require('./lib/rules/valid-suite-description'),
-        'valid-test-description': require('./lib/rules/valid-test-description')
+        'valid-test-description': require('./lib/rules/valid-test-description'),
+        'no-empty-description': require('./lib/rules/no-empty-description.js')
     },
     configs: {
         all: {
@@ -51,7 +52,8 @@ module.exports = {
                 'mocha/no-top-level-hooks': 'error',
                 'mocha/prefer-arrow-callback': 'error',
                 'mocha/valid-suite-description': 'error',
-                'mocha/valid-test-description': 'error'
+                'mocha/valid-test-description': 'error',
+                'mocha/no-empty-description': 'error'
             }
         },
 
@@ -80,7 +82,8 @@ module.exports = {
                 'mocha/no-top-level-hooks': 'warn',
                 'mocha/prefer-arrow-callback': 'off',
                 'mocha/valid-suite-description': 'off',
-                'mocha/valid-test-description': 'off'
+                'mocha/valid-test-description': 'off',
+                'no-empty-description': 'error'
             }
         }
     }
diff --git a/lib/rules/no-empty-description.js b/lib/rules/no-empty-description.js
new file mode 100644
index 0000000..d40932b
--- /dev/null
+++ b/lib/rules/no-empty-description.js
@@ -0,0 +1,82 @@
+'use strict';
+
+const { getStringIfConstant } = require('eslint-utils');
+
+const DEFAULT_TEST_NAMES = [ 'describe', 'context', 'suite', 'it', 'test', 'specify' ];
+const ERROR_MESSAGE = 'Unexpected empty test description.';
+
+function objectOptions(options = {}) {
+    const {
+        testNames = DEFAULT_TEST_NAMES,
+        message
+    } = options;
+
+    return { testNames, message };
+}
+
+module.exports = {
+    meta: {
+        type: 'suggestion',
+        docs: {
+            description: 'Disallow empty test descriptions',
+            url: 'https://github.com/lo1tuma/eslint-plugin-mocha/blob/master/docs/rules/no-empty-description.md'
+        },
+        messages: {
+            error: ERROR_MESSAGE
+        },
+        schema: [
+            {
+                type: 'object',
+                properties: {
+                    testNames: {
+                        type: 'array',
+                        items: {
+                            type: 'string'
+                        }
+                    },
+                    message: {
+                        type: 'string'
+                    }
+                },
+                additionalProperties: false
+            }
+        ]
+    },
+    create(context) {
+        const options = context.options[0];
+
+        const { testNames, message } = objectOptions(options);
+
+        function isTest(node) {
+            return node.callee && node.callee.name && testNames.includes(node.callee.name);
+        }
+
+        function isTemplateString(node) {
+            return [ 'TaggedTemplateExpression', 'TemplateLiteral' ].includes(node && node.type);
+        }
+
+        function checkDescription(mochaCallExpression) {
+            const description = mochaCallExpression.arguments[0];
+            const text = getStringIfConstant(description);
+
+            if (isTemplateString(description) && text === null) {
+                return true;
+            }
+
+            return text && text.trim().length;
+        }
+
+        return {
+            CallExpression(node) {
+                if (isTest(node)) {
+                    if (!checkDescription(node)) {
+                        context.report({
+                            node,
+                            message: message || ERROR_MESSAGE
+                        });
+                    }
+                }
+            }
+        };
+    }
+};


Test patch:
diff --git a/test/rules/no-empty-description.js b/test/rules/no-empty-description.js
new file mode 100644
index 0000000..3faa7b1
--- /dev/null
+++ b/test/rules/no-empty-description.js
@@ -0,0 +1,83 @@
+'use strict';
+
+const RuleTester = require('eslint').RuleTester;
+const rules = require('../..').rules;
+const ruleTester = new RuleTester();
+const defaultErrorMessage = 'Unexpected empty test description.';
+const firstLine = { column: 1, line: 1 };
+
+ruleTester.run('no-empty-description', rules['no-empty-description'], {
+
+    valid: [
+        'describe("some text")',
+        'describe.only("some text")',
+        'describe("some text", function() { })',
+
+        'context("some text")',
+        'context.only("some text")',
+        'context("some text", function() { })',
+
+        'it("some text")',
+        'it.only("some text")',
+        'it("some text", function() { })',
+
+        'suite("some text")',
+        'suite.only("some text")',
+        'suite("some text", function() { })',
+
+        'test("some text")',
+        'test.only("some text")',
+        'test("some text", function() { })',
+
+        'notTest()',
+
+        {
+            parserOptions: { ecmaVersion: 2019 },
+            code: 'it(string`template`, function () {});'
+        },
+        {
+            parserOptions: { ecmaVersion: 2019 },
+            code: 'it(`template strings`, function () {});'
+        },
+        {
+            parserOptions: { ecmaVersion: 2019 },
+            code: 'it(`${foo} template strings`, function () {});'
+        },
+        {
+            options: [ { testNames: [ 'someFunction' ] } ],
+            code: 'someFunction("this is a test", function () { });'
+        }
+    ],
+
+    invalid: [
+        {
+            code: 'test()',
+            errors: [ { message: defaultErrorMessage, ...firstLine } ]
+        },
+        {
+            code: 'test(function() { })',
+            errors: [ { message: defaultErrorMessage, ...firstLine } ]
+        },
+        {
+            code: 'test("", function() { })',
+            errors: [ { message: defaultErrorMessage, ...firstLine } ]
+        },
+        {
+            code: 'test("      ", function() { })',
+            errors: [ { message: defaultErrorMessage, ...firstLine } ]
+        },
+
+        {
+            options: [ { testNames: [ 'someFunction' ], message: 'Custom Error' } ],
+            code: 'someFunction(function() { })',
+            errors: [ { message: 'Custom Error', ...firstLine } ]
+        },
+        {
+            parserOptions: { ecmaVersion: 2019 },
+            code: 'it(` `, function () { });',
+            errors: [ { message: defaultErrorMessage, ...firstLine } ]
+        }
+    ]
+
+});
+