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

Issue:
Rule proposal: forbid top level tests 
Tests should be always inside of a describe block. 

Pull request description:
Implement new rule no-global-tests
Fixes #36 

Patch:
diff --git a/docs/rules/README.md b/docs/rules/README.md
index 91ad829..5cd4ab2 100644
--- a/docs/rules/README.md
+++ b/docs/rules/README.md
@@ -3,3 +3,4 @@
 * [no-exclusive-tests](no-exclusive-tests.md) - disallow exclusive mocha tests (fixable)
 * [handle-done-callback](handle-done-callback.md) - enforces handling of callbacks for async tests
 * [no-synchronous-tests](no-synchronous-tests.md) - disallow synchronous tests
+* [no-global-tests](no-global-tests.md) - disallow global tests
diff --git a/index.js b/index.js
index af4cbdf..ddbd4fc 100644
--- a/index.js
+++ b/index.js
@@ -4,6 +4,7 @@ module.exports = {
     rules: {
         'no-exclusive-tests': require('./lib/rules/no-exclusive-tests'),
         'handle-done-callback': require('./lib/rules/handle-done-callback'),
-        'no-synchronous-tests': require('./lib/rules/no-synchronous-tests')
+        'no-synchronous-tests': require('./lib/rules/no-synchronous-tests'),
+        'no-global-tests': require('./lib/rules/no-global-tests')
     }
 };


Test patch:
diff --git a/docs/rules/no-global-tests.md b/docs/rules/no-global-tests.md
new file mode 100644
index 0000000..2e0a801
--- /dev/null
+++ b/docs/rules/no-global-tests.md
@@ -0,0 +1,60 @@
+# Disallow global tests (no-global-tests)
+
+Mocha gives you the possibility to structure your tests inside of suites using `describe`, `suite` or `context`.
+
+Example:
+
+```js
+describe('something', function () {
+    it('should work', function () {});
+});
+```
+
+This rule aims to prevent writing tests outside of test-suites:
+
+```js
+it('should work', function () {});
+```
+
+## Rule Details
+
+This rule checks each mocha test function to not be located directly in the global scope.
+
+The following patterns are considered problems:
+
+```js
+it('foo');
+
+test('bar');
+
+it.only('foo');
+
+test.skip('bar');
+```
+
+These patterns would not be considered problems:
+
+```js
+describe('foo', function () {
+    it('bar');
+});
+
+suite('foo', function () {
+    test('bar');
+});
+```
+
+## Caveats
+
+It is not always possible to statically detect in which scope a mocha test function will be used at runtime.
+For example imagine a parameterized test which is generated inside of a `forEach` callback:
+
+```js
+function parameterizedTest(params) {
+    params.forEach(function (i) {
+        it('should work with ' + i, function () {});
+    });
+}
+
+parameterizedTest([1,2,3]);
+```
diff --git a/lib/rules/no-global-tests.js b/lib/rules/no-global-tests.js
new file mode 100644
index 0000000..1291d3a
--- /dev/null
+++ b/lib/rules/no-global-tests.js
@@ -0,0 +1,40 @@
+'use strict';
+
+module.exports = function (context) {
+    var testFunctionNames = [
+            'it',
+            'it.only',
+            'it.skip',
+            'test',
+            'test.only',
+            'test.skip'
+        ];
+
+    function getFnName(callee) {
+        if (callee.type === 'MemberExpression') {
+            if (callee.computed) {
+                return callee.object.name + '.' + callee.property.value;
+            }
+
+            return callee.object.name + '.' + callee.property.name;
+        }
+
+        return callee.name;
+    }
+
+    function isGlobalScope(scope) {
+        return scope.type === 'global' || scope.type === 'module';
+    }
+
+    return {
+        CallExpression: function (node) {
+            var callee = node.callee,
+                fnName = getFnName(callee),
+                scope = context.getScope();
+
+            if (testFunctionNames.indexOf(fnName) !== -1 && isGlobalScope(scope)) {
+                context.report(callee, 'Unexpected global mocha test.');
+            }
+        }
+    };
+};
diff --git a/test/rules/no-global-tests.js b/test/rules/no-global-tests.js
new file mode 100644
index 0000000..9873cac
--- /dev/null
+++ b/test/rules/no-global-tests.js
@@ -0,0 +1,94 @@
+'use strict';
+
+var RuleTester = require('eslint').RuleTester,
+    rule = require('../../lib/rules/no-global-tests'),
+    ruleTester = new RuleTester(),
+    expectedErrorMessage = 'Unexpected global mocha test.';
+
+ruleTester.run('no-global-tests', rule, {
+
+    valid: [
+        'describe();',
+        'suite();',
+        'context();',
+        'describe("", function () { it(); });',
+        'describe("", function () { it.only(); });',
+        'describe("", function () { it.skip(); });',
+        'describe("", function () { test();  });',
+        'describe("", function () { test.only(); });',
+        'describe("", function () { test.skip(); });',
+        'describe.only("", function () { it(); });',
+        'describe.skip("", function () { it(); });',
+        'suite("", function () { it(); });',
+        'suite("", function () { test(); });',
+        'suite.only("", function () { it(); });',
+        'suite.skip("", function () { it(); });',
+        'context("", function () { it(); });',
+        'context("", function () { test(); });',
+        'context.only("", function () { it(); });',
+        'context.skip("", function () { it(); });',
+        'something("", function () { describe("", function () { it(); }); });',
+        'something("", function () { it(); });',
+        'something("", function () { test(); } );',
+        '[1,2,3].forEach(function () { it(); });',
+        {
+            code: 'import foo from "bar"; describe("", () => it());',
+            ecmaFeatures: {
+                modules: true,
+                arrowFunctions: true
+            }
+        }
+    ],
+
+    invalid: [
+        {
+            code: 'it();',
+            errors: [ { message: expectedErrorMessage, column: 1, line: 1 } ]
+        },
+        {
+            code: 'it.only();',
+            errors: [ { message: expectedErrorMessage, column: 1, line: 1 } ]
+        },
+        {
+            code: 'it["only"]();',
+            errors: [ { message: expectedErrorMessage, column: 1, line: 1 } ]
+        },
+        {
+            code: 'it.skip();',
+            errors: [ { message: expectedErrorMessage, column: 1, line: 1 } ]
+        },
+        {
+            code: 'it["skip"]();',
+            errors: [ { message: expectedErrorMessage, column: 1, line: 1 } ]
+        },
+        {
+            code: 'test();',
+            errors: [ { message: expectedErrorMessage, column: 1, line: 1 } ]
+        },
+        {
+            code: 'test.only();',
+            errors: [ { message: expectedErrorMessage, column: 1, line: 1 } ]
+        },
+        {
+            code: 'test["only"]();',
+            errors: [ { message: expectedErrorMessage, column: 1, line: 1 } ]
+        },
+        {
+            code: 'test.skip();',
+            errors: [ { message: expectedErrorMessage, column: 1, line: 1 } ]
+        },
+        {
+            code: 'test["skip"]();',
+            errors: [ { message: expectedErrorMessage, column: 1, line: 1 } ]
+        },
+        {
+            code: 'import foo from "bar"; it("");',
+            ecmaFeatures: {
+                modules: true
+            },
+            errors: [ { message: expectedErrorMessage, column: 24, line: 1 } ]
+        }
+
+    ]
+
+});