Repository: vuejs/eslint-plugin-vue
Pull request #1966 (2022-09-10 07:19:26)

Create `padding-line-between-tags` rule
Add rule for #1832 

FloEdelmann: Could you please have a look at the CI failures?

* https://app.circleci.com/pipelines/github/vuejs/eslint-plugin-vue/1666/workflows/9f5355a9-5677-4568-9930-23bd379ec8e3/jobs/12552
* https://app.netlify.com/sites/eslint-plugin-vue/deploys/631c3a7fd3c155000899f827#L91
dev1437: Anything else? Let me know @ota-meshi @FloEdelmann thank you

Patch:
diff --git a/docs/.vuepress/components/eslint-code-block.vue b/docs/.vuepress/components/eslint-code-block.vue
index f71e606a..b95e0d07 100644
--- a/docs/.vuepress/components/eslint-code-block.vue
+++ b/docs/.vuepress/components/eslint-code-block.vue
@@ -3,7 +3,7 @@
     <eslint-editor
       :linter="linter"
       :config="config"
-      :code="code"
+      v-model="code"
       :style="{ height }"
       class="eslint-code-block"
       :filename="filename"
@@ -43,23 +43,50 @@ export default {
     language: {
       type: String,
       default: 'html'
+    },
+    /**
+     * If enabled, `@typescript-eslint/parser` will be used.
+     * This must be enabled when used for `ts` code blocks.
+     */
+    typescript: {
+      type: Boolean,
+      default: false
     }
   },
 
   data() {
+    const code = this.computeCodeFromSlot()
+    // The height is determined in the initial processing.
+    // This is because later code changes do not change the height.
+    const lines = code.split('\n').length
+    const height = `${Math.max(120, 19 * lines)}px`
     return {
+      code,
+      height,
       linter: null,
       preprocess: processors['.vue'].preprocess,
       postprocess: processors['.vue'].postprocess,
       format: {
         insertSpaces: true,
         tabSize: 2
-      }
+      },
+      tsEslintParser: null
     }
   },
 
   computed: {
     config() {
+      let parser = null // Use default parser (`espree`)
+      if (this.typescript) {
+        // Use `@typescript-eslint/parser`.
+        parser = this.tsEslintParser
+      } else if (this.langTs) {
+        // Use `@typescript-eslint/parser` only when `<script lang="ts">` or `<script lang="typescript">`.
+        parser = {
+          ts: this.tsEslintParser,
+          typescript: this.tsEslintParser
+        }
+      }
       return {
         globals: {
           console: false,
@@ -90,6 +117,7 @@ export default {
         rules: this.rules,
         parser: 'vue-eslint-parser',
         parserOptions: {
+          parser,
           ecmaVersion: 'latest',
           sourceType: 'module',
           ecmaFeatures: {
@@ -99,24 +127,37 @@ export default {
       }
     },
 
-    code() {
-      return `${this.computeCodeFromSlot(this.$slots.default).trim()}\n`
-    },
+    /**
+     * Checks whether code may be using lang="ts" or lang="typescript".
+     * @returns {boolean} If `true`, may be using lang="ts" or lang="typescript".
+     */
+    langTs() {
+      return /lang\s*=\s*(?:"ts"|ts|'ts'|"typescript"|typescript|'typescript')/u.test(
+        this.code
+      )
+    }
+  },
 
-    height() {
-      const lines = this.code.split('\n').length
-      return `${Math.max(120, 19 * lines)}px`
+  watch: {
+    typescript(value) {
+      if (value) {
+        this.loadTypescriptESLint()
+      }
+    },
+    langTs(value) {
+      if (value) {
+        this.loadTypescriptESLint()
+      }
     }
   },
 
   methods: {
-    computeCodeFromSlot(nodes) {
-      if (!Array.isArray(nodes)) {
-        return ''
-      }
-      return nodes
-        .map((node) => node.text || this.computeCodeFromSlot(node.children))
-        .join('')
+    computeCodeFromSlot() {
+      return `${computeCodeFromSlot(this.$slots.default).trim()}\n`
+    },
+
+    async loadTypescriptESLint() {
+      this.tsEslintParser = await import('@typescript-eslint/parser')
     }
   },
 
@@ -126,6 +167,9 @@ export default {
       import('eslint/lib/linter'),
       import('espree').then(() => import('vue-eslint-parser'))
     ])
+    if (this.langTs || this.typescript) {
+      await this.loadTypescriptESLint()
+    }
 
     const linter = (this.linter = new Linter())
 
@@ -136,6 +180,15 @@ export default {
     linter.defineParser('vue-eslint-parser', { parseForESLint })
   }
 }
+
+function computeCodeFromSlot(nodes) {
+  if (!Array.isArray(nodes)) {
+    return ''
+  }
+  return nodes
+    .map((node) => node.text || computeCodeFromSlot(node.children))
+    .join('')
+}
 </script>
 
 <style>
diff --git a/docs/.vuepress/config.js b/docs/.vuepress/config.js
index 68aec1af..38ad965e 100644
--- a/docs/.vuepress/config.js
+++ b/docs/.vuepress/config.js
@@ -132,7 +132,8 @@ module.exports = {
           '@eslint/eslintrc/universal': path.resolve(
             __dirname,
             '../../node_modules/@eslint/eslintrc/dist/eslintrc-universal.cjs'
-          )
+          ),
+          globby: require.resolve('./shim/globby')
         }
       }
     }
diff --git a/docs/.vuepress/enhanceApp.js b/docs/.vuepress/enhanceApp.js
index 03082d60..1e62f60a 100644
--- a/docs/.vuepress/enhanceApp.js
+++ b/docs/.vuepress/enhanceApp.js
@@ -12,7 +12,7 @@ export default (
     window.process = new Proxy(
       {
         env: {},
-        cwd: () => undefined
+        cwd: () => ''
       },
       {
         get(target, name) {
diff --git a/docs/.vuepress/shim/globby.js b/docs/.vuepress/shim/globby.js
new file mode 100644
index 00000000..4ba52ba2
--- /dev/null
+++ b/docs/.vuepress/shim/globby.js
@@ -0,0 +1,1 @@
+module.exports = {}
diff --git a/docs/.vuepress/shim/module.js b/docs/.vuepress/shim/module.js
index 66ab9785..cfeccf31 100644
--- a/docs/.vuepress/shim/module.js
+++ b/docs/.vuepress/shim/module.js
@@ -1,3 +1,11 @@
 module.exports = {
-  createRequire: () => () => null
+  createRequire: () => (module) => {
+    if (module === 'espree') {
+      return require('espree')
+    }
+    if (module === 'eslint-scope') {
+      return require('eslint-scope')
+    }
+    throw new Error(`Not implemented: ${module}`)
+  }
 }
diff --git a/docs/rules/README.md b/docs/rules/README.md
index 4343ba64..7c2b8c50 100644
--- a/docs/rules/README.md
+++ b/docs/rules/README.md
@@ -227,6 +227,7 @@ For example:
 | [vue/no-empty-component-block](./no-empty-component-block.md) | disallow the `<template>` `<script>` `<style>` block to be empty |  | :hammer: |
 | [vue/no-multiple-objects-in-class](./no-multiple-objects-in-class.md) | disallow to pass multiple objects into array to class |  | :hammer: |
 | [vue/no-potential-component-option-typo](./no-potential-component-option-typo.md) | disallow a potential typo in your component property | :bulb: | :hammer: |
+| [vue/no-ref-object-destructure](./no-ref-object-destructure.md) | disallow destructuring of ref objects that can lead to loss of reactivity |  | :warning: |
 | [vue/no-restricted-block](./no-restricted-block.md) | disallow specific block |  | :hammer: |
 | [vue/no-restricted-call-after-await](./no-restricted-call-after-await.md) | disallow asynchronously called restricted methods |  | :hammer: |
 | [vue/no-restricted-class](./no-restricted-class.md) | disallow specific classes in Vue components |  | :warning: |
@@ -257,6 +258,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/no-ref-object-destructure.md b/docs/rules/no-ref-object-destructure.md
new file mode 100644
index 00000000..4c4b837a
--- /dev/null
+++ b/docs/rules/no-ref-object-destructure.md
@@ -0,0 +1,55 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/no-ref-object-destructure
+description: disallow destructuring of ref objects that can lead to loss of reactivity
+---
+# vue/no-ref-object-destructure
+
+> disallow destructuring of ref objects that can lead to loss of reactivity
+
+- :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 reports the destructuring of ref objects causing the value to lose reactivity.
+
+<eslint-code-block :rules="{'vue/no-ref-object-destructure': ['error']}" language="javascript" filename="example.js" >
+
+```js
+import { ref } from 'vue'
+const count = ref(0)
+const v1 = count.value /* ✗ BAD */
+const { value: v2 } = count /* ✗ BAD */
+const v3 = computed(() => count.value /* ✓ GOOD */)
+const v4 = fn(count.value) /* ✗ BAD */
+const v5 = fn(count) /* ✓ GOOD */
+const v6 = computed(() => fn(count.value) /* ✓ GOOD */)
+```
+
+</eslint-code-block>
+
+This rule also supports Reactivity Transform, but Reactivity Transform is an experimental feature and may have false positives due to future Vue changes.  
+See the [RFC](https://github.com/vuejs/rfcs/pull/420) for more information on Reactivity Transform.
+
+<eslint-code-block :rules="{'vue/no-ref-object-destructure': ['error']}" language="javascript" filename="example.js" >
+
+```js
+const count = $ref(0)
+const v1 = count /* ✗ BAD */
+const v2 = $computed(() => count /* ✓ GOOD */)
+const v3 = fn(count) /* ✗ BAD */
+const v4 = fn($$(count)) /* ✓ GOOD */
+const v5 = $computed(() => fn(count) /* ✓ GOOD */)
+```
+
+</eslint-code-block>
+
+## :wrench: Options
+
+Nothing.
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-ref-object-destructure.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-ref-object-destructure.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 5718794c..007a584e 100644
--- a/docs/rules/valid-define-emits.md
+++ b/docs/rules/valid-define-emits.md
@@ -45,6 +45,8 @@ This rule reports `defineEmits` compiler macros in the following cases:
 
 </eslint-code-block>
 
+<eslint-code-block :rules="{'vue/valid-define-emits': ['error']}">
+
 ```vue
 <script setup lang="ts">
   /* ✓ GOOD */
@@ -52,6 +54,8 @@ This rule reports `defineEmits` compiler macros in the following cases:
 </script>
 ```
 
+</eslint-code-block>
+
 <eslint-code-block :rules="{'vue/valid-define-emits': ['error']}">
 
 ```vue
@@ -78,6 +82,8 @@ This rule reports `defineEmits` compiler macros in the following cases:
 
 </eslint-code-block>
 
+<eslint-code-block :rules="{'vue/valid-define-emits': ['error']}">
+
 ```vue
 <script setup lang="ts">
   /* ✗ BAD */
@@ -85,6 +91,8 @@ This rule reports `defineEmits` compiler macros in the following cases:
 </script>
 ```
 
+</eslint-code-block>
+
 <eslint-code-block :rules="{'vue/valid-define-emits': ['error']}">
 
 ```vue
diff --git a/docs/rules/valid-define-props.md b/docs/rules/valid-define-props.md
index bd7d5f75..960573bb 100644
--- a/docs/rules/valid-define-props.md
+++ b/docs/rules/valid-define-props.md
@@ -45,6 +45,8 @@ This rule reports `defineProps` compiler macros in the following cases:
 
 </eslint-code-block>
 
+<eslint-code-block :rules="{'vue/valid-define-props': ['error']}">
+
 ```vue
 <script setup lang="ts">
   /* ✓ GOOD */
@@ -52,6 +54,8 @@ This rule reports `defineProps` compiler macros in the following cases:
 </script>
 ```
 
+</eslint-code-block>
+
 <eslint-code-block :rules="{'vue/valid-define-props': ['error']}">
 
 ```vue
@@ -78,6 +82,8 @@ This rule reports `defineProps` compiler macros in the following cases:
 
 </eslint-code-block>
 
+<eslint-code-block :rules="{'vue/valid-define-props': ['error']}">
+
 ```vue
 <script setup lang="ts">
   /* ✗ BAD */
@@ -85,6 +91,8 @@ This rule reports `defineProps` compiler macros in the following cases:
 </script>
 ```
 
+</eslint-code-block>
+
 <eslint-code-block :rules="{'vue/valid-define-props': ['error']}">
 
 ```vue
diff --git a/lib/index.js b/lib/index.js
index c4979a98..ca155949 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -104,6 +104,7 @@ module.exports = {
     'no-parsing-error': require('./rules/no-parsing-error'),
     'no-potential-component-option-typo': require('./rules/no-potential-component-option-typo'),
     'no-ref-as-operand': require('./rules/no-ref-as-operand'),
+    'no-ref-object-destructure': require('./rules/no-ref-object-destructure'),
     'no-reserved-component-names': require('./rules/no-reserved-component-names'),
     'no-reserved-keys': require('./rules/no-reserved-keys'),
     'no-reserved-props': require('./rules/no-reserved-props'),
@@ -183,6 +184,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/no-ref-as-operand.js b/lib/rules/no-ref-as-operand.js
index 4f536daf..067be4ec 100644
--- a/lib/rules/no-ref-as-operand.js
+++ b/lib/rules/no-ref-as-operand.js
@@ -3,9 +3,14 @@
  * See LICENSE file in root directory for full license.
  */
 'use strict'
-const { ReferenceTracker, findVariable } = require('eslint-utils')
+
+const { extractRefObjectReferences } = require('../utils/ref-object-references')
 const utils = require('../utils')
 
+/**
+ * @typedef {import('../utils/ref-object-references').RefObjectReferences} RefObjectReferences
+ */
+
 module.exports = {
   meta: {
     type: 'suggestion',
@@ -24,20 +29,14 @@ module.exports = {
   },
   /** @param {RuleContext} context */
   create(context) {
-    /**
-     * @typedef {object} ReferenceData
-     * @property {VariableDeclarator} variableDeclarator
-     * @property {VariableDeclaration | null} variableDeclaration
-     * @property {string} method
-     */
-    /** @type {Map<Identifier, ReferenceData>} */
-    const refReferenceIds = new Map()
+    /** @type {RefObjectReferences} */
+    let refReferences
 
     /**
      * @param {Identifier} node
      */
     function reportIfRefWrapped(node) {
-      const data = refReferenceIds.get(node)
+      const data = refReferences.get(node)
       if (!data) {
         return
       }
@@ -54,59 +53,7 @@ module.exports = {
     }
     return {
       Program() {
-        const tracker = new ReferenceTracker(context.getScope())
-        const traceMap = utils.createCompositionApiTraceMap({
-          [ReferenceTracker.ESM]: true,
-          ref: {
-            [ReferenceTracker.CALL]: true
-          },
-          computed: {
-            [ReferenceTracker.CALL]: true
-          },
-          toRef: {
-            [ReferenceTracker.CALL]: true
-          },
-          customRef: {
-            [ReferenceTracker.CALL]: true
-          },
-          shallowRef: {
-            [ReferenceTracker.CALL]: true
-          }
-        })
-
-        for (const { node, path } of tracker.iterateEsmReferences(traceMap)) {
-          const variableDeclarator = node.parent
-          if (
-            !variableDeclarator ||
-            variableDeclarator.type !== 'VariableDeclarator' ||
-            variableDeclarator.id.type !== 'Identifier'
-          ) {
-            continue
-          }
-          const variable = findVariable(
-            context.getScope(),
-            variableDeclarator.id
-          )
-          if (!variable) {
-            continue
-          }
-          const variableDeclaration =
-            (variableDeclarator.parent &&
-              variableDeclarator.parent.type === 'VariableDeclaration' &&
-              variableDeclarator.parent) ||
-            null
-          for (const reference of variable.references) {
-            if (!reference.isRead()) {
-              continue
-            }
-
-            refReferenceIds.set(reference.identifier, {
-              variableDeclarator,
-              variableDeclaration,
-              method: path[1]
-            })
-          }
-        }
+        refReferences = extractRefObjectReferences(context)
       },
       // if (refValue)
       /** @param {Identifier} node */
@@ -148,11 +95,9 @@ module.exports = {
           return
         }
         // Report only constants.
-        const data = refReferenceIds.get(node)
-        if (!data) {
-          return
-        }
+        const data = refReferences.get(node)
         if (
+          !data ||
           !data.variableDeclaration ||
           data.variableDeclaration.kind !== 'const'
         ) {
diff --git a/lib/rules/no-ref-object-destructure.js b/lib/rules/no-ref-object-destructure.js
new file mode 100644
index 00000000..85754d18
--- /dev/null
+++ b/lib/rules/no-ref-object-destructure.js
@@ -0,0 +1,183 @@
+/**
+ * @author Yosuke Ota <https://github.com/ota-meshi>
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const utils = require('../utils')
+const {
+  extractRefObjectReferences,
+  extractReactiveVariableReferences
+} = require('../utils/ref-object-references')
+
+// ------------------------------------------------------------------------------
+// Helpers
+// ------------------------------------------------------------------------------
+
+/**
+ * @typedef {import('../utils/ref-object-references').RefObjectReferences} RefObjectReferences
+ * @typedef {import('../utils/ref-object-references').RefObjectReference} RefObjectReference
+ */
+
+/**
+ * Checks whether writing assigns a value to the given pattern.
+ * @param {Pattern | AssignmentProperty | Property} node
+ * @returns {boolean}
+ */
+function isUpdate(node) {
+  const parent = node.parent
+  if (parent.type === 'UpdateExpression' && parent.argument === node) {
+    // e.g. `pattern++`
+    return true
+  }
+  if (parent.type === 'AssignmentExpression' && parent.left === node) {
+    // e.g. `pattern = 42`
+    return true
+  }
+  if (
+    (parent.type === 'Property' && parent.value === node) ||
+    parent.type === 'ArrayPattern' ||
+    (parent.type === 'ObjectPattern' &&
+      parent.properties.includes(/** @type {any} */ (node))) ||
+    (parent.type === 'AssignmentPattern' && parent.left === node) ||
+    parent.type === 'RestElement' ||
+    (parent.type === 'MemberExpression' && parent.object === node)
+  ) {
+    return isUpdate(parent)
+  }
+  return false
+}
+
+// ------------------------------------------------------------------------------
+// Rule Definition
+// ------------------------------------------------------------------------------
+
+module.exports = {
+  meta: {
+    type: 'problem',
+    docs: {
+      description:
+        'disallow destructuring of ref objects that can lead to loss of reactivity',
+      categories: undefined,
+      url: 'https://eslint.vuejs.org/rules/no-ref-object-destructure.html'
+    },
+    fixable: null,
+    schema: [],
+    messages: {
+      getValueInSameScope:
+        'Getting a value from the ref object in the same scope will cause the value to lose reactivity.',
+      getReactiveVariableInSameScope:
+        'Getting a reactive variable in the same scope will cause the value to lose reactivity.'
+    }
+  },
+  /**
+   * @param {RuleContext} context
+   * @returns {RuleListener}
+   */
+  create(context) {
+    /**
+     * @typedef {object} ScopeStack
+     * @property {ScopeStack | null} upper
+     * @property {Program | FunctionExpression | FunctionDeclaration | ArrowFunctionExpression} node
+     */
+    /** @type {ScopeStack} */
+    let scopeStack = { upper: null, node: context.getSourceCode().ast }
+    /** @type {Map<CallExpression, ScopeStack>} */
+    const scopes = new Map()
+
+    const refObjectReferences = extractRefObjectReferences(context)
+    const reactiveVariableReferences =
+      extractReactiveVariableReferences(context)
+
+    /**
+     * Verify the given ref object value. `refObj = ref(); refObj.value;`
+     * @param {Expression | Super | ObjectPattern} node
+     */
+    function verifyRefObjectValue(node) {
+      const ref = refObjectReferences.get(node)
+      if (!ref) {
+        return
+      }
+      if (scopes.get(ref.define) !== scopeStack) {
+        // Not in the same scope
+        return
+      }
+
+      context.report({
+        node,
+        messageId: 'getValueInSameScope'
+      })
+    }
+
+    /**
+     * Verify the given reactive variable. `refVal = $ref(); refVal;`
+     * @param {Identifier} node
+     */
+    function verifyReactiveVariable(node) {
+      const ref = reactiveVariableReferences.get(node)
+      if (!ref || ref.escape) {
+        return
+      }
+      if (scopes.get(ref.define) !== scopeStack) {
+        // Not in the same scope
+        return
+      }
+
+      context.report({
+        node,
+        messageId: 'getReactiveVariableInSameScope'
+      })
+    }
+
+    return {
+      ':function'(node) {
+        scopeStack = { upper: scopeStack, node }
+      },
+      ':function:exit'() {
+        scopeStack = scopeStack.upper || scopeStack
+      },
+      CallExpression(node) {
+        scopes.set(node, scopeStack)
+      },
+      /**
+       * Check for `refObj.value`.
+       */
+      'MemberExpression:exit'(node) {
+        if (isUpdate(node)) {
+          // e.g. `refObj.value = 42`, `refObj.value++`
+          return
+        }
+        const name = utils.getStaticPropertyName(node)
+        if (name !== 'value') {
+          return
+        }
+        verifyRefObjectValue(node.object)
+      },
+      /**
+       * Check for `{value} = refObj`.
+       */
+      'ObjectPattern:exit'(node) {
+        const prop = utils.findAssignmentProperty(node, 'value')
+        if (!prop) {
+          return
+        }
+        verifyRefObjectValue(node)
+      },
+      /**
+       * Check for reactive variable`.
+       * @param {Identifier} node
+       */
+      'Identifier:exit'(node) {
+        if (isUpdate(node)) {
+          // e.g. `reactiveVariable = 42`, `reactiveVariable++`
+          return
+        }
+        verifyReactiveVariable(node)
+      }
+    }
+  }
+}
diff --git a/lib/rules/no-undef-components.js b/lib/rules/no-undef-components.js
index f6e66400..dc58fd2e 100644
--- a/lib/rules/no-undef-components.js
+++ b/lib/rules/no-undef-components.js
@@ -50,7 +50,9 @@ module.exports = {
       }
     ],
     messages: {
-      undef: "The '<{{name}}>' component has been used, but not defined."
+      undef: "The '<{{name}}>' component has been used, but not defined.",
+      typeOnly:
+        "The '<{{name}}>' component has been used, but '{{name}}' only refers to a type."
     }
   },
   /** @param {RuleContext} context */
@@ -121,6 +123,7 @@ module.exports = {
       // For <script setup>
       /** @type {Set<string>} */
       const scriptVariableNames = new Set()
+      const scriptTypeOnlyNames = new Set()
       const globalScope = context.getSourceCode().scopeManager.globalScope
       if (globalScope) {
         for (const variable of globalScope.variables) {
@@ -130,7 +133,11 @@ module.exports = {
           (scope) => scope.type === 'module'
         )
         for (const variable of (moduleScope && moduleScope.variables) || []) {
-          scriptVariableNames.add(variable.name)
+          if (variable.isTypeVariable) {
+            scriptTypeOnlyNames.add(variable.name)
+          } else {
+            scriptVariableNames.add(variable.name)
+          }
         }
       }
       /**
@@ -167,7 +174,7 @@ module.exports = {
 
         context.report({
           node: reportNode,
-          messageId: 'undef',
+          messageId: scriptTypeOnlyNames.has(rawName) ? 'typeOnly' : 'undef',
           data: {
             name: rawName
           }
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)
+    })
+  }
+}
diff --git a/lib/utils/property-references.js b/lib/utils/property-references.js
index beb3118d..d1b1e6f6 100644
--- a/lib/utils/property-references.js
+++ b/lib/utils/property-references.js
@@ -15,11 +15,23 @@ const eslintUtils = require('eslint-utils')
  * @typedef {object} IHasPropertyOption
  * @property {boolean} [unknownCallAsAny]
  */
+/**
+ * @typedef {object} NestPropertyNodeForExpression
+ * @property {'expression'} type
+ * @property {MemberExpression} node
+ *
+ * @typedef {object} NestPropertyNodeForPattern
+ * @property {'pattern'} type
+ * @property {MemberExpression | Identifier | ObjectPattern | ArrayPattern} node
+ *
+ * @typedef {NestPropertyNodeForExpression | NestPropertyNodeForPattern} NestPropertyNode
+ */
 /**
  * @typedef {object} IPropertyReferences
  * @property { (name: string, option?: IHasPropertyOption) => boolean } hasProperty
- * @property { () => Map<string, {nodes:ASTNode[]}> } allProperties
- * @property { (name: string) => IPropertyReferences } getNest
+ * @property { () => Map<string, {nodes:ASTNode[]}> } allProperties Get all properties.
+ * @property { (name: string) => IPropertyReferences } getNest Get the nesting property references.
+ * @property { (name: string) => Iterable<NestPropertyNode> } getNestNodes Get the nesting property nodes.
  */
 
 // ------------------------------------------------------------------------------
@@ -30,14 +42,16 @@ const eslintUtils = require('eslint-utils')
 const ANY = {
   hasProperty: () => true,
   allProperties: () => new Map(),
-  getNest: () => ANY
+  getNest: () => ANY,
+  getNestNodes: () => []
 }
 
 /** @type {IPropertyReferences} */
 const NEVER = {
   hasProperty: () => false,
   allProperties: () => new Map(),
-  getNest: () => NEVER
+  getNest: () => NEVER,
+  getNestNodes: () => []
 }
 
 /**
@@ -137,7 +151,7 @@ function definePropertyReferenceExtractor(context) {
 
   /**
    * Collects the property references for member expr.
-   * @implements IPropertyReferences
+   * @implements {IPropertyReferences}
    */
   class PropertyReferencesForMember {
     /**
@@ -172,10 +186,23 @@ function definePropertyReferenceExtractor(context) {
         ? extractFromExpression(this.node, this.withInTemplate)
         : NEVER
     }
+
+    /**
+     * @param {string} name
+     * @returns {Iterable<NestPropertyNodeForExpression>}
+     */
+    *getNestNodes(name) {
+      if (name === this.name) {
+        yield {
+          type: 'expression',
+          node: this.node
+        }
+      }
+    }
   }
   /**
    * Collects the property references for object.
-   * @implements IPropertyReferences
+   * @implements {IPropertyReferences}
    */
   class PropertyReferencesForObject {
     constructor() {
@@ -210,6 +237,35 @@ function definePropertyReferenceExtractor(context) {
           )
         : NEVER
     }
+
+    /**
+     * @param {string} name
+     * @returns {Iterable<NestPropertyNodeForPattern>}
+     */
+    *getNestNodes(name) {
+      const properties = this.properties[name]
+      if (!properties) {
+        return
+      }
+      const values = properties.map((property) => property.value)
+      let node
+      while ((node = values.shift())) {
+        if (
+          node.type === 'Identifier' ||
+          node.type === 'MemberExpression' ||
+          node.type === 'ObjectPattern' ||
+          node.type === 'ArrayPattern'
+        ) {
+          yield {
+            type: 'pattern',
+            node
+          }
+        } else if (node.type === 'AssignmentPattern') {
+          values.unshift(node.left)
+        }
+      }
+      return properties ? properties.map((p) => p.value) : []
+    }
   }
 
   /**
@@ -249,7 +305,9 @@ function definePropertyReferenceExtractor(context) {
       switch (parent.type) {
         case 'AssignmentExpression': {
           // `({foo} = arg)`
-          return !withInTemplate && parent.right === node
+          return !withInTemplate &&
+            parent.right === node &&
+            parent.operator === '='
             ? extractFromPattern(parent.left)
             : NEVER
         }
@@ -404,7 +462,8 @@ function definePropertyReferenceExtractor(context) {
           return Boolean(options && options.unknownCallAsAny)
         },
         allProperties: () => new Map(),
-        getNest: () => ANY
+        getNest: () => ANY,
+        getNestNodes: () => []
       }
     }
 
@@ -422,7 +481,8 @@ function definePropertyReferenceExtractor(context) {
           return Boolean(options && options.unknownCallAsAny)
         },
         allProperties: () => new Map(),
-        getNest: () => ANY
+        getNest: () => ANY,
+        getNestNodes: () => []
       }
     }
 
@@ -477,7 +537,8 @@ function definePropertyReferenceExtractor(context) {
         hasProperty: (name) => name === segmentName,
         allProperties: () => new Map([[segmentName, { nodes: [node] }]]),
         getNest: (name) =>
-          name === segmentName ? extractFromSegments(segments.slice(1)) : NEVER
+          name === segmentName ? extractFromSegments(segments.slice(1)) : NEVER,
+        getNestNodes: () => []
       }
     }
   }
@@ -496,7 +557,8 @@ function definePropertyReferenceExtractor(context) {
       ? {
           hasProperty: (name) => name === referenceName,
           allProperties: () => new Map([[referenceName, { nodes: [node] }]]),
-          getNest: (name) => (name === referenceName ? ANY : NEVER)
+          getNest: (name) => (name === referenceName ? ANY : NEVER),
+          getNestNodes: () => []
         }
       : NEVER
   }
@@ -513,7 +575,8 @@ function definePropertyReferenceExtractor(context) {
       hasProperty: (name) => name === referenceName,
       allProperties: () => new Map([[referenceName, { nodes: [nameNode] }]]),
       getNest: (name) =>
-        name === referenceName ? (getNest ? getNest() : ANY) : NEVER
+        name === referenceName ? (getNest ? getNest() : ANY) : NEVER,
+      getNestNodes: () => []
     }
   }
 
@@ -548,7 +611,8 @@ function definePropertyReferenceExtractor(context) {
     return {
       hasProperty: (name, option) => base.hasProperty(name, option),
       allProperties: () => base.allProperties(),
-      getNest: (name) => base.getNest(name).getNest('value')
+      getNest: (name) => base.getNest(name).getNest('value'),
+      getNestNodes: (name) => base.getNest(name).getNestNodes('value')
     }
   }
 
@@ -634,7 +698,7 @@ function mergePropertyReferences(references) {
 
 /**
  * Collects the property references for merge.
- * @implements IPropertyReferences
+ * @implements {IPropertyReferences}
  */
 class PropertyReferencesForMerge {
   /**
@@ -681,4 +745,16 @@ class PropertyReferencesForMerge {
     }
     return mergePropertyReferences(nest)
   }
+
+  /**
+   * @param {string} name
+   * @returns {Iterable<NestPropertyNode>}
+   */
+  *getNestNodes(name) {
+    for (const ref of this.references) {
+      if (ref.hasProperty(name)) {
+        yield* ref.getNestNodes(name)
+      }
+    }
+  }
 }
diff --git a/lib/utils/ref-object-references.js b/lib/utils/ref-object-references.js
new file mode 100644
index 00000000..1b3781bb
--- /dev/null
+++ b/lib/utils/ref-object-references.js
@@ -0,0 +1,621 @@
+/**
+ * @author Yosuke Ota
+ * @copyright 2022 Yosuke Ota. All rights reserved.
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+const utils = require('./index')
+const eslintUtils = require('eslint-utils')
+const { definePropertyReferenceExtractor } = require('./property-references')
+const { ReferenceTracker } = eslintUtils
+
+// ------------------------------------------------------------------------------
+// Helpers
+// ------------------------------------------------------------------------------
+
+/**
+ * @typedef {object} RefObjectReferenceForExpression
+ * @property {'expression'} type
+ * @property {MemberExpression | CallExpression} node
+ * @property {string} method
+ * @property {CallExpression} define
+ *
+ * @typedef {object} RefObjectReferenceForPattern
+ * @property {'pattern'} type
+ * @property {ObjectPattern} node
+ * @property {string} method
+ * @property {CallExpression} define
+ *
+ * @typedef {object} RefObjectReferenceForIdentifier
+ * @property {'expression' | 'pattern'} type
+ * @property {Identifier} node
+ * @property {VariableDeclaration | null} variableDeclaration
+ * @property {string} method
+ * @property {CallExpression} define
+ *
+ * @typedef {RefObjectReferenceForIdentifier | RefObjectReferenceForExpression | RefObjectReferenceForPattern} RefObjectReference
+ */
+/**
+ * @typedef {object} ReactiveVariableReference
+ * @property {Identifier} node
+ * @property {boolean} escape Within escape hint (`$$()`)
+ * @property {VariableDeclaration} variableDeclaration
+ * @property {string} method
+ * @property {CallExpression} define
+ */
+
+/**
+ * @typedef {object} RefObjectReferences
+ * @property {<T extends Identifier | Expression | Pattern | Super> (node: T) =>
+ *   T extends Identifier ?
+ *     RefObjectReferenceForIdentifier | null :
+ *   T extends Expression ?
+ *     RefObjectReferenceForExpression | null :
+ *   T extends Pattern ?
+ *     RefObjectReferenceForPattern | null :
+ *   null} get
+ */
+/**
+ * @typedef {object} ReactiveVariableReferences
+ * @property {(node: Identifier) => ReactiveVariableReference | null} get
+ */
+
+const REF_MACROS = [
+  '$ref',
+  '$computed',
+  '$shallowRef',
+  '$customRef',
+  '$toRef',
+  '$'
+]
+
+/** @type {WeakMap<Program, RefObjectReferences>} */
+const cacheForRefObjectReferences = new WeakMap()
+/** @type {WeakMap<Program, ReactiveVariableReferences>} */
+const cacheForReactiveVariableReferences = new WeakMap()
+
+/**
+ * Iterate the call expressions that define the ref object.
+ * @param {import('eslint').Scope.Scope} globalScope
+ * @returns {Iterable<{ node: CallExpression, name: string }>}
+ */
+function* iterateDefineRefs(globalScope) {
+  const tracker = new ReferenceTracker(globalScope)
+  const traceMap = utils.createCompositionApiTraceMap({
+    [ReferenceTracker.ESM]: true,
+    ref: {
+      [ReferenceTracker.CALL]: true
+    },
+    computed: {
+      [ReferenceTracker.CALL]: true
+    },
+    toRef: {
+      [ReferenceTracker.CALL]: true
+    },
+    customRef: {
+      [ReferenceTracker.CALL]: true
+    },
+    shallowRef: {
+      [ReferenceTracker.CALL]: true
+    },
+    toRefs: {
+      [ReferenceTracker.CALL]: true
+    }
+  })
+  for (const { node, path } of tracker.iterateEsmReferences(traceMap)) {
+    const expr = /** @type {CallExpression} */ (node)
+    yield {
+      node: expr,
+      name: path[path.length - 1]
+    }
+  }
+}
+
+/**
+ * Iterate the call expressions that define the reactive variables.
+ * @param {import('eslint').Scope.Scope} globalScope
+ * @returns {Iterable<{ node: CallExpression, name: string }>}
+ */
+function* iterateDefineReactiveVariables(globalScope) {
+  for (const { identifier } of iterateRefMacroReferences()) {
+    if (
+      identifier.parent.type === 'CallExpression' &&
+      identifier.parent.callee === identifier
+    ) {
+      yield {
+        node: identifier.parent,
+        name: identifier.name
+      }
+    }
+  }
+
+  /**
+   * Iterate ref macro reference.
+   * @returns {Iterable<Reference>}
+   */
+  function* iterateRefMacroReferences() {
+    yield* REF_MACROS.map((m) => globalScope.set.get(m))
+      .filter(utils.isDef)
+      .flatMap((v) => v.references)
+    for (const ref of globalScope.through) {
+      if (REF_MACROS.includes(ref.identifier.name)) {
+        yield ref
+      }
+    }
+  }
+}
+
+/**
+ *  Iterate the call expressions that the escape hint values.
+ * @param {import('eslint').Scope.Scope} globalScope
+ * @returns {Iterable<CallExpression>}
+ */
+function* iterateEscapeHintValueRefs(globalScope) {
+  for (const { identifier } of iterateEscapeHintReferences()) {
+    if (
+      identifier.parent.type === 'CallExpression' &&
+      identifier.parent.callee === identifier
+    ) {
+      yield identifier.parent
+    }
+  }
+
+  /**
+   * Iterate escape hint reference.
+   * @returns {Iterable<Reference>}
+   */
+  function* iterateEscapeHintReferences() {
+    const escapeHint = globalScope.set.get('$$')
+    if (escapeHint) {
+      yield* escapeHint.references
+    }
+    for (const ref of globalScope.through) {
+      if (ref.identifier.name === '$$') {
+        yield ref
+      }
+    }
+  }
+}
+
+/**
+ * Extract identifier from given pattern node.
+ * @param {Pattern} node
+ * @returns {Iterable<Identifier>}
+ */
+function* extractIdentifier(node) {
+  switch (node.type) {
+    case 'Identifier': {
+      yield node
+      break
+    }
+    case 'ObjectPattern': {
+      for (const property of node.properties) {
+        if (property.type === 'Property') {
+          yield* extractIdentifier(property.value)
+        } else if (property.type === 'RestElement') {
+          yield* extractIdentifier(property)
+        }
+      }
+      break
+    }
+    case 'ArrayPattern': {
+      for (const element of node.elements) {
+        if (element) {
+          yield* extractIdentifier(element)
+        }
+      }
+      break
+    }
+    case 'AssignmentPattern': {
+      yield* extractIdentifier(node.left)
+      break
+    }
+    case 'RestElement': {
+      yield* extractIdentifier(node.argument)
+      break
+    }
+    case 'MemberExpression': {
+      // can't extract
+      break
+    }
+    // No default
+  }
+}
+
+/**
+ * Iterate references of the given identifier.
+ * @param {Identifier} id
+ * @param {import('eslint').Scope.Scope} globalScope
+ * @returns {Iterable<import('eslint').Scope.Reference>}
+ */
+function* iterateIdentifierReferences(id, globalScope) {
+  const variable = eslintUtils.findVariable(globalScope, id)
+  if (!variable) {
+    return
+  }
+
+  for (const reference of variable.references) {
+    yield reference
+  }
+}
+
+/**
+ * @param {RuleContext} context The rule context.
+ */
+function getGlobalScope(context) {
+  const sourceCode = context.getSourceCode()
+  return (
+    sourceCode.scopeManager.globalScope || sourceCode.scopeManager.scopes[0]
+  )
+}
+// ------------------------------------------------------------------------------
+// Public
+// ------------------------------------------------------------------------------
+
+module.exports = {
+  extractRefObjectReferences,
+  extractReactiveVariableReferences
+}
+
+/**
+ * @implements {RefObjectReferences}
+ */
+class RefObjectReferenceExtractor {
+  /**
+   * @param {RuleContext} context The rule context.
+   */
+  constructor(context) {
+    this.context = context
+    /** @type {Map<Identifier | MemberExpression | CallExpression | ObjectPattern, RefObjectReference>} */
+    this.references = new Map()
+
+    /** @type {Set<Identifier>} */
+    this._processedIds = new Set()
+  }
+
+  /**
+   * @template {Identifier | Expression | Pattern | Super} T
+   * @param {T} node
+   * @returns {T extends Identifier ?
+   *     RefObjectReferenceForIdentifier | null :
+   *   T extends Expression ?
+   *     RefObjectReferenceForExpression | null :
+   *   T extends Pattern ?
+   *     RefObjectReferenceForPattern | null :
+   *   null}
+   */
+  get(node) {
+    return /** @type {never} */ (
+      this.references.get(/** @type {never} */ (node)) || null
+    )
+  }
+
+  /**
+   * @param {CallExpression} node
+   * @param {string} method
+   */
+  processDefineRef(node, method) {
+    const parent = node.parent
+    /** @type {Pattern | null} */
+    let pattern = null
+    if (parent.type === 'VariableDeclarator') {
+      pattern = parent.id
+    } else if (
+      parent.type === 'AssignmentExpression' &&
+      parent.operator === '='
+    ) {
+      pattern = parent.left
+    } else {
+      if (method !== 'toRefs') {
+        this.references.set(node, {
+          type: 'expression',
+          node,
+          method,
+          define: node
+        })
+      }
+      return
+    }
+
+    if (method === 'toRefs') {
+      const propertyReferenceExtractor = definePropertyReferenceExtractor(
+        this.context
+      )
+      const propertyReferences =
+        propertyReferenceExtractor.extractFromPattern(pattern)
+      for (const name of propertyReferences.allProperties().keys()) {
+        for (const nest of propertyReferences.getNestNodes(name)) {
+          if (nest.type === 'expression') {
+            this.processMemberExpression(nest.node, method, node)
+          } else if (nest.type === 'pattern') {
+            this.processPattern(nest.node, method, node)
+          }
+        }
+      }
+    } else {
+      this.processPattern(pattern, method, node)
+    }
+  }
+
+  /**
+   * @param {Expression} node
+   * @param {string} method
+   * @param {CallExpression} define
+   */
+  processExpression(node, method, define) {
+    const parent = node.parent
+    if (parent.type === 'AssignmentExpression') {
+      if (parent.operator === '=' && parent.right === node) {
+        // `(foo = obj.mem)`
+        this.processPattern(parent.left, method, define)
+        return true
+      }
+    } else if (parent.type === 'VariableDeclarator' && parent.init === node) {
+      // `const foo = obj.mem`
+      this.processPattern(parent.id, method, define)
+      return true
+    }
+    return false
+  }
+  /**
+   * @param {MemberExpression} node
+   * @param {string} method
+   * @param {CallExpression} define
+   */
+  processMemberExpression(node, method, define) {
+    if (this.processExpression(node, method, define)) {
+      return
+    }
+    this.references.set(node, {
+      type: 'expression',
+      node,
+      method,
+      define
+    })
+  }
+
+  /**
+   * @param {Pattern} node
+   * @param {string} method
+   * @param {CallExpression} define
+   */
+  processPattern(node, method, define) {
+    switch (node.type) {
+      case 'Identifier': {
+        this.processIdentifierPattern(node, method, define)
+        break
+      }
+      case 'ArrayPattern':
+      case 'RestElement':
+      case 'MemberExpression': {
+        return
+      }
+      case 'ObjectPattern': {
+        this.references.set(node, {
+          type: 'pattern',
+          node,
+          method,
+          define
+        })
+        return
+      }
+      case 'AssignmentPattern': {
+        this.processPattern(node.left, method, define)
+        return
+      }
+      // No default
+    }
+  }
+
+  /**
+   * @param {Identifier} node
+   * @param {string} method
+   * @param {CallExpression} define
+   */
+  processIdentifierPattern(node, method, define) {
+    if (this._processedIds.has(node)) {
+      return
+    }
+    this._processedIds.add(node)
+
+    for (const reference of iterateIdentifierReferences(
+      node,
+      getGlobalScope(this.context)
+    )) {
+      const def =
+        reference.resolved &&
+        reference.resolved.defs.length === 1 &&
+        reference.resolved.defs[0].type === 'Variable'
+          ? reference.resolved.defs[0]
+          : null
+      if (def && def.name === reference.identifier) {
+        continue
+      }
+      if (
+        reference.isRead() &&
+        this.processExpression(reference.identifier, method, define)
+      ) {
+        continue
+      }
+      this.references.set(reference.identifier, {
+        type: reference.isWrite() ? 'pattern' : 'expression',
+        node: reference.identifier,
+        method,
+        define,
+        variableDeclaration: def ? def.parent : null
+      })
+    }
+  }
+}
+
+/**
+ * Extracts references of all ref objects.
+ * @param {RuleContext} context The rule context.
+ * @returns {RefObjectReferences}
+ */
+function extractRefObjectReferences(context) {
+  const sourceCode = context.getSourceCode()
+  const cachedReferences = cacheForRefObjectReferences.get(sourceCode.ast)
+  if (cachedReferences) {
+    return cachedReferences
+  }
+  const references = new RefObjectReferenceExtractor(context)
+
+  for (const { node, name } of iterateDefineRefs(getGlobalScope(context))) {
+    references.processDefineRef(node, name)
+  }
+
+  cacheForRefObjectReferences.set(sourceCode.ast, references)
+
+  return references
+}
+
+/**
+ * @implements {ReactiveVariableReferences}
+ */
+class ReactiveVariableReferenceExtractor {
+  /**
+   * @param {RuleContext} context The rule context.
+   */
+  constructor(context) {
+    this.context = context
+    /** @type {Map<Identifier, ReactiveVariableReference>} */
+    this.references = new Map()
+
+    /** @type {Set<Identifier>} */
+    this._processedIds = new Set()
+
+    /** @type {Set<CallExpression>} */
+    this._escapeHintValueRefs = new Set(
+      iterateEscapeHintValueRefs(getGlobalScope(context))
+    )
+  }
+
+  /**
+   * @param {Identifier} node
+   * @returns {ReactiveVariableReference | null}
+   */
+  get(node) {
+    return this.references.get(node) || null
+  }
+
+  /**
+   * @param {CallExpression} node
+   * @param {string} method
+   */
+  processDefineReactiveVariable(node, method) {
+    const parent = node.parent
+    if (parent.type !== 'VariableDeclarator') {
+      return
+    }
+    /** @type {Pattern | null} */
+    const pattern = parent.id
+
+    if (method === '$') {
+      for (const id of extractIdentifier(pattern)) {
+        this.processIdentifierPattern(id, method, node)
+      }
+    } else {
+      if (pattern.type === 'Identifier') {
+        this.processIdentifierPattern(pattern, method, node)
+      }
+    }
+  }
+
+  /**
+   * @param {Identifier} node
+   * @param {string} method
+   * @param {CallExpression} define
+   */
+  processIdentifierPattern(node, method, define) {
+    if (this._processedIds.has(node)) {
+      return
+    }
+    this._processedIds.add(node)
+
+    for (const reference of iterateIdentifierReferences(
+      node,
+      getGlobalScope(this.context)
+    )) {
+      const def =
+        reference.resolved &&
+        reference.resolved.defs.length === 1 &&
+        reference.resolved.defs[0].type === 'Variable'
+          ? reference.resolved.defs[0]
+          : null
+      if (!def || def.name === reference.identifier) {
+        continue
+      }
+      this.references.set(reference.identifier, {
+        node: reference.identifier,
+        escape: this.withinEscapeHint(reference.identifier),
+        method,
+        define,
+        variableDeclaration: def.parent
+      })
+    }
+  }
+
+  /**
+   * Checks whether the given identifier node within the escape hints (`$$()`) or not.
+   * @param {Identifier} node
+   */
+  withinEscapeHint(node) {
+    /** @type {Identifier | ObjectExpression | ArrayExpression | SpreadElement | Property | AssignmentProperty} */
+    let target = node
+    /** @type {ASTNode | null} */
+    let parent = target.parent
+    while (parent) {
+      if (parent.type === 'CallExpression') {
+        if (
+          parent.arguments.includes(/** @type {any} */ (target)) &&
+          this._escapeHintValueRefs.has(parent)
+        ) {
+          return true
+        }
+        return false
+      }
+      if (
+        (parent.type === 'Property' && parent.value === target) ||
+        (parent.type === 'ObjectExpression' &&
+          parent.properties.includes(/** @type {any} */ (target))) ||
+        parent.type === 'ArrayExpression' ||
+        parent.type === 'SpreadElement'
+      ) {
+        target = parent
+        parent = target.parent
+      } else {
+        return false
+      }
+    }
+    return false
+  }
+}
+
+/**
+ * Extracts references of all reactive variables.
+ * @param {RuleContext} context The rule context.
+ * @returns {ReactiveVariableReferences}
+ */
+function extractReactiveVariableReferences(context) {
+  const sourceCode = context.getSourceCode()
+  const cachedReferences = cacheForReactiveVariableReferences.get(
+    sourceCode.ast
+  )
+  if (cachedReferences) {
+    return cachedReferences
+  }
+
+  const references = new ReactiveVariableReferenceExtractor(context)
+
+  for (const { node, name } of iterateDefineReactiveVariables(
+    getGlobalScope(context)
+  )) {
+    references.processDefineReactiveVariable(node, name)
+  }
+
+  cacheForReactiveVariableReferences.set(sourceCode.ast, references)
+
+  return references
+}
diff --git a/tsconfig.json b/tsconfig.json
index 40f2ceee..cbaefbd8 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,6 +1,6 @@
 {
   "compilerOptions": {
-    "target": "ES2020",
+    "target": "ES2019",
     "module": "commonjs",
     "lib": ["es2020"],
     "allowJs": true,
diff --git a/typings/eslint-plugin-vue/util-types/ast/es-ast.ts b/typings/eslint-plugin-vue/util-types/ast/es-ast.ts
index 16a734d2..f4dd208c 100644
--- a/typings/eslint-plugin-vue/util-types/ast/es-ast.ts
+++ b/typings/eslint-plugin-vue/util-types/ast/es-ast.ts
@@ -268,6 +268,7 @@ export interface ImportDeclaration extends HasParentNode {
     | ImportNamespaceSpecifier
   )[]
   source: Literal & { value: string }
+  importKind?: 'type' | 'value'
 }
 export interface ImportSpecifier extends HasParentNode {
   type: 'ImportSpecifier'
diff --git a/typings/eslint/index.d.ts b/typings/eslint/index.d.ts
index 7bfda024..949d56f6 100644
--- a/typings/eslint/index.d.ts
+++ b/typings/eslint/index.d.ts
@@ -54,6 +54,9 @@ export namespace Scope {
     eslintExplicitGlobal?: boolean | undefined
     eslintExplicitGlobalComments?: Comment[] | undefined
     eslintImplicitGlobalSetting?: 'readonly' | 'writable' | undefined
+
+    isTypeVariable?: boolean
+    isValueVariable?: boolean
   }
   interface Reference {
     identifier: VAST.Identifier


Test patch:
diff --git a/tests/fixtures/utils/ref-object-references/reactive-vars/$-with-destructuring/result.js b/tests/fixtures/utils/ref-object-references/reactive-vars/$-with-destructuring/result.js
new file mode 100644
index 00000000..805890a3
--- /dev/null
+++ b/tests/fixtures/utils/ref-object-references/reactive-vars/$-with-destructuring/result.js
@@ -0,0 +1,16 @@
+let { a, b: c, d: [,f], g: [...h], ...i } = $(foo)
+let [ x, y = 42 ] = $(bar)
+
+console.log(
+  /*>*/a/*<{"escape":false,"method":"$"}*/,
+  b,
+  /*>*/c/*<{"escape":false,"method":"$"}*/,
+  d,
+  e,
+  /*>*/f/*<{"escape":false,"method":"$"}*/,
+  g,
+  /*>*/h/*<{"escape":false,"method":"$"}*/,
+  /*>*/i/*<{"escape":false,"method":"$"}*/,
+  /*>*/x/*<{"escape":false,"method":"$"}*/,
+  /*>*/y/*<{"escape":false,"method":"$"}*/
+)
diff --git a/tests/fixtures/utils/ref-object-references/reactive-vars/$-with-destructuring/source.js b/tests/fixtures/utils/ref-object-references/reactive-vars/$-with-destructuring/source.js
new file mode 100644
index 00000000..bfb92574
--- /dev/null
+++ b/tests/fixtures/utils/ref-object-references/reactive-vars/$-with-destructuring/source.js
@@ -0,0 +1,16 @@
+let { a, b: c, d: [,f], g: [...h], ...i } = $(foo)
+let [ x, y = 42 ] = $(bar)
+
+console.log(
+  a,
+  b,
+  c,
+  d,
+  e,
+  f,
+  g,
+  h,
+  i,
+  x,
+  y
+)
diff --git a/tests/fixtures/utils/ref-object-references/reactive-vars/$/result.js b/tests/fixtures/utils/ref-object-references/reactive-vars/$/result.js
new file mode 100644
index 00000000..80d709de
--- /dev/null
+++ b/tests/fixtures/utils/ref-object-references/reactive-vars/$/result.js
@@ -0,0 +1,6 @@
+let v = $(foo)
+let { x, y } = $(bar)
+console.log(/*>*/v/*<{"escape":false,"method":"$"}*/, /*>*/x/*<{"escape":false,"method":"$"}*/, /*>*/y/*<{"escape":false,"method":"$"}*/)
+let a = /*>*/v/*<{"escape":false,"method":"$"}*/
+console.log(a)
+console.log($$({ /*>*/v/*<{"escape":true,"method":"$"}*/, /*>*/x/*<{"escape":true,"method":"$"}*/ }))
diff --git a/tests/fixtures/utils/ref-object-references/reactive-vars/$/source.js b/tests/fixtures/utils/ref-object-references/reactive-vars/$/source.js
new file mode 100644
index 00000000..28c98cbc
--- /dev/null
+++ b/tests/fixtures/utils/ref-object-references/reactive-vars/$/source.js
@@ -0,0 +1,6 @@
+let v = $(foo)
+let { x, y } = $(bar)
+console.log(v, x, y)
+let a = v
+console.log(a)
+console.log($$({ v, x }))
diff --git a/tests/fixtures/utils/ref-object-references/reactive-vars/$computed/result.js b/tests/fixtures/utils/ref-object-references/reactive-vars/$computed/result.js
new file mode 100644
index 00000000..a45a0ae6
--- /dev/null
+++ b/tests/fixtures/utils/ref-object-references/reactive-vars/$computed/result.js
@@ -0,0 +1,5 @@
+let v = $computed(() => 42)
+console.log(/*>*/v/*<{"escape":false,"method":"$computed"}*/)
+let a = /*>*/v/*<{"escape":false,"method":"$computed"}*/
+console.log(a)
+console.log($$(/*>*/v/*<{"escape":true,"method":"$computed"}*/))
diff --git a/tests/fixtures/utils/ref-object-references/reactive-vars/$computed/source.js b/tests/fixtures/utils/ref-object-references/reactive-vars/$computed/source.js
new file mode 100644
index 00000000..ff0f3476
--- /dev/null
+++ b/tests/fixtures/utils/ref-object-references/reactive-vars/$computed/source.js
@@ -0,0 +1,5 @@
+let v = $computed(() => 42)
+console.log(v)
+let a = v
+console.log(a)
+console.log($$(v))
diff --git a/tests/fixtures/utils/ref-object-references/reactive-vars/$customRef/result.js b/tests/fixtures/utils/ref-object-references/reactive-vars/$customRef/result.js
new file mode 100644
index 00000000..a8a3ae90
--- /dev/null
+++ b/tests/fixtures/utils/ref-object-references/reactive-vars/$customRef/result.js
@@ -0,0 +1,10 @@
+let v = $customRef((track, trigger) => {
+  return {
+    get() { return count },
+    set(newValue) { count = newValue }
+  }
+})
+console.log(/*>*/v/*<{"escape":false,"method":"$customRef"}*/)
+let a = /*>*/v/*<{"escape":false,"method":"$customRef"}*/
+console.log(a)
+console.log($$(/*>*/v/*<{"escape":true,"method":"$customRef"}*/))
diff --git a/tests/fixtures/utils/ref-object-references/reactive-vars/$customRef/source.js b/tests/fixtures/utils/ref-object-references/reactive-vars/$customRef/source.js
new file mode 100644
index 00000000..dd62754b
--- /dev/null
+++ b/tests/fixtures/utils/ref-object-references/reactive-vars/$customRef/source.js
@@ -0,0 +1,10 @@
+let v = $customRef((track, trigger) => {
+  return {
+    get() { return count },
+    set(newValue) { count = newValue }
+  }
+})
+console.log(v)
+let a = v
+console.log(a)
+console.log($$(v))
diff --git a/tests/fixtures/utils/ref-object-references/reactive-vars/$ref/result.js b/tests/fixtures/utils/ref-object-references/reactive-vars/$ref/result.js
new file mode 100644
index 00000000..518f0c56
--- /dev/null
+++ b/tests/fixtures/utils/ref-object-references/reactive-vars/$ref/result.js
@@ -0,0 +1,5 @@
+let v = $ref(0)
+console.log(/*>*/v/*<{"escape":false,"method":"$ref"}*/)
+let a = /*>*/v/*<{"escape":false,"method":"$ref"}*/
+console.log(a)
+console.log($$(/*>*/v/*<{"escape":true,"method":"$ref"}*/))
diff --git a/tests/fixtures/utils/ref-object-references/reactive-vars/$ref/source.js b/tests/fixtures/utils/ref-object-references/reactive-vars/$ref/source.js
new file mode 100644
index 00000000..5c8d7248
--- /dev/null
+++ b/tests/fixtures/utils/ref-object-references/reactive-vars/$ref/source.js
@@ -0,0 +1,5 @@
+let v = $ref(0)
+console.log(v)
+let a = v
+console.log(a)
+console.log($$(v))
diff --git a/tests/fixtures/utils/ref-object-references/reactive-vars/$shallowRef/result.js b/tests/fixtures/utils/ref-object-references/reactive-vars/$shallowRef/result.js
new file mode 100644
index 00000000..dbe345a1
--- /dev/null
+++ b/tests/fixtures/utils/ref-object-references/reactive-vars/$shallowRef/result.js
@@ -0,0 +1,5 @@
+let v = $shallowRef({ count: 0 })
+console.log(/*>*/v/*<{"escape":false,"method":"$shallowRef"}*/)
+let a = /*>*/v/*<{"escape":false,"method":"$shallowRef"}*/
+console.log(a)
+console.log($$(/*>*/v/*<{"escape":true,"method":"$shallowRef"}*/))
diff --git a/tests/fixtures/utils/ref-object-references/reactive-vars/$shallowRef/source.js b/tests/fixtures/utils/ref-object-references/reactive-vars/$shallowRef/source.js
new file mode 100644
index 00000000..da4c1932
--- /dev/null
+++ b/tests/fixtures/utils/ref-object-references/reactive-vars/$shallowRef/source.js
@@ -0,0 +1,5 @@
+let v = $shallowRef({ count: 0 })
+console.log(v)
+let a = v
+console.log(a)
+console.log($$(v))
diff --git a/tests/fixtures/utils/ref-object-references/reactive-vars/$toRef/result.js b/tests/fixtures/utils/ref-object-references/reactive-vars/$toRef/result.js
new file mode 100644
index 00000000..a7bff121
--- /dev/null
+++ b/tests/fixtures/utils/ref-object-references/reactive-vars/$toRef/result.js
@@ -0,0 +1,5 @@
+let v = $toRef(foo, 'bar')
+console.log(/*>*/v/*<{"escape":false,"method":"$toRef"}*/)
+let a = /*>*/v/*<{"escape":false,"method":"$toRef"}*/
+console.log(a)
+console.log($$(/*>*/v/*<{"escape":true,"method":"$toRef"}*/))
diff --git a/tests/fixtures/utils/ref-object-references/reactive-vars/$toRef/source.js b/tests/fixtures/utils/ref-object-references/reactive-vars/$toRef/source.js
new file mode 100644
index 00000000..b94ddea9
--- /dev/null
+++ b/tests/fixtures/utils/ref-object-references/reactive-vars/$toRef/source.js
@@ -0,0 +1,5 @@
+let v = $toRef(foo, 'bar')
+console.log(v)
+let a = v
+console.log(a)
+console.log($$(v))
diff --git a/tests/fixtures/utils/ref-object-references/ref-objects/computed/result.js b/tests/fixtures/utils/ref-object-references/ref-objects/computed/result.js
new file mode 100644
index 00000000..2217a818
--- /dev/null
+++ b/tests/fixtures/utils/ref-object-references/ref-objects/computed/result.js
@@ -0,0 +1,5 @@
+import { computed } from 'vue'
+let v = computed(() => 42)
+console.log(/*>*/v/*<{"type":"expression","method":"computed"}*/.value)
+let a = v
+console.log(/*>*/a/*<{"type":"expression","method":"computed"}*/.value)
diff --git a/tests/fixtures/utils/ref-object-references/ref-objects/computed/source.js b/tests/fixtures/utils/ref-object-references/ref-objects/computed/source.js
new file mode 100644
index 00000000..2b4a6412
--- /dev/null
+++ b/tests/fixtures/utils/ref-object-references/ref-objects/computed/source.js
@@ -0,0 +1,5 @@
+import { computed } from 'vue'
+let v = computed(() => 42)
+console.log(v.value)
+let a = v
+console.log(a.value)
diff --git a/tests/fixtures/utils/ref-object-references/ref-objects/customRef/result.js b/tests/fixtures/utils/ref-object-references/ref-objects/customRef/result.js
new file mode 100644
index 00000000..e7af0774
--- /dev/null
+++ b/tests/fixtures/utils/ref-object-references/ref-objects/customRef/result.js
@@ -0,0 +1,10 @@
+import { customRef } from 'vue'
+let v = customRef((track, trigger) => {
+  return {
+    get() { return count },
+    set(newValue) { count = newValue }
+  }
+})
+console.log(/*>*/v/*<{"type":"expression","method":"customRef"}*/.value)
+let a = v
+console.log(/*>*/a/*<{"type":"expression","method":"customRef"}*/.value)
diff --git a/tests/fixtures/utils/ref-object-references/ref-objects/customRef/source.js b/tests/fixtures/utils/ref-object-references/ref-objects/customRef/source.js
new file mode 100644
index 00000000..c94db730
--- /dev/null
+++ b/tests/fixtures/utils/ref-object-references/ref-objects/customRef/source.js
@@ -0,0 +1,10 @@
+import { customRef } from 'vue'
+let v = customRef((track, trigger) => {
+  return {
+    get() { return count },
+    set(newValue) { count = newValue }
+  }
+})
+console.log(v.value)
+let a = v
+console.log(a.value)
diff --git a/tests/fixtures/utils/ref-object-references/ref-objects/ref-to-pattern/result.js b/tests/fixtures/utils/ref-object-references/ref-objects/ref-to-pattern/result.js
new file mode 100644
index 00000000..a0681af4
--- /dev/null
+++ b/tests/fixtures/utils/ref-object-references/ref-objects/ref-to-pattern/result.js
@@ -0,0 +1,9 @@
+import { ref } from 'vue'
+let v = ref(0)
+const /*>*/{ value: a }/*<{"type":"pattern","method":"ref"}*/ = v
+const /*>*/{ value = 42 }/*<{"type":"pattern","method":"ref"}*/ = v
+console.log(a)
+console.log(value)
+
+const [x] = v
+console.log(x)
diff --git a/tests/fixtures/utils/ref-object-references/ref-objects/ref-to-pattern/source.js b/tests/fixtures/utils/ref-object-references/ref-objects/ref-to-pattern/source.js
new file mode 100644
index 00000000..3a2c5c33
--- /dev/null
+++ b/tests/fixtures/utils/ref-object-references/ref-objects/ref-to-pattern/source.js
@@ -0,0 +1,9 @@
+import { ref } from 'vue'
+let v = ref(0)
+const { value: a } = v
+const { value = 42 } = v
+console.log(a)
+console.log(value)
+
+const [x] = v
+console.log(x)
diff --git a/tests/fixtures/utils/ref-object-references/ref-objects/ref/result.js b/tests/fixtures/utils/ref-object-references/ref-objects/ref/result.js
new file mode 100644
index 00000000..c356f2c2
--- /dev/null
+++ b/tests/fixtures/utils/ref-object-references/ref-objects/ref/result.js
@@ -0,0 +1,5 @@
+import { ref } from 'vue'
+let v = ref(0)
+console.log(/*>*/v/*<{"type":"expression","method":"ref"}*/.value)
+let a = v
+console.log(/*>*/a/*<{"type":"expression","method":"ref"}*/.value)
diff --git a/tests/fixtures/utils/ref-object-references/ref-objects/ref/source.js b/tests/fixtures/utils/ref-object-references/ref-objects/ref/source.js
new file mode 100644
index 00000000..836b4577
--- /dev/null
+++ b/tests/fixtures/utils/ref-object-references/ref-objects/ref/source.js
@@ -0,0 +1,5 @@
+import { ref } from 'vue'
+let v = ref(0)
+console.log(v.value)
+let a = v
+console.log(a.value)
diff --git a/tests/fixtures/utils/ref-object-references/ref-objects/shallowRef/result.js b/tests/fixtures/utils/ref-object-references/ref-objects/shallowRef/result.js
new file mode 100644
index 00000000..206f3e88
--- /dev/null
+++ b/tests/fixtures/utils/ref-object-references/ref-objects/shallowRef/result.js
@@ -0,0 +1,5 @@
+import { shallowRef } from 'vue'
+let v = shallowRef({ count: 0 })
+console.log(/*>*/v/*<{"type":"expression","method":"shallowRef"}*/.value)
+let a = v
+console.log(/*>*/a/*<{"type":"expression","method":"shallowRef"}*/.value)
diff --git a/tests/fixtures/utils/ref-object-references/ref-objects/shallowRef/source.js b/tests/fixtures/utils/ref-object-references/ref-objects/shallowRef/source.js
new file mode 100644
index 00000000..98496ae7
--- /dev/null
+++ b/tests/fixtures/utils/ref-object-references/ref-objects/shallowRef/source.js
@@ -0,0 +1,5 @@
+import { shallowRef } from 'vue'
+let v = shallowRef({ count: 0 })
+console.log(v.value)
+let a = v
+console.log(a.value)
diff --git a/tests/fixtures/utils/ref-object-references/ref-objects/toRef/result.js b/tests/fixtures/utils/ref-object-references/ref-objects/toRef/result.js
new file mode 100644
index 00000000..28bea16d
--- /dev/null
+++ b/tests/fixtures/utils/ref-object-references/ref-objects/toRef/result.js
@@ -0,0 +1,5 @@
+import { toRef } from 'vue'
+let v = toRef(foo, 'bar')
+console.log(/*>*/v/*<{"type":"expression","method":"toRef"}*/.value)
+let a = v
+console.log(/*>*/a/*<{"type":"expression","method":"toRef"}*/.value)
diff --git a/tests/fixtures/utils/ref-object-references/ref-objects/toRef/source.js b/tests/fixtures/utils/ref-object-references/ref-objects/toRef/source.js
new file mode 100644
index 00000000..df884f00
--- /dev/null
+++ b/tests/fixtures/utils/ref-object-references/ref-objects/toRef/source.js
@@ -0,0 +1,5 @@
+import { toRef } from 'vue'
+let v = toRef(foo, 'bar')
+console.log(v.value)
+let a = v
+console.log(a.value)
diff --git a/tests/fixtures/utils/ref-object-references/ref-objects/toRefs-to-pattern/result.js b/tests/fixtures/utils/ref-object-references/ref-objects/toRefs-to-pattern/result.js
new file mode 100644
index 00000000..527acdc7
--- /dev/null
+++ b/tests/fixtures/utils/ref-object-references/ref-objects/toRefs-to-pattern/result.js
@@ -0,0 +1,5 @@
+import { toRefs } from 'vue'
+let bar = toRefs(foo)
+const { x, y = 42 } = bar
+console.log(/*>*/x/*<{"type":"expression","method":"toRefs"}*/.value)
+console.log(/*>*/y/*<{"type":"expression","method":"toRefs"}*/.value)
diff --git a/tests/fixtures/utils/ref-object-references/ref-objects/toRefs-to-pattern/source.js b/tests/fixtures/utils/ref-object-references/ref-objects/toRefs-to-pattern/source.js
new file mode 100644
index 00000000..b2c39b4a
--- /dev/null
+++ b/tests/fixtures/utils/ref-object-references/ref-objects/toRefs-to-pattern/source.js
@@ -0,0 +1,5 @@
+import { toRefs } from 'vue'
+let bar = toRefs(foo)
+const { x, y = 42 } = bar
+console.log(x.value)
+console.log(y.value)
diff --git a/tests/fixtures/utils/ref-object-references/ref-objects/toRefs/result.js b/tests/fixtures/utils/ref-object-references/ref-objects/toRefs/result.js
new file mode 100644
index 00000000..809dfc90
--- /dev/null
+++ b/tests/fixtures/utils/ref-object-references/ref-objects/toRefs/result.js
@@ -0,0 +1,18 @@
+import { toRefs } from 'vue'
+let { x, y } = toRefs(foo)
+console.log(/*>*/x/*<{"type":"expression","method":"toRefs"}*/.value)
+let a = y
+console.log(/*>*/a/*<{"type":"expression","method":"toRefs"}*/.value)
+console.log(/*>*/y/*<{"type":"expression","method":"toRefs"}*/.value)
+
+let bar = toRefs(foo)
+console.log(bar)
+console.log(/*>*/bar.x/*<{"type":"expression","method":"toRefs"}*/.value)
+console.log(/*>*/bar.y/*<{"type":"expression","method":"toRefs"}*/.value)
+
+const z = bar.z
+console.log(/*>*/z/*<{"type":"expression","method":"toRefs"}*/.value)
+
+let b;
+/*>*/b/*<{"type":"pattern","method":"toRefs"}*/ = bar.b
+console.log(/*>*/b/*<{"type":"expression","method":"toRefs"}*/.value)
diff --git a/tests/fixtures/utils/ref-object-references/ref-objects/toRefs/source.js b/tests/fixtures/utils/ref-object-references/ref-objects/toRefs/source.js
new file mode 100644
index 00000000..638e6929
--- /dev/null
+++ b/tests/fixtures/utils/ref-object-references/ref-objects/toRefs/source.js
@@ -0,0 +1,18 @@
+import { toRefs } from 'vue'
+let { x, y } = toRefs(foo)
+console.log(x.value)
+let a = y
+console.log(a.value)
+console.log(y.value)
+
+let bar = toRefs(foo)
+console.log(bar)
+console.log(bar.x.value)
+console.log(bar.y.value)
+
+const z = bar.z
+console.log(z.value)
+
+let b;
+b = bar.b
+console.log(b.value)
diff --git a/tests/lib/rules/no-ref-object-destructure.js b/tests/lib/rules/no-ref-object-destructure.js
new file mode 100644
index 00000000..4ef92acf
--- /dev/null
+++ b/tests/lib/rules/no-ref-object-destructure.js
@@ -0,0 +1,449 @@
+/**
+ * @author Yosuke Ota <https://github.com/ota-meshi>
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+const RuleTester = require('eslint').RuleTester
+const rule = require('../../../lib/rules/no-ref-object-destructure')
+
+const tester = new RuleTester({
+  parserOptions: {
+    ecmaVersion: 2020,
+    sourceType: 'module'
+  }
+})
+
+tester.run('no-ref-object-destructure', rule, {
+  valid: [
+    `
+    import { ref } from 'vue'
+    const count = ref(0)
+    const value1 = computed(() => count.value)
+    const value2 = fn(count)
+    const value3 = computed(() => fn(count.value))
+    `,
+    `
+    import { toRefs } from 'vue'
+    const { count } = toRefs(foo)
+    const value1 = computed(() => count.value)
+    const value2 = fn(count)
+    const value3 = computed(() => fn(count.value))
+    `,
+    `
+    import { toRefs } from 'vue'
+    const refs = toRefs(foo)
+    const value1 = computed(() => refs.count.value)
+    const value2 = fn(refs.count)
+    const value3 = computed(() => fn(refs.count.value))
+    `,
+    `
+    import { ref } from 'vue'
+    const count = ref(0)
+    count.value = 42
+    `,
+    `
+    import { ref } from 'vue'
+    const count = ref(0)
+    count.value++
+    `,
+    `
+    import { ref } from 'vue'
+    const count = ref(0)
+    ;( { foo: count.value } = bar )
+    `,
+    `
+    import { ref, computed, shallowRef, customRef, toRef } from 'vue'
+    const r = ref(0)
+    const c = computed(() => r.value)
+    const sr = shallowRef({ count: 1 })
+    const cr = customRef((track, trigger) => {
+      return {
+        get() { return sr.value },
+        set(newValue) { sr.value = newValue }
+      }
+    })
+    const tr = toRef(sr, 'count')
+    function fn() {
+      console.log(c.value, cr.value, tr.value)
+    }
+    `,
+    // unknown
+    `
+    import { ref } from 'vue'
+    const [a] = ref(0)
+    foo.bar = ref(0)
+    unknown.value
+    `,
+    `
+    import { ref } from 'vue'
+    let foo = ref(0)
+    foo = foo
+    `,
+    // Reactivity Transform
+    `
+    const count = $ref(0)
+    const value1 = computed(() => count)
+    const value2 = fn($$(count))
+    const value3 = computed(() => fn(count))
+    `,
+    `
+    const count = $(foo)
+    const value1 = computed(() => count)
+    const value2 = fn($$(count))
+    const value3 = computed(() => fn(count))
+    `,
+    `
+    const { count } = $(foo)
+    const value1 = computed(() => count)
+    const value2 = fn($$(count))
+    const value3 = computed(() => fn(count))
+    `,
+    `
+    let count = $ref(0)
+    count = 42
+    `,
+    `
+    let count = $ref(0)
+    count++
+    `,
+    `
+    let count = $ref(0)
+    ;( { foo: count } = bar )
+    `,
+    `
+    const { v1, v2, v3, v4 } = $(foo)
+    fn($$({ v1, a: [v2], a: [...v3], ...v4}))
+    `,
+    `
+    let r = $ref(0)
+    let c = $computed(() => r)
+    let sr = $shallowRef({ count: 1 })
+    let cr = $customRef((track, trigger) => {
+      return {
+        get() { return sr },
+        set(newValue) { sr = newValue }
+      }
+    })
+    let tr = $toRef($$(sr), 'count')
+    function fn() {
+      console.log(
+        r.value,
+        c.value,
+        sr.value,
+        cr.value,
+        tr.value
+      )
+    }
+    `
+  ],
+  invalid: [
+    {
+      code: `
+      import { ref } from 'vue'
+      const count = ref(0)
+      const value1 = count.value
+      const { value: value2 } = count
+      const value3 = fn(count.value)
+      const { value: value4 = 42 } = count
+      if (foo) {
+        const value1 = count.value
+      }
+      `,
+      errors: [
+        {
+          message:
+            'Getting a value from the ref object in the same scope will cause the value to lose reactivity.',
+          line: 4
+        },
+        {
+          message:
+            'Getting a value from the ref object in the same scope will cause the value to lose reactivity.',
+          line: 5
+        },
+        {
+          message:
+            'Getting a value from the ref object in the same scope will cause the value to lose reactivity.',
+          line: 6
+        },
+        {
+          message:
+            'Getting a value from the ref object in the same scope will cause the value to lose reactivity.',
+          line: 7
+        },
+        {
+          message:
+            'Getting a value from the ref object in the same scope will cause the value to lose reactivity.',
+          line: 9
+        }
+      ]
+    },
+    {
+      code: `
+      import { toRefs } from 'vue'
+      const { count } = toRefs(foo)
+      const value1 = count.value
+      const { value: value2 } = count
+      const value3 = fn(count.value)
+      `,
+      errors: [
+        {
+          message:
+            'Getting a value from the ref object in the same scope will cause the value to lose reactivity.',
+          line: 4
+        },
+        {
+          message:
+            'Getting a value from the ref object in the same scope will cause the value to lose reactivity.',
+          line: 5
+        },
+        {
+          message:
+            'Getting a value from the ref object in the same scope will cause the value to lose reactivity.',
+          line: 6
+        }
+      ]
+    },
+    {
+      code: `
+      import { toRefs } from 'vue'
+      const refs = toRefs(foo)
+      const value1 = refs.count.value
+      const { value: value2 } = refs.count
+      const value3 = fn(refs.count.value)
+      `,
+      errors: [
+        {
+          message:
+            'Getting a value from the ref object in the same scope will cause the value to lose reactivity.',
+          line: 4
+        },
+        {
+          message:
+            'Getting a value from the ref object in the same scope will cause the value to lose reactivity.',
+          line: 5
+        },
+        {
+          message:
+            'Getting a value from the ref object in the same scope will cause the value to lose reactivity.',
+          line: 6
+        }
+      ]
+    },
+    {
+      code: `
+      import { ref } from 'vue'
+      const count = ref(0).value
+      `,
+      errors: [
+        {
+          message:
+            'Getting a value from the ref object in the same scope will cause the value to lose reactivity.',
+          line: 3
+        }
+      ]
+    },
+    {
+      code: `
+      import { toRefs } from 'vue'
+      const refs = toRefs(foo)
+      const { foo = 42 } = refs
+      const v = foo.value
+      `,
+      errors: [
+        {
+          message:
+            'Getting a value from the ref object in the same scope will cause the value to lose reactivity.',
+          line: 5
+        }
+      ]
+    },
+    {
+      code: `
+      import { ref, computed, shallowRef, customRef, toRef } from 'vue'
+      const r = ref(0)
+      const c = computed(() => r.value)
+      const sr = shallowRef({ count: 1 })
+      const cr = customRef((track, trigger) => {
+        return {
+          get() { return sr.value },
+          set(newValue) { sr.value = newValue }
+        }
+      })
+      const tr = toRef(sr, 'count')
+
+      console.log(
+        r.value,
+        c.value,
+        sr.value,
+        cr.value,
+        tr.value
+      )
+      `,
+      errors: [
+        {
+          message:
+            'Getting a value from the ref object in the same scope will cause the value to lose reactivity.',
+          line: 15
+        },
+        {
+          message:
+            'Getting a value from the ref object in the same scope will cause the value to lose reactivity.',
+          line: 16
+        },
+        {
+          message:
+            'Getting a value from the ref object in the same scope will cause the value to lose reactivity.',
+          line: 17
+        },
+        {
+          message:
+            'Getting a value from the ref object in the same scope will cause the value to lose reactivity.',
+          line: 18
+        },
+        {
+          message:
+            'Getting a value from the ref object in the same scope will cause the value to lose reactivity.',
+          line: 19
+        }
+      ]
+    },
+    // Reactivity Transform
+    {
+      code: `
+      const count = $ref(0)
+      const value1 = count
+      const value2 = fn(count)
+      `,
+      errors: [
+        {
+          message:
+            'Getting a reactive variable in the same scope will cause the value to lose reactivity.',
+          line: 3
+        },
+        {
+          message:
+            'Getting a reactive variable in the same scope will cause the value to lose reactivity.',
+          line: 4
+        }
+      ]
+    },
+    {
+      code: `
+      const count = $(foo)
+      const value1 = count
+      const value2 = fn(count)
+      `,
+      errors: [
+        {
+          message:
+            'Getting a reactive variable in the same scope will cause the value to lose reactivity.',
+          line: 3
+        },
+        {
+          message:
+            'Getting a reactive variable in the same scope will cause the value to lose reactivity.',
+          line: 4
+        }
+      ]
+    },
+    {
+      code: `
+      const { count } = $(foo)
+      const value1 = count
+      const value2 = fn(count)
+      `,
+      errors: [
+        {
+          message:
+            'Getting a reactive variable in the same scope will cause the value to lose reactivity.',
+          line: 3
+        },
+        {
+          message:
+            'Getting a reactive variable in the same scope will cause the value to lose reactivity.',
+          line: 4
+        }
+      ]
+    },
+    {
+      code: `
+      const { v1, a: [v2 = 42], b: [...v3], ...v4 } = $(foo)
+      const value1 = v1
+      const value2 = v2
+      const value3 = v3
+      const value4 = v4
+      `,
+      errors: [
+        {
+          message:
+            'Getting a reactive variable in the same scope will cause the value to lose reactivity.',
+          line: 3
+        },
+        {
+          message:
+            'Getting a reactive variable in the same scope will cause the value to lose reactivity.',
+          line: 4
+        },
+        {
+          message:
+            'Getting a reactive variable in the same scope will cause the value to lose reactivity.',
+          line: 5
+        },
+        {
+          message:
+            'Getting a reactive variable in the same scope will cause the value to lose reactivity.',
+          line: 6
+        }
+      ]
+    },
+    {
+      code: `
+      let r = $ref(0)
+      let c = $computed(() => r)
+      let sr = $shallowRef({ count: 1 })
+      let cr = $customRef((track, trigger) => {
+        return {
+          get() { return sr },
+          set(newValue) { sr = newValue }
+        }
+      })
+      let tr = $toRef($$(sr), 'count')
+      console.log(
+        r.value,
+        c.value,
+        sr.value,
+        cr.value,
+        tr.value
+      )
+      `,
+      errors: [
+        {
+          message:
+            'Getting a reactive variable in the same scope will cause the value to lose reactivity.',
+          line: 13
+        },
+        {
+          message:
+            'Getting a reactive variable in the same scope will cause the value to lose reactivity.',
+          line: 14
+        },
+        {
+          message:
+            'Getting a reactive variable in the same scope will cause the value to lose reactivity.',
+          line: 15
+        },
+        {
+          message:
+            'Getting a reactive variable in the same scope will cause the value to lose reactivity.',
+          line: 16
+        },
+        {
+          message:
+            'Getting a reactive variable in the same scope will cause the value to lose reactivity.',
+          line: 17
+        }
+      ]
+    }
+  ]
+})
diff --git a/tests/lib/rules/no-undef-components.js b/tests/lib/rules/no-undef-components.js
index b3a7af10..fc3461e5 100644
--- a/tests/lib/rules/no-undef-components.js
+++ b/tests/lib/rules/no-undef-components.js
@@ -661,6 +661,66 @@ tester.run('no-undef-components', rule, {
         }
       ]
     },
+    {
+      filename: 'test.vue',
+      code: `
+      <script setup lang="ts">
+        import type Foo from './Foo.vue'
+        import type {HelloWorld1} from './components/HelloWorld'
+        import { type HelloWorld2 } from './components/HelloWorld2'
+        import type {HelloWorld as HelloWorld3} from './components/HelloWorld3'
+        import { type HelloWorld as HelloWorld4 } from './components/HelloWorld4';
+        import { type default as HelloWorld5 } from './components/HelloWorld5';
+      </script>
+
+      <template>
+        <Foo />
+        <HelloWorld1 />
+        <HelloWorld2 />
+        <HelloWorld3 />
+        <HelloWorld4 />
+        <HelloWorld5 />
+      </template>
+      `,
+      parserOptions: {
+        ecmaVersion: 6,
+        sourceType: 'module',
+        parser: require.resolve('@typescript-eslint/parser')
+      },
+      parser: require.resolve('vue-eslint-parser'),
+      errors: [
+        {
+          message:
+            "The '<Foo>' component has been used, but 'Foo' only refers to a type.",
+          line: 12
+        },
+        {
+          message:
+            "The '<HelloWorld1>' component has been used, but 'HelloWorld1' only refers to a type.",
+          line: 13
+        },
+        {
+          message:
+            "The '<HelloWorld2>' component has been used, but 'HelloWorld2' only refers to a type.",
+          line: 14
+        },
+        {
+          message:
+            "The '<HelloWorld3>' component has been used, but 'HelloWorld3' only refers to a type.",
+          line: 15
+        },
+        {
+          message:
+            "The '<HelloWorld4>' component has been used, but 'HelloWorld4' only refers to a type.",
+          line: 16
+        },
+        {
+          message:
+            "The '<HelloWorld5>' component has been used, but 'HelloWorld5' only refers to a type.",
+          line: 17
+        }
+      ]
+    },
 
     // options API
     {
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: '*' }]]
+    }
+  ]
+})
diff --git a/tests/lib/utils/ref-object-references.js b/tests/lib/utils/ref-object-references.js
new file mode 100644
index 00000000..66ee89d0
--- /dev/null
+++ b/tests/lib/utils/ref-object-references.js
@@ -0,0 +1,164 @@
+'use strict'
+
+const fs = require('fs')
+const path = require('path')
+const assert = require('assert')
+
+const Linter = require('eslint').Linter
+
+const {
+  extractRefObjectReferences,
+  extractReactiveVariableReferences
+} = require('../../../lib/utils/ref-object-references')
+
+const FIXTURE_ROOT = path.resolve(
+  __dirname,
+  '../../fixtures/utils/ref-object-references'
+)
+const REF_OBJECTS_FIXTURE_ROOT = path.resolve(FIXTURE_ROOT, 'ref-objects')
+const REACTIVE_VARS_FIXTURE_ROOT = path.resolve(FIXTURE_ROOT, 'reactive-vars')
+
+/**
+ * Load test patterns from fixtures.
+ *
+ * @returns {object} The loaded patterns.
+ */
+function loadPatterns(rootDir) {
+  return fs.readdirSync(rootDir).map((name) => {
+    const code0 = fs.readFileSync(path.join(rootDir, name, 'source.js'), 'utf8')
+    const code = code0.replace(/^<!--(.+?)-->/, `<!--${name}-->`)
+    return { code, name }
+  })
+}
+
+function extractRefs(code, extract) {
+  const linter = new Linter()
+  const references = []
+
+  linter.defineRule('vue/extract-test', (context) => {
+    const refs = extract(context)
+
+    const processed = new Set()
+    return {
+      '*'(node) {
+        if (processed.has(node)) {
+          // Old ESLint may be called twice on the same node.
+          return
+        }
+        processed.add(node)
+        const data = refs.get(node)
+        if (data) {
+          references.push(data)
+        }
+      }
+    }
+  })
+  const messages = linter.verify(
+    code,
+    {
+      parserOptions: { ecmaVersion: 2020, sourceType: 'module' },
+      rules: { 'vue/extract-test': 'error' },
+      globals: {
+        $ref: 'readonly',
+        $computed: 'readonly',
+        $shallowRef: 'readonly',
+        $customRef: 'readonly',
+        $toRef: 'readonly',
+        $: 'readonly',
+        $$: 'readonly'
+      }
+    },
+    undefined,
+    true
+  )
+
+  const errors = messages.map((message) => message.message)
+  if (errors.length > 0) {
+    assert.fail(errors.join(','))
+  }
+
+  return references
+}
+
+describe('extractRefObjectReferences()', () => {
+  for (const { name, code } of loadPatterns(REF_OBJECTS_FIXTURE_ROOT)) {
+    describe(`'test/fixtures/utils/ref-object-references/ref-objects/${name}/source.vue'`, () => {
+      it('should to extract the references to match the expected references.', () => {
+        /** @type {import('../../../lib/utils/ref-object-references').RefObjectReference[]} */
+        const references = [...extractRefs(code, extractRefObjectReferences)]
+
+        let result = ''
+        let start = 0
+        let ref
+        while ((ref = references.shift())) {
+          result += code.slice(start, ref.node.range[0])
+          result += `/*>*/`
+          result += code.slice(...ref.node.range)
+          result += `/*<${JSON.stringify({
+            type: ref.type,
+            method: ref.method
+          })}*/`
+          start = ref.node.range[1]
+        }
+        result += code.slice(start)
+
+        const actual = result
+
+        // update fixture
+        // fs.writeFileSync(
+        //   path.join(REF_OBJECTS_FIXTURE_ROOT, name, 'result.js'),
+        //   actual,
+        //   'utf8'
+        // )
+
+        const expected = fs.readFileSync(
+          path.join(REF_OBJECTS_FIXTURE_ROOT, name, 'result.js'),
+          'utf8'
+        )
+        assert.strictEqual(actual, expected)
+      })
+    })
+  }
+})
+describe('extractReactiveVariableReferences()', () => {
+  for (const { name, code } of loadPatterns(REACTIVE_VARS_FIXTURE_ROOT)) {
+    describe(`'test/fixtures/utils/ref-object-references/reactive-vars/${name}/source.vue'`, () => {
+      it('should to extract the references to match the expected references.', () => {
+        /** @type {import('../../../lib/utils/ref-object-references').ReactiveVariableReference[]} */
+        const references = [
+          ...extractRefs(code, extractReactiveVariableReferences)
+        ]
+
+        let result = ''
+        let start = 0
+        let ref
+        while ((ref = references.shift())) {
+          result += code.slice(start, ref.node.range[0])
+          result += `/*>*/`
+          result += code.slice(...ref.node.range)
+          result += `/*<${JSON.stringify({
+            escape: ref.escape,
+            method: ref.method
+          })}*/`
+          start = ref.node.range[1]
+        }
+        result += code.slice(start)
+
+        const actual = result
+
+        // update fixture
+        // fs.writeFileSync(
+        //   path.join(REACTIVE_VARS_FIXTURE_ROOT, name, 'result.js'),
+        //   actual,
+        //   'utf8'
+        // )
+
+        const expected = fs.readFileSync(
+          path.join(REACTIVE_VARS_FIXTURE_ROOT, name, 'result.js'),
+          'utf8'
+        )
+        assert.strictEqual(actual, expected)
+      })
+    })
+  }
+})