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

Issue:
Rule proposal: no-nested-its
Today I accidentally wrote some tests like this:

```
describe("a", function() {
  it("b", function() {
    it("c1", function() {
       expect(...);
    });

    it("c2", function() {
       expect(...);
    });
  });
});
```

instead of like this:


```
describe("a", function() {
  describe("b", function() { // <-----------
    it("c1", function() {
       expect(...);
    });

    it("c2", function() {
       expect(...);
    });
  });
});
```

This caused mocha to not run tests "c1" or "c2" at all, since they were accidentally declared inside an `it` block instead of a `describe` block.

I only noticed because my code coverage tool said it was missing certain execution branches that I could have sworn I had written tests for. A linter setting to point out this subtle mistake would be helpful IMO.

Pull request:
Add no-nested-tests rule
fixes #108

Patch:
diff --git a/docs/rules/README.md b/docs/rules/README.md
index 0407771..3325116 100644
--- a/docs/rules/README.md
+++ b/docs/rules/README.md
@@ -16,3 +16,4 @@
 * [no-top-level-hooks](no-top-level-hooks.md) - disallow top-level hooks
 * [no-identical-title](no-identical-title.md) - disallow identical titles
 * [max-top-level-suites](max-top-level-suites.md) - limit the number of top-level suites in a single file
+* [no-nested-tests](no-nested-tests.md) - disallow tests to be nested within other tests
diff --git a/index.js b/index.js
index bc75223..9d776b0 100644
--- a/index.js
+++ b/index.js
@@ -17,7 +17,8 @@ module.exports = {
         'no-sibling-hooks': require('./lib/rules/no-sibling-hooks'),
         'no-top-level-hooks': require('./lib/rules/no-top-level-hooks'),
         'no-identical-title': require('./lib/rules/no-identical-title'),
-        'max-top-level-suites': require('./lib/rules/max-top-level-suites')
+        'max-top-level-suites': require('./lib/rules/max-top-level-suites'),
+        'no-nested-tests': require('./lib/rules/no-nested-tests')
     },
     configs: {
         recommended: {
diff --git a/lib/util/ast.js b/lib/util/ast.js
index 50b47bb..d3fd5dd 100644
--- a/lib/util/ast.js
+++ b/lib/util/ast.js
@@ -6,7 +6,9 @@ var describeAliases = [ 'describe', 'xdescribe', 'describe.only', 'describe.skip
                         'context', 'xcontext', 'context.only', 'context.skip',
                         'suite', 'xsuite', 'suite.only', 'suite.skip' ],
     hooks = [ 'before', 'after', 'beforeEach', 'afterEach' ],
-    testCaseNames = [ 'it', 'it.only', 'test', 'test.only', 'specify', 'specify.only' ];
+    testCaseNames = [ 'it', 'it.only', 'it.skip',
+                      'test', 'test.only', 'test.skip',
+                      'specify', 'specify.only', 'specify.skip' ];
 
 function getNodeName(node) {
     if (node.type === 'MemberExpression') {


Test patch:
diff --git a/docs/rules/no-nested-tests.md b/docs/rules/no-nested-tests.md
new file mode 100644
index 0000000..2c78aa6
--- /dev/null
+++ b/docs/rules/no-nested-tests.md
@@ -0,0 +1,47 @@
+# Disallow tests to be nested within other tests (no-nested-tests)
+
+Test cases in mocha can be either global or within a suite but they can’t be nested within other tests. Unfortunately there is nothing stopping you from creating a test case within another test case but mocha will simply ignore those tests.
+
+```js
+it('something', function () {
+    it('should work', function () {
+        assert(fasle);
+    });
+});
+```
+Something like this could be easily happen by accident where the outer test case was actually meant to be a suite instead of a test.
+This rule reports such nested test cases in order to prevent problems where those nested tests are skipped silently.
+
+## Rule Details
+
+This rule looks for all test cases (`it`, `specify` and `test`) or suites (`describe`, `context` and `suite`) which are nested within another test case.
+
+The following patterns are considered problems:
+
+```js
+it('something', function () {
+    it('should work', function () {});
+});
+
+test('something', function () {
+    specify('should work', function () {});
+});
+
+it('something', function () {
+    describe('other thing', function () {
+        // …
+    });
+});
+
+```
+
+These patterns would not be considered problems:
+
+```js
+it('should work', function () {});
+it('should work too', function () {});
+
+describe('something', function () {
+    it('should work', function () {});
+});
+```
diff --git a/lib/rules/no-nested-tests.js b/lib/rules/no-nested-tests.js
new file mode 100644
index 0000000..84fddef
--- /dev/null
+++ b/lib/rules/no-nested-tests.js
@@ -0,0 +1,38 @@
+'use strict';
+
+var astUtils = require('../util/ast');
+
+module.exports = function noNestedTests(context) {
+    var testNestingLevel = 0;
+
+    function report(callExpression, isTestCase) {
+        var message = isTestCase ? 'Unexpected test nested within another test.' :
+            'Unexpected suite nested within a test.';
+
+        context.report({
+            message: message,
+            node: callExpression.callee
+        });
+    }
+
+    return {
+        CallExpression: function (node) {
+            var isTestCase = astUtils.isTestCase(node),
+                isDescribe = astUtils.isDescribe(node);
+
+            if (testNestingLevel > 0 && (isTestCase || isDescribe)) {
+                report(node, isTestCase);
+            }
+
+            if (isTestCase) {
+                testNestingLevel += 1;
+            }
+        },
+
+        'CallExpression:exit': function (node) {
+            if (astUtils.isTestCase(node)) {
+                testNestingLevel -= 1;
+            }
+        }
+    };
+};
diff --git a/test/rules/no-nested-tests.js b/test/rules/no-nested-tests.js
new file mode 100644
index 0000000..a3c126c
--- /dev/null
+++ b/test/rules/no-nested-tests.js
@@ -0,0 +1,109 @@
+'use strict';
+
+var RuleTester = require('eslint').RuleTester,
+    rule = require('../../lib/rules/no-nested-tests'),
+    ruleTester = new RuleTester();
+
+ruleTester.run('no-nested-tests', rule, {
+    valid: [
+        'it()',
+        'it(); it(); it()',
+        'describe("", function () { it(); })',
+        'describe("", function () { describe("", function () { it(); }); it(); })'
+    ],
+
+    invalid: [
+        {
+            code: 'it("", function () { it() });',
+            errors: [ {
+                message: 'Unexpected test nested within another test.',
+                line: 1,
+                column: 22
+            } ]
+        },
+        {
+            code: 'it.only("", function () { it() });',
+            errors: [ {
+                message: 'Unexpected test nested within another test.',
+                line: 1,
+                column: 27
+            } ]
+        },
+        {
+            code: 'it.skip("", function () { it() });',
+            errors: [ {
+                message: 'Unexpected test nested within another test.',
+                line: 1,
+                column: 27
+            } ]
+        },
+        {
+            code: 'test("", function () { it() });',
+            errors: [ {
+                message: 'Unexpected test nested within another test.',
+                line: 1,
+                column: 24
+            } ]
+        },
+        {
+            code: 'specify("", function () { it() });',
+            errors: [ {
+                message: 'Unexpected test nested within another test.',
+                line: 1,
+                column: 27
+            } ]
+        },
+        {
+            code: 'it("", function () { describe() });',
+            errors: [ {
+                message: 'Unexpected suite nested within a test.',
+                line: 1,
+                column: 22
+            } ]
+        },
+        {
+            code: 'it("", function () { context() });',
+            errors: [ {
+                message: 'Unexpected suite nested within a test.',
+                line: 1,
+                column: 22
+            } ]
+        },
+        {
+            code: 'it("", function () { suite() });',
+            errors: [ {
+                message: 'Unexpected suite nested within a test.',
+                line: 1,
+                column: 22
+            } ]
+        },
+        {
+            code: 'it("", function () { describe.skip() });',
+            errors: [ {
+                message: 'Unexpected suite nested within a test.',
+                line: 1,
+                column: 22
+            } ]
+        },
+        {
+            code: 'it("", function () { describe("", function () { it(); it(); }); });',
+            errors: [
+                {
+                    message: 'Unexpected suite nested within a test.',
+                    line: 1,
+                    column: 22
+                },
+                {
+                    message: 'Unexpected test nested within another test.',
+                    line: 1,
+                    column: 49
+                },
+                {
+                    message: 'Unexpected test nested within another test.',
+                    line: 1,
+                    column: 55
+                }
+            ]
+        }
+    ]
+});