Repository: lesshint/lesshint (js)

Issue:
singleLinePerProperty rule should support single-line definitions
My team just started using this linter and it has already helped out tremendously.  One rule we would like to see altered however, is the `singleLinePerProperty` rule.  By name, this sounds like a rule that we should support, but it causes a warning on simple lines of this format:

```
.ion-help-circled { cursor: pointer; }
```

I feel like there should be a separate rule governing whether or not you should be allowed to put attribute definitions on the same line as the selector.  As it stands, we have to disable this rule, even though we would like to be able to catch definitions that look like:

```
.section-title {
    font-size: 16px; margin-bottom: 20px;
}
```


Pull request description:
Add allowSingleLineRules option to singleLinePerProperty
I'm not really satisfied with the `allowSingleLineRules` option name. Suggestions welcome!


Closes #269.

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/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/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/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 79c74a5..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);
         });
     });
@@ -385,6 +394,7 @@ describe('cli', function () {
 
         return result.fail(function (status) {
             console.log.restore();
+
             expect(status).to.equal(2);
         });
     });
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);
+            });
+        });
     });
 });