Repository: sasstools/sass-lint (js)

Issue:
Feature Request: Enable Disabling Linters via Source
SCSS-lint has the ability to disable the linters within the source files

https://github.com/brigade/scss-lint#disabling-linters-via-source

This is unfortunately important for my project's workflow :)


Pull request description:
Disable linters
Ongoing work for disabling linters via source.

closes #70 

Ongoing from the work of @donabrams in #402

Everyone feel free to contribute to this branch feature/disable-linters


Patch:
diff --git a/.eslintrc b/.eslintrc
index 1353aff..236fef8 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -135,8 +135,12 @@ rules:
     - global
   valid-jsdoc:
     - 2
-    - prefer:
-        return: returns
+    -
+      requireParamDescription: true
+      requireReturnDescription: true
+      requireReturn: false
+      prefer:
+        return: "returns"
   wrap-iife: 2
   yoda:
     - 2
diff --git a/README.md b/README.md
index 5e43b27..0d3cad3 100644
--- a/README.md
+++ b/README.md
@@ -82,7 +82,7 @@ If you want to configure options, set the rule to an array, where the first item
 Here is an example configuration of a rule, where we are specifying that breaking the [indentation rule](https://github.com/sasstools/sass-lint/blob/master/docs/rules/indentation.md) should be treated as an error (its severity set to two), and setting the `size` option of the rule to 2 spaces:  
 
 ```yml
-rules: 
+rules:
   indentation:
     - 2
     -
@@ -93,6 +93,83 @@ rules:
 
 ---
 
+## Disabling Linters via Source
+
+Special comments can be used to disable and enable certain rules throughout your source files in a variety of scenarios. These can be useful when dealing with legacy code or with certain necessary code smells. You can read the documentation for this feature [here](https://github.com/sasstools/sass-lint/tree/master/docs/toggle-rules-in-src.md).
+
+Below are examples of how to use this feature:
+
+
+### Disable a rule for the entire file
+
+```scss
+// sass-lint:disable border-zero
+p {
+  border: none; // No lint reported
+}
+```
+
+### Disable more than 1 rule
+
+```scss
+// sass-lint:disable border-zero, quotes
+p {
+  border: none; // No lint reported
+  content: "hello"; // No lint reported
+}
+```
+
+### Disable a rule for a single line
+
+```scss
+p {
+  border: none; // sass-lint:disable-line border-zero
+}
+```
+
+### Disable all lints within a block (and all contained blocks)
+
+```scss
+p {
+  // sass-lint:disable-block border-zero
+  border: none; // No result reported
+}
+
+a {
+  border: none; // Failing result reported
+}
+```
+
+### Disable and enable again
+
+```scss
+// sass-lint:disable border-zero
+p {
+  border: none; // No result reported
+}
+// sass-lint:enable border-zero
+
+a {
+  border: none; // Failing result reported
+}
+```
+
+### Disable/enable all linters
+
+```scss
+// sass-lint:disable-all
+p {
+  border: none; // No result reported
+}
+// sass-lint:enable-all
+
+a {
+  border: none; // Failing result reported
+}
+```
+
+---
+
 ## CLI
 
 Sass Lint [`v1.1.0`](https://github.com/sasstools/sass-lint/releases/tag/v1.1.0) introduced the ability to run Sass Lint through a command line interface. See the [CLI Docs](https://github.com/sasstools/sass-lint/tree/master/docs/cli) for full documentation on how to use the CLI.
diff --git a/docs/toggle-rules-in-src.md b/docs/toggle-rules-in-src.md
new file mode 100644
index 0000000..05e5ba7
--- /dev/null
+++ b/docs/toggle-rules-in-src.md
@@ -0,0 +1,71 @@
+# Toggling Rules Inside Source Files
+
+For special cases where a particular lint doesn't make sense in a specific area of a file, special inline comments can be used to enable/disable linters. Some examples are provided below:
+
+## Disable a rule for the entire file
+
+```scss
+// sass-lint:disable border-zero
+p {
+  border: none; // No lint reported
+}
+```
+
+## Disable more than 1 rule
+
+```scss
+// sass-lint:disable border-zero, quotes
+p {
+  border: none; // No lint reported
+  content: "hello"; // No lint reported
+}
+```
+
+## Disable a rule for a single line
+
+```scss
+p {
+  border: none; // sass-lint:disable-line border-zero
+}
+```
+
+## Disable all lints within a block (and all contained blocks)
+
+```scss
+p {
+  // sass-lint:disable-block border-zero
+  border: none; // No result reported
+}
+
+a {
+  border: none; // Failing result reported
+}
+```
+
+## Disable and enable again
+
+```scss
+// sass-lint:disable border-zero
+p {
+  border: none; // No result reported
+}
+// sass-lint:enable border-zero
+
+a {
+  border: none; // Failing result reported
+}
+```
+
+## Disable/enable all linters
+
+```scss
+// sass-lint:disable-all
+p {
+  border: none; // No result reported
+}
+// sass-lint:enable-all
+
+a {
+  border: none; // Failing result reported
+}
+```
diff --git a/index.js b/index.js
index e222c61..8f1fae8 100644
--- a/index.js
+++ b/index.js
@@ -5,11 +5,15 @@ var slConfig = require('./lib/config'),
     exceptions = require('./lib/exceptions'),
     helpers = require('./lib/helpers'),
     slRules = require('./lib/rules'),
+    ruleToggler = require('./lib/ruleToggler'),
     glob = require('glob'),
     path = require('path'),
     fs = require('fs-extra'),
     globule = require('globule');
 
+var getToggledRules = ruleToggler.getToggledRules,
+    isResultEnabled = ruleToggler.isResultEnabled;
+
 var sassLint = function (config) {
   config = require('./lib/config')(config);
   return;
@@ -102,7 +106,9 @@ sassLint.lintText = function (file, options, configPath) {
       detects,
       results = [],
       errors = 0,
-      warnings = 0;
+      warnings = 0,
+      ruleToggles = null,
+      isEnabledFilter = null;
 
   try {
     ast = groot(file.text, file.format, file.filename);
@@ -121,8 +127,12 @@ sassLint.lintText = function (file, options, configPath) {
   }
 
   if (ast.content && ast.content.length > 0) {
+    ruleToggles = getToggledRules(ast);
+    isEnabledFilter = isResultEnabled(ruleToggles);
+
     rules.forEach(function (rule) {
-      detects = rule.rule.detect(ast, rule);
+      detects = rule.rule.detect(ast, rule)
+        .filter(isEnabledFilter);
       results = results.concat(detects);
       if (detects.length) {
         if (rule.severity === 1) {
diff --git a/lib/ruleToggler.js b/lib/ruleToggler.js
new file mode 100644
index 0000000..f4bbd52
--- /dev/null
+++ b/lib/ruleToggler.js
@@ -0,0 +1,249 @@
+'use strict';
+
+/**
+ * Adds each rule in our array of rules in a disable comment into the toggledRules object
+ * under the correct rule name along with the line and column number where the disable comment
+ * was encountered
+ *
+ * @param {Object} toggledRules - Contains the information about each rule disable/enable
+                                  encountered and and what line/column it occurred on
+ * @param {Array} rules - An array of rule names
+ * @param {number} line - The line number the disable appeared on
+ * @param {number} column - The column number the disable appeared on
+ */
+var addDisable = function (toggledRules, rules, line, column) {
+  rules.map(function (rule) {
+    toggledRules.ruleEnable[rule] = toggledRules.ruleEnable[rule] || [];
+    toggledRules.ruleEnable[rule].push([false, line, column]);
+  });
+};
+
+/**
+ * Adds each rule in our array of rules in a enable comment into the toggledRules object
+ * under the correct rule name along with the line and column number where the enable comment
+ * was encountered
+ *
+ * @param {Object} toggledRules - Contains the information about each rule enable
+                                  encountered and and what line/column it occurred on
+ * @param {Array} rules - An array of rule names
+ * @param {number} line - The line number the enable appeared on
+ * @param {number} column - The column number the enable appeared on
+ */
+var addEnable = function (toggledRules, rules, line, column) {
+  rules.map(function (rule) {
+    toggledRules.ruleEnable[rule] = toggledRules.ruleEnable[rule] || [];
+    toggledRules.ruleEnable[rule].push([true, line, column]);
+  });
+};
+
+/**
+ * Adds each rule in our array of rules in a disable block comment into the toggledRules object
+ * under the correct rule name along with the line and column number where the disable block comment
+ * was encountered
+ *
+ * @param {Object} toggledRules - Contains the information about each rule enable
+                                  encountered and and what line/column that block occurred on
+ * @param {Array} rules - An array of rule names
+ * @param {Object} block - The block that is to be disabled
+ */
+var addDisableBlock = function (toggledRules, rules, block) {
+  rules.map(function (rule) {
+    toggledRules.ruleEnable[rule] = toggledRules.ruleEnable[rule] || [];
+    toggledRules.ruleEnable[rule].push([false, block.start.line, block.start.column]);
+    toggledRules.ruleEnable[rule].push([true, block.end.line, block.end.column]);
+  });
+};
+
+/**
+ * Adds a globally disabled flag to the toggled rules globalEnable property including the line and column
+ * that this comment was encountered on.
+ *
+ * @param {Object} toggledRules - Contains the information about the global disable comment
+                                  encountered and and what line/column it occurred on
+ * @param {number} line - The line number the disable appeared on
+ * @param {number} column - The column number the disable appeared on
+ */
+var addDisableAll = function (toggledRules, line, column) {
+  toggledRules.globalEnable
+    .push([false, line, column]);
+};
+
+/**
+ * Adds a globally enabled flag to the toggled rules globalEnable property including the line and column
+ * that this comment was encountered on.
+ *
+ * @param {Object} toggledRules - Contains the information about the global enable comment
+                                  encountered and and what line/column it occurred on
+ * @param {number} line - The line number the enable appeared on
+ * @param {number} column - The column number the enable appeared on
+ */
+var addEnableAll = function (toggledRules, line, column) {
+  toggledRules.globalEnable
+    .push([true, line, column]);
+};
+
+/**
+ * Adds a line disabled flag to the ruleEnable property of the toggledRules object for each rule name
+ * encountered in the comment and which line this comment was discovered on / refers to
+ *
+ * @param {Object} toggledRules - Contains the information about the line disable comment encountered, the rules
+ *                              it relates to and which line it was encountered on
+ * @param {Array} rules - An array of rule names to apply
+ * @param {number} line - The line number that this disable should refer to
+ */
+var addDisableLine = function (toggledRules, rules, line) {
+  rules.map(function (rule) {
+    toggledRules.ruleEnable[rule] = toggledRules.ruleEnable[rule] || [];
+    // NOTE: corner case not handled here: a 2nd disable inside an ignored line, which is unrealistically pathological.
+    toggledRules.ruleEnable[rule].push([false, line, 1]);
+    toggledRules.ruleEnable[rule].push([true, line + 1, 1]);
+  });
+};
+
+/**
+ * This is the sorting function we use to sort the toggle stacks in our getToggledRules method
+ * First sorts by line and then by column if the lines are identical
+ *
+ * @param {Array} toggleRangeA - The first rule to sort
+ * @param {Array} toggleRangeB - The second rule to sort
+ *
+ * @returns {number} A pointer to signify to the sort method how the currently in focus value should be sorted
+ */
+var sortRange = function (toggleRangeA, toggleRangeB) {
+  var aLine = toggleRangeA[1],
+      aCol = toggleRangeA[2],
+      bLine = toggleRangeB[1],
+      bCol = toggleRangeB[2];
+  if (aLine < bLine) {
+    return -1;
+  }
+  if (aLine > bLine) {
+    return 1;
+  }
+  if (aCol < bCol) {
+    return -1;
+  }
+  if (aCol > bCol) {
+    return 1;
+  }
+  return 0;
+};
+
+/**
+ * Checks if line number A is before line number B, if it's the same then it checks if the column of A
+ * is before the column of B
+ *
+ * @param {number} x - The line number of A
+ * @param {number} y - The column number of A
+ * @param {number} x2 - The line number of B
+ * @param {number} y2 - The column number of B
+ *
+ * @returns {Boolean} Whether the current line/column A is before or the same as B
+ */
+var isBeforeOrSame = function (x, y, x2, y2) {
+  return x < x2 || (x === x2 && y < y2);
+};
+
+/**
+ * Traverses the AST looking for sass-lint disable/enable comments and then builds an Object/node representation
+ * of any it encounters
+ *
+ * @param {Object} ast - Gonzales PE abstract syntax tree
+ *
+ * @returns {Object} The toggledRules object containing all of our rule enable/disable information
+ */
+module.exports.getToggledRules = function (ast) {
+  var toggledRules = {
+    ruleEnable: {
+      // Format in here is [isEnabled, line, column]
+    },
+    globalEnable: []
+  };
+  if (!ast.traverseByTypes) {
+    return toggledRules;
+  }
+  ast.traverseByTypes(['multilineComment', 'singlelineComment'], function (comment, i, parent) {
+    var content = comment.content;
+    if (!content) {
+      return;
+    }
+    var tokens = content.split(/[\s,]+/)
+      .filter(function (s) {
+        return s.trim().length > 0;
+      });
+    if (!tokens.length) {
+      return;
+    }
+    var first = tokens[0],
+        rules = tokens.slice(1);
+    switch (first) {
+    case 'sass-lint:disable':
+      addDisable(toggledRules, rules, comment.start.line, comment.start.column);
+      break;
+    case 'sass-lint:enable':
+      addEnable(toggledRules, rules, comment.start.line, comment.start.column);
+      break;
+    case 'sass-lint:disable-block':
+      // future ref: not sure what the appropriate behavior is if there is no parent block; currently NPEs
+      addDisableBlock(toggledRules, rules, parent);
+      break;
+    case 'sass-lint:disable-all':
+      addDisableAll(toggledRules, comment.start.line, comment.start.column);
+      break;
+    case 'sass-lint:enable-all':
+      addEnableAll(toggledRules, comment.start.line, comment.start.column);
+      break;
+    case 'sass-lint:disable-line':
+      addDisableLine(toggledRules, rules, comment.start.line);
+      break;
+    default:
+      return;
+    }
+  });
+  // Sort these toggle stacks so reading them is easier (algorithmically).
+  // Usually already sorted but since it's not guaranteed by the contract with gonzales-pe, ensuring it is.
+  toggledRules.globalEnable.sort(sortRange);
+  Object.keys(toggledRules.ruleEnable).map(function (ruleId) {
+    toggledRules.ruleEnable[ruleId].sort(sortRange);
+  });
+  return toggledRules;
+};
+
+/**
+ * Filters our rule results by checking the lint result and its line/column against our
+ * toggledRules object to see whether we should still be reporting this lint.
+ *
+ * @param {Object} toggledRules - The toggledRules object containing all of our rule enable/disable information
+ *
+ * @returns {Boolean} Whether the current rule is disabled for this lint report
+ */
+module.exports.isResultEnabled = function (toggledRules) {
+  return function (ruleResult) {
+    var ruleId = ruleResult.ruleId;
+    // Convention: if no column or line, assume rule is targetting 1.
+    var line = ruleResult.line || 1;
+    var column = ruleResult.column || 1;
+    var isGloballyEnabled = toggledRules.globalEnable
+      .reduce(function (acc, toggleRange) {
+        return isBeforeOrSame(line, column, toggleRange[1], toggleRange[2])
+          ? acc
+          : toggleRange[0];
+      }, true);
+    if (!isGloballyEnabled) {
+      return false;
+    }
+    if (!toggledRules.ruleEnable[ruleId]) {
+      return true;
+    }
+    var isRuleEnabled = toggledRules.ruleEnable[ruleId]
+      .reduce(function (acc, toggleRange) {
+        return isBeforeOrSame(line, column, toggleRange[1], toggleRange[2])
+          ? acc
+          : toggleRange[0];
+      }, true);
+    if (!isRuleEnabled) {
+      return false;
+    }
+    return true;
+  };
+};


Test patch:
diff --git a/tests/ruleToggler.js b/tests/ruleToggler.js
new file mode 100644
index 0000000..22b3c60
--- /dev/null
+++ b/tests/ruleToggler.js
@@ -0,0 +1,326 @@
+var path = require('path'),
+    fs = require('fs'),
+    groot = require('../lib/groot'),
+    ruleToggler = require('../lib/ruleToggler'),
+    assert = require('assert'),
+    deepEqual = require('deep-equal');
+
+var getToggledRules = ruleToggler.getToggledRules,
+    isResultEnabled = ruleToggler.isResultEnabled;
+
+var generateToggledRules = function (filename) {
+  var filePath = path.join(process.cwd(), 'tests', 'sass', filename);
+  var file = {
+    'text': fs.readFileSync(filePath),
+    'format': path.extname(filePath).replace('.', ''),
+    'filename': path.basename(filePath)
+  };
+  var ast = groot(file.text, file.format, file.filename);
+  return getToggledRules(ast);
+};
+
+describe('rule toggling', function () {
+  describe('getToggledRules', function () {
+    //////////////////////////////
+    // SCSS syntax tests
+    //////////////////////////////
+    it('should allow all rules to be disabled', function () {
+      assert(deepEqual(generateToggledRules('ruleToggler-disable-all.scss'), {
+        globalEnable: [[false, 1, 1]],
+        ruleEnable: {}
+      }) === true);
+    });
+    it('should allow all rules to be disabled then re-enabled', function () {
+      var ruleToggles = generateToggledRules('ruleToggler-disable-all-then-reenable.scss');
+      assert(deepEqual(ruleToggles, {
+        globalEnable: [
+          [false, 1, 1],
+          [true, 3, 1]
+        ],
+        ruleEnable: {}
+      }) === true);
+    });
+    it('should allow a single rule to be disabled', function () {
+      assert(deepEqual(generateToggledRules('ruleToggler-disable-a-rule.scss'), {
+        globalEnable: [],
+        ruleEnable: {
+          a: [[false, 1, 1]]
+        }
+      }) === true);
+    });
+    it('should allow multiple rules to be disabled', function () {
+      assert(deepEqual(generateToggledRules('ruleToggler-disable-multiple-rules.scss'), {
+        globalEnable: [],
+        ruleEnable: {
+          a: [[false, 1, 1]],
+          b: [[false, 1, 1]],
+          c: [[false, 1, 1]],
+          d: [[false, 1, 1]]
+        }
+      }) === true);
+    });
+    it('should be able to disable a single line', function () {
+      var ruleToggles = generateToggledRules('ruleToggler-disable-a-line.scss');
+      assert(deepEqual(ruleToggles, {
+        globalEnable: [],
+        ruleEnable: {
+          a: [[false, 2, 1], [true, 3, 1]]
+        }
+      }) === true);
+    });
+    it('should be able to disable a block of code', function () {
+      var ruleToggles = generateToggledRules('ruleToggler-disable-a-block.scss');
+      assert(deepEqual(ruleToggles, {
+        globalEnable: [],
+        ruleEnable: {
+          a: [[false, 1, 3], [true, 3, 1]]
+        }
+      }) === true);
+    });
+    it('should be able to enable a disabled rule', function () {
+      var ruleToggles = generateToggledRules('ruleToggler-disable-then-enable.scss');
+      assert(deepEqual(ruleToggles, {
+        globalEnable: [],
+        ruleEnable: {
+          a: [[false, 2, 5], [true, 4, 5]]
+        }
+      }) === true);
+    });
+    it('should ignore comments that don\'t fit known formats', function () {
+      var ruleToggles = generateToggledRules('ruleToggler-ignore-unknown.scss');
+      assert(deepEqual(ruleToggles, {
+        globalEnable: [],
+        ruleEnable: {}
+      }) === true);
+    });
+    it('should ignore empty files', function () {
+      var ruleToggles = generateToggledRules('empty-file.scss');
+      assert(deepEqual(ruleToggles, {
+        globalEnable: [],
+        ruleEnable: {}
+      }) === true);
+    });
+    it('should ignore empty comments', function () {
+      var ruleToggles = generateToggledRules('ruleToggler-empty-comment.scss');
+      assert(deepEqual(ruleToggles, {
+        globalEnable: [],
+        ruleEnable: {}
+      }) === true);
+    });
+    it('should be ordered', function () {
+      var ruleToggles = generateToggledRules('ruleToggler-guarantee-order.scss');
+      assert(deepEqual(ruleToggles, {
+        globalEnable: [],
+        ruleEnable: {
+          a: [[false, 1, 3],
+              [false, 2, 5],
+              [true, 6, 1],
+              [false, 8, 3],
+              [false, 8, 5],
+              [true, 12, 1],
+              [false, 14, 6],
+              [false, 14, 32]]
+        }
+      }) === true);
+    });
+
+    //////////////////////////////
+    // SASS syntax tests
+    //////////////////////////////
+    it('should allow all rules to be disabled', function () {
+      assert(deepEqual(generateToggledRules('ruleToggler-disable-all.sass'), {
+        globalEnable: [[false, 1, 1]],
+        ruleEnable: {}
+      }) === true);
+    });
+    it('should allow all rules to be disabled then re-enabled', function () {
+      var ruleToggles = generateToggledRules('ruleToggler-disable-all-then-reenable.sass');
+      assert(deepEqual(ruleToggles, {
+        globalEnable: [
+          [false, 1, 1],
+          [true, 3, 1]
+        ],
+        ruleEnable: {}
+      }) === true);
+    });
+    it('should allow a single rule to be disabled', function () {
+      assert(deepEqual(generateToggledRules('ruleToggler-disable-a-rule.sass'), {
+        globalEnable: [],
+        ruleEnable: {
+          a: [[false, 1, 1]]
+        }
+      }) === true);
+    });
+    it('should allow multiple rules to be disabled', function () {
+      assert(deepEqual(generateToggledRules('ruleToggler-disable-multiple-rules.sass'), {
+        globalEnable: [],
+        ruleEnable: {
+          a: [[false, 1, 1]],
+          b: [[false, 1, 1]],
+          c: [[false, 1, 1]],
+          d: [[false, 1, 1]]
+        }
+      }) === true);
+    });
+    it('should be able to disable a single line', function () {
+      var ruleToggles = generateToggledRules('ruleToggler-disable-a-line.sass');
+      assert(deepEqual(ruleToggles, {
+        globalEnable: [],
+        ruleEnable: {
+          a: [[false, 2, 1], [true, 3, 1]]
+        }
+      }) === true);
+    });
+    it('should be able to disable a block of code', function () {
+      var ruleToggles = generateToggledRules('ruleToggler-disable-a-block.sass');
+      assert(deepEqual(ruleToggles, {
+        globalEnable: [],
+        ruleEnable: {
+          a: [[false, 2, 1], [true, 2, 32]]
+        }
+      }) === true);
+    });
+    it('should be able to enable a disabled rule', function () {
+      var ruleToggles = generateToggledRules('ruleToggler-disable-then-enable.sass');
+      assert(deepEqual(ruleToggles, {
+        globalEnable: [],
+        ruleEnable: {
+          a: [[false, 2, 5], [true, 4, 5]]
+        }
+      }) === true);
+    });
+    it('should ignore comments that don\'t fit known formats', function () {
+      var ruleToggles = generateToggledRules('ruleToggler-ignore-unknown.sass');
+      assert(deepEqual(ruleToggles, {
+        globalEnable: [],
+        ruleEnable: {}
+      }) === true);
+    });
+    it('should ignore empty files', function () {
+      var ruleToggles = generateToggledRules('empty-file.sass');
+      assert(deepEqual(ruleToggles, {
+        globalEnable: [],
+        ruleEnable: {}
+      }) === true);
+    });
+    it('should ignore empty comments', function () {
+      var ruleToggles = generateToggledRules('ruleToggler-empty-comment.sass');
+      assert(deepEqual(ruleToggles, {
+        globalEnable: [],
+        ruleEnable: {}
+      }) === true);
+    });
+    it('should be ordered', function () {
+      var ruleToggles = generateToggledRules('ruleToggler-guarantee-order.sass');
+      assert(deepEqual(ruleToggles, {
+        globalEnable: [],
+        ruleEnable: {
+          a: [[false, 2, 1],
+              [false, 2, 5],
+              [true, 5, 20],
+              [false, 8, 1],
+              [false, 8, 5],
+              [true, 11, 20],
+              [false, 14, 5],
+              [false, 15, 5]]
+        }
+      }) === true);
+    });
+  });
+  describe('isResultEnabled', function () {
+    it('should disable all rules if global is disabled', function () {
+      assert(isResultEnabled({
+        globalEnable: [[false, 1, 1]],
+        ruleEnable: {}
+      })({
+        ruleId: 'anything',
+        line: 2,
+        column: 1
+      }) === false);
+    });
+    it('should disable a rule', function () {
+      assert(isResultEnabled({
+        globalEnable: [],
+        ruleEnable: {
+          a: [[false, 1, 1]]
+        }
+      })({
+        ruleId: 'a',
+        line: 2,
+        column: 1
+      }) === false);
+    });
+    it('should not disable an unrelated rule', function () {
+      assert(isResultEnabled({
+        globalEnable: [],
+        ruleEnable: {
+          b: [[false, 1, 1]]
+        }
+      })({
+        ruleId: 'a',
+        line: 2,
+        column: 1
+      }) === true);
+    });
+    it('should support enabling a previously disabled rule', function () {
+      assert(isResultEnabled({
+        globalEnable: [],
+        ruleEnable: {
+          a: [[false, 1, 1], [true, 2, 1]]
+        }
+      })({
+        ruleId: 'a',
+        line: 3,
+        column: 1
+      }) === true);
+    });
+    it('should support disabling a previously re-enabled rule', function () {
+      assert(isResultEnabled({
+        globalEnable: [],
+        ruleEnable: {
+          a: [[false, 1, 1], [true, 2, 1], [false, 3, 1]]
+        }
+      })({
+        ruleId: 'a',
+        line: 4,
+        column: 1
+      }) === false);
+    });
+    it('should support enabling a previously re-enabled then disabled rule (in enabled part)', function () {
+      assert(isResultEnabled({
+        globalEnable: [],
+        ruleEnable: {
+          a: [[false, 1, 1], [true, 2, 1], [false, 3, 1], [true, 4, 1]]
+        }
+      })({
+        ruleId: 'a',
+        line: 5,
+        column: 1
+      }) === true);
+    });
+    it('should support enabling a previously re-enabled then disabled rule (in disabled part)', function () {
+      assert(isResultEnabled({
+        globalEnable: [],
+        ruleEnable: {
+          a: [[false, 1, 1], [true, 2, 1], [false, 3, 1], [true, 4, 1]]
+        }
+      })({
+        ruleId: 'a',
+        line: 3,
+        column: 10
+      }) === false);
+    });
+    it('should support disabling a rule that is later re-enabled', function () {
+      assert(isResultEnabled({
+        globalEnable: [],
+        ruleEnable: {
+          a: [[false, 1, 1], [true, 3, 1], [false, 4, 1]]
+        }
+      })({
+        ruleId: 'a',
+        line: 2,
+        column: 1
+      }) === false);
+    });
+  });
+});
diff --git a/tests/sass/border-zero.scss b/tests/sass/border-zero.scss
index 398d1cb..5336c60 100644
--- a/tests/sass/border-zero.scss
+++ b/tests/sass/border-zero.scss
@@ -21,3 +21,9 @@
 .norf {
   border: 1px;
 }
+
+.norf {
+  // sass-lint:disable border-zero
+  border: none;
+  // sass-lint:enable border-zero
+}
diff --git a/tests/sass/empty-file.sass b/tests/sass/empty-file.sass
new file mode 100644
index 0000000..e69de29
diff --git a/tests/sass/ruleToggler-disable-a-block.sass b/tests/sass/ruleToggler-disable-a-block.sass
new file mode 100644
index 0000000..79981c6
--- /dev/null
+++ b/tests/sass/ruleToggler-disable-a-block.sass
@@ -0,0 +1,2 @@
+p
+    // sass-lint:disable-block a
diff --git a/tests/sass/ruleToggler-disable-a-block.scss b/tests/sass/ruleToggler-disable-a-block.scss
new file mode 100644
index 0000000..8eddb3e
--- /dev/null
+++ b/tests/sass/ruleToggler-disable-a-block.scss
@@ -0,0 +1,3 @@
+p {
+    // sass-lint:disable-block a
+}
diff --git a/tests/sass/ruleToggler-disable-a-line.sass b/tests/sass/ruleToggler-disable-a-line.sass
new file mode 100644
index 0000000..9c44919
--- /dev/null
+++ b/tests/sass/ruleToggler-disable-a-line.sass
@@ -0,0 +1,2 @@
+p
+    border-color: red // sass-lint:disable-line a
diff --git a/tests/sass/ruleToggler-disable-a-line.scss b/tests/sass/ruleToggler-disable-a-line.scss
new file mode 100644
index 0000000..7d6ca7c
--- /dev/null
+++ b/tests/sass/ruleToggler-disable-a-line.scss
@@ -0,0 +1,3 @@
+p {
+    border-color: red; // sass-lint:disable-line a
+}
diff --git a/tests/sass/ruleToggler-disable-a-rule.sass b/tests/sass/ruleToggler-disable-a-rule.sass
new file mode 100644
index 0000000..5f04af3
--- /dev/null
+++ b/tests/sass/ruleToggler-disable-a-rule.sass
@@ -0,0 +1,1 @@
+// sass-lint:disable a
diff --git a/tests/sass/ruleToggler-disable-a-rule.scss b/tests/sass/ruleToggler-disable-a-rule.scss
new file mode 100644
index 0000000..5f04af3
--- /dev/null
+++ b/tests/sass/ruleToggler-disable-a-rule.scss
@@ -0,0 +1,1 @@
+// sass-lint:disable a
diff --git a/tests/sass/ruleToggler-disable-all-then-reenable.sass b/tests/sass/ruleToggler-disable-all-then-reenable.sass
new file mode 100644
index 0000000..63eeb62
--- /dev/null
+++ b/tests/sass/ruleToggler-disable-all-then-reenable.sass
@@ -0,0 +1,3 @@
+// sass-lint:disable-all
+
+// sass-lint:enable-all
diff --git a/tests/sass/ruleToggler-disable-all-then-reenable.scss b/tests/sass/ruleToggler-disable-all-then-reenable.scss
new file mode 100644
index 0000000..63eeb62
--- /dev/null
+++ b/tests/sass/ruleToggler-disable-all-then-reenable.scss
@@ -0,0 +1,3 @@
+// sass-lint:disable-all
+
+// sass-lint:enable-all
diff --git a/tests/sass/ruleToggler-disable-all.sass b/tests/sass/ruleToggler-disable-all.sass
new file mode 100644
index 0000000..1776554
--- /dev/null
+++ b/tests/sass/ruleToggler-disable-all.sass
@@ -0,0 +1,1 @@
+// sass-lint:disable-all
diff --git a/tests/sass/ruleToggler-disable-all.scss b/tests/sass/ruleToggler-disable-all.scss
new file mode 100644
index 0000000..1776554
--- /dev/null
+++ b/tests/sass/ruleToggler-disable-all.scss
@@ -0,0 +1,1 @@
+// sass-lint:disable-all
diff --git a/tests/sass/ruleToggler-disable-multiple-rules.sass b/tests/sass/ruleToggler-disable-multiple-rules.sass
new file mode 100644
index 0000000..8e44d37
--- /dev/null
+++ b/tests/sass/ruleToggler-disable-multiple-rules.sass
@@ -0,0 +1,1 @@
+// sass-lint:disable a b, c d
diff --git a/tests/sass/ruleToggler-disable-multiple-rules.scss b/tests/sass/ruleToggler-disable-multiple-rules.scss
new file mode 100644
index 0000000..8e44d37
--- /dev/null
+++ b/tests/sass/ruleToggler-disable-multiple-rules.scss
@@ -0,0 +1,1 @@
+// sass-lint:disable a b, c d
diff --git a/tests/sass/ruleToggler-disable-then-enable.sass b/tests/sass/ruleToggler-disable-then-enable.sass
new file mode 100644
index 0000000..5fdc7cf
--- /dev/null
+++ b/tests/sass/ruleToggler-disable-then-enable.sass
@@ -0,0 +1,5 @@
+p
+    // sass-lint:disable a
+    border: none
+    // sass-lint:enable a
+    color: blue
diff --git a/tests/sass/ruleToggler-disable-then-enable.scss b/tests/sass/ruleToggler-disable-then-enable.scss
new file mode 100644
index 0000000..44f55f6
--- /dev/null
+++ b/tests/sass/ruleToggler-disable-then-enable.scss
@@ -0,0 +1,6 @@
+p {
+    // sass-lint:disable a
+    border: none;
+    // sass-lint:enable a
+    color: blue;
+}
diff --git a/tests/sass/ruleToggler-empty-comment.sass b/tests/sass/ruleToggler-empty-comment.sass
new file mode 100644
index 0000000..eea459d
--- /dev/null
+++ b/tests/sass/ruleToggler-empty-comment.sass
@@ -0,0 +1,2 @@
+p
+    /**/
diff --git a/tests/sass/ruleToggler-empty-comment.scss b/tests/sass/ruleToggler-empty-comment.scss
new file mode 100644
index 0000000..1d7676a
--- /dev/null
+++ b/tests/sass/ruleToggler-empty-comment.scss
@@ -0,0 +1,3 @@
+p {
+    /**/
+}
\ No newline at end of file
diff --git a/tests/sass/ruleToggler-guarantee-order.sass b/tests/sass/ruleToggler-guarantee-order.sass
new file mode 100644
index 0000000..62efa91
--- /dev/null
+++ b/tests/sass/ruleToggler-guarantee-order.sass
@@ -0,0 +1,17 @@
+p
+    // sass-lint:disable a
+    border: 0
+    // sass-lint:disable-block a
+    font-size: 100%
+
+a
+    // sass-lint:disable a
+    border: 0
+    // sass-lint:disable-block a
+    font-size: 100%
+
+li
+    /* sass-lint:disable a
+    /* sass-lint:disable a
+    border: 0
+    font-size: 100%
diff --git a/tests/sass/ruleToggler-guarantee-order.scss b/tests/sass/ruleToggler-guarantee-order.scss
new file mode 100644
index 0000000..7c6d617
--- /dev/null
+++ b/tests/sass/ruleToggler-guarantee-order.scss
@@ -0,0 +1,17 @@
+p {
+    // sass-lint:disable a
+    border: 0;
+    // sass-lint:disable-block a
+    font-size: 100%;
+}
+
+a { // sass-lint:disable a
+    border: 0;
+    // sass-lint:disable-block a
+    font-size: 100%;
+}
+
+li { /* sass-lint:disable a */ /* sass-lint:disable a */
+    border: 0;
+    font-size: 100%;
+}
diff --git a/tests/sass/ruleToggler-ignore-unknown.sass b/tests/sass/ruleToggler-ignore-unknown.sass
new file mode 100644
index 0000000..7913ac8
--- /dev/null
+++ b/tests/sass/ruleToggler-ignore-unknown.sass
@@ -0,0 +1,2 @@
+p
+    //sass-lint:random a
diff --git a/tests/sass/ruleToggler-ignore-unknown.scss b/tests/sass/ruleToggler-ignore-unknown.scss
new file mode 100644
index 0000000..698ec2e
--- /dev/null
+++ b/tests/sass/ruleToggler-ignore-unknown.scss
@@ -0,0 +1,3 @@
+p {
+    //sass-lint:random a
+}
\ No newline at end of file