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

Issue:
Rule proposal: No multiple hooks on the same level
In the codebase of one of my projects, I often see multiple hooks of the same type being defined under the same `describe` level:

```js
describe('foo', function() {
  before(function() {
    // set up something
  });
  before(function() {
    // set up something else
  });

  // tests
});
```

This can be confusing when one hook is before the tests and the other after the tests (think `after`/`afterEach`). Anyway, this is IMO not a good pattern as you can and should do everything in one hook.

Pull request description:
Add rule `no-sibling-hooks` (fixes #82)
Add rule `no-sibling-hooks` (fixes #82)

Let me know if you want me to change something or if I've missed something :)

I deactivatd two rules [here](https://github.com/lo1tuma/eslint-plugin-mocha/pull/85/files#diff-23841b5a313e8e46a51f3d758eaa6f4fR22) about the function complexity. I could not make the function less complex. I could move the last part of the function to another function, but I'm not sure it would make it less complex, as you'd pass a few arguments around. Not opposed to doing that though. Let me know what you wish for me to do.

Patch:
diff --git a/docs/rules/README.md b/docs/rules/README.md
index df8f4cd..9cee1af 100644
--- a/docs/rules/README.md
+++ b/docs/rules/README.md
@@ -8,5 +8,6 @@
 * [no-global-tests](no-global-tests.md) - disallow global tests
 * [valid-test-description](valid-test-description.md) - match test descriptions against a pre-configured regular expression
 * [valid-suite-description](valid-suite-description.md) - match suite descriptions against a pre-configured regular expression
+* [no-sibling-hooks](no-sibling-hooks.md) - disallow duplicate uses of a hook at the same level inside a describe
 * [no-mocha-arrows](no-mocha-arrows.md) - disallow arrow functions as arguments to mocha globals
 * [no-hooks](no-hooks.md) - disallow hooks
diff --git a/docs/rules/no-sibling-hooks.md b/docs/rules/no-sibling-hooks.md
new file mode 100644
index 0000000..282a85b
--- /dev/null
+++ b/docs/rules/no-sibling-hooks.md
@@ -0,0 +1,59 @@
+# Disallow hooks (no-hooks)
+
+Mocha proposes hooks that allow code to be run before or after every or all tests. This helps define a common setup or teardown process for every test.
+It is possible to declare a hook multiple times inside the same test suite, but it can be confusing. It is better to have one hook handle the whole of the setup or teardown logic of the test suite.
+
+## Rule Details
+
+This rule looks for every call to `before`, `after`, `beforeEach` and `afterEach` and reports them if they are called at least twice inside of a test suite at the same level.
+
+The following patterns are considered warnings:
+
+```js
+describe('foo', function () {
+    var mockUser;
+    var mockLocation;
+
+    before(function () { // Is allowed this time
+        mockUser = {age: 50};
+    });
+
+    before(function () { // Duplicate! Is not allowed this time
+        mockLocation = {city: 'New York'};
+    });
+
+    // Same for the other hooks
+    after(function () {});
+    after(function () {}); // Duplicate!
+
+    beforeEach(function () {});
+    beforeEach(function () {}); // Duplicate!
+
+    afterEach(function () {});
+    afterEach(function () {}); // Duplicate!
+});
+```
+
+These patterns would not be considered warnings:
+
+```js
+describe('foo', function () {
+    var mockUser;
+    var mockLocation;
+
+    before(function () { // Is allowed this time
+        mockUser = {age: 50};
+        mockLocation = {city: 'New York'};
+    });
+
+    describe('bar', function () {
+      before(function () { // Is allowed because it's nested in a new describe
+        // ...
+      });
+    });
+});
+```
+
+## When Not To Use It
+
+* If you use another library which exposes a similar API as mocha (e.g. `before`, `after`), you should turn this rule off, because it would raise warnings.
diff --git a/index.js b/index.js
index e018c8a..30fbabe 100644
--- a/index.js
+++ b/index.js
@@ -11,7 +11,8 @@ module.exports = {
         'valid-test-description': require('./lib/rules/valid-test-description'),
         'valid-suite-description': require('./lib/rules/valid-suite-description'),
         'no-mocha-arrows': require('./lib/rules/no-mocha-arrows'),
-        'no-hooks': require('./lib/rules/no-hooks')
+        'no-hooks': require('./lib/rules/no-hooks'),
+        'no-sibling-hooks': require('./lib/rules/no-sibling-hooks')
     },
     configs: {
         recommended: {
diff --git a/lib/rules/no-sibling-hooks.js b/lib/rules/no-sibling-hooks.js
new file mode 100644
index 0000000..8cb89d4
--- /dev/null
+++ b/lib/rules/no-sibling-hooks.js
@@ -0,0 +1,64 @@
+'use strict';
+
+var describeAliases = [ 'describe', 'xdescribe', 'context', 'xcontext' ];
+
+function newDescribeLayer(describeNode) {
+    return {
+        describeNode: describeNode,
+        before: false,
+        after: false,
+        beforeEach: false,
+        afterEach: false
+    };
+}
+
+function isDescribeIdentifier(node) {
+    return node.type === 'Identifier' && describeAliases.indexOf(node.name) !== -1;
+}
+
+function isDescribe(node) {
+  return node
+      && node.type === 'CallExpression'
+      && (isDescribeIdentifier(node.callee)
+          // eslint-disable-next-line no-extra-parens
+          || (node.callee.type === 'MemberExpression' && isDescribeIdentifier(node.callee.object))
+      );
+}
+
+module.exports = function (context) {
+    var hooks = [ 'before', 'after', 'beforeEach', 'afterEach' ],
+        isUsed = [];
+
+    return {
+        Program: function (node) {
+            isUsed.push(newDescribeLayer(node));
+        },
+
+        CallExpression: function (node) { // eslint-disable-line complexity
+            var name = node.callee && node.callee.name;
+            if (isDescribe(node)) {
+              isUsed.push(newDescribeLayer(node));
+              return;
+            }
+
+            if (node.callee.type !== 'Identifier' || hooks.indexOf(name) === -1) {
+              return;
+            }
+
+            if (isUsed[isUsed.length - 1][name]) {
+                context.report({
+                    node: node.callee,
+                    message: 'Unexpected use of duplicate Mocha `' + name + '` hook'
+                });
+            }
+
+            isUsed[isUsed.length - 1][name] = true;
+        },
+
+        'CallExpression:exit': function (node) {
+            if (isUsed[isUsed.length - 1].describeNode === node) {
+              isUsed.pop();
+            }
+        }
+    };
+};


Test patch:
diff --git a/test/rules/no-sibling-hooks.js b/test/rules/no-sibling-hooks.js
new file mode 100644
index 0000000..1616458
--- /dev/null
+++ b/test/rules/no-sibling-hooks.js
@@ -0,0 +1,120 @@
+'use strict';
+
+var RuleTester = require('eslint').RuleTester,
+    rules = require('../../').rules,
+    ruleTester = new RuleTester();
+
+ruleTester.run('no-sibling-hooks', rules['no-sibling-hooks'], {
+
+    valid: [
+        'describe(function() { before(function() {}); it(function() {}); });',
+        'describe(function() { after(function() {}); it(function() {}); });',
+        'describe(function() { beforeEach(function() {}); it(function() {}); });',
+        'describe(function() { afterEach(function() {}); it(function() {}); });',
+        'describe(function() { before(function() {}); after(function() {}); });',
+        'describe(function() { before(function() {}); beforeEach(function() {}); });',
+        'describe(function() { beforeEach(function() {}); afterEach(function() {}); });',
+        'before(function() {}); beforeEach(function() {});',
+        'foo.before(function() {}); foo.before(function() {});',
+        [
+          'describe(function() {',
+          '    before(function() {});',
+          '    describe(function() {',
+          '        before(function() {});',
+          '    });',
+          '});'
+        ].join('\n'),
+        [
+          'describe(function() {',
+          '    describe(function() {',
+          '        before(function() {});',
+          '    });',
+          '    before(function() {});',
+          '});'
+        ].join('\n'),
+        [
+          'describe(function() {',
+          '    describe.only(function() {',
+          '        before(function() {});',
+          '    });',
+          '    before(function() {});',
+          '});'
+        ].join('\n'),
+        [
+          'describe(function() {',
+          '    describe.skip(function() {',
+          '        before(function() {});',
+          '    });',
+          '    before(function() {});',
+          '});'
+        ].join('\n'),
+        [
+          'describe(function() {',
+          '    xdescribe(function() {',
+          '        before(function() {});',
+          '    });',
+          '    before(function() {});',
+          '});'
+        ].join('\n'),
+        [
+          'describe(function() {',
+          '    context(function() {',
+          '        before(function() {});',
+          '    });',
+          '    before(function() {});',
+          '});'
+        ].join('\n'),
+        [
+          'describe(function() {',
+          '    xcontext(function() {',
+          '        before(function() {});',
+          '    });',
+          '    before(function() {});',
+          '});'
+        ].join('\n')
+    ],
+
+    invalid: [
+        {
+            code: 'describe(function() { before(function() {}); before(function() {}); });',
+            errors: [ { message: 'Unexpected use of duplicate Mocha `before` hook', column: 46, line: 1 } ]
+        },
+        {
+            code: 'describe(function() { after(function() {}); after(function() {}); });',
+            errors: [ { message: 'Unexpected use of duplicate Mocha `after` hook', column: 45, line: 1 } ]
+        },
+        {
+            code: 'describe(function() { beforeEach(function() {}); beforeEach(function() {}); });',
+            errors: [ { message: 'Unexpected use of duplicate Mocha `beforeEach` hook', column: 50, line: 1 } ]
+        },
+        {
+            code: 'describe(function() { afterEach(function() {}); afterEach(function() {}); });',
+            errors: [ { message: 'Unexpected use of duplicate Mocha `afterEach` hook', column: 49, line: 1 } ]
+        },
+        {
+            code: [
+              'describe(function() {',
+              '    before(function() {});',
+              '    describe(function() {',
+              '        before(function() {});',
+              '        before(function() {});',
+              '    });',
+              '});'
+            ].join('\n'),
+            errors: [ { message: 'Unexpected use of duplicate Mocha `before` hook', column: 9, line: 5 } ]
+        },
+        {
+            code: [
+              'describe(function() {',
+              '    before(function() {});',
+              '    describe(function() {',
+              '        before(function() {});',
+              '    });',
+              '    before(function() {});',
+              '});'
+            ].join('\n'),
+            errors: [ { message: 'Unexpected use of duplicate Mocha `before` hook', column: 5, line: 6 } ]
+        }
+    ]
+
+});