Repository: lesshint/lesshint
Pull request #285 (2016-11-14 19:36:30)

excludedFiles should check paths before reading them
As discussed in #251.

Can anyone see any BC breaks with this approach? I'm keeping the old `excludedFiles` check since we want people to exclude a single file.


Patch:
diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md
new file mode 100644
index 0000000..374cbe9
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE.md
@@ -0,0 +1,19 @@
+<!--
+Please include as much information as possible about your issue/request.
+
+Not all headers below are relevant for all issues, if something's irrelevant, just remove it.
+-->
+
+**Which version of `lesshint` are you using?**
+
+**How are you running `lesshint`? CLI, Node.js API, Grunt/Gulp plugin?**
+
+**What's your `.lesshintrc` configuration?**
+
+**If you're reporting a bug, please show us some code that's failing.**
+
+**What did you expect to happen?**
+
+**What's the actual result?**
+
+**Would you be interested in submitting a PR for this issue?**
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 0000000..9e640c7
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,8 @@
+<!--
+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?**
+
+**Is there anything in this PR that needs extra explaining or should something specific be focused on?**
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 765ac73..98c0e59 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,4 +1,17 @@
 # Changelog
+## 2.4.0 (2016-12-14)
+* Added a `allowSingleLineRules` option to `singleLinePerProperty`. ([52879a7](https://github.com/lesshint/lesshint/commit/52879a7acfe4bba6d532a8a231b338616a5ce42c))
+* Added a `position` field to results. ([6292387](https://github.com/lesshint/lesshint/commit/62923873b287ed1a347532eac233e8347a5213af))
+* Test console spies are now restored before doing assertions. ([00f2f1d](https://github.com/lesshint/lesshint/commit/00f2f1d8b16740b83e6456676ed16193b942c66c))
+* Added issue and PR templates. ([33f767d](https://github.com/lesshint/lesshint/commit/33f767de2ef3119024088cfecf8ec95f4c927e2e))
+* Fixed a "there's"/"there are" typo in the README. ([31e5eb3](https://github.com/lesshint/lesshint/commit/31e5eb3043c1f0b2d61b9b3c7347fd008d8d04ee))
+
+## 2.3.0 (2016-11-29)
+* Added a `-x`/`--max-warnings` CLI flag. ([21e184d](https://github.com/lesshint/lesshint/commit/21e184d1006317bd60e7b5b98b2d52edfe0fc90c))
+* Added a JSON reporter. ([a69bdee](https://github.com/lesshint/lesshint/commit/a69bdee9d48502bf1fa266c583d06d7129dfe8f8))
+* Reporters can now also be objects and not just file paths.  ([376940c](https://github.com/lesshint/lesshint/commit/376940c0923bcd6b72138518a8dc6694639d662f))
+* Fixed a "is"/"are" typo in the README. ([282fdea](https://github.com/lesshint/lesshint/commit/282fdea03ad8cc0f4a88e4d96e6ecadb539beef5))
+
 ## 2.2.1 (2016-10-30)
 * Fixed a `newlineAfterBlock` regression from `2.2.0`. ([117a2b5](https://github.com/lesshint/lesshint/commit/117a2b55bd44f508a334f1b7b540afc44702bd61))
 
diff --git a/README.md b/README.md
index 43085a5..86f17ec 100644
--- a/README.md
+++ b/README.md
@@ -43,7 +43,7 @@ Each option is then specified by its own JSON object, for example:
         "style": "one_space" // Comments are allowed
     },
 
-    "emptyRule": true // If there's no options for a rule, you can simply enable it by setting it to true
+    "emptyRule": true // If there are no options for a rule, you can simply enable it by setting it to true
 }
 ```
 
@@ -193,13 +193,14 @@ Run `lesshint` from the command-line by passing one or more files/directories to
 lesshint src/less/ lib/style.less
 ```
 
-Available Flags     | Description
---------------------|----------------------------------------------
-`-c`/`--config`     | Specify the configuration file to use (will be merged with defaults).
-`-e`/`--exclude`    | A [minimatch glob pattern](https://github.com/isaacs/minimatch) or a file to exclude from being linted.
-`-l`/`--linters`    | Require paths of custom linters to add to the built-in list.
-`-r`/`--reporter`   | The reporter to use. See "Reporters" below for possible values.
-`-V`/`--version`    | Show version.
+Available Flags       | Description
+----------------------|----------------------------------------------
+`-c`/`--config`       | Specify the configuration file to use (will be merged with defaults).
+`-e`/`--exclude`      | A [minimatch glob pattern](https://github.com/isaacs/minimatch) or a file to exclude from being linted.
+`-l`/`--linters`      | Require paths of custom linters to add to the built-in list.
+`-r`/`--reporter`     | The reporter to use. See "Reporters" below for possible values.
+`-V`/`--version`      | Show version.
+`-x`/`--max-warnings` | Number of warnings to allow before exiting with a non-zero code. Passing `-1` will allow any number of warnings to pass.
 
 ### Exit status codes
 Depending on the linter results and options supplied, the exit status code returned by the CLI will differ.
diff --git a/bin/lesshint b/bin/lesshint
index 260724a..ddb6e2d 100755
--- a/bin/lesshint
+++ b/bin/lesshint
@@ -19,6 +19,7 @@ program
     .option('-e, --exclude [pattern]', 'path pattern to exclude')
     .option('-l --linters [linters]', 'paths to custom linters')
     .option('-r, --reporter [reporter]', 'reporter to use (<PATH>|stylish), defaults to "stylish"')
+    .option('-x, --max-warnings [int]', 'Number of warnings to trigger nonzero exit code, defaults to 0')
     .parse(process.argv);
 
 cli(program);
diff --git a/lib/cli.js b/lib/cli.js
index 7c3ed4d..a412279 100644
--- a/lib/cli.js
+++ b/lib/cli.js
@@ -61,7 +61,9 @@ module.exports = function (program) {
     promises = program.args.map(lesshint.checkPath, lesshint);
     Vow.all(promises).then(function (results) {
         var hasError;
+        var maxWarnings;
         var reporter;
+        var warnings;
 
         results = [].concat.apply([], results);
 
@@ -86,7 +88,22 @@ module.exports = function (program) {
         if (hasError) {
             exitDefer.reject(EXIT_CODES.ERROR);
         } else {
-            exitDefer.reject(EXIT_CODES.WARNING);
+            maxWarnings = parseInt(program.maxWarnings, 10) || 0;
+
+            if (maxWarnings === -1) {
+                exitDefer.resolve(EXIT_CODES.OK);
+                return;
+            }
+
+            warnings = results.reduce(function (sum, result) {
+                return sum + (result.severity === 'warning' ? 1 : 0);
+            }, 0);
+
+            if (warnings > maxWarnings) {
+                exitDefer.reject(EXIT_CODES.WARNING);
+            } else {
+                exitDefer.resolve(EXIT_CODES.OK);
+            }
         }
     }).fail(function (error) {
         console.error('An unknown error occurred when checking "%s", please file an issue with this info:', error.lesshintFile);
diff --git a/lib/config/defaults.json b/lib/config/defaults.json
index 7cfe36a..01ef585 100644
--- a/lib/config/defaults.json
+++ b/lib/config/defaults.json
@@ -117,7 +117,8 @@
     },
 
     "singleLinePerProperty": {
-        "enabled": true
+        "enabled": true,
+        "allowSingleLineRules": false
     },
 
     "singleLinePerSelector": {
diff --git a/lib/lesshint.js b/lib/lesshint.js
index a866229..9a72386 100644
--- a/lib/lesshint.js
+++ b/lib/lesshint.js
@@ -13,6 +13,12 @@ var Lesshint = function () {
 };
 
 Lesshint.prototype.checkDirectory = function (checkPath) {
+    if (this.isExcluded(checkPath)) {
+        return new Vow.Promise(function (resolve) {
+            resolve([]);
+        });
+    }
+
     return fs.listDir(checkPath).then(function (files) {
         files = files.map(function (file) {
             var fullPath = path.join(checkPath, file);
@@ -49,6 +55,12 @@ Lesshint.prototype.checkFile = function (checkPath) {
 };
 
 Lesshint.prototype.checkPath = function (checkPath) {
+    if (this.isExcluded(checkPath)) {
+        return new Vow.Promise(function (resolve) {
+            resolve([]);
+        });
+    }
+
     return fs.stat(checkPath).then(function (stats) {
         if (stats.isDirectory()) {
             return this.checkDirectory(checkPath);
@@ -127,6 +139,11 @@ Lesshint.prototype.getReporter = function (reporter) {
         return require(reporterPath);
     }
 
+    // Object with .report as a method: return it directly
+    if (reporter.constructor !== 'string' && reporter.report) {
+        return reporter;
+    }
+
     // Try to find it somewhere on disk
     try {
         reporterPath = path.resolve(process.cwd(), reporter);
diff --git a/lib/linter.js b/lib/linter.js
index 3ba818b..f5febfd 100644
--- a/lib/linter.js
+++ b/lib/linter.js
@@ -91,7 +91,7 @@ var getInlineOptions = function (node) {
     return parseRules(node);
 };
 
-var runChecks = function (ast, fullPath, lines, config) {
+var runChecks = function (ast, fullPath, lines, linesRaw, config) {
     var filename = path.basename(fullPath);
     var lineOptions = [];
     var results = [];
@@ -154,12 +154,10 @@ var runChecks = function (ast, fullPath, lines, config) {
                         return;
                     }
 
-                    /**
-                     * Each piece of lint (linter result) can override
-                     * the default properties of the result we're adding
-                     * to the collection
-                     */
-                    results.push(merge({
+                    // Each piece of lint (linter result) can override
+                    // the default properties of the result we're adding
+                    // to the collection
+                    piece = merge({
                         column: node.source.start.column,
                         file: filename,
                         fullPath: fullPath,
@@ -168,7 +166,13 @@ var runChecks = function (ast, fullPath, lines, config) {
                         message: 'Unknown linting error. Source: ' + linter.name,
                         severity: options.severity || exports.resultSeverity.warning,
                         source: line ? lines[line - 1] : ''
-                    }, piece));
+                    }, piece);
+
+                    piece.position = piece.column
+                        + linesRaw.slice(0, piece.line - 1).join('').length
+                        - 1;
+
+                    results.push(piece);
                 });
             }
         });
@@ -235,9 +239,11 @@ exports.resultSeverity = {
 exports.lint = function (input, fullPath, config) {
     var results;
     var parser;
+    var linesRaw;
     var lines;
     var ast;
 
+    linesRaw = input.match(/[^\n]+(?:\r?\n|$)/g); // Include trailing line endings
     input = input.replace(/\r\n|\r|\n/g, '\n'); // Normalize line endings. See #40
     lines = input.split('\n');
 
@@ -245,7 +251,7 @@ exports.lint = function (input, fullPath, config) {
         parser = this.getParser(input);
         ast = parser.root.toResult();
 
-        results = runChecks(ast, fullPath, lines, config);
+        results = runChecks(ast, fullPath, lines, linesRaw, config);
         results = sortBy(results, 'column');
         results = sortBy(results, 'line');
 
diff --git a/lib/linters/README.md b/lib/linters/README.md
index d9e1e12..f156a77 100644
--- a/lib/linters/README.md
+++ b/lib/linters/README.md
@@ -532,6 +532,10 @@ camel case         | .btnPrimary        | `disallowUnderscore`, `disallowDash`
 ## singleLinePerProperty
 Each property should be on its own line.
 
+Option                  | Description
+----------------------- | ------------
+`allowSingleLineRules`  | `false` (**default**), `true`
+
 ### invalid
 ```less
 .foo {
diff --git a/lib/linters/single_line_per_property.js b/lib/linters/single_line_per_property.js
index c2767ec..d664857 100644
--- a/lib/linters/single_line_per_property.js
+++ b/lib/linters/single_line_per_property.js
@@ -33,6 +33,12 @@ module.exports = {
             return;
         }
 
+        if (config.allowSingleLineRules) {
+            if (node.source.start.line === node.source.end.line && node.nodes.length === 1) {
+                return;
+            }
+        }
+
         node.nodes.forEach(function (child, index) {
             var validBefore;
             var validAfter;
diff --git a/lib/reporters/json.js b/lib/reporters/json.js
new file mode 100644
index 0000000..2c3ac45
--- /dev/null
+++ b/lib/reporters/json.js
@@ -0,0 +1,10 @@
+/*eslint no-console: 0*/
+
+'use strict';
+
+module.exports = {
+    name: 'json',
+    report: function report (results) {
+        console.log(JSON.stringify(results));
+    }
+};
diff --git a/package.json b/package.json
index 7853b51..55d490d 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
   "name": "lesshint",
   "description": "A tool to aid you in writing clean and consistent Less.",
-  "version": "2.2.1",
+  "version": "2.4.0",
   "main": "./lib/lesshint.js",
   "author": {
     "name": "Jonathan Wilsson",


Test patch:
diff --git a/test/data/config/three-warns.json b/test/data/config/three-warns.json
new file mode 100644
index 0000000..ec4394e
--- /dev/null
+++ b/test/data/config/three-warns.json
@@ -0,0 +1,16 @@
+{
+    "spaceAfterPropertyColon": {
+        "enabled": true,
+        "style": "no_space"
+    },
+
+    "spaceBeforeBrace": {
+        "enabled": true,
+        "style": "new_line"
+    },
+
+    "spaceAfterPropertyValue": {
+        "enabled": true,
+        "style": "one_space"
+    }
+}
diff --git a/test/data/config/two-warns.json b/test/data/config/two-warns.json
new file mode 100644
index 0000000..e8c5878
--- /dev/null
+++ b/test/data/config/two-warns.json
@@ -0,0 +1,11 @@
+{
+    "spaceAfterPropertyColon": {
+        "enabled": true,
+        "style": "no_space"
+    },
+
+    "spaceBeforeBrace": {
+        "enabled": true,
+        "style": "new_line"
+    }
+}
diff --git a/test/specs/cli.js b/test/specs/cli.js
index d91b798..e5b0833 100644
--- a/test/specs/cli.js
+++ b/test/specs/cli.js
@@ -38,9 +38,11 @@ describe('cli', function () {
         });
 
         return result.fail(function () {
-            expect(console.error.calledOnce).to.equal(true);
+            var called = console.error.calledOnce;
 
             console.error.restore();
+
+            expect(called).to.equal(true);
         });
     });
 
@@ -54,9 +56,11 @@ describe('cli', function () {
         });
 
         return result.fail(function () {
-            expect(console.error.calledOnce).to.equal(true);
+            var called = console.error.calledOnce;
 
             console.error.restore();
+
+            expect(called).to.equal(true);
         });
     });
 
@@ -71,9 +75,89 @@ describe('cli', function () {
         });
 
         return result.fail(function (status) {
+            console.log.restore();
+
             expect(status).to.equal(1);
+        });
+    });
 
-            console.log.restore();
+    it('should exit with a non-zero status code when lint warnings pass `--max-warnings`', function () {
+        var result;
+
+        result = cli({
+            args: [
+                path.dirname(__dirname) + '/data/files/file.less',
+            ],
+            config: path.resolve(process.cwd() + '/lib/config/defaults.json'),
+            maxWarnings: '0',
+        });
+
+        return result.fail(function (status) {
+            expect(status).to.equal(1);
+        });
+    });
+
+    it('should exit with a non-zero status code with three warings and `--max-warnings 2`', function () {
+        var result;
+
+        result = cli({
+            args: [
+                path.dirname(__dirname) + '/data/files/file.less',
+            ],
+            config: path.resolve(process.cwd() + '/test/data/config/three-warns.json'),
+            maxWarnings: '2',
+        });
+
+        return result.fail(function (status) {
+            expect(status).to.equal(1);
+        });
+    });
+
+    it('should exit with zero status code with 2 warings and `--max-warnings 2`', function () {
+        var result;
+
+        result = cli({
+            args: [
+                path.dirname(__dirname) + '/data/files/file.less',
+            ],
+            config: path.resolve(process.cwd() + '/test/data/config/two-warns.json'),
+            maxWarnings: '2',
+        });
+
+        return result.then(function (status) {
+            expect(status).to.equal(0);
+        });
+    });
+
+    it('should exit with a zero status code when lint warnings do not pass `--max-warnings`', function () {
+        var result;
+
+        result = cli({
+            args: [
+                path.dirname(__dirname) + '/data/files/file.less',
+            ],
+            config: path.resolve(process.cwd() + '/lib/config/defaults.json'),
+            maxWarnings: '999',
+        });
+
+        return result.then(function (status) {
+            expect(status).to.equal(0);
+        });
+    });
+
+    it('should exit with a zero status code with `--max-warnings -1` even if lint has warings', function () {
+        var result;
+
+        result = cli({
+            args: [
+                path.dirname(__dirname) + '/data/files/file.less',
+            ],
+            config: path.resolve(process.cwd() + '/lib/config/defaults.json'),
+            maxWarnings: '-1',
+        });
+
+        return result.then(function (status) {
+            expect(status).to.equal(0);
         });
     });
 
@@ -154,6 +238,7 @@ describe('cli', function () {
 
         return result.fail(function (status) {
             console.log.restore();
+
             expect(status).to.equal(1);
         });
     });
@@ -170,6 +255,7 @@ describe('cli', function () {
 
         return result.fail(function (status) {
             console.log.restore();
+
             expect(status).to.equal(1);
         });
     });
@@ -187,6 +273,7 @@ describe('cli', function () {
 
         return result.fail(function (status) {
             console.log.restore();
+
             expect(status).to.equal(1);
         });
     });
@@ -204,6 +291,7 @@ describe('cli', function () {
 
         return result.fail(function (status) {
             console.log.restore();
+
             expect(status).to.equal(70);
         });
     });
@@ -221,6 +309,7 @@ describe('cli', function () {
 
         return result.fail(function (status) {
             console.log.restore();
+
             expect(status).to.equal(78);
         });
     });
@@ -251,6 +340,22 @@ describe('cli', function () {
         });
     });
 
+    it('should exit without errors when passed a reporter object', function () {
+        var result;
+
+        result = cli({
+            args: [path.dirname(__dirname) + '/data/files/ok.less'],
+            reporter: {
+                name: 'test',
+                report: function () {}
+            }
+        });
+
+        return result.then(function (status) {
+            expect(status).to.equal(0);
+        });
+    });
+
     it('should exit with error when passed a invalid reporter name', function () {
         var result;
 
@@ -289,6 +394,7 @@ describe('cli', function () {
 
         return result.fail(function (status) {
             console.log.restore();
+
             expect(status).to.equal(2);
         });
     });
diff --git a/test/specs/lesshint.js b/test/specs/lesshint.js
index da92dc1..5824262 100644
--- a/test/specs/lesshint.js
+++ b/test/specs/lesshint.js
@@ -55,6 +55,20 @@ describe('lesshint', function () {
             });
         });
 
+        it('should ignore excluded folders', function () {
+            var testPath = path.join(path.dirname(__dirname), '/data/excluded-files');
+            var lesshint = new Lesshint();
+            var config = {
+                excludedFiles: ['**/excluded-files']
+            };
+
+            lesshint.configure(config);
+
+            return lesshint.checkDirectory(testPath).then(function (result) {
+                expect(result).to.have.length(0);
+            });
+        });
+
         it('should only check files with the correct extension and a leading dot', function () {
             var testPath = path.join(path.dirname(__dirname), '/data/excluded-files');
             var lesshint = new Lesshint();
@@ -120,6 +134,34 @@ describe('lesshint', function () {
                 expect(result).to.have.length(2);
             });
         });
+
+        it('should ignore excluded files', function () {
+            var testPath = path.join(path.dirname(__dirname), '/data/excluded-files/exclude.less');
+            var lesshint = new Lesshint();
+            var config = {
+                excludedFiles: ['**/excluded-files/*']
+            };
+
+            lesshint.configure(config);
+
+            return lesshint.checkPath(testPath).then(function (result) {
+                expect(result).to.have.length(0);
+            });
+        });
+
+        it('should ignore excluded folders', function () {
+            var testPath = path.join(path.dirname(__dirname), '/data/excluded-files');
+            var lesshint = new Lesshint();
+            var config = {
+                excludedFiles: ['**/excluded-files/*']
+            };
+
+            lesshint.configure(config);
+
+            return lesshint.checkPath(testPath).then(function (result) {
+                expect(result).to.have.length(0);
+            });
+        });
     });
 
     describe('checkString', function () {
@@ -185,13 +227,23 @@ describe('lesshint', function () {
     });
 
     describe('getReporter', function () {
-        it('should load the specified reporter', function () {
+        it('should load the specified reporter by path', function () {
             var lesshint = new Lesshint();
             var reporter = lesshint.getReporter(path.resolve(process.cwd() + '/lib/reporters/default.js'));
 
             expect(reporter.name).to.equal('default');
         });
 
+        it('should load the specified reporter directly', function () {
+            var lesshint = new Lesshint();
+            var reporter = lesshint.getReporter({
+                name: 'test',
+                report: function () {}
+            });
+
+            expect(reporter.name).to.equal('test');
+        });
+
         it('should load the default when nothing is passed', function () {
             var lesshint = new Lesshint();
             var reporter = lesshint.getReporter();
diff --git a/test/specs/linter.js b/test/specs/linter.js
index 9453dde..d7e7eab 100644
--- a/test/specs/linter.js
+++ b/test/specs/linter.js
@@ -7,6 +7,11 @@ var fs = require('fs');
 describe('linter', function () {
     var linter = require('../../lib/linter');
 
+    var readFileSync = function (filePath) {
+        return fs.readFileSync(path.resolve(process.cwd(), filePath), 'utf8')
+            .replace(/\r\n|\r|\n/g, '\n'); // Normalize line endings
+    };
+
     describe('lint', function () {
         it('should return array of errors', function () {
             var source = '.foo{ color:red; }\n';
@@ -41,6 +46,7 @@ describe('linter', function () {
                 line: 2,
                 linter: 'spaceAfterPropertyColon',
                 message: 'Colon after property name should be followed by one space.',
+                position: 22,
                 severity: 'warning',
                 source: ' margin-right:10px; }'
             }];
@@ -69,6 +75,7 @@ describe('linter', function () {
                 line: 1,
                 linter: 'spaceBeforeBrace',
                 message: 'Opening curly brace should be preceded by one space.',
+                position: 4,
                 severity: 'warning',
                 source: '.foo{ color: red; }'
             }];
@@ -98,6 +105,7 @@ describe('linter', function () {
                     line: 1,
                     linter: 'stringQuotes',
                     message: 'Strings should use single quotes.',
+                    position: 6,
                     severity: 'warning',
                     source: '[type="text"], [type=email] {'
                 },
@@ -108,6 +116,7 @@ describe('linter', function () {
                     line: 1,
                     linter: 'attributeQuotes',
                     message: 'Attribute selectors should use quotes.',
+                    position: 21,
                     severity: 'warning',
                     source: '[type="text"], [type=email] {'
                 },
@@ -118,6 +127,7 @@ describe('linter', function () {
                     line: 3,
                     linter: 'propertyOrdering',
                     message: 'Property ordering is not alphabetized',
+                    position: 50,
                     severity: 'warning',
                     source: 'color: red;'
                 },
@@ -128,6 +138,7 @@ describe('linter', function () {
                     line: 4,
                     linter: 'duplicateProperty',
                     message: 'Duplicate property: "color".',
+                    position: 62,
                     severity: 'warning',
                     source: 'color: blue;'
                 }
@@ -212,6 +223,7 @@ describe('linter', function () {
                 line: 1,
                 linter: 'spaceAfterPropertyColon',
                 message: 'Colon after property name should be followed by one space.',
+                position: 13,
                 severity: 'error',
                 source: '.foo { color:red; }'
             }];
@@ -230,7 +242,7 @@ describe('linter', function () {
         });
 
         it('should only care about the first inline comment', function () {
-            var source = fs.readFileSync(path.resolve(process.cwd(), './test/data/inline-options/file-options-enabled.less'), 'utf8');
+            var source = readFileSync('./test/data/inline-options/file-options-enabled.less');
             var result;
 
             var expected = [{
@@ -240,6 +252,7 @@ describe('linter', function () {
                 line: 6,
                 linter: 'spaceBeforeBrace',
                 message: 'Opening curly brace should be preceded by one space.',
+                position: 95,
                 severity: 'warning',
                 source: '.bar{'
             }];
@@ -262,7 +275,7 @@ describe('linter', function () {
         });
 
         it('should not report rules that are disabled inline', function () {
-            var source = fs.readFileSync(path.resolve(process.cwd(), './test/data/inline-options/file-options-enabled.less'), 'utf8');
+            var source = readFileSync('./test/data/inline-options/file-options-enabled.less');
             var result;
 
             var expected = [{
@@ -272,6 +285,7 @@ describe('linter', function () {
                 line: 6,
                 linter: 'spaceBeforeBrace',
                 message: 'Opening curly brace should be preceded by one space.',
+                position: 95,
                 severity: 'warning',
                 source: '.bar{'
             }];
@@ -294,7 +308,7 @@ describe('linter', function () {
         });
 
         it('should allow disabled rules to be enabled inline', function () {
-            var source = fs.readFileSync(path.resolve(process.cwd(), './test/data/inline-options/file-options-disabled.less'), 'utf8');
+            var source = readFileSync('./test/data/inline-options/file-options-disabled.less');
             var result;
 
             var expected = [{
@@ -304,6 +318,7 @@ describe('linter', function () {
                 line: 3,
                 linter: 'spaceAfterPropertyColon',
                 message: 'Colon after property name should be followed by one space.',
+                position: 110,
                 severity: 'warning',
                 source: '    color:red;'
             }, {
@@ -313,6 +328,7 @@ describe('linter', function () {
                 line: 6,
                 linter: 'emptyRule',
                 message: "There shouldn't be any empty rules present.",
+                position: 124,
                 severity: 'warning',
                 source: '.bar {'
             }];
@@ -332,7 +348,7 @@ describe('linter', function () {
         });
 
         it('should not report rules that are disabled on a single line', function () {
-            var source = fs.readFileSync(path.resolve(process.cwd(), './test/data/inline-options/line-options.less'), 'utf8');
+            var source = readFileSync('./test/data/inline-options/line-options.less');
             var result;
 
             var expected = [{
@@ -342,6 +358,7 @@ describe('linter', function () {
                 line: 5,
                 linter: 'spaceBeforeBrace',
                 message: 'Opening curly brace should be preceded by one space.',
+                position: 70,
                 severity: 'warning',
                 source: '.bar{'
             }];
@@ -359,7 +376,7 @@ describe('linter', function () {
         });
 
         it('should report invalid inline options', function () {
-            var source = fs.readFileSync(path.resolve(process.cwd(), './test/data/inline-options/options-invalid.less'), 'utf8');
+            var source = readFileSync('./test/data/inline-options/options-invalid.less');
 
             expect(function () {
                 linter.lint(source, 'options-invalid.less', {});
@@ -393,6 +410,7 @@ describe('linter', function () {
                 line: 1,
                 linter: 'sample',
                 message: 'Sample error.',
+                position: 0,
                 severity: 'warning',
                 source: source.trim()
             }];
@@ -419,6 +437,7 @@ describe('linter', function () {
                 line: 1,
                 linter: 'sample',
                 message: 'Sample error.',
+                position: 0,
                 severity: 'warning',
                 source: source.trim()
             }];
diff --git a/test/specs/linters/single_line_per_property.js b/test/specs/linters/single_line_per_property.js
index 23ef320..f21e493 100644
--- a/test/specs/linters/single_line_per_property.js
+++ b/test/specs/linters/single_line_per_property.js
@@ -330,5 +330,65 @@ describe('lesshint', function () {
                 expect(result).to.be.undefined;
             });
         });
+
+        it('should allow single line rules when "allowSingleLineRules" is "true"', function () {
+            var source = '.foo { color: red; }';
+            var options = {
+                allowSingleLineRules: true
+            };
+
+            return spec.parse(source, function (ast) {
+                var result = spec.linter.lint(options, ast.root.first);
+
+                expect(result).to.be.undefined;
+            });
+        });
+
+        it('should not allow single line rules when "allowSingleLineRules" is "false"', function () {
+            var source = '.foo { color: red; }';
+            var options = {
+                allowSingleLineRules: false
+            };
+
+            var expected = [
+                {
+                    column: 8,
+                    line: 1,
+                    message: 'Each property should be on its own line.'
+                }
+            ];
+
+            return spec.parse(source, function (ast) {
+                var result = spec.linter.lint(options, ast.root.first);
+
+                expect(result).to.deep.equal(expected);
+            });
+        });
+
+        it('should not allow single line rules with multiple declarations when "allowSingleLineRules" is "true"', function () {
+            var source = '.foo { color: red; margin-right: 10px; }';
+            var options = {
+                allowSingleLineRules: true
+            };
+
+            var expected = [
+                {
+                    column: 8,
+                    line: 1,
+                    message: 'Each property should be on its own line.'
+                },
+                {
+                    column: 20,
+                    line: 1,
+                    message: 'Each property should be on its own line.'
+                }
+            ];
+
+            return spec.parse(source, function (ast) {
+                var result = spec.linter.lint(options, ast.root.first);
+
+                expect(result).to.deep.equal(expected);
+            });
+        });
     });
 });
diff --git a/test/specs/reporters/json.js b/test/specs/reporters/json.js
new file mode 100644
index 0000000..732b3ae
--- /dev/null
+++ b/test/specs/reporters/json.js
@@ -0,0 +1,43 @@
+/*eslint no-console: 0*/
+
+'use strict';
+
+var expect = require('chai').expect;
+var rewire = require('rewire');
+var sinon = require('sinon');
+
+describe('reporter:json', function () {
+    var reporter = rewire('../../../lib/reporters/json.js');
+
+    beforeEach(function () {
+        sinon.stub(process.stdout, 'write');
+    });
+
+    afterEach(function () {
+        if (process.stdout.write.restore) {
+            process.stdout.write.restore();
+        }
+    });
+
+    it('should print an error completely', function () {
+        var message;
+        var errors = [{
+            line: 1,
+            file: 'file.less',
+            linter: 'spaceBeforeBrace',
+            message: 'Opening curly brace should be preceded by one space.',
+            severity: 'error',
+            source: '.foo{ color: red; }'
+        }];
+
+        sinon.spy(console, 'log');
+
+        reporter.report(errors);
+
+        message = console.log.getCall(0).args[0];
+
+        expect(message).to.equal(JSON.stringify(errors));
+
+        console.log.restore();
+    });
+});