Repository: eslint/eslint (js)

Issue:
eslint-disable-next-line works in shebang comments
<!--
    ESLint adheres to the [JS Foundation Code of Conduct](https://js.foundation/community/code-of-conduct).

    This template is for bug reports. If you are reporting a bug, please continue on. If you are here for another reason, please see below:

    1. To propose a new rule: http://eslint.org/docs/developer-guide/contributing/new-rules
    2. To request a change: http://eslint.org/docs/developer-guide/contributing/changes
    3. If you have any questions, please stop by our chatroom: https://gitter.im/eslint/eslint

    Note that leaving sections blank will make it difficult for us to troubleshoot and we may have to close the issue.
-->

**Tell us about your environment**

* **ESLint Version:** 4.6.1
* **Node Version:** 8.4.0
* **npm Version:** 5.3.0

**What parser (default, Babel-ESLint, etc.) are you using?**

default

**Please show your full configuration:**

<details>
<summary>Configuration</summary>

<!-- Paste your configuration below: -->
```js
module.exports = {
    rules: {
        no-undef: "error"
    }
};
```

</details>

**What did you do? Please include the actual source code causing the issue.**

<!-- Paste the source code below: -->
```js
#! eslint-disable-next-line
foo;
```

**What did you expect to happen?**

I expected an error to be reported, because [`eslint-disable-next-line`](https://eslint.org/docs/user-guide/configuring#disabling-rules-with-inline-comments) is only supposed to work when using line comments.

**What actually happened? Please include the actual, raw output from ESLint.**

No error was reported, because ESLint considered the shebang to be a valid `eslint-disable-next-line` comment.

It looks like the issue is occurring as part of the inline comment processing [here](https://github.com/eslint/eslint/blob/3c41a053026b502bc8ec3890e526f45129c84ebd/lib/linter.js#L339-L399). In a previous version of ESLint, `ast.comments` did not contain shebangs, so it was safe for that logic to assume that all comments in the array had a type of either `Block` or `Line`. However, `ast.comments` now includes shebang comments (they have type `Shebang`), so this is no longer a safe assumption.

There are a few different ways that this could be fixed:

* Filter out shebangs before doing any processing on them.
* Make the logic explicitly check that a comment has type `Line` before assuming that it's a line comment.

Pull request description:
Fix: shebang error in eslint-disable-new-line; add tests (fixes #9238)
<!--
    ESLint adheres to the [JS Foundation Code of Conduct](https://js.foundation/community/code-of-conduct).
-->

**What is the purpose of this pull request? (put an "X" next to item)**

[ ] Documentation update
[X] Bug fix ([template](https://raw.githubusercontent.com/eslint/eslint/master/templates/bug-report.md))
[ ] New rule ([template](https://raw.githubusercontent.com/eslint/eslint/master/templates/rule-proposal.md))
[ ] Changes an existing rule ([template](https://raw.githubusercontent.com/eslint/eslint/master/templates/rule-change-proposal.md))
[ ] Add autofixing to a rule
[ ] Add a CLI option
[ ] Add something to the core
[ ] Other, please explain:

<!--
    https://github.com/eslint/eslint/issues/9238
-->

<!--
    Please ensure your pull request is ready:

    - Read the pull request guide (http://eslint.org/docs/developer-guide/contributing/pull-requests)
    - Include tests for this change
    - Update documentation for this change (if appropriate)
-->

<!--
    The following is required for all pull requests:
-->

**What changes did you make? (Give an overview)**
I filtered out shebang comments before comments are processed for the function `modifyConfigsFromComments` in linter.js. I also added additional tests to the linter.js test file.

**Is there anything you'd like reviewers to focus on?**
N/A



Patch:
diff --git a/docs/rules/sort-imports.md b/docs/rules/sort-imports.md
index c1b019546..c7c56e3aa 100644
--- a/docs/rules/sort-imports.md
+++ b/docs/rules/sort-imports.md
@@ -62,8 +62,8 @@ Examples of **correct** code for this rule when using default options:
 ```js
 /*eslint sort-imports: "error"*/
 import 'module-without-export.js';
-import * as foo from 'foo.js';
 import * as bar from 'bar.js';
+import * as foo from 'foo.js';
 import {alpha, beta} from 'alpha.js';
 import {delta, gamma} from 'delta.js';
 import a from 'baz.js';
@@ -76,7 +76,7 @@ import c from 'baz.js';
 
 /*eslint sort-imports: "error"*/
 import 'foo.js'
-import * from 'bar.js';
+import * as bar from 'bar.js';
 import {a, b} from 'baz.js';
 import c from 'qux.js';
 
diff --git a/lib/linter.js b/lib/linter.js
index 76487c53e..a9bf2bf25 100755
--- a/lib/linter.js
+++ b/lib/linter.js
@@ -18,6 +18,7 @@ const EventEmitter = require("events").EventEmitter,
     ConfigOps = require("./config/config-ops"),
     validator = require("./config/config-validator"),
     Environments = require("./config/environments"),
+    applyDisableDirectives = require("./util/apply-disable-directives"),
     NodeEventGenerator = require("./util/node-event-generator"),
     SourceCode = require("./util/source-code"),
     Traverser = require("./util/traverser"),
@@ -249,68 +250,23 @@ function addDeclaredGlobals(program, globalScope, config, envContext) {
 }
 
 /**
- * Add data to reporting configuration to disable reporting for list of rules
- * starting from start location
- * @param  {Object[]} reportingConfig Current reporting configuration
- * @param  {Object} start Position to start
- * @param  {string[]} rulesToDisable List of rules
- * @returns {void}
- */
-function disableReporting(reportingConfig, start, rulesToDisable) {
-
-    if (rulesToDisable.length) {
-        rulesToDisable.forEach(rule => {
-            reportingConfig.push({
-                start,
-                end: null,
-                rule
-            });
-        });
-    } else {
-        reportingConfig.push({
-            start,
-            end: null,
-            rule: null
-        });
-    }
-}
-
-/**
- * Add data to reporting configuration to enable reporting for list of rules
- * starting from start location
- * @param  {Object[]} reportingConfig Current reporting configuration
- * @param  {Object} start Position to start
- * @param  {string[]} rulesToEnable List of rules
- * @returns {void}
+ * Creates a collection of disable directives from a comment
+ * @param {("disable"|"enable"|"disable-line"|"disable-next-line")} type The type of directive comment
+ * @param {{line: number, column: number}} loc The 0-based location of the comment token
+ * @param {string} value The value after the directive in the comment
+ * comment specified no specific rules, so it applies to all rules (e.g. `eslint-disable`)
+ * @returns {{
+ *     type: ("disable"|"enable"|"disable-line"|"disable-next-line"),
+ *     line: number,
+ *     column: number,
+ *     ruleId: (string|null)
+ * }[]} Directives from the comment
  */
-function enableReporting(reportingConfig, start, rulesToEnable) {
-    let i;
-
-    if (rulesToEnable.length) {
-        rulesToEnable.forEach(rule => {
-            for (i = reportingConfig.length - 1; i >= 0; i--) {
-                if (!reportingConfig[i].end && reportingConfig[i].rule === rule) {
-                    reportingConfig[i].end = start;
-                    break;
-                }
-            }
-        });
-    } else {
+function createDisableDirectives(type, loc, value) {
+    const ruleIds = Object.keys(parseListConfig(value));
+    const directiveRules = ruleIds.length ? ruleIds : [null];
 
-        // find all previous disabled locations if they was started as list of rules
-        let prevStart;
-
-        for (i = reportingConfig.length - 1; i >= 0; i--) {
-            if (prevStart && prevStart !== reportingConfig[i].start) {
-                break;
-            }
-
-            if (!reportingConfig[i].end) {
-                reportingConfig[i].end = start;
-                prevStart = reportingConfig[i].start;
-            }
-        }
-    }
+    return directiveRules.map(ruleId => ({ type, line: loc.line, column: loc.column + 1, ruleId }));
 }
 
 /**
@@ -321,7 +277,16 @@ function enableReporting(reportingConfig, start, rulesToEnable) {
  * @param {ASTNode} ast The top node of the AST.
  * @param {Object} config The existing configuration data.
  * @param {Linter} linterContext Linter context object
- * @returns {{config: Object, problems: Problem[]}} Modified config object, along with any problems encountered
+ * @returns {{
+ *      config: Object,
+ *      problems: Problem[],
+ *      disableDirectives: {
+ *          type: ("disable"|"enable"|"disable-line"|"disable-next-line"),
+ *          line: number,
+ *          column: number,
+ *          ruleId: (string|null)
+ *      }[]
+ * }} Modified config object, along with any problems encountered
  * while parsing config comments
  */
 function modifyConfigsFromComments(filename, ast, config, linterContext) {
@@ -334,9 +299,9 @@ function modifyConfigsFromComments(filename, ast, config, linterContext) {
     };
     const commentRules = {};
     const problems = [];
-    const reportingConfig = linterContext.reportingConfig;
+    const disableDirectives = [];
 
-    ast.comments.forEach(comment => {
+    ast.comments.filter(token => token.type !== "Shebang").forEach(comment => {
 
         let value = comment.value.trim();
         const match = /^(eslint(-\w+){0,3}|exported|globals?)(\s|$)/.exec(value);
@@ -360,11 +325,11 @@ function modifyConfigsFromComments(filename, ast, config, linterContext) {
                         break;
 
                     case "eslint-disable":
-                        disableReporting(reportingConfig, comment.loc.start, Object.keys(parseListConfig(value)));
+                        [].push.apply(disableDirectives, createDisableDirectives("disable", comment.loc.start, value));
                         break;
 
                     case "eslint-enable":
-                        enableReporting(reportingConfig, comment.loc.start, Object.keys(parseListConfig(value)));
+                        [].push.apply(disableDirectives, createDisableDirectives("enable", comment.loc.start, value));
                         break;
 
                     case "eslint": {
@@ -388,11 +353,9 @@ function modifyConfigsFromComments(filename, ast, config, linterContext) {
                 }
             } else { // comment.type === "Line"
                 if (match[1] === "eslint-disable-line") {
-                    disableReporting(reportingConfig, { line: comment.loc.start.line, column: 0 }, Object.keys(parseListConfig(value)));
-                    enableReporting(reportingConfig, comment.loc.end, Object.keys(parseListConfig(value)));
+                    [].push.apply(disableDirectives, createDisableDirectives("disable-line", comment.loc.start, value));
                 } else if (match[1] === "eslint-disable-next-line") {
-                    disableReporting(reportingConfig, comment.loc.start, Object.keys(parseListConfig(value)));
-                    enableReporting(reportingConfig, { line: comment.loc.start.line + 2 }, Object.keys(parseListConfig(value)));
+                    [].push.apply(disableDirectives, createDisableDirectives("disable-next-line", comment.loc.start, value));
                 }
             }
         }
@@ -410,33 +373,11 @@ function modifyConfigsFromComments(filename, ast, config, linterContext) {
 
     return {
         config: ConfigOps.merge(config, commentConfig),
-        problems
+        problems,
+        disableDirectives
     };
 }
 
-/**
- * Check if message of rule with ruleId should be ignored in location
- * @param  {Object[]} reportingConfig  Collection of ignore records
- * @param  {string} ruleId   Id of rule
- * @param  {Object} location 1-indexed location of message
- * @returns {boolean}          True if message should be ignored, false otherwise
- */
-function isDisabledByReportingConfig(reportingConfig, ruleId, location) {
-
-    for (let i = 0, c = reportingConfig.length; i < c; i++) {
-
-        const ignore = reportingConfig[i];
-
-        if ((!ignore.rule || ignore.rule === ruleId) &&
-            (location.line > ignore.start.line || (location.line === ignore.start.line && location.column > ignore.start.column)) &&
-            (!ignore.end || (location.line < ignore.end.line || (location.line === ignore.end.line && location.column - 1 <= ignore.end.column)))) {
-            return true;
-        }
-    }
-
-    return false;
-}
-
 /**
  * Normalize ECMAScript version from the initial config
  * @param  {number} ecmaVersion ECMAScript version from the initial config
@@ -705,7 +646,6 @@ class Linter {
         this.currentConfig = null;
         this.scopeManager = null;
         this.traverser = null;
-        this.reportingConfig = [];
         this.sourceCode = null;
         this.version = pkg.version;
 
@@ -721,7 +661,6 @@ class Linter {
         this.currentConfig = null;
         this.scopeManager = null;
         this.traverser = null;
-        this.reportingConfig = [];
         this.sourceCode = null;
     }
 
@@ -822,6 +761,7 @@ class Linter {
 
         const problems = [];
         const sourceCode = this.sourceCode;
+        let disableDirectives;
 
         // parse global comments and modify config
         if (allowInlineConfig !== false) {
@@ -829,6 +769,9 @@ class Linter {
 
             config = modifyConfigResult.config;
             modifyConfigResult.problems.forEach(problem => problems.push(problem));
+            disableDirectives = modifyConfigResult.disableDirectives;
+        } else {
+            disableDirectives = [];
         }
 
         const emitter = new EventEmitter().setMaxListeners(Infinity);
@@ -985,9 +928,10 @@ class Linter {
             }
         });
 
-        return problems
-            .filter(problem => !isDisabledByReportingConfig(this.reportingConfig, problem.ruleId, problem))
-            .sort((problemA, problemB) => problemA.line - problemB.line || problemA.column - problemB.column);
+        return applyDisableDirectives({
+            directives: disableDirectives,
+            problems: problems.sort((problemA, problemB) => problemA.line - problemB.line || problemA.column - problemB.column)
+        });
     }
 
     /**
@@ -1095,14 +1039,6 @@ class Linter {
         });
     }
 
-    /**
-     * Gets the default eslint configuration.
-     * @returns {Object} Object mapping rule IDs to their default configurations
-     */
-    defaults() { // eslint-disable-line class-methods-use-this
-        return defaultConfig;
-    }
-
     /**
      * Gets an object with all loaded rules.
      * @returns {Map} All loaded rules
diff --git a/lib/util/apply-disable-directives.js b/lib/util/apply-disable-directives.js
new file mode 100644
index 000000000..d9d6374e6
--- /dev/null
+++ b/lib/util/apply-disable-directives.js
@@ -0,0 +1,115 @@
+/**
+ * @fileoverview A module that filters reported problems based on `eslint-disable` and `eslint-enable` comments
+ * @author Teddy Katz
+ */
+
+"use strict";
+
+const lodash = require("lodash");
+
+/**
+ * Compares the locations of two objects in a source file
+ * @param {{line: number, column: number}} itemA The first object
+ * @param {{line: number, column: number}} itemB The second object
+ * @returns {number} A value less than 1 if itemA appears before itemB in the source file, greater than 1 if
+ * itemA appears after itemB in the source file, or 0 if itemA and itemB have the same location.
+ */
+function compareLocations(itemA, itemB) {
+    return itemA.line - itemB.line || itemA.column - itemB.column;
+}
+
+/**
+ * Given a list of directive comments (i.e. metadata about eslint-disable and eslint-enable comments) and a list
+ * of reported problems, determines which problems should be reported.
+ * @param {Object} options Information about directives and problems
+ * @param {{
+ *      type: ("disable"|"enable"|"disable-line"|"disable-next-line"),
+ *      ruleId: (string|null),
+ *      line: number,
+ *      column: number
+ * }} options.directives Directive comments found in the file, with one-based columns.
+ * Two directive comments can only have the same location if they also have the same type (e.g. a single eslint-disable
+ * comment for two different rules is represented as two directives).
+ * @param {{ruleId: (string|null), line: number, column: number}[]} options.problems
+ * A list of problems reported by rules, sorted by increasing location in the file, with one-based columns.
+ * @returns {{ruleId: (string|null), line: number, column: number}[]}
+ * A list of reported problems that were not disabled by the directive comments.
+ */
+module.exports = options => {
+    const processedDirectives = lodash.flatMap(options.directives, directive => {
+        switch (directive.type) {
+            case "disable":
+            case "enable":
+                return [directive];
+
+            case "disable-line":
+                return [
+                    { type: "disable", line: directive.line, column: 1, ruleId: directive.ruleId },
+                    { type: "enable", line: directive.line + 1, column: 1, ruleId: directive.ruleId }
+                ];
+
+            case "disable-next-line":
+                return [
+                    { type: "disable", line: directive.line + 1, column: 1, ruleId: directive.ruleId },
+                    { type: "enable", line: directive.line + 2, column: 1, ruleId: directive.ruleId }
+                ];
+
+            default:
+                throw new TypeError(`Unrecognized directive type '${directive.type}'`);
+        }
+    }).sort(compareLocations);
+
+    const problems = [];
+    let nextDirectiveIndex = 0;
+    let globalDisableActive = false;
+
+    // disabledRules is only used when there is no active global /* eslint-disable */ comment.
+    const disabledRules = new Set();
+
+    // enabledRules is only used when there is an active global /* eslint-disable */ comment.
+    const enabledRules = new Set();
+
+    for (const problem of options.problems) {
+        while (
+            nextDirectiveIndex < processedDirectives.length &&
+            compareLocations(processedDirectives[nextDirectiveIndex], problem) <= 0
+        ) {
+            const directive = processedDirectives[nextDirectiveIndex++];
+
+            switch (directive.type) {
+                case "disable":
+                    if (directive.ruleId === null) {
+                        globalDisableActive = true;
+                        enabledRules.clear();
+                    } else if (globalDisableActive) {
+                        enabledRules.delete(directive.ruleId);
+                    } else {
+                        disabledRules.add(directive.ruleId);
+                    }
+                    break;
+
+                case "enable":
+                    if (directive.ruleId === null) {
+                        globalDisableActive = false;
+                        disabledRules.clear();
+                    } else if (globalDisableActive) {
+                        enabledRules.add(directive.ruleId);
+                    } else {
+                        disabledRules.delete(directive.ruleId);
+                    }
+                    break;
+
+                // no default
+            }
+        }
+
+        if (
+            globalDisableActive && enabledRules.has(problem.ruleId) ||
+            !globalDisableActive && !disabledRules.has(problem.ruleId)
+        ) {
+            problems.push(problem);
+        }
+    }
+
+    return problems;
+};


Test patch:
diff --git a/tests/lib/cli-engine.js b/tests/lib/cli-engine.js
index 419df9eef..ea564ca00 100644
--- a/tests/lib/cli-engine.js
+++ b/tests/lib/cli-engine.js
@@ -19,12 +19,8 @@ const assert = require("chai").assert,
     os = require("os"),
     hash = require("../../lib/util/hash");
 
-require("shelljs/global");
-
 const proxyquire = require("proxyquire").noCallThru().noPreserveCache();
 
-/* global mkdir, rm, cp */
-
 //------------------------------------------------------------------------------
 // Tests
 //------------------------------------------------------------------------------
@@ -87,8 +83,8 @@ describe("CLIEngine", () => {
     // copy into clean area so as not to get "infected" by this project's .eslintrc files
     before(() => {
         fixtureDir = path.join(os.tmpdir(), "/eslint/fixtures");
-        mkdir("-p", fixtureDir);
-        cp("-r", "./tests/fixtures/.", fixtureDir);
+        shell.mkdir("-p", fixtureDir);
+        shell.cp("-r", "./tests/fixtures/.", fixtureDir);
         fixtureDir = fs.realpathSync(fixtureDir);
     });
 
@@ -97,7 +93,7 @@ describe("CLIEngine", () => {
     });
 
     after(() => {
-        rm("-r", fixtureDir);
+        shell.rm("-r", fixtureDir);
     });
 
     describe("new CLIEngine(options)", () => {
diff --git a/tests/lib/linter.js b/tests/lib/linter.js
index 92d7fa8c7..d3f665315 100644
--- a/tests/lib/linter.js
+++ b/tests/lib/linter.js
@@ -908,12 +908,8 @@ describe("Linter", () => {
         }
 
         it("should pass parser as parserPath to all rules when default parser is used", () => {
-
-            const DEFAULT_PARSER = linter.defaults().parser;
-
-            linter.reset();
             linter.defineRule("test-rule", sandbox.mock().withArgs(
-                sinon.match({ parserPath: DEFAULT_PARSER })
+                sinon.match({ parserPath: "espree" })
             ).returns({}));
 
             const config = { rules: { "test-rule": 2 } };
@@ -1671,7 +1667,7 @@ describe("Linter", () => {
             assert.equal(messages[1].column, 19);
         });
 
-        it("should not report a violation", () => {
+        it("should report a violation", () => {
 
             const code = [
                 "/*eslint-disable */",
@@ -1687,7 +1683,7 @@ describe("Linter", () => {
 
             const messages = linter.verify(code, config, filename);
 
-            assert.equal(messages.length, 0);
+            assert.equal(messages.length, 1);
         });
 
 
@@ -1873,6 +1869,26 @@ describe("Linter", () => {
                 assert.equal(messages[0].ruleId, "no-console");
             });
 
+            it("should ignore violations of only the specified rule on next line", () => {
+                const code = [
+                    "// eslint-disable-next-line quotes",
+                    "alert(\"test\");",
+                    "console.log('test');"
+                ].join("\n");
+                const config = {
+                    rules: {
+                        "no-alert": 1,
+                        quotes: [1, "single"],
+                        "no-console": 1
+                    }
+                };
+                const messages = linter.verify(code, config, filename);
+
+                assert.equal(messages.length, 2);
+                assert.equal(messages[0].ruleId, "no-alert");
+                assert.equal(messages[1].ruleId, "no-console");
+            });
+
             it("should ignore violations of specified rule on next line only", () => {
                 const code = [
                     "alert('test');",
@@ -1913,7 +1929,7 @@ describe("Linter", () => {
                 assert.equal(messages[0].ruleId, "no-console");
             });
 
-            it("should not report if comment is in block quotes", () => {
+            it("should not ignore violations if comment is in block quotes", () => {
                 const code = [
                     "alert('test');",
                     "/* eslint-disable-next-line no-alert */",
@@ -1933,6 +1949,25 @@ describe("Linter", () => {
                 assert.equal(messages[1].ruleId, "no-alert");
                 assert.equal(messages[2].ruleId, "no-console");
             });
+
+            it("should not ignore violations if comment is of the type Shebang", () => {
+                const code = [
+                    "#! eslint-disable-next-line no-alert",
+                    "alert('test');",
+                    "console.log('test');"
+                ].join("\n");
+                const config = {
+                    rules: {
+                        "no-alert": 1,
+                        "no-console": 1
+                    }
+                };
+                const messages = linter.verify(code, config, filename);
+
+                assert.equal(messages.length, 2);
+                assert.equal(messages[0].ruleId, "no-alert");
+                assert.equal(messages[1].ruleId, "no-console");
+            });
         });
     });
 
@@ -2024,7 +2059,7 @@ describe("Linter", () => {
                 "console.log('test');",
                 "/*eslint-enable */",
 
-                "alert('test');",
+                "alert('test');", // here
                 "console.log('test');", // here
 
                 "/*eslint-enable */",
@@ -2038,16 +2073,19 @@ describe("Linter", () => {
 
             const messages = linter.verify(code, config, filename);
 
-            assert.equal(messages.length, 3);
+            assert.equal(messages.length, 4);
 
-            assert.equal(messages[0].ruleId, "no-console");
-            assert.equal(messages[0].line, 7);
+            assert.equal(messages[0].ruleId, "no-alert");
+            assert.equal(messages[0].line, 6);
 
-            assert.equal(messages[1].ruleId, "no-alert");
-            assert.equal(messages[1].line, 9);
+            assert.equal(messages[1].ruleId, "no-console");
+            assert.equal(messages[1].line, 7);
 
-            assert.equal(messages[2].ruleId, "no-console");
-            assert.equal(messages[2].line, 10);
+            assert.equal(messages[2].ruleId, "no-alert");
+            assert.equal(messages[2].line, 9);
+
+            assert.equal(messages[3].ruleId, "no-console");
+            assert.equal(messages[3].line, 10);
 
         });
 
@@ -2389,14 +2427,6 @@ describe("Linter", () => {
         });
     });
 
-    describe("when calling defaults", () => {
-        it("should return back config object", () => {
-            const config = linter.defaults();
-
-            assert.isNotNull(config.rules);
-        });
-    });
-
     describe("when calling getRules", () => {
         it("should return all loaded rules", () => {
             const rules = linter.getRules();
diff --git a/tests/lib/util/apply-disable-directives.js b/tests/lib/util/apply-disable-directives.js
new file mode 100644
index 000000000..9b8d26926
--- /dev/null
+++ b/tests/lib/util/apply-disable-directives.js
@@ -0,0 +1,395 @@
+/**
+ * @fileoverview Tests for filter-by-disable-comments
+ * @author Teddy Katz
+ */
+
+"use strict";
+
+const assert = require("chai").assert;
+const applyDisableDirectives = require("../../../lib/util/apply-disable-directives");
+
+describe("comment-reporting-config", () => {
+    describe("/* eslint-disable */ comments without rules", () => {
+        it("keeps problems before the comment on the same line", () => {
+            assert.deepEqual(
+                applyDisableDirectives({
+                    directives: [{ type: "disable", line: 1, column: 8, ruleId: null }],
+                    problems: [{ line: 1, column: 7, ruleId: "foo" }]
+                }),
+                [{ ruleId: "foo", line: 1, column: 7 }]
+            );
+        });
+
+        it("keeps problems on a previous line before the comment", () => {
+            assert.deepEqual(
+                applyDisableDirectives({
+                    directives: [{ type: "disable", line: 2, column: 8, ruleId: null }],
+                    problems: [{ line: 1, column: 10, ruleId: "foo" }]
+                }),
+                [{ ruleId: "foo", line: 1, column: 10 }]
+            );
+        });
+
+        it("filters problems at the same location as the comment", () => {
+            assert.deepEqual(
+                applyDisableDirectives({
+                    directives: [{ type: "disable", line: 1, column: 8, ruleId: null }],
+                    problems: [{ line: 1, column: 8, ruleId: null }]
+                }),
+                []
+            );
+        });
+
+        it("filters out problems after the comment on the same line", () => {
+            assert.deepEqual(
+                applyDisableDirectives({
+                    directives: [{ type: "disable", line: 1, column: 8, ruleId: null }],
+                    problems: [{ line: 1, column: 10, ruleId: "foo" }]
+                }),
+                []
+            );
+        });
+
+        it("filters out problems on a later line than the comment", () => {
+            assert.deepEqual(
+                applyDisableDirectives({
+                    directives: [{ type: "disable", line: 1, column: 8, ruleId: null }],
+                    problems: [{ line: 2, column: 3, ruleId: "foo" }]
+                }),
+                []
+            );
+        });
+    });
+
+    describe("/* eslint-disable */ comments with rules", () => {
+        it("filters problems after the comment that have the same ruleId", () => {
+            assert.deepEqual(
+                applyDisableDirectives({
+                    directives: [{ type: "disable", line: 1, column: 8, ruleId: "foo" }],
+                    problems: [{ line: 2, column: 3, ruleId: "foo" }]
+                }),
+                []
+            );
+        });
+
+        it("filters problems in the same location as the comment that have the same ruleId", () => {
+            assert.deepEqual(
+                applyDisableDirectives({
+                    directives: [{ type: "disable", line: 1, column: 8, ruleId: "foo" }],
+                    problems: [{ line: 1, column: 8, ruleId: "foo" }]
+                }),
+                []
+            );
+        });
+
+        it("keeps problems after the comment that have a different ruleId", () => {
+            assert.deepEqual(
+                applyDisableDirectives({
+                    directives: [{ type: "disable", line: 1, column: 8, ruleId: "foo" }],
+                    problems: [{ line: 2, column: 3, ruleId: "not-foo" }]
+                }),
+                [{ line: 2, column: 3, ruleId: "not-foo" }]
+            );
+        });
+
+        it("keeps problems before the comment that have the same ruleId", () => {
+            assert.deepEqual(
+                applyDisableDirectives({
+                    directives: [{ type: "disable", line: 1, column: 8, ruleId: "foo" }],
+                    problems: [{ line: 1, column: 7, ruleId: "foo" }]
+                }),
+                [{ line: 1, column: 7, ruleId: "foo" }]
+            );
+        });
+    });
+
+    describe("eslint-enable comments without rules", () => {
+        it("keeps problems after the eslint-enable comment", () => {
+            assert.deepEqual(
+                applyDisableDirectives({
+                    directives: [
+                        { type: "disable", line: 1, column: 1, ruleId: null },
+                        { type: "enable", line: 1, column: 5, ruleId: null }
+                    ],
+                    problems: [{ line: 1, column: 7, ruleId: "foo" }]
+                }),
+                [{ line: 1, column: 7, ruleId: "foo" }]
+            );
+        });
+
+        it("keeps problems in the same location as the eslint-enable comment", () => {
+            assert.deepEqual(
+                applyDisableDirectives({
+                    directives: [
+                        { type: "disable", line: 1, column: 1, ruleId: null },
+                        { type: "enable", line: 1, column: 5, ruleId: null }
+                    ],
+                    problems: [{ line: 1, column: 5, ruleId: "foo" }]
+                }),
+                [{ line: 1, column: 5, ruleId: "foo" }]
+            );
+        });
+
+        it("filters out problems before the eslint-enable comment", () => {
+            assert.deepEqual(
+                applyDisableDirectives({
+                    directives: [
+                        { type: "disable", line: 1, column: 1, ruleId: null },
+                        { type: "enable", line: 1, column: 5, ruleId: null }
+                    ],
+                    problems: [{ line: 1, column: 3, ruleId: "foo" }]
+                }),
+                []
+            );
+        });
+
+        it("keeps problems before the eslint-enable comment if there is no corresponding disable comment", () => {
+            assert.deepEqual(
+                applyDisableDirectives({
+                    directives: [
+                        { type: "disable", line: 1, column: 1, ruleId: "foo" },
+                        { type: "enable", line: 1, column: 5, ruleId: null }
+                    ],
+                    problems: [{ line: 1, column: 3, ruleId: "not-foo" }]
+                }),
+                [{ line: 1, column: 3, ruleId: "not-foo" }]
+            );
+        });
+    });
+
+    describe("eslint-enable comments with rules", () => {
+        it("keeps problems after the comment that have the same ruleId as the eslint-enable comment", () => {
+            assert.deepEqual(
+                applyDisableDirectives({
+                    directives: [
+                        { type: "disable", line: 1, column: 4, ruleId: null },
+                        { type: "enable", line: 2, column: 1, ruleId: "foo" }
+                    ],
+                    problems: [{ line: 2, column: 4, ruleId: "foo" }]
+                }),
+                [{ line: 2, column: 4, ruleId: "foo" }]
+            );
+        });
+
+        it("keeps problems in the same location as the comment that have the same ruleId as the eslint-enable comment", () => {
+            assert.deepEqual(
+                applyDisableDirectives({
+                    directives: [
+                        { type: "disable", line: 1, column: 4, ruleId: null },
+                        { type: "enable", line: 2, column: 1, ruleId: "foo" }
+                    ],
+                    problems: [{ line: 2, column: 1, ruleId: "foo" }]
+                }),
+                [{ line: 2, column: 1, ruleId: "foo" }]
+            );
+        });
+
+        it("filters problems after the comment that have a different ruleId as the eslint-enable comment", () => {
+            assert.deepEqual(
+                applyDisableDirectives({
+                    directives: [
+                        { type: "disable", line: 1, column: 4, ruleId: null },
+                        { type: "enable", line: 2, column: 1, ruleId: "foo" }
+                    ],
+                    problems: [{ line: 2, column: 4, ruleId: "not-foo" }]
+                }),
+                []
+            );
+        });
+
+        it("reenables reporting correctly even when followed by another enable comment", () => {
+            assert.deepEqual(
+                applyDisableDirectives({
+                    directives: [
+                        { type: "disable", line: 1, column: 1, ruleId: null },
+                        { type: "enable", line: 1, column: 3, ruleId: "foo" },
+                        { type: "enable", line: 1, column: 5, ruleId: "bar" }
+                    ],
+                    problems: [
+                        { line: 1, column: 2, ruleId: "foo" },
+                        { line: 1, column: 2, ruleId: "bar" },
+                        { line: 1, column: 4, ruleId: "foo" },
+                        { line: 1, column: 4, ruleId: "bar" },
+                        { line: 1, column: 6, ruleId: "foo" },
+                        { line: 1, column: 6, ruleId: "bar" }
+                    ]
+                }),
+                [
+                    { line: 1, column: 4, ruleId: "foo" },
+                    { line: 1, column: 6, ruleId: "foo" },
+                    { line: 1, column: 6, ruleId: "bar" }
+                ]
+            );
+        });
+    });
+
+    describe("eslint-disable-line comments without rules", () => {
+        it("keeps problems on a previous line", () => {
+            assert.deepEqual(
+                applyDisableDirectives({
+                    directives: [{ type: "disable-line", line: 2, column: 1, ruleId: null }],
+                    problems: [{ line: 1, column: 5, ruleId: "foo" }]
+                }),
+                [{ line: 1, column: 5, ruleId: "foo" }]
+            );
+        });
+
+        it("filters problems before the comment on the same line", () => {
+            assert.deepEqual(
+                applyDisableDirectives({
+                    directives: [{ type: "disable-line", line: 1, column: 5, ruleId: null }],
+                    problems: [{ line: 1, column: 1, ruleId: "foo" }]
+                }),
+                []
+            );
+        });
+
+        it("filters problems after the comment on the same line", () => {
+            assert.deepEqual(
+                applyDisableDirectives({
+                    directives: [{ type: "disable-line", line: 1, column: 5, ruleId: null }],
+                    problems: [{ line: 1, column: 10, ruleId: "foo" }]
+                }),
+                []
+            );
+        });
+
+        it("keeps problems on a following line", () => {
+            assert.deepEqual(
+                applyDisableDirectives({
+                    directives: [{ type: "disable-line", line: 1, column: 4 }],
+                    problems: [{ line: 2, column: 1, ruleId: "foo" }]
+                }),
+                [{ line: 2, column: 1, ruleId: "foo" }]
+            );
+        });
+    });
+
+    describe("eslint-disable-line comments with rules", () => {
+        it("filters problems on the current line that match the ruleId", () => {
+            assert.deepEqual(
+                applyDisableDirectives({
+                    directives: [{ type: "disable-line", line: 1, column: 4, ruleId: "foo" }],
+                    problems: [{ line: 1, column: 2, ruleId: "foo" }]
+                }),
+                []
+            );
+        });
+
+        it("keeps problems on the current line that do not match the ruleId", () => {
+            assert.deepEqual(
+                applyDisableDirectives({
+                    directives: [{ type: "disable-line", line: 1, column: 4, ruleId: "foo" }],
+                    problems: [{ line: 1, column: 2, ruleId: "not-foo" }]
+                }),
+                [{ line: 1, column: 2, ruleId: "not-foo" }]
+            );
+        });
+
+        it("filters problems on the current line that do not match the ruleId if preceded by a disable comment", () => {
+            assert.deepEqual(
+                applyDisableDirectives({
+                    directives: [
+                        { type: "disable", line: 1, column: 1, ruleId: null },
+                        { type: "disable-line", line: 1, column: 3, ruleId: "foo" }
+                    ],
+                    problems: [{ line: 1, column: 5, ruleId: "not-foo" }]
+                }),
+                []
+            );
+        });
+    });
+
+    describe("eslint-disable-next-line comments without rules", () => {
+        it("filters problems on the next line", () => {
+            assert.deepEqual(
+                applyDisableDirectives({
+                    directives: [{ type: "disable-next-line", line: 1, column: 1, ruleId: null }],
+                    problems: [{ line: 2, column: 3, ruleId: "foo" }]
+                }),
+                []
+            );
+        });
+
+        it("keeps problems on the same line", () => {
+            assert.deepEqual(
+                applyDisableDirectives({
+                    directives: [{ type: "disable-next-line", line: 1, column: 1, ruleId: null }],
+                    problems: [{ line: 1, column: 3, ruleId: "foo" }]
+                }),
+                [{ line: 1, column: 3, ruleId: "foo" }]
+            );
+        });
+
+        it("keeps problems after the next line", () => {
+            assert.deepEqual(
+                applyDisableDirectives({
+                    directives: [{ type: "disable-next-line", line: 1, column: 1, ruleId: null }],
+                    problems: [{ line: 3, column: 3, ruleId: "foo" }]
+                }),
+                [{ line: 3, column: 3, ruleId: "foo" }]
+            );
+        });
+
+        it("filters problems on the next line even if there is an eslint-enable comment on the same line", () => {
+            assert.deepEqual(
+                applyDisableDirectives({
+                    directives: [
+                        { type: "disable-next-line", line: 1, column: 1, ruleId: null },
+                        { type: "enable", line: 1, column: 5, ruleId: null }
+                    ],
+                    problems: [{ line: 2, column: 2, ruleId: "foo" }]
+                }),
+                []
+            );
+        });
+
+        it("keeps problems on the next line if there is an eslint-enable comment before the problem on the next line", () => {
+            assert.deepEqual(
+                applyDisableDirectives({
+                    directives: [
+                        { type: "disable-next-line", line: 1, column: 1, ruleId: null },
+                        { type: "enable", line: 2, column: 1, ruleId: null }
+                    ],
+                    problems: [{ line: 2, column: 2, ruleId: "foo" }]
+                }),
+                [{ line: 2, column: 2, ruleId: "foo" }]
+            );
+        });
+    });
+
+    describe("eslint-disable-next-line comments with rules", () => {
+        it("filters problems on the next line that match the ruleId", () => {
+            assert.deepEqual(
+                applyDisableDirectives({
+                    directives: [{ type: "disable-next-line", line: 1, column: 1, ruleId: "foo" }],
+                    problems: [{ line: 2, column: 1, ruleId: "foo" }]
+                }),
+                []
+            );
+        });
+
+        it("keeps problems on the next line that do not match the ruleId", () => {
+            assert.deepEqual(
+                applyDisableDirectives({
+                    directives: [{ type: "disable-next-line", line: 1, column: 1, ruleId: "foo" }],
+                    problems: [{ line: 2, column: 1, ruleId: "not-foo" }]
+                }),
+                [{ line: 2, column: 1, ruleId: "not-foo" }]
+            );
+        });
+    });
+
+    describe("unrecognized directive types", () => {
+        it("throws a TypeError when it encounters an unrecognized directive", () => {
+            assert.throws(
+                () =>
+                    applyDisableDirectives({
+                        directives: [{ type: "foo", line: 1, column: 4, ruleId: "foo" }],
+                        problems: []
+                    }),
+                "Unrecognized directive type 'foo'"
+            );
+        });
+    });
+});