Repository: vuejs/eslint-plugin-vue
Pull request #95 (2017-07-18 23:25:27)

Add rule `attribute-hyphenation`.
This PR implements rules proposed in #92

**TODO:**
- [x] Create documtation
- [x] Add tests
- [x] Implement rule
- [x] Do not update `data-` attribute
- [x] Do not update Svg & MathML attributes
- [x] Add auto-fix
- [x] Do not update `aria-` attribute
- [x] Add tests for auto-fix
- [x] wait for mysticatea/vue-eslint-parser#12
- [x] Use use `node.namespace` to determine if node is Svg & MathML instead of checking node names
- [x] Improve tests


This PR contains changes proposed in #120 and should be merged after it.

armano2: Based on https://github.com/vuejs/eslint-plugin-vue/issues/92#issuecomment-316506944 we are missing some cases
armano2: @michalsnik rule is done

Patch:
diff --git a/docs/rules/attribute-hyphenation.md b/docs/rules/attribute-hyphenation.md
new file mode 100644
index 00000000..7d83c9fa
--- /dev/null
+++ b/docs/rules/attribute-hyphenation.md
@@ -0,0 +1,53 @@
+# Define if attributes on cusom components can be hyphened. (attribute-hyphenation)
+
+## :wrench: Options
+
+Default casing is set to `always`
+
+```
+'vue/attribute-hyphenation': [2, 'always'|'never']
+```
+
+### `"always"` - Use hyphenated name. (It errors on upper case letters.)
+
+:+1: Examples of **correct** code`:
+
+```html
+<template>
+  <foo my-prop="prop">
+    <a onClick="return false"></a>
+  </foo>
+</template>
+```
+
+:-1: Examples of **incorrect** code`:
+
+```html
+<template>
+  <foo myProp="prop">
+    <a onClick="return false"></a>
+  </foo>
+</template>
+```
+
+### `"never"` - Don't use hyphenated name. (It errors on hyphens except `data-` and `aria-`.)
+
+:+1: Examples of **correct** code`:
+
+```html
+<template>
+  <foo myProp="prop">
+    <a onClick="return false"></a>
+  </foo>
+</template>
+```
+
+:-1: Examples of **incorrect** code`:
+
+```html
+<template>
+  <foo my-prop="prop">
+    <a onClick="return false"></a>
+  </foo>
+</template>
+```
diff --git a/lib/rules/attribute-hyphenation.js b/lib/rules/attribute-hyphenation.js
new file mode 100644
index 00000000..177b7a10
--- /dev/null
+++ b/lib/rules/attribute-hyphenation.js
@@ -0,0 +1,76 @@
+/**
+ * @fileoverview Define a style for the props casing in templates.
+ * @author Armano
+ */
+'use strict'
+
+const utils = require('../utils')
+const casing = require('../utils/casing')
+
+// ------------------------------------------------------------------------------
+// Rule Definition
+// ------------------------------------------------------------------------------
+
+function create (context) {
+  const sourceCode = context.getSourceCode()
+  const options = context.options[0]
+  const useHyphenated = options !== 'never'
+
+  const caseConverter = casing.getConverter(useHyphenated ? 'kebab-case' : 'camelCase')
+
+  function reportIssue (node, name) {
+    const text = sourceCode.getText(node.key)
+
+    context.report({
+      node: node.key,
+      loc: node.loc,
+      message: useHyphenated ? "Attribute '{{text}}' must be hyphenated." : "Attribute '{{text}}' cann't be hyphenated.",
+      data: {
+        text
+      },
+      fix: fixer => fixer.replaceText(node.key, text.replace(name, caseConverter(name)))
+    })
+  }
+
+  function isIgnoredAttribute (value) {
+    if (value.indexOf('data-') !== -1 || value.indexOf('aria-') !== -1) {
+      return true
+    }
+    return useHyphenated ? value.toLowerCase() === value : !/-/.test(value)
+  }
+
+  // ----------------------------------------------------------------------
+  // Public
+  // ----------------------------------------------------------------------
+
+  utils.registerTemplateBodyVisitor(context, {
+    VAttribute (node) {
+      if (!utils.isCustomComponent(node.parent.parent)) return
+
+      const name = !node.directive ? node.key.rawName : node.key.name === 'bind' ? node.key.raw.argument : false
+      if (!name || isIgnoredAttribute(name)) return
+
+      reportIssue(node, name)
+    }
+  })
+
+  return {}
+}
+
+module.exports = {
+  meta: {
+    docs: {
+      description: 'Define a style for the props casing in templates.',
+      category: 'Stylistic Issues',
+      recommended: false
+    },
+    fixable: 'code',
+    schema: [
+      {
+        enum: ['always', 'never']
+      }
+    ]
+  },
+
+  create
+}
diff --git a/package-lock.json b/package-lock.json
index 3496e7ce..21b7a2a5 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -3198,9 +3198,9 @@
       "dev": true
     },
     "vue-eslint-parser": {
-      "version": "2.0.0-beta.2",
-      "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-2.0.0-beta.2.tgz",
-      "integrity": "sha512-bcoX+jDpF7xY3nbPK9W6yBiH7I0WOFA1SU0LnYkusZzY6ZRGbJWOiyNpl1TWAkNmLH0Az2uGVVnzsBVB1aSl6w==",
+      "version": "2.0.0-beta.3",
+      "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-2.0.0-beta.3.tgz",
+      "integrity": "sha512-/h9CORTgmIPEsu6PKXY5IbNzcwCkb1aV5mpnSojwNcShdWi+VKXi95HMnpiWuLAa3M6Hh2tMENwR/twyrX3h1w==",
       "requires": {
         "debug": "2.6.8",
         "eslint-scope": "3.7.1",
diff --git a/package.json b/package.json
index c6bc95c0..2c1457f8 100644
--- a/package.json
+++ b/package.json
@@ -45,7 +45,7 @@
   },
   "dependencies": {
     "requireindex": "^1.1.0",
-    "vue-eslint-parser": "^2.0.0-beta.2"
+    "vue-eslint-parser": "^2.0.0-beta.3"
   },
   "devDependencies": {
     "@types/node": "^4.2.16",


Test patch:
diff --git a/tests/lib/rules/attribute-hyphenation.js b/tests/lib/rules/attribute-hyphenation.js
new file mode 100644
index 00000000..b5dabe4a
--- /dev/null
+++ b/tests/lib/rules/attribute-hyphenation.js
@@ -0,0 +1,116 @@
+/**
+ * @fileoverview Define a style for the props casing in templates.
+ * @author Armano
+ */
+'use strict'
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const rule = require('../../../lib/rules/attribute-hyphenation')
+
+const RuleTester = require('eslint').RuleTester
+
+// ------------------------------------------------------------------------------
+// Tests
+// ------------------------------------------------------------------------------
+
+const ruleTester = new RuleTester({
+  parser: 'vue-eslint-parser',
+  parserOptions: { ecmaVersion: 2015 }
+})
+
+ruleTester.run('attribute-hyphenation', rule, {
+
+  valid: [
+    {
+      filename: 'test.vue',
+      code: ''
+    },
+    {
+      filename: 'test.vue',
+      code: '<template><div><custom data-id="foo" aria-test="bar" my-prop="prop"></custom></div></template>',
+      options: ['always']
+    },
+    {
+      filename: 'test.vue',
+      code: '<template><div><custom data-id="foo" aria-test="bar" myProp="prop"></custom></div></template>',
+      options: ['never']
+    },
+    {
+      filename: 'test.vue',
+      code: '<template><div data-id="foo" aria-test="bar"><a onClick="" my-prop="prop"></a></div></template>',
+      options: ['never']
+    }
+  ],
+
+  invalid: [
+    {
+      filename: 'test.vue',
+      code: '<template><div><custom my-prop="foo"></custom></div></template>',
+      output: '<template><div><custom myProp="foo"></custom></div></template>',
+      options: ['never'],
+      errors: [{
+        message: "Attribute 'my-prop' cann't be hyphenated.",
+        type: 'VIdentifier',
+        line: 1
+      }]
+    },
+    {
+      filename: 'test.vue',
+      code: '<template><div><custom MyProp="Bar"></custom></div></template>',
+      output: '<template><div><custom my-prop="Bar"></custom></div></template>',
+      options: ['always'],
+      errors: [{
+        message: "Attribute 'MyProp' must be hyphenated.",
+        type: 'VIdentifier',
+        line: 1
+      }]
+    },
+    {
+      filename: 'test.vue',
+      code: '<template><div><custom :my-prop="prop"></custom></div></template>',
+      output: '<template><div><custom :myProp="prop"></custom></div></template>',
+      options: ['never'],
+      errors: [{
+        message: "Attribute ':my-prop' cann't be hyphenated.",
+        type: 'VDirectiveKey',
+        line: 1
+      }]
+    },
+    {
+      filename: 'test.vue',
+      code: '<template><div><custom :MyProp="prop"></custom></div></template>',
+      output: '<template><div><custom :my-prop="prop"></custom></div></template>',
+      options: ['always'],
+      errors: [{
+        message: "Attribute ':MyProp' must be hyphenated.",
+        type: 'VDirectiveKey',
+        line: 1
+      }]
+    },
+    {
+      filename: 'test.vue',
+      code: '<template><div><custom v-bind:my-prop="prop"></custom></div></template>',
+      output: '<template><div><custom v-bind:myProp="prop"></custom></div></template>',
+      options: ['never'],
+      errors: [{
+        message: "Attribute 'v-bind:my-prop' cann't be hyphenated.",
+        type: 'VDirectiveKey',
+        line: 1
+      }]
+    },
+    {
+      filename: 'test.vue',
+      code: '<template><div><custom v-bind:MyProp="prop"></custom></div></template>',
+      output: '<template><div><custom v-bind:my-prop="prop"></custom></div></template>',
+      options: ['always'],
+      errors: [{
+        message: "Attribute 'v-bind:MyProp' must be hyphenated.",
+        type: 'VDirectiveKey',
+        line: 1
+      }]
+    }
+  ]
+})