Repository: lesshint/lesshint (js)

Issue:
Report absolute line position
For tools (like automutate) that manipulate files as `string[]`s rather than `string[][]` it can be useful to have the absolute position of a complaint, in addition to line & column number.

I think we just compute these by creating a new `linesRaw` that has lines *with* their trailing newline characters.

Required for #278.

Pull request:
Absolute positions
Fixes #287.

It's computed by keeping the "raw" (trailing endlines included) lines from inputs.

Note that these are 0-based indices instead of 1-based like columns and lines.

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..733c210 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,4 +1,10 @@
 # Changelog
+## 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/lib/lesshint.js b/lib/lesshint.js
index a866229..255ac72 100644
--- a/lib/lesshint.js
+++ b/lib/lesshint.js
@@ -127,6 +127,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/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..8432e73 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.3.0",
   "main": "./lib/lesshint.js",
   "author": {
     "name": "Jonathan Wilsson",


Test patch:
diff --git a/test/specs/cli.js b/test/specs/cli.js
index 9e78a8d..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,9 @@ describe('cli', function () {
         });
 
         return result.fail(function (status) {
-            expect(status).to.equal(1);
-
             console.log.restore();
+
+            expect(status).to.equal(1);
         });
     });
 
@@ -234,6 +238,7 @@ describe('cli', function () {
 
         return result.fail(function (status) {
             console.log.restore();
+
             expect(status).to.equal(1);
         });
     });
@@ -250,6 +255,7 @@ describe('cli', function () {
 
         return result.fail(function (status) {
             console.log.restore();
+
             expect(status).to.equal(1);
         });
     });
@@ -267,6 +273,7 @@ describe('cli', function () {
 
         return result.fail(function (status) {
             console.log.restore();
+
             expect(status).to.equal(1);
         });
     });
@@ -284,6 +291,7 @@ describe('cli', function () {
 
         return result.fail(function (status) {
             console.log.restore();
+
             expect(status).to.equal(70);
         });
     });
@@ -301,6 +309,7 @@ describe('cli', function () {
 
         return result.fail(function (status) {
             console.log.restore();
+
             expect(status).to.equal(78);
         });
     });
@@ -331,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;
 
@@ -369,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..9f9a1b6 100644
--- a/test/specs/lesshint.js
+++ b/test/specs/lesshint.js
@@ -185,13 +185,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/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();
+    });
+});