Repository: vuejs/eslint-plugin-vue
Pull request #545 (2018-08-03 15:16:21)

(implements #414) Add "no-unused-components" rule
This PR implements rule proposed in #414 


Patch:
diff --git a/README.md b/README.md
index 7870ed23..a38bee46 100644
--- a/README.md
+++ b/README.md
@@ -156,6 +156,7 @@ Enforce all the rules in this category, as well as all higher priority rules, wi
 |  | [vue/no-side-effects-in-computed-properties](./docs/rules/no-side-effects-in-computed-properties.md) | disallow side effects in computed properties |
 |  | [vue/no-template-key](./docs/rules/no-template-key.md) | disallow `key` attribute on `<template>` |
 |  | [vue/no-textarea-mustache](./docs/rules/no-textarea-mustache.md) | disallow mustaches in `<textarea>` |
+|  | [vue/no-unused-components](./docs/rules/no-unused-components.md) | disallow unused components |
 |  | [vue/no-unused-vars](./docs/rules/no-unused-vars.md) | disallow unused variable definitions of v-for directives or scope attributes |
 |  | [vue/no-use-v-if-with-v-for](./docs/rules/no-use-v-if-with-v-for.md) | disallow use v-if on the same element as v-for |
 |  | [vue/require-component-is](./docs/rules/require-component-is.md) | require `v-bind:is` of `<component>` elements |
diff --git a/docs/rules/no-unused-components.md b/docs/rules/no-unused-components.md
new file mode 100644
index 00000000..85a584ba
--- /dev/null
+++ b/docs/rules/no-unused-components.md
@@ -0,0 +1,67 @@
+# disallow unused components (vue/no-unused-components)
+
+- :gear: This rule is included in all of `"plugin:vue/essential"`, `"plugin:vue/strongly-recommended"` and `"plugin:vue/recommended"`.
+
+This rule reports components that haven't been used in the template.
+
+## :book: Rule Details
+
+:-1: Examples of **incorrect** code for this rule:
+
+```html
+<template>
+  <div>
+    <h2>Lorem ipsum</h2>
+    <TheModal />
+  </div>
+</template>
+
+<script>
+  import TheButton from 'components/TheButton.vue'
+  import TheModal from 'components/TheModal.vue'
+
+  export default {
+    components: {
+      TheButton // Unused component
+      'the-modal': TheModal // Unused component
+    }
+  }
+</script>
+```
+
+Note that components registered under other than `PascalCase` name have to be called directly under the specified name, whereas if you register it using `PascalCase` you can call it however you like, except using `snake_case`.
+
+:+1: Examples of **correct** code for this rule:
+
+```html
+<template>
+  <div>
+    <h2>Lorem ipsum</h2>
+    <the-modal>
+      <component is="TheInput" />
+      <component :is="'TheDropdown'" />
+      <TheButton>CTA</TheButton>
+    </the-modal>
+  </div>
+</template>
+
+<script>
+  import TheButton from 'components/TheButton.vue'
+  import TheModal from 'components/TheModal.vue'
+  import TheInput from 'components/TheInput.vue'
+  import TheDropdown from 'components/TheDropdown.vue'
+
+  export default {
+    components: {
+      TheButton,
+      TheModal,
+      TheInput,
+      TheDropdown,
+    }
+  }
+</script>
+```
+
+## :wrench: Options
+
+Nothing.
diff --git a/lib/configs/essential.js b/lib/configs/essential.js
index 955571ad..4022c37b 100644
--- a/lib/configs/essential.js
+++ b/lib/configs/essential.js
@@ -15,6 +15,7 @@ module.exports = {
     'vue/no-side-effects-in-computed-properties': 'error',
     'vue/no-template-key': 'error',
     'vue/no-textarea-mustache': 'error',
+    'vue/no-unused-components': 'error',
     'vue/no-unused-vars': 'error',
     'vue/no-use-v-if-with-v-for': 'error',
     'vue/require-component-is': 'error',
diff --git a/lib/index.js b/lib/index.js
index 702fd41e..4d3fb5d4 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -33,6 +33,7 @@ module.exports = {
     'no-template-key': require('./rules/no-template-key'),
     'no-template-shadow': require('./rules/no-template-shadow'),
     'no-textarea-mustache': require('./rules/no-textarea-mustache'),
+    'no-unused-components': require('./rules/no-unused-components'),
     'no-unused-vars': require('./rules/no-unused-vars'),
     'no-use-v-if-with-v-for': require('./rules/no-use-v-if-with-v-for'),
     'no-v-html': require('./rules/no-v-html'),
diff --git a/lib/rules/attribute-hyphenation.js b/lib/rules/attribute-hyphenation.js
index c430f446..8abc4a8e 100644
--- a/lib/rules/attribute-hyphenation.js
+++ b/lib/rules/attribute-hyphenation.js
@@ -16,7 +16,7 @@ module.exports = {
     docs: {
       description: 'enforce attribute naming style on custom components in template',
       category: 'strongly-recommended',
-      url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.1/docs/rules/attribute-hyphenation.md'
+      url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.2/docs/rules/attribute-hyphenation.md'
     },
     fixable: 'code',
     schema: [
diff --git a/lib/rules/attributes-order.js b/lib/rules/attributes-order.js
index c3b3e665..4cc67a0a 100644
--- a/lib/rules/attributes-order.js
+++ b/lib/rules/attributes-order.js
@@ -121,7 +121,7 @@ module.exports = {
     docs: {
       description: 'enforce order of attributes',
       category: 'recommended',
-      url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.1/docs/rules/attributes-order.md'
+      url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.2/docs/rules/attributes-order.md'
     },
     fixable: 'code',
     schema: {
diff --git a/lib/rules/comment-directive.js b/lib/rules/comment-directive.js
index 790d3ef3..46c77c68 100644
--- a/lib/rules/comment-directive.js
+++ b/lib/rules/comment-directive.js
@@ -109,7 +109,7 @@ module.exports = {
     docs: {
       description: 'support comment-directives in `<template>`',
       category: 'base',
-      url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.1/docs/rules/comment-directive.md'
+      url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.2/docs/rules/comment-directive.md'
     },
     schema: []
   },
diff --git a/lib/rules/component-name-in-template-casing.js b/lib/rules/component-name-in-template-casing.js
index 689398a7..7d0eeba4 100644
--- a/lib/rules/component-name-in-template-casing.js
+++ b/lib/rules/component-name-in-template-casing.js
@@ -23,7 +23,7 @@ module.exports = {
     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'
+      url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.2/docs/rules/component-name-in-template-casing.md'
     },
     fixable: 'code',
     schema: [
diff --git a/lib/rules/html-closing-bracket-newline.js b/lib/rules/html-closing-bracket-newline.js
index 2e7d4787..e36605ed 100644
--- a/lib/rules/html-closing-bracket-newline.js
+++ b/lib/rules/html-closing-bracket-newline.js
@@ -32,7 +32,7 @@ module.exports = {
     docs: {
       description: "require or disallow a line break before tag's closing brackets",
       category: 'strongly-recommended',
-      url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.1/docs/rules/html-closing-bracket-newline.md'
+      url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.2/docs/rules/html-closing-bracket-newline.md'
     },
     fixable: 'whitespace',
     schema: [{
diff --git a/lib/rules/html-closing-bracket-spacing.js b/lib/rules/html-closing-bracket-spacing.js
index 296f0074..84454094 100644
--- a/lib/rules/html-closing-bracket-spacing.js
+++ b/lib/rules/html-closing-bracket-spacing.js
@@ -53,7 +53,7 @@ module.exports = {
     docs: {
       description: 'require or disallow a space before tag\'s closing brackets',
       category: 'strongly-recommended',
-      url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.1/docs/rules/html-closing-bracket-spacing.md'
+      url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.2/docs/rules/html-closing-bracket-spacing.md'
     },
     schema: [{
       type: 'object',
diff --git a/lib/rules/html-end-tags.js b/lib/rules/html-end-tags.js
index 327b4f67..393d1606 100644
--- a/lib/rules/html-end-tags.js
+++ b/lib/rules/html-end-tags.js
@@ -20,7 +20,7 @@ module.exports = {
     docs: {
       description: 'enforce end tag style',
       category: 'strongly-recommended',
-      url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.1/docs/rules/html-end-tags.md'
+      url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.2/docs/rules/html-end-tags.md'
     },
     fixable: 'code',
     schema: []
diff --git a/lib/rules/html-indent.js b/lib/rules/html-indent.js
index 46df0488..94c81684 100644
--- a/lib/rules/html-indent.js
+++ b/lib/rules/html-indent.js
@@ -29,7 +29,7 @@ module.exports = {
     docs: {
       description: 'enforce consistent indentation in `<template>`',
       category: 'strongly-recommended',
-      url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.1/docs/rules/html-indent.md'
+      url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.2/docs/rules/html-indent.md'
     },
     fixable: 'whitespace',
     schema: [
diff --git a/lib/rules/html-quotes.js b/lib/rules/html-quotes.js
index 9ab04ba3..bb86f811 100644
--- a/lib/rules/html-quotes.js
+++ b/lib/rules/html-quotes.js
@@ -20,7 +20,7 @@ module.exports = {
     docs: {
       description: 'enforce quotes style of HTML attributes',
       category: 'recommended',
-      url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.1/docs/rules/html-quotes.md'
+      url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.2/docs/rules/html-quotes.md'
     },
     fixable: 'code',
     schema: [
diff --git a/lib/rules/html-self-closing.js b/lib/rules/html-self-closing.js
index 5c13af92..165273f4 100644
--- a/lib/rules/html-self-closing.js
+++ b/lib/rules/html-self-closing.js
@@ -88,7 +88,7 @@ module.exports = {
     docs: {
       description: 'enforce self-closing style',
       category: 'strongly-recommended',
-      url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.1/docs/rules/html-self-closing.md'
+      url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.2/docs/rules/html-self-closing.md'
     },
     fixable: 'code',
     schema: {
diff --git a/lib/rules/jsx-uses-vars.js b/lib/rules/jsx-uses-vars.js
index d5fed14b..8a80671a 100644
--- a/lib/rules/jsx-uses-vars.js
+++ b/lib/rules/jsx-uses-vars.js
@@ -39,7 +39,7 @@ module.exports = {
     docs: {
       description: 'prevent variables used in JSX to be marked as unused', // eslint-disable-line consistent-docs-description
       category: 'base',
-      url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.1/docs/rules/jsx-uses-vars.md'
+      url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.2/docs/rules/jsx-uses-vars.md'
     },
     schema: []
   },
diff --git a/lib/rules/max-attributes-per-line.js b/lib/rules/max-attributes-per-line.js
index 600a2045..73ced6a6 100644
--- a/lib/rules/max-attributes-per-line.js
+++ b/lib/rules/max-attributes-per-line.js
@@ -14,7 +14,7 @@ module.exports = {
     docs: {
       description: 'enforce the maximum number of attributes per line',
       category: 'strongly-recommended',
-      url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.1/docs/rules/max-attributes-per-line.md'
+      url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.2/docs/rules/max-attributes-per-line.md'
     },
     fixable: 'whitespace', // or "code" or "whitespace"
     schema: [
diff --git a/lib/rules/mustache-interpolation-spacing.js b/lib/rules/mustache-interpolation-spacing.js
index 52e1927b..bb20a36f 100644
--- a/lib/rules/mustache-interpolation-spacing.js
+++ b/lib/rules/mustache-interpolation-spacing.js
@@ -19,7 +19,7 @@ module.exports = {
     docs: {
       description: 'enforce unified spacing in mustache interpolations',
       category: 'strongly-recommended',
-      url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.1/docs/rules/mustache-interpolation-spacing.md'
+      url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.2/docs/rules/mustache-interpolation-spacing.md'
     },
     fixable: 'whitespace',
     schema: [
diff --git a/lib/rules/name-property-casing.js b/lib/rules/name-property-casing.js
index 405b2cf1..2461e84e 100644
--- a/lib/rules/name-property-casing.js
+++ b/lib/rules/name-property-casing.js
@@ -17,7 +17,7 @@ module.exports = {
     docs: {
       description: 'enforce specific casing for the name property in Vue components',
       category: 'strongly-recommended',
-      url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.1/docs/rules/name-property-casing.md'
+      url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.2/docs/rules/name-property-casing.md'
     },
     fixable: 'code', // or "code" or "whitespace"
     schema: [
diff --git a/lib/rules/no-async-in-computed-properties.js b/lib/rules/no-async-in-computed-properties.js
index fdead1be..685c6ac2 100644
--- a/lib/rules/no-async-in-computed-properties.js
+++ b/lib/rules/no-async-in-computed-properties.js
@@ -64,7 +64,7 @@ module.exports = {
     docs: {
       description: 'disallow asynchronous actions in computed properties',
       category: 'essential',
-      url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.1/docs/rules/no-async-in-computed-properties.md'
+      url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.2/docs/rules/no-async-in-computed-properties.md'
     },
     fixable: null,
     schema: []
diff --git a/lib/rules/no-confusing-v-for-v-if.js b/lib/rules/no-confusing-v-for-v-if.js
index 2a340050..623e4129 100644
--- a/lib/rules/no-confusing-v-for-v-if.js
+++ b/lib/rules/no-confusing-v-for-v-if.js
@@ -39,7 +39,7 @@ module.exports = {
     docs: {
       description: 'disallow confusing `v-for` and `v-if` on the same element',
       category: 'recommended',
-      url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.1/docs/rules/no-confusing-v-for-v-if.md',
+      url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.2/docs/rules/no-confusing-v-for-v-if.md',
       replacedBy: ['no-use-v-if-with-v-for']
     },
     deprecated: true,
diff --git a/lib/rules/no-dupe-keys.js b/lib/rules/no-dupe-keys.js
index 02a62add..0b8e6fac 100644
--- a/lib/rules/no-dupe-keys.js
+++ b/lib/rules/no-dupe-keys.js
@@ -17,7 +17,7 @@ module.exports = {
     docs: {
       description: 'disallow duplication of field names',
       category: 'essential',
-      url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.1/docs/rules/no-dupe-keys.md'
+      url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.2/docs/rules/no-dupe-keys.md'
     },
     fixable: null, // or "code" or "whitespace"
     schema: [
diff --git a/lib/rules/no-duplicate-attributes.js b/lib/rules/no-duplicate-attributes.js
index f0aef908..2f02cecc 100644
--- a/lib/rules/no-duplicate-attributes.js
+++ b/lib/rules/no-duplicate-attributes.js
@@ -39,7 +39,7 @@ module.exports = {
     docs: {
       description: 'disallow duplication of attributes',
       category: 'essential',
-      url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.1/docs/rules/no-duplicate-attributes.md'
+      url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.2/docs/rules/no-duplicate-attributes.md'
     },
     fixable: null,
 
diff --git a/lib/rules/no-multi-spaces.js b/lib/rules/no-multi-spaces.js
index fd08e4c0..144f0fea 100644
--- a/lib/rules/no-multi-spaces.js
+++ b/lib/rules/no-multi-spaces.js
@@ -13,7 +13,7 @@ module.exports = {
     docs: {
       description: 'disallow multiple spaces',
       category: 'strongly-recommended',
-      url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.1/docs/rules/no-multi-spaces.md'
+      url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.2/docs/rules/no-multi-spaces.md'
     },
     fixable: 'whitespace', // or "code" or "whitespace"
     schema: []
diff --git a/lib/rules/no-parsing-error.js b/lib/rules/no-parsing-error.js
index 076ef9b3..e3060870 100644
--- a/lib/rules/no-parsing-error.js
+++ b/lib/rules/no-parsing-error.js
@@ -58,7 +58,7 @@ module.exports = {
     docs: {
       description: 'disallow parsing errors in `<template>`',
       category: 'essential',
-      url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.1/docs/rules/no-parsing-error.md'
+      url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.2/docs/rules/no-parsing-error.md'
     },
     fixable: null,
     schema: [
diff --git a/lib/rules/no-reserved-keys.js b/lib/rules/no-reserved-keys.js
index 89c8aa4c..2f1526ee 100644
--- a/lib/rules/no-reserved-keys.js
+++ b/lib/rules/no-reserved-keys.js
@@ -18,7 +18,7 @@ module.exports = {
     docs: {
       description: 'disallow overwriting reserved keys',
       category: 'essential',
-      url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.1/docs/rules/no-reserved-keys.md'
+      url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.2/docs/rules/no-reserved-keys.md'
     },
     fixable: null,
     schema: [
diff --git a/lib/rules/no-shared-component-data.js b/lib/rules/no-shared-component-data.js
index 6d23f46c..22c838e1 100644
--- a/lib/rules/no-shared-component-data.js
+++ b/lib/rules/no-shared-component-data.js
@@ -40,7 +40,7 @@ module.exports = {
     docs: {
       description: "enforce component's data property to be a function",
       category: 'essential',
-      url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.1/docs/rules/no-shared-component-data.md'
+      url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.2/docs/rules/no-shared-component-data.md'
     },
     fixable: 'code',
     schema: []
diff --git a/lib/rules/no-side-effects-in-computed-properties.js b/lib/rules/no-side-effects-in-computed-properties.js
index 2e588998..45872458 100644
--- a/lib/rules/no-side-effects-in-computed-properties.js
+++ b/lib/rules/no-side-effects-in-computed-properties.js
@@ -15,7 +15,7 @@ module.exports = {
     docs: {
       description: 'disallow side effects in computed properties',
       category: 'essential',
-      url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.1/docs/rules/no-side-effects-in-computed-properties.md'
+      url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.2/docs/rules/no-side-effects-in-computed-properties.md'
     },
     fixable: null,
     schema: []
diff --git a/lib/rules/no-template-key.js b/lib/rules/no-template-key.js
index f994f685..1eb8a452 100644
--- a/lib/rules/no-template-key.js
+++ b/lib/rules/no-template-key.js
@@ -20,7 +20,7 @@ module.exports = {
     docs: {
       description: 'disallow `key` attribute on `<template>`',
       category: 'essential',
-      url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.1/docs/rules/no-template-key.md'
+      url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.2/docs/rules/no-template-key.md'
     },
     fixable: null,
     schema: []
diff --git a/lib/rules/no-template-shadow.js b/lib/rules/no-template-shadow.js
index 6a476d67..6b860601 100644
--- a/lib/rules/no-template-shadow.js
+++ b/lib/rules/no-template-shadow.js
@@ -21,7 +21,7 @@ module.exports = {
     docs: {
       description: 'disallow variable declarations from shadowing variables declared in the outer scope',
       category: 'strongly-recommended',
-      url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.1/docs/rules/no-template-shadow.md'
+      url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.2/docs/rules/no-template-shadow.md'
     },
     fixable: null,
     schema: []
diff --git a/lib/rules/no-textarea-mustache.js b/lib/rules/no-textarea-mustache.js
index 1e4e1670..675c9e80 100644
--- a/lib/rules/no-textarea-mustache.js
+++ b/lib/rules/no-textarea-mustache.js
@@ -20,7 +20,7 @@ module.exports = {
     docs: {
       description: 'disallow mustaches in `<textarea>`',
       category: 'essential',
-      url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.1/docs/rules/no-textarea-mustache.md'
+      url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.2/docs/rules/no-textarea-mustache.md'
     },
     fixable: null,
     schema: []
diff --git a/lib/rules/no-unused-components.js b/lib/rules/no-unused-components.js
new file mode 100644
index 00000000..5719f0cd
--- /dev/null
+++ b/lib/rules/no-unused-components.js
@@ -0,0 +1,91 @@
+/**
+ * @fileoverview Report used components
+ * @author Michał Sajnóg
+ */
+'use strict'
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const utils = require('../utils')
+const casing = require('../utils/casing')
+
+// ------------------------------------------------------------------------------
+// Rule Definition
+// ------------------------------------------------------------------------------
+
+module.exports = {
+  meta: {
+    docs: {
+      description: 'disallow registering components that are not used inside templates',
+      category: 'essential',
+      url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.1/docs/rules/no-unused-components.md'
+    },
+    fixable: null,
+    schema: []
+  },
+
+  create (context) {
+    const usedComponents = []
+    let registeredComponents = []
+    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 (usedComponentName) {
+          usedComponents.push(usedComponentName)
+        }
+      },
+      "VElement[name='template']" (rootNode) {
+        templateLocation = templateLocation || rootNode.loc.start
+      },
+      "VElement[name='template']:exit" (rootNode) {
+        if (rootNode.loc.start !== templateLocation) return
+
+        registeredComponents
+          .filter(({ name }) => {
+            // If the component name is PascalCase
+            // it can be used in varoious of ways inside template,
+            // like "theComponent", "The-component" etc.
+            // but except snake_case
+            if (casing.pascalCase(name) === name) {
+              return !usedComponents.some(n => {
+                return n.indexOf('_') === -1 && name === casing.pascalCase(n)
+              })
+            } else {
+              // In any other case the used component name must exactly match
+              // the registered name
+              return usedComponents.indexOf(name) === -1
+            }
+          })
+          .forEach(({ node, name }) => context.report({
+            node,
+            message: 'The "{{name}}" component has been registered but not used.',
+            data: {
+              name
+            }
+          }))
+      }
+    }, utils.executeOnVue(context, (obj) => {
+      registeredComponents = utils.getRegisteredComponents(obj)
+    }))
+  }
+}
diff --git a/lib/rules/no-unused-vars.js b/lib/rules/no-unused-vars.js
index 2c0ab949..e7baf6c1 100644
--- a/lib/rules/no-unused-vars.js
+++ b/lib/rules/no-unused-vars.js
@@ -15,7 +15,7 @@ module.exports = {
     docs: {
       description: 'disallow unused variable definitions of v-for directives or scope attributes',
       category: 'essential',
-      url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.1/docs/rules/no-unused-vars.md'
+      url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.2/docs/rules/no-unused-vars.md'
     },
     fixable: null,
     schema: []
diff --git a/lib/rules/no-use-v-if-with-v-for.js b/lib/rules/no-use-v-if-with-v-for.js
index eedd9b2e..d884a070 100644
--- a/lib/rules/no-use-v-if-with-v-for.js
+++ b/lib/rules/no-use-v-if-with-v-for.js
@@ -52,7 +52,7 @@ module.exports = {
     docs: {
       description: 'disallow use v-if on the same element as v-for',
       category: 'essential',
-      url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.1/docs/rules/no-use-v-if-with-v-for.md'
+      url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.2/docs/rules/no-use-v-if-with-v-for.md'
     },
     fixable: null,
     schema: [{
diff --git a/lib/rules/no-v-html.js b/lib/rules/no-v-html.js
index ca5b5514..dcce8dcc 100644
--- a/lib/rules/no-v-html.js
+++ b/lib/rules/no-v-html.js
@@ -14,7 +14,7 @@ module.exports = {
     docs: {
       description: 'disallow use of v-html to prevent XSS attack',
       category: 'recommended',
-      url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.1/docs/rules/no-v-html.md'
+      url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.2/docs/rules/no-v-html.md'
     },
     fixable: null,
     schema: []
diff --git a/lib/rules/order-in-components.js b/lib/rules/order-in-components.js
index 8799dde2..545c55a9 100644
--- a/lib/rules/order-in-components.js
+++ b/lib/rules/order-in-components.js
@@ -135,7 +135,7 @@ module.exports = {
     docs: {
       description: 'enforce order of properties in components',
       category: 'recommended',
-      url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.1/docs/rules/order-in-components.md'
+      url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.2/docs/rules/order-in-components.md'
     },
     fixable: 'code', // null or "code" or "whitespace"
     schema: [
diff --git a/lib/rules/prop-name-casing.js b/lib/rules/prop-name-casing.js
index 53c6333b..bbae383a 100644
--- a/lib/rules/prop-name-casing.js
+++ b/lib/rules/prop-name-casing.js
@@ -91,7 +91,7 @@ module.exports = {
     docs: {
       description: 'enforce specific casing for the Prop name in Vue components',
       category: 'strongly-recommended',
-      url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.1/docs/rules/prop-name-casing.md'
+      url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.2/docs/rules/prop-name-casing.md'
     },
     fixable: 'code',  // null or "code" or "whitespace"
     schema: [
diff --git a/lib/rules/require-component-is.js b/lib/rules/require-component-is.js
index 5534c6c3..6a8baeda 100644
--- a/lib/rules/require-component-is.js
+++ b/lib/rules/require-component-is.js
@@ -20,7 +20,7 @@ module.exports = {
     docs: {
       description: 'require `v-bind:is` of `<component>` elements',
       category: 'essential',
-      url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.1/docs/rules/require-component-is.md'
+      url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.2/docs/rules/require-component-is.md'
     },
     fixable: null,
     schema: []
diff --git a/lib/rules/require-default-prop.js b/lib/rules/require-default-prop.js
index ac4cddb3..45d92759 100644
--- a/lib/rules/require-default-prop.js
+++ b/lib/rules/require-default-prop.js
@@ -15,7 +15,7 @@ module.exports = {
     docs: {
       description: 'require default value for props',
       category: 'strongly-recommended',
-      url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.1/docs/rules/require-default-prop.md'
+      url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.2/docs/rules/require-default-prop.md'
     },
     fixable: null, // or "code" or "whitespace"
     schema: []
diff --git a/lib/rules/require-prop-types.js b/lib/rules/require-prop-types.js
index 2ca5ffbc..11c7b6a5 100644
--- a/lib/rules/require-prop-types.js
+++ b/lib/rules/require-prop-types.js
@@ -15,7 +15,7 @@ module.exports = {
     docs: {
       description: 'require type definitions in props',
       category: 'strongly-recommended',
-      url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.1/docs/rules/require-prop-types.md'
+      url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.2/docs/rules/require-prop-types.md'
     },
     fixable: null, // or "code" or "whitespace"
     schema: [
diff --git a/lib/rules/require-render-return.js b/lib/rules/require-render-return.js
index 3c9b53e0..c8f1700f 100644
--- a/lib/rules/require-render-return.js
+++ b/lib/rules/require-render-return.js
@@ -15,7 +15,7 @@ module.exports = {
     docs: {
       description: 'enforce render function to always return value',
       category: 'essential',
-      url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.1/docs/rules/require-render-return.md'
+      url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.2/docs/rules/require-render-return.md'
     },
     fixable: null, // or "code" or "whitespace"
     schema: []
diff --git a/lib/rules/require-v-for-key.js b/lib/rules/require-v-for-key.js
index c0aa21ce..e379c37c 100644
--- a/lib/rules/require-v-for-key.js
+++ b/lib/rules/require-v-for-key.js
@@ -20,7 +20,7 @@ module.exports = {
     docs: {
       description: 'require `v-bind:key` with `v-for` directives',
       category: 'essential',
-      url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.1/docs/rules/require-v-for-key.md'
+      url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.2/docs/rules/require-v-for-key.md'
     },
     fixable: null,
     schema: []
diff --git a/lib/rules/require-valid-default-prop.js b/lib/rules/require-valid-default-prop.js
index 95831fa6..1888c522 100644
--- a/lib/rules/require-valid-default-prop.js
+++ b/lib/rules/require-valid-default-prop.js
@@ -24,7 +24,7 @@ module.exports = {
     docs: {
       description: 'enforce props default values to be valid',
       category: 'essential',
-      url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.1/docs/rules/require-valid-default-prop.md'
+      url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.2/docs/rules/require-valid-default-prop.md'
     },
     fixable: null,
     schema: []
diff --git a/lib/rules/return-in-computed-property.js b/lib/rules/return-in-computed-property.js
index f9deeb85..9c2a0aa5 100644
--- a/lib/rules/return-in-computed-property.js
+++ b/lib/rules/return-in-computed-property.js
@@ -15,7 +15,7 @@ module.exports = {
     docs: {
       description: 'enforce that a return statement is present in computed property',
       category: 'essential',
-      url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.1/docs/rules/return-in-computed-property.md'
+      url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.2/docs/rules/return-in-computed-property.md'
     },
     fixable: null, // or "code" or "whitespace"
     schema: [
diff --git a/lib/rules/script-indent.js b/lib/rules/script-indent.js
index 7eb1a809..0b03f80a 100644
--- a/lib/rules/script-indent.js
+++ b/lib/rules/script-indent.js
@@ -19,7 +19,7 @@ module.exports = {
     docs: {
       description: 'enforce consistent indentation in `<script>`',
       category: undefined,
-      url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.1/docs/rules/script-indent.md'
+      url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.2/docs/rules/script-indent.md'
     },
     fixable: 'whitespace',
     schema: [
diff --git a/lib/rules/this-in-template.js b/lib/rules/this-in-template.js
index 5ae6947e..3657b178 100644
--- a/lib/rules/this-in-template.js
+++ b/lib/rules/this-in-template.js
@@ -20,7 +20,7 @@ module.exports = {
     docs: {
       description: 'enforce usage of `this` in template',
       category: 'recommended',
-      url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.1/docs/rules/this-in-template.md'
+      url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.2/docs/rules/this-in-template.md'
     },
     fixable: null,
     schema: [
diff --git a/lib/rules/v-bind-style.js b/lib/rules/v-bind-style.js
index 3a5b2161..fdee0485 100644
--- a/lib/rules/v-bind-style.js
+++ b/lib/rules/v-bind-style.js
@@ -20,7 +20,7 @@ module.exports = {
     docs: {
       description: 'enforce `v-bind` directive style',
       category: 'strongly-recommended',
-      url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.1/docs/rules/v-bind-style.md'
+      url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.2/docs/rules/v-bind-style.md'
     },
     fixable: 'code',
     schema: [
diff --git a/lib/rules/v-on-style.js b/lib/rules/v-on-style.js
index 3f0882f0..b0081be5 100644
--- a/lib/rules/v-on-style.js
+++ b/lib/rules/v-on-style.js
@@ -20,7 +20,7 @@ module.exports = {
     docs: {
       description: 'enforce `v-on` directive style',
       category: 'strongly-recommended',
-      url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.1/docs/rules/v-on-style.md'
+      url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.2/docs/rules/v-on-style.md'
     },
     fixable: 'code',
     schema: [
diff --git a/lib/rules/valid-template-root.js b/lib/rules/valid-template-root.js
index 10e44939..a2452aac 100644
--- a/lib/rules/valid-template-root.js
+++ b/lib/rules/valid-template-root.js
@@ -20,7 +20,7 @@ module.exports = {
     docs: {
       description: 'enforce valid template root',
       category: 'essential',
-      url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.1/docs/rules/valid-template-root.md'
+      url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.2/docs/rules/valid-template-root.md'
     },
     fixable: null,
     schema: []
diff --git a/lib/rules/valid-v-bind.js b/lib/rules/valid-v-bind.js
index e12ace7b..45f4a3d9 100644
--- a/lib/rules/valid-v-bind.js
+++ b/lib/rules/valid-v-bind.js
@@ -26,7 +26,7 @@ module.exports = {
     docs: {
       description: 'enforce valid `v-bind` directives',
       category: 'essential',
-      url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.1/docs/rules/valid-v-bind.md'
+      url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.2/docs/rules/valid-v-bind.md'
     },
     fixable: null,
     schema: []
diff --git a/lib/rules/valid-v-cloak.js b/lib/rules/valid-v-cloak.js
index e35158b2..493601bb 100644
--- a/lib/rules/valid-v-cloak.js
+++ b/lib/rules/valid-v-cloak.js
@@ -20,7 +20,7 @@ module.exports = {
     docs: {
       description: 'enforce valid `v-cloak` directives',
       category: 'essential',
-      url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.1/docs/rules/valid-v-cloak.md'
+      url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.2/docs/rules/valid-v-cloak.md'
     },
     fixable: null,
     schema: []
diff --git a/lib/rules/valid-v-else-if.js b/lib/rules/valid-v-else-if.js
index 0c5cc895..42f84001 100644
--- a/lib/rules/valid-v-else-if.js
+++ b/lib/rules/valid-v-else-if.js
@@ -20,7 +20,7 @@ module.exports = {
     docs: {
       description: 'enforce valid `v-else-if` directives',
       category: 'essential',
-      url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.1/docs/rules/valid-v-else-if.md'
+      url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.2/docs/rules/valid-v-else-if.md'
     },
     fixable: null,
     schema: []
diff --git a/lib/rules/valid-v-else.js b/lib/rules/valid-v-else.js
index 1faa2fe1..ff454832 100644
--- a/lib/rules/valid-v-else.js
+++ b/lib/rules/valid-v-else.js
@@ -20,7 +20,7 @@ module.exports = {
     docs: {
       description: 'enforce valid `v-else` directives',
       category: 'essential',
-      url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.1/docs/rules/valid-v-else.md'
+      url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.2/docs/rules/valid-v-else.md'
     },
     fixable: null,
     schema: []
diff --git a/lib/rules/valid-v-for.js b/lib/rules/valid-v-for.js
index 9f163147..f4084760 100644
--- a/lib/rules/valid-v-for.js
+++ b/lib/rules/valid-v-for.js
@@ -106,7 +106,7 @@ module.exports = {
     docs: {
       description: 'enforce valid `v-for` directives',
       category: 'essential',
-      url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.1/docs/rules/valid-v-for.md'
+      url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.2/docs/rules/valid-v-for.md'
     },
     fixable: null,
     schema: []
diff --git a/lib/rules/valid-v-html.js b/lib/rules/valid-v-html.js
index 40f5e003..d57db6be 100644
--- a/lib/rules/valid-v-html.js
+++ b/lib/rules/valid-v-html.js
@@ -20,7 +20,7 @@ module.exports = {
     docs: {
       description: 'enforce valid `v-html` directives',
       category: 'essential',
-      url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.1/docs/rules/valid-v-html.md'
+      url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.2/docs/rules/valid-v-html.md'
     },
     fixable: null,
     schema: []
diff --git a/lib/rules/valid-v-if.js b/lib/rules/valid-v-if.js
index f2768f50..adc1516c 100644
--- a/lib/rules/valid-v-if.js
+++ b/lib/rules/valid-v-if.js
@@ -20,7 +20,7 @@ module.exports = {
     docs: {
       description: 'enforce valid `v-if` directives',
       category: 'essential',
-      url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.1/docs/rules/valid-v-if.md'
+      url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.2/docs/rules/valid-v-if.md'
     },
     fixable: null,
     schema: []
diff --git a/lib/rules/valid-v-model.js b/lib/rules/valid-v-model.js
index 2ff430a7..57b4deb9 100644
--- a/lib/rules/valid-v-model.js
+++ b/lib/rules/valid-v-model.js
@@ -82,7 +82,7 @@ module.exports = {
     docs: {
       description: 'enforce valid `v-model` directives',
       category: 'essential',
-      url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.1/docs/rules/valid-v-model.md'
+      url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.2/docs/rules/valid-v-model.md'
     },
     fixable: null,
     schema: []
diff --git a/lib/rules/valid-v-on.js b/lib/rules/valid-v-on.js
index 29f3abd1..3a15d408 100644
--- a/lib/rules/valid-v-on.js
+++ b/lib/rules/valid-v-on.js
@@ -115,7 +115,7 @@ module.exports = {
     docs: {
       description: 'enforce valid `v-on` directives',
       category: 'essential',
-      url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.1/docs/rules/valid-v-on.md'
+      url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.2/docs/rules/valid-v-on.md'
     },
     fixable: null,
     schema: []
diff --git a/lib/rules/valid-v-once.js b/lib/rules/valid-v-once.js
index e54578ef..1db6cb14 100644
--- a/lib/rules/valid-v-once.js
+++ b/lib/rules/valid-v-once.js
@@ -20,7 +20,7 @@ module.exports = {
     docs: {
       description: 'enforce valid `v-once` directives',
       category: 'essential',
-      url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.1/docs/rules/valid-v-once.md'
+      url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.2/docs/rules/valid-v-once.md'
     },
     fixable: null,
     schema: []
diff --git a/lib/rules/valid-v-pre.js b/lib/rules/valid-v-pre.js
index 028870ce..f1ef92cc 100644
--- a/lib/rules/valid-v-pre.js
+++ b/lib/rules/valid-v-pre.js
@@ -20,7 +20,7 @@ module.exports = {
     docs: {
       description: 'enforce valid `v-pre` directives',
       category: 'essential',
-      url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.1/docs/rules/valid-v-pre.md'
+      url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.2/docs/rules/valid-v-pre.md'
     },
     fixable: null,
     schema: []
diff --git a/lib/rules/valid-v-show.js b/lib/rules/valid-v-show.js
index 174ee303..e303c201 100644
--- a/lib/rules/valid-v-show.js
+++ b/lib/rules/valid-v-show.js
@@ -20,7 +20,7 @@ module.exports = {
     docs: {
       description: 'enforce valid `v-show` directives',
       category: 'essential',
-      url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.1/docs/rules/valid-v-show.md'
+      url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.2/docs/rules/valid-v-show.md'
     },
     fixable: null,
     schema: []
diff --git a/lib/rules/valid-v-text.js b/lib/rules/valid-v-text.js
index 9dd22dea..c92843c9 100644
--- a/lib/rules/valid-v-text.js
+++ b/lib/rules/valid-v-text.js
@@ -20,7 +20,7 @@ module.exports = {
     docs: {
       description: 'enforce valid `v-text` directives',
       category: 'essential',
-      url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.1/docs/rules/valid-v-text.md'
+      url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.2/docs/rules/valid-v-text.md'
     },
     fixable: null,
     schema: []
diff --git a/lib/utils/casing.js b/lib/utils/casing.js
index db73a5a2..f0c02b01 100644
--- a/lib/utils/casing.js
+++ b/lib/utils/casing.js
@@ -75,5 +75,10 @@ module.exports = {
     assert(typeof name === 'string')
 
     return convertersMap[name] || pascalCase
-  }
+  },
+
+  camelCase,
+  pascalCase,
+  kebabCase,
+  snakeCase
 }
diff --git a/lib/utils/index.js b/lib/utils/index.js
index e0279636..b39eeba0 100644
--- a/lib/utils/index.js
+++ b/lib/utils/index.js
@@ -76,40 +76,64 @@ module.exports = {
   },
 
   /**
-   * Check whether the given start tag has specific directive.
+   * Finds attribute in the given start tag
    * @param {ASTNode} node The start tag node to check.
    * @param {string} name The attribute name to check.
    * @param {string} [value] The attribute value to check.
-   * @returns {boolean} `true` if the start tag has the directive.
+   * @returns {ASTNode} attribute node
    */
-  hasAttribute (node, name, value) {
+  findAttribute (node, name, value) {
     assert(node && node.type === 'VElement')
-    return node.startTag.attributes.some(a =>
-      !a.directive &&
-      a.key.name === name &&
+    return node.startTag.attributes.find(attr => (
+      !attr.directive &&
+      attr.key.name === name &&
       (
         value === undefined ||
-        (a.value != null && a.value.value === value)
+        (attr.value != null && attr.value.value === value)
       )
-    )
+    ))
   },
 
   /**
    * Check whether the given start tag has specific directive.
    * @param {ASTNode} node The start tag node to check.
+   * @param {string} name The attribute name to check.
+   * @param {string} [value] The attribute value to check.
+   * @returns {boolean} `true` if the start tag has the attribute.
+   */
+  hasAttribute (node, name, value) {
+    assert(node && node.type === 'VElement')
+    return Boolean(this.findAttribute(node, name, value))
+  },
+
+  /**
+   * Finds directive in the given start tag
+   * @param {ASTNode} node The start tag node to check.
    * @param {string} name The directive name to check.
    * @param {string} [argument] The directive argument to check.
-   * @returns {boolean} `true` if the start tag has the directive.
+   * @returns {ASTNode} directive node
    */
-  hasDirective (node, name, argument) {
+  findDirective (node, name, argument) {
     assert(node && node.type === 'VElement')
-    return node.startTag.attributes.some(a =>
+    return node.startTag.attributes.find(a =>
       a.directive &&
       a.key.name === name &&
       (argument === undefined || a.key.argument === argument)
     )
   },
 
+  /**
+   * Check whether the given start tag has specific directive.
+   * @param {ASTNode} node The start tag node to check.
+   * @param {string} name The directive name to check.
+   * @param {string} [argument] The directive argument to check.
+   * @returns {boolean} `true` if the start tag has the directive.
+   */
+  hasDirective (node, name, argument) {
+    assert(node && node.type === 'VElement')
+    return Boolean(this.findDirective(node, name, argument))
+  },
+
   /**
    * Check whether the given attribute has their attribute value.
    * @param {ASTNode} node The attribute node to check.
@@ -158,6 +182,30 @@ module.exports = {
     )
   },
 
+  /**
+   * Returns the list of all registered components
+   * @param {ASTNode} componentObject
+   * @returns {Array} Array of ASTNodes
+   */
+  getRegisteredComponents (componentObject) {
+    const componentsNode = componentObject.properties
+      .find(p =>
+        p.type === 'Property' &&
+        p.key.type === 'Identifier' &&
+        p.key.name === 'components' &&
+        p.value.type === 'ObjectExpression'
+      )
+
+    if (!componentsNode) { return [] }
+
+    return componentsNode.value.properties
+      .filter(p => p.type === 'Property')
+      .map(node => ({
+        node,
+        name: this.getStaticPropertyName(node.key)
+      }))
+  },
+
   /**
    * Check whether the previous sibling element has `if` or `else-if` directive.
    * @param {ASTNode} node The element node to check.
diff --git a/package.json b/package.json
index fc5cc094..e89a3614 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "eslint-plugin-vue",
-  "version": "5.0.0-beta.1",
+  "version": "5.0.0-beta.2",
   "description": "Official ESLint plugin for Vue.js",
   "main": "lib/index.js",
   "scripts": {


Test patch:
diff --git a/tests/lib/rules/no-unused-components.js b/tests/lib/rules/no-unused-components.js
new file mode 100644
index 00000000..f412b3ec
--- /dev/null
+++ b/tests/lib/rules/no-unused-components.js
@@ -0,0 +1,369 @@
+/**
+ * @fileoverview Report used components
+ * @author Michał Sajnóg
+ */
+'use strict'
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const RuleTester = require('eslint').RuleTester
+const rule = require('../../../lib/rules/no-unused-components')
+
+// ------------------------------------------------------------------------------
+// Tests
+// ------------------------------------------------------------------------------
+
+const tester = new RuleTester({
+  parser: 'vue-eslint-parser',
+  parserOptions: {
+    ecmaVersion: 2018,
+    sourceType: 'module'
+  }
+})
+
+tester.run('no-unused-components', rule, {
+  valid: [
+    {
+      filename: 'test.vue',
+      code: `<template><div>Lorem ipsum</div></template>`
+    },
+    {
+      filename: 'test.vue',
+      code: `<template>
+        <div>
+          <h2>Lorem ipsum</h2>
+        </div>
+      </template>`
+    },
+    {
+      filename: 'test.vue',
+      code: `<template>
+        <div>
+          <TheButton />
+        </div>
+      </template>
+      <script>
+        export default {
+          components: {
+            TheButton
+          }
+        }
+      </script>`
+    },
+    {
+      filename: 'test.vue',
+      code: `<template>
+        <div>
+          <component :is="'TheButton'" />
+        </div>
+      </template>
+      <script>
+        export default {
+          components: {
+            TheButton
+          }
+        }
+      </script>`
+    },
+    {
+      filename: 'test.vue',
+      code: `<template>
+        <div>
+          <component is="TheButton" />
+        </div>
+      </template>
+      <script>
+        export default {
+          components: {
+            TheButton
+          }
+        }
+      </script>`
+    },
+    {
+      filename: 'test.vue',
+      code: `<template>
+        <div>
+          <theButton />
+        </div>
+      </template>
+      <script>
+        export default {
+          components: {
+            TheButton
+          }
+        }
+      </script>`
+    },
+    {
+      filename: 'test.vue',
+      code: `<template>
+        <div>
+          <component is="theButton" />
+        </div>
+      </template>
+      <script>
+        export default {
+          components: {
+            TheButton
+          }
+        }
+      </script>`
+    },
+    {
+      filename: 'test.vue',
+      code: `<template>
+        <div>
+          <the-button />
+        </div>
+      </template>
+      <script>
+        export default {
+          components: {
+            TheButton
+          }
+        }
+      </script>`
+    },
+    {
+      filename: 'test.vue',
+      code: `<template>
+        <div>
+          <component is="the-button" />
+        </div>
+      </template>
+      <script>
+        export default {
+          components: {
+            TheButton
+          }
+        }
+      </script>`
+    },
+    {
+      filename: 'test.vue',
+      code: `<template>
+        <div>
+          <The-button />
+        </div>
+      </template>
+      <script>
+        export default {
+          components: {
+            TheButton
+          }
+        }
+      </script>`
+    },
+    {
+      filename: 'test.vue',
+      code: `<template>
+        <div>
+          <component is="The-button" />
+        </div>
+      </template>
+      <script>
+        export default {
+          components: {
+            TheButton
+          }
+        }
+      </script>`
+    },
+    {
+      filename: 'test.vue',
+      code: `<template>
+        <div>
+          <The-Button />
+        </div>
+      </template>
+      <script>
+        export default {
+          components: {
+            TheButton
+          }
+        }
+      </script>`
+    },
+    {
+      filename: 'test.vue',
+      code: `<template>
+        <div>
+          <component is="The-Button" />
+        </div>
+      </template>
+      <script>
+        export default {
+          components: {
+            TheButton
+          }
+        }
+      </script>`
+    },
+    {
+      filename: 'test.vue',
+      code: `<template>
+        <div>
+          <the-Button />
+        </div>
+      </template>
+      <script>
+        export default {
+          components: {
+            TheButton
+          }
+        }
+      </script>`
+    },
+    {
+      filename: 'test.vue',
+      code: `<template>
+        <div>
+          <component is="the-Button" />
+        </div>
+      </template>
+      <script>
+        export default {
+          components: {
+            TheButton
+          }
+        }
+      </script>`
+    },
+    {
+      filename: 'test.vue',
+      code: `<template>
+        <div>
+          <the-button />
+          <next_Button />
+        </div>
+      </template>
+      <script>
+        export default {
+          components: {
+            'the-button': TheButton,
+            'next_Button': NextButton
+          }
+        }
+      </script>`
+    },
+    {
+      filename: 'test.vue',
+      code: `<template>
+        <div>
+          <component is="the-button" />
+          <component is="next_Button" />
+        </div>
+      </template>
+      <script>
+        export default {
+          components: {
+            'the-button': TheButton,
+            'next_Button': NextButton
+          }
+        }
+      </script>`
+    },
+    {
+      filename: 'test.vue',
+      code: `<template>
+        <div>
+          <h2>Lorem ipsum</h2>
+          <component is="TheButton" />
+        </div>
+      </template>
+      <script>
+        export default {
+          components: {
+            TheButton
+          }
+        }
+      </script>`
+    },
+
+    // Ignore when `render` is used instead of temoplate
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+        export default {
+          components: {
+            TheButton
+          },
+          render() {
+            return
+          }
+        }
+      </script>`
+    }
+  ],
+  invalid: [
+    {
+      filename: 'test.vue',
+      code: `
+        <template>
+          <div>
+            <h2>Lorem ipsum</h2>
+          </div>
+        </template>
+        <script>
+          export default {
+            components: {
+              TheButton
+            },
+          }
+        </script>
+      `,
+      errors: [{
+        message: 'The "TheButton" component has been registered but not used.',
+        line: 10
+      }]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <template>
+          <div>
+            <h2>Lorem ipsum</h2>
+            <the_button />
+          </div>
+        </template>
+        <script>
+          export default {
+            components: {
+              TheButton
+            },
+          }
+        </script>
+      `,
+      errors: [{
+        message: 'The "TheButton" component has been registered but not used.',
+        line: 11
+      }]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <template>
+          <div>
+            <h2>Lorem ipsum</h2>
+            <TheButton />
+          </div>
+        </template>
+        <script>
+          export default {
+            components: {
+              'the-button': TheButton
+            },
+          }
+        </script>
+      `,
+      errors: [{
+        message: 'The "the-button" component has been registered but not used.',
+        line: 11
+      }]
+    }
+  ]
+})
diff --git a/tests/lib/utils/index.js b/tests/lib/utils/index.js
index 09e39ef2..067ddc59 100644
--- a/tests/lib/utils/index.js
+++ b/tests/lib/utils/index.js
@@ -208,3 +208,42 @@ describe('parseMemberOrCallExpression', () => {
     assert.equal(parsed, 'this.lorem[][].map().dolor.reduce().sit')
   })
 })
+
+describe('getRegisteredComponents', () => {
+  let node
+
+  const parse = function (code) {
+    return babelEslint.parse(code).body[0].declarations[0].init
+  }
+
+  it('should return empty array when there are no components registered', () => {
+    node = parse(`const test = {
+      name: 'test',
+    }`)
+
+    assert.equal(
+      utils.getRegisteredComponents(node).length,
+      0
+    )
+  })
+
+  it('should return an array with all registered components', () => {
+    node = parse(`const test = {
+      name: 'test',
+      components: {
+        ...test,
+        PrimaryButton,
+        secondaryButton,
+        'the-modal': TheModal,
+        the_dropdown: TheDropdown,
+        the_input,
+        ['SomeComponent']: SomeComponent
+      }
+    }`)
+
+    assert.deepEqual(
+      utils.getRegisteredComponents(node).map(c => c.name),
+      ['PrimaryButton', 'secondaryButton', 'the-modal', 'the_dropdown', 'the_input', 'SomeComponent'],
+    )
+  })
+})