Repository: vuejs/eslint-plugin-vue
Pull request #559 (2018-08-16 00:46:44)

Add Vetur settings to "Why doesn't it work on .vue file?"
I had an issue where the plugin works, but red markings of errors remained. So I found out the instructions for this section are missing a note regarding Vetur. You need the following setting to complete the setup:
```
"vetur.validation.template": false
```

**Note**: While it solved my issue, there is something to consider with how it's applied. Eslint is mostly used in a per-project manner. This means, if you apply the rule above to `settings.json`, it's applied globally, thus Vue projects that do not use this plugin will not show template errors. To solve this, you need to apply the setting locally. In the context of VS Code, it's easy. You just save a "workspace" to your project root and that creates a file, where you can apply the settings, thus locally. See [here](https://github.com/Microsoft/vscode/issues/37519) for details.


Patch:
diff --git a/README.md b/README.md
index 4ad4a204..2cebaac5 100644
--- a/README.md
+++ b/README.md
@@ -284,8 +284,9 @@ The `vue-eslint-parser` uses the parser which is set by `parserOptions.parser` t
   ```
 
 2. Make sure your tool is set to lint `.vue` files.
-  - CLI targets only `.js` files by default. You have to specify additional extensions by `--ext` option or glob patterns. E.g. `eslint "src/**/*.{js,vue}"` or `eslint src --ext .vue`.
+  - CLI targets only `.js` files by default. You have to specify additional extensions by `--ext` option or glob patterns. E.g. `eslint "src/**/*.{js,vue}"` or `eslint src --ext .vue`. If you use `@vue/cli-plugin-eslint` and the `vue-cli-service lint` command - you don't have to worry about it.
   - VSCode targets only JavaScript or HTML files by default. You have to add `"vue"` to the `"eslint.validate"` array in vscode settings. e.g. `"eslint.validate": [ "javascript", "javascriptreact", "vue" ]`
+  - If you use `Vetur` plugin in VSCode - set `"vetur.validation.template": false` to avoid default Vetur template validation. Check out [vetur documentation](https://github.com/vuejs/vetur/blob/master/docs/linting-error.md) for more info.
 
 ## :anchor: Semantic Versioning Policy
 
diff --git a/docs/rules/no-unused-components.md b/docs/rules/no-unused-components.md
index 7220df0c..6375c111 100644
--- a/docs/rules/no-unused-components.md
+++ b/docs/rules/no-unused-components.md
@@ -64,4 +64,85 @@ Note that components registered under other than `PascalCase` name have to be ca
 
 ## :wrench: Options
 
-Nothing.
+```json
+{
+    "vue/no-unused-components": ["error", {
+        "ignoreWhenBindingPresent": true
+    }]
+}
+```
+
+- `ignoreWhenBindingPresent` ... surpresses all errors if binding has been detected in the template
+    default `true`
+
+
+:+1: Examples of **incorrect** code:
+
+```json
+{
+    "vue/no-unused-components": ["error", {
+        "ignoreWhenBindingPresent": false
+    }]
+}
+```
+
+```html
+<template>
+  <div>
+    <h2>Lorem ipsum</h2>
+    <component :is="computedComponent" />
+  </div>
+</template>
+
+<script>
+  import TheButton from 'components/TheButton.vue'
+  import TheSelect from 'components/TheSelect.vue'
+  import TheInput from 'components/TheInput.vue'
+
+  export default {
+    components: {
+      TheButton, // <- not used
+      TheSelect, // <- not used
+      TheInput, // <- not used
+    },
+    computed: {
+      computedComponent() { ... }
+    }
+  }
+</script>
+```
+
+:+1: Examples of **correct** code:
+
+```json
+{
+    "vue/no-unused-components": ["error", {
+        "ignoreWhenBindingPresent": false
+    }]
+}
+```
+
+```html
+<template>
+  <div>
+    <h2>Lorem ipsum</h2>
+    <TheButton v-if="" />
+    <TheSelect v-else-if="" />
+    <TheInput v-else="" />
+  </div>
+</template>
+
+<script>
+  import TheButton from 'components/TheButton.vue'
+  import TheSelect from 'components/TheSelect.vue'
+  import TheInput from 'components/TheInput.vue'
+
+  export default {
+    components: {
+      TheButton,
+      TheSelect,
+      TheInput,
+    },
+  }
+</script>
+```
diff --git a/docs/rules/no-v-html.md b/docs/rules/no-v-html.md
index a5494a2f..d5e82d17 100644
--- a/docs/rules/no-v-html.md
+++ b/docs/rules/no-v-html.md
@@ -2,7 +2,7 @@
 
 - :gear: This rule is included in `"plugin:vue/recommended"`.
 
-This rule reports use of `v-html` directive in order to reduce the risk of injecting potentially unsafe / unescaped html into the browser leading to Cross Side Scripting (XSS) attacks.
+This rule reports use of `v-html` directive in order to reduce the risk of injecting potentially unsafe / unescaped html into the browser leading to Cross-Site Scripting (XSS) attacks.
 
 ## :book: Rule Details
 
diff --git a/lib/rules/html-self-closing.js b/lib/rules/html-self-closing.js
index 65aac0a5..de310743 100644
--- a/lib/rules/html-self-closing.js
+++ b/lib/rules/html-self-closing.js
@@ -165,7 +165,11 @@ module.exports = {
               if (elementType === ELEMENT_TYPE.VOID) {
                 return fixer.replaceText(close, '>')
               }
-              return fixer.replaceText(close, `></${node.rawName}>`)
+              // If only `close` is targeted for replacement, it conflicts with `component-name-in-template-casing`,
+              // so replace the entire element.
+              // return fixer.replaceText(close, `></${node.rawName}>`)
+              const elementPart = sourceCode.text.slice(node.range[0], close.range[0])
+              return fixer.replaceText(node, elementPart + `></${node.rawName}>`)
             }
           })
         }
diff --git a/lib/rules/no-unused-components.js b/lib/rules/no-unused-components.js
index 90673dff..aff3743b 100644
--- a/lib/rules/no-unused-components.js
+++ b/lib/rules/no-unused-components.js
@@ -23,42 +23,51 @@ module.exports = {
       url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.3/docs/rules/no-unused-components.md'
     },
     fixable: null,
-    schema: []
+    schema: [{
+      type: 'object',
+      properties: {
+        ignoreWhenBindingPresent: {
+          type: 'boolean'
+        }
+      },
+      additionalProperties: false
+    }]
   },
 
   create (context) {
+    const options = context.options[0] || {}
+    const ignoreWhenBindingPresent = options.ignoreWhenBindingPresent !== undefined ? options.ignoreWhenBindingPresent : true
     const usedComponents = []
     let registeredComponents = []
+    let ignoreReporting = false
     let templateLocation
 
     return utils.defineTemplateBodyVisitor(context, {
       VElement (node) {
-        if (!utils.isCustomComponent(node)) return
-        let usedComponentName
-
-        if (utils.hasAttribute(node, 'is')) {
-          usedComponentName = utils.findAttribute(node, 'is').value.value
-        } else if (utils.hasDirective(node, 'bind', 'is')) {
-          const directiveNode = utils.findDirective(node, 'bind', 'is')
-          if (
-            directiveNode.value.type === 'VExpressionContainer' &&
-            directiveNode.value.expression.type === 'Literal'
-          ) {
-            usedComponentName = directiveNode.value.expression.value
-          }
-        } else {
-          usedComponentName = node.rawName
+        if (utils.isHtmlElementNode(node) && !utils.isHtmlWellKnownElementName(node.rawName)) {
+          usedComponents.push(node.rawName)
         }
+      },
+      "VAttribute[directive=true][key.name='bind'][key.argument='is']" (node) {
+        if (node.value.type !== 'VExpressionContainer') return
 
-        if (usedComponentName) {
-          usedComponents.push(usedComponentName)
+        if (node.value.expression.type === 'Literal') {
+          usedComponents.push(node.value.expression.value)
+        } else if (ignoreWhenBindingPresent) {
+          ignoreReporting = true
         }
       },
+      "VAttribute[directive=false][key.name='is']" (node) {
+        usedComponents.push(node.value.value)
+      },
       "VElement[name='template']" (rootNode) {
         templateLocation = templateLocation || rootNode.loc.start
       },
       "VElement[name='template']:exit" (rootNode) {
-        if (rootNode.loc.start !== templateLocation) return
+        if (
+          rootNode.loc.start !== templateLocation ||
+          ignoreReporting
+        ) return
 
         registeredComponents
           .filter(({ name }) => {
diff --git a/lib/utils/index.js b/lib/utils/index.js
index b39eeba0..8c39c63d 100644
--- a/lib/utils/index.js
+++ b/lib/utils/index.js
@@ -233,7 +233,7 @@ module.exports = {
     assert(node && node.type === 'VElement')
 
     return (
-      (this.isHtmlElementNode(node) && !this.isHtmlWellKnownElementName(node.name)) ||
+      (this.isHtmlElementNode(node) && !this.isHtmlWellKnownElementName(node.rawName)) ||
       this.hasAttribute(node, 'is') ||
       this.hasDirective(node, 'bind', 'is')
     )
@@ -280,7 +280,7 @@ module.exports = {
   isHtmlWellKnownElementName (name) {
     assert(typeof name === 'string')
 
-    return HTML_ELEMENT_NAMES.has(name.toLowerCase())
+    return HTML_ELEMENT_NAMES.has(name)
   },
 
   /**
@@ -291,7 +291,7 @@ module.exports = {
   isHtmlVoidElementName (name) {
     assert(typeof name === 'string')
 
-    return VOID_ELEMENT_NAMES.has(name.toLowerCase())
+    return VOID_ELEMENT_NAMES.has(name)
   },
 
   /**


Test patch:
diff --git a/tests/lib/autofix.js b/tests/lib/autofix.js
new file mode 100644
index 00000000..7d1b9604
--- /dev/null
+++ b/tests/lib/autofix.js
@@ -0,0 +1,98 @@
+/**
+ * @author Yosuke Ota <https://github.com/ota-meshi>
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+const Linter = require('eslint').Linter
+const chai = require('chai')
+
+const rules = require('../..').rules
+
+const assert = chai.assert
+
+const baseConfig = {
+  parser: 'vue-eslint-parser',
+  parserOptions: {
+    ecmaVersion: 2018,
+    sourceType: 'module',
+    ecmaFeatures: {
+      jsx: true
+    }
+  }
+}
+
+describe('Complex autofix test cases', () => {
+  const linter = new Linter()
+  for (const key of Object.keys(rules)) {
+    const ruleId = `vue/${key}`
+    linter.defineRule(ruleId, rules[key])
+  }
+
+  // https://github.com/vuejs/eslint-plugin-vue/issues/554
+  describe('Autofix of `html-self-closing` and `component-name-in-template-casing` should not conflict.', () => {
+    const kebabConfig = Object.assign({}, baseConfig, { 'rules': {
+      'vue/html-self-closing': ['error', {
+        'html': {
+          'component': 'never'
+        }
+      }],
+      'vue/component-name-in-template-casing': ['error', 'kebab-case']
+    }})
+    const pascalConfig = Object.assign({}, baseConfig, { 'rules': {
+      'vue/html-self-closing': ['error', {
+        'html': {
+          'component': 'never'
+        }
+      }],
+      'vue/component-name-in-template-casing': ['error']
+    }})
+    it('Even if set kebab-case, the output should be as expected.', () => {
+      const code = `
+      <template>
+        <VueComponent />
+      </template>`
+      const output = `
+      <template>
+        <vue-component ></vue-component>
+      </template>`
+
+      assert.equal(
+        linter.verifyAndFix(code, kebabConfig, 'test.vue').output,
+        output
+      )
+    })
+    it('Even if set PascalCase, the output should be as expected.', () => {
+      const code = `
+      <template>
+        <vue-component />
+      </template>`
+      const output = `
+      <template>
+        <VueComponent ></VueComponent>
+      </template>`
+
+      assert.equal(
+        linter.verifyAndFix(code, pascalConfig, 'test.vue').output,
+        output
+      )
+    })
+    it('Even if element have an attributes, the output should be as expected.', () => {
+      const code = `
+      <template>
+        <vue-component attr
+          id="item1" />
+      </template>`
+      const output = `
+      <template>
+        <VueComponent attr
+          id="item1" ></VueComponent>
+      </template>`
+
+      assert.equal(
+        linter.verifyAndFix(code, pascalConfig, 'test.vue').output,
+        output
+      )
+    })
+  })
+})
diff --git a/tests/lib/rules/no-unused-components.js b/tests/lib/rules/no-unused-components.js
index f412b3ec..939413a5 100644
--- a/tests/lib/rules/no-unused-components.js
+++ b/tests/lib/rules/no-unused-components.js
@@ -282,6 +282,57 @@ tester.run('no-unused-components', rule, {
         }
       </script>`
     },
+    {
+      filename: 'test.vue',
+      code: `<template>
+        <div id="app">
+          <Header></Header>
+          <div class="content">
+            <router-view></router-view>
+          </div>
+          <Footer />
+        </div>
+      </template>
+      <script>
+        import Header from 'components/Layout/Header';
+        import Footer from 'components/Layout/Footer';
+
+        export default {
+          name: 'App',
+          components: {
+            Header,
+            Footer,
+          },
+        };
+      </script>`
+    },
+
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div>
+          <component :is="dynamicComponent"></component>
+        </div>
+      </template>
+      <script>
+        import Foo from 'components/Foo';
+        import Bar from 'components/Bar';
+
+        export default {
+          components: {
+            Foo,
+            Bar
+          },
+          computed: {
+            dynamicComponent() {
+              return '...'
+            }
+          }
+        }
+      </script>`,
+      options: [{ ignoreWhenBindingPresent: true }]
+    },
 
     // Ignore when `render` is used instead of temoplate
     {
@@ -364,6 +415,40 @@ tester.run('no-unused-components', rule, {
         message: 'The "the-button" component has been registered but not used.',
         line: 11
       }]
+    },
+    // Setting: ignoreWhenBindingPresent
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div>
+          <component :is="dynamicComponent"></component>
+        </div>
+      </template>
+      <script>
+        import Foo from 'components/Foo';
+        import Bar from 'components/Bar';
+
+        export default {
+          components: {
+            Foo,
+            Bar
+          },
+          computed: {
+            dynamicComponent() {
+              return '...'
+            }
+          }
+        }
+      </script>`,
+      options: [{ ignoreWhenBindingPresent: false }],
+      errors: [{
+        message: 'The "Foo" component has been registered but not used.',
+        line: 13
+      }, {
+        message: 'The "Bar" component has been registered but not used.',
+        line: 14
+      }]
     }
   ]
 })