Repository: sasstools/sass-lint (js)

Issue:
no-vendor-prefixes : Throws error on Non-Standard Rule
When enforcing the [no-vendor-prefixes](https://github.com/sasstools/sass-lint/blob/master/docs/rules/no-vendor-prefixes.md) rule, it throws errors due to non-standard rules, as utilized in [normalize.css](https://necolas.github.io/normalize.css/4.1.1/normalize.css):

/* Remove inner padding and border in Firefox 4+.*/
`button::-moz-focus-inner,
input::-moz-focus-inner {
    border: 0;
    padding: 0;
}`

/* Fix the cursor style for Chrome's increment/decrement buttons. For certain font-size values of the input, it causes the cursor style of the decrement button to change from default` to text.*/

`input[type="number"]::-webkit-inner-spin-button,
input[type="number"]::-webkit-outer-spin-button {
    height: auto;
}`

> error  Vendor prefixes should not be used  no-vendor-prefixes

Yes, this is a vendor prefix, but it is for a non-standard rule, and is targeting Specific Browser Sudo Elements

I'm tempted to exclude our normalize.scss file from linting, but I would prefer not to.

Could there be an option, added to the rule, that would allow the user to list non-standard css rules that would then be ignored by the linter?

What version of Sass Lint are you using?
"gulp-sass": "2.0.4"
"gulp-sass-lint": "1.7.0"



Pull request description:
Non standard property prefixes
Added the `ignore-non-standard` option to the `no-vendor-prefixes` rule. This allows you to blanket ignore the vendor prefixes on any non standard properties as defined by our definitive list. I'd like to discuss if this should also apply for pseudo elements, as far as I can see the only pseudo elements that require vendor prefixes are 'experimental' anyway so there's no need to do the extra check here

### IMPORTANT
* I've chosen to leave this new option as false to keep it functioning exactly as it had been before this change but I'm of the opinion it should probably default to true, open to suggestions here.

* I added the property 'keyframes' to the property list, this could be controversial but it seems that when prefixed Gonzales ignores the '@' part of the ident and even in some instances of the use `@keyframes` it does the same so I've added this property to ensure we can lint against it. What are peoples opinions here too? If we don't at least include it as a special case in the rule then `@-webkit-keyframes` would be considered a non-standard property.

* Updated the strip prefix helper method to cater for the above inconsistency too as originally it would have stripped the `@` had it been present. This sort of future proofs this method for any other similarly structured properties.

closes #702 

`<DCO 1.1 Signed-off-by: Dan Purdy dan@danpurdy.co.uk>`


Patch:
diff --git a/data/properties.yml b/data/properties.yml
index e7b6aca..e817c78 100644
--- a/data/properties.yml
+++ b/data/properties.yml
@@ -270,6 +270,7 @@ justify-items
 justify-self
 kerning
 @keyframes
+keyframes
 left
 letter-spacing
 lighting-color
diff --git a/docs/rules/no-vendor-prefixes.md b/docs/rules/no-vendor-prefixes.md
index cc8a9e8..22bd156 100644
--- a/docs/rules/no-vendor-prefixes.md
+++ b/docs/rules/no-vendor-prefixes.md
@@ -11,6 +11,7 @@ List of prefixes affected by default:
 
 * `additional-identifiers`: `[array of additional prefixes to check for]` (defaults to empty array `[]`)
 * `excluded-identifiers`: `[array of prefixes to exclude checking for]` (defaults to empty array `[]`)
+* `ignore-non-standard`: `true`:`false` (defaults to `false`)
 
 ## Examples
 
@@ -38,6 +39,8 @@ When enabled, the following are disallowed:
 }
 ```
 
+### Additional Identifiers
+
 When `additional-identifiers` contains a custom prefix value of `test` as show below
 
 ```yaml
@@ -56,6 +59,8 @@ The following would now also be disallowed
 }
 ```
 
+### Excluded Identifiers
+
 When `excluded-identifiers` contains currently disallowed prefix values such as `webkit` and `moz` as show below
 
 ```yaml
@@ -95,3 +100,27 @@ While the following would remain disallowed
   -ms-hyphenate-limit-lines: no-limit;
 }
 ```
+
+### Ignore Non Standard
+
+`ignore-non-standard` is an option that allows you to specify whether only standard properties from our [properties list](https://github.com/sasstools/sass-lint/blob/master/data/properties.yml) should be affected by this rule or if any prefixed property / element should be affected.
+
+When `ignore-non-standard` is set to `false` the following are disallowed, when `ignore-non-standard` is set to `true` the following are allowed:
+
+```scss
+
+html {
+  -webkit-tap-highlight-color: $link-color-hover;
+}
+
+button::-moz-focus-inner,
+input::-moz-focus-inner {
+  border: 0;
+  padding: 0;
+}
+
+input[type="number"]::-webkit-inner-spin-button,
+input[type="number"]::-webkit-outer-spin-button {
+  height: auto;
+}
+```
diff --git a/lib/helpers.js b/lib/helpers.js
index 97f37b0..1d14cfa 100644
--- a/lib/helpers.js
+++ b/lib/helpers.js
@@ -223,14 +223,20 @@ helpers.stripQuotes = function (str) {
  * @returns {string} The string without vendor prefixes
  */
 helpers.stripPrefix = function (str) {
-  var modProperty = str.slice(1),
-      prefixLength = modProperty.indexOf('-');
+  var modPropertyArr = str.split('-'),
+      modProperty = '',
+      prefLength = modPropertyArr[2] === 'osx' ? 2 : 1;
 
-  // check for the -osx- vendor prefix too as well as the original -moz- etc
-  if (modProperty.slice(prefixLength + 1).slice(0, 3) === 'osx') {
-    return modProperty.slice(prefixLength + 5);
-  }
-  return modProperty.slice(prefixLength + 1);
+  modPropertyArr.splice(1, prefLength);
+
+  modPropertyArr.forEach(function (item, index) {
+    modProperty = modProperty + item;
+    if (index > 0 && index < modPropertyArr.length - 1) {
+      modProperty = modProperty + '-';
+    }
+  });
+
+  return modProperty;
 };
 
 /**
diff --git a/lib/rules/no-vendor-prefixes.js b/lib/rules/no-vendor-prefixes.js
index 79ea1cc..529d8bb 100644
--- a/lib/rules/no-vendor-prefixes.js
+++ b/lib/rules/no-vendor-prefixes.js
@@ -1,13 +1,30 @@
 'use strict';
 
-var helpers = require('../helpers');
+var helpers = require('../helpers'),
+    yaml = require('js-yaml'),
+    fs = require('fs'),
+    path = require('path');
 
-var prefixes = ['webkit', 'moz', 'ms'];
+var properties = yaml.safeLoad(fs.readFileSync(path.join(__dirname, '../../data', 'properties.yml'), 'utf8')).split(' '),
+    prefixes = ['webkit', 'moz', 'ms'];
 
+/**
+ * Returns a copy of the prefixes array so that it can be safely modified
+ *
+ * @param {Array} prefixArr - The array of prefixes
+ * @returns {Array} A copy of the prefixes array
+ */
 var getPrefixCopy = function (prefixArr) {
   return prefixArr.slice();
 };
 
+/**
+ * Removes specified vendor prefixes from the prefixes array
+ *
+ * @param {Array} prefixArr - The array of prefixes
+ * @param {Array} excludes - An array of prefixes to exclude
+ * @returns {Array} The prefixes array minus any excluded prefixes
+ */
 var handleExcludes = function (prefixArr, excludes) {
   excludes.forEach(function (item) {
     var index = prefixArr.indexOf(item);
@@ -20,6 +37,13 @@ var handleExcludes = function (prefixArr, excludes) {
   return prefixArr;
 };
 
+/**
+ * Adds specified vendor prefixes to the prefixes array
+ *
+ * @param {Array} prefixArr - The array of prefixes
+ * @param {Array} includes - An array of prefixes to include
+ * @returns {Array} The prefixes array plus any extra included prefixes
+ */
 var handleIncludes = function (prefixArr, includes) {
   includes.forEach(function (item) {
     if (prefixArr.indexOf(item) === -1) {
@@ -30,6 +54,15 @@ var handleIncludes = function (prefixArr, includes) {
   return prefixArr;
 };
 
+/**
+ * Creates and returns a regex pattern based on all the included prefixes so that
+ * we can test our values against it.
+ *
+ * @param {Array} prefixArr - The array of prefixes
+ * @param {Array} includes - An array of prefixes to include
+ * @param {Array} excludes - An array of prefixes to exclude
+ * @returns {RegExp} The regex pattern for us to test values against
+ */
 var precompileRegEx = function (prefixArr, includes, excludes) {
   if (includes.length) {
     prefixArr = handleIncludes(prefixArr, includes);
@@ -42,11 +75,22 @@ var precompileRegEx = function (prefixArr, includes, excludes) {
   return new RegExp('-(' + prefixArr.join('|') + ')-');
 };
 
+/**
+ * Checks to see if the property is a standard property or a browser specific one
+ *
+ * @param {string} property - The property string we want to test
+ * @returns {boolean} Whether the property is standard or not
+ */
+var isStandardProperty = function (property) {
+  return properties.indexOf(helpers.stripPrefix(property)) !== -1;
+};
+
 module.exports = {
   'name': 'no-vendor-prefixes',
   'defaults': {
     'additional-identifiers': [],
-    'excluded-identifiers': []
+    'excluded-identifiers': [],
+    'ignore-non-standard': false
   },
   'detect': function (ast, parser) {
 
@@ -56,6 +100,9 @@ module.exports = {
 
     ast.traverseByType('ident', function (value) {
       if (statement.test(value.content)) {
+        if (!isStandardProperty(value.content) && parser.options['ignore-non-standard']) {
+          return;
+        }
         result = helpers.addUnique(result, {
           'ruleId': parser.rule.name,
           'line': value.start.line,


Test patch:
diff --git a/tests/helpers.js b/tests/helpers.js
index 1ce50f0..744699d 100644
--- a/tests/helpers.js
+++ b/tests/helpers.js
@@ -1047,6 +1047,14 @@ describe('helpers', function () {
     done();
   });
 
+  it('stripPrefix - [@-webkit-keyframes - @keyframes]', function (done) {
+
+    var result = helpers.stripPrefix('@-webkit-keyframes');
+
+    assert.equal('@keyframes', result);
+    done();
+  });
+
   //////////////////////////////
   // stripLastSpace
   //////////////////////////////
diff --git a/tests/rules/no-vendor-prefixes.js b/tests/rules/no-vendor-prefixes.js
index 46ff4b7..37d0579 100644
--- a/tests/rules/no-vendor-prefixes.js
+++ b/tests/rules/no-vendor-prefixes.js
@@ -12,7 +12,7 @@ describe('no vendor prefix - scss', function () {
     lint.test(file, {
       'no-vendor-prefixes': 1
     }, function (data) {
-      lint.assert.equal(5, data.warningCount);
+      lint.assert.equal(10, data.warningCount);
       done();
     });
   });
@@ -29,7 +29,7 @@ describe('no vendor prefix - scss', function () {
         }
       ]
     }, function (data) {
-      lint.assert.equal(3, data.warningCount);
+      lint.assert.equal(5, data.warningCount);
       done();
     });
   });
@@ -64,7 +64,7 @@ describe('no vendor prefix - scss', function () {
         }
       ]
     }, function (data) {
-      lint.assert.equal(6, data.warningCount);
+      lint.assert.equal(11, data.warningCount);
       done();
     });
   });
@@ -84,7 +84,38 @@ describe('no vendor prefix - scss', function () {
         }
       ]
     }, function (data) {
-      lint.assert.equal(6, data.warningCount);
+      lint.assert.equal(11, data.warningCount);
+      done();
+    });
+  });
+
+  it('[ignore-non-standard: true]', function (done) {
+    lint.test(file, {
+      'no-vendor-prefixes': [
+        1,
+        {
+          'ignore-non-standard': true
+        }
+      ]
+    }, function (data) {
+      lint.assert.equal(4, data.warningCount);
+      done();
+    });
+  });
+
+  it('[ignore-non-standard: true][excluded-identifiers: webkit]', function (done) {
+    lint.test(file, {
+      'no-vendor-prefixes': [
+        1,
+        {
+          'excluded-identifiers': [
+            'webkit'
+          ],
+          'ignore-non-standard': true
+        }
+      ]
+    }, function (data) {
+      lint.assert.equal(1, data.warningCount);
       done();
     });
   });
@@ -100,7 +131,7 @@ describe('no vendor prefix - sass', function () {
     lint.test(file, {
       'no-vendor-prefixes': 1
     }, function (data) {
-      lint.assert.equal(5, data.warningCount);
+      lint.assert.equal(10, data.warningCount);
       done();
     });
   });
@@ -117,7 +148,7 @@ describe('no vendor prefix - sass', function () {
         }
       ]
     }, function (data) {
-      lint.assert.equal(3, data.warningCount);
+      lint.assert.equal(5, data.warningCount);
       done();
     });
   });
@@ -152,7 +183,7 @@ describe('no vendor prefix - sass', function () {
         }
       ]
     }, function (data) {
-      lint.assert.equal(6, data.warningCount);
+      lint.assert.equal(11, data.warningCount);
       done();
     });
   });
@@ -172,7 +203,38 @@ describe('no vendor prefix - sass', function () {
         }
       ]
     }, function (data) {
-      lint.assert.equal(6, data.warningCount);
+      lint.assert.equal(11, data.warningCount);
+      done();
+    });
+  });
+
+  it('[ignore-non-standard: true]', function (done) {
+    lint.test(file, {
+      'no-vendor-prefixes': [
+        1,
+        {
+          'ignore-non-standard': true
+        }
+      ]
+    }, function (data) {
+      lint.assert.equal(4, data.warningCount);
+      done();
+    });
+  });
+
+  it('[ignore-non-standard: true][excluded-identifiers: webkit]', function (done) {
+    lint.test(file, {
+      'no-vendor-prefixes': [
+        1,
+        {
+          'excluded-identifiers': [
+            'webkit'
+          ],
+          'ignore-non-standard': true
+        }
+      ]
+    }, function (data) {
+      lint.assert.equal(1, data.warningCount);
       done();
     });
   });
diff --git a/tests/sass/no-vendor-prefixes.sass b/tests/sass/no-vendor-prefixes.sass
index 9c72206..337e4d5 100644
--- a/tests/sass/no-vendor-prefixes.sass
+++ b/tests/sass/no-vendor-prefixes.sass
@@ -21,3 +21,18 @@
 
 .ms-block
   -ms-hyphenate-limit-lines: no-limit
+
+// Issue #705 - ignore non standard properties
+html
+  -webkit-tap-highlight-color: $link-color-hover
+
+// Issue #702 - ignore non standard properties
+button::-moz-focus-inner,
+input::-moz-focus-inner
+  border: 0
+  padding: 0
+
+// Issue #702 - ignore non standard properties
+input[type="number"]::-webkit-inner-spin-button,
+input[type="number"]::-webkit-outer-spin-button
+  height: auto
diff --git a/tests/sass/no-vendor-prefixes.scss b/tests/sass/no-vendor-prefixes.scss
index af061db..8f0a763 100644
--- a/tests/sass/no-vendor-prefixes.scss
+++ b/tests/sass/no-vendor-prefixes.scss
@@ -21,3 +21,21 @@
 .ms-block {
   -ms-hyphenate-limit-lines: no-limit;
 }
+
+// Issue #705 - ignore non standard properties
+html {
+  -webkit-tap-highlight-color: $link-color-hover;
+}
+
+// Issue #702 - ignore non standard properties
+button::-moz-focus-inner,
+input::-moz-focus-inner {
+  border: 0;
+  padding: 0;
+}
+
+// Issue #702 - ignore non standard properties
+input[type="number"]::-webkit-inner-spin-button,
+input[type="number"]::-webkit-outer-spin-button {
+  height: auto;
+}