Repository: vuejs/eslint-plugin-vue
Pull request #552 (2018-08-11 08:20:40)

[New] Add `vue/singleline-html-element-content-newline` rule
This PR adds `vue/singleline-html-element-content-newline` rule.
This implements rule proposed in https://github.com/vuejs/eslint-plugin-vue/pull/547#issuecomment-410517455

It is related to #415.


Patch:
diff --git a/README.md b/README.md
index 7870ed23..4ad4a204 100644
--- a/README.md
+++ b/README.md
@@ -156,9 +156,11 @@ 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 registering components that are not used inside templates |
 |  | [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 |
+| :wrench: | [vue/require-prop-type-constructor](./docs/rules/require-prop-type-constructor.md) | require prop type to be a constructor |
 |  | [vue/require-render-return](./docs/rules/require-render-return.md) | enforce render function to always return value |
 |  | [vue/require-v-for-key](./docs/rules/require-v-for-key.md) | require `v-bind:key` with `v-for` directives |
 |  | [vue/require-valid-default-prop](./docs/rules/require-valid-default-prop.md) | enforce props default values to be valid |
@@ -230,7 +232,10 @@ 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/multiline-html-element-content-newline](./docs/rules/multiline-html-element-content-newline.md) | require a line break before and after the contents of a multiline element |
+| :wrench: | [vue/no-spaces-around-equal-signs-in-attribute](./docs/rules/no-spaces-around-equal-signs-in-attribute.md) | disallow spaces around equal signs in attribute |
 | :wrench: | [vue/script-indent](./docs/rules/script-indent.md) | enforce consistent indentation in `<script>` |
+| :wrench: | [vue/singleline-html-element-content-newline](./docs/rules/singleline-html-element-content-newline.md) | require a line break before and after the contents of a singleline element |
 
 ### Deprecated
 
diff --git a/docs/rules/multiline-html-element-content-newline.md b/docs/rules/multiline-html-element-content-newline.md
new file mode 100644
index 00000000..51be5b0f
--- /dev/null
+++ b/docs/rules/multiline-html-element-content-newline.md
@@ -0,0 +1,87 @@
+# require a line break before and after the contents of a multiline element (vue/multiline-html-element-content-newline)
+
+- :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.
+
+## :book: Rule Details
+
+This rule enforces a line break before and after the contents of a multiline element.
+
+
+:-1: Examples of **incorrect** code:
+
+```html
+<div>multiline
+  content</div>
+
+<div
+  attr
+>multiline start tag</div>
+
+<tr><td>multiline</td>
+  <td>children</td></tr>
+
+<div><!-- multiline
+  comment --></div>
+
+<div
+></div>
+```
+
+:+1: Examples of **correct** code:
+
+```html
+<div>
+  multiline
+  content
+</div>
+
+<div
+  attr
+>
+  multiline start tag
+</div>
+
+<tr>
+  <td>multiline</td>
+  <td>children</td>
+</tr>
+
+<div>
+  <!-- multiline
+       comment -->
+</div>
+
+<div
+>
+</div>
+
+<div attr>singleline element</div>
+```
+
+
+## :wrench: Options
+
+```json
+{
+    "vue/multiline-html-element-content-newline": ["error", {
+        "ignores": ["pre", "textarea"]
+    }]
+}
+```
+
+- `ignores` ... the configuration for element names to ignore line breaks style.  
+    default `["pre", "textarea"]`
+
+
+:+1: Examples of **correct** code:
+
+```html
+/* eslint vue/multiline-html-element-content-newline: ["error", { "ignores": ["VueComponent", "pre", "textarea"]}] */
+
+<VueComponent>multiline
+content</VueComponent>
+
+<VueComponent><span
+  class="bold">For example,</span>
+Defines the Vue component that accepts preformatted text.</VueComponent>
+```
diff --git a/docs/rules/no-spaces-around-equal-signs-in-attribute.md b/docs/rules/no-spaces-around-equal-signs-in-attribute.md
new file mode 100644
index 00000000..7ab72b26
--- /dev/null
+++ b/docs/rules/no-spaces-around-equal-signs-in-attribute.md
@@ -0,0 +1,25 @@
+# disallow spaces around equal signs in attribute (vue/no-spaces-around-equal-signs-in-attribute)
+
+- :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 disallow spaces around equal signs in attribute.
+
+HTML5 allows spaces around equal signs. But space-less is easier to read, and groups entities better together.
+
+## :book: Rule Details
+
+:-1: Examples of **incorrect** code for this rule:
+
+```html
+<div class = "item">
+```
+
+:+1: Examples of **correct** code for this rule:
+
+```html
+<div class="item">
+```
+
+## Further Reading
+
+* [HTML5 Style Guide - W3Schools *Spaces and Equal Signs*](https://www.w3schools.com/html/html5_syntax.asp)
\ No newline at end of file
diff --git a/docs/rules/no-unused-components.md b/docs/rules/no-unused-components.md
new file mode 100644
index 00000000..7220df0c
--- /dev/null
+++ b/docs/rules/no-unused-components.md
@@ -0,0 +1,67 @@
+# disallow registering components that are not used inside templates (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..b6835060
--- /dev/null
+++ b/docs/rules/require-prop-type-constructor.md
@@ -0,0 +1,61 @@
+# require prop type to be a constructor (vue/require-prop-type-constructor)
+
+- :gear: This rule is included in all of `"plugin:vue/essential"`, `"plugin:vue/strongly-recommended"` and `"plugin:vue/recommended"`.
+- :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/docs/rules/script-indent.md b/docs/rules/script-indent.md
index 1e073899..bb882d90 100644
--- a/docs/rules/script-indent.md
+++ b/docs/rules/script-indent.md
@@ -79,6 +79,24 @@ const d = {
 </script>
 ```
 
+## Important note
+
+This rule only checks `.vue` files and does not interfere with other `.js` files. Unfortunately the default `indent` rule when turned on will try to lint both, so in order to make them complementary you can use `overrides` setting and disable `indent` rule on `.vue` files:
+
+```
+"rules": {
+  "vue/script-indent": ["error", 4, { "baseIndent": 1 }]
+},
+"overrides": [
+  {
+    "files": ["*.vue"],
+    "rules": {
+      "indent": "off"
+    }
+  }
+]
+```
+
 ## Related rules
 
 - [indent](https://eslint.org/docs/rules/indent)
diff --git a/docs/rules/singleline-html-element-content-newline.md b/docs/rules/singleline-html-element-content-newline.md
new file mode 100644
index 00000000..c58622ed
--- /dev/null
+++ b/docs/rules/singleline-html-element-content-newline.md
@@ -0,0 +1,101 @@
+# require a line break before and after the contents of a singleline element (vue/singleline-html-element-content-newline)
+
+- :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.
+
+## :book: Rule Details
+
+This rule enforces a line break before and after the contents of a singleline element.
+
+
+:-1: Examples of **incorrect** code:
+
+```html
+<div attr>content</div>
+
+<tr attr><td>{{ data1 }}</td><td>{{ data2 }}</td></tr>
+
+<div attr><!-- comment --></div>
+```
+
+:+1: Examples of **correct** code:
+
+```html
+<div attr>
+  content
+</div>
+
+<tr attr>
+  <td>
+    {{ data1 }}
+  </td>
+  <td>
+    {{ data2 }}
+  </td>
+</tr>
+
+<div attr>
+  <!-- comment -->
+</div>
+```
+
+## :wrench: Options
+
+```json
+{
+  "vue/singleline-html-element-content-newline": ["error", {
+    "ignoreWhenNoAttributes": true,
+    "ignores": ["pre", "textarea"]
+  }]
+}
+```
+
+- `ignoreWhenNoAttributes` ... allows having contents in one line, when given element has no attributes.
+    default `true`
+- `ignores` ... the configuration for element names to ignore line breaks style.  
+    default `["pre", "textarea"]`
+
+:-1: Examples of **incorrect** code for `{ignoreWhenNoAttributes: false}`:
+
+```html
+/* eslint vue/singleline-html-element-content-newline: ["error", { "ignoreWhenNoAttributes": false}] */
+
+<div>content</div>
+
+<tr><td>{{ data1 }}</td><td>{{ data2 }}</td></tr>
+
+<div><!-- comment --></div>
+```
+
+:+1: Examples of **correct** code for `{ignoreWhenNoAttributes: true}` (default):
+
+```html
+/* eslint vue/singleline-html-element-content-newline: ["error", { "ignoreWhenNoAttributes": true}] */
+
+<div>content</div>
+
+<tr><td>{{ data1 }}</td><td>{{ data2 }}</td></tr>
+
+<div><!-- comment --></div>
+```
+
+:-1: Examples of **incorrect** code for `{ignoreWhenNoAttributes: true}` (default):
+
+```html
+/* eslint vue/singleline-html-element-content-newline: ["error", { "ignoreWhenNoAttributes": true}] */
+
+<div attr>content</div>
+
+<tr attr><td>{{ data1 }}</td><td>{{ data2 }}</td></tr>
+
+<div attr><!-- comment --></div>
+```
+
+:+1: Examples of **correct** code for `ignores`:
+
+```html
+/* eslint vue/singleline-html-element-content-newline: ["error", { "ignores": ["VueComponent", "pre", "textarea"]}] */
+
+<VueComponent>content</VueComponent>
+
+<VueComponent attr><span>content</span></VueComponent>
+```
diff --git a/lib/configs/essential.js b/lib/configs/essential.js
index 955571ad..32228e0c 100644
--- a/lib/configs/essential.js
+++ b/lib/configs/essential.js
@@ -15,9 +15,11 @@ 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',
+    'vue/require-prop-type-constructor': 'error',
     'vue/require-render-return': 'error',
     'vue/require-v-for-key': 'error',
     'vue/require-valid-default-prop': 'error',
diff --git a/lib/index.js b/lib/index.js
index 702fd41e..fff64d39 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -19,6 +19,7 @@ module.exports = {
     'html-self-closing': require('./rules/html-self-closing'),
     'jsx-uses-vars': require('./rules/jsx-uses-vars'),
     'max-attributes-per-line': require('./rules/max-attributes-per-line'),
+    'multiline-html-element-content-newline': require('./rules/multiline-html-element-content-newline'),
     'mustache-interpolation-spacing': require('./rules/mustache-interpolation-spacing'),
     'name-property-casing': require('./rules/name-property-casing'),
     'no-async-in-computed-properties': require('./rules/no-async-in-computed-properties'),
@@ -30,9 +31,11 @@ module.exports = {
     'no-reserved-keys': require('./rules/no-reserved-keys'),
     'no-shared-component-data': require('./rules/no-shared-component-data'),
     'no-side-effects-in-computed-properties': require('./rules/no-side-effects-in-computed-properties'),
+    'no-spaces-around-equal-signs-in-attribute': require('./rules/no-spaces-around-equal-signs-in-attribute'),
     '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,12 +43,14 @@ 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'),
     'require-valid-default-prop': require('./rules/require-valid-default-prop'),
     'return-in-computed-property': require('./rules/return-in-computed-property'),
     'script-indent': require('./rules/script-indent'),
+    'singleline-html-element-content-newline': require('./rules/singleline-html-element-content-newline'),
     'this-in-template': require('./rules/this-in-template'),
     'v-bind-style': require('./rules/v-bind-style'),
     'v-on-style': require('./rules/v-on-style'),
diff --git a/lib/rules/multiline-html-element-content-newline.js b/lib/rules/multiline-html-element-content-newline.js
new file mode 100644
index 00000000..016883aa
--- /dev/null
+++ b/lib/rules/multiline-html-element-content-newline.js
@@ -0,0 +1,157 @@
+/**
+ * @author Yosuke Ota
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const utils = require('../utils')
+
+// ------------------------------------------------------------------------------
+// Helpers
+// ------------------------------------------------------------------------------
+
+function isMultilineElement (element) {
+  return element.loc.start.line < element.endTag.loc.start.line
+}
+
+function parseOptions (options) {
+  return Object.assign({
+    'ignores': ['pre', 'textarea']
+  }, options)
+}
+
+function getPhrase (lineBreaks) {
+  switch (lineBreaks) {
+    case 0: return 'no'
+    default: return `${lineBreaks}`
+  }
+}
+/**
+ * Check whether the given element is empty or not.
+ * This ignores whitespaces, doesn't ignore comments.
+ * @param {VElement} node The element node to check.
+ * @param {SourceCode} sourceCode The source code object of the current context.
+ * @returns {boolean} `true` if the element is empty.
+ */
+function isEmpty (node, sourceCode) {
+  const start = node.startTag.range[1]
+  const end = node.endTag.range[0]
+  return sourceCode.text.slice(start, end).trim() === ''
+}
+
+// ------------------------------------------------------------------------------
+// Rule Definition
+// ------------------------------------------------------------------------------
+
+module.exports = {
+  meta: {
+    docs: {
+      description: 'require a line break before and after the contents of a multiline element',
+      category: undefined,
+      url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.2/docs/rules/multiline-html-element-content-newline.md'
+    },
+    fixable: 'whitespace',
+    schema: [{
+      type: 'object',
+      properties: {
+        'ignores': {
+          type: 'array',
+          items: { type: 'string' },
+          uniqueItems: true,
+          additionalItems: false
+        }
+      },
+      additionalProperties: false
+    }],
+    messages: {
+      unexpectedAfterClosingBracket: 'Expected 1 line break after opening tag (`<{{name}}>`), but {{actual}} line breaks found.',
+      unexpectedBeforeOpeningBracket: 'Expected 1 line break before closing tag (`</{{name}}>`), but {{actual}} line breaks found.'
+    }
+  },
+
+  create (context) {
+    const ignores = parseOptions(context.options[0]).ignores
+    const template = context.parserServices.getTemplateBodyTokenStore && context.parserServices.getTemplateBodyTokenStore()
+    const sourceCode = context.getSourceCode()
+
+    let inIgnoreElement
+
+    return utils.defineTemplateBodyVisitor(context, {
+      'VElement' (node) {
+        if (inIgnoreElement) {
+          return
+        }
+        if (ignores.indexOf(node.name) >= 0) {
+          // ignore element name
+          inIgnoreElement = node
+          return
+        }
+        if (node.startTag.selfClosing || !node.endTag) {
+          // self closing
+          return
+        }
+
+        if (!isMultilineElement(node)) {
+          return
+        }
+
+        const getTokenOption = { includeComments: true, filter: (token) => token.type !== 'HTMLWhitespace' }
+        const contentFirst = template.getTokenAfter(node.startTag, getTokenOption)
+        const contentLast = template.getTokenBefore(node.endTag, getTokenOption)
+
+        const beforeLineBreaks = contentFirst.loc.start.line - node.startTag.loc.end.line
+        const afterLineBreaks = node.endTag.loc.start.line - contentLast.loc.end.line
+        if (beforeLineBreaks !== 1) {
+          context.report({
+            node: template.getLastToken(node.startTag),
+            loc: {
+              start: node.startTag.loc.end,
+              end: contentFirst.loc.start
+            },
+            messageId: 'unexpectedAfterClosingBracket',
+            data: {
+              name: node.name,
+              actual: getPhrase(beforeLineBreaks)
+            },
+            fix (fixer) {
+              const range = [node.startTag.range[1], contentFirst.range[0]]
+              return fixer.replaceTextRange(range, '\n')
+            }
+          })
+        }
+
+        if (isEmpty(node, sourceCode)) {
+          return
+        }
+
+        if (afterLineBreaks !== 1) {
+          context.report({
+            node: template.getFirstToken(node.endTag),
+            loc: {
+              start: contentLast.loc.end,
+              end: node.endTag.loc.start
+            },
+            messageId: 'unexpectedBeforeOpeningBracket',
+            data: {
+              name: node.name,
+              actual: getPhrase(afterLineBreaks)
+            },
+            fix (fixer) {
+              const range = [contentLast.range[1], node.endTag.range[0]]
+              return fixer.replaceTextRange(range, '\n')
+            }
+          })
+        }
+      },
+      'VElement:exit' (node) {
+        if (inIgnoreElement === node) {
+          inIgnoreElement = null
+        }
+      }
+    })
+  }
+}
diff --git a/lib/rules/no-spaces-around-equal-signs-in-attribute.js b/lib/rules/no-spaces-around-equal-signs-in-attribute.js
new file mode 100644
index 00000000..e2e0766b
--- /dev/null
+++ b/lib/rules/no-spaces-around-equal-signs-in-attribute.js
@@ -0,0 +1,54 @@
+/**
+ * @author Yosuke Ota
+ * issue https://github.com/vuejs/eslint-plugin-vue/issues/460
+ */
+'use strict'
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const utils = require('../utils')
+
+// ------------------------------------------------------------------------------
+// Rule Definition
+// ------------------------------------------------------------------------------
+
+module.exports = {
+  meta: {
+    docs: {
+      description: 'disallow spaces around equal signs in attribute',
+      category: undefined,
+      url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.2/docs/rules/no-spaces-around-equal-signs-in-attribute.md'
+    },
+    fixable: 'whitespace',
+    schema: []
+  },
+
+  create (context) {
+    const sourceCode = context.getSourceCode()
+    return utils.defineTemplateBodyVisitor(context, {
+      'VAttribute' (node) {
+        if (!node.value) {
+          return
+        }
+        const range = [node.key.range[1], node.value.range[0]]
+        const eqText = sourceCode.text.slice(range[0], range[1])
+        const expect = eqText.trim()
+
+        if (eqText !== expect) {
+          context.report({
+            node: node.key,
+            loc: {
+              start: node.key.loc.end,
+              end: node.value.loc.start
+            },
+            message: 'Unexpected spaces found around equal signs.',
+            data: {},
+            fix: fixer => fixer.replaceTextRange(range, expect)
+          })
+        }
+      }
+    })
+  }
+}
diff --git a/lib/rules/no-unused-components.js b/lib/rules/no-unused-components.js
new file mode 100644
index 00000000..e7825971
--- /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.2/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/require-prop-type-constructor.js b/lib/rules/require-prop-type-constructor.js
new file mode 100644
index 00000000..2b3e03b9
--- /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: 'essential',
+      url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.2/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/singleline-html-element-content-newline.js b/lib/rules/singleline-html-element-content-newline.js
new file mode 100644
index 00000000..5bf0f727
--- /dev/null
+++ b/lib/rules/singleline-html-element-content-newline.js
@@ -0,0 +1,152 @@
+/**
+ * @author Yosuke Ota
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const utils = require('../utils')
+
+// ------------------------------------------------------------------------------
+// Helpers
+// ------------------------------------------------------------------------------
+
+function isSinglelineElement (element) {
+  return element.loc.start.line === element.endTag.loc.start.line
+}
+
+function parseOptions (options) {
+  return Object.assign({
+    'ignores': ['pre', 'textarea'],
+    'ignoreWhenNoAttributes': true
+  }, options)
+}
+
+/**
+ * Check whether the given element is empty or not.
+ * This ignores whitespaces, doesn't ignore comments.
+ * @param {VElement} node The element node to check.
+ * @param {SourceCode} sourceCode The source code object of the current context.
+ * @returns {boolean} `true` if the element is empty.
+ */
+function isEmpty (node, sourceCode) {
+  const start = node.startTag.range[1]
+  const end = node.endTag.range[0]
+  return sourceCode.text.slice(start, end).trim() === ''
+}
+
+// ------------------------------------------------------------------------------
+// Rule Definition
+// ------------------------------------------------------------------------------
+
+module.exports = {
+  meta: {
+    docs: {
+      description: 'require a line break before and after the contents of a singleline element',
+      category: undefined,
+      url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.2/docs/rules/singleline-html-element-content-newline.md'
+    },
+    fixable: 'whitespace',
+    schema: [{
+      type: 'object',
+      properties: {
+        'ignoreWhenNoAttributes': {
+          type: 'boolean'
+        },
+        'ignores': {
+          type: 'array',
+          items: { type: 'string' },
+          uniqueItems: true,
+          additionalItems: false
+        }
+      },
+      additionalProperties: false
+    }],
+    messages: {
+      unexpectedAfterClosingBracket: 'Expected 1 line break after opening tag (`<{{name}}>`), but no line breaks found.',
+      unexpectedBeforeOpeningBracket: 'Expected 1 line break before closing tag (`</{{name}}>`), but no line breaks found.'
+    }
+  },
+
+  create (context) {
+    const options = parseOptions(context.options[0])
+    const ignores = options.ignores
+    const ignoreWhenNoAttributes = options.ignoreWhenNoAttributes
+    const template = context.parserServices.getTemplateBodyTokenStore && context.parserServices.getTemplateBodyTokenStore()
+    const sourceCode = context.getSourceCode()
+
+    let inIgnoreElement
+
+    return utils.defineTemplateBodyVisitor(context, {
+      'VElement' (node) {
+        if (inIgnoreElement) {
+          return
+        }
+        if (ignores.indexOf(node.name) >= 0) {
+          // ignore element name
+          inIgnoreElement = node
+          return
+        }
+        if (node.startTag.selfClosing || !node.endTag) {
+          // self closing
+          return
+        }
+
+        if (!isSinglelineElement(node)) {
+          return
+        }
+        if (ignoreWhenNoAttributes && node.startTag.attributes.length === 0) {
+          return
+        }
+
+        const getTokenOption = { includeComments: true, filter: (token) => token.type !== 'HTMLWhitespace' }
+        const contentFirst = template.getTokenAfter(node.startTag, getTokenOption)
+        const contentLast = template.getTokenBefore(node.endTag, getTokenOption)
+
+        context.report({
+          node: template.getLastToken(node.startTag),
+          loc: {
+            start: node.startTag.loc.end,
+            end: contentFirst.loc.start
+          },
+          messageId: 'unexpectedAfterClosingBracket',
+          data: {
+            name: node.name
+          },
+          fix (fixer) {
+            const range = [node.startTag.range[1], contentFirst.range[0]]
+            return fixer.replaceTextRange(range, '\n')
+          }
+        })
+
+        if (isEmpty(node, sourceCode)) {
+          return
+        }
+
+        context.report({
+          node: template.getFirstToken(node.endTag),
+          loc: {
+            start: contentLast.loc.end,
+            end: node.endTag.loc.start
+          },
+          messageId: 'unexpectedBeforeOpeningBracket',
+          data: {
+            name: node.name
+          },
+          fix (fixer) {
+            const range = [contentLast.range[1], node.endTag.range[0]]
+            return fixer.replaceTextRange(range, '\n')
+          }
+        })
+      },
+      'VElement:exit' (node) {
+        if (inIgnoreElement === node) {
+          inIgnoreElement = null
+        }
+      }
+    })
+  }
+}
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.


Test patch:
diff --git a/tests/lib/rules/multiline-html-element-content-newline.js b/tests/lib/rules/multiline-html-element-content-newline.js
new file mode 100644
index 00000000..c60fa513
--- /dev/null
+++ b/tests/lib/rules/multiline-html-element-content-newline.js
@@ -0,0 +1,499 @@
+/**
+ * @author Yosuke Ota
+ */
+'use strict'
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const rule = require('../../../lib/rules/multiline-html-element-content-newline')
+const RuleTester = require('eslint').RuleTester
+
+// ------------------------------------------------------------------------------
+// Tests
+// ------------------------------------------------------------------------------
+
+const tester = new RuleTester({
+  parser: 'vue-eslint-parser',
+  parserOptions: {
+    ecmaVersion: 2015
+  }
+})
+
+tester.run('multiline-html-element-content-newline', rule, {
+  valid: [
+    `<template><div class="panel">content</div></template>`,
+    `<template><div class="panel"><div></div></div></template>`,
+    `<template><div class="panel"><!-- comment --></div></template>`,
+    `
+      <template>
+        <div class="panel">
+          content
+        </div>
+      </template>`,
+    `
+      <template>
+        <div
+          class="panel"
+        >
+          content
+        </div>
+      </template>`,
+    `
+      <template>
+        <div>
+          <div>
+            content
+            content
+          </div>
+        </div>
+      </template>`,
+    `<div>multiline end tag</div
+      >`,
+    // empty
+    `<template><div class="panel"></div></template>`,
+    `
+      <template>
+        <div
+          class="panel">
+        </div>
+      </template>`,
+    // self closing
+    `
+      <template>
+        <self-closing />
+      </template>`,
+    // ignores
+    `
+      <template>
+        <pre>content</pre>
+        <pre
+          id="test-pre"
+        >content</pre>
+        <pre><div
+          >content</div></pre>
+        <pre>content
+          content</pre>
+        <textarea>content</textarea>
+        <textarea
+          id="test-textarea"
+        >content</textarea>
+        <textarea>content
+          content</textarea>
+      </template>`,
+    {
+      code: `
+        <template>
+          <ignore-tag>content</ignore-tag>
+          <ignore-tag
+            id="test-pre"
+          >content</ignore-tag>
+          <ignore-tag><div
+            >content</div></ignore-tag>
+          <ignore-tag>>content
+            content</ignore-tag>
+        </template>`,
+      options: [{
+        ignores: ['ignore-tag']
+      }]
+    },
+    // Ignore if no closing brackets
+    `
+      <template>
+        <div
+          id=
+          ""
+    `
+  ],
+  invalid: [
+    {
+      code: `
+        <template>
+          <div
+            class="panel"
+          >content</div>
+        </template>
+      `,
+      output: `
+        <template>
+          <div
+            class="panel"
+          >
+content
+</div>
+        </template>
+      `,
+      errors: [
+        {
+          message: 'Expected 1 line break after opening tag (`<div>`), but no line breaks found.',
+          line: 5,
+          column: 12,
+          nodeType: 'HTMLTagClose',
+          endLine: 5,
+          endColumn: 12
+        },
+        {
+          message: 'Expected 1 line break before closing tag (`</div>`), but no line breaks found.',
+          line: 5,
+          column: 19,
+          nodeType: 'HTMLEndTagOpen',
+          endLine: 5,
+          endColumn: 19
+        }
+      ]
+    },
+    // spaces
+    {
+      code: `
+        <template>
+          <div
+            class="panel"
+          >   content</div>
+        </template>
+      `,
+      output: `
+        <template>
+          <div
+            class="panel"
+          >
+content
+</div>
+        </template>
+      `,
+      errors: [
+        {
+          message: 'Expected 1 line break after opening tag (`<div>`), but no line breaks found.',
+          line: 5,
+          column: 12,
+          endLine: 5,
+          endColumn: 15
+        },
+        {
+          message: 'Expected 1 line break before closing tag (`</div>`), but no line breaks found.',
+          line: 5,
+          column: 22,
+          endLine: 5,
+          endColumn: 22
+        }
+      ]
+    },
+    // elements
+    {
+      code: `
+        <template>
+          <div><div></div>
+          <div></div></div>
+        </template>
+      `,
+      output: `
+        <template>
+          <div>
+<div></div>
+          <div></div>
+</div>
+        </template>
+      `,
+      errors: [
+        {
+          message: 'Expected 1 line break after opening tag (`<div>`), but no line breaks found.',
+          line: 3,
+          column: 16,
+          endLine: 3,
+          endColumn: 16
+        },
+        {
+          message: 'Expected 1 line break before closing tag (`</div>`), but no line breaks found.',
+          line: 4,
+          column: 22,
+          endLine: 4,
+          endColumn: 22
+        }
+      ]
+    },
+    // contents
+    {
+      code: `
+        <template>
+          <div>multiline
+            content</div>
+        </template>`,
+      output: `
+        <template>
+          <div>
+multiline
+            content
+</div>
+        </template>`,
+      errors: [
+        'Expected 1 line break after opening tag (`<div>`), but no line breaks found.',
+        'Expected 1 line break before closing tag (`</div>`), but no line breaks found.'
+      ]
+    },
+    {
+      code: `
+        <template>
+          <div>multiline content
+          </div>
+        </template>`,
+      output: `
+        <template>
+          <div>
+multiline content
+          </div>
+        </template>`,
+      errors: [
+        'Expected 1 line break after opening tag (`<div>`), but no line breaks found.'
+      ]
+    },
+    {
+      code: `
+        <template>
+          <div>
+            multiline content</div>
+        </template>`,
+      output: `
+        <template>
+          <div>
+            multiline content
+</div>
+        </template>`,
+      errors: [
+        'Expected 1 line break before closing tag (`</div>`), but no line breaks found.'
+      ]
+    },
+    // comments
+    {
+      code: `
+        <template>
+          <div><!--comment-->
+          <!--comment--></div>
+        </template>
+      `,
+      output: `
+        <template>
+          <div>
+<!--comment-->
+          <!--comment-->
+</div>
+        </template>
+      `,
+      errors: [
+        'Expected 1 line break after opening tag (`<div>`), but no line breaks found.',
+        'Expected 1 line break before closing tag (`</div>`), but no line breaks found.'
+      ]
+    },
+    {
+      code: `
+        <template>
+          <div><!--comment
+            comment--></div>
+        </template>
+      `,
+      output: `
+        <template>
+          <div>
+<!--comment
+            comment-->
+</div>
+        </template>
+      `,
+      errors: [
+        'Expected 1 line break after opening tag (`<div>`), but no line breaks found.',
+        'Expected 1 line break before closing tag (`</div>`), but no line breaks found.'
+      ]
+    },
+    // one error
+    {
+      code: `
+        <template>
+          <div>content
+            content
+          </div>
+        </template>
+      `,
+      output: `
+        <template>
+          <div>
+content
+            content
+          </div>
+        </template>
+      `,
+      errors: [
+        'Expected 1 line break after opening tag (`<div>`), but no line breaks found.'
+      ]
+    },
+    {
+      code: `
+        <template>
+          <div>
+          content
+          content</div>
+        </template>
+      `,
+      output: `
+        <template>
+          <div>
+          content
+          content
+</div>
+        </template>
+      `,
+      errors: [
+        'Expected 1 line break before closing tag (`</div>`), but no line breaks found.'
+      ]
+    },
+    // multi
+    {
+      code: `
+        <template><div>content<div>content
+        content</div>content</div></template>
+      `,
+      output: `
+        <template>
+<div>
+content<div>
+content
+        content
+</div>content
+</div>
+</template>
+      `,
+      errors: [
+        'Expected 1 line break after opening tag (`<template>`), but no line breaks found.',
+        'Expected 1 line break after opening tag (`<div>`), but no line breaks found.',
+        'Expected 1 line break after opening tag (`<div>`), but no line breaks found.',
+        'Expected 1 line break before closing tag (`</div>`), but no line breaks found.',
+        'Expected 1 line break before closing tag (`</div>`), but no line breaks found.',
+        'Expected 1 line break before closing tag (`</template>`), but no line breaks found.'
+      ]
+    },
+    // multi line breaks
+    {
+      code: `
+        <template>
+          <div>
+
+            content
+            content
+
+          </div>
+        </template>
+      `,
+      output: `
+        <template>
+          <div>
+content
+            content
+</div>
+        </template>
+      `,
+      errors: [
+        'Expected 1 line break after opening tag (`<div>`), but 2 line breaks found.',
+        'Expected 1 line break before closing tag (`</div>`), but 2 line breaks found.'
+      ]
+    },
+    // mustache
+    {
+      code: `
+        <template>
+          <div>{{content}}
+          {{content2}}</div>
+        </template>
+      `,
+      output: `
+        <template>
+          <div>
+{{content}}
+          {{content2}}
+</div>
+        </template>
+      `,
+      errors: [
+        'Expected 1 line break after opening tag (`<div>`), but no line breaks found.',
+        'Expected 1 line break before closing tag (`</div>`), but no line breaks found.'
+      ]
+    },
+    // mix
+    {
+      code: `
+        <template>
+          <div>content
+          <child></child>
+          <!-- comment --></div>
+        </template>
+      `,
+      output: `
+        <template>
+          <div>
+content
+          <child></child>
+          <!-- comment -->
+</div>
+        </template>
+      `,
+      errors: [
+        'Expected 1 line break after opening tag (`<div>`), but no line breaks found.',
+        'Expected 1 line break before closing tag (`</div>`), but no line breaks found.'
+      ]
+    },
+    // start tag
+    {
+      code: `
+        <template>
+          <div
+            >content</div>
+        </template>
+      `,
+      output: `
+        <template>
+          <div
+            >
+content
+</div>
+        </template>
+      `,
+      errors: [
+        'Expected 1 line break after opening tag (`<div>`), but no line breaks found.',
+        'Expected 1 line break before closing tag (`</div>`), but no line breaks found.'
+      ]
+    },
+    {
+      code: `
+        <template>
+          <div
+            attr>content</div>
+        </template>
+      `,
+      output: `
+        <template>
+          <div
+            attr>
+content
+</div>
+        </template>
+      `,
+      errors: [
+        'Expected 1 line break after opening tag (`<div>`), but no line breaks found.',
+        'Expected 1 line break before closing tag (`</div>`), but no line breaks found.'
+      ]
+    },
+    {
+      code: `
+        <template>
+          <div
+            ></div>
+        </template>
+      `,
+      output: `
+        <template>
+          <div
+            >
+</div>
+        </template>
+      `,
+      errors: ['Expected 1 line break after opening tag (`<div>`), but no line breaks found.']
+    }
+  ]
+})
diff --git a/tests/lib/rules/no-spaces-around-equal-signs-in-attribute.js b/tests/lib/rules/no-spaces-around-equal-signs-in-attribute.js
new file mode 100644
index 00000000..7bd090f3
--- /dev/null
+++ b/tests/lib/rules/no-spaces-around-equal-signs-in-attribute.js
@@ -0,0 +1,234 @@
+/**
+ * @author Yosuke Ota
+ */
+'use strict'
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const rule = require('../../../lib/rules/no-spaces-around-equal-signs-in-attribute')
+const RuleTester = require('eslint').RuleTester
+
+// ------------------------------------------------------------------------------
+// Tests
+// ------------------------------------------------------------------------------
+
+const tester = new RuleTester({
+  parser: 'vue-eslint-parser'
+})
+
+tester.run('no-spaces-around-equal-signs-in-attribute', rule, {
+  valid: [
+    '<template><div attr="value" /></template>',
+    '<template><div attr="" /></template>',
+    '<template><div attr=\'value\' /></template>',
+    '<template><div attr=value /></template>',
+    '<template><div attr /></template>',
+    '<template><div/></template>',
+    `<template>
+      <div
+        is="header"
+        v-for="item in items"
+        v-if="!visible"
+        v-once
+        id="uniqueID"
+        ref="header"
+        v-model="headerData"
+        myProp="prop"
+        @click="functionCall"
+        v-text="textContent">
+      </div>
+    </template>`
+  ],
+  invalid: [
+    {
+      code: '<template><div attr = "value" /></template>',
+      output: '<template><div attr="value" /></template>',
+      errors: [
+        {
+          message: 'Unexpected spaces found around equal signs.',
+          line: 1,
+          column: 20,
+          endLine: 1,
+          endColumn: 23
+        }
+      ]
+    },
+    {
+      code: '<template><div attr = "" /></template>',
+      output: '<template><div attr="" /></template>',
+      errors: [
+        {
+          message: 'Unexpected spaces found around equal signs.',
+          line: 1,
+          column: 20,
+          endLine: 1,
+          endColumn: 23
+        }
+      ]
+    },
+    {
+      code: '<template><div attr = \'value\' /></template>',
+      output: '<template><div attr=\'value\' /></template>',
+      errors: [
+        {
+          message: 'Unexpected spaces found around equal signs.',
+          line: 1,
+          column: 20,
+          endLine: 1,
+          endColumn: 23
+        }
+      ]
+    },
+    {
+      code: '<template><div attr = value /></template>',
+      output: '<template><div attr=value /></template>',
+      errors: [
+        {
+          message: 'Unexpected spaces found around equal signs.',
+          line: 1,
+          column: 20,
+          endLine: 1,
+          endColumn: 23
+        }
+      ]
+    },
+    {
+      code: '<template><div attr \t\n   =   \t\n "value" /></template>',
+      output: '<template><div attr="value" /></template>',
+      errors: [
+        {
+          message: 'Unexpected spaces found around equal signs.',
+          line: 1,
+          column: 20,
+          endLine: 3,
+          endColumn: 2
+        }
+      ]
+    },
+    {
+      code: '<template><div attr ="value" /></template>',
+      output: '<template><div attr="value" /></template>',
+      errors: [
+        {
+          message: 'Unexpected spaces found around equal signs.',
+          line: 1,
+          column: 20,
+          endLine: 1,
+          endColumn: 22
+        }
+      ]
+    },
+    {
+      code: '<template><div attr= "value" /></template>',
+      output: '<template><div attr="value" /></template>',
+      errors: [
+        {
+          message: 'Unexpected spaces found around equal signs.',
+          line: 1,
+          column: 20,
+          endLine: 1,
+          endColumn: 22
+        }
+      ]
+    },
+    {
+      code:
+        `<template>
+          <div
+            is = "header"
+            v-for = "item in items"
+            v-if = "!visible"
+            v-once
+            id = "uniqueID"
+            ref = "header"
+            v-model = "headerData"
+            myProp = "prop"
+            @click = "functionCall"
+            v-text = "textContent">
+          </div>
+        </template>`,
+      output:
+        `<template>
+          <div
+            is="header"
+            v-for="item in items"
+            v-if="!visible"
+            v-once
+            id="uniqueID"
+            ref="header"
+            v-model="headerData"
+            myProp="prop"
+            @click="functionCall"
+            v-text="textContent">
+          </div>
+        </template>`,
+      errors: [
+        {
+          message: 'Unexpected spaces found around equal signs.',
+          line: 3,
+          column: 15,
+          endLine: 3,
+          endColumn: 18
+        },
+        {
+          message: 'Unexpected spaces found around equal signs.',
+          line: 4,
+          column: 18,
+          endLine: 4,
+          endColumn: 21
+        },
+        {
+          message: 'Unexpected spaces found around equal signs.',
+          line: 5,
+          column: 17,
+          endLine: 5,
+          endColumn: 20
+        },
+        {
+          message: 'Unexpected spaces found around equal signs.',
+          line: 7,
+          column: 15,
+          endLine: 7,
+          endColumn: 18
+        },
+        {
+          message: 'Unexpected spaces found around equal signs.',
+          line: 8,
+          column: 16,
+          endLine: 8,
+          endColumn: 19
+        },
+        {
+          message: 'Unexpected spaces found around equal signs.',
+          line: 9,
+          column: 20,
+          endLine: 9,
+          endColumn: 23
+        },
+        {
+          message: 'Unexpected spaces found around equal signs.',
+          line: 10,
+          column: 19,
+          endLine: 10,
+          endColumn: 22
+        },
+        {
+          message: 'Unexpected spaces found around equal signs.',
+          line: 11,
+          column: 19,
+          endLine: 11,
+          endColumn: 22
+        },
+        {
+          message: 'Unexpected spaces found around equal signs.',
+          line: 12,
+          column: 19,
+          endLine: 12,
+          endColumn: 22
+        }
+      ]
+    }
+  ]
+})
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/rules/singleline-html-element-content-newline.js b/tests/lib/rules/singleline-html-element-content-newline.js
new file mode 100644
index 00000000..dcb65fd7
--- /dev/null
+++ b/tests/lib/rules/singleline-html-element-content-newline.js
@@ -0,0 +1,439 @@
+/**
+ * @author Yosuke Ota
+ */
+'use strict'
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const rule = require('../../../lib/rules/singleline-html-element-content-newline')
+const RuleTester = require('eslint').RuleTester
+
+// ------------------------------------------------------------------------------
+// Tests
+// ------------------------------------------------------------------------------
+
+const tester = new RuleTester({
+  parser: 'vue-eslint-parser',
+  parserOptions: {
+    ecmaVersion: 2015
+  }
+})
+
+tester.run('singleline-html-element-content-newline', rule, {
+  valid: [
+    `
+      <template>
+        <div class="panel">
+          content
+        </div>
+      </template>`,
+    `
+      <template>
+        <div class="panel">
+          <div>
+          </div>
+        </div>
+      </template>`,
+    `
+      <template>
+        <div class="panel">
+          <!-- comment -->
+        </div>
+      </template>`,
+    `
+      <template>
+        <div>
+          content
+        </div>
+      </template>`,
+    `
+      <template>
+        <div>
+          <div>
+          </div>
+        </div>
+      </template>`,
+    `
+      <template>
+        <div>
+          <!-- comment -->
+        </div>
+      </template>`,
+    // ignoreWhenNoAttributes: true
+    `
+      <template>
+        <div>singleline content</div>
+      </template>`,
+    `
+      <template>
+        <tr><td>singleline</td><td>children</td></tr>
+      </template>`,
+    `
+      <template>
+        <div><!-- singleline comment --></div>
+      </template>`,
+    `
+      <template>
+        <div     >singleline element</div     >
+      </template>`,
+    `
+      <template>
+        <div></div>
+      </template>`,
+    `
+      <template>
+        <div>    </div>
+      </template>`,
+    // ignoreWhenNoAttributes: false
+    {
+      code: `
+      <template>
+        <div>
+          content
+        </div>
+      </template>`,
+      options: [{ ignoreWhenNoAttributes: false }]
+    },
+    {
+      code: `
+      <template>
+        <div>
+          <div>
+          </div>
+        </div>
+      </template>`,
+      options: [{ ignoreWhenNoAttributes: false }]
+    },
+    {
+      code: `
+      <template>
+        <div>
+          <!-- comment -->
+        </div>
+      </template>`,
+      options: [{ ignoreWhenNoAttributes: false }]
+    },
+    // self closing
+    `
+      <template>
+        <self-closing />
+      </template>`,
+    // ignores
+    `
+      <template>
+        <pre>content</pre>
+        <pre attr>content</pre>
+        <pre><span attr>content</span></pre>
+        <textarea>content</textarea>
+        <textarea attr>content</textarea>
+        <textarea><span attr>content</span></textarea>
+      </template>`,
+    {
+      code: `
+        <template>
+          <ignore-tag>content</ignore-tag>
+          <ignore-tag attr>content</ignore-tag>
+          <ignore-tag><span attr>content</span></ignore-tag>
+        </template>`,
+      options: [{
+        ignores: ['ignore-tag']
+      }]
+    },
+    // not target
+    `
+      <template>
+        <div>content
+        </div>
+      </template>`,
+    `
+      <template>
+        <div>
+          content</div>
+      </template>`,
+    // Ignore if no closing brackets
+    `
+      <template>
+        <div
+          id=
+          ""
+    `
+  ],
+  invalid: [
+    {
+      code: `
+        <template>
+          <div class="panel">content</div>
+        </template>
+      `,
+      output: `
+        <template>
+          <div class="panel">
+content
+</div>
+        </template>
+      `,
+      errors: [
+        {
+          message: 'Expected 1 line break after opening tag (`<div>`), but no line breaks found.',
+          line: 3,
+          column: 30,
+          nodeType: 'HTMLTagClose',
+          endLine: 3,
+          endColumn: 30
+        },
+        {
+          message: 'Expected 1 line break before closing tag (`</div>`), but no line breaks found.',
+          line: 3,
+          column: 37,
+          nodeType: 'HTMLEndTagOpen',
+          endLine: 3,
+          endColumn: 37
+        }
+      ]
+    },
+    {
+      code: `
+        <template>
+          <tr attr><td>singleline</td><td>children</td></tr>
+        </template>
+      `,
+      output: `
+        <template>
+          <tr attr>
+<td>singleline</td><td>children</td>
+</tr>
+        </template>
+      `,
+      errors: [
+        'Expected 1 line break after opening tag (`<tr>`), but no line breaks found.',
+        'Expected 1 line break before closing tag (`</tr>`), but no line breaks found.'
+      ]
+    },
+    {
+      code: `
+        <template>
+          <div attr><!-- singleline comment --></div>
+        </template>
+      `,
+      output: `
+        <template>
+          <div attr>
+<!-- singleline comment -->
+</div>
+        </template>
+      `,
+      errors: [
+        'Expected 1 line break after opening tag (`<div>`), but no line breaks found.',
+        'Expected 1 line break before closing tag (`</div>`), but no line breaks found.'
+      ]
+    },
+    {
+      code: `
+        <template>
+          <div attr><!-- singleline --><!-- comments --></div>
+        </template>
+      `,
+      output: `
+        <template>
+          <div attr>
+<!-- singleline --><!-- comments -->
+</div>
+        </template>
+      `,
+      errors: [
+        'Expected 1 line break after opening tag (`<div>`), but no line breaks found.',
+        'Expected 1 line break before closing tag (`</div>`), but no line breaks found.'
+      ]
+    },
+    {
+      code: `
+        <template>
+          <div attr     >content</div    >
+        </template>
+      `,
+      output: `
+        <template>
+          <div attr     >
+content
+</div    >
+        </template>
+      `,
+      errors: [
+        'Expected 1 line break after opening tag (`<div>`), but no line breaks found.',
+        'Expected 1 line break before closing tag (`</div>`), but no line breaks found.'
+      ]
+    },
+    {
+      code: `
+        <template>
+          <div attr     >content</div
+          >
+        </template>
+      `,
+      output: `
+        <template>
+          <div attr     >
+content
+</div
+          >
+        </template>
+      `,
+      errors: [
+        'Expected 1 line break after opening tag (`<div>`), but no line breaks found.',
+        'Expected 1 line break before closing tag (`</div>`), but no line breaks found.'
+      ]
+    },
+    {
+      code: `
+        <template>
+          <div attr></div>
+        </template>
+      `,
+      output: `
+        <template>
+          <div attr>
+</div>
+        </template>
+      `,
+      errors: [
+        'Expected 1 line break after opening tag (`<div>`), but no line breaks found.'
+      ]
+    },
+    {
+      code: `
+        <template>
+          <div attr>    </div>
+        </template>
+      `,
+      output: `
+        <template>
+          <div attr>
+</div>
+        </template>
+      `,
+      errors: [
+        'Expected 1 line break after opening tag (`<div>`), but no line breaks found.'
+      ]
+    },
+    {
+      code: `
+        <template>
+          <div>singleline content</div>
+        </template>
+      `,
+      options: [{ ignoreWhenNoAttributes: false }],
+      output: `
+        <template>
+          <div>
+singleline content
+</div>
+        </template>
+      `,
+      errors: [
+        'Expected 1 line break after opening tag (`<div>`), but no line breaks found.',
+        'Expected 1 line break before closing tag (`</div>`), but no line breaks found.'
+      ]
+    },
+    {
+      code: `
+        <template>
+          <tr><td>singleline</td><td>children</td></tr>
+        </template>
+      `,
+      options: [{ ignoreWhenNoAttributes: false }],
+      output: `
+        <template>
+          <tr>
+<td>
+singleline
+</td><td>
+children
+</td>
+</tr>
+        </template>
+      `,
+      errors: [
+        'Expected 1 line break after opening tag (`<tr>`), but no line breaks found.',
+        'Expected 1 line break after opening tag (`<td>`), but no line breaks found.',
+        'Expected 1 line break before closing tag (`</td>`), but no line breaks found.',
+        'Expected 1 line break after opening tag (`<td>`), but no line breaks found.',
+        'Expected 1 line break before closing tag (`</td>`), but no line breaks found.',
+        'Expected 1 line break before closing tag (`</tr>`), but no line breaks found.'
+      ]
+    },
+    {
+      code: `
+        <template>
+          <div><!-- singleline comment --></div>
+        </template>
+      `,
+      options: [{ ignoreWhenNoAttributes: false }],
+      output: `
+        <template>
+          <div>
+<!-- singleline comment -->
+</div>
+        </template>
+      `,
+      errors: [
+        'Expected 1 line break after opening tag (`<div>`), but no line breaks found.',
+        'Expected 1 line break before closing tag (`</div>`), but no line breaks found.'
+      ]
+    },
+    {
+      code: `
+        <template>
+          <div   >   singleline element   </div   >
+        </template>
+      `,
+      options: [{ ignoreWhenNoAttributes: false }],
+      output: `
+        <template>
+          <div   >
+singleline element
+</div   >
+        </template>
+      `,
+      errors: [
+        'Expected 1 line break after opening tag (`<div>`), but no line breaks found.',
+        'Expected 1 line break before closing tag (`</div>`), but no line breaks found.'
+      ]
+    },
+    {
+      code: `
+        <template>
+          <div></div>
+        </template>
+      `,
+      options: [{ ignoreWhenNoAttributes: false }],
+      output: `
+        <template>
+          <div>
+</div>
+        </template>
+      `,
+      errors: [
+        'Expected 1 line break after opening tag (`<div>`), but no line breaks found.'
+      ]
+    },
+    {
+      code: `
+        <template>
+          <div>    </div>
+        </template>
+      `,
+      options: [{ ignoreWhenNoAttributes: false }],
+      output: `
+        <template>
+          <div>
+</div>
+        </template>
+      `,
+      errors: [
+        'Expected 1 line break after opening tag (`<div>`), but no line breaks found.'
+      ]
+    }
+  ]
+})
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'],
+    )
+  })
+})