Repository: lesshint/lesshint (js)

Issue:
Create blankLineAfterDeclaration linter
We'd like to be able to enforce a blank new line after a declaration. For example:
```less
// bad
.thing1 {
    color: red;
}
.thing2 {
    color: blue;
}
```

```less
// good
.thing1 {
    color: red;
}

.thing2 {
    color: blue;
}
```



Pull request description:
Add blankLineAfterBlock linter
I named it `blankLineAfterBlock` since it's actually checking blocks and not declarations (petty thing, I know).

Closes #116.

Patch:
diff --git a/lib/config/defaults.json b/lib/config/defaults.json
index ca9b128..feebf82 100644
--- a/lib/config/defaults.json
+++ b/lib/config/defaults.json
@@ -76,6 +76,10 @@
         "leadingUnderscore": false
     },
 
+    "newlineAfterBlock": {
+        "enabled": true
+    },
+
     "propertyOrdering": {
         "enabled": true,
         "style": "alpha"
diff --git a/lib/linter.js b/lib/linter.js
index 2ad5f31..949ecc1 100644
--- a/lib/linter.js
+++ b/lib/linter.js
@@ -8,6 +8,7 @@ var path = require('path');
 
 var defaultLinters = [
     require('./linters/attribute_quotes'),
+    require('./linters/newline_after_block'),
     require('./linters/border_zero'),
     require('./linters/comment'),
     require('./linters/decimal_zero'),
diff --git a/lib/linters/README.md b/lib/linters/README.md
index da26f1f..7a0c8b3 100644
--- a/lib/linters/README.md
+++ b/lib/linters/README.md
@@ -14,6 +14,7 @@
 * [idSelector](#idselector)
 * [importantRule](#importantrule)
 * [importPath](#importpath)
+* [newlineAfterBlock](#newlineafterblock)
 * [propertyOrdering](#propertyordering)
 * [propertyUnits](#propertyunits)
 * [qualifyingElement](#qualifyingelement)
@@ -65,6 +66,30 @@ input[type="text"] {
 }
 ```
 
+## newlineAfterBlock
+All blocks should be followed by a new line.
+
+### invalid
+```less
+.foo {
+    color: red;
+}
+.bar {
+    color: blue;
+}
+```
+
+### valid
+```less
+.foo {
+    color: red;
+}
+
+.bar {
+    color: blue;
+}
+```
+
 ## borderZero
 Prefer `0` over `none` in border declarations.
 
diff --git a/lib/linters/newline_after_block.js b/lib/linters/newline_after_block.js
new file mode 100644
index 0000000..58f33a5
--- /dev/null
+++ b/lib/linters/newline_after_block.js
@@ -0,0 +1,49 @@
+'use strict';
+
+module.exports = {
+    name: 'newlineAfterBlock',
+    nodeTypes: ['atrule', 'rule'],
+    message: 'All blocks should be followed by a new line.',
+
+    lint: function newlineAfterBlockLinter (config, node) {
+        var parent = node.parent;
+        var prev;
+
+        prev = parent.index(node) - 1;
+        prev = parent.nodes[prev] || {};
+
+        // Ignore at-rules without a body
+        if (node.ruleWithoutBody || (node.type === 'atrule' && !node.nodes)) {
+            /**
+             * Hopefully node.ruleWithoutBody will be implemented on AtRule nodes
+             * in the future: https://github.com/webschik/postcss-less/issues/55
+             */
+            return;
+        }
+
+        /**
+         * If it's the first node in a block, ignore it since we actually check
+         * if the node is preceded by an empty line
+         */
+        if (Object.is(node, parent.first)) {
+            return;
+        }
+
+        /**
+         * Allow a preceding comment without a empty line,
+         * but account for an empty line before the comment,
+         * unless it's the first comment of a block.
+         */
+        if (prev.type === 'comment') {
+            if (Object.is(prev, parent.first) || prev.raws.before === '\n\n') {
+                return;
+            }
+        }
+
+        if (node.raws.before !== '\n\n') {
+            return [{
+                message: this.message
+            }];
+        }
+    }
+};


Test patch:
diff --git a/test/specs/linters/newline_after_block.js b/test/specs/linters/newline_after_block.js
new file mode 100644
index 0000000..9a2b171
--- /dev/null
+++ b/test/specs/linters/newline_after_block.js
@@ -0,0 +1,120 @@
+'use strict';
+
+var expect = require('chai').expect;
+var spec = require('../util.js').setup();
+
+describe('lesshint', function () {
+    describe('#newlineAfterBlock()', function () {
+        it('should have the proper node types', function () {
+            var source = '.foo { color: red; }';
+
+            return spec.parse(source, function (ast) {
+                expect(spec.linter.nodeTypes).to.include(ast.root.first.type);
+            });
+        });
+
+        it('should not allow adjacent blocks without a new line', function () {
+            var source = '';
+            var expected = [{
+                message: 'All blocks should be followed by a new line.'
+            }];
+
+            source += '.foo {';
+            source += '    color: red;';
+            source += '}';
+            source += '.bar {';
+            source += '    color: blue;';
+            source += '}';
+
+            return spec.parse(source, function (ast) {
+                var result = spec.linter.lint({}, ast.root.last);
+
+                expect(result).to.deep.equal(expected);
+            });
+        });
+
+        it('should allow adjacent blocks with a new line', function () {
+            var source = '';
+
+            source += '.foo {';
+            source += '    color: red;';
+            source += '}';
+            source += '\n\n';
+            source += '.bar {';
+            source += '    color: blue;';
+            source += '}';
+
+            return spec.parse(source, function (ast) {
+                var result = spec.linter.lint({}, ast.root.last);
+
+                expect(result).to.be.undefined;
+            });
+        });
+
+        it('should not allow adjacent blocks without a new line', function () {
+            var source = '';
+            var expected = [{
+                message: 'All blocks should be followed by a new line.'
+            }];
+
+            source += '.bar {';
+            source += '    color: blue;';
+            source += '}';
+            source += '@media (screen) {';
+            source += '}';
+
+            return spec.parse(source, function (ast) {
+                var result = spec.linter.lint({}, ast.root.last);
+
+                expect(result).to.deep.equal(expected);
+            });
+        });
+
+        it('should allow adjacent at-rule blocks with a new line', function () {
+            var source = '';
+
+            source += '.bar {';
+            source += '    color: blue;';
+            source += '}';
+            source += '\n\n';
+            source += '@media (screen) {';
+            source += '}';
+
+            return spec.parse(source, function (ast) {
+                var result = spec.linter.lint({}, ast.root.last);
+
+                expect(result).to.be.undefined;
+            });
+        });
+
+        it('should not check at-rules without a body', function () {
+            var source = '@import "foo";';
+
+            return spec.parse(source, function (ast) {
+                var result = spec.linter.lint({}, ast.root.last);
+
+                expect(result).to.be.undefined;
+            });
+        });
+
+        it('should allow preceding comments without a new line', function () {
+            var source = '';
+
+            source += '.foo {';
+            source += '    color: red;';
+            source += '}';
+            source += '\n\n';
+            source += '// A comment';
+            source += '\n';
+            source += '.bar {';
+            source += '    color: blue;';
+            source += '}';
+
+            return spec.parse(source, function (ast) {
+                var result = spec.linter.lint({}, ast.root.last);
+
+                expect(result).to.be.undefined;
+            });
+        });
+    });
+});