Repository: lesshint/lesshint
Pull request #439 (2017-11-10 21:02:31)

Linter space around bang
<!--
If you're submitting a PR for a new feature, please open an issue about it first so we can discuss it.
If you're submitting a PR for something else, for example a documentation fix, go right ahead!
-->

**Which issue, if any, does this resolve?**
#267 

**Is there anything in this PR that needs extra explaining or should something specific be focused on?**
Not really, tests currently will fail due to two bugs I found in `postcss-less` which I've got a [fix pending for](https://github.com/shellscape/postcss-less/pull/95).


Patch:
diff --git a/lib/config/defaults.json b/lib/config/defaults.json
index 575b336..e922baa 100644
--- a/lib/config/defaults.json
+++ b/lib/config/defaults.json
@@ -145,6 +145,11 @@
         "style": "no_space"
     },
 
+    "spaceAroundBang": {
+      "enabled": true,
+      "style": "before"
+    },
+
     "spaceAroundComma": {
         "allowNewline": false,
         "enabled": true,
diff --git a/lib/linter.js b/lib/linter.js
index 1678575..f95a5fa 100644
--- a/lib/linter.js
+++ b/lib/linter.js
@@ -35,6 +35,7 @@ const defaultLinters = [
     require('./linters/space_after_property_colon'),
     require('./linters/space_after_property_name'),
     require('./linters/space_after_property_value'),
+    require('./linters/space_around_bang'),
     require('./linters/space_around_comma'),
     require('./linters/space_around_operator'),
     require('./linters/space_before_brace'),
diff --git a/lib/linters/README.md b/lib/linters/README.md
index 9f76081..7b80167 100644
--- a/lib/linters/README.md
+++ b/lib/linters/README.md
@@ -26,6 +26,7 @@
 * [spaceAfterPropertyColon](#spaceafterpropertycolon)
 * [spaceAfterPropertyName](#spaceafterpropertyname)
 * [spaceAfterPropertyValue](#spaceafterpropertyvalue)
+* [spaceAroundBang](#spacearoundbang)
 * [spaceAroundComma](#spacearoundcomma)
 * [spaceAroundOperator](#spacearoundoperator)
 * [spaceBeforeBrace](#spacebeforebrace)
@@ -709,6 +710,41 @@ Option     | Description
 }
 ```
 
+## spaceAroundBang
+Defines how the exclamation mark (bang) in `!important` etc. should be formatted by a space to aid readability.
+
+Option  | Description
+--------| ------------
+`style` | `after`, `before` (**default**), `both`, `none`
+
+### after
+```less
+.foo {
+    color: red! important;
+}
+```
+
+### before
+```less
+.foo {
+    color: red !important;
+}
+```
+
+### both
+```less
+.foo {
+    color: red ! important;
+}
+```
+
+### none
+```less
+.foo {
+    color: red!important;
+}
+```
+
 ## spaceAroundComma
 Defines how commas in functions, mixins, etc. should be formatted by a space to aid readability.
 
diff --git a/lib/linters/space_around_bang.js b/lib/linters/space_around_bang.js
new file mode 100644
index 0000000..9f52713
--- /dev/null
+++ b/lib/linters/space_around_bang.js
@@ -0,0 +1,64 @@
+'use strict';
+
+const util = require('util');
+
+module.exports = {
+    name: 'spaceAroundBang',
+    nodeTypes: ['decl', 'rule'],
+    message: 'Exclamation marks should%s be %s by %s space.',
+
+    lint: function spaceAroundCommaLinter (config, node) {
+        let important;
+
+        if (node.important && node.raws.important) {
+            important = node.raws.important;
+        } else if (node.important) {
+            important = ' !important';
+        } else {
+            return;
+        }
+
+        let message;
+
+        switch (config.style) {
+            case 'after':
+                if (important !== '! important') {
+                    message = util.format(this.message, '', 'followed', 'one');
+                }
+
+                break;
+            case 'before':
+                if (important !== ' !important') {
+                    message = util.format(this.message, '', 'preceded', 'one');
+                }
+
+                break;
+            case 'both':
+                if (important !== ' ! important') {
+                    message = util.format(this.message, '', 'preceded and followed', 'one');
+                }
+
+                break;
+            case 'none':
+                if (important !== '!important') {
+                    message = util.format(this.message, ' not', 'preceded nor followed', 'any');
+                }
+
+                break;
+            default:
+                throw new Error(`Invalid setting value for spaceAroundBang: ${ config.style }`);
+        }
+
+        if (message) {
+            const position = node.positionBy({
+                word: important.trim()
+            });
+
+            return [{
+                column: position.column,
+                line: position.line,
+                message: message
+            }];
+        }
+    }
+};
diff --git a/package.json b/package.json
index 12082cd..984ee7f 100644
--- a/package.json
+++ b/package.json
@@ -25,7 +25,7 @@
     "lodash.sortby": "^4.0.1",
     "minimatch": "^3.0.2",
     "postcss": "^6.0.0",
-    "postcss-less": "^1.0.0",
+    "postcss-less": "^1.1.3",
     "postcss-selector-parser": "^3.0.0",
     "postcss-values-parser": "^1.3.1",
     "rcfinder": "^0.1.8",


Test patch:
diff --git a/test/specs/linters/space_around_bang.js b/test/specs/linters/space_around_bang.js
new file mode 100644
index 0000000..2632f32
--- /dev/null
+++ b/test/specs/linters/space_around_bang.js
@@ -0,0 +1,375 @@
+'use strict';
+
+const expect = require('chai').expect;
+const spec = require('../util.js').setup();
+
+describe('lesshint', function () {
+    describe('#spaceAroundBang', function () {
+        let options;
+
+        it('should have the proper node types', function () {
+            const source = 'color: red !important;';
+
+            return spec.parse(source, function (ast) {
+                expect(spec.linter.nodeTypes).to.include(ast.root.first.type);
+            });
+        });
+
+        describe('when "style" is "after"', function () {
+            beforeEach(function () {
+                options = {
+                    style: 'after'
+                };
+            });
+
+            it('should allow one space after bang', function () {
+                const source = 'color: red! important;';
+
+                return spec.parse(source, function (ast) {
+                    const result = spec.linter.lint(options, ast.root.first);
+
+                    expect(result).to.be.undefined;
+                });
+            });
+
+            it('should not allow missing space after comma', function () {
+                const source = 'color: red !important;';
+                const expected = [
+                    {
+                        column: 12,
+                        line: 1,
+                        message: 'Exclamation marks should be followed by one space.'
+                    }
+                ];
+
+                return spec.parse(source, function (ast) {
+                    const result = spec.linter.lint(options, ast.root.first);
+
+                    expect(result).to.deep.equal(expected);
+                });
+            });
+
+            it('should allow one space after bang in mixins', function () {
+                const source = '.mixin()! important;';
+
+                return spec.parse(source, function (ast) {
+                    const result = spec.linter.lint(options, ast.root.first);
+
+                    expect(result).to.be.undefined;
+                });
+            });
+
+            it('should not allow missing space after bang in mixins', function () {
+                const source = '.mixin()!important;';
+                const expected = [{
+                    column: 9,
+                    line: 1,
+                    message: 'Exclamation marks should be followed by one space.'
+                }];
+
+                return spec.parse(source, function (ast) {
+                    const result = spec.linter.lint(options, ast.root.first);
+
+                    expect(result).to.deep.equal(expected);
+                });
+            });
+        }); // "after"
+
+        describe('when "style" is "before"', function () {
+            beforeEach(function () {
+                options = {
+                    style: 'before'
+                };
+            });
+
+            it('should allow one space before bang', function () {
+                const source = 'color: red !important;';
+
+                return spec.parse(source, function (ast) {
+                    const result = spec.linter.lint(options, ast.root.first);
+
+                    expect(result).to.be.undefined;
+                });
+            });
+
+            it('should not allow missing space before bang', function () {
+                const source = 'color: red!important;';
+                const expected = [
+                    {
+                        column: 11,
+                        line: 1,
+                        message: 'Exclamation marks should be preceded by one space.'
+                    }
+                ];
+
+                return spec.parse(source, function (ast) {
+                    const result = spec.linter.lint(options, ast.root.first);
+
+                    expect(result).to.deep.equal(expected);
+                });
+            });
+
+            it('should allow one space before bang in mixins', function () {
+                const source = '.mixin() !important;';
+
+                return spec.parse(source, function (ast) {
+                    const result = spec.linter.lint(options, ast.root.first);
+
+                    expect(result).to.be.undefined;
+                });
+            });
+
+            it('should not allow missing space before bang in mixins', function () {
+                const source = '.mixin()!important;';
+                const expected = [{
+                    column: 9,
+                    line: 1,
+                    message: 'Exclamation marks should be preceded by one space.'
+                }];
+
+                return spec.parse(source, function (ast) {
+                    const result = spec.linter.lint(options, ast.root.first);
+
+                    expect(result).to.deep.equal(expected);
+                });
+            });
+        }); // "before"
+
+        describe('when "style" is "both"', function () {
+            beforeEach(function () {
+                options = {
+                    style: 'both'
+                };
+            });
+
+            it('should allow one space before and after bang', function () {
+                const source = 'color: red ! important;';
+
+                return spec.parse(source, function (ast) {
+                    const result = spec.linter.lint(options, ast.root.first);
+
+                    expect(result).to.be.undefined;
+                });
+            });
+
+            it('should not allow missing space before bang', function () {
+                const source = 'color: red! important;';
+                const expected = [
+                    {
+                        column: 11,
+                        line: 1,
+                        message: 'Exclamation marks should be preceded and followed by one space.'
+                    }
+                ];
+
+                return spec.parse(source, function (ast) {
+                    const result = spec.linter.lint(options, ast.root.first);
+
+                    expect(result).to.deep.equal(expected);
+                });
+            });
+
+            it('should not allow missing space after bang', function () {
+                const source = 'color: red !important;';
+                const expected = [
+                    {
+                        column: 12,
+                        line: 1,
+                        message: 'Exclamation marks should be preceded and followed by one space.'
+                    }
+                ];
+
+                return spec.parse(source, function (ast) {
+                    const result = spec.linter.lint(options, ast.root.first);
+
+                    expect(result).to.deep.equal(expected);
+                });
+            });
+
+            it('should not allow missing space before and after comma', function () {
+                const source = 'color: red!important;';
+                const expected = [
+                    {
+                        column: 11,
+                        line: 1,
+                        message: 'Exclamation marks should be preceded and followed by one space.'
+                    }
+                ];
+
+                return spec.parse(source, function (ast) {
+                    const result = spec.linter.lint(options, ast.root.first);
+
+                    expect(result).to.deep.equal(expected);
+                });
+            });
+
+            it('should allow one space before and after bang in mixins', function () {
+                const source = '.mixin() ! important;';
+
+                return spec.parse(source, function (ast) {
+                    const result = spec.linter.lint(options, ast.root.first);
+
+                    expect(result).to.be.undefined;
+                });
+            });
+
+            it('should not allow missing space before bang in mixins', function () {
+                const source = '.mixin()! important;';
+                const expected = [{
+                    column: 9,
+                    line: 1,
+                    message: 'Exclamation marks should be preceded and followed by one space.'
+                }];
+
+                return spec.parse(source, function (ast) {
+                    const result = spec.linter.lint(options, ast.root.first);
+
+                    expect(result).to.deep.equal(expected);
+                });
+            });
+
+            it('should not allow missing space after bang in mixins', function () {
+                const source = '.mixin() !important;';
+                const expected = [{
+                    column: 10,
+                    line: 1,
+                    message: 'Exclamation marks should be preceded and followed by one space.'
+                }];
+
+                return spec.parse(source, function (ast) {
+                    const result = spec.linter.lint(options, ast.root.first);
+
+                    expect(result).to.deep.equal(expected);
+                });
+            });
+        }); // "both"
+
+        describe('when "style" is "none"', function () {
+            beforeEach(function () {
+                options = {
+                    style: 'none'
+                };
+            });
+
+            it('should allow a missing space after bang', function () {
+                const source = 'color: red!important;';
+
+                return spec.parse(source, function (ast) {
+                    const result = spec.linter.lint(options, ast.root.first);
+
+                    expect(result).to.be.undefined;
+                });
+            });
+
+            it('should not allow one space after bang', function () {
+                const source = 'color: red! important;';
+                const expected = [
+                    {
+                        column: 11,
+                        line: 1,
+                        message: 'Exclamation marks should not be preceded nor followed by any space.'
+                    }
+                ];
+
+                return spec.parse(source, function (ast) {
+                    const result = spec.linter.lint(options, ast.root.first);
+
+                    expect(result).to.deep.equal(expected);
+                });
+            });
+
+            it('should not allow one space before comma', function () {
+                const source = 'color: red !important;';
+                const expected = [
+                    {
+                        column: 12,
+                        line: 1,
+                        message: 'Exclamation marks should not be preceded nor followed by any space.'
+                    }
+                ];
+
+                return spec.parse(source, function (ast) {
+                    const result = spec.linter.lint(options, ast.root.first);
+
+                    expect(result).to.deep.equal(expected);
+                });
+            });
+
+            it('should not allow one space before and after comma', function () {
+                const source = 'color: red ! important;';
+                const expected = [
+                    {
+                        column: 12,
+                        line: 1,
+                        message: 'Exclamation marks should not be preceded nor followed by any space.'
+                    }
+                ];
+
+                return spec.parse(source, function (ast) {
+                    const result = spec.linter.lint(options, ast.root.first);
+
+                    expect(result).to.deep.equal(expected);
+                });
+            });
+
+            it('should allow a missing space after bang in mixins', function () {
+                const source = '.mixin()!important;';
+
+                return spec.parse(source, function (ast) {
+                    const result = spec.linter.lint(options, ast.root.first);
+
+                    expect(result).to.be.undefined;
+                });
+            });
+
+            it('should not allow one space after bang in mixins', function () {
+                const source = '.mixin()! important;';
+                const expected = [{
+                    column: 9,
+                    line: 1,
+                    message: 'Exclamation marks should not be preceded nor followed by any space.'
+                }];
+
+                return spec.parse(source, function (ast) {
+                    const result = spec.linter.lint(options, ast.root.first);
+
+                    expect(result).to.deep.equal(expected);
+                });
+            });
+
+            it('should not allow one space before bang in mixins', function () {
+                const source = '.mixin() !important;';
+                const expected = [{
+                    column: 10,
+                    line: 1,
+                    message: 'Exclamation marks should not be preceded nor followed by any space.'
+                }];
+
+                return spec.parse(source, function (ast) {
+                    const result = spec.linter.lint(options, ast.root.first);
+
+                    expect(result).to.deep.equal(expected);
+                });
+            });
+        }); // "none"
+
+        describe('with invalid "style" value', function () {
+            beforeEach(function () {
+                options = {
+                    style: 'invalid'
+                };
+            });
+
+            it('should throw an error', function () {
+                const source = 'color: red !important;';
+
+                return spec.parse(source, function (ast) {
+                    const node = ast.root.first;
+                    const lint = spec.linter.lint.bind(null, options, node);
+
+                    expect(lint).to.throw(Error);
+                });
+            });
+        }); // "invalid"
+    });
+});