Repository: lesshint/lesshint
Pull request #376 (2017-04-22 11:05:36)

Refactor some common operations into utils
<!--
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?**
N/A

**Is there anything in this PR that needs extra explaining or should something specific be focused on?**
Not really, it should be pretty self-explanatory.


Patch:
diff --git a/docs/developer-guide/API.md b/docs/developer-guide/API.md
index f4bab60..38f047f 100644
--- a/docs/developer-guide/API.md
+++ b/docs/developer-guide/API.md
@@ -2,5 +2,6 @@
 
 * [Lesshint class](/docs/developer-guide/api/lesshint.md)
 * [Runner class](/docs/developer-guide/api/runner.md)
+* [Utils](/docs/developer-guide/api/utils.md)
 
 _Note: Anything not documented here should be considered a internal API which may change at any time._
diff --git a/docs/developer-guide/api/utils.md b/docs/developer-guide/api/utils.md
new file mode 100644
index 0000000..4245f4a
--- /dev/null
+++ b/docs/developer-guide/api/utils.md
@@ -0,0 +1,37 @@
+# Utils
+
+There are a copule of utilities included with `lesshint` which can be used when creating custom linters.
+
+```js
+const utils = require('lesshint').utils;
+```
+
+## `isAbsoluteURL`
+Check if a string is a absolute URL.
+
+```js
+utils.isAbsoluteURL('http://example.com'); // true
+utils.isAbsoluteURL('file.less'); // false
+```
+
+## `isVariable()`
+Check if a value is a Less variable.
+
+```js
+utils.isVariable('@foo'); // true
+utils.isVariable('blue'); // false
+```
+
+## `parseSelector`
+Parse a selector using [`postcss-selector-parser`](https://github.com/postcss/postcss-selector-parser).
+
+```js
+utils.parseSelector('.foo'); // Parser tree
+```
+
+## `parseValue`
+Parse a value using [`postcss-values-parser`](https://github.com/lesshint/postcss-values-parser).
+
+```js
+utils.parseValue('10px'); // Parser tree
+```
diff --git a/lib/index.js b/lib/index.js
index ead25f6..a106d84 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -2,5 +2,6 @@
 
 module.exports = {
     Lesshint: require('./lesshint'),
-    Runner: require('./runner')
+    Runner: require('./runner'),
+    utils: require('./utils')
 };
diff --git a/lib/linters/attribute_quotes.js b/lib/linters/attribute_quotes.js
index 852530c..0cfae4a 100644
--- a/lib/linters/attribute_quotes.js
+++ b/lib/linters/attribute_quotes.js
@@ -1,6 +1,6 @@
 'use strict';
 
-const parser = require('postcss-selector-parser');
+const parseSelector = require('../utils/parse-selector');
 
 module.exports = {
     name: 'attributeQuotes',
@@ -12,22 +12,21 @@ module.exports = {
             return;
         }
 
+        const selectors = parseSelector(node.selector);
         const results = [];
 
-        parser((selectors) => {
-            selectors.walkAttributes((selector) => {
-                if (selector.operator && !selector.quoted) {
-                    const column = node.source.start.column + selector.source.start.column
-                             + selector.attribute.length + selector.operator.length;
+        selectors.walkAttributes((selector) => {
+            if (selector.operator && !selector.quoted) {
+                const column = node.source.start.column + selector.source.start.column
+                         + selector.attribute.length + selector.operator.length;
 
-                    results.push({
-                        column: column,
-                        line: node.source.start.line + selector.source.start.line - 1,
-                        message: this.message
-                    });
-                }
-            });
-        }).process(node.selector);
+                results.push({
+                    column: column,
+                    line: node.source.start.line + selector.source.start.line - 1,
+                    message: this.message
+                });
+            }
+        });
 
         if (results.length) {
             return results;
diff --git a/lib/linters/decimal_zero.js b/lib/linters/decimal_zero.js
index 0d6c9c3..9ff58e6 100644
--- a/lib/linters/decimal_zero.js
+++ b/lib/linters/decimal_zero.js
@@ -1,6 +1,6 @@
 'use strict';
 
-const parser = require('postcss-values-parser');
+const parseValue = require('../utils/parse-value');
 const util = require('util');
 
 module.exports = {
@@ -9,10 +9,7 @@ module.exports = {
     message: '%s should be written %s %s zero.',
 
     lint: function decimalZeroLinter (config, node) {
-        const ast = parser(node.value, {
-            loose: true
-        }).parse();
-
+        const ast = parseValue(node.value);
         const results = [];
 
         ast.walk((child) => {
diff --git a/lib/linters/hex_length.js b/lib/linters/hex_length.js
index e3bebe5..f679d26 100644
--- a/lib/linters/hex_length.js
+++ b/lib/linters/hex_length.js
@@ -1,6 +1,6 @@
 'use strict';
 
-const parser = require('postcss-values-parser');
+const parseValue = require('../utils/parse-value');
 const util = require('util');
 
 module.exports = {
@@ -15,10 +15,7 @@ module.exports = {
          */
         node.value = node.value.replace(/;$/, '');
 
-        const ast = parser(node.value, {
-            loose: true
-        }).parse();
-
+        const ast = parseValue(node.value);
         const results = [];
 
         ast.first.walk((child) => {
diff --git a/lib/linters/hex_notation.js b/lib/linters/hex_notation.js
index 8157cb5..aa826f1 100644
--- a/lib/linters/hex_notation.js
+++ b/lib/linters/hex_notation.js
@@ -1,6 +1,6 @@
 'use strict';
 
-const parser = require('postcss-values-parser');
+const parseValue = require('../utils/parse-value');
 const util = require('util');
 
 module.exports = {
@@ -14,21 +14,19 @@ module.exports = {
             uppercase: /^#[0-9A-Z]+$/
         };
 
+        if (config.style && !styles[config.style]) {
+            throw new Error(`Invalid setting value for hexNotation: ${ config.style }`);
+        }
+
         /**
          * Just in case, since postcss will sometimes include the semicolon
          * in declaration values
          */
         node.value = node.value.replace(/;$/, '');
 
-        const ast = parser(node.value, {
-            loose: true
-        }).parse();
-
-        if (config.style && !styles[config.style]) {
-            throw new Error(`Invalid setting value for hexNotation: ${ config.style }`);
-        }
-
+        const ast = parseValue(node.value);
         const results = [];
+
         ast.first.walk((child) => {
             if (child.type !== 'word' || !/^#/.test(child.value)) {
                 return;
diff --git a/lib/linters/hex_validation.js b/lib/linters/hex_validation.js
index df6758a..e7c41d1 100644
--- a/lib/linters/hex_validation.js
+++ b/lib/linters/hex_validation.js
@@ -1,6 +1,6 @@
 'use strict';
 
-const parser = require('postcss-values-parser');
+const parseValue = require('../utils/parse-value');
 const util = require('util');
 
 module.exports = {
@@ -18,10 +18,7 @@ module.exports = {
          */
         node.value = node.value.replace(/;$/, '');
 
-        const ast = parser(node.value, {
-            loose: true
-        }).parse();
-
+        const ast = parseValue(node.value);
         const results = [];
 
         ast.first.walk((child) => {
diff --git a/lib/linters/id_selector.js b/lib/linters/id_selector.js
index 3f2b1f8..9553496 100644
--- a/lib/linters/id_selector.js
+++ b/lib/linters/id_selector.js
@@ -1,6 +1,6 @@
 'use strict';
 
-const parser = require('postcss-selector-parser');
+const parseSelector = require('../utils/parse-selector');
 
 module.exports = {
     name: 'idSelector',
@@ -8,14 +8,8 @@ module.exports = {
     message: 'Selectors should not use IDs.',
 
     lint: function idSelectorLinter (config, node) {
-        let tree;
-
-        parser(function (selectors) {
-            tree = selectors;
-        })
-        .process(node.selector);
-
-        const excludes = config.exclude.map(function (id) {
+        const tree = parseSelector(node.selector);
+        const excludes = config.exclude.map((id) => {
             // Remove #
             return id.replace(/^\#/, '');
         });
diff --git a/lib/linters/import_path.js b/lib/linters/import_path.js
index 64954ef..118607c 100644
--- a/lib/linters/import_path.js
+++ b/lib/linters/import_path.js
@@ -1,7 +1,7 @@
 'use strict';
 
-const parser = require('postcss-values-parser');
-const helpers = require('../helpers');
+const isAbsoluteURL = require('../utils/is-absolute-url');
+const parseValue = require('../utils/parse-value');
 const path = require('path');
 const util = require('util');
 
@@ -24,9 +24,7 @@ module.exports = {
          */
         const importOptsRx = /(\s+)?\((\s+)?(reference|inline|less|css|once|multiple|optional)(\s+)?\)(\s+)?/gi;
         const params = node.params.replace(importOptsRx, '');
-        const ast = parser(params, {
-            loose: true
-        }).parse();
+        const ast = parseValue(params);
 
         let value = ast.first.first;
 
@@ -37,13 +35,8 @@ module.exports = {
 
         const file = value.value.trim().replace(/['"]/g, '');
 
-        // Ignore absolute URLs
-        if (helpers.isAbsoluteURL(file)) {
-            return;
-        }
-
-        // Check if file is excluded
-        if (config.exclude.indexOf(file) !== -1) {
+        // Exit if the path is a absolute URL or excluded
+        if (isAbsoluteURL(file) || config.exclude.indexOf(file) !== -1) {
             return;
         }
 
diff --git a/lib/linters/property_ordering.js b/lib/linters/property_ordering.js
index a9efb33..8817a2f 100644
--- a/lib/linters/property_ordering.js
+++ b/lib/linters/property_ordering.js
@@ -1,5 +1,7 @@
 'use strict';
 
+const isVariable = require('../utils/is-variable');
+
 module.exports = {
     name: 'propertyOrdering',
     nodeTypes: ['rule'],
@@ -22,7 +24,7 @@ module.exports = {
             const property = child.prop;
 
             // Ignore declarations without a property and variables
-            if (!property || (property && /^@/.test(property))) {
+            if (!property || isVariable(property)) {
                 return;
             }
 
diff --git a/lib/linters/property_units.js b/lib/linters/property_units.js
index bbeb5b9..329cb19 100644
--- a/lib/linters/property_units.js
+++ b/lib/linters/property_units.js
@@ -1,6 +1,7 @@
 'use strict';
 
-const parser = require('postcss-values-parser');
+const isVariable = require('../utils/is-variable');
+const parseValue = require('../utils/parse-value');
 const util = require('util');
 
 module.exports = {
@@ -13,8 +14,7 @@ module.exports = {
     lint: function propertyUnitsLinter (config, node) {
         const property = node.prop;
 
-        // Ignore variables
-        if (property.substring(0, 1) === '@') {
+        if (isVariable(property)) {
             return;
         }
 
@@ -24,10 +24,7 @@ module.exports = {
         config.properties = config.properties || {};
 
         const allowed = config.properties[property];
-        const ast = parser(node.value, {
-            loose: true
-        }).parse();
-
+        const ast = parseValue(node.value);
         const results = [];
 
         ast.first.walk((value) => {
diff --git a/lib/linters/qualifying_element.js b/lib/linters/qualifying_element.js
index b0cc38f..1a70e87 100644
--- a/lib/linters/qualifying_element.js
+++ b/lib/linters/qualifying_element.js
@@ -1,6 +1,6 @@
 'use strict';
 
-const parser = require('postcss-selector-parser');
+const parseSelector = require('../utils/parse-selector');
 const util = require('util');
 
 module.exports = {
@@ -40,66 +40,65 @@ module.exports = {
     },
 
     lint: function qualifyingElementLinter (config, node) {
+        const selectors = parseSelector(node.selector);
         const selectorTypes = ['nesting', 'tag'];
         const results = [];
 
-        parser((selectors) => {
-            node.selectorAst = selectors;
+        node.selectorAst = selectors;
 
-            selectors.each((selector) => {
-                let result;
+        selectors.each((selector) => {
+            let result;
 
-                selector.nodes.forEach((element, index) => {
-                    if (selectorTypes.indexOf(element.type) === -1) {
-                        return;
-                    }
+            selector.nodes.forEach((element, index) => {
+                if (selectorTypes.indexOf(element.type) === -1) {
+                    return;
+                }
 
-                    // Fetch the next node to check it
-                    const next = selector.at(index + 1);
+                // Fetch the next node to check it
+                const next = selector.at(index + 1);
 
-                    if (!next) {
-                        return;
-                    }
+                if (!next) {
+                    return;
+                }
 
-                    const parent = this.inspectParent(node);
+                const parent = this.inspectParent(node);
 
-                    switch (next.type) {
-                        case 'attribute':
-                            if (config.allowWithAttribute) {
-                                return;
-                            }
+                switch (next.type) {
+                    case 'attribute':
+                        if (config.allowWithAttribute) {
+                            return;
+                        }
 
-                            result = next;
+                        result = next;
 
-                            break;
-                        case 'class':
-                            if (config.allowWithClass || (parent.startsWith === 'class' && !parent.hasTag)) {
-                                return;
-                            }
+                        break;
+                    case 'class':
+                        if (config.allowWithClass || (parent.startsWith === 'class' && !parent.hasTag)) {
+                            return;
+                        }
 
-                            result = next;
+                        result = next;
 
-                            break;
-                        case 'id':
-                            if (config.allowWithId) {
-                                return;
-                            }
+                        break;
+                    case 'id':
+                        if (config.allowWithId) {
+                            return;
+                        }
 
-                            result = next;
+                        result = next;
 
-                            break;
-                    }
+                        break;
+                }
 
-                    if (result) {
-                        results.push({
-                            column: node.source.start.column + result.source.start.column - 1,
-                            line: node.source.start.line + result.source.start.line - 1,
-                            message: util.format(this.message, result.type.charAt(0).toUpperCase() + result.type.substring(1))
-                        });
-                    }
-                });
+                if (result) {
+                    results.push({
+                        column: node.source.start.column + result.source.start.column - 1,
+                        line: node.source.start.line + result.source.start.line - 1,
+                        message: util.format(this.message, result.type.charAt(0).toUpperCase() + result.type.substring(1))
+                    });
+                }
             });
-        }).process(node.selector);
+        });
 
         if (results.length) {
             return results;
diff --git a/lib/linters/selector_naming.js b/lib/linters/selector_naming.js
index a5ab7c4..aa82635 100644
--- a/lib/linters/selector_naming.js
+++ b/lib/linters/selector_naming.js
@@ -1,6 +1,6 @@
 'use strict';
 
-const parser = require('postcss-selector-parser');
+const parseSelector = require('../utils/parse-selector');
 const util = require('util');
 
 module.exports = {
@@ -9,35 +9,34 @@ module.exports = {
     message: 'Selector "%s" should follow naming conventions.',
 
     lint: function selectorNamingLinter (config, node) {
+        const selectors = parseSelector(node.selector);
         const selectorTypes = ['class', 'id'];
-        const exclude = config.exclude;
+        const exclude = config.exclude || [];
         const results = [];
 
-        parser((selectors) => {
-            selectors.walk((selector) => {
-                if (selectorTypes.indexOf(selector.type) === -1) {
-                    return;
-                }
-
-                const name = selector.value;
-
-                if (exclude && exclude.indexOf(name) !== -1) {
-                    return;
-                }
-
-                if (
-                    (config.disallowUppercase && name.toLowerCase() !== name) ||
-                    (config.disallowUnderscore && name.indexOf('_') !== -1) ||
-                    (config.disallowDash && name.indexOf('-') !== -1)
-                ) {
-                    results.push({
-                        column: node.source.start.column + selector.source.start.column - 1,
-                        line: node.source.start.line + selector.source.start.line - 1,
-                        message: util.format(this.message, name)
-                    });
-                }
-            });
-        }).process(node.selector);
+        selectors.walk((selector) => {
+            if (selectorTypes.indexOf(selector.type) === -1) {
+                return;
+            }
+
+            const name = selector.value;
+
+            if (exclude.indexOf(name) !== -1) {
+                return;
+            }
+
+            if (
+                (config.disallowUppercase && name.toLowerCase() !== name) ||
+                (config.disallowUnderscore && name.indexOf('_') !== -1) ||
+                (config.disallowDash && name.indexOf('-') !== -1)
+            ) {
+                results.push({
+                    column: node.source.start.column + selector.source.start.column - 1,
+                    line: node.source.start.line + selector.source.start.line - 1,
+                    message: util.format(this.message, name)
+                });
+            }
+        });
 
         if (results.length) {
             return results;
diff --git a/lib/linters/single_line_per_selector.js b/lib/linters/single_line_per_selector.js
index 4874071..fa1ff3b 100644
--- a/lib/linters/single_line_per_selector.js
+++ b/lib/linters/single_line_per_selector.js
@@ -1,6 +1,6 @@
 'use strict';
 
-const parser = require('postcss-selector-parser');
+const parseSelector = require('../utils/parse-selector');
 
 module.exports = {
     name: 'singleLinePerSelector',
@@ -8,13 +8,10 @@ module.exports = {
     message: 'Each selector should be on its own line.',
 
     lint: function singleLinePerSelectorLinter (config, node) {
+        const tree = parseSelector(node.selector);
         const results = [];
-        let valid = true;
-        let tree;
 
-        parser((result) => {
-            tree = result;
-        }).process(node.selector);
+        let valid = true;
 
         tree.each((selector) => {
             let reported = false;
diff --git a/lib/linters/space_after_property_value.js b/lib/linters/space_after_property_value.js
index 88c3ae3..b420ce2 100644
--- a/lib/linters/space_after_property_value.js
+++ b/lib/linters/space_after_property_value.js
@@ -12,7 +12,7 @@ module.exports = {
 
         switch (config.style) {
             case 'no_space':
-                if (node.raws.value && /.*\s$/.test(node.raws.value.raw)) {
+                if (/.*\s$/.test(node.raws.important) || node.raws.value && /.*\s$/.test(node.raws.value.raw)) {
                     results.push({
                         column: node.source.start.column + node.prop.length + node.raws.between.length + node.value.length,
                         line: node.source.end.line,
@@ -22,7 +22,7 @@ module.exports = {
 
                 break;
             case 'one_space':
-                if (!node.raws.value) {
+                if (!/.*\s$/.test(node.raws.important) && !node.raws.value) {
                     results.push({
                         column: node.source.start.column + node.prop.length + node.raws.between.length + node.value.length,
                         line: node.source.end.line,
diff --git a/lib/linters/space_around_comma.js b/lib/linters/space_around_comma.js
index 25daeee..27807b1 100644
--- a/lib/linters/space_around_comma.js
+++ b/lib/linters/space_around_comma.js
@@ -1,6 +1,6 @@
 'use strict';
 
-const parser = require('postcss-values-parser');
+const parseValue = require('../utils/parse-value');
 const util = require('util');
 
 module.exports = {
@@ -22,10 +22,7 @@ module.exports = {
             return;
         }
 
-        const ast = parser(value, {
-            loose: true
-        }).parse();
-
+        const ast = parseValue(value);
         const results = [];
 
         ast.walk((child) => {
diff --git a/lib/linters/space_around_operator.js b/lib/linters/space_around_operator.js
index 1038e13..d54f582 100644
--- a/lib/linters/space_around_operator.js
+++ b/lib/linters/space_around_operator.js
@@ -1,6 +1,6 @@
 'use strict';
 
-const parser = require('postcss-values-parser');
+const parseValue = require('../utils/parse-value');
 const util = require('util');
 
 module.exports = {
@@ -9,10 +9,7 @@ module.exports = {
     message: 'Operators should%s be %s by %s space.',
 
     lint: function spaceAroundOperatorLinter (config, node) {
-        const ast = parser(node.value, {
-            loose: true
-        }).parse();
-
+        const ast = parseValue(node.value);
         const results = [];
 
         ast.walk((child) => {
diff --git a/lib/linters/space_between_parens.js b/lib/linters/space_between_parens.js
index 5faf547..69b97d8 100644
--- a/lib/linters/space_between_parens.js
+++ b/lib/linters/space_between_parens.js
@@ -1,6 +1,6 @@
 'use strict';
 
-const parser = require('postcss-values-parser');
+const parseValue = require('../utils/parse-value');
 const util = require('util');
 
 module.exports = {
@@ -25,10 +25,7 @@ module.exports = {
             return;
         }
 
-        const ast = parser(value, {
-            loose: true
-        }).parse();
-
+        const ast = parseValue(value);
         const results = [];
 
         ast.walk((child) => {
diff --git a/lib/linters/string_quotes.js b/lib/linters/string_quotes.js
index 537ed01..68ed78c 100644
--- a/lib/linters/string_quotes.js
+++ b/lib/linters/string_quotes.js
@@ -1,7 +1,7 @@
 'use strict';
 
-const selectorParser = require('postcss-selector-parser');
-const valuesParser = require('postcss-values-parser');
+const parseSelector = require('../utils/parse-selector');
+const parseValue = require('../utils/parse-value');
 const util = require('util');
 
 module.exports = {
@@ -47,9 +47,7 @@ module.exports = {
                 });
             }
         } else if (node.type === 'decl') {
-            const ast = valuesParser(node.value, {
-                loose: true
-            }).parse();
+            const ast = parseValue(node.value);
 
             ast.first.walk((decl) => {
                 if (decl.type !== 'string') {
@@ -71,20 +69,20 @@ module.exports = {
                 }
             });
         } else {
-            selectorParser((selectors) => {
-                selectors.walkAttributes((selector) => {
-                    if (selector.quoted && !quotes[config.style].test(selector.value)) {
-                        const column = node.source.start.column + selector.source.start.column
-                                 + selector.attribute.length + selector.operator.length;
+            const selectors = parseSelector(node.selector);
 
-                        results.push({
-                            column: column,
-                            line: node.source.start.line,
-                            message: util.format(this.message, config.style)
-                        });
-                    }
-                });
-            }).process(node.selector);
+            selectors.walkAttributes((selector) => {
+                if (selector.quoted && !quotes[config.style].test(selector.value)) {
+                    const column = node.source.start.column + selector.source.start.column
+                             + selector.attribute.length + selector.operator.length;
+
+                    results.push({
+                        column: column,
+                        line: node.source.start.line,
+                        message: util.format(this.message, config.style)
+                    });
+                }
+            });
         }
 
         if (results.length) {
diff --git a/lib/linters/universal_selector.js b/lib/linters/universal_selector.js
index 3985bd9..36648c5 100644
--- a/lib/linters/universal_selector.js
+++ b/lib/linters/universal_selector.js
@@ -1,6 +1,6 @@
 'use strict';
 
-const parser = require('postcss-selector-parser');
+const parseSelector = require('../utils/parse-selector');
 
 module.exports = {
     name: 'universalSelector',
@@ -8,13 +8,7 @@ module.exports = {
     message: "The universal selector shouldn't be used.",
 
     lint: function universalSelectorLinter (config, node) {
-        let tree;
-
-        parser(function (selectors) {
-            tree = selectors;
-        })
-        .process(node.selector);
-
+        const tree = parseSelector(node.selector);
         const results = [];
 
         tree.each((selector) => {
diff --git a/lib/linters/url_format.js b/lib/linters/url_format.js
index 2d1bf9f..f8322e2 100644
--- a/lib/linters/url_format.js
+++ b/lib/linters/url_format.js
@@ -1,7 +1,7 @@
 'use strict';
 
-const parser = require('postcss-values-parser');
-const helpers = require('../helpers');
+const isAbsoluteURL = require('../utils/is-absolute-url');
+const parseValue = require('../utils/parse-value');
 const util = require('util');
 
 module.exports = {
@@ -11,9 +11,9 @@ module.exports = {
 
     lint: function urlFormatLinter (config, decl) {
         const style = {
-            absolute: helpers.isAbsoluteURL,
+            absolute: isAbsoluteURL,
             relative: (url) => {
-                return !helpers.isAbsoluteURL(url);
+                return !isAbsoluteURL(url);
             }
         };
 
@@ -25,10 +25,7 @@ module.exports = {
          * Traverse all child value-nodes
          * https://github.com/lesshint/lesshint/issues/225
          */
-        const ast = parser(decl.params || decl.value, {
-            loose: true
-        }).parse();
-
+        const ast = parseValue(decl.params || decl.value);
         const results = [];
 
         ast.first.nodes.forEach((child) => {
diff --git a/lib/linters/url_quotes.js b/lib/linters/url_quotes.js
index b4c52f4..ecbd708 100644
--- a/lib/linters/url_quotes.js
+++ b/lib/linters/url_quotes.js
@@ -1,6 +1,7 @@
 'use strict';
 
-const parser = require('postcss-values-parser');
+const isVariable = require('../utils/is-variable');
+const parseValue = require('../utils/parse-value');
 
 module.exports = {
     name: 'urlQuotes',
@@ -21,9 +22,7 @@ module.exports = {
          * Traverse all child value-nodes
          * https://github.com/lesshint/lesshint/issues/225
          */
-        const ast = parser(node.params || node.value, {
-            loose: true
-        }).parse();
+        const ast = parseValue(node.params || node.value);
 
         ast.first.nodes.forEach((child) => {
             if (child.type !== 'func' || child.value !== 'url') {
@@ -34,8 +33,7 @@ module.exports = {
             const uri = child.first.next();
             const value = uri.value.trim();
 
-            // Ignore variables
-            if (value.substring(0, 1) === '@') {
+            if (isVariable(value)) {
                 return;
             }
 
diff --git a/lib/linters/variable_value.js b/lib/linters/variable_value.js
index 6e2fe4e..6a308e8 100644
--- a/lib/linters/variable_value.js
+++ b/lib/linters/variable_value.js
@@ -1,5 +1,6 @@
 'use strict';
 
+const isVariable = require('../utils/is-variable');
 const util = require('util');
 
 module.exports = {
@@ -8,10 +9,8 @@ module.exports = {
     message: '%s is not allowed in "%s" property.',
 
     lint: function variableValueLinter (config, node) {
-        const variableRegex = /@/i;
-
         if (config.never && config.never.indexOf(node.prop) !== -1) {
-            if (variableRegex.test(node.value)) {
+            if (isVariable(node.value)) {
                 return [{
                     column: node.source.start.column +
                             node.prop.length +
@@ -23,7 +22,7 @@ module.exports = {
         }
 
         if (config.always && config.always.indexOf(node.prop) !== -1) {
-            if (!variableRegex.test(node.value)) {
+            if (!isVariable(node.value)) {
                 return [{
                     column: node.source.start.column +
                             node.prop.length +
diff --git a/lib/linters/zero_unit.js b/lib/linters/zero_unit.js
index 65116bc..eefda70 100644
--- a/lib/linters/zero_unit.js
+++ b/lib/linters/zero_unit.js
@@ -1,6 +1,6 @@
 'use strict';
 
-const parser = require('postcss-values-parser');
+const parseValue = require('../utils/parse-value');
 const util = require('util');
 
 module.exports = {
@@ -29,10 +29,7 @@ module.exports = {
             return;
         }
 
-        const ast = parser(node.value, {
-            loose: true
-        }).parse();
-
+        const ast = parseValue(node.value);
         const results = [];
 
         ast.first.each((child) => {
diff --git a/lib/utils/index.js b/lib/utils/index.js
new file mode 100644
index 0000000..05b4303
--- /dev/null
+++ b/lib/utils/index.js
@@ -0,0 +1,8 @@
+'use strict';
+
+module.exports = {
+    isAbsoluteURL: require('./is-absolute-url'),
+    isVariable: require('./is-variable'),
+    parseSelector: require('./parse-selector'),
+    parseValue: require('./parse-value')
+};
diff --git a/lib/helpers.js b/lib/utils/is-absolute-url.js
similarity index 54%
rename from lib/helpers.js
rename to lib/utils/is-absolute-url.js
index 110503c..1a45182 100644
--- a/lib/helpers.js
+++ b/lib/utils/is-absolute-url.js
@@ -1,5 +1,5 @@
 'use strict';
 
-module.exports.isAbsoluteURL = function (str) {
+module.exports = function (str) {
     return /^(?:\w+:)?\/\//.test(str);
 };
diff --git a/lib/utils/is-variable.js b/lib/utils/is-variable.js
new file mode 100644
index 0000000..93ec2d4
--- /dev/null
+++ b/lib/utils/is-variable.js
@@ -0,0 +1,5 @@
+'use strict';
+
+module.exports = (str) => {
+    return /^@/.test(str);
+};
diff --git a/lib/utils/parse-selector.js b/lib/utils/parse-selector.js
new file mode 100644
index 0000000..5d2163b
--- /dev/null
+++ b/lib/utils/parse-selector.js
@@ -0,0 +1,14 @@
+'use strict';
+
+const parser = require('postcss-selector-parser');
+
+module.exports = function (selector) {
+    let tree;
+
+    parser((selectors) => {
+        tree = selectors;
+    })
+    .process(selector);
+
+    return tree;
+};
diff --git a/lib/utils/parse-value.js b/lib/utils/parse-value.js
new file mode 100644
index 0000000..dffc099
--- /dev/null
+++ b/lib/utils/parse-value.js
@@ -0,0 +1,11 @@
+'use strict';
+
+const parser = require('postcss-values-parser');
+
+module.exports = function (value) {
+    const tree = parser(value, {
+        loose: true
+    }).parse();
+
+    return tree;
+};


Test patch:
diff --git a/test/specs/index.js b/test/specs/index.js
index 0b66811..35a552e 100644
--- a/test/specs/index.js
+++ b/test/specs/index.js
@@ -12,4 +12,8 @@ describe('lesshint', function () {
     it('should expose Runner class in public API', function () {
         expect(api.Runner).to.be.a('function');
     });
+
+    it('should expose utils class in public API', function () {
+        expect(api.utils).to.be.a('object');
+    });
 });
diff --git a/test/specs/linters/space_after_property_value.js b/test/specs/linters/space_after_property_value.js
index 493e6ce..c023596 100644
--- a/test/specs/linters/space_after_property_value.js
+++ b/test/specs/linters/space_after_property_value.js
@@ -26,6 +26,19 @@ describe('lesshint', function () {
             });
         });
 
+        it('should allow a missing space after "!important" when "style" is "no_space"', function () {
+            const source = '.foo { color: red !important; }';
+            const options = {
+                style: 'no_space'
+            };
+
+            return spec.parse(source, function (ast) {
+                const result = spec.linter.lint(options, ast.root.first.first);
+
+                expect(result).to.be.undefined;
+            });
+        });
+
         it('should not allow any space when "style" is "no_space"', function () {
             const source = '.foo { color: red ; }';
             const expected = [{
@@ -45,6 +58,25 @@ describe('lesshint', function () {
             });
         });
 
+        it('should not allow any space after "!important" when "style" is "no_space"', function () {
+            const source = '.foo { color: red !important ; }';
+            const expected = [{
+                column: 18,
+                line: 1,
+                message: 'Semicolon after property value should not be preceded by any space.'
+            }];
+
+            const options = {
+                style: 'no_space'
+            };
+
+            return spec.parse(source, function (ast) {
+                const result = spec.linter.lint(options, ast.root.first.first);
+
+                expect(result).to.deep.equal(expected);
+            });
+        });
+
         it('should allow one space when "style" is "one_space"', function () {
             const source = '.foo { color: red ; }';
             const options = {
@@ -58,6 +90,19 @@ describe('lesshint', function () {
             });
         });
 
+        it('should allow one space after "!important" when "style" is "one_space"', function () {
+            const source = '.foo { color: red !important ; }';
+            const options = {
+                style: 'one_space'
+            };
+
+            return spec.parse(source, function (ast) {
+                const result = spec.linter.lint(options, ast.root.first.first);
+
+                expect(result).to.be.undefined;
+            });
+        });
+
         it('should not allow a missing space when "style" is "one_space"', function () {
             const source = '.foo { color: red; }';
             const expected = [{
@@ -77,6 +122,24 @@ describe('lesshint', function () {
             });
         });
 
+        it('should not allow a missing space after "!important" when "style" is "one_space"', function () {
+            const source = '.foo { color: red !important; }';
+            const expected = [{
+                column: 18,
+                line: 1,
+                message: 'Semicolon after property value should be preceded by one space.'
+            }];
+
+            const options = {
+                style: 'one_space'
+            };
+
+            return spec.parse(source, function (ast) {
+                const result = spec.linter.lint(options, ast.root.first.first);
+
+                expect(result).to.deep.equal(expected);
+            });
+        });
 
         it('should throw on invalid "style" value', function () {
             const source = '.foo { color: red ; }';