diff --git a/docs/options/toggle-rules-in-src.md b/docs/options/toggle-rules-in-src.md new file mode 100644 index 00000000..05e5ba78 --- /dev/null +++ b/docs/options/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 5af2e440..4466e553 100644 --- a/index.js +++ b/index.js @@ -4,11 +4,14 @@ var slConfig = require('./lib/config'), groot = require('./lib/groot'), helpers = require('./lib/helpers'), slRules = require('./lib/rules'), + ruleToggler = require('./lib/ruleToggler'), glob = require('glob'), path = require('path'), jsonFormatter = require('eslint/lib/formatters/json'), fs = require('fs-extra'); +var getToggledRules = ruleToggler.getToggledRules, + isResultEnabled = ruleToggler.isResultEnabled; var sassLint = function (config) { config = require('./lib/config')(config); @@ -37,11 +40,14 @@ sassLint.lintText = function (file, options, configPath) { detects, results = [], errors = 0, - warnings = 0; + warnings = 0, + ruleToggles = getToggledRules(ast), + isEnabledFilter = isResultEnabled(ruleToggles); if (ast.content.length > 0) { 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 00000000..736e6531 --- /dev/null +++ b/lib/ruleToggler.js @@ -0,0 +1,154 @@ +'use strict'; + +var addDisable = function (toggledRules, rules, line, column) { + rules.map(function (rule) { + toggledRules.ruleEnable[rule] = toggledRules.ruleEnable[rule] || []; + toggledRules.ruleEnable[rule].push([false, line, column]); + }); +}; + +var addEnable = function (toggledRules, rules, line, column) { + rules.map(function (rule) { + toggledRules.ruleEnable[rule] = toggledRules.ruleEnable[rule] || []; + toggledRules.ruleEnable[rule].push([true, line, column]); + }); +}; + +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]); + }); +}; + +var addDisableAll = function (toggledRules, line, column) { + toggledRules.globalEnable + .push([false, line, column]); +}; + +var addEnableAll = function (toggledRules, line, column) { + toggledRules.globalEnable + .push([true, line, column]); +}; + +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]); + }); +}; + +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; +}; + +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': + // TODO: 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; +}; + +var isBeforeOrSame = function (x, y, x2, y2) { + return x < x2 || (x === x2 && y < y2); +}; + +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; + }; +}; diff --git a/tests/ruleToggler.js b/tests/ruleToggler.js new file mode 100644 index 00000000..16210985 --- /dev/null +++ b/tests/ruleToggler.js @@ -0,0 +1,220 @@ +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 () { + 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); + }); + }); + 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 398d1cb8..5336c600 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/ruleToggler-disable-a-block.scss b/tests/sass/ruleToggler-disable-a-block.scss new file mode 100644 index 00000000..8eddb3ea --- /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.scss b/tests/sass/ruleToggler-disable-a-line.scss new file mode 100644 index 00000000..7d6ca7cc --- /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.scss b/tests/sass/ruleToggler-disable-a-rule.scss new file mode 100644 index 00000000..5f04af37 --- /dev/null +++ b/tests/sass/ruleToggler-disable-a-rule.scss @@ -0,0 +1 @@ +// sass-lint:disable a 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 00000000..63eeb627 --- /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.scss b/tests/sass/ruleToggler-disable-all.scss new file mode 100644 index 00000000..1776554a --- /dev/null +++ b/tests/sass/ruleToggler-disable-all.scss @@ -0,0 +1 @@ +// sass-lint:disable-all diff --git a/tests/sass/ruleToggler-disable-multiple-rules.scss b/tests/sass/ruleToggler-disable-multiple-rules.scss new file mode 100644 index 00000000..8e44d373 --- /dev/null +++ b/tests/sass/ruleToggler-disable-multiple-rules.scss @@ -0,0 +1 @@ +// sass-lint:disable a b, c d diff --git a/tests/sass/ruleToggler-disable-then-enable.scss b/tests/sass/ruleToggler-disable-then-enable.scss new file mode 100644 index 00000000..44f55f67 --- /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.scss b/tests/sass/ruleToggler-empty-comment.scss new file mode 100644 index 00000000..1d7676a4 --- /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.scss b/tests/sass/ruleToggler-guarantee-order.scss new file mode 100644 index 00000000..7c6d6178 --- /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.scss b/tests/sass/ruleToggler-ignore-unknown.scss new file mode 100644 index 00000000..698ec2e5 --- /dev/null +++ b/tests/sass/ruleToggler-ignore-unknown.scss @@ -0,0 +1,3 @@ +p { + //sass-lint:random a +} \ No newline at end of file