Repository: sasstools/sass-lint (js)

Issue:
New Rule: Hex Notation
Check hexadecimal values are either all lowercase (default) or all uppercase.

as per [SCSS Lint](https://github.com/brigade/scss-lint/blob/master/lib/scss_lint/linter/README.md#hexnotation)

Pull request:
Add hex notation rule
Add a rule to enforce the consistency of hexadecimal value cases.

Add 3 new helper functions
* isNumber - check is integer
* isUpperCase - check if string passed is all uppercase
* isLowerCase - check if string passed is all lower case

I kept the case helper functions separate to allow use in other rules to follow (property name case checking?). They allow you to pass many non alphanumeric characters too as part of a string without erroneously flagging them as upper or lower case.

Would like to add tests for all the helper functions too in the future.

Closes #77

DCO 1.1 Signed-off-by: Dan Purdy danjpurdy@gmail.com

Patch:
diff --git a/docs/rules/hex-notation.md b/docs/rules/hex-notation.md
new file mode 100644
index 0000000..a8e9865
--- /dev/null
+++ b/docs/rules/hex-notation.md
@@ -0,0 +1,45 @@
+# Hex Notation
+
+Rule `hex-notation` will enforce the case of hexadecimal values
+
+## Options
+
+* `style`: `lowercase`/`uppercase` (defaults to `lowercase`)
+
+## Examples
+
+When `style: lowercase`, the following are allowed. When `style: uppercase`, the following are disallowed:
+
+```scss
+$foo-color: #fff;
+
+.bar {
+  background: linear-gradient(top, #cc2, #44d);
+}
+
+.baz {
+  color: #12a;
+}
+```
+
+When `style: uppercase`, the following are allowed. When `style: lowercase`, the following are disallowed:
+
+```scss
+$foo-color: #FFF;
+
+.bar {
+  background: linear-gradient(top, #CC2, #44D);
+}
+
+.baz {
+  color: #12A;
+}
+```
+
+In both cases the following will be allowed as the values contain only numbers:
+
+```scss
+.qux {
+  color: #123;
+}
+```
diff --git a/lib/config/sass-lint.yml b/lib/config/sass-lint.yml
index d343532..4c3bc41 100644
--- a/lib/config/sass-lint.yml
+++ b/lib/config/sass-lint.yml
@@ -27,6 +27,7 @@ rules:
   # Style Guide
   clean-import-paths: 1
   hex-length: 1
+  hex-notation: 1
   hex-validation: 1
   indentation: 1
   leading-zero: 1
diff --git a/lib/helpers.js b/lib/helpers.js
index 922b6d0..4310303 100644
--- a/lib/helpers.js
+++ b/lib/helpers.js
@@ -74,6 +74,55 @@ helpers.sortDetects = function (a, b) {
   return 0;
 };
 
+helpers.isNumber = function (val) {
+  if (isNaN(parseInt(val, 10))) {
+    return false;
+  }
+  return true;
+};
+
+helpers.isUpperCase = function (str) {
+  var pieces = str.split(''),
+      i,
+      result = 0;
+
+  for (i = 0; i < pieces.length; i++) {
+    if (!helpers.isNumber(pieces[i])) {
+      if (pieces[i] === pieces[i].toUpperCase() && pieces[i] !== pieces[i].toLowerCase()) {
+        result++;
+      }
+      else {
+        return false;
+      }
+    }
+  }
+  if (result) {
+    return true;
+  }
+  return false;
+};
+
+helpers.isLowerCase = function (str) {
+  var pieces = str.split(''),
+      i,
+      result = 0;
+
+  for (i = 0; i < pieces.length; i++) {
+    if (!helpers.isNumber(pieces[i])) {
+      if (pieces[i] === pieces[i].toLowerCase() && pieces[i] !== pieces[i].toUpperCase()) {
+        result++;
+      }
+      else {
+        return false;
+      }
+    }
+  }
+  if (result) {
+    return true;
+  }
+  return false;
+};
+
 helpers.isValidHex = function (str) {
   if (str.match(/^([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/)) {
     return true;
diff --git a/lib/rules/hex-notation.js b/lib/rules/hex-notation.js
new file mode 100644
index 0000000..d6b2132
--- /dev/null
+++ b/lib/rules/hex-notation.js
@@ -0,0 +1,42 @@
+'use strict';
+
+var helpers = require('../helpers');
+
+module.exports = {
+  'name': 'hex-notation',
+  'defaults': {
+    'style': 'lowercase'
+  },
+  'detect': function (ast, parser) {
+    var result = [];
+
+    ast.traverseByType('color', function (value) {
+      if (value.content.match(/[a-z]/i)) {
+        if (parser.options.style === 'lowercase') {
+          if (!helpers.isLowerCase(value.content)) {
+            result = helpers.addUnique(result, {
+              'ruleId': parser.rule.name,
+              'line': value.start.line,
+              'column': value.start.column,
+              'message': 'hex notation should all be lower case',
+              'severity': parser.severity
+            });
+          }
+        }
+        else if (parser.options.style === 'uppercase') {
+          if (!helpers.isUpperCase(value.content)) {
+            result = helpers.addUnique(result, {
+              'ruleId': parser.rule.name,
+              'line': value.start.line,
+              'column': value.start.column,
+              'message': 'hex notation should all be upper case',
+              'severity': parser.severity
+            });
+          }
+        }
+      }
+    });
+
+    return result;
+  }
+};


Test patch:
diff --git a/tests/main.js b/tests/main.js
index df887e1..61c686e 100644
--- a/tests/main.js
+++ b/tests/main.js
@@ -107,20 +107,6 @@ describe('rule', function () {
     });
   });
 
-  //////////////////////////////
-  // Hex Validation
-  //////////////////////////////
-  it('hex validation', function (done) {
-    lintFile('hex-validation.scss', {
-      'rules': {
-        'hex-length': 0
-      }
-    }, function (data) {
-      assert.equal(16, data.warningCount);
-      done();
-    });
-  });
-
   //////////////////////////////
   // Hex Length Short - Default
   //////////////////////////////
@@ -154,6 +140,59 @@ describe('rule', function () {
     });
   });
 
+  //////////////////////////////
+  // Hex notation Lowercase - Default
+  //////////////////////////////
+  it('hex notation - lowercase', function (done) {
+    lintFile('hex-notation.scss', {
+      'rules': {
+        'hex-notation': 1,
+        'hex-length': 0,
+        'hex-validation': 0
+      }
+    }, function (data) {
+      assert.equal(6, data.warningCount);
+      done();
+    });
+  });
+
+  //////////////////////////////
+  // Hex Notation Uppercase
+  //////////////////////////////
+  it('hex notation - uppercase', function (done) {
+    lintFile('hex-notation.scss', {
+      'rules': {
+        'hex-notation': [
+          1,
+          {
+            'style': 'uppercase'
+          }
+        ],
+        'hex-length': 0,
+        'hex-validation': 0
+      }
+    }, function (data) {
+      assert.equal(7, data.warningCount);
+      done();
+    });
+  });
+
+  //////////////////////////////
+  // Hex Validation
+  //////////////////////////////
+  it('hex validation', function (done) {
+    lintFile('hex-validation.scss', {
+      'rules': {
+        'hex-length': 0,
+        'hex-notation': 0,
+        'hex-validation': 1
+      }
+    }, function (data) {
+      assert.equal(16, data.warningCount);
+      done();
+    });
+  });
+
   //////////////////////////////
   // Mixins Before DEclarations
   //////////////////////////////
diff --git a/tests/sass/hex-notation.scss b/tests/sass/hex-notation.scss
new file mode 100644
index 0000000..49e8b76
--- /dev/null
+++ b/tests/sass/hex-notation.scss
@@ -0,0 +1,33 @@
+// numbers only won't match
+$foo-color: #123;
+
+.foo {
+  background: linear-gradient(top, #cc2, #44d);
+  color: #fff;
+}
+
+$bar-color: #123456;
+
+.bar {
+  background: linear-gradient(top, #CC2, #44D);
+  color: #FFF;
+}
+
+.qux {
+  color: #cC2;
+}
+
+$lower-numbers-color: #123cc2;
+$upper-lower-color: #CCCCCc;
+
+$map-vals: (
+  mixed: #123Cde,
+);
+
+// check that only hex colours are being parsed
+
+$literal-color: red;
+$rgb-color: rgb(255, 255, 255);
+$rgba-color: rgba(0, 0, 0, .1);
+$hsl-color: hsl(40, 50%, 50%);
+$hsla-color: hsla(40, 50%, 50%, .3);