Repository: sasstools/sass-lint
Pull request #257 (2015-10-07 03:04:04)

Add rule: variable-name-format
Add rule: variable-name-format which validates that all variables adhere to a convention

Options:
`allow-leading-underscore`: Allow variables to begin with an underscore (default true)
`convention`: one of `hyphenatedlowercase` (default), `camelcase`, `snakecase`, or a RegExp
`convention-explanation`: defaults to `should match regular expression /[regex]/[flags]`, explanation if the RegExp from `convention` fails

I took the patterns for `isCamelCase`, `isHyphenatedLowercase` and `isSnakeCase` from how scss-lint implements them for consistency between projects.

I'll add the related rules to this Pull Request, but since they are very similar I wanted to get feedback early to avoid applying fixes across four rules.

Part 1 of 4 for #17 

DCO 1.1 Signed-off-by: Ben Rothman <bensrothman@gmail.com>


Patch:
diff --git a/docs/rules/variable-name-format.md b/docs/rules/variable-name-format.md
new file mode 100644
index 0000000..2ff3984
--- /dev/null
+++ b/docs/rules/variable-name-format.md
@@ -0,0 +1,122 @@
+# Variable Name Format
+
+Rule `variable-name-format` will enforce the use of hexadecimal color values rather than literals.
+
+## Options
+
+* `allow-leading-underscore`: `true`/`false` (defaults to `true`)
+* `convention`: `'hyphenatedlowercase'` (default), `camelcase`, `snakecase`, or a Regular Expression that the variable name must match (e.g. `/^[_A-Z]+$/`)
+* `convention-explanation`: Custom explanation to display to the user if a variable doesn't adhere to the convention
+
+## Example 1
+
+Settings:
+- `allow-leading-underscore: true`
+- `convention: hyphenatedlowercase`
+
+When enabled, the following are allowed:
+
+```scss
+$hyphenated-lowercase: 1px;
+$_leading-underscore: 1px;
+
+.foo {
+  width: $hyphenated-lowercase;
+}
+
+```
+
+When enabled, the following are disallowed:
+
+```scss
+$HYPHENATED-UPPERCASE: 1px;
+$_camelCaseWithLeadingUnderscore: 1px;
+
+.foo {
+  width: $snake_case;
+}
+```
+
+## Example 2
+
+Settings:
+- `allow-leading-underscore: false`
+- `convention: camelcase`
+
+When enabled, the following are allowed:
+
+```scss
+$camelCase: 1px;
+
+.foo {
+  width: $anotherCamelCase;
+}
+```
+
+When enabled, the following are disallowed:
+
+```scss
+$HYPHENATED-UPPERCASE: 1px;
+$_camelCaseWithLeadingUnderscore: 1px;
+
+.foo {
+  width: $snake_case;
+}
+```
+
+## Example 3
+
+Settings:
+- `allow-leading-underscore: false`
+- `convention: snakecase`
+
+When enabled, the following are allowed:
+
+```scss
+$snake_case: 1px;
+
+.foo {
+  width: $another_snake_case;
+}
+```
+
+When enabled, the following are disallowed:
+
+```scss
+$HYPHENATED-UPPERCASE: 1px;
+$_snake_case_with_leading_underscore: 1px;
+
+.foo {
+  width: $camelCase;
+}
+```
+
+## Example 4
+
+Settings:
+- `allow-leading-underscore: true`
+- `convention: /^[_A-Z]+$/`
+- `convention-explanation: 'Variables must contain only uppercase letters and underscores'`
+
+When enabled, the following are allowed:
+
+```scss
+$SCREAMING_SNAKE_CASE: 1px;
+
+.foo {
+  width: $_LEADING_UNDERSCORE;
+}
+```
+
+When enabled, the following are disallowed:
+
+(Each line with a variable will report `Variables must contain only uppercase letters and underscores` when linted.)
+
+```scss
+$HYPHENATED-UPPERCASE: 1px;
+$_snake_case_with_leading_underscore: 1px;
+
+.foo {
+  width: $camelCase;
+}
+```
diff --git a/lib/helpers.js b/lib/helpers.js
index b14d11d..ba4e9fe 100644
--- a/lib/helpers.js
+++ b/lib/helpers.js
@@ -126,6 +126,18 @@ helpers.isLowerCase = function (str) {
   return false;
 };
 
+helpers.isCamelCase = function (str) {
+  return /^[a-z][a-zA-Z0-9]*$/.test(str);
+};
+
+helpers.isHyphenatedLowercase = function (str) {
+  return !(/[_A-Z]/.test(str));
+};
+
+helpers.isSnakeCase = function (str) {
+  return !(/[^_a-z0-9]/.test(str));
+};
+
 helpers.isValidHex = function (str) {
   if (str.match(/^([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/)) {
     return true;
diff --git a/lib/rules/variable-name-format.js b/lib/rules/variable-name-format.js
new file mode 100644
index 0000000..223eee6
--- /dev/null
+++ b/lib/rules/variable-name-format.js
@@ -0,0 +1,68 @@
+'use strict';
+
+var helpers = require('../helpers');
+
+module.exports = {
+  'name': 'variable-name-format',
+  'defaults': {
+    'allow-leading-underscore': true,
+    'convention': 'hyphenatedlowercase'
+  },
+  'detect': function (ast, parser) {
+    var result = [];
+
+    ast.traverseByType('variable', function (variable) {
+      var name = variable.first().content;
+
+      if (parser.options['allow-leading-underscore'] && name[0] === '_') {
+        name = name.slice(1);
+      }
+
+      if (parser.options.convention === 'hyphenatedlowercase' && !helpers.isHyphenatedLowercase(name)) {
+        result = helpers.addUnique(result, {
+          'ruleId': parser.rule.name,
+          'line': variable.start.line,
+          'column': variable.start.column,
+          'message': 'Variable \'' + variable.first().content + '\' should be written in lowercase with hyphens',
+          'severity': parser.severity
+        });
+      }
+      else if (parser.options.convention === 'camelcase' && !helpers.isCamelCase(name)) {
+        result = helpers.addUnique(result, {
+          'ruleId': parser.rule.name,
+          'line': variable.start.line,
+          'column': variable.start.column,
+          'message': 'Variable \'' + variable.first().content + '\' should be written in camelCase',
+          'severity': parser.severity
+        });
+      }
+      else if (parser.options.convention === 'snakecase' && !helpers.isSnakeCase(name)) {
+        result = helpers.addUnique(result, {
+          'ruleId': parser.rule.name,
+          'line': variable.start.line,
+          'column': variable.start.column,
+          'message': 'Variable \'' + variable.first().content + '\' should be written in snake_case',
+          'severity': parser.severity
+        });
+      }
+      else if (parser.options.convention instanceof RegExp && !parser.options.convention.test(name)) {
+        var message = parser.options['convention-explanation'];
+
+        if (!message) {
+          message = 'should match regular expression /' + parser.options.convention.source + '/' +
+                    (parser.options.convention.ignoreCase ? 'i' : '');
+        }
+
+        result = helpers.addUnique(result, {
+          'ruleId': parser.rule.name,
+          'line': variable.start.line,
+          'column': variable.start.column,
+          'message': 'Variable \'' + variable.first().content + '\' ' + message,
+          'severity': parser.severity
+        });
+      }
+    });
+
+    return result;
+  }
+};


Test patch:
diff --git a/tests/helpers.js b/tests/helpers.js
index 48d2c1d..1ed628b 100644
--- a/tests/helpers.js
+++ b/tests/helpers.js
@@ -451,6 +451,154 @@ describe('helpers', function () {
     done();
   });
 
+  //////////////////////////////
+  // isCamelCase
+  //////////////////////////////
+
+  it('isCamelCase - [\'TEST\' - false]', function (done) {
+
+    var result = helpers.isCamelCase('TEST');
+
+    assert.equal(false, result);
+    done();
+  });
+
+  it('isCamelCase - [\'test\' - true]', function (done) {
+
+    var result = helpers.isCamelCase('test');
+
+    assert.equal(true, result);
+    done();
+  });
+
+  it('isCamelCase - [abcDEF - true]', function (done) {
+
+    var result = helpers.isCamelCase('abcDEF');
+
+    assert.equal(true, result);
+    done();
+  });
+
+  it('isCamelCase - [\'123\' - false]', function (done) {
+
+    var result = helpers.isCamelCase('123');
+
+    assert.equal(false, result);
+    done();
+  });
+
+  it('isCamelCase - [\'aBcDeF\' - true]', function (done) {
+
+    var result = helpers.isCamelCase('aBcDeF');
+
+    assert.equal(true, result);
+    done();
+  });
+
+  //////////////////////////////
+  // isHyphenatedLowercase
+  //////////////////////////////
+
+  it('isHyphenatedLowercase - [\'abc-def\' - true]', function (done) {
+
+    var result = helpers.isHyphenatedLowercase('abc-def');
+
+    assert.equal(true, result);
+    done();
+  });
+
+  it('isHyphenatedLowercase - [\'TEST\' - false]', function (done) {
+
+    var result = helpers.isHyphenatedLowercase('TEST');
+
+    assert.equal(false, result);
+    done();
+  });
+
+  it('isHyphenatedLowercase - [\'test\' - true]', function (done) {
+
+    var result = helpers.isHyphenatedLowercase('test');
+
+    assert.equal(true, result);
+    done();
+  });
+
+  it('isHyphenatedLowercase - [abcDEF - false]', function (done) {
+
+    var result = helpers.isHyphenatedLowercase('abcDEF');
+
+    assert.equal(false, result);
+    done();
+  });
+
+  it('isHyphenatedLowercase - [\'123\' - true]', function (done) {
+
+    var result = helpers.isHyphenatedLowercase('123');
+
+    assert.equal(true, result);
+    done();
+  });
+
+  it('isHyphenatedLowercase - [\'aBcDeF\' - false]', function (done) {
+
+    var result = helpers.isHyphenatedLowercase('aBcDeF');
+
+    assert.equal(false, result);
+    done();
+  });
+
+  //////////////////////////////
+  // isSnakeCase
+  //////////////////////////////
+
+  it('isSnakeCase - [\'TEST\' - false]', function (done) {
+
+    var result = helpers.isSnakeCase('TEST');
+
+    assert.equal(false, result);
+    done();
+  });
+
+  it('isSnakeCase - [\'test\' - true]', function (done) {
+
+    var result = helpers.isSnakeCase('test');
+
+    assert.equal(true, result);
+    done();
+  });
+
+  it('isSnakeCase - [abcDEF - false]', function (done) {
+
+    var result = helpers.isSnakeCase('abcDEF');
+
+    assert.equal(false, result);
+    done();
+  });
+
+  it('isSnakeCase - [\'123\' - true]', function (done) {
+
+    var result = helpers.isSnakeCase('123');
+
+    assert.equal(true, result);
+    done();
+  });
+
+  it('isSnakeCase - [\'ab_cd_ef\' - true]', function (done) {
+
+    var result = helpers.isSnakeCase('ab_cd_ef');
+
+    assert.equal(true, result);
+    done();
+  });
+
+  it('isSnakeCase - [\'ab_cd-ef\' - false]', function (done) {
+
+    var result = helpers.isSnakeCase('ab_cd-ef');
+
+    assert.equal(false, result);
+    done();
+  });
+
   //////////////////////////////
   // isValidHex
   //////////////////////////////
diff --git a/tests/rules/variable-name-format.js b/tests/rules/variable-name-format.js
new file mode 100644
index 0000000..6a8c328
--- /dev/null
+++ b/tests/rules/variable-name-format.js
@@ -0,0 +1,143 @@
+'use strict';
+
+var lint = require('./_lint');
+
+describe('variable name format - scss', function () {
+  var file = lint.file('variable-name-format.scss');
+
+  it('[convention: hyphenatedlowercase]', function (done) {
+    lint.test(file, {
+      'variable-name-format': 1
+    }, function (data) {
+      lint.assert.equal(7, data.warningCount);
+      done();
+    });
+  });
+
+  it('[convention: camelcase]', function (done) {
+    lint.test(file, {
+      'variable-name-format': [
+        1,
+        {
+          'convention': 'camelcase'
+        }
+      ]
+    }, function (data) {
+      lint.assert.equal(8, data.warningCount);
+      done();
+    });
+  });
+
+  it('[convention: snakecase]', function (done) {
+    lint.test(file, {
+      'variable-name-format': [
+        1,
+        {
+          'convention': 'snakecase'
+        }
+      ]
+    }, function (data) {
+      lint.assert.equal(7, data.warningCount);
+      done();
+    });
+  });
+
+  it('[convention: RegExp /^[_A-Z]+$/]', function (done) {
+    lint.test(file, {
+      'variable-name-format': [
+        1,
+        {
+          'convention': /^[_A-Z]+$/,
+          'convention-explanation': 'Its bad and you should feel bad.'
+        }
+      ]
+    }, function (data) {
+      lint.assert.equal(8, data.warningCount);
+      done();
+    });
+  });
+
+  it('[convention: allow-leading-underscore false]', function (done) {
+    lint.test(file, {
+      'variable-name-format': [
+        1,
+        {
+          'allow-leading-underscore': false
+        }
+      ]
+    }, function (data) {
+      lint.assert.equal(8, data.warningCount);
+      done();
+    });
+  });
+});
+
+describe('variable name format - sass', function () {
+  var file = lint.file('variable-name-format.sass');
+
+  it('[convention: hyphenatedlowercase]', function (done) {
+    lint.test(file, {
+      'variable-name-format': 1
+    }, function (data) {
+      lint.assert.equal(7, data.warningCount);
+      done();
+    });
+  });
+
+  it('[convention: camelcase]', function (done) {
+    lint.test(file, {
+      'variable-name-format': [
+        1,
+        {
+          'convention': 'camelcase'
+        }
+      ]
+    }, function (data) {
+      lint.assert.equal(8, data.warningCount);
+      done();
+    });
+  });
+
+  it('[convention: snakecase]', function (done) {
+    lint.test(file, {
+      'variable-name-format': [
+        1,
+        {
+          'convention': 'snakecase'
+        }
+      ]
+    }, function (data) {
+      lint.assert.equal(7, data.warningCount);
+      done();
+    });
+  });
+
+  it('[convention: RegExp /^[_A-Z]+$/]', function (done) {
+    lint.test(file, {
+      'variable-name-format': [
+        1,
+        {
+          'convention': /^[_A-Z]+$/,
+          'convention-explanation': 'Its bad and you should feel bad.'
+        }
+      ]
+    }, function (data) {
+      lint.assert.equal(8, data.warningCount);
+      done();
+    });
+  });
+
+  it('[convention: allow-leading-underscore false]', function (done) {
+    lint.test(file, {
+      'variable-name-format': [
+        1,
+        {
+          'allow-leading-underscore': false
+        }
+      ]
+    }, function (data) {
+      lint.assert.equal(8, data.warningCount);
+      done();
+    });
+  });
+});
diff --git a/tests/sass/variable-name-format.sass b/tests/sass/variable-name-format.sass
new file mode 100644
index 0000000..16e3322
--- /dev/null
+++ b/tests/sass/variable-name-format.sass
@@ -0,0 +1,11 @@
+$kabab-case: 1
+$snake_case: 1
+$camelCase: 1
+$PascalCase: 1
+$Camel_Snake_Case: 1
+$SCREAMING_SNAKE_CASE: 1
+$_with-leading-underscore: 1
+$_does_NOT-fitSTANDARD: 1
+
+.class
+  width: $snake_case
diff --git a/tests/sass/variable-name-format.scss b/tests/sass/variable-name-format.scss
new file mode 100644
index 0000000..3a90562
--- /dev/null
+++ b/tests/sass/variable-name-format.scss
@@ -0,0 +1,12 @@
+$kabab-case: 1;
+$snake_case: 1;
+$camelCase: 1;
+$PascalCase: 1;
+$Camel_Snake_Case: 1;
+$SCREAMING_SNAKE_CASE: 1;
+$_with-leading-underscore: 1;
+$_does_NOT-fitSTANDARD: 1;
+
+.class {
+  width: $snake_case;
+}