Repository: vuejs/eslint-plugin-vue
Pull request #546 (2018-08-03 17:33:57)

(implements #323) Add `require-prop-type-constructor` rule
This PR implements rule proposed in #323.

Unfortunately we can't detect all possible scenarios, hence I focused on those that are trivial and should catch most commonly made mistakes. We still allow CallExpressions and MemberExpressions, as we cannot assume those will not represent or return constructor implicitly.


Patch:
diff --git a/README.md b/README.md
index 7870ed23..7c6d387e 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 |
@@ -230,6 +231,7 @@ Enforce all the rules in this category, as well as all higher priority rules, wi
 |    | Rule ID | Description |
 |:---|:--------|:------------|
 | :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/require-prop-type-constructor](./docs/rules/require-prop-type-constructor.md) | require prop type to be a constructor |
 | :wrench: | [vue/script-indent](./docs/rules/script-indent.md) | enforce consistent indentation in `<script>` |
 
 ### Deprecated
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/docs/rules/require-prop-type-constructor.md b/docs/rules/require-prop-type-constructor.md
new file mode 100644
index 00000000..f5a5f3ba
--- /dev/null
+++ b/docs/rules/require-prop-type-constructor.md
@@ -0,0 +1,60 @@
+# require prop type to be a constructor (vue/require-prop-type-constructor)
+
+- :wrench: The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) can automatically fix some of the problems reported by this rule.
+
+This rule reports prop types that can't be presumed as constructors.
+
+It's impossible to catch every possible case and know whether the prop type is a constructor or not, hence this rule black list few types of nodes, instead of white-listing correct ones.
+
+The following types are forbidden and will be reported:
+
+- Literal
+- TemplateLiteral
+- BinaryExpression
+- UpdateExpression
+
+It will catch most commonly made mistakes which are using strings instead of constructors.
+
+## Rule Details
+
+Examples of **incorrect** code for this rule:
+
+```js
+export default {
+  props: {
+    myProp: "Number",
+    anotherProp: ["Number", "String"],
+    myFieldWithBadType: {
+      type: "Object",
+      default: function() {
+        return {}
+      },
+    },
+    myOtherFieldWithBadType: {
+      type: "Number",
+      default: 1,
+    },
+  }
+}
+```
+
+Examples of **correct** code for this rule:
+
+```js
+export default {
+  props: {
+    myProp: Number,
+    anotherProp: [Number, String],
+    myFieldWithBadType: {
+      type: Object,
+      default: function() {
+        return {}
+      },
+    },
+    myOtherFieldWithBadType: {
+      type: Number,
+      default: 1,
+    },
+  }
+}
+```
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..1cd8d59f 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'),
@@ -40,6 +41,7 @@ module.exports = {
     'prop-name-casing': require('./rules/prop-name-casing'),
     'require-component-is': require('./rules/require-component-is'),
     'require-default-prop': require('./rules/require-default-prop'),
+    'require-prop-type-constructor': require('./rules/require-prop-type-constructor'),
     'require-prop-types': require('./rules/require-prop-types'),
     'require-render-return': require('./rules/require-render-return'),
     'require-v-for-key': require('./rules/require-v-for-key'),
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-type-constructor.js b/lib/rules/require-prop-type-constructor.js
new file mode 100644
index 00000000..ce2a2da1
--- /dev/null
+++ b/lib/rules/require-prop-type-constructor.js
@@ -0,0 +1,98 @@
+/**
+ * @fileoverview require prop type to be a constructor
+ * @author Michał Sajnóg
+ */
+'use strict'
+
+const utils = require('../utils')
+
+// ------------------------------------------------------------------------------
+// Rule Definition
+// ------------------------------------------------------------------------------
+
+const message = 'The "{{name}}" property should be a constructor.'
+
+const forbiddenTypes = [
+  'Literal',
+  'TemplateLiteral',
+  'BinaryExpression',
+  'UpdateExpression'
+]
+
+const isForbiddenType = nodeType => forbiddenTypes.indexOf(nodeType) > -1
+
+module.exports = {
+  meta: {
+    docs: {
+      description: 'require prop type to be a constructor',
+      category: undefined, // essential
+      url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.1/docs/rules/require-prop-type-constructor.md'
+    },
+    fixable: 'code',  // or "code" or "whitespace"
+    schema: []
+  },
+
+  create (context) {
+    const fix = node => fixer => {
+      if (node.type === 'Literal') {
+        return fixer.replaceText(node, node.value)
+      } else if (
+        node.type === 'TemplateLiteral' &&
+        node.expressions.length === 0 &&
+        node.quasis.length === 1
+      ) {
+        return fixer.replaceText(node, node.quasis[0].value.cooked)
+      }
+    }
+
+    const checkPropertyNode = (p) => {
+      if (isForbiddenType(p.value.type)) {
+        context.report({
+          node: p.value,
+          message,
+          data: {
+            name: utils.getStaticPropertyName(p.key)
+          },
+          fix: fix(p.value)
+        })
+      } else if (p.value.type === 'ArrayExpression') {
+        p.value.elements
+          .filter(prop => isForbiddenType(prop.type))
+          .forEach(prop => context.report({
+            node: prop,
+            message,
+            data: {
+              name: utils.getStaticPropertyName(p.key)
+            },
+            fix: fix(prop)
+          }))
+      }
+    }
+
+    return utils.executeOnVueComponent(context, (obj) => {
+      const node = obj.properties.find(p =>
+        p.type === 'Property' &&
+        p.key.type === 'Identifier' &&
+        p.key.name === 'props' &&
+        p.value.type === 'ObjectExpression'
+      )
+
+      if (!node) return
+
+      node.value.properties.forEach(p => {
+        if (isForbiddenType(p.value.type) || p.value.type === 'ArrayExpression') {
+          checkPropertyNode(p)
+        } else if (p.value.type === 'ObjectExpression') {
+          const typeProperty = p.value.properties.find(prop =>
+            prop.type === 'Property' &&
+            prop.key.name === 'type'
+          )
+
+          if (!typeProperty) return
+
+          checkPropertyNode(typeProperty)
+        }
+      })
+    })
+  }
+}
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/rules/require-prop-type-constructor.js b/tests/lib/rules/require-prop-type-constructor.js
new file mode 100644
index 00000000..f0740385
--- /dev/null
+++ b/tests/lib/rules/require-prop-type-constructor.js
@@ -0,0 +1,134 @@
+/**
+ * @fileoverview require prop type to be a constructor
+ * @author Michał Sajnóg
+ */
+'use strict'
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const rule = require('../../../lib/rules/require-prop-type-constructor')
+const RuleTester = require('eslint').RuleTester
+
+// ------------------------------------------------------------------------------
+// Tests
+// ------------------------------------------------------------------------------
+
+var ruleTester = new RuleTester({
+  parserOptions: {
+    ecmaVersion: 7,
+    sourceType: 'module'
+  }
+})
+ruleTester.run('require-prop-type-constructor', rule, {
+
+  valid: [
+    {
+      filename: 'SomeComponent.vue',
+      code: `
+        export default {
+          props: {
+            myProp: Number,
+            anotherType: [Number, String],
+            extraProp: {
+              type: Number,
+              default: 10
+            },
+            lastProp: {
+              type: [Number, Boolean]
+            }
+          }
+        }
+      `
+    }
+  ],
+
+  invalid: [
+    {
+      filename: 'SomeComponent.vue',
+      code: `
+      export default {
+        props: {
+          myProp: 'Number',
+          anotherType: ['Number', 'String'],
+          extraProp: {
+            type: 'Number',
+            default: 10
+          },
+          lastProp: {
+            type: ['Boolean']
+          }
+        }
+      }
+      `,
+      output: `
+      export default {
+        props: {
+          myProp: Number,
+          anotherType: [Number, String],
+          extraProp: {
+            type: Number,
+            default: 10
+          },
+          lastProp: {
+            type: [Boolean]
+          }
+        }
+      }
+      `,
+      errors: [{
+        message: 'The "myProp" property should be a constructor.',
+        line: 4
+      }, {
+        message: 'The "anotherType" property should be a constructor.',
+        line: 5
+      }, {
+        message: 'The "anotherType" property should be a constructor.',
+        line: 5
+      }, {
+        message: 'The "type" property should be a constructor.',
+        line: 7
+      }, {
+        message: 'The "type" property should be a constructor.',
+        line: 11
+      }]
+    },
+    {
+      filename: 'SomeComponent.vue',
+      code: `
+      export default {
+        props: {
+          a: \`String\`,
+          b: Foo + '',
+          c: 1,
+          d: true,
+        }
+      }
+      `,
+      output: `
+      export default {
+        props: {
+          a: String,
+          b: Foo + '',
+          c: 1,
+          d: true,
+        }
+      }
+      `,
+      errors: [{
+        message: 'The "a" property should be a constructor.',
+        line: 4
+      }, {
+        message: 'The "b" property should be a constructor.',
+        line: 5
+      }, {
+        message: 'The "c" property should be a constructor.',
+        line: 6
+      }, {
+        message: 'The "d" property should be a constructor.',
+        line: 7
+      }]
+    }
+  ]
+})
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'],
+    )
+  })
+})