Repository: lesshint/lesshint (js)

Issue:
urlFormat and urlQuotes only checks the first URL in a declaration
When declaring multiple URLs, for example:
```less
background-image: url(http://example.com/img/image.jpg), url(http://example.com/img/image2.jpg);
```

With this config:
```json
{
    "urlFormat": {
        "enabled": true,
        "style": "relative"
    },
    "urlQuotes": {
        "enabled": true
    }
}
```

Only the first URL will be reported by both linters.

Pull request description:
fixes #225: multiple urls in urlFormt, urlQuotes

Patch:
diff --git a/lib/linters/url_format.js b/lib/linters/url_format.js
index a70b427..5636301 100644
--- a/lib/linters/url_format.js
+++ b/lib/linters/url_format.js
@@ -18,31 +18,44 @@ module.exports = {
                 return !helpers.isAbsoluteURL(url);
             }
         };
-        var uri = ast.first.first;
-        var column;
+        var self = this;
+        var results = [];
 
         if (config.style && !style[config.style]) {
             throw new Error('Invalid setting value for urlFormat: ' + config.style);
         }
 
-        if (uri.type !== 'func' || uri.value !== 'url') {
-            return;
-        }
+        // traverse all child value-nodes
+        // https://github.com/lesshint/lesshint/issues/225
+        ast.first.nodes.forEach(function (child) {
+            var func = child;
+            var column;
+            var uri;
+
+            if (func.type !== 'func' || func.value !== 'url') {
+                return;
+            }
 
-        uri = uri.first.next();
+            // skip over the 'paren' node; the following node is the value node.
+            uri = func.first.next();
 
-        if (!style[config.style](uri.value)) {
+            if (!style[config.style](uri.value)) {
 
-            column = (decl.raws.between ? decl.raws.between.length : 0) +
-                     decl.source.start.column +
-                     decl.prop.length +
-                     uri.source.start.column - 1;
+                column = (decl.raws.between ? decl.raws.between.length : 0) +
+                         decl.source.start.column +
+                         decl.prop.length +
+                         uri.source.start.column - 1;
+
+                results.push({
+                    column: column,
+                    line: decl.source.start.line,
+                    message: util.format(self.message, uri.value, config.style)
+                });
+            }
+        });
 
-            return [{
-                column: column,
-                line: decl.source.start.line,
-                message: util.format(this.message, uri.value, config.style)
-            }];
+        if (results.length) {
+            return results;
         }
     }
 };
diff --git a/lib/linters/url_quotes.js b/lib/linters/url_quotes.js
index 8d60fe3..76cf80f 100644
--- a/lib/linters/url_quotes.js
+++ b/lib/linters/url_quotes.js
@@ -8,12 +8,11 @@ module.exports = {
     message: 'URLs should be enclosed in quotes.',
 
     lint: function urlQuotesLinter (config, node) {
+        var results = [];
         var rSingle = /^\'(.+)\'$/;
         var rDouble = /^\"(.+)\"$/;
-        var column;
-        var value;
+        var self = this;
         var ast;
-        var uri;
 
         // Nothing to check, bail
         if (!node.params && !node.value) {
@@ -21,32 +20,45 @@ module.exports = {
         }
 
         ast = parser(node.params || node.value).parse();
-        uri = ast.first.first;
 
-        if (uri.type !== 'func' || uri.value !== 'url') {
-            return;
-        }
+        // traverse all child value-nodes
+        // https://github.com/lesshint/lesshint/issues/225
+        ast.first.nodes.forEach(function (child) {
+            var func = child;
+            var uri;
+            var column;
+            var value;
+
+            if (func.type !== 'func' || func.value !== 'url') {
+                return;
+            }
+
+            // skip over the 'paren' node; the following node is the value node.
+            uri = func.first.next();
+            value = uri.value.trim();
+
+            // postcss-values-parser has a bug with url string params surrounded by
+            // spaces. so account for that here.
+            // tracking: https://github.com/lesshint/postcss-values-parser/issues/1
+            if ((uri.type === 'word' &&
+                !(rSingle.test(value) || rDouble.test(value))) &&
+                uri.type !== 'string') {
+
+                column = (node.raws.between ? node.raws.between.length : 0) +
+                         node.source.start.column +
+                         (node.prop || node.name).length +
+                         uri.source.start.column - 1;
+
+                results.push({
+                    column: column,
+                    line: node.source.start.line,
+                    message: self.message
+                });
+            }
+        });
 
-        uri = uri.first.next();
-        value = uri.value.trim();
-
-        // postcss-values-parser has a bug with url string params surrounded by
-        // spaces. so account for that here.
-        // tracking: https://github.com/lesshint/postcss-values-parser/issues/1
-        if ((uri.type === 'word' &&
-            !(rSingle.test(value) || rDouble.test(value))) &&
-            uri.type !== 'string') {
-
-            column = (node.raws.between ? node.raws.between.length : 0) +
-                     node.source.start.column +
-                     (node.prop || node.name).length +
-                     uri.source.start.column - 1;
-
-            return [{
-                column: column,
-                line: node.source.start.line,
-                message: this.message
-            }];
+        if (results.length) {
+            return results;
         }
     }
 };


Test patch:
diff --git a/test/specs/linters/url_format.js b/test/specs/linters/url_format.js
index b628f81..5499e29 100644
--- a/test/specs/linters/url_format.js
+++ b/test/specs/linters/url_format.js
@@ -26,6 +26,19 @@ describe('lesshint', function () {
             });
         });
 
+        it('should allow multiple relative URLs when "style" is "relative"', function () {
+            var source = '.foo { background-image: url(img/image.jpg), url(img/foo.jpg); }';
+            var options = {
+                style: 'relative'
+            };
+
+            return spec.parse(source, function (ast) {
+                var result = spec.linter.lint(options, ast.root.first.first);
+
+                expect(result).to.be.undefined;
+            });
+        });
+
         it('should not allow absolute URLs when "style" is "relative"', function () {
             var source = '.foo { background-image: url(http://example.com/img/image.jpg); }';
             var expected = [{
@@ -45,6 +58,32 @@ describe('lesshint', function () {
             });
         });
 
+        it('should not allow multiple absolute URLs when "style" is "relative"', function () {
+            var source = '.foo { background-image: url(http://example.com/img/image.jpg), url(http://example.com/img/foo.jpg); }';
+            var expected = [
+                {
+                    column: 30,
+                    line: 1,
+                    message: 'URL "http://example.com/img/image.jpg" should be relative.'
+                },
+                {
+                    column: 69,
+                    line: 1,
+                    message: 'URL "http://example.com/img/foo.jpg" should be relative.'
+                }
+            ];
+
+            var options = {
+                style: 'relative'
+            };
+
+            return spec.parse(source, function (ast) {
+                var result = spec.linter.lint(options, ast.root.first.first);
+
+                expect(result).to.deep.equal(expected);
+            });
+        });
+
         it('should allow absolute URLs when "style" is "absolute"', function () {
             var source = '.foo { background-image: url(http://example.com/img/image.jpg); }';
             var options = {
diff --git a/test/specs/linters/url_quotes.js b/test/specs/linters/url_quotes.js
index 7e7fa5c..f9de57a 100644
--- a/test/specs/linters/url_quotes.js
+++ b/test/specs/linters/url_quotes.js
@@ -51,6 +51,16 @@ describe('lesshint', function () {
             });
         });
 
+        it('should allow single quotes with multiple urls', function () {
+            var source = '.foo { background-image: url(\'img/image.jpg\'), url(\'img/foo.jpg\'); }';
+
+            return spec.parse(source, function (ast) {
+                var result = spec.linter.lint({}, ast.root.first.first);
+
+                expect(result).to.be.undefined;
+            });
+        });
+
         it('should allow double quotes', function () {
             var source = '.foo { background-image: url("img/image.jpg"); }';
 
@@ -61,6 +71,16 @@ describe('lesshint', function () {
             });
         });
 
+        it('should allow double quotes with multiple urls', function () {
+            var source = '.foo { background-image: url("img/image.jpg"), url("img/foo.jpg"); }';
+
+            return spec.parse(source, function (ast) {
+                var result = spec.linter.lint({}, ast.root.first.first);
+
+                expect(result).to.be.undefined;
+            });
+        });
+
         it('should not allow missing quotes', function () {
             var source = '.foo { background-image: url(img/image.jpg); }';
             var expected = [{
@@ -76,6 +96,28 @@ describe('lesshint', function () {
             });
         });
 
+        it('should not allow missing quotes with multiple urls', function () {
+            var source = '.foo { background-image: url(img/image.jpg), url(img/foo.jpg); }';
+            var expected = [
+                {
+                    column: 30,
+                    line: 1,
+                    message: 'URLs should be enclosed in quotes.'
+                },
+                {
+                    column: 50,
+                    line: 1,
+                    message: 'URLs should be enclosed in quotes.'
+                }
+            ];
+
+            return spec.parse(source, function (ast) {
+                var result = spec.linter.lint({}, ast.root.first.first);
+
+                expect(result).to.deep.equal(expected);
+            });
+        });
+
         it('should not allow missing quotes in imports', function () {
             var source = '@import url(http://example.com)';
             var expected = [{