Repository: vuejs/eslint-plugin-vue
Pull request #1968 (2022-09-12 18:11:04)

Add type-based declaration rule of `defineProps` and `defineEmits`
Implement #1960 #1959 

Amorites: Found `Parsing error` in docs editor and removed `eslint-code-block`. Maybe I could look into a solution and get it back later.
ota-meshi: Good point. The current documentation site does not parse typescript.

Patch:
diff --git a/docs/rules/README.md b/docs/rules/README.md
index de00e7e5..a60dc51b 100644
--- a/docs/rules/README.md
+++ b/docs/rules/README.md
@@ -212,7 +212,9 @@ For example:
 | [vue/component-name-in-template-casing](./component-name-in-template-casing.md) | enforce specific casing for the component naming style in template | :wrench: | :hammer: |
 | [vue/component-options-name-casing](./component-options-name-casing.md) | enforce the casing of component name in `components` options | :wrench::bulb: | :hammer: |
 | [vue/custom-event-name-casing](./custom-event-name-casing.md) | enforce specific casing for custom event name |  | :hammer: |
+| [vue/define-emits-declaration](./define-emits-declaration.md) | enforce declaration style of `defineEmits` |  | :hammer: |
 | [vue/define-macros-order](./define-macros-order.md) | enforce order of `defineEmits` and `defineProps` compiler macros | :wrench: | :lipstick: |
+| [vue/define-props-declaration](./define-props-declaration.md) | enforce declaration style of `defineProps` |  | :hammer: |
 | [vue/html-button-has-type](./html-button-has-type.md) | disallow usage of button without an explicit type attribute |  | :hammer: |
 | [vue/html-comment-content-newline](./html-comment-content-newline.md) | enforce unified line brake in HTML comments | :wrench: | :lipstick: |
 | [vue/html-comment-content-spacing](./html-comment-content-spacing.md) | enforce unified spacing in HTML comments | :wrench: | :lipstick: |
@@ -258,6 +260,7 @@ For example:
 | [vue/require-name-property](./require-name-property.md) | require a name property in Vue components |  | :hammer: |
 | [vue/script-indent](./script-indent.md) | enforce consistent indentation in `<script>` | :wrench: | :lipstick: |
 | [vue/sort-keys](./sort-keys.md) | enforce sort-keys in a manner that is compatible with order-in-components |  | :hammer: |
+| [vue/padding-line-between-tags](./padding-line-between-tags.md) | Insert newlines between sibling tags in template  | :wrench: | :warning: |
 | [vue/static-class-names-order](./static-class-names-order.md) | enforce static class names order | :wrench: | :hammer: |
 | [vue/v-for-delimiter-style](./v-for-delimiter-style.md) | enforce `v-for` directive's delimiter style | :wrench: | :lipstick: |
 | [vue/v-on-function-call](./v-on-function-call.md) | enforce or forbid parentheses after method calls without arguments in `v-on` directives | :wrench: | :hammer: |
diff --git a/docs/rules/define-emits-declaration.md b/docs/rules/define-emits-declaration.md
new file mode 100644
index 00000000..50af9f95
--- /dev/null
+++ b/docs/rules/define-emits-declaration.md
@@ -0,0 +1,78 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/define-emits-declaration
+description: enforce declaration style of `defineEmits`
+---
+# vue/define-emits-declaration
+
+> enforce declaration style of `defineEmits`
+
+- :exclamation: <badge text="This rule has not been released yet." vertical="middle" type="error"> ***This rule has not been released yet.*** </badge>
+
+## :book: Rule Details
+
+This rule enforces `defineEmits` typing style which you should use `type-based` or `runtime` declaration.
+
+This rule only works in setup script and `lang="ts"`.
+
+<eslint-code-block :rules="{'vue/define-emits-declaration': ['error']}">
+
+```vue
+<script setup lang="ts">
+/* ✓ GOOD */
+const emit = defineEmits<{
+  (e: 'change', id: number): void
+  (e: 'update', value: string): void
+}>()
+
+/* ✗ BAD */
+const emit = defineEmits(['change', 'update'])
+</script>
+```
+
+</eslint-code-block>
+
+## :wrench: Options
+
+```json
+  "vue/define-emits-declaration": ["error", "type-based" | "runtime"]
+```
+
+- `type-based` (default) enforces type-based declaration
+- `runtime` enforces runtime declaration
+
+### `runtime`
+
+<eslint-code-block :rules="{'vue/define-emits-declaration': ['error', 'runtime']}">
+
+```vue
+<script setup lang="ts">
+/* ✗ BAD */
+const emit = defineEmits<{
+  (e: 'change', id: number): void
+  (e: 'update', value: string): void
+}>()
+
+/* ✓ GOOD */
+const emit = defineEmits(['change', 'update'])
+</script>
+```
+
+</eslint-code-block>
+
+## :couple: Related Rules
+
+- [vue/define-props-declaration](./define-props-declaration.md)
+- [vue/valid-define-emits](./valid-define-emits.md)
+
+## :books: Further Reading
+
+- [`defineEmits`](https://vuejs.org/api/sfc-script-setup.html#defineprops-defineemits)
+- [Typescript-only-features of `defineEmits`](https://vuejs.org/api/sfc-script-setup.html#typescript-only-features)
+- [Guide - Typing-component-emits](https://vuejs.org/guide/typescript/composition-api.html#typing-component-emits)
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/define-emits-declaration.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/define-emits-declaration.js)
diff --git a/docs/rules/define-props-declaration.md b/docs/rules/define-props-declaration.md
new file mode 100644
index 00000000..db68791f
--- /dev/null
+++ b/docs/rules/define-props-declaration.md
@@ -0,0 +1,79 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/define-props-declaration
+description: enforce declaration style of `defineProps`
+---
+# vue/define-props-declaration
+
+> enforce declaration style of `defineProps`
+
+- :exclamation: <badge text="This rule has not been released yet." vertical="middle" type="error"> ***This rule has not been released yet.*** </badge>
+
+## :book: Rule Details
+
+This rule enforces `defineProps` typing style which you should use `type-based` or `runtime` declaration.
+
+This rule only works in setup script and `lang="ts"`.
+
+<eslint-code-block :rules="{'vue/define-props-declaration': ['error']}">
+
+```vue
+<script setup lang="ts">
+/* ✓ GOOD */
+const props = defineProps<{
+  kind: string
+}>()
+
+/* ✗ BAD */
+const props = defineProps({
+  kind: { type: String }
+})
+</script>
+```
+
+</eslint-code-block>
+
+## :wrench: Options
+
+```json
+  "vue/define-props-declaration": ["error", "type-based" | "runtime"]
+```
+
+- `type-based` (default) enforces type-based declaration
+- `runtime` enforces runtime declaration
+
+### `"runtime"`
+
+<eslint-code-block :rules="{'vue/define-emits-declaration': ['error', 'runtime']}">
+
+```vue
+<script setup lang="ts">
+/* ✗ BAD */
+const emit = defineEmits<{
+  (e: 'change', id: number): void
+  (e: 'update', value: string): void
+}>()
+
+/* ✓ GOOD */
+const emit = defineEmits(['change', 'update'])
+</script>
+```
+
+</eslint-code-block>
+
+## :couple: Related Rules
+
+- [vue/define-emits-declaration](./define-emits-declaration.md)
+- [vue/valid-define-props](./valid-define-props.md)
+
+## :books: Further Reading
+
+- [`defineProps`](https://vuejs.org/api/sfc-script-setup.html#defineprops-defineemits)
+- [Typescript-only-features of `defineProps`](https://vuejs.org/api/sfc-script-setup.html#typescript-only-features)
+- [Guide - Typing-component-props](https://vuejs.org/guide/typescript/composition-api.html#typing-component-props)
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/define-props-declaration.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/define-props-declaration.js)
diff --git a/docs/rules/padding-line-between-tags.md b/docs/rules/padding-line-between-tags.md
new file mode 100644
index 00000000..6e84a195
--- /dev/null
+++ b/docs/rules/padding-line-between-tags.md
@@ -0,0 +1,163 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/padding-line-between-tags
+description: Require or disallow newlines between sibling tags in template
+---
+# vue/padding-line-between-tags
+
+> Require or disallow newlines between sibling tags in template
+
+- :exclamation: <badge text="This rule has not been released yet." vertical="middle" type="error"> ***This rule has not been released yet.*** </badge>
+- :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 requires or disallows newlines between sibling HTML tags.
+
+<eslint-code-block fix :rules="{'vue/padding-line-between-tags': ['error']}">
+
+```vue
+<template>
+  <div>
+    <!-- ✓ GOOD: -->
+    <div></div>
+
+    <div>
+    </div>
+    
+    <div />
+
+    <div />
+    <!-- ✗ BAD: -->
+    <div></div>
+    <div>
+    </div>
+    <div /><div />
+  </div>
+</template>
+```
+
+</eslint-code-block>
+
+## :wrench: Options
+
+```json
+{
+  "vue/padding-line-between-tags": ["error", [
+    { "blankLine": "always", "prev": "*", "next": "*" }
+  ]]
+}
+```
+
+This rule requires blank lines between each sibling HTML tag by default.
+
+A configuration is an object which has 3 properties; blankLine, prev and next. For example, { blankLine: "always", prev: "br", next: "div" } means “one or more blank lines are required between a br tag and a div tag.” You can supply any number of configurations. If a tag pair matches multiple configurations, the last matched configuration will be used.
+
+- `blankLine` is one of the following:
+  - `always` requires one or more blank lines.
+  - `never` disallows blank lines.
+- `prev` any tag name without brackets.
+- `next` any tag name without brackets.
+
+### Disallow blank lines between all tags
+
+`{ blankLine: 'never', prev: '*', next: '*' }`
+
+<eslint-code-block fix :rules="{'vue/padding-line-between-tags': ['error', [
+  { blankLine: 'never', prev: '*', next: '*' }
+]]}">
+
+```vue
+<template>
+  <div>
+    <div></div>
+    <div>
+    </div>
+    <div />
+  </div>
+</template>
+```
+
+</eslint-code-block>
+
+### Require newlines after `<br>`
+
+`{ blankLine: 'always', prev: 'br', next: '*' }`
+
+<eslint-code-block fix :rules="{'vue/padding-line-between-tags': ['error', [
+  { blankLine: 'always', prev: 'br', next: '*' }
+]]}">
+
+```vue
+<template>
+  <div>
+    <ul>
+      <li>
+      </li>
+      <br />
+
+      <li>
+      </li>
+    </ul>
+  </div>
+</template>
+```
+
+</eslint-code-block>
+
+### Require newlines before `<br>`
+
+`{ blankLine: 'always', prev: '*', next: 'br' }`
+
+<eslint-code-block fix :rules="{'vue/padding-line-between-tags': ['error', [
+  { blankLine: 'always', prev: '*', next: 'br' }
+]]}">
+
+```vue
+<template>
+  <div>
+    <ul>
+      <li>
+      </li>
+      
+      <br />
+      <li>
+      </li>
+    </ul>
+  </div>
+</template>
+```
+
+</eslint-code-block>
+
+### Require newlines between `<br>` and `<img>`
+
+`{ blankLine: 'always', prev: 'br', next: 'img' }`
+
+<eslint-code-block fix :rules="{'vue/padding-line-between-tags': ['error', [
+  { blankLine: 'always', prev: 'br', next: 'img' }
+]]}">
+
+```vue
+<template>
+  <div>
+    <ul>
+      <li>
+      </li>
+      <br />
+
+      <img />
+      <li>
+      </li>
+    </ul>
+  </div>
+</template>
+```
+
+</eslint-code-block>
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/padding-line-between-tags.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/padding-line-between-tags.js)
diff --git a/docs/rules/valid-define-emits.md b/docs/rules/valid-define-emits.md
index 007a584e..f7de8454 100644
--- a/docs/rules/valid-define-emits.md
+++ b/docs/rules/valid-define-emits.md
@@ -27,8 +27,8 @@ This rule reports `defineEmits` compiler macros in the following cases:
 
 ```vue
 <script setup>
-  /* ✓ GOOD */
-  defineEmits({ notify: null })
+/* ✓ GOOD */
+defineEmits({ notify: null })
 </script>
 ```
 
@@ -38,8 +38,8 @@ This rule reports `defineEmits` compiler macros in the following cases:
 
 ```vue
 <script setup>
-  /* ✓ GOOD */
-  defineEmits(['notify'])
+/* ✓ GOOD */
+defineEmits(['notify'])
 </script>
 ```
 
@@ -49,8 +49,8 @@ This rule reports `defineEmits` compiler macros in the following cases:
 
 ```vue
 <script setup lang="ts">
-  /* ✓ GOOD */
-  defineEmits<(e: 'notify')=>void>()
+/* ✓ GOOD */
+defineEmits<(e: 'notify') => void>()
 </script>
 ```
 
@@ -60,11 +60,11 @@ This rule reports `defineEmits` compiler macros in the following cases:
 
 ```vue
 <script>
-  const def = { notify: null }
+const def = { notify: null }
 </script>
 <script setup>
-  /* ✓ GOOD */
-  defineEmits(def)
+/* ✓ GOOD */
+defineEmits(def)
 </script>
 ```
 
@@ -74,9 +74,9 @@ This rule reports `defineEmits` compiler macros in the following cases:
 
 ```vue
 <script setup>
-  /* ✗ BAD */
-  const def = { notify: null }
-  defineEmits(def)
+/* ✗ BAD */
+const def = { notify: null }
+defineEmits(def)
 </script>
 ```
 
@@ -86,8 +86,8 @@ This rule reports `defineEmits` compiler macros in the following cases:
 
 ```vue
 <script setup lang="ts">
-  /* ✗ BAD */
-  defineEmits<(e: 'notify')=>void>({ submit: null })
+/* ✗ BAD */
+defineEmits<(e: 'notify') => void>({ submit: null })
 </script>
 ```
 
@@ -97,9 +97,9 @@ This rule reports `defineEmits` compiler macros in the following cases:
 
 ```vue
 <script setup>
-  /* ✗ BAD */
-  defineEmits({ notify: null })
-  defineEmits({ submit: null })
+/* ✗ BAD */
+defineEmits({ notify: null })
+defineEmits({ submit: null })
 </script>
 ```
 
@@ -109,13 +109,13 @@ This rule reports `defineEmits` compiler macros in the following cases:
 
 ```vue
 <script>
-  export default {
-    emits: { notify: null }
-  }
+export default {
+  emits: { notify: null }
+}
 </script>
 <script setup>
-  /* ✗ BAD */
-  defineEmits({ submit: null })
+/* ✗ BAD */
+defineEmits({ submit: null })
 </script>
 ```
 
@@ -125,8 +125,8 @@ This rule reports `defineEmits` compiler macros in the following cases:
 
 ```vue
 <script setup>
-  /* ✗ BAD */
-  defineEmits()
+/* ✗ BAD */
+defineEmits()
 </script>
 ```
 
@@ -136,6 +136,11 @@ This rule reports `defineEmits` compiler macros in the following cases:
 
 Nothing.
 
+## :couple: Related Rules
+
+- [vue/define-emits-declaration](./define-emits-declaration.md)
+- [vue/valid-define-props](./valid-define-props.md)
+
 ## :rocket: Version
 
 This rule was introduced in eslint-plugin-vue v7.13.0
diff --git a/docs/rules/valid-define-props.md b/docs/rules/valid-define-props.md
index 960573bb..4345d242 100644
--- a/docs/rules/valid-define-props.md
+++ b/docs/rules/valid-define-props.md
@@ -27,8 +27,8 @@ This rule reports `defineProps` compiler macros in the following cases:
 
 ```vue
 <script setup>
-  /* ✓ GOOD */
-  defineProps({ msg: String })
+/* ✓ GOOD */
+defineProps({ msg: String })
 </script>
 ```
 
@@ -38,8 +38,8 @@ This rule reports `defineProps` compiler macros in the following cases:
 
 ```vue
 <script setup>
-  /* ✓ GOOD */
-  defineProps(['msg'])
+/* ✓ GOOD */
+defineProps(['msg'])
 </script>
 ```
 
@@ -49,8 +49,8 @@ This rule reports `defineProps` compiler macros in the following cases:
 
 ```vue
 <script setup lang="ts">
-  /* ✓ GOOD */
-  defineProps<{ msg?:string }>()
+/* ✓ GOOD */
+defineProps<{ msg?: string }>()
 </script>
 ```
 
@@ -60,11 +60,11 @@ This rule reports `defineProps` compiler macros in the following cases:
 
 ```vue
 <script>
-  const def = { msg: String }
+const def = { msg: String }
 </script>
 <script setup>
-  /* ✓ GOOD */
-  defineProps(def)
+/* ✓ GOOD */
+defineProps(def)
 </script>
 ```
 
@@ -74,9 +74,9 @@ This rule reports `defineProps` compiler macros in the following cases:
 
 ```vue
 <script setup>
-  /* ✗ BAD */
-  const def = { msg: String }
-  defineProps(def)
+/* ✗ BAD */
+const def = { msg: String }
+defineProps(def)
 </script>
 ```
 
@@ -86,8 +86,8 @@ This rule reports `defineProps` compiler macros in the following cases:
 
 ```vue
 <script setup lang="ts">
-  /* ✗ BAD */
-  defineProps<{ msg?:string }>({ msg: String })
+/* ✗ BAD */
+defineProps<{ msg?: string }>({ msg: String })
 </script>
 ```
 
@@ -97,9 +97,9 @@ This rule reports `defineProps` compiler macros in the following cases:
 
 ```vue
 <script setup>
-  /* ✗ BAD */
-  defineProps({ msg: String })
-  defineProps({ count: Number })
+/* ✗ BAD */
+defineProps({ msg: String })
+defineProps({ count: Number })
 </script>
 ```
 
@@ -109,13 +109,13 @@ This rule reports `defineProps` compiler macros in the following cases:
 
 ```vue
 <script>
-  export default {
-    props: { msg: String }
-  }
+export default {
+  props: { msg: String }
+}
 </script>
 <script setup>
-  /* ✗ BAD */
-  defineProps({ count: Number })
+/* ✗ BAD */
+defineProps({ count: Number })
 </script>
 ```
 
@@ -125,8 +125,8 @@ This rule reports `defineProps` compiler macros in the following cases:
 
 ```vue
 <script setup>
-  /* ✗ BAD */
-  defineProps()
+/* ✗ BAD */
+defineProps()
 </script>
 ```
 
@@ -136,6 +136,11 @@ This rule reports `defineProps` compiler macros in the following cases:
 
 Nothing.
 
+## :couple: Related Rules
+
+- [vue/define-props-declaration](./define-props-declaration.md)
+- [vue/valid-define-emits](./valid-define-emits.md)
+
 ## :rocket: Version
 
 This rule was introduced in eslint-plugin-vue v7.13.0
diff --git a/lib/index.js b/lib/index.js
index ca3a9308..483dfc5d 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -27,7 +27,9 @@ module.exports = {
     'component-options-name-casing': require('./rules/component-options-name-casing'),
     'component-tags-order': require('./rules/component-tags-order'),
     'custom-event-name-casing': require('./rules/custom-event-name-casing'),
+    'define-emits-declaration': require('./rules/define-emits-declaration'),
     'define-macros-order': require('./rules/define-macros-order'),
+    'define-props-declaration': require('./rules/define-props-declaration'),
     'dot-location': require('./rules/dot-location'),
     'dot-notation': require('./rules/dot-notation'),
     eqeqeq: require('./rules/eqeqeq'),
@@ -184,6 +186,7 @@ module.exports = {
     'script-setup-uses-vars': require('./rules/script-setup-uses-vars'),
     'singleline-html-element-content-newline': require('./rules/singleline-html-element-content-newline'),
     'sort-keys': require('./rules/sort-keys'),
+    'padding-line-between-tags': require('./rules/padding-line-between-tags'),
     'space-in-parens': require('./rules/space-in-parens'),
     'space-infix-ops': require('./rules/space-infix-ops'),
     'space-unary-ops': require('./rules/space-unary-ops'),
diff --git a/lib/rules/define-emits-declaration.js b/lib/rules/define-emits-declaration.js
new file mode 100644
index 00000000..758097f9
--- /dev/null
+++ b/lib/rules/define-emits-declaration.js
@@ -0,0 +1,68 @@
+/**
+ * @author Amorites
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const utils = require('../utils')
+
+// ------------------------------------------------------------------------------
+// Rule Definition
+// ------------------------------------------------------------------------------
+
+module.exports = {
+  meta: {
+    type: 'suggestion',
+    docs: {
+      description: 'enforce declaration style of `defineEmits`',
+      categories: undefined,
+      url: 'https://eslint.vuejs.org/rules/define-emits-declaration.html'
+    },
+    fixable: null,
+    messages: {
+      hasArg: 'Use type-based declaration instead of runtime declaration.',
+      hasTypeArg: 'Use runtime declaration instead of type-based declaration.'
+    },
+    schema: [
+      {
+        enum: ['type-based', 'runtime']
+      }
+    ]
+  },
+  /** @param {RuleContext} context */
+  create(context) {
+    const scriptSetup = utils.getScriptSetupElement(context)
+    if (!scriptSetup || !utils.hasAttribute(scriptSetup, 'lang', 'ts')) {
+      return {}
+    }
+
+    const defineType = context.options[0] || 'type-based'
+    return utils.defineScriptSetupVisitor(context, {
+      onDefineEmitsEnter(node) {
+        switch (defineType) {
+          case 'type-based':
+            if (node.arguments.length > 0) {
+              context.report({
+                node,
+                messageId: 'hasArg'
+              })
+            }
+            break
+
+          case 'runtime':
+            if (node.typeParameters && node.typeParameters.params.length > 0) {
+              context.report({
+                node,
+                messageId: 'hasTypeArg'
+              })
+            }
+            break
+        }
+      }
+    })
+  }
+}
diff --git a/lib/rules/define-props-declaration.js b/lib/rules/define-props-declaration.js
new file mode 100644
index 00000000..2a73ed10
--- /dev/null
+++ b/lib/rules/define-props-declaration.js
@@ -0,0 +1,68 @@
+/**
+ * @author Amorites
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const utils = require('../utils')
+
+// ------------------------------------------------------------------------------
+// Rule Definition
+// ------------------------------------------------------------------------------
+
+module.exports = {
+  meta: {
+    type: 'suggestion',
+    docs: {
+      description: 'enforce declaration style of `defineProps`',
+      categories: undefined,
+      url: 'https://eslint.vuejs.org/rules/define-props-declaration.html'
+    },
+    fixable: null,
+    messages: {
+      hasArg: 'Use type-based declaration instead of runtime declaration.',
+      hasTypeArg: 'Use runtime declaration instead of type-based declaration.'
+    },
+    schema: [
+      {
+        enum: ['type-based', 'runtime']
+      }
+    ]
+  },
+  /** @param {RuleContext} context */
+  create(context) {
+    const scriptSetup = utils.getScriptSetupElement(context)
+    if (!scriptSetup || !utils.hasAttribute(scriptSetup, 'lang', 'ts')) {
+      return {}
+    }
+
+    const defineType = context.options[0] || 'type-based'
+    return utils.defineScriptSetupVisitor(context, {
+      onDefinePropsEnter(node) {
+        switch (defineType) {
+          case 'type-based':
+            if (node.arguments.length > 0) {
+              context.report({
+                node,
+                messageId: 'hasArg'
+              })
+            }
+            break
+
+          case 'runtime':
+            if (node.typeParameters && node.typeParameters.params.length > 0) {
+              context.report({
+                node,
+                messageId: 'hasTypeArg'
+              })
+            }
+            break
+        }
+      }
+    })
+  }
+}
diff --git a/lib/rules/padding-line-between-tags.js b/lib/rules/padding-line-between-tags.js
new file mode 100644
index 00000000..3e0994a1
--- /dev/null
+++ b/lib/rules/padding-line-between-tags.js
@@ -0,0 +1,189 @@
+/**
+ * @author dev1437
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const utils = require('../utils')
+
+/**
+ * Split the source code into multiple lines based on the line delimiters.
+ * Copied from padding-line-between-blocks
+ * @param {string} text Source code as a string.
+ * @returns {string[]} Array of source code lines.
+ */
+function splitLines(text) {
+  return text.split(/\r\n|[\r\n\u2028\u2029]/gu)
+}
+
+/**
+ * @param {RuleContext} context
+ * @param {VElement} tag
+ * @param {VElement} sibling
+ */
+function insertNewLine(context, tag, sibling) {
+  context.report({
+    messageId: 'always',
+    loc: sibling.loc,
+    // @ts-ignore
+    fix(fixer) {
+      return fixer.insertTextAfter(tag, '\n')
+    }
+  })
+}
+
+/**
+ * @param {RuleContext} context
+ * @param {VEndTag | VStartTag} endTag
+ * @param {VElement} sibling
+ */
+function removeExcessLines(context, endTag, sibling) {
+  context.report({
+    messageId: 'never',
+    loc: sibling.loc,
+    // @ts-ignore
+    fix(fixer) {
+      const start = endTag.range[1]
+      const end = sibling.range[0]
+      const paddingText = context.getSourceCode().text.slice(start, end)
+      const textBetween = splitLines(paddingText)
+      let newTextBetween = `\n${textBetween.pop()}`
+      for (let i = textBetween.length - 1; i >= 0; i--) {
+        if (!/^\s*$/.test(textBetween[i])) {
+          newTextBetween = `${i === 0 ? '' : '\n'}${
+            textBetween[i]
+          }${newTextBetween}`
+        }
+      }
+      return fixer.replaceTextRange([start, end], `${newTextBetween}`)
+    }
+  })
+}
+
+// ------------------------------------------------------------------------------
+// Rule Definition
+// ------------------------------------------------------------------------------
+
+/**
+ * @param {RuleContext} context
+ */
+function checkNewline(context) {
+  /** @type {Array<{blankLine: "always" | "never", prev: string, next: string}>} */
+  const configureList = context.options[0] || [
+    { blankLine: 'always', prev: '*', next: '*' }
+  ]
+
+  /**
+   * @param {VElement} block
+   */
+  return (block) => {
+    if (!block.parent.parent) {
+      return
+    }
+
+    const endTag = block.endTag || block.startTag
+    const lowerSiblings = block.parent.children
+      .filter(
+        (element) =>
+          element.type === 'VElement' && element.range !== block.range
+      )
+      .filter((sibling) => sibling.range[0] - endTag.range[1] >= 0)
+
+    if (lowerSiblings.length === 0) {
+      return
+    }
+
+    const closestSibling = /** @type {VElement} */ (lowerSiblings[0])
+
+    for (let i = configureList.length - 1; i >= 0; --i) {
+      const configure = configureList[i]
+      const matched =
+        (configure.prev === '*' || block.name === configure.prev) &&
+        (configure.next === '*' || closestSibling.name === configure.next)
+
+      if (matched) {
+        const lineDifference =
+          closestSibling.loc.start.line - endTag.loc.end.line
+        if (configure.blankLine === 'always') {
+          if (lineDifference === 1) {
+            insertNewLine(context, block, closestSibling)
+          } else if (lineDifference === 0) {
+            context.report({
+              messageId: 'always',
+              loc: closestSibling.loc,
+              // @ts-ignore
+              fix(fixer) {
+                const lastSpaces = /** @type {RegExpExecArray} */ (
+                  /^\s*/.exec(
+                    context.getSourceCode().lines[endTag.loc.start.line - 1]
+                  )
+                )[0]
+
+                return fixer.insertTextAfter(endTag, `\n\n${lastSpaces}`)
+              }
+            })
+          }
+        } else {
+          if (lineDifference > 1) {
+            let hasOnlyTextBetween = true
+            for (
+              let i = endTag.loc.start.line;
+              i < closestSibling.loc.start.line - 1 && hasOnlyTextBetween;
+              i++
+            ) {
+              hasOnlyTextBetween = !/^\s*$/.test(
+                context.getSourceCode().lines[i]
+              )
+            }
+            if (!hasOnlyTextBetween) {
+              removeExcessLines(context, endTag, closestSibling)
+            }
+          }
+        }
+        break
+      }
+    }
+  }
+}
+
+module.exports = {
+  meta: {
+    type: 'layout',
+    docs: {
+      description:
+        'require or disallow newlines between sibling tags in template',
+      categories: undefined,
+      url: 'https://eslint.vuejs.org/rules/padding-line-between-tags.html'
+    },
+    fixable: 'whitespace',
+    schema: [
+      {
+        type: 'array',
+        items: {
+          type: 'object',
+          properties: {
+            blankLine: { enum: ['always', 'never'] },
+            prev: { type: 'string' },
+            next: { type: 'string' }
+          },
+          additionalProperties: false,
+          required: ['blankLine', 'prev', 'next']
+        }
+      }
+    ],
+    messages: {
+      never: 'Unexpected blank line before this tag.',
+      always: 'Expected blank line before this tag.'
+    }
+  },
+  /** @param {RuleContext} context */
+  create(context) {
+    return utils.defineTemplateBodyVisitor(context, {
+      VElement: checkNewline(context)
+    })
+  }
+}


Test patch:
diff --git a/tests/lib/rules/define-emits-declaration.js b/tests/lib/rules/define-emits-declaration.js
new file mode 100644
index 00000000..279d8049
--- /dev/null
+++ b/tests/lib/rules/define-emits-declaration.js
@@ -0,0 +1,151 @@
+/**
+ * @author Amorites
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+const RuleTester = require('eslint').RuleTester
+const rule = require('../../../lib/rules/define-emits-declaration')
+
+const tester = new RuleTester({
+  parser: require.resolve('vue-eslint-parser'),
+  parserOptions: {
+    ecmaVersion: 2020,
+    sourceType: 'module'
+  }
+})
+
+tester.run('define-emits-declaration', rule, {
+  valid: [
+    {
+      filename: 'test.vue',
+      code: `
+        <script setup>
+          const emit = defineEmits(['change', 'update'])
+        </script>
+       `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <script setup lang="ts">
+        const emit = defineEmits<{
+          (e: 'change', id: number): void
+          (e: 'update', value: string): void
+        }>()
+        </script>
+       `,
+      parserOptions: {
+        parser: require.resolve('@typescript-eslint/parser')
+      }
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <script setup lang="ts">
+        const emit = defineEmits<{
+          (e: 'change', id: number): void
+          (e: 'update', value: string): void
+        }>()
+        </script>
+       `,
+      parserOptions: {
+        parser: require.resolve('@typescript-eslint/parser')
+      },
+      options: ['type-based']
+    },
+    {
+      filename: 'test.vue',
+      code: `
+       <script setup lang="ts">
+       const emit = defineEmits(['change', 'update'])
+       </script>
+       `,
+      options: ['runtime']
+    },
+    {
+      filename: 'test.vue',
+      // ignore code without defineEmits
+      code: `
+        <script setup lang="ts">
+        const props = defineProps({
+          kind: { type: String },
+        })
+        </script>
+       `,
+      parserOptions: {
+        parser: require.resolve('@typescript-eslint/parser')
+      }
+    },
+    {
+      filename: 'test.vue',
+      code: `
+         <script lang="ts">
+         import { PropType } from 'vue'
+ 
+         export default {
+           props: {
+             kind: { type: String as PropType<'primary' | 'secondary'> },
+           },
+           emits: ['check']
+         }
+         </script>
+       `,
+      parserOptions: {
+        parser: require.resolve('@typescript-eslint/parser')
+      }
+    }
+  ],
+  invalid: [
+    {
+      filename: 'test.vue',
+      code: `
+       <script setup lang="ts">
+       const emit = defineEmits(['change', 'update'])
+       </script>
+       `,
+      errors: [
+        {
+          message: 'Use type-based declaration instead of runtime declaration.',
+          line: 3
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+       <script setup lang="ts">
+       const emit = defineEmits(['change', 'update'])
+       </script>
+       `,
+      errors: [
+        {
+          message: 'Use type-based declaration instead of runtime declaration.',
+          line: 3
+        }
+      ],
+      options: ['type-based']
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <script setup lang="ts">
+        const emit = defineEmits<{
+          (e: 'change', id: number): void
+          (e: 'update', value: string): void
+        }>()
+        </script>
+       `,
+      parserOptions: {
+        parser: require.resolve('@typescript-eslint/parser')
+      },
+      errors: [
+        {
+          message: 'Use runtime declaration instead of type-based declaration.',
+          line: 3
+        }
+      ],
+      options: ['runtime']
+    }
+  ]
+})
diff --git a/tests/lib/rules/define-props-declaration.js b/tests/lib/rules/define-props-declaration.js
new file mode 100644
index 00000000..67ea338e
--- /dev/null
+++ b/tests/lib/rules/define-props-declaration.js
@@ -0,0 +1,157 @@
+/**
+ * @author Amorites
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+const RuleTester = require('eslint').RuleTester
+const rule = require('../../../lib/rules/define-props-declaration')
+
+const tester = new RuleTester({
+  parser: require.resolve('vue-eslint-parser'),
+  parserOptions: {
+    ecmaVersion: 2020,
+    sourceType: 'module'
+  }
+})
+
+tester.run('define-props-declaration', rule, {
+  valid: [
+    {
+      filename: 'test.vue',
+      code: `
+      <script setup lang="ts">
+      const props = defineProps<{
+        kind: string;
+      }>()
+      </script>
+      `,
+      parserOptions: {
+        parser: require.resolve('@typescript-eslint/parser')
+      }
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script setup lang="ts">
+      const props = defineProps<{
+        kind: string;
+      }>()
+      </script>
+      `,
+      parserOptions: {
+        parser: require.resolve('@typescript-eslint/parser')
+      },
+      options: ['type-based']
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script setup lang="ts">
+      const props = defineProps({
+        kind: { type: String },
+      })
+      </script>
+      `,
+      options: ['runtime']
+    },
+    {
+      filename: 'test.vue',
+      // ignore script without lang="ts"
+      code: `
+      <script setup>
+      const props = defineProps({
+        kind: { type: String },
+      })
+      </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script setup lang="ts">
+      const emit = defineEmits({
+        click: (event: PointerEvent) => !!event
+      })
+      </script>
+      `,
+      parserOptions: {
+        parser: require.resolve('@typescript-eslint/parser')
+      }
+    },
+    {
+      filename: 'test.vue',
+      // ignore non-setup script
+      code: `
+        <script lang="ts">
+        import { PropType } from 'vue'
+
+        export default {
+          props: {
+            kind: { type: String as PropType<'primary' | 'secondary'> },
+          },
+          emits: ['check']
+        }
+        </script>
+      `,
+      parserOptions: {
+        parser: require.resolve('@typescript-eslint/parser')
+      }
+    }
+  ],
+  invalid: [
+    {
+      filename: 'test.vue',
+      code: `
+      <script setup lang="ts">
+      const props = defineProps({
+        kind: { type: String },
+      })
+      </script>
+      `,
+      errors: [
+        {
+          message: 'Use type-based declaration instead of runtime declaration.',
+          line: 3
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script setup lang="ts">
+      const props = defineProps({
+        kind: { type: String },
+      })
+      </script>
+      `,
+      errors: [
+        {
+          message: 'Use type-based declaration instead of runtime declaration.',
+          line: 3
+        }
+      ],
+      options: ['type-based']
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script setup lang="ts">
+      const props = defineProps<{
+        kind: string;
+      }>()
+      </script>
+      `,
+      parserOptions: {
+        parser: require.resolve('@typescript-eslint/parser')
+      },
+      options: ['runtime'],
+      errors: [
+        {
+          message: 'Use runtime declaration instead of type-based declaration.',
+          line: 3
+        }
+      ]
+    }
+  ]
+})
diff --git a/tests/lib/rules/padding-line-between-tags.js b/tests/lib/rules/padding-line-between-tags.js
new file mode 100644
index 00000000..18d6743a
--- /dev/null
+++ b/tests/lib/rules/padding-line-between-tags.js
@@ -0,0 +1,1032 @@
+/**
+ * @author dev1437
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+const RuleTester = require('eslint').RuleTester
+const rule = require('../../../lib/rules/padding-line-between-tags')
+
+const tester = new RuleTester({
+  parser: require.resolve('vue-eslint-parser'),
+  parserOptions: {
+    ecmaVersion: 2020,
+    sourceType: 'module'
+  }
+})
+
+tester.run('padding-line-between-tags', rule, {
+  valid: [
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div>
+          <div></div>
+
+          <div>
+          </div>
+
+          <div />
+        </div>
+      </template>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <p>Foo <b>bar</b> baz.</p>
+      </template>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div>
+          <div></div>
+          <div>
+          </div>
+          <div />
+        </div>
+      </template>
+      `,
+      options: [[{ blankLine: 'never', prev: '*', next: '*' }]]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div>
+          <ul>
+            <li>
+            </li>
+            <br />
+            <li>
+            </li>
+          </ul>
+        </div>
+      </template>
+      `,
+      options: [
+        [
+          { blankLine: 'always', prev: '*', next: '*' },
+          { blankLine: 'never', prev: '*', next: 'br' },
+          { blankLine: 'never', prev: 'br', next: '*' }
+        ]
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div>
+          <ul>
+            <li>
+            </li>
+
+            <br />
+            <div></div>
+
+            <li>
+            </li>
+          </ul>
+        </div>
+      </template>
+      `,
+      options: [
+        [
+          { blankLine: 'always', prev: '*', next: '*' },
+          { blankLine: 'never', prev: 'br', next: 'div' }
+        ]
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div>
+          <ul>
+            <li>
+            </li>
+            <br />
+
+            <div></div>
+            <li>
+            </li>
+          </ul>
+        </div>
+      </template>
+      `,
+      options: [
+        [
+          { blankLine: 'never', prev: '*', next: '*' },
+          { blankLine: 'always', prev: 'br', next: 'div' }
+        ]
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div>
+          <ul>
+            <li>
+            </li>
+            <br />
+
+            <img />
+            <li>
+            </li>
+          </ul>
+        </div>
+      </template>
+      `,
+      options: [[{ blankLine: 'always', prev: 'br', next: 'img' }]]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+      </template>
+      <script>
+      </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div>
+          <ul>
+            <li>
+            </li>
+            <br />
+
+            <img />
+            <li>
+            </li>
+          </ul>
+        </div>
+      </template>
+      `,
+      options: [
+        [
+          { blankLine: 'never', prev: 'br', next: 'img' },
+          { blankLine: 'always', prev: 'br', next: 'img' }
+        ]
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div>
+          </div>A
+          B
+          C<div />
+        </div>
+      </template>
+      `,
+      options: [[{ blankLine: 'never', prev: '*', next: '*' }]]
+    }
+  ],
+  invalid: [
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div>
+          <div></div>
+          <div>
+          </div>
+          <div />
+        </div>
+      </template>
+      `,
+      output: `
+      <template>
+        <div>
+          <div></div>
+
+          <div>
+          </div>
+
+          <div />
+        </div>
+      </template>
+      `,
+      errors: [
+        {
+          message: 'Expected blank line before this tag.',
+          line: 5,
+          column: 11
+        },
+        {
+          message: 'Expected blank line before this tag.',
+          line: 7,
+          column: 11
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div>
+          <ul>
+            <li>
+            </li>
+            <li>
+            </li>
+            <br />
+            <li>
+            </li>
+          </ul>
+        </div>
+      </template>
+      `,
+      output: `
+      <template>
+        <div>
+          <ul>
+            <li>
+            </li>
+
+            <li>
+            </li>
+
+            <br />
+
+            <li>
+            </li>
+          </ul>
+        </div>
+      </template>
+      `,
+      errors: [
+        {
+          message: 'Expected blank line before this tag.',
+          line: 7,
+          column: 13
+        },
+        {
+          message: 'Expected blank line before this tag.',
+          line: 9,
+          column: 13
+        },
+        {
+          message: 'Expected blank line before this tag.',
+          line: 10,
+          column: 13
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div>
+          <ul>
+          </ul>
+          <ul>
+            <li>
+              Test Text
+            </li>
+          </ul>
+        </div>
+      </template>
+      `,
+      output: `
+      <template>
+        <div>
+          <ul>
+          </ul>
+
+          <ul>
+            <li>
+              Test Text
+            </li>
+          </ul>
+        </div>
+      </template>
+      `,
+      errors: [
+        {
+          message: 'Expected blank line before this tag.',
+          line: 6,
+          column: 11
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div>
+          <ul>
+          </ul>
+          <ul>
+            <li>
+              <p>
+              </p>
+              <p>
+              </p>
+            </li>
+          </ul>
+        </div>
+        <div>
+        </div>
+      </template>
+      `,
+      output: `
+      <template>
+        <div>
+          <ul>
+          </ul>
+
+          <ul>
+            <li>
+              <p>
+              </p>
+
+              <p>
+              </p>
+            </li>
+          </ul>
+        </div>
+
+        <div>
+        </div>
+      </template>
+      `,
+      errors: [
+        {
+          message: 'Expected blank line before this tag.',
+          line: 6,
+          column: 11
+        },
+        {
+          message: 'Expected blank line before this tag.',
+          line: 10,
+          column: 15
+        },
+        {
+          message: 'Expected blank line before this tag.',
+          line: 15,
+          column: 9
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div>
+          <ul>
+            <li>
+            </li>
+            <br />
+            <li>
+            </li>
+          </ul>
+        </div>
+      </template>
+      `,
+      output: `
+      <template>
+        <div>
+          <ul>
+            <li>
+            </li>
+
+            <br />
+            <li>
+            </li>
+          </ul>
+        </div>
+      </template>
+      `,
+      errors: [
+        {
+          message: 'Expected blank line before this tag.',
+          line: 7,
+          column: 13
+        }
+      ],
+      options: [
+        [
+          { blankLine: 'always', prev: '*', next: '*' },
+          { blankLine: 'never', prev: 'br', next: '*' }
+        ]
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div>
+          <ul>
+            <li>
+            </li>
+            <br />
+            <li>
+            </li>
+          </ul>
+        </div>
+      </template>
+      `,
+      output: `
+      <template>
+        <div>
+          <ul>
+            <li>
+            </li>
+            <br />
+
+            <li>
+            </li>
+          </ul>
+        </div>
+      </template>
+      `,
+      errors: [
+        {
+          message: 'Expected blank line before this tag.',
+          line: 8,
+          column: 13
+        }
+      ],
+      options: [
+        [
+          { blankLine: 'always', prev: '*', next: '*' },
+          { blankLine: 'never', prev: '*', next: 'br' }
+        ]
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div>
+          <ul>
+            <li>
+            </li>
+
+
+            <br />
+
+            <li>
+            </li>
+          </ul>
+        </div>
+      </template>
+      `,
+      output: `
+      <template>
+        <div>
+          <ul>
+            <li>
+            </li>
+            <br />
+            <li>
+            </li>
+          </ul>
+        </div>
+      </template>
+      `,
+      errors: [
+        {
+          message: 'Unexpected blank line before this tag.',
+          line: 9,
+          column: 13
+        },
+        {
+          message: 'Unexpected blank line before this tag.',
+          line: 11,
+          column: 13
+        }
+      ],
+      options: [[{ blankLine: 'never', prev: '*', next: '*' }]]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div>
+          <div></div>
+
+          <div>
+          </div>
+
+          <div />
+        </div>
+      </template>
+      `,
+      output: `
+      <template>
+        <div>
+          <div></div>
+          <div>
+          </div>
+          <div />
+        </div>
+      </template>
+      `,
+      errors: [
+        {
+          message: 'Unexpected blank line before this tag.',
+          line: 6,
+          column: 11
+        },
+        {
+          message: 'Unexpected blank line before this tag.',
+          line: 9,
+          column: 11
+        }
+      ],
+      options: [[{ blankLine: 'never', prev: '*', next: '*' }]]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div>
+          <ul>
+            <li>
+            </li>
+            <br />
+            <div></div>
+            <li>
+            </li>
+          </ul>
+        </div>
+      </template>
+      `,
+      output: `
+      <template>
+        <div>
+          <ul>
+            <li>
+            </li>
+            <br />
+
+            <div></div>
+            <li>
+            </li>
+          </ul>
+        </div>
+      </template>
+      `,
+      errors: [
+        {
+          message: 'Expected blank line before this tag.',
+          line: 8,
+          column: 13
+        }
+      ],
+      options: [
+        [
+          { blankLine: 'never', prev: '*', next: '*' },
+          { blankLine: 'always', prev: 'br', next: 'div' }
+        ]
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div>
+          <h1></h1>
+          <br />
+          <div></div>
+          <br />
+          <img />
+          <h1></h1>
+        </div>
+      </template>
+      `,
+      output: `
+      <template>
+        <div>
+          <h1></h1>
+
+          <br />
+          <div></div>
+
+          <br />
+          <img />
+
+          <h1></h1>
+        </div>
+      </template>
+      `,
+      errors: [
+        {
+          message: 'Expected blank line before this tag.',
+          line: 5,
+          column: 11
+        },
+        {
+          message: 'Expected blank line before this tag.',
+          line: 7,
+          column: 11
+        },
+        {
+          message: 'Expected blank line before this tag.',
+          line: 9,
+          column: 11
+        }
+      ],
+      options: [
+        [
+          { blankLine: 'always', prev: '*', next: '*' },
+          { blankLine: 'never', prev: 'br', next: 'div' },
+          { blankLine: 'never', prev: 'br', next: 'img' }
+        ]
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div>
+          <img />
+          <br />
+
+          <div></div>
+          <br />
+          <ul></ul>
+        </div>
+      </template>
+      `,
+      output: `
+      <template>
+        <div>
+          <img />
+          <br />
+
+          <div></div>
+
+          <br />
+          <ul></ul>
+        </div>
+      </template>
+      `,
+      errors: [
+        {
+          message: 'Expected blank line before this tag.',
+          line: 8,
+          column: 11
+        }
+      ],
+      options: [
+        [
+          { blankLine: 'always', prev: 'br', next: 'div' },
+          { blankLine: 'always', prev: 'div', next: 'br' }
+        ]
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div>
+          <img />
+          <br />
+
+          <div></div>
+          <br />
+          <br />
+        </div>
+      </template>
+      `,
+      output: `
+      <template>
+        <div>
+          <img />
+          <br />
+
+          <div></div>
+          <br />
+
+          <br />
+        </div>
+      </template>
+      `,
+      errors: [
+        {
+          message: 'Expected blank line before this tag.',
+          line: 9,
+          column: 11
+        }
+      ],
+      options: [
+        [
+          { blankLine: 'always', prev: 'br', next: 'div' },
+          { blankLine: 'always', prev: 'br', next: 'br' }
+        ]
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div>
+          <img />
+          <br />
+
+          <div></div>
+          <br />
+
+          <br />
+        </div>
+      </template>
+      `,
+      output: `
+      <template>
+        <div>
+          <img />
+
+          <br />
+
+          <div></div>
+
+          <br />
+          <br />
+        </div>
+      </template>
+      `,
+      errors: [
+        {
+          message: 'Expected blank line before this tag.',
+          line: 5,
+          column: 11
+        },
+        {
+          message: 'Expected blank line before this tag.',
+          line: 8,
+          column: 11
+        },
+        {
+          message: 'Unexpected blank line before this tag.',
+          line: 10,
+          column: 11
+        }
+      ],
+      options: [
+        [
+          { blankLine: 'always', prev: '*', next: '*' },
+          { blankLine: 'never', prev: 'br', next: 'br' }
+        ]
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div>
+          <img />
+          <br />
+
+          <div></div>
+
+          <br />
+
+          <br />
+        </div>
+      </template>
+      `,
+      output: `
+      <template>
+        <div>
+          <img />
+          <br />
+
+          <div></div>
+
+          <br />
+          <br />
+        </div>
+      </template>
+      `,
+      errors: [
+        {
+          message: 'Unexpected blank line before this tag.',
+          line: 11,
+          column: 11
+        }
+      ],
+      options: [[{ blankLine: 'never', prev: 'br', next: 'br' }]]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div>
+          <div></div>
+          <div>
+          </div>
+          <br />
+          <div />
+        </div>
+      </template>
+      `,
+      output: `
+      <template>
+        <div>
+          <div></div>
+          <div>
+          </div>
+
+          <br />
+          <div />
+        </div>
+      </template>
+      `,
+      errors: [
+        {
+          message: 'Expected blank line before this tag.',
+          line: 7,
+          column: 11
+        }
+      ],
+      options: [[{ blankLine: 'always', prev: '*', next: 'br' }]]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div>
+          <div /><div />
+        </div>
+      </template>
+      `,
+      output: `
+      <template>
+        <div>
+          <div />
+
+          <div />
+        </div>
+      </template>
+      `,
+      errors: [
+        {
+          message: 'Expected blank line before this tag.',
+          line: 4,
+          column: 18
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div>
+          Foo  <div /><div />
+        </div>
+      </template>
+      `,
+      output: `
+      <template>
+        <div>
+          Foo  <div />
+
+          <div />
+        </div>
+      </template>
+      `,
+      errors: [
+        {
+          message: 'Expected blank line before this tag.',
+          line: 4,
+          column: 23
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div>
+          </div>A
+
+          C<div />
+        </div>
+      </template>
+      `,
+      output: `
+      <template>
+        <div>
+          </div>A
+          C<div />
+        </div>
+      </template>
+      `,
+      errors: [
+        {
+          message: 'Unexpected blank line before this tag.',
+          line: 6,
+          column: 12
+        }
+      ],
+      options: [[{ blankLine: 'never', prev: '*', next: '*' }]]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div>
+          </div>A
+          B
+
+          C<div />
+        </div>
+      </template>
+      `,
+      output: `
+      <template>
+        <div>
+          </div>A
+          B
+          C<div />
+        </div>
+      </template>
+      `,
+      errors: [
+        {
+          message: 'Unexpected blank line before this tag.',
+          line: 7,
+          column: 12
+        }
+      ],
+      options: [[{ blankLine: 'never', prev: '*', next: '*' }]]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div>
+          </div>A
+
+
+          B
+          C<div />
+        </div>
+      </template>
+      `,
+      output: `
+      <template>
+        <div>
+          </div>A
+          B
+          C<div />
+        </div>
+      </template>
+      `,
+      errors: [
+        {
+          message: 'Unexpected blank line before this tag.',
+          line: 8,
+          column: 12
+        }
+      ],
+      options: [[{ blankLine: 'never', prev: '*', next: '*' }]]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div>
+          </div>A
+
+
+          B
+
+
+          C<div />
+        </div>
+      </template>
+      `,
+      output: `
+      <template>
+        <div>
+          </div>A
+          B
+          C<div />
+        </div>
+      </template>
+      `,
+      errors: [
+        {
+          message: 'Unexpected blank line before this tag.',
+          line: 10,
+          column: 12
+        }
+      ],
+      options: [[{ blankLine: 'never', prev: '*', next: '*' }]]
+    }
+  ]
+})