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

Issue:
Support function calls as custom names (to stop false warnings)
### Motivation

Hi. I'd like the following sorts of things to be supported, but they actually give errors in the recommended set:

```javascript
// Say we have a custom function (this one is supposed to ignore tests completely in some situations):
describe.on = (platform) => (process.env.PLATFORM === platform ? describe : (() => {}));

describe('does something', () => {
  before(...);

  // This isn't treated as a suite, so we can get errors about multiple top-level suites, multiple sibling hooks, etc:
  describe.on('windows')('nested', () => {
    before(...); // <- this is treated as a sibling of the above before.
  });
});
```
and
```javascript
const forEach = require('mocha-each');

forEach([ 1, 2, 3 ]).describe('works', (n) => {
  before(...); // <- this is treated as a hook at the top level, even though it's nested here.
  it(...);
});
```

### Possible solution

Allow call expressions to have a `name` when getting the node. It's already the case that dotted names will work and can be specified as custom additional ones.
It's then possible for `additionalCustomNames` to support names of the form
* `{ name: 'describe.on()', type: 'suite' }`,
* `{ name: 'forEach().describe', type: 'suite' }`

so that I can selectively rule these ones *in* to the set of suites.

### Alternatives

* I tried turning off the rule with some `eslint-disable`s but we actually have too many (of the first form I gave in Motivation) to want to do this. And of course, we do want the rules in most places, so globally switching it off isn't suitable.
* In the related PR, my first attempt was to have call expressions with *just* the name, but this didn't work for the `max-top-level-suites` rule (because `describe.on()` and `describe.on()(...)` had the "same" name)

Pull request description:
Support function calls as custom names (to stop false warnings)
Call expressions can have a name when getting the node. Fixes #335 

There might have been better ways to achieve this, but this looked like the most straightforward.

Patch:
diff --git a/README.md b/README.md
index 39a7c4a..70c6156 100644
--- a/README.md
+++ b/README.md
@@ -29,23 +29,51 @@ Then add a reference to this plugin and selected rules in your eslint config:
 
 This plugin supports the following settings, which are used by multiple rules:
 
-* `additionalCustomNames`: This allows rules to check additional function names when looking for suites or test cases. This might be used with a custom Mocha extension, such as [`ember-mocha`](https://github.com/switchfly/ember-mocha)
-**Example:**
-
-```json
-{
-    "rules": {
-        "mocha/no-skipped-tests": "error",
-        "mocha/no-exclusive-tests": "error"
-    },
-    "settings": {
-        "mocha/additionalCustomNames": [
-            { "name": "describeModule", "type": "suite", "interfaces": [ "BDD" ] },
-            { "name": "testModule", "type": "testCase", "interfaces": [ "TDD" ] }
-        ]
+* `additionalCustomNames`: This allows rules to check additional function names when looking for suites or test cases. This might be used with a custom Mocha extension, such as [`ember-mocha`](https://github.com/switchfly/ember-mocha) or [`mocha-each`](https://github.com/ryym/mocha-each).
+
+   **Example:**
+
+    ```json
+    {
+        "rules": {
+            "mocha/no-skipped-tests": "error",
+            "mocha/no-exclusive-tests": "error"
+        },
+        "settings": {
+            "mocha/additionalCustomNames": [
+                { "name": "describeModule", "type": "suite", "interfaces": [ "BDD" ] },
+                { "name": "testModule", "type": "testCase", "interfaces": [ "TDD" ] }
+            ]
+        }
     }
-}
-```
+    ```
+
+  The `name` property can be in any of the following forms:
+  * A plain name e.g. `describeModule`, which allows:
+
+    ```javascript
+    describeModule("example", function() { ... });
+    ```
+
+  * A dotted name, e.g. `describe.modifier`, which allows:
+
+    ```javascript
+    describe.modifier("example", function() { ... });
+    ```
+
+  * A name with parentheses, e.g. `forEach().describe`, which allows:
+
+    ```javascript
+    forEach([ 1, 2, 3 ])
+        .describe("example", function(n) { ... });
+    ```
+
+  * Any combination of the above, e.g. `forEach().describeModule.modifier`, which allows:
+
+    ```javascript
+    forEach([ 1, 2, 3 ])
+        .describeModule.modifier("example", function(n) { ... });
+    ```
 
 ## Configs
 
diff --git a/lib/util/ast.js b/lib/util/ast.js
index 29b88ec..fecc57a 100644
--- a/lib/util/ast.js
+++ b/lib/util/ast.js
@@ -31,6 +31,9 @@ function getNodeName(node) {
     if (node.type === 'ThisExpression') {
         return 'this';
     }
+    if (node.type === 'CallExpression') {
+        return `${getNodeName(node.callee)}()`;
+    }
     if (node.type === 'MemberExpression') {
         return `${getNodeName(node.object)}.${getPropertyName(node.property)}`;
     }


Test patch:
diff --git a/test/rules/max-top-level-suites.js b/test/rules/max-top-level-suites.js
index fe41c39..7656772 100644
--- a/test/rules/max-top-level-suites.js
+++ b/test/rules/max-top-level-suites.js
@@ -68,6 +68,40 @@ ruleTester.run('max-top-level-suites', rules['max-top-level-suites'], {
                 sourceType: 'module',
                 ecmaVersion: 2015
             }
+        },
+        {
+            code: [
+                'describe.foo("", function () {',
+                '    describe("", function () {});',
+                '    describe("", function () {});',
+                '});'
+            ].join('\n'),
+            parserOptions: {
+                sourceType: 'module',
+                ecmaVersion: 2015
+            },
+            settings: {
+                mocha: {
+                    additionalCustomNames: [ { name: 'describe.foo', type: 'suite', interfaces: [ 'BDD' ] } ]
+                }
+            }
+        },
+        {
+            code: [
+                'describe.foo("bar")("", function () {',
+                '    describe("", function () {});',
+                '    describe("", function () {});',
+                '});'
+            ].join('\n'),
+            parserOptions: {
+                sourceType: 'module',
+                ecmaVersion: 2015
+            },
+            settings: {
+                mocha: {
+                    additionalCustomNames: [ { name: 'describe.foo()', type: 'suite', interfaces: [ 'BDD' ] } ]
+                }
+            }
         }
     ],
 
@@ -211,6 +245,34 @@ ruleTester.run('max-top-level-suites', rules['max-top-level-suites'], {
             errors: [
                 { message: 'The number of top-level suites is more than 1.' }
             ]
+        }, {
+            code: 'describe.foo("bar")("this is a test", function () { });' +
+                  'context.foo("this is a different test", function () { });',
+            settings: {
+                mocha: {
+                    additionalCustomNames: [
+                        { name: 'describe.foo()', type: 'suite', interfaces: [ 'BDD' ] },
+                        { name: 'context.foo', type: 'suite', interfaces: [ 'BDD' ] }
+                    ]
+                }
+            },
+            errors: [
+                { message: 'The number of top-level suites is more than 1.' }
+            ]
+        }, {
+            code: 'forEach([ 1, 2, 3 ]).describe("this is a test", function () { });' +
+                  'context.foo("this is a different test", function () { });',
+            settings: {
+                mocha: {
+                    additionalCustomNames: [
+                        { name: 'forEach().describe', type: 'suite', interfaces: [ 'BDD' ] },
+                        { name: 'context.foo', type: 'suite', interfaces: [ 'BDD' ] }
+                    ]
+                }
+            },
+            errors: [
+                { message: 'The number of top-level suites is more than 1.' }
+            ]
         }
     ]
 });
diff --git a/test/rules/no-async-describe.js b/test/rules/no-async-describe.js
index bcb16c2..c22728d 100644
--- a/test/rules/no-async-describe.js
+++ b/test/rules/no-async-describe.js
@@ -72,6 +72,40 @@ ruleTester.run('no-async-describe', rule, {
                 line: 1,
                 column: 19
             } ]
+        },
+        {
+            code: 'describe.foo("bar")("hello", async () => {})',
+            output: 'describe.foo("bar")("hello", () => {})',
+            settings: {
+                mocha: {
+                    additionalCustomNames: [
+                        { name: 'describe.foo()', type: 'suite', interfaces: [ 'BDD' ] }
+                    ]
+                }
+            },
+            parserOptions: { ecmaVersion: 8 },
+            errors: [ {
+                message: 'Unexpected async function in describe.foo()()',
+                line: 1,
+                column: 30
+            } ]
+        },
+        {
+            code: 'forEach([ 1, 2, 3 ]).describe.foo("hello", async () => {})',
+            output: 'forEach([ 1, 2, 3 ]).describe.foo("hello", () => {})',
+            settings: {
+                mocha: {
+                    additionalCustomNames: [
+                        { name: 'forEach().describe.foo', type: 'suite', interfaces: [ 'BDD' ] }
+                    ]
+                }
+            },
+            parserOptions: { ecmaVersion: 8 },
+            errors: [ {
+                message: 'Unexpected async function in forEach().describe.foo()',
+                line: 1,
+                column: 44
+            } ]
         }
     ]
 });
diff --git a/test/rules/no-exclusive-tests.js b/test/rules/no-exclusive-tests.js
index e36bb58..96c0ce2 100644
--- a/test/rules/no-exclusive-tests.js
+++ b/test/rules/no-exclusive-tests.js
@@ -38,6 +38,14 @@ ruleTester.run('no-exclusive-tests', rules['no-exclusive-tests'], {
                     additionalCustomNames: [ { name: 'a.b.c', type: 'testCase', interfaces: [ 'BDD' ] } ]
                 }
             }
+        },
+        {
+            code: 'a.b().c.skip()',
+            settings: {
+                mocha: {
+                    additionalCustomNames: [ { name: 'a.b().c', type: 'testCase', interfaces: [ 'BDD' ] } ]
+                }
+            }
         }
     ],
 
@@ -175,6 +183,15 @@ ruleTester.run('no-exclusive-tests', rules['no-exclusive-tests'], {
                 }
             },
             errors: [ { message: expectedErrorMessage, column: 9, line: 1 } ]
+        },
+        {
+            code: 'foo("bar").it.only()',
+            settings: {
+                mocha: {
+                    additionalCustomNames: [ { name: 'foo().it', type: 'testCase', interfaces: [ 'BDD' ] } ]
+                }
+            },
+            errors: [ { message: expectedErrorMessage, column: 15, line: 1 } ]
         }
     ]
 });
diff --git a/test/rules/no-sibling-hooks.js b/test/rules/no-sibling-hooks.js
index 3c62fa0..14ac2d1 100644
--- a/test/rules/no-sibling-hooks.js
+++ b/test/rules/no-sibling-hooks.js
@@ -98,6 +98,72 @@ ruleTester.run('no-sibling-hooks', rules['no-sibling-hooks'], {
                     additionalCustomNames: [ { name: 'foo', type: 'suite', interfaces: [ 'BDD' ] } ]
                 }
             }
+        }, {
+            code: [
+                'describe(function() {',
+                '    before(function() {});',
+                '    describe.foo(function() {',
+                '        before(function() {});',
+                '    });',
+                '});'
+            ].join('\n'),
+            settings: {
+                mocha: {
+                    additionalCustomNames: [ { name: 'describe.foo', type: 'suite', interfaces: [ 'BDD' ] } ]
+                }
+            }
+        }, {
+            code: [
+                'describe(function() {',
+                '    before(function() {});',
+                '    describe.foo()(function() {',
+                '        before(function() {});',
+                '    });',
+                '});'
+            ].join('\n'),
+            settings: {
+                mocha: {
+                    additionalCustomNames: [ { name: 'describe.foo()', type: 'suite', interfaces: [ 'BDD' ] } ]
+                }
+            }
+        }, {
+            code: [
+                'describe(function() {',
+                '    before(function() {});',
+                '    forEach([ 1, 2, 3 ]).describe(function() {',
+                '        before(function() {});',
+                '    });',
+                '    forEach([ 1, 2, 3 ]).context(function() {',
+                '        before(function() {});',
+                '    });',
+                '    forEach([ 1, 2, 3 ]).describe.only(function() {',
+                '        before(function() {});',
+                '    });',
+                '    forEach([ 1, 2, 3 ]).describe.skip(function() {',
+                '        before(function() {});',
+                '    });',
+                '    forEach([ 1, 2, 3 ]).describe.foo(function() {',
+                '        before(function() {});',
+                '    });',
+                '    forEach([ 1, 2, 3 ]).describe.bar([ 9, 8, 7 ])(function() {',
+                '        before(function() {});',
+                '    });',
+                '    deep.forEach({ a: [ 1, 2, 3 ], b: [ 4, 5, 6 ] }).describe(function() {',
+                '        before(function() {});',
+                '    });',
+                '});'
+            ].join('\n'),
+            settings: {
+                mocha: {
+                    additionalCustomNames: [
+                        { name: 'forEach().describe', type: 'suite', interfaces: [ 'BDD' ] },
+                        { name: 'forEach().context', type: 'suite', interfaces: [ 'BDD' ] },
+                        { name: 'forEach().describe.foo', type: 'suite', interfaces: [ 'BDD' ] },
+                        { name: 'forEach().describe.bar()', type: 'suite', interfaces: [ 'BDD' ] },
+                        { name: 'deep.forEach().describe', type: 'suite', interfaces: [ 'BDD' ] }
+                    ]
+                }
+            }
         }
     ],
 
@@ -172,6 +238,54 @@ ruleTester.run('no-sibling-hooks', rules['no-sibling-hooks'], {
                 }
             },
             errors: [ { message: 'Unexpected use of duplicate Mocha `before` hook', column: 5, line: 6 } ]
+        }, {
+            code: [
+                'describe.foo(function() {',
+                '    before(function() {});',
+                '    describe.foo(function() {',
+                '        before(function() {});',
+                '    });',
+                '    before(function() {});',
+                '});'
+            ].join('\n'),
+            settings: {
+                mocha: {
+                    additionalCustomNames: [ { name: 'describe.foo', type: 'suite', interfaces: [ 'BDD' ] } ]
+                }
+            },
+            errors: [ { message: 'Unexpected use of duplicate Mocha `before` hook', column: 5, line: 6 } ]
+        }, {
+            code: [
+                'describe.foo()(function() {',
+                '    before(function() {});',
+                '    describe.foo()(function() {',
+                '        before(function() {});',
+                '    });',
+                '    before(function() {});',
+                '});'
+            ].join('\n'),
+            settings: {
+                mocha: {
+                    additionalCustomNames: [ { name: 'describe.foo()', type: 'suite', interfaces: [ 'BDD' ] } ]
+                }
+            },
+            errors: [ { message: 'Unexpected use of duplicate Mocha `before` hook', column: 5, line: 6 } ]
+        }, {
+            code: [
+                'forEach([ 1, 2, 3 ]).describe(function() {',
+                '    before(function() {});',
+                '    forEach([ 4, 5, 6 ]).describe(function() {',
+                '        before(function() {});',
+                '    });',
+                '    before(function() {});',
+                '});'
+            ].join('\n'),
+            settings: {
+                mocha: {
+                    additionalCustomNames: [ { name: 'forEach().describe', type: 'suite', interfaces: [ 'BDD' ] } ]
+                }
+            },
+            errors: [ { message: 'Unexpected use of duplicate Mocha `before` hook', column: 5, line: 6 } ]
         }
     ]
 
diff --git a/test/rules/no-skipped-tests.js b/test/rules/no-skipped-tests.js
index 7655c5e..2f755b5 100644
--- a/test/rules/no-skipped-tests.js
+++ b/test/rules/no-skipped-tests.js
@@ -139,6 +139,15 @@ ruleTester.run('no-skipped-tests', rules['no-skipped-tests'], {
                 }
             },
             errors: [ { message: expectedErrorMessage, column: 1, line: 1 } ]
+        },
+        {
+            code: 'custom("bar").it.skip()',
+            settings: {
+                mocha: {
+                    additionalCustomNames: [ { name: 'custom().it', type: 'testCase', interfaces: [ 'BDD' ] } ]
+                }
+            },
+            errors: [ { message: expectedErrorMessage, column: 18, line: 1 } ]
         }
     ]
 
diff --git a/test/rules/no-top-level-hooks.js b/test/rules/no-top-level-hooks.js
index b1d76f2..5fed652 100644
--- a/test/rules/no-top-level-hooks.js
+++ b/test/rules/no-top-level-hooks.js
@@ -34,6 +34,27 @@ ruleTester.run('no-top-level-hooks', rules['no-top-level-hooks'], {
                     additionalCustomNames: [ { name: 'foo', type: 'suite', interfaces: [ 'BDD' ] } ]
                 }
             }
+        }, {
+            code: 'describe.foo(function() { before(function() {}); });',
+            settings: {
+                mocha: {
+                    additionalCustomNames: [ { name: 'describe.foo', type: 'suite', interfaces: [ 'BDD' ] } ]
+                }
+            }
+        }, {
+            code: 'describe.foo()(function() { before(function() {}); });',
+            settings: {
+                mocha: {
+                    additionalCustomNames: [ { name: 'describe.foo()', type: 'suite', interfaces: [ 'BDD' ] } ]
+                }
+            }
+        }, {
+            code: 'forEach([ 1, 2, 3 ]).describe(function() { before(function() {}); });',
+            settings: {
+                mocha: {
+                    additionalCustomNames: [ { name: 'forEach().describe', type: 'suite', interfaces: [ 'BDD' ] } ]
+                }
+            }
         }
     ],
     invalid: [
@@ -92,6 +113,30 @@ ruleTester.run('no-top-level-hooks', rules['no-top-level-hooks'], {
                 column: 21,
                 line: 1
             } ]
+        },
+        {
+            code: 'describe()(function() {}); before(function() {});',
+            errors: [ {
+                message: 'Unexpected use of Mocha `before` hook outside of a test suite',
+                column: 28,
+                line: 1
+            } ]
+        },
+        {
+            code: 'describe.foo()(function() {}); before(function() {});',
+            errors: [ {
+                message: 'Unexpected use of Mocha `before` hook outside of a test suite',
+                column: 32,
+                line: 1
+            } ]
+        },
+        {
+            code: 'forEach([ 1, 2, 3 ]).describe(function() {}); before(function() {});',
+            errors: [ {
+                message: 'Unexpected use of Mocha `before` hook outside of a test suite',
+                column: 47,
+                line: 1
+            } ]
         }
     ]