Repository: vuejs/eslint-plugin-vue
Pull request #397 (2018-02-19 14:28:04)

⭐️New: Add `vue/component-name-in-template-casing`
This PR adds `vue/component-name-in-template-casing` rule.
This implements rule proposed in #250

ota-meshi: @michalsnik
Thank you for review!
I changed it, so please check it.
ota-meshi: I think we do not have to emphasize html because this rule targets `custom components` and `html elements` like `<div>` and `<span>` are not targets.
However, I am not good at English,  so maybe I haven't understood your intention correctly.
shroudedcode: Any updates on this pull request, @ota-meshi? I would love to see this merged!
trollepierre: I made the modification in this pull request: https://github.com/vuejs/eslint-plugin-vue/pull/485
can we merge it?
ota-meshi: I'm sorry to reply late. I recognize the following situation.

1. Rule name
It is proposed that `html-element-name-casing` should be used, but [I think that `component-name-in-template-casing` is good](https://github.com/vuejs/eslint-plugin-vue/pull/397#issuecomment-368066556).
Originally `component-name-in-template-casing` is also the name defined in #250, so I would like to ask the opinion of @michalsnik.

2. Default casing
I personally like `kebab-case`. However, in the official [StyleGuide](https://vuejs.org/v2/style-guide/#Component-name-casing-in-templates-strongly-recommended) of the Vue community, it is recommended to unify it with `PascalCase` first. Since `eslint-plugin-vue` is the official plugin of the Vue community, I think that `PascalCase` should be most easily available.

Either way, I will respect the opinion of the main members(@michalsnik @mysticatea @chrisvfritz), so I would like to receive opinions.
trollepierre: @michalsnik @mysticatea @chrisvfritz : do you have an opinion of this topic, so we can merge this PR or the other PR I made with the requested change (https://github.com/vuejs/eslint-plugin-vue/pull/485#pullrequestreview-124832375) ?
chrisvfritz: - Rule name: I personally like `component-name-in-template-casing` for the name. Since in-DOM HTML can _never_ be PascalCase, I think `html-element-name-casing` would be too broad.

- Default casing: Yep, PascalCase should be the default for _components_.

**Here's the issue though**: Can this rule distinguish between Vue components and non-Vue custom elements? Those will still need to be kebab-case. For that reason, I'm not sure this rule is actually feasible to implement.

I think new, uncategorized `html-element-name-kebab-casing` rule could be a good idea for people who prefer that style though, because that would actually implementable.
chrisvfritz: @ota-meshi The problem is, I'm not sure it's actually a solvable problem. 😞

And yes, I recommend creating a new issue for that rule. It should be much less controversial than this one and porbably not even require any options. 🙂
ota-meshi: @chrisvfritz
I added a new issue.#499
Please check whether the contents are consistent.
michalsnik: Sorry guys for such a long absence..

I think that:
- `component-name-in-template-casing` is a good name
-`PascalCase` is sensible default aligned with our style guide

Also:
- this rule checks only content of SFC's `<template>` tags
- this rule looks only for custom elements, using [this helper](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/utils/index.js#L184) thus it's exactly targeting what name suggests: `component-name`

Given that I think it's ready to be merged. Unless I missed some vital information in all above comments? It would still be in an `unassigned` category for some time, and it would give as a moment to test it in real life projects.
ota-meshi: @michalsnik  I agree.
If few custom elements are used, I think that it can be solved with option.
I think that it is unlikely that products using Vue.js uses a lot of custom elements, so I think that this rule will be useful.

Patch:
diff --git a/README.md b/README.md
index ea0d0c22..1e5ed04d 100644
--- a/README.md
+++ b/README.md
@@ -190,6 +190,7 @@ Enforce all the rules in this category, as well as all higher priority rules, wi
 |    | Rule ID | Description |
 |:---|:--------|:------------|
 | :wrench: | [vue/attribute-hyphenation](./docs/rules/attribute-hyphenation.md) | enforce attribute naming style on custom components in template |
+| :wrench: | [vue/component-name-in-template-casing](./docs/rules/component-name-in-template-casing.md) | enforce specific casing for the component naming style in template |
 | :wrench: | [vue/html-closing-bracket-newline](./docs/rules/html-closing-bracket-newline.md) | require or disallow a line break before tag's closing brackets |
 | :wrench: | [vue/html-closing-bracket-spacing](./docs/rules/html-closing-bracket-spacing.md) | require or disallow a space before tag's closing brackets |
 | :wrench: | [vue/html-end-tags](./docs/rules/html-end-tags.md) | enforce end tag style |
diff --git a/docs/rules/component-name-in-template-casing.md b/docs/rules/component-name-in-template-casing.md
new file mode 100644
index 00000000..68cfa791
--- /dev/null
+++ b/docs/rules/component-name-in-template-casing.md
@@ -0,0 +1,74 @@
+# enforce specific casing for the component naming style in template (vue/component-name-in-template-casing)
+
+- :gear: This rule is included in `"plugin:vue/strongly-recommended"` and `"plugin:vue/recommended"`.
+- :wrench: The `--fix` option on the [command line](http://eslint.org/docs/user-guide/command-line-interface#fix) can automatically fix some of the problems reported by this rule.
+
+Define a style for the component name in template casing for consistency purposes.
+
+## :book: Rule Details
+
+:+1: Examples of **correct** code for `PascalCase`:
+
+```html
+<template>
+  <TheComponent />
+</template>
+```
+
+:-1: Examples of **incorrect** code for `PascalCase`:
+
+```html
+<template>
+  <the-component />
+  <theComponent />
+  <The-component />
+</template>
+```
+
+:+1: Examples of **correct** code for `kebab-case`:
+
+```html
+<template>
+  <the-component />
+</template>
+```
+
+:-1: Examples of **incorrect** code for `kebab-case`:
+
+```html
+<template>
+  <TheComponent />
+  <theComponent />
+  <Thecomponent />
+  <The-component />
+</template>
+```
+
+## :wrench: Options
+
+Default casing is set to `PascalCase`.
+
+```json
+  "vue/component-name-in-template-casing": ["error",
+    "PascalCase|kebab-case",
+    {
+      "ignores": []
+    }
+  ]
+```
+
+- `ignores` (`string[]`) ... The element name to ignore. Sets the element name to allow. For example, a custom element or a non-Vue component.
+
+
+:+1: Examples of **correct** code for `{ignores: ["custom-element"]}`:
+
+```html
+<template>
+  <custom-element></custom-element>
+  <TheComponent/>
+</template>
+```
+
+## Related links
+
+- [Style guide - Component name casing in templates](https://vuejs.org/v2/style-guide/#Component-name-casing-in-templates-strongly-recommended)
diff --git a/lib/configs/strongly-recommended.js b/lib/configs/strongly-recommended.js
index 53acc859..57ae9c8e 100644
--- a/lib/configs/strongly-recommended.js
+++ b/lib/configs/strongly-recommended.js
@@ -7,6 +7,7 @@ module.exports = {
   extends: require.resolve('./essential'),
   rules: {
     'vue/attribute-hyphenation': 'error',
+    'vue/component-name-in-template-casing': 'error',
     'vue/html-closing-bracket-newline': 'error',
     'vue/html-closing-bracket-spacing': 'error',
     'vue/html-end-tags': 'error',
diff --git a/lib/index.js b/lib/index.js
index 79769f2a..8e5709a1 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -10,6 +10,7 @@ module.exports = {
     'attribute-hyphenation': require('./rules/attribute-hyphenation'),
     'attributes-order': require('./rules/attributes-order'),
     'comment-directive': require('./rules/comment-directive'),
+    'component-name-in-template-casing': require('./rules/component-name-in-template-casing'),
     'html-closing-bracket-newline': require('./rules/html-closing-bracket-newline'),
     'html-closing-bracket-spacing': require('./rules/html-closing-bracket-spacing'),
     'html-end-tags': require('./rules/html-end-tags'),
diff --git a/lib/rules/component-name-in-template-casing.js b/lib/rules/component-name-in-template-casing.js
new file mode 100644
index 00000000..689398a7
--- /dev/null
+++ b/lib/rules/component-name-in-template-casing.js
@@ -0,0 +1,108 @@
+/**
+ * @author Yosuke Ota
+ * issue https://github.com/vuejs/eslint-plugin-vue/issues/250
+ */
+'use strict'
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const utils = require('../utils')
+const casing = require('../utils/casing')
+
+const allowedCaseOptions = ['PascalCase', 'kebab-case']
+const defaultCase = 'PascalCase'
+
+// ------------------------------------------------------------------------------
+// Rule Definition
+// ------------------------------------------------------------------------------
+
+module.exports = {
+  meta: {
+    docs: {
+      description: 'enforce specific casing for the component naming style in template',
+      category: undefined, // strongly-recommended
+      url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.1/docs/rules/component-name-in-template-casing.md'
+    },
+    fixable: 'code',
+    schema: [
+      {
+        enum: allowedCaseOptions
+      },
+      {
+        type: 'object',
+        properties: {
+          ignores: {
+            type: 'array',
+            items: { type: 'string' },
+            uniqueItems: true,
+            additionalItems: false
+          }
+        },
+        additionalProperties: false
+      }
+    ]
+  },
+
+  create (context) {
+    const caseOption = context.options[0]
+    const options = context.options[1] || {}
+    const caseType = allowedCaseOptions.indexOf(caseOption) !== -1 ? caseOption : defaultCase
+    const ignores = options.ignores || []
+    const tokens = context.parserServices.getTemplateBodyTokenStore && context.parserServices.getTemplateBodyTokenStore()
+    const sourceCode = context.getSourceCode()
+
+    let hasInvalidEOF = false
+
+    return utils.defineTemplateBodyVisitor(context, {
+      'VElement' (node) {
+        if (hasInvalidEOF) {
+          return
+        }
+
+        if (!utils.isCustomComponent(node)) {
+          return
+        }
+
+        const name = node.rawName
+        if (ignores.indexOf(name) >= 0) {
+          return
+        }
+        const casingName = casing.getConverter(caseType)(name)
+        if (casingName !== name) {
+          const startTag = node.startTag
+          const open = tokens.getFirstToken(startTag)
+
+          context.report({
+            node: open,
+            loc: open.loc,
+            message: 'Component name "{{name}}" is not {{caseType}}.',
+            data: {
+              name,
+              caseType
+            },
+            fix: fixer => {
+              const endTag = node.endTag
+              if (!endTag) {
+                return fixer.replaceText(open, `<${casingName}`)
+              }
+              const endTagOpen = tokens.getFirstToken(endTag)
+              // If we can upgrade requirements to `eslint@>4.1.0`, this code can be replaced by:
+              // return [
+              //   fixer.replaceText(open, `<${casingName}`),
+              //   fixer.replaceText(endTagOpen, `</${casingName}`)
+              // ]
+              const code = `<${casingName}${sourceCode.text.slice(open.range[1], endTagOpen.range[0])}</${casingName}`
+              return fixer.replaceTextRange([open.range[0], endTagOpen.range[1]], code)
+            }
+          })
+        }
+      }
+    }, {
+      Program (node) {
+        hasInvalidEOF = utils.hasInvalidEOF(node)
+      }
+    })
+  }
+}


Test patch:
diff --git a/tests/lib/rules/component-name-in-template-casing.js b/tests/lib/rules/component-name-in-template-casing.js
new file mode 100644
index 00000000..b2fc9871
--- /dev/null
+++ b/tests/lib/rules/component-name-in-template-casing.js
@@ -0,0 +1,320 @@
+/**
+ * @author Yosuke Ota
+ */
+'use strict'
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const rule = require('../../../lib/rules/component-name-in-template-casing')
+const RuleTester = require('eslint').RuleTester
+
+// ------------------------------------------------------------------------------
+// Tests
+// ------------------------------------------------------------------------------
+
+const tester = new RuleTester({
+  parser: 'vue-eslint-parser'
+})
+
+tester.run('component-name-in-template-casing', rule, {
+  valid: [
+    // default
+    '<template><div/></template>',
+    '<template><img></template>',
+    '<template><TheComponent/></template>',
+    '<template><svg><path/></svg></template>',
+    '<template><math><mspace/></math></template>',
+    '<template><div><slot></slot></div></template>',
+
+    // kebab-case
+    {
+      code: '<template><the-component></the-component></template>',
+      options: ['kebab-case']
+    },
+    {
+      code: '<template><div/></template>',
+      options: ['kebab-case']
+    },
+    {
+      code: '<template><img></template>',
+      options: ['kebab-case']
+    },
+    {
+      code: '<template><svg><path/></svg></template>',
+      options: ['kebab-case']
+    },
+    {
+      code: '<template><math><mspace/></math></template>',
+      options: ['kebab-case']
+    },
+    // ignores
+    {
+      code: '<template><custom-element></custom-element></template>',
+      options: ['PascalCase', { ignores: ['custom-element'] }]
+    },
+    {
+      code: '<template><custom-element><TheComponent/></custom-element></template>',
+      options: ['PascalCase', { ignores: ['custom-element'] }]
+    },
+    // Invalid EOF
+    '<template><the-component a=">test</the-component></template>',
+    '<template><the-component><!--test</the-component></template>'
+  ],
+  invalid: [
+    {
+      code: `
+      <template>
+        <the-component id="id">
+          <!-- comment -->
+        </the-component>
+      </template>
+      `,
+      output: `
+      <template>
+        <TheComponent id="id">
+          <!-- comment -->
+        </TheComponent>
+      </template>
+      `,
+      errors: ['Component name "the-component" is not PascalCase.']
+    },
+    {
+      code: `
+      <template>
+        <the-component id="id"/>
+      </template>
+      `,
+      output: `
+      <template>
+        <TheComponent id="id"/>
+      </template>
+      `,
+      errors: ['Component name "the-component" is not PascalCase.']
+    },
+    {
+      code: `
+      <template>
+        <TheComponent id="id">
+          <!-- comment -->
+        </TheComponent>
+      </template>
+      `,
+      options: ['kebab-case'],
+      output: `
+      <template>
+        <the-component id="id">
+          <!-- comment -->
+        </the-component>
+      </template>
+      `,
+      errors: ['Component name "TheComponent" is not kebab-case.']
+    },
+    {
+      code: `
+      <template>
+        <TheComponent id="id"/>
+      </template>
+      `,
+      options: ['kebab-case'],
+      output: `
+      <template>
+        <the-component id="id"/>
+      </template>
+      `,
+      errors: ['Component name "TheComponent" is not kebab-case.']
+    },
+    {
+      code: `
+      <template>
+        <the-component
+          id="id"/>
+      </template>
+      `,
+      output: `
+      <template>
+        <TheComponent
+          id="id"/>
+      </template>
+      `,
+      errors: ['Component name "the-component" is not PascalCase.']
+    },
+    {
+      code: `
+      <template>
+        <the-component/>
+      </template>
+      `,
+      output: `
+      <template>
+        <TheComponent/>
+      </template>
+      `,
+      errors: ['Component name "the-component" is not PascalCase.']
+    },
+    {
+      code: `
+      <template>
+        <the-component></the-component>
+      </template>
+      `,
+      output: `
+      <template>
+        <TheComponent></TheComponent>
+      </template>
+      `,
+      errors: ['Component name "the-component" is not PascalCase.']
+    },
+    {
+      code: `
+      <template>
+        <theComponent/>
+      </template>
+      `,
+      output: `
+      <template>
+        <TheComponent/>
+      </template>
+      `,
+      errors: ['Component name "theComponent" is not PascalCase.']
+    },
+    {
+      code: `
+      <template>
+        <theComponent/>
+      </template>
+      `,
+      options: ['kebab-case'],
+      output: `
+      <template>
+        <the-component/>
+      </template>
+      `,
+      errors: ['Component name "theComponent" is not kebab-case.']
+    },
+    {
+      code: `
+      <template>
+        <The-component/>
+      </template>
+      `,
+      output: `
+      <template>
+        <TheComponent/>
+      </template>
+      `,
+      errors: ['Component name "The-component" is not PascalCase.']
+    },
+    {
+      code: `
+      <template>
+        <The-component/>
+      </template>
+      `,
+      options: ['kebab-case'],
+      output: `
+      <template>
+        <the-component/>
+      </template>
+      `,
+      errors: ['Component name "The-component" is not kebab-case.']
+    },
+    {
+      code: `
+      <template>
+        <Thecomponent/>
+      </template>
+      `,
+      options: ['kebab-case'],
+      output: `
+      <template>
+        <thecomponent/>
+      </template>
+      `,
+      errors: ['Component name "Thecomponent" is not kebab-case.']
+    },
+    {
+      code: `
+      <template>
+        <the-component></the-component  >
+      </template>
+      `,
+      output: `
+      <template>
+        <TheComponent></TheComponent  >
+      </template>
+      `,
+      errors: ['Component name "the-component" is not PascalCase.']
+    },
+    {
+      code: `
+      <template>
+        <the-component></the-component
+        >
+      </template>
+      `,
+      output: `
+      <template>
+        <TheComponent></TheComponent
+        >
+      </template>
+      `,
+      errors: ['Component name "the-component" is not PascalCase.']
+    },
+    {
+      code: `
+      <template>
+        <the-component></the-component end-tag-attr="attr" >
+      </template>
+      `,
+      output: `
+      <template>
+        <TheComponent></TheComponent end-tag-attr="attr" >
+      </template>
+      `,
+      errors: ['Component name "the-component" is not PascalCase.']
+    },
+
+    // ignores
+    {
+      code: `
+      <template>
+        <custom-element>
+          <the-component />
+        </custom-element>
+      </template>`,
+      output: `
+      <template>
+        <custom-element>
+          <TheComponent />
+        </custom-element>
+      </template>`,
+      options: ['PascalCase', { ignores: ['custom-element'] }],
+      errors: ['Component name "the-component" is not PascalCase.']
+    },
+    {
+      code: `
+      <template>
+        <custom-element1>
+          <the-component />
+        </custom-element1>
+        <custom-element2 />
+        <the-component />
+      </template>`,
+      output: `
+      <template>
+        <custom-element1>
+          <TheComponent />
+        </custom-element1>
+        <custom-element2 />
+        <TheComponent />
+      </template>`,
+      options: ['PascalCase', { ignores: ['custom-element1', 'custom-element2'] }],
+      errors: [
+        'Component name "the-component" is not PascalCase.',
+        'Component name "the-component" is not PascalCase.'
+      ]
+    }
+  ]
+})