Repository: vuejs/eslint-plugin-vue
Pull request #2110 (2023-03-21 08:44:54)

Add new `vue/enforce-style-attribute` rule
This PR adds a new rule as proposed in [this issue](https://github.com/vuejs/eslint-plugin-vue/issues/2109).

mussinbenarbia: @ota-meshi Sorry for the very long absence 🙏 I made some modifications to the code to mimick how it's done in the `eslint-plugin-vue-scoped-css` plugin. Could you please take a look at the implementation? I have not updated the docs as I would like to make sure you agree with the implementation first.

Thank you 🙇 
ota-meshi: That rule sounds good to me. Could you undraft this PR when it is ready for review?
mussinbenarbia: @ota-meshi Thank you for taking a look! I will update the docs to reflect the new options and mark it as ready for review.
mussinbenarbia: @ota-meshi (Looping in @FloEdelmann as well) I updated the docs but I have a question. When I run `docs:watch` or `docs:build`, the following piece of text gets automatically added to my doc file for the rule:

```
- :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.
```

I'm not sure why. This rule does not have a fixer at the moment, so that paragraph is not required but it keeps being added automatically. Is it ok to remove it and push the code? Is that not going to make the CI fail?
mussinbenarbia: Also, I'm now wondering if the `allows` option should actually be singular form `allow` to keep some form of consistency with other rules such as [no-shadow](https://github.com/eslint/eslint/blob/main/docs/src/rules/no-shadow.md) 🤔 Do you agree?

Patch:
diff --git a/docs/rules/define-macros-order.md b/docs/rules/define-macros-order.md
index 37074f05..24d25c56 100644
--- a/docs/rules/define-macros-order.md
+++ b/docs/rules/define-macros-order.md
@@ -20,12 +20,14 @@ This rule reports the `defineProps` and `defineEmits` compiler macros when they
 ```json
 {
   "vue/define-macros-order": ["error", {
-    "order": ["defineProps", "defineEmits"]
+    "order": ["defineProps", "defineEmits"],
+    "defineExposeLast": false
   }]
 }
 ```
 
 - `order` (`string[]`) ... The order of defineEmits and defineProps macros. You can also add `"defineOptions"` and `"defineSlots"`.
+- `defineExposeLast` (`boolean`) ... Force `defineExpose` at the end.
 
 ### `{ "order": ["defineProps", "defineEmits"] }` (default)
 
@@ -111,6 +113,36 @@ const slots = defineSlots()
 
 </eslint-code-block>
 
+### `{ "defineExposeLast": true }`
+
+<eslint-code-block fix :rules="{'vue/define-macros-order': ['error', {defineExposeLast: true}]}">
+
+```vue
+<!-- ✓ GOOD -->
+<script setup>
+defineProps(/* ... */)
+defineEmits(/* ... */)
+const slots = defineSlots()
+defineExpose({/* ... */})
+</script>
+```
+
+</eslint-code-block>
+
+<eslint-code-block fix :rules="{'vue/define-macros-order': ['error', {defineExposeLast: true}]}">
+
+```vue
+<!-- ✗ BAD -->
+<script setup>
+defineProps(/* ... */)
+defineEmits(/* ... */)
+defineExpose({/* ... */})
+const slots = defineSlots()
+</script>
+```
+
+</eslint-code-block>
+
 ## :rocket: Version
 
 This rule was introduced in eslint-plugin-vue v8.7.0
diff --git a/docs/rules/enforce-style-attribute.md b/docs/rules/enforce-style-attribute.md
new file mode 100644
index 00000000..6dc04235
--- /dev/null
+++ b/docs/rules/enforce-style-attribute.md
@@ -0,0 +1,86 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/enforce-style-attribute
+description: enforce or forbid the use of the `scoped` and `module` attributes in SFC top level style tags
+---
+
+# vue/enforce-style-attribute
+
+> enforce or forbid the use of the `scoped` and `module` attributes in SFC top level style tags
+
+- :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 allows you to explicitly allow the use of the `scoped` and `module` attributes on your top level style tags.
+
+### `"scoped"`
+
+<eslint-code-block :rules="{'vue/enforce-style-attribute': ['error', { allow: ['scoped'] }]}">
+
+```vue
+<!-- ✓ GOOD -->
+<style scoped></style>
+<style lang="scss" src="../path/to/style.scss" scoped></style>
+
+<!-- ✗ BAD -->
+<style module></style>
+
+<!-- ✗ BAD -->
+<style></style>
+```
+
+</eslint-code-block>
+
+### `"module"`
+
+<eslint-code-block :rules="{'vue/enforce-style-attribute': ['error', { allow: ['module'] }]}">
+
+```vue
+<!-- ✓ GOOD -->
+<style module></style>
+
+<!-- ✗ BAD -->
+<style scoped></style>
+
+<!-- ✗ BAD -->
+<style></style>
+```
+
+</eslint-code-block>
+
+### `"plain"`
+
+<eslint-code-block :rules="{'vue/enforce-style-attribute': ['error', { allow: ['plain']}]}">
+
+```vue
+<!-- ✓ GOOD -->
+<style></style>
+
+<!-- ✗ BAD -->
+<style scoped></style>
+
+<!-- ✗ BAD -->
+<style module></style>
+```
+
+</eslint-code-block>
+
+## :wrench: Options
+
+```json
+{
+  "vue/enforce-style-attribute": [
+    "error",
+    { "allow": ["scoped", "module", "plain"] }
+  ]
+}
+```
+
+- `"allow"` (`["scoped" | "module" | "plain"]`) Array of attributes to allow on a top level style tag. The option `plain` is used to allow style tags that have neither the `scoped` nor `module` attributes. Default: `["scoped"]`
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/enforce-style-attribute.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/enforce-style-attribute.js)
diff --git a/docs/rules/html-closing-bracket-newline.md b/docs/rules/html-closing-bracket-newline.md
index ac6c505d..f47d4ac7 100644
--- a/docs/rules/html-closing-bracket-newline.md
+++ b/docs/rules/html-closing-bracket-newline.md
@@ -5,6 +5,7 @@ title: vue/html-closing-bracket-newline
 description: require or disallow a line break before tag's closing brackets
 since: v4.1.0
 ---
+
 # vue/html-closing-bracket-newline
 
 > require or disallow a line break before tag's closing brackets
@@ -58,19 +59,31 @@ This rule aims to warn the right angle brackets which are at the location other
 
 ```json
 {
-  "vue/html-closing-bracket-newline": ["error", {
-    "singleline": "never",
-    "multiline": "always"
-  }]
+  "vue/html-closing-bracket-newline": [
+    "error",
+    {
+      "singleline": "never",
+      "multiline": "always",
+      "selfClosingTag": {
+        "singleline": "never",
+        "multiline": "always"
+      }
+    }
+  ]
 }
 ```
 
-- `singleline` ... the configuration for single-line elements. It's a single-line element if the element does not have attributes or the last attribute is on the same line as the opening bracket.
-  - `"never"` (default) ... disallow line breaks before the closing bracket.
-  - `"always"` ... require one line break before the closing bracket.
-- `multiline` ... the configuration for multiline elements. It's a multiline element if the last attribute is not on the same line of the opening bracket.
-  - `"never"` ... disallow line breaks before the closing bracket.
-  - `"always"` (default) ... require one line break before the closing bracket.
+- `singleline` (`"never"` by default) ... the configuration for single-line elements. It's a single-line element if the element does not have attributes or the last attribute is on the same line as the opening bracket.
+- `multiline` (`"always"` by default) ... the configuration for multiline elements. It's a multiline element if the last attribute is not on the same line of the opening bracket.
+- `selfClosingTag.singleline` ... the configuration for single-line self closing elements.
+- `selfClosingTag.multiline` ... the configuration for multiline self closing elements.
+
+Every option can be set to one of the following values:
+
+- `"always"` ... require one line break before the closing bracket.
+- `"never"` ... disallow line breaks before the closing bracket.
+
+If `selfClosingTag` is not specified, the `singleline` and `multiline` options are inherited for self-closing tags.
 
 Plus, you can use [`vue/html-indent`](./html-indent.md) rule to enforce indent-level of the closing brackets.
 
@@ -95,6 +108,25 @@ Plus, you can use [`vue/html-indent`](./html-indent.md) rule to enforce indent-l
 
 </eslint-code-block>
 
+### `"selfClosingTag": { "multiline": "always" }`
+
+<eslint-code-block fix :rules="{'vue/html-closing-bracket-newline': ['error', { 'selfClosingTag': {'multiline': 'always'} }]}">
+
+```vue
+<template>
+  <!-- ✓ GOOD -->
+  <MyComponent
+      :foo="foo"
+  />
+
+  <!-- ✗ BAD -->
+  <MyComponent
+      :foo="foo" />
+</template>
+```
+
+</eslint-code-block>
+
 ## :rocket: Version
 
 This rule was introduced in eslint-plugin-vue v4.1.0
diff --git a/docs/rules/index.md b/docs/rules/index.md
index 71a05d42..4ff1cebb 100644
--- a/docs/rules/index.md
+++ b/docs/rules/index.md
@@ -62,7 +62,7 @@ Rules in this category are enabled for all presets provided by eslint-plugin-vue
 | [vue/no-deprecated-slot-attribute](./no-deprecated-slot-attribute.md) | disallow deprecated `slot` attribute (in Vue.js 2.6.0+) | :wrench: | :three::hammer: |
 | [vue/no-deprecated-slot-scope-attribute](./no-deprecated-slot-scope-attribute.md) | disallow deprecated `slot-scope` attribute (in Vue.js 2.6.0+) | :wrench: | :three::hammer: |
 | [vue/no-deprecated-v-bind-sync](./no-deprecated-v-bind-sync.md) | disallow use of deprecated `.sync` modifier on `v-bind` directive (in Vue.js 3.0.0+) | :wrench: | :three::warning: |
-| [vue/no-deprecated-v-is](./no-deprecated-v-is.md) | disallow deprecated `v-is` directive (in Vue.js 3.1.0+) | :wrench: | :three::hammer: |
+| [vue/no-deprecated-v-is](./no-deprecated-v-is.md) | disallow deprecated `v-is` directive (in Vue.js 3.1.0+) |  | :three::hammer: |
 | [vue/no-deprecated-v-on-native-modifier](./no-deprecated-v-on-native-modifier.md) | disallow using deprecated `.native` modifiers (in Vue.js 3.0.0+) |  | :three::warning: |
 | [vue/no-deprecated-v-on-number-modifiers](./no-deprecated-v-on-number-modifiers.md) | disallow using deprecated number (keycode) modifiers (in Vue.js 3.0.0+) | :wrench: | :three::warning: |
 | [vue/no-deprecated-vue-config-keycodes](./no-deprecated-vue-config-keycodes.md) | disallow using deprecated `Vue.config.keyCodes` (in Vue.js 3.0.0+) |  | :three::warning: |
@@ -215,6 +215,7 @@ For example:
 | [vue/define-emits-declaration](./define-emits-declaration.md) | enforce declaration style of `defineEmits` |  | :hammer: |
 | [vue/define-macros-order](./define-macros-order.md) | enforce order of `defineEmits` and `defineProps` compiler macros | :wrench: | :lipstick: |
 | [vue/define-props-declaration](./define-props-declaration.md) | enforce declaration style of `defineProps` |  | :hammer: |
+| [vue/enforce-style-attribute](./enforce-style-attribute.md) | enforce or forbid the use of the `scoped` and `module` attributes in SFC top level style tags |  | :hammer: |
 | [vue/html-button-has-type](./html-button-has-type.md) | disallow usage of button without an explicit type attribute |  | :hammer: |
 | [vue/html-comment-content-newline](./html-comment-content-newline.md) | enforce unified line brake in HTML comments | :wrench: | :lipstick: |
 | [vue/html-comment-content-spacing](./html-comment-content-spacing.md) | enforce unified spacing in HTML comments | :wrench: | :lipstick: |
@@ -225,7 +226,7 @@ For example:
 | [vue/new-line-between-multi-line-property](./new-line-between-multi-line-property.md) | enforce new lines between multi-line properties in Vue components | :wrench: | :lipstick: |
 | [vue/next-tick-style](./next-tick-style.md) | enforce Promise or callback style in `nextTick` | :wrench: | :hammer: |
 | [vue/no-bare-strings-in-template](./no-bare-strings-in-template.md) | disallow the use of bare strings in `<template>` |  | :hammer: |
-| [vue/no-boolean-default](./no-boolean-default.md) | disallow boolean defaults | :wrench: | :hammer: |
+| [vue/no-boolean-default](./no-boolean-default.md) | disallow boolean defaults |  | :hammer: |
 | [vue/no-deprecated-model-definition](./no-deprecated-model-definition.md) | disallow deprecated `model` definition (in Vue.js 3.0.0+) | :bulb: | :warning: |
 | [vue/no-duplicate-attr-inheritance](./no-duplicate-attr-inheritance.md) | enforce `inheritAttrs` to be set to `false` when using `v-bind="$attrs"` |  | :hammer: |
 | [vue/no-empty-component-block](./no-empty-component-block.md) | disallow the `<template>` `<script>` `<style>` block to be empty |  | :hammer: |
diff --git a/docs/rules/no-boolean-default.md b/docs/rules/no-boolean-default.md
index d0782c60..8ad4ace7 100644
--- a/docs/rules/no-boolean-default.md
+++ b/docs/rules/no-boolean-default.md
@@ -9,15 +9,13 @@ since: v7.0.0
 
 > disallow boolean defaults
 
-- :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.
-
 The rule prevents Boolean props from having a default value.
 
 ## :book: Rule Details
 
 The rule is to enforce the HTML standard of always defaulting boolean attributes to false.
 
-<eslint-code-block fix :rules="{'vue/no-boolean-default': ['error']}">
+<eslint-code-block :rules="{'vue/no-boolean-default': ['error']}">
 
 ```vue
 <script>
diff --git a/docs/rules/no-deprecated-v-is.md b/docs/rules/no-deprecated-v-is.md
index df5c7ede..1e5af3f4 100644
--- a/docs/rules/no-deprecated-v-is.md
+++ b/docs/rules/no-deprecated-v-is.md
@@ -10,7 +10,6 @@ since: v7.11.0
 > disallow deprecated `v-is` directive (in Vue.js 3.1.0+)
 
 - :gear: This rule is included in all of `"plugin:vue/vue3-essential"`, `"plugin:vue/vue3-strongly-recommended"` and `"plugin:vue/vue3-recommended"`.
-- :wrench: The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) can automatically fix some of the problems reported by this rule.
 
 ## :book: Rule Details
 
@@ -18,7 +17,7 @@ This rule reports deprecated `v-is` directive in Vue.js v3.1.0+.
 
 Use [`is` attribute with `vue:` prefix](https://vuejs.org/api/built-in-special-attributes.html#is) instead.
 
-<eslint-code-block fix :rules="{'vue/no-deprecated-v-is': ['error']}">
+<eslint-code-block :rules="{'vue/no-deprecated-v-is': ['error']}">
 
 ```vue
 <template>
diff --git a/docs/rules/no-template-shadow.md b/docs/rules/no-template-shadow.md
index 5bddba45..0dcfb217 100644
--- a/docs/rules/no-template-shadow.md
+++ b/docs/rules/no-template-shadow.md
@@ -36,13 +36,13 @@ This rule aims to eliminate shadowed variable declarations of v-for directives o
 </template>
 
 <script>
-  export default {
-    data () {
-      return {
-        l: false
-      }
+export default {
+  data() {
+    return {
+      l: false
     }
   }
+}
 </script>
 ```
 
@@ -50,7 +50,37 @@ This rule aims to eliminate shadowed variable declarations of v-for directives o
 
 ## :wrench: Options
 
-Nothing.
+This rule takes one optional object option, with the property `"allow"`.
+
+```json
+{
+  "no-template-shadow": ["error", { "allow": [] }]
+}
+```
+
+- `"allow"` (`[string]`) Array of identifier names for which shadowing is allowed.
+
+Examples of correct code for the `{ "allow": ["i"] }` option:
+
+<eslint-code-block :rules="{'vue/no-template-shadow': ['error', { allow: ['i'] }]}">
+
+```vue
+<template>
+  <div v-for="i in 5"></div>
+</template>
+
+<script>
+export default {
+  data() {
+    return {
+      i: 'some value'
+    }
+  }
+}
+</script>
+```
+
+</eslint-code-block>
 
 ## :rocket: Version
 
diff --git a/docs/rules/no-v-text-v-html-on-component.md b/docs/rules/no-v-text-v-html-on-component.md
index 420eefda..376974b0 100644
--- a/docs/rules/no-v-text-v-html-on-component.md
+++ b/docs/rules/no-v-text-v-html-on-component.md
@@ -36,7 +36,33 @@ If you use v-text / v-html on a component, it will overwrite the component's con
 
 ## :wrench: Options
 
-Nothing.
+```json
+{
+  "vue/no-v-text-v-html-on-component": [
+    "error",
+    { "allow": ["router-link", "nuxt-link"] }
+  ]
+}
+```
+
+- `allow` (`string[]`) ... Specify a list of custom components for which the rule should not apply.
+
+### `{ "allow": ["router-link", "nuxt-link"] }`
+
+<eslint-code-block :rules="{'vue/no-v-text-v-html-on-component': ['error', { allow: ['router-link', 'nuxt-link'] }]}">
+
+```vue
+<template>
+  <!-- ✓ GOOD -->
+  <router-link v-html="content" />
+  <NuxtLink v-html="content" />
+  
+  <!-- ✗ BAD -->
+  <MyComponent v-html="content" />
+</template>
+```
+
+</eslint-code-block>
 
 ## :rocket: Version
 
diff --git a/eslint-internal-rules/no-invalid-meta-docs-categories.js b/eslint-internal-rules/no-invalid-meta-docs-categories.js
index 03df5eb5..bfdd8cdd 100644
--- a/eslint-internal-rules/no-invalid-meta-docs-categories.js
+++ b/eslint-internal-rules/no-invalid-meta-docs-categories.js
@@ -112,6 +112,7 @@ module.exports = {
       description: 'enforce correct use of `meta` property in core rules',
       categories: ['Internal']
     },
+    // eslint-disable-next-line eslint-plugin/require-meta-fixable -- fixer is not recognized
     fixable: 'code',
     schema: [],
     messages: {
diff --git a/eslint.config.js b/eslint.config.js
index 3407dd3b..f640b670 100644
--- a/eslint.config.js
+++ b/eslint.config.js
@@ -142,6 +142,10 @@ module.exports = [
       'prefer-const': 2,
 
       'prettier/prettier': 'error',
+      'eslint-plugin/require-meta-fixable': [
+        'error',
+        { catchNoFixerButFixableProperty: true }
+      ],
       'eslint-plugin/report-message-format': ['error', "^[A-Z`'{].*\\.$"],
 
       'no-debugger': 'error',
diff --git a/lib/index.js b/lib/index.js
index 3497a7b4..3eb5208c 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -35,6 +35,7 @@ module.exports = {
     'define-props-declaration': require('./rules/define-props-declaration'),
     'dot-location': require('./rules/dot-location'),
     'dot-notation': require('./rules/dot-notation'),
+    'enforce-style-attribute': require('./rules/enforce-style-attribute'),
     eqeqeq: require('./rules/eqeqeq'),
     'first-attribute-linebreak': require('./rules/first-attribute-linebreak'),
     'func-call-spacing': require('./rules/func-call-spacing'),
diff --git a/lib/rules/define-macros-order.js b/lib/rules/define-macros-order.js
index 5a678db4..b55c5e8c 100644
--- a/lib/rules/define-macros-order.js
+++ b/lib/rules/define-macros-order.js
@@ -95,8 +95,12 @@ function create(context) {
   const options = context.options
   /** @type {[string, string]} */
   const order = (options[0] && options[0].order) || DEFAULT_ORDER
+  /** @type {boolean} */
+  const defineExposeLast = (options[0] && options[0].defineExposeLast) || false
   /** @type {Map<string, ASTNode>} */
   const macrosNodes = new Map()
+  /** @type {ASTNode} */
+  let defineExposeNode
 
   return utils.compositingVisitors(
     utils.defineScriptSetupVisitor(context, {
@@ -111,6 +115,9 @@ function create(context) {
       },
       onDefineSlotsExit(node) {
         macrosNodes.set(MACROS_SLOTS, getDefineMacrosStatement(node))
+      },
+      onDefineExposeExit(node) {
+        defineExposeNode = getDefineMacrosStatement(node)
       }
     }),
     {
@@ -131,6 +138,14 @@ function create(context) {
             (data) => utils.isDef(data.node)
           )
 
+        // check last node
+        if (defineExposeLast) {
+          const lastNode = program.body[program.body.length - 1]
+          if (defineExposeNode && lastNode !== defineExposeNode) {
+            reportExposeNotOnBottom(defineExposeNode, lastNode)
+          }
+        }
+
         for (const [index, should] of orderedList.entries()) {
           const targetStatement = program.body[firstStatementIndex + index]
 
@@ -172,6 +187,58 @@ function create(context) {
     })
   }
 
+  /**
+   * @param {ASTNode} node
+   * @param {ASTNode} lastNode
+   */
+  function reportExposeNotOnBottom(node, lastNode) {
+    context.report({
+      node,
+      loc: node.loc,
+      messageId: 'defineExposeNotTheLast',
+      suggest: [
+        {
+          messageId: 'putExposeAtTheLast',
+          fix(fixer) {
+            return moveNodeToLast(fixer, node, lastNode)
+          }
+        }
+      ]
+    })
+  }
+
+  /**
+   * Move all lines of "node" with its comments to after the "target"
+   * @param {RuleFixer} fixer
+   * @param {ASTNode} node
+   * @param {ASTNode} target
+   */
+  function moveNodeToLast(fixer, node, target) {
+    // get comments under tokens(if any)
+    const beforeNodeToken = sourceCode.getTokenBefore(node)
+    const nodeComment = sourceCode.getTokenAfter(beforeNodeToken, {
+      includeComments: true
+    })
+    const nextNodeComment = sourceCode.getTokenAfter(node, {
+      includeComments: true
+    })
+
+    // remove position: node (and comments) to next node (and comments)
+    const cutStart = getLineStartIndex(nodeComment, beforeNodeToken)
+    const cutEnd = getLineStartIndex(nextNodeComment, node)
+
+    // insert text: comment + node
+    const textNode = sourceCode.getText(
+      node,
+      node.range[0] - beforeNodeToken.range[1]
+    )
+
+    return [
+      fixer.insertTextAfter(target, textNode),
+      fixer.removeRange([cutStart, cutEnd])
+    ]
+  }
+
   /**
    * Move all lines of "node" with its comments to before the "target"
    * @param {RuleFixer} fixer
@@ -255,6 +322,7 @@ module.exports = {
       url: 'https://eslint.vuejs.org/rules/define-macros-order.html'
     },
     fixable: 'code',
+    hasSuggestions: true,
     schema: [
       {
         type: 'object',
@@ -266,6 +334,9 @@ module.exports = {
             },
             uniqueItems: true,
             additionalItems: false
+          },
+          defineExposeLast: {
+            type: 'boolean'
           }
         },
         additionalProperties: false
@@ -273,7 +344,11 @@ module.exports = {
     ],
     messages: {
       macrosNotOnTop:
-        '{{macro}} should be the first statement in `<script setup>` (after any potential import statements or type definitions).'
+        '{{macro}} should be the first statement in `<script setup>` (after any potential import statements or type definitions).',
+      defineExposeNotTheLast:
+        '`defineExpose` should be the last statement in `<script setup>`.',
+      putExposeAtTheLast:
+        'Put `defineExpose` as the last statement in `<script setup>`.'
     }
   },
   create
diff --git a/lib/rules/enforce-style-attribute.js b/lib/rules/enforce-style-attribute.js
new file mode 100644
index 00000000..b4b39a73
--- /dev/null
+++ b/lib/rules/enforce-style-attribute.js
@@ -0,0 +1,153 @@
+/**
+ * @author Mussin Benarbia
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+const { isVElement } = require('../utils')
+
+/**
+ * check whether a tag has the `scoped` attribute
+ * @param {VElement} componentBlock
+ */
+function isScoped(componentBlock) {
+  return componentBlock.startTag.attributes.some(
+    (attribute) => !attribute.directive && attribute.key.name === 'scoped'
+  )
+}
+
+/**
+ * check whether a tag has the `module` attribute
+ * @param {VElement} componentBlock
+ */
+function isModule(componentBlock) {
+  return componentBlock.startTag.attributes.some(
+    (attribute) => !attribute.directive && attribute.key.name === 'module'
+  )
+}
+
+/**
+ * check if a tag doesn't have either the `scoped` nor `module` attribute
+ * @param {VElement} componentBlock
+ */
+function isPlain(componentBlock) {
+  return !isScoped(componentBlock) && !isModule(componentBlock)
+}
+
+function getUserDefinedAllowedAttrs(context) {
+  if (context.options[0] && context.options[0].allow) {
+    return context.options[0].allow
+  }
+  return []
+}
+
+const defaultAllowedAttrs = ['scoped']
+
+module.exports = {
+  meta: {
+    type: 'suggestion',
+    docs: {
+      description:
+        'enforce or forbid the use of the `scoped` and `module` attributes in SFC top level style tags',
+      categories: undefined,
+      url: 'https://eslint.vuejs.org/rules/enforce-style-attribute.html'
+    },
+    fixable: null,
+    schema: [
+      {
+        type: 'object',
+        properties: {
+          allow: {
+            type: 'array',
+            minItems: 1,
+            uniqueItems: true,
+            items: {
+              type: 'string',
+              enum: ['plain', 'scoped', 'module']
+            }
+          }
+        },
+        additionalProperties: false
+      }
+    ],
+    messages: {
+      notAllowedScoped:
+        'The scoped attribute is not allowed. Allowed: {{ allowedAttrsString }}.',
+      notAllowedModule:
+        'The module attribute is not allowed. Allowed: {{ allowedAttrsString }}.',
+      notAllowedPlain:
+        'Plain <style> tags are not allowed. Allowed: {{ allowedAttrsString }}.'
+    }
+  },
+
+  /** @param {RuleContext} context */
+  create(context) {
+    const sourceCode = context.getSourceCode()
+    if (!sourceCode.parserServices.getDocumentFragment) {
+      return {}
+    }
+    const documentFragment = sourceCode.parserServices.getDocumentFragment()
+    if (!documentFragment) {
+      return {}
+    }
+
+    const topLevelElements = documentFragment.children.filter(isVElement)
+    const topLevelStyleTags = topLevelElements.filter(
+      (element) => element.rawName === 'style'
+    )
+
+    if (topLevelStyleTags.length === 0) {
+      return {}
+    }
+
+    const userDefinedAllowedAttrs = getUserDefinedAllowedAttrs(context)
+    const allowedAttrs =
+      userDefinedAllowedAttrs.length > 0
+        ? userDefinedAllowedAttrs
+        : defaultAllowedAttrs
+
+    const allowsPlain = allowedAttrs.includes('plain')
+    const allowsScoped = allowedAttrs.includes('scoped')
+    const allowsModule = allowedAttrs.includes('module')
+    const allowedAttrsString = [...allowedAttrs].sort().join(', ')
+
+    return {
+      Program() {
+        for (const styleTag of topLevelStyleTags) {
+          if (!allowsPlain && isPlain(styleTag)) {
+            context.report({
+              node: styleTag,
+              messageId: 'notAllowedPlain',
+              data: {
+                allowedAttrsString
+              }
+            })
+            return
+          }
+
+          if (!allowsScoped && isScoped(styleTag)) {
+            context.report({
+              node: styleTag,
+              messageId: 'notAllowedScoped',
+              data: {
+                allowedAttrsString
+              }
+            })
+            return
+          }
+
+          if (!allowsModule && isModule(styleTag)) {
+            context.report({
+              node: styleTag,
+              messageId: 'notAllowedModule',
+              data: {
+                allowedAttrsString
+              }
+            })
+            return
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lib/rules/html-closing-bracket-newline.js b/lib/rules/html-closing-bracket-newline.js
index c353c35d..58f7fc8e 100644
--- a/lib/rules/html-closing-bracket-newline.js
+++ b/lib/rules/html-closing-bracket-newline.js
@@ -24,6 +24,45 @@ function getPhrase(lineBreaks) {
   }
 }
 
+/**
+ * @typedef LineBreakBehavior
+ * @type {('always'|'never')}
+ */
+
+/**
+ * @typedef LineType
+ * @type {('singleline'|'multiline')}
+ */
+
+/**
+ * @typedef RuleOptions
+ * @type {object}
+ * @property {LineBreakBehavior} singleline - The behavior for single line tags.
+ * @property {LineBreakBehavior} multiline - The behavior for multiline tags.
+ * @property {object} selfClosingTag
+ * @property {LineBreakBehavior} selfClosingTag.singleline - The behavior for single line self closing tags.
+ * @property {LineBreakBehavior} selfClosingTag.multiline - The behavior for multiline self closing tags.
+ */
+
+/**
+ * @param {VStartTag | VEndTag} node - The node representing a start or end tag.
+ * @param {RuleOptions} options - The options for line breaks.
+ * @param {LineType} type - The type of line break.
+ * @returns {number} - The expected line breaks.
+ */
+function getExpectedLineBreaks(node, options, type) {
+  const isSelfClosingTag = node.type === 'VStartTag' && node.selfClosing
+  if (
+    isSelfClosingTag &&
+    options.selfClosingTag &&
+    options.selfClosingTag[type]
+  ) {
+    return options.selfClosingTag[type] === 'always' ? 1 : 0
+  }
+
+  return options[type] === 'always' ? 1 : 0
+}
+
 module.exports = {
   meta: {
     type: 'layout',
@@ -39,7 +78,16 @@ module.exports = {
         type: 'object',
         properties: {
           singleline: { enum: ['always', 'never'] },
-          multiline: { enum: ['always', 'never'] }
+          multiline: { enum: ['always', 'never'] },
+          selfClosingTag: {
+            type: 'object',
+            properties: {
+              singleline: { enum: ['always', 'never'] },
+              multiline: { enum: ['always', 'never'] }
+            },
+            additionalProperties: false,
+            minProperties: 1
+          }
         },
         additionalProperties: false
       }
@@ -80,7 +128,9 @@ module.exports = {
           node.loc.start.line === prevToken.loc.end.line
             ? 'singleline'
             : 'multiline'
-        const expectedLineBreaks = options[type] === 'always' ? 1 : 0
+
+        const expectedLineBreaks = getExpectedLineBreaks(node, options, type)
+
         const actualLineBreaks =
           closingBracketToken.loc.start.line - prevToken.loc.end.line
 
diff --git a/lib/rules/html-indent.js b/lib/rules/html-indent.js
index 6a423ecc..397cdc52 100644
--- a/lib/rules/html-indent.js
+++ b/lib/rules/html-indent.js
@@ -29,6 +29,7 @@ module.exports = {
       categories: ['vue3-strongly-recommended', 'strongly-recommended'],
       url: 'https://eslint.vuejs.org/rules/html-indent.html'
     },
+    // eslint-disable-next-line eslint-plugin/require-meta-fixable -- fixer is not recognized
     fixable: 'whitespace',
     schema: [
       {
diff --git a/lib/rules/no-boolean-default.js b/lib/rules/no-boolean-default.js
index 66ca114b..6796c032 100644
--- a/lib/rules/no-boolean-default.js
+++ b/lib/rules/no-boolean-default.js
@@ -38,7 +38,7 @@ module.exports = {
       categories: undefined,
       url: 'https://eslint.vuejs.org/rules/no-boolean-default.html'
     },
-    fixable: 'code',
+    fixable: null,
     schema: [
       {
         enum: ['default-false', 'no-default']
diff --git a/lib/rules/no-deprecated-scope-attribute.js b/lib/rules/no-deprecated-scope-attribute.js
index dcb5d627..80bcc02b 100644
--- a/lib/rules/no-deprecated-scope-attribute.js
+++ b/lib/rules/no-deprecated-scope-attribute.js
@@ -15,6 +15,7 @@ module.exports = {
       categories: ['vue3-essential'],
       url: 'https://eslint.vuejs.org/rules/no-deprecated-scope-attribute.html'
     },
+    // eslint-disable-next-line eslint-plugin/require-meta-fixable -- fixer is not recognized
     fixable: 'code',
     schema: [],
     messages: {
diff --git a/lib/rules/no-deprecated-slot-attribute.js b/lib/rules/no-deprecated-slot-attribute.js
index f4c89353..664c1435 100644
--- a/lib/rules/no-deprecated-slot-attribute.js
+++ b/lib/rules/no-deprecated-slot-attribute.js
@@ -15,6 +15,7 @@ module.exports = {
       categories: ['vue3-essential'],
       url: 'https://eslint.vuejs.org/rules/no-deprecated-slot-attribute.html'
     },
+    // eslint-disable-next-line eslint-plugin/require-meta-fixable -- fixer is not recognized
     fixable: 'code',
     schema: [
       {
diff --git a/lib/rules/no-deprecated-slot-scope-attribute.js b/lib/rules/no-deprecated-slot-scope-attribute.js
index c3a08b8e..20a4f00b 100644
--- a/lib/rules/no-deprecated-slot-scope-attribute.js
+++ b/lib/rules/no-deprecated-slot-scope-attribute.js
@@ -16,6 +16,7 @@ module.exports = {
       categories: ['vue3-essential'],
       url: 'https://eslint.vuejs.org/rules/no-deprecated-slot-scope-attribute.html'
     },
+    // eslint-disable-next-line eslint-plugin/require-meta-fixable -- fixer is not recognized
     fixable: 'code',
     schema: [],
     messages: {
diff --git a/lib/rules/no-deprecated-v-is.js b/lib/rules/no-deprecated-v-is.js
index 7e8d3278..c497b012 100644
--- a/lib/rules/no-deprecated-v-is.js
+++ b/lib/rules/no-deprecated-v-is.js
@@ -15,7 +15,7 @@ module.exports = {
       categories: ['vue3-essential'],
       url: 'https://eslint.vuejs.org/rules/no-deprecated-v-is.html'
     },
-    fixable: 'code',
+    fixable: null,
     schema: [],
     messages: {
       forbiddenVIs: '`v-is` directive is deprecated.'
diff --git a/lib/rules/no-template-shadow.js b/lib/rules/no-template-shadow.js
index 4b7528a0..8be04b9c 100644
--- a/lib/rules/no-template-shadow.js
+++ b/lib/rules/no-template-shadow.js
@@ -20,6 +20,13 @@ const GROUP_NAMES = [
   'setup'
 ]
 
+function isAllowedVarName(context, variableName) {
+  if (context.options[0] && context.options[0].allow) {
+    return context.options[0].allow.includes(variableName)
+  }
+  return false
+}
+
 module.exports = {
   meta: {
     type: 'suggestion',
@@ -30,7 +37,21 @@ module.exports = {
       url: 'https://eslint.vuejs.org/rules/no-template-shadow.html'
     },
     fixable: null,
-    schema: [],
+    schema: [
+      {
+        type: 'object',
+        properties: {
+          allow: {
+            type: 'array',
+            items: {
+              type: 'string'
+            },
+            uniqueItems: true
+          }
+        },
+        additionalProperties: false
+      }
+    ],
     messages: {
       alreadyDeclaredInUpperScope:
         "Variable '{{name}}' is already declared in the upper scope."
@@ -102,6 +123,11 @@ module.exports = {
           for (const variable of node.variables) {
             const varNode = variable.id
             const name = varNode.name
+
+            if (isAllowedVarName(context, name)) {
+              continue
+            }
+
             if (
               scopeStack.nodes.some((node) => node.name === name) ||
               jsVars.has(name)
diff --git a/lib/rules/no-unsupported-features.js b/lib/rules/no-unsupported-features.js
index 0bcd61a5..07123818 100644
--- a/lib/rules/no-unsupported-features.js
+++ b/lib/rules/no-unsupported-features.js
@@ -72,6 +72,7 @@ module.exports = {
       categories: undefined,
       url: 'https://eslint.vuejs.org/rules/no-unsupported-features.html'
     },
+    // eslint-disable-next-line eslint-plugin/require-meta-fixable -- fixer is not recognized
     fixable: 'code',
     schema: [
       {
diff --git a/lib/rules/no-unused-emit-declarations.js b/lib/rules/no-unused-emit-declarations.js
index f96004a3..ee43248c 100644
--- a/lib/rules/no-unused-emit-declarations.js
+++ b/lib/rules/no-unused-emit-declarations.js
@@ -56,6 +56,17 @@ function hasReferenceId(value, setupContext) {
   )
 }
 
+/**
+ * Check if the given name matches emitReferenceIds variable name
+ * @param {string} name
+ * @param {Set<Identifier>} emitReferenceIds
+ * @returns {boolean}
+ */
+function isEmitVariableName(name, emitReferenceIds) {
+  const emitVariable = emitReferenceIds.values().next().value.name
+  return emitVariable === name
+}
+
 module.exports = {
   meta: {
     type: 'suggestion',
@@ -191,6 +202,15 @@ module.exports = {
           }
 
           const { contextReferenceIds, emitReferenceIds } = setupContext
+
+          // verify defineEmits variable in template
+          if (
+            callee.type === 'Identifier' &&
+            isEmitVariableName(callee.name, emitReferenceIds)
+          ) {
+            addEmitCall(node)
+          }
+
           // verify setup(props,{emit}) {emit()}
           addEmitCallByReference(callee, emitReferenceIds, node)
           if (emit && emit.name === 'emit') {
diff --git a/lib/rules/no-unused-properties.js b/lib/rules/no-unused-properties.js
index fd9b0df2..b64c42d3 100644
--- a/lib/rules/no-unused-properties.js
+++ b/lib/rules/no-unused-properties.js
@@ -268,6 +268,7 @@ module.exports = {
       }
       return container
     }
+
     /**
      * @param {string[]} segments
      * @param {Expression} propertyValue
@@ -350,6 +351,7 @@ module.exports = {
           ) {
             continue
           }
+
           if (propertyReferences.hasProperty(property.name)) {
             // used
             if (
@@ -453,8 +455,84 @@ module.exports = {
         }
       }),
       utils.defineVueVisitor(context, {
-        onVueObjectEnter(node) {
-          const container = getVueComponentPropertiesContainer(node)
+        /**
+         * e.g. `mapMutations({ add: 'increment' })`
+         * @param {Property} node
+         * @param {VueObjectData} vueData
+         */
+        'CallExpression[callee.name=/^(mapMutations|mapActions)$/][arguments] ObjectExpression Property'(
+          node,
+          vueData
+        ) {
+          const container = getVueComponentPropertiesContainer(vueData.node)
+
+          container.properties.push({
+            type: 'array',
+            name: utils.getStaticPropertyName(node),
+            groupName: 'methods',
+            node
+          })
+        },
+
+        /**
+         * e.g. `mapMutations(['add'])`
+         * @param {Literal} node
+         * @param {VueObjectData} vueData
+         */
+        'CallExpression[callee.name=/^(mapMutations|mapActions)$/][arguments] ArrayExpression Literal'(
+          node,
+          vueData
+        ) {
+          const container = getVueComponentPropertiesContainer(vueData.node)
+
+          container.properties.push({
+            type: 'array',
+            name: utils.getStringLiteralValue(node),
+            groupName: 'methods',
+            node
+          })
+        },
+
+        /**
+         * e.g. `mapState({ count: state => state.todosCount })`
+         * @param {Property} node
+         * @param {VueObjectData} vueData
+         */
+        'CallExpression[callee.name=/^(mapState|mapGetters)$/][arguments] ObjectExpression Property'(
+          node,
+          vueData
+        ) {
+          const container = getVueComponentPropertiesContainer(vueData.node)
+
+          container.properties.push({
+            type: 'array',
+            name: utils.getStaticPropertyName(node),
+            groupName: 'computed',
+            node
+          })
+        },
+
+        /**
+         * e.g. `mapState(['count'])`
+         * @param {Literal} node
+         * @param {VueObjectData} vueData
+         */
+        'CallExpression[callee.name=/^(mapState|mapGetters)$/][arguments] ArrayExpression Literal'(
+          node,
+          vueData
+        ) {
+          const container = getVueComponentPropertiesContainer(vueData.node)
+
+          container.properties.push({
+            type: 'array',
+            name: utils.getStringLiteralValue(node),
+            groupName: 'computed',
+            node
+          })
+        },
+
+        onVueObjectEnter(node, vueNode) {
+          const container = getVueComponentPropertiesContainer(vueNode.node)
 
           for (const watcherOrExpose of utils.iterateProperties(
             node,
@@ -495,8 +573,10 @@ module.exports = {
               )
             }
           }
+
           container.properties.push(...utils.iterateProperties(node, groups))
         },
+
         /** @param { (FunctionExpression | ArrowFunctionExpression) & { parent: Property }} node */
         'ObjectExpression > Property > :function[params.length>0]'(
           node,
diff --git a/lib/rules/no-v-text-v-html-on-component.js b/lib/rules/no-v-text-v-html-on-component.js
index c517167f..92289f6b 100644
--- a/lib/rules/no-v-text-v-html-on-component.js
+++ b/lib/rules/no-v-text-v-html-on-component.js
@@ -5,6 +5,7 @@
 'use strict'
 
 const utils = require('../utils')
+const casing = require('../utils/casing')
 
 module.exports = {
   meta: {
@@ -15,7 +16,21 @@ module.exports = {
       url: 'https://eslint.vuejs.org/rules/no-v-text-v-html-on-component.html'
     },
     fixable: null,
-    schema: [],
+    schema: [
+      {
+        type: 'object',
+        properties: {
+          allow: {
+            type: 'array',
+            items: {
+              type: 'string'
+            },
+            uniqueItems: true
+          }
+        },
+        additionalProperties: false
+      }
+    ],
     messages: {
       disallow:
         "Using {{directiveName}} on component may break component's content."
@@ -23,13 +38,31 @@ module.exports = {
   },
   /** @param {RuleContext} context */
   create(context) {
+    const options = context.options[0] || {}
+    /** @type {Set<string>} */
+    const allow = new Set(options.allow)
+
+    /**
+     * Check whether the given node is an allowed component or not.
+     * @param {VElement} node The start tag node to check.
+     * @returns {boolean} `true` if the node is an allowed component.
+     */
+    function isAllowedComponent(node) {
+      const componentName = node.rawName
+      return (
+        allow.has(componentName) ||
+        allow.has(casing.pascalCase(componentName)) ||
+        allow.has(casing.kebabCase(componentName))
+      )
+    }
+
     /**
      * Verify for v-text and v-html directive
      * @param {VDirective} node
      */
     function verify(node) {
       const element = node.parent.parent
-      if (utils.isCustomComponent(element)) {
+      if (utils.isCustomComponent(element) && !isAllowedComponent(element)) {
         context.report({
           node,
           loc: node.loc,
diff --git a/lib/rules/padding-line-between-blocks.js b/lib/rules/padding-line-between-blocks.js
index f0765572..d5bbbd86 100644
--- a/lib/rules/padding-line-between-blocks.js
+++ b/lib/rules/padding-line-between-blocks.js
@@ -119,6 +119,7 @@ module.exports = {
       categories: undefined,
       url: 'https://eslint.vuejs.org/rules/padding-line-between-blocks.html'
     },
+    // eslint-disable-next-line eslint-plugin/require-meta-fixable -- fixer is not recognized
     fixable: 'whitespace',
     schema: [
       {
diff --git a/lib/rules/padding-line-between-tags.js b/lib/rules/padding-line-between-tags.js
index 479aed7c..245fea03 100644
--- a/lib/rules/padding-line-between-tags.js
+++ b/lib/rules/padding-line-between-tags.js
@@ -173,6 +173,7 @@ module.exports = {
       categories: undefined,
       url: 'https://eslint.vuejs.org/rules/padding-line-between-tags.html'
     },
+    // eslint-disable-next-line eslint-plugin/require-meta-fixable -- fixer is not recognized
     fixable: 'whitespace',
     schema: [
       {
diff --git a/lib/rules/require-explicit-emits.js b/lib/rules/require-explicit-emits.js
index 7e06bf47..ba5c406b 100644
--- a/lib/rules/require-explicit-emits.js
+++ b/lib/rules/require-explicit-emits.js
@@ -71,6 +71,22 @@ function getNameParamNode(node) {
   return null
 }
 
+/**
+ * Check if the given name matches defineEmitsNode variable name
+ * @param {string} name
+ * @param {CallExpression | undefined} defineEmitsNode
+ * @returns {boolean}
+ */
+function isEmitVariableName(name, defineEmitsNode) {
+  const node = defineEmitsNode?.parent
+
+  if (node?.type === 'VariableDeclarator' && node.id.type === 'Identifier') {
+    return name === node.id.name
+  }
+
+  return false
+}
+
 module.exports = {
   meta: {
     type: 'suggestion',
@@ -251,7 +267,16 @@ module.exports = {
           if (!vueTemplateDefineData) {
             return
           }
-          if (callee.type === 'Identifier' && callee.name === '$emit') {
+
+          // e.g. $emit() / emit() in template
+          if (
+            callee.type === 'Identifier' &&
+            (callee.name === '$emit' ||
+              isEmitVariableName(
+                callee.name,
+                vueTemplateDefineData.defineEmits
+              ))
+          ) {
             verifyEmit(
               vueTemplateDefineData.emits,
               vueTemplateDefineData.props,
diff --git a/lib/rules/script-indent.js b/lib/rules/script-indent.js
index d5fdf541..c9d9d5e9 100644
--- a/lib/rules/script-indent.js
+++ b/lib/rules/script-indent.js
@@ -15,6 +15,7 @@ module.exports = {
       categories: undefined,
       url: 'https://eslint.vuejs.org/rules/script-indent.html'
     },
+    // eslint-disable-next-line eslint-plugin/require-meta-fixable -- fixer is not recognized
     fixable: 'whitespace',
     schema: [
       {
diff --git a/lib/rules/v-if-else-key.js b/lib/rules/v-if-else-key.js
index 0a6ec9cf..75add9ca 100644
--- a/lib/rules/v-if-else-key.js
+++ b/lib/rules/v-if-else-key.js
@@ -103,6 +103,7 @@ module.exports = {
       recommended: false,
       url: 'https://eslint.vuejs.org/rules/v-if-else-key.html'
     },
+    // eslint-disable-next-line eslint-plugin/require-meta-fixable -- fixer is not recognized
     fixable: 'code',
     schema: [],
     messages: {
diff --git a/lib/utils/index.js b/lib/utils/index.js
index 9127fa95..1928571c 100644
--- a/lib/utils/index.js
+++ b/lib/utils/index.js
@@ -1314,6 +1314,8 @@ module.exports = {
    * - `onDefineOptionsExit` ... Event when defineOptions visit ends.
    * - `onDefineSlotsEnter` ... Event when defineSlots is found.
    * - `onDefineSlotsExit` ... Event when defineSlots visit ends.
+   * - `onDefineExposeEnter` ... Event when defineExpose is found.
+   * - `onDefineExposeExit` ... Event when defineExpose visit ends.
    *
    * @param {RuleContext} context The ESLint rule context object.
    * @param {ScriptSetupVisitor} visitor The visitor to traverse the AST nodes.
@@ -1401,6 +1403,13 @@ module.exports = {
         'onDefineSlotsExit',
         (candidateMacro, node) => candidateMacro === node,
         () => undefined
+      ),
+      new MacroListener(
+        'defineExpose',
+        'onDefineExposeEnter',
+        'onDefineExposeExit',
+        (candidateMacro, node) => candidateMacro === node,
+        () => undefined
       )
     ].filter((m) => m.hasListener)
     if (macroListenerList.length > 0) {
diff --git a/lib/utils/property-references.js b/lib/utils/property-references.js
index 4d4e3980..1ed26640 100644
--- a/lib/utils/property-references.js
+++ b/lib/utils/property-references.js
@@ -317,7 +317,22 @@ function definePropertyReferenceExtractor(
           if (parent.object === node) {
             // `arg.foo`
             const name = utils.getStaticPropertyName(parent)
-            if (name) {
+
+            if (
+              name === '$props' &&
+              parent.parent.type === 'MemberExpression'
+            ) {
+              // `$props.arg`
+              const propName = utils.getStaticPropertyName(parent.parent)
+
+              if (!propName) return unknownMemberAsUnreferenced ? NEVER : ANY
+
+              return new PropertyReferencesForMember(
+                parent.parent,
+                propName,
+                withInTemplate
+              )
+            } else if (name) {
               return new PropertyReferencesForMember(
                 parent,
                 name,
@@ -655,8 +670,17 @@ function definePropertyReferenceExtractor(
           references.push(extractFromExpression(id, true))
         }
       } else {
+        const referenceId =
+          id.name === '$props' &&
+          id.parent.type === 'MemberExpression' &&
+          id.parent.property.type === 'Identifier'
+            ? id.parent.property
+            : id
+
         references.push(
-          extractFromName(id.name, id, () => extractFromExpression(id, true))
+          extractFromName(referenceId.name, referenceId, () =>
+            extractFromExpression(referenceId, true)
+          )
         )
       }
     }
diff --git a/typings/eslint-plugin-vue/util-types/utils.ts b/typings/eslint-plugin-vue/util-types/utils.ts
index b2704769..4d8384d6 100644
--- a/typings/eslint-plugin-vue/util-types/utils.ts
+++ b/typings/eslint-plugin-vue/util-types/utils.ts
@@ -44,6 +44,8 @@ export interface ScriptSetupVisitor extends ScriptSetupVisitorBase {
   onDefineOptionsExit?(node: CallExpression): void
   onDefineSlotsEnter?(node: CallExpression): void
   onDefineSlotsExit?(node: CallExpression): void
+  onDefineExposeEnter?(node: CallExpression): void
+  onDefineExposeExit?(node: CallExpression): void
   [query: string]:
     | ((node: VAST.ParamNode) => void)
     | ((node: CallExpression, props: ComponentProp[]) => void)


Test patch:
diff --git a/tests/lib/rules/define-macros-order.js b/tests/lib/rules/define-macros-order.js
index 428e42ea..c58fd516 100644
--- a/tests/lib/rules/define-macros-order.js
+++ b/tests/lib/rules/define-macros-order.js
@@ -27,10 +27,22 @@ const optionsPropsFirst = [
   }
 ]
 
+const optionsExposeLast = [
+  {
+    defineExposeLast: true
+  }
+]
+
 function message(macro) {
   return `${macro} should be the first statement in \`<script setup>\` (after any potential import statements or type definitions).`
 }
 
+const defineExposeNotTheLast =
+  '`defineExpose` should be the last statement in `<script setup>`.'
+
+const putExposeAtBottom =
+  'Put `defineExpose` as the last statement in `<script setup>`.'
+
 tester.run('define-macros-order', rule, {
   valid: [
     {
@@ -170,6 +182,48 @@ tester.run('define-macros-order', rule, {
           order: ['defineOptions', 'defineEmits', 'defineProps', 'defineSlots']
         }
       ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <script setup>
+          import Foo from 'foo'
+          /** props */
+          defineProps(['foo'])
+          /** options */
+          defineOptions({})
+          /** expose */
+          defineExpose({})
+        </script>
+      `,
+      options: optionsExposeLast
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <script setup lang="ts">
+          import Foo from 'foo'
+          /** props */
+          const props = defineProps({
+            test: Boolean
+          })
+          /** emits */
+          defineEmits(['update:foo'])
+          /** slots */
+          const slots = defineSlots()
+          /** expose */
+          defineExpose({})
+        </script>
+      `,
+      options: [
+        {
+          order: ['defineProps', 'defineEmits'],
+          defineExposeLast: true
+        }
+      ],
+      parserOptions: {
+        parser: require.resolve('@typescript-eslint/parser')
+      }
     }
   ],
   invalid: [
@@ -622,6 +676,108 @@ tester.run('define-macros-order', rule, {
           line: 6
         }
       ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <script setup>
+          /** emits */
+          defineEmits(['update:foo'])
+          /** expose */
+          defineExpose({})
+          /** slots */
+          const slots = defineSlots()
+        </script>
+      `,
+      output: null,
+      options: optionsExposeLast,
+      errors: [
+        {
+          message: defineExposeNotTheLast,
+          line: 6,
+          suggestions: [
+            {
+              desc: putExposeAtBottom,
+              output: `
+        <script setup>
+          /** emits */
+          defineEmits(['update:foo'])
+          /** slots */
+          const slots = defineSlots()
+          /** expose */
+          defineExpose({})
+        </script>
+      `
+            }
+          ]
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <script setup>
+          /** emits */
+          defineEmits(['update:foo'])
+          /** expose */
+          defineExpose({})
+          /** options */
+          defineOptions({})
+          /** props */
+          const props = defineProps(['foo'])
+          /** slots */
+          const slots = defineSlots()
+        </script>
+      `,
+      output: `
+        <script setup>
+          /** options */
+          defineOptions({})
+          /** emits */
+          defineEmits(['update:foo'])
+          /** expose */
+          defineExpose({})
+          /** props */
+          const props = defineProps(['foo'])
+          /** slots */
+          const slots = defineSlots()
+        </script>
+      `,
+      options: [
+        {
+          order: ['defineOptions', 'defineEmits', 'defineProps'],
+          defineExposeLast: true
+        }
+      ],
+      errors: [
+        {
+          message: defineExposeNotTheLast,
+          line: 6,
+          suggestions: [
+            {
+              desc: putExposeAtBottom,
+              output: `
+        <script setup>
+          /** emits */
+          defineEmits(['update:foo'])
+          /** options */
+          defineOptions({})
+          /** props */
+          const props = defineProps(['foo'])
+          /** slots */
+          const slots = defineSlots()
+          /** expose */
+          defineExpose({})
+        </script>
+      `
+            }
+          ]
+        },
+        {
+          message: message('defineOptions'),
+          line: 8
+        }
+      ]
     }
   ]
 })
diff --git a/tests/lib/rules/enforce-style-attribute.js b/tests/lib/rules/enforce-style-attribute.js
new file mode 100644
index 00000000..994b785e
--- /dev/null
+++ b/tests/lib/rules/enforce-style-attribute.js
@@ -0,0 +1,148 @@
+/**
+ * @author Mussin Benarbia
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+const RuleTester = require('eslint').RuleTester
+const rule = require('../../../lib/rules/enforce-style-attribute')
+
+const tester = new RuleTester({
+  parser: require.resolve('vue-eslint-parser'),
+  parserOptions: {
+    ecmaVersion: 2020,
+    sourceType: 'module'
+  }
+})
+
+tester.run('enforce-style-attribute', rule, {
+  valid: [
+    // Default (scoped)
+    {
+      filename: 'test.vue',
+      code: '<template></template><script></script><style scoped></style>'
+    },
+    // With scoped option
+    {
+      filename: 'test.vue',
+      code: '<template></template><script></script><style scoped></style>',
+      options: [{ allow: ['scoped'] }]
+    },
+    // With module option
+    {
+      filename: 'test.vue',
+      code: '<template></template><script></script><style module></style>',
+      options: [{ allow: ['module'] }]
+    },
+    // With plain option
+    {
+      filename: 'test.vue',
+      code: '<template></template><script></script><style></style>',
+      options: [{ allow: ['plain'] }]
+    },
+    // With all options
+    {
+      filename: 'test.vue',
+      code: '<template></template><script></script><style scoped></style>',
+      options: [{ allow: ['scoped', 'module', 'plain'] }]
+    },
+    {
+      filename: 'test.vue',
+      code: '<template></template><script></script><style module></style>',
+      options: [{ allow: ['scoped', 'module', 'plain'] }]
+    },
+    {
+      filename: 'test.vue',
+      code: '<template></template><script></script><style></style>',
+      options: [{ allow: ['scoped', 'module', 'plain'] }]
+    }
+  ],
+  invalid: [
+    // With default (scoped)
+    {
+      code: `<template></template><script></script><style></style>`,
+      errors: [
+        {
+          message: 'Plain <style> tags are not allowed. Allowed: scoped.'
+        }
+      ]
+    },
+    {
+      code: `<template></template><script></script><style module></style>`,
+      errors: [
+        {
+          message: 'The module attribute is not allowed. Allowed: scoped.'
+        }
+      ]
+    },
+    // With scoped option
+    {
+      code: `<template></template><script></script><style></style>`,
+      options: [{ allow: ['scoped'] }],
+      errors: [
+        {
+          message: 'Plain <style> tags are not allowed. Allowed: scoped.'
+        }
+      ]
+    },
+    {
+      code: `<template></template><script></script><style module></style>`,
+      options: [{ allow: ['scoped'] }],
+      errors: [
+        {
+          message: 'The module attribute is not allowed. Allowed: scoped.'
+        }
+      ]
+    },
+    // With module option
+    {
+      code: `<template></template><script></script><style></style>`,
+      options: [{ allow: ['module'] }],
+      errors: [
+        {
+          message: 'Plain <style> tags are not allowed. Allowed: module.'
+        }
+      ]
+    },
+    {
+      code: `<template></template><script></script><style scoped></style>`,
+      options: [{ allow: ['module'] }],
+      errors: [
+        {
+          message: 'The scoped attribute is not allowed. Allowed: module.'
+        }
+      ]
+    },
+    // With different combinations of two options
+    {
+      code: `<template></template><script></script><style></style>`,
+      options: [{ allow: ['module', 'scoped'] }],
+      errors: [
+        {
+          message:
+            'Plain <style> tags are not allowed. Allowed: module, scoped.'
+        }
+      ]
+    },
+    {
+      code: `<template></template><script></script><style module></style>`,
+      options: [{ allow: ['scoped', 'plain'] }],
+      errors: [
+        {
+          message:
+            'The module attribute is not allowed. Allowed: plain, scoped.'
+        }
+      ]
+    },
+    {
+      code: `<template></template><script></script><style scoped></style>`,
+      options: [{ allow: ['module', 'plain'] }],
+      errors: [
+        {
+          message:
+            'The scoped attribute is not allowed. Allowed: module, plain.'
+        }
+      ]
+    }
+  ]
+})
diff --git a/tests/lib/rules/html-closing-bracket-newline.js b/tests/lib/rules/html-closing-bracket-newline.js
index 523b2d53..3a032ba8 100644
--- a/tests/lib/rules/html-closing-bracket-newline.js
+++ b/tests/lib/rules/html-closing-bracket-newline.js
@@ -116,6 +116,42 @@ tester.run('html-closing-bracket-newline', rule, {
         }
       ]
     },
+    {
+      code: `
+        <template>
+          <MyComp
+            :foo="foo"
+          />
+          <MyComp :foo="foo" />
+        </template>
+      `,
+      options: [
+        {
+          selfClosingTag: {
+            singleline: 'never',
+            multiline: 'always'
+          }
+        }
+      ]
+    },
+    {
+      code: `
+        <template>
+          <MyComp :foo="foo"
+          />
+          <MyComp
+            :foo="foo" />
+        </template>
+      `,
+      options: [
+        {
+          selfClosingTag: {
+            singleline: 'always',
+            multiline: 'never'
+          }
+        }
+      ]
+    },
 
     // Ignore if no closing brackets
     `
@@ -479,6 +515,106 @@ tester.run('html-closing-bracket-newline', rule, {
           endColumn: 23
         }
       ]
+    },
+    {
+      code: `
+        <template>
+          <MyComp
+          />
+        </template>
+      `,
+      output: `
+        <template>
+          <MyComp/>
+        </template>
+      `,
+      options: [
+        {
+          selfClosingTag: {
+            singleline: 'never',
+            multiline: 'always'
+          }
+        }
+      ],
+      errors: [
+        'Expected no line breaks before closing bracket, but 1 line break found.'
+      ]
+    },
+    {
+      code: `
+        <template>
+          <MyComp
+            :foo="foo"/>
+        </template>
+      `,
+      output: `
+        <template>
+          <MyComp
+            :foo="foo"
+/>
+        </template>
+      `,
+      options: [
+        {
+          selfClosingTag: {
+            singleline: 'never',
+            multiline: 'always'
+          }
+        }
+      ],
+      errors: [
+        'Expected 1 line break before closing bracket, but no line breaks found.'
+      ]
+    },
+    {
+      code: `
+        <template>
+          <MyComp :foo="foo"/>
+        </template>
+      `,
+      output: `
+        <template>
+          <MyComp :foo="foo"
+/>
+        </template>
+      `,
+      options: [
+        {
+          selfClosingTag: {
+            singleline: 'always',
+            multiline: 'never'
+          }
+        }
+      ],
+      errors: [
+        'Expected 1 line break before closing bracket, but no line breaks found.'
+      ]
+    },
+    {
+      code: `
+        <template>
+          <MyComp
+            :foo="foo"
+          />
+        </template>
+      `,
+      output: `
+        <template>
+          <MyComp
+            :foo="foo"/>
+        </template>
+      `,
+      options: [
+        {
+          selfClosingTag: {
+            singleline: 'always',
+            multiline: 'never'
+          }
+        }
+      ],
+      errors: [
+        'Expected no line breaks before closing bracket, but 1 line break found.'
+      ]
     }
   ]
 })
diff --git a/tests/lib/rules/no-template-shadow.js b/tests/lib/rules/no-template-shadow.js
index e2ea096b..6844b5f9 100644
--- a/tests/lib/rules/no-template-shadow.js
+++ b/tests/lib/rules/no-template-shadow.js
@@ -155,6 +155,21 @@ ruleTester.run('no-template-shadow', rule, {
       defineProps({k:Number})
       </script>
       `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div v-for="i in 5">
+          <div v-for="i in 5">
+          </div>
+        </div>
+      </template>
+      <script setup>
+      defineProps({i:Number})
+      </script>
+      `,
+      options: [{ allow: ['i'] }]
     }
   ],
 
diff --git a/tests/lib/rules/no-unused-emit-declarations.js b/tests/lib/rules/no-unused-emit-declarations.js
index 0667ad46..10bf3b33 100644
--- a/tests/lib/rules/no-unused-emit-declarations.js
+++ b/tests/lib/rules/no-unused-emit-declarations.js
@@ -98,6 +98,17 @@ tester.run('no-unused-emit-declarations', rule, {
         }
       `
     },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <button @click="emits('bar')">Bar</button>
+      </template>
+      <script setup>
+        const emits = defineEmits(['bar'])
+      </script>
+      `
+    },
     {
       filename: 'test.vue',
       code: `
@@ -237,7 +248,7 @@ tester.run('no-unused-emit-declarations', rule, {
         export default {
           emits: ['foo'],
           setup(_, context) {
-            useCustomComposable({ emit: context.emit }) 
+            useCustomComposable({ emit: context.emit })
           }
         }
       </script>
@@ -282,7 +293,7 @@ tester.run('no-unused-emit-declarations', rule, {
         export default {
           emits: ['foo'],
           setup(_, { emit }) {
-            useCustomComposable({ emit: emit }) 
+            useCustomComposable({ emit: emit })
           }
         }
       </script>
@@ -453,6 +464,25 @@ tester.run('no-unused-emit-declarations', rule, {
         }
       ]
     },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <button @click="emit('bar')">Bar</button>
+      </template>
+      <script setup>
+        const emit = defineEmits(['foo', 'bar'])
+      </script>
+      `,
+      errors: [
+        {
+          messageId: 'unused',
+          line: 6,
+          column: 35,
+          endColumn: 40
+        }
+      ]
+    },
     {
       filename: 'test.vue',
       code: `
diff --git a/tests/lib/rules/no-unused-properties.js b/tests/lib/rules/no-unused-properties.js
index 0139559b..f71a2651 100644
--- a/tests/lib/rules/no-unused-properties.js
+++ b/tests/lib/rules/no-unused-properties.js
@@ -53,39 +53,57 @@ const unreferencedOptions = {
 
 tester.run('no-unused-properties', rule, {
   valid: [
-    // a property used in a script expression
     {
       filename: 'test.vue',
       code: `
         <script>
           export default {
-            props: ['count'],
-            created() {
-              alert(this.count + 1)
+            computed: {
+              ...mapGetters(["bar"]),
+            },
+            methods: {
+              baz() {
+                return this.bar
+              }
             }
-          };
+          }
+        </script>
+        <template>
+          {{ baz() }}
+        </template>
+      `,
+      options: allOptions
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <script>
+          export default {
+            methods: mapMutations({
+              add: 'increment'
+            })
+          }
         </script>
+        <template>
+          {{ add() }}
+        </template>
       `
     },
-    // default options
     {
       filename: 'test.vue',
       code: `
         <script>
           export default {
-            data () {
-              return {
-                foo: 1
-              }
-            },
-            computed: {
-              bar() {}
-            },
             methods: {
-              baz () {}
+              ...mapMutations({
+                add: 'increment'
+              })
             }
-          };
+          }
         </script>
+        <template>
+          {{ add() }}
+        </template>
       `
     },
     {
@@ -93,206 +111,181 @@ tester.run('no-unused-properties', rule, {
       code: `
         <script>
           export default {
-            setup () {
-              return {
-                foo
-              }
-            },
-          };
+            methods: mapMutations(['add'])
+          }
         </script>
+        <template>
+          {{ add() }}
+        </template>
       `
     },
-
-    // a property being watched
     {
       filename: 'test.vue',
       code: `
         <script>
           export default {
-            props: {
-              count: {
-                type: Number,
-                default: 0
+            methods: {
+              methods: {
+                ...mapMutations(['add'])
               }
-            },
-            watch: {
-              count() {
-                alert('Increased!');
-              },
-            },
-          };
+            }
+          }
         </script>
+        <template>
+          {{ add() }}
+        </template>
       `
     },
-
-    // a property used as a template identifier
     {
       filename: 'test.vue',
       code: `
-        <template>
-          <div>{{ count }}</div>
-        </template>
         <script>
           export default {
-            props: ['count']
+            methods: {
+              ...mapActions({
+                add: 'increment'
+              })
+            }
           }
         </script>
+        <template>
+          {{ add() }}
+        </template>
       `
     },
-
-    // properties used in a template expression
     {
       filename: 'test.vue',
       code: `
-        <template>
-          <div>{{ count1 + count2 }}</div>
-        </template>
         <script>
           export default {
-            props: ['count1', 'count2']
-          };
+            methods: mapActions({
+              add: 'increment'
+            })
+          }
         </script>
+        <template>
+          {{ add() }}
+        </template>
       `
     },
-
-    // a property used in v-if
     {
       filename: 'test.vue',
       code: `
-        <template>
-          <div v-if="count > 0"></div>
-        </template>
         <script>
           export default {
-            props: {
-              count: {
-                type: Number
-              }
-            }
-          };
+            methods: mapActions(['add'])
+          }
         </script>
+        <template>
+          {{ add() }}
+        </template>
       `
     },
-
-    // a property used in v-for
     {
       filename: 'test.vue',
       code: `
-        <template>
-          <div v-for="color in colors">{{ color }}</div>
-        </template>
         <script>
           export default {
-            props: {
-              colors: {
-                type: Array,
-                default: () => []
-              }
+            methods: {
+              ...mapActions(['add'])
             }
-          };
+          }
         </script>
+        <template>
+          {{ add() }}
+        </template>
       `
     },
-
-    // a property used in v-html
     {
       filename: 'test.vue',
       code: `
-        <template>
-          <div v-html="message" />
-        </template>
         <script>
           export default {
-            props: ['message']
-          };
+            computed: mapState({
+              count: state => state.todosCount
+            })
+          }
         </script>
+        <template>
+          {{ count }}
+        </template>
       `
     },
-
-    // a property passed in a component
     {
       filename: 'test.vue',
       code: `
-        <template>
-          <counter :count="count" />
-        </template>
         <script>
           export default {
-            props: ['count']
-          };
+            computed: {
+              ...mapState({
+                count: state => state.todosCount
+              })
+            }
+          }
         </script>
+        <template>
+          {{ count }}
+        </template>
       `
     },
-
-    // a property used in v-on
     {
       filename: 'test.vue',
       code: `
-        <template>
-          <button @click="alert(count)" />
-        </template>
         <script>
           export default {
-            props: ['count']
-          };
+            computed: mapState({
+              count (state) {
+                return state.todosCount
+              }
+            })
+          }
         </script>
+        <template>
+          {{ count }}
+        </template>
       `
     },
-
-    // data used in a script expression
     {
       filename: 'test.vue',
       code: `
         <script>
           export default {
-            data () {
-              return {
-                count: 2
-              };
-            },
-            created() {
-              alert(this.count + 1)
+            computed: {
+              ...mapGetters(['count']),
             }
-          };
+          }
         </script>
-      `,
-      options: allOptions
+        <template>
+          {{ count }}
+        </template>
+      `
     },
-
-    // data being watched
     {
       filename: 'test.vue',
       code: `
         <script>
           export default {
-            data() {
-              return {
-                count: 2
-              };
-            },
-            watch: {
-              count() {
-                alert('Increased!');
-              },
-            },
-          };
+            computed: mapGetters(['count1', 'count2'])
+          }
         </script>
-      `,
-      options: allOptions
+        <template>
+          {{ count1 }} {{ count2 }}
+        </template>
+      `
     },
     {
       filename: 'test.vue',
       code: `
         <script>
           export default {
-            props: ['foo'],
-            watch: {
-              'foo.bar'() {
-                alert('Increased!');
-              },
-            },
-          };
+            computed: mapGetters({
+              count: 'count'
+            })
+          }
         </script>
+        <template>
+          {{ count }}
+        </template>
       `
     },
     {
@@ -300,312 +293,253 @@ tester.run('no-unused-properties', rule, {
       code: `
         <script>
           export default {
-            props: ['foo'],
-            watch: {
-              foo: [
-                'bar',
-                {
-                  handler: 'baz'
-                }
-              ],
-            },
-            methods: {
-              bar () {},
-              baz () {},
+            computed: {
+              ...mapGetters({
+                count: 'count'
+              })
             }
-          };
+          }
         </script>
-      `,
-      options: allOptions
+        <template>
+          {{ count }}
+        </template>
+      `
     },
-
-    // data used as a template identifier
     {
       filename: 'test.vue',
       code: `
-        <template>
-          <div>{{ count }}</div>
-        </template>
         <script>
           export default {
-            data () {
-              return {
-                count: 2
-              };
+            computed: {
+              ...mapState(['count'])
             }
           }
         </script>
-      `,
-      options: allOptions
+        <template>
+          {{ count }}
+        </template>
+      `
     },
-
-    // data used in a template expression
+    // a property used in a script expression
     {
       filename: 'test.vue',
       code: `
-        <template>
-          <div>{{ count1 + count2 }}</div>
-        </template>
         <script>
           export default {
-            data () {
-              return {
-                count1: 1,
-                count2: 2
-              };
+            props: ['count'],
+            created() {
+              alert(this.count + 1)
             }
           };
         </script>
-      `,
-      options: allOptions
+      `
     },
-
-    // data used in v-if
     {
       filename: 'test.vue',
       code: `
-        <template>
-          <div v-if="count > 0"></div>
-        </template>
         <script>
           export default {
-            data () {
-              return {
-                count: 2
-              };
+            props: ['count'],
+            created() {
+              alert(this.$props.count + 1)
             }
           };
         </script>
-      `,
-      options: allOptions
+      `
     },
-
-    // data used in v-for
+    // default options
     {
       filename: 'test.vue',
       code: `
-        <template>
-          <div v-for="color in colors">{{ color }}</div>
-        </template>
         <script>
           export default {
             data () {
               return {
-                colors: ["purple", "green"]
-              };
+                foo: 1
+              }
+            },
+            computed: {
+              bar() {}
+            },
+            methods: {
+              baz () {}
             }
           };
         </script>
-      `,
-      options: allOptions
+      `
     },
-
-    // data used in v-html
     {
       filename: 'test.vue',
       code: `
-        <template>
-          <div v-html="message" />
-        </template>
         <script>
           export default {
-            data () {
+            setup () {
               return {
-                message: "<span>Hey!</span>"
-              };
-            }
+                foo
+              }
+            },
           };
         </script>
-      `,
-      options: allOptions
+      `
     },
 
-    // data used in v-model
+    // a property being watched
     {
       filename: 'test.vue',
       code: `
-        <template>
-          <input v-model="count" />
-        </template>
         <script>
           export default {
-            data () {
-              return {
-                count: 2
-              };
-            }
+            props: {
+              count: {
+                type: Number,
+                default: 0
+              }
+            },
+            watch: {
+              count() {
+                alert('Increased!');
+              },
+            },
           };
         </script>
-      `,
-      options: allOptions
+      `
     },
 
-    // data passed in a component
+    // a property used as a template identifier
     {
       filename: 'test.vue',
       code: `
         <template>
-          <counter :count="count" />
+          <div>{{ count }}</div>
         </template>
         <script>
           export default {
-            data () {
-              return {
-                count: 2
-              };
-            }
-          };
+            props: ['count']
+          }
         </script>
-      `,
-      options: allOptions
+      `
     },
-
-    // data used in v-on
+    // a property used as a template $props member expression
     {
       filename: 'test.vue',
       code: `
         <template>
-          <button @click="count++" />
+          <div>{{ $props.count }}</div>
         </template>
         <script>
           export default {
-            data () {
-              return {
-                count: 2
-              };
-            }
-          };
+            props: ['count']
+          }
         </script>
-      `,
-      options: allOptions
+      `
     },
 
-    // computed property used in a script expression
+    // properties used in a template expression
     {
       filename: 'test.vue',
       code: `
+        <template>
+          <div>{{ count1 + count2 }}</div>
+        </template>
         <script>
           export default {
-            computed: {
-              count() {
-                return 2;
-              }
-            },
-            created() {
-              const dummy = this.count + 1;
-            }
+            props: ['count1', 'count2']
           };
         </script>
-      `,
-      options: allOptions
+      `
     },
-
-    // computed property being watched
+    // properties used in a template expression as $props member expression
     {
       filename: 'test.vue',
       code: `
+        <template>
+          <div>{{ $props.count1 + $props.count2 }}</div>
+        </template>
         <script>
           export default {
-            computed: {
-              count() {
-                return 2;
-              }
-            },
-            watch: {
-              count() {
-                alert('Increased!');
-              },
-            },
+            props: ['count1', 'count2']
           };
         </script>
-      `,
-      options: allOptions
+      `
     },
 
-    // computed property used as a template identifier
+    // a property used in v-if
     {
       filename: 'test.vue',
       code: `
         <template>
-          <div>{{ count }}</div>
+          <div v-if="count > 0"></div>
         </template>
         <script>
           export default {
-            computed: {
-              count() {
-                return 2;
+            props: {
+              count: {
+                type: Number
               }
             }
-          }
+          };
         </script>
-      `,
-      options: allOptions
+      `
     },
-
-    // computed properties used in a template expression
+    // a property used in v-if as $props member expression
     {
       filename: 'test.vue',
       code: `
         <template>
-          <div>{{ count1 + count2 }}</div>
+          <div v-if="$props.count > 0"></div>
         </template>
         <script>
           export default {
-            computed: {
-              count1() {
-                return 1;
-              },
-              count2() {
-                return 2;
+            props: {
+              count: {
+                type: Number
               }
             }
-          }
+          };
         </script>
-      `,
-      options: allOptions
+      `
     },
 
-    // computed property used in v-if
+    // a property used in v-for
     {
       filename: 'test.vue',
       code: `
         <template>
-          <div v-if="count > 0"></div>
+          <div v-for="color in colors">{{ color }}</div>
         </template>
         <script>
           export default {
-            computed: {
-              count() {
-                return 2;
+            props: {
+              colors: {
+                type: Array,
+                default: () => []
               }
             }
-          }
+          };
         </script>
-      `,
-      options: allOptions
+      `
     },
-
-    // computed property used in v-for
+    // a property used in v-for as $props member expression
     {
       filename: 'test.vue',
       code: `
         <template>
-          <div v-for="color in colors">{{ color }}</div>
+          <div v-for="color in $props.colors">{{ color }}</div>
         </template>
         <script>
           export default {
-            computed: {
-              colors() {
-                return ["purple", "green"];
+            props: {
+              colors: {
+                type: Array,
+                default: () => []
               }
             }
           };
         </script>
-      `,
-      options: allOptions
+      `
     },
 
-    // computed property used in v-html
+    // a property used in v-html
     {
       filename: 'test.vue',
       code: `
@@ -614,51 +548,27 @@ tester.run('no-unused-properties', rule, {
         </template>
         <script>
           export default {
-            computed: {
-              message() {
-                return "<span>Hey!</span>";
-              }
-            }
+            props: ['message']
           };
         </script>
-      `,
-      options: allOptions
+      `
     },
-
-    // computed property used in v-model
+    // a property used in v-html as $props member expression
     {
       filename: 'test.vue',
       code: `
         <template>
-          <input v-model="fullName" />
+          <div v-html="$props.message" />
         </template>
         <script>
           export default {
-            data() {
-              return {
-                firstName: "David",
-                lastName: "Attenborough"
-              }
-            },
-            computed: {
-              fullName: {
-                get() {
-                  return this.firstName + ' ' + this.lastName
-                },
-                set(newValue) {
-                  var names = newValue.split(' ')
-                  this.firstName = names[0]
-                  this.lastName = names[names.length - 1]
-                }
-              }
-            }
+            props: ['message']
           };
         </script>
-      `,
-      options: allOptions
+      `
     },
 
-    // computed property passed in a component
+    // a property passed in a component
     {
       filename: 'test.vue',
       code: `
@@ -667,51 +577,68 @@ tester.run('no-unused-properties', rule, {
         </template>
         <script>
           export default {
-            computed: {
-              count() {
-                return 2;
-              }
-            }
-          }
+            props: ['count']
+          };
         </script>
-      `,
-      options: allOptions
+      `
     },
-
-    // async data passed in a component
+    // a property passed in a component as $props member expression
     {
       filename: 'test.vue',
       code: `
         <template>
-          <counter :count="foo" />
+          <counter :count="$props.count" />
         </template>
         <script>
           export default {
-            asyncData() {
-              return {
-                foo: 2
-              }
-            }
-          }
+            props: ['count']
+          };
         </script>
-      `,
-      options: allOptions
+      `
     },
 
-    // ignores unused data when marked with eslint-disable
+    // a property used in v-on
     {
       filename: 'test.vue',
       code: `
         <template>
-          <div>{{ cont }}</div>
+          <button @click="alert(count)" />
+        </template>
+        <script>
+          export default {
+            props: ['count']
+          };
+        </script>
+      `
+    },
+    // a property used in v-on as $props member expression
+    {
+      filename: 'test.vue',
+      code: `
+        <template>
+          <button @click="alert($props.count)" />
         </template>
+        <script>
+          export default {
+            props: ['count']
+          };
+        </script>
+      `
+    },
+
+    // data used in a script expression
+    {
+      filename: 'test.vue',
+      code: `
         <script>
           export default {
             data () {
               return {
-                // eslint-disable-next-line
                 count: 2
               };
+            },
+            created() {
+              alert(this.count + 1)
             }
           };
         </script>
@@ -719,35 +646,38 @@ tester.run('no-unused-properties', rule, {
       options: allOptions
     },
 
-    // trace this
+    // data being watched
     {
       filename: 'test.vue',
       code: `
         <script>
           export default {
-            props: ['count'],
-            methods: {
-              click () {
-                const vm = this
-                fn(vm.count)
-              }
-            }
+            data() {
+              return {
+                count: 2
+              };
+            },
+            watch: {
+              count() {
+                alert('Increased!');
+              },
+            },
           };
         </script>
-      `
+      `,
+      options: allOptions
     },
     {
       filename: 'test.vue',
       code: `
         <script>
           export default {
-            props: ['count'],
-            methods: {
-              click () {
-                const vm = this
-                const {count} = vm
-              }
-            }
+            props: ['foo'],
+            watch: {
+              'foo.bar'() {
+                alert('Increased!');
+              },
+            },
           };
         </script>
       `
@@ -757,24 +687,31 @@ tester.run('no-unused-properties', rule, {
       code: `
         <script>
           export default {
-            props: ['count'],
+            props: ['foo'],
+            watch: {
+              foo: [
+                'bar',
+                {
+                  handler: 'baz'
+                }
+              ],
+            },
             methods: {
-              click () {
-                const vm = this
-                let count;
-                ({count} = vm)
-              }
+              bar () {},
+              baz () {},
             }
           };
         </script>
-      `
+      `,
+      options: allOptions
     },
-    // use rest
+
+    // data used as a template identifier
     {
       filename: 'test.vue',
       code: `
         <template>
-          <div>{{ foo }}</div>
+          <div>{{ count }}</div>
         </template>
         <script>
           export default {
@@ -782,139 +719,589 @@ tester.run('no-unused-properties', rule, {
               return {
                 count: 2
               };
-            },
-            methods: {
-              click () {
-                const vm = this
-                const {...r} = vm
-              },
-              focus () {
-                fn(this.foo)
-              },
-              blur () {
-                const {foo} = this
-              },
-              keydown () {
-                let foo;
-                ({foo} = this)
-              }
             }
-          };
+          }
         </script>
-      `
+      `,
+      options: allOptions
     },
 
-    // function trace
+    // data used in a template expression
     {
       filename: 'test.vue',
       code: `
+        <template>
+          <div>{{ count1 + count2 }}</div>
+        </template>
         <script>
           export default {
-            props: ['foo', 'bar', 'baz'],
-            setup (props) {
-              fn(props)
+            data () {
+              return {
+                count1: 1,
+                count2: 2
+              };
             }
-          }
-
-          function fn(p) {
-            return fn2(p)
-          }
-          function fn2(p2) {
-            const {...a} = p2
-          }
+          };
         </script>
-      `
+      `,
+      options: allOptions
     },
+
+    // data used in v-if
     {
       filename: 'test.vue',
       code: `
+        <template>
+          <div v-if="count > 0"></div>
+        </template>
         <script>
           export default {
-            props: ['foo', 'bar', 'baz'],
-            setup (props) {
-              fn(props)
+            data () {
+              return {
+                count: 2
+              };
             }
-          }
-
-          function fn(a) {
-            return a[foo]
-          }
+          };
         </script>
-      `
+      `,
+      options: allOptions
     },
 
-    // render & functional
+    // data used in v-for
     {
-      filename: 'test.js',
+      filename: 'test.vue',
       code: `
-      Vue.component('smart-list', {
-        functional: true,
-        props: {
-          items: {
-            type: Array,
-            required: true
-          },
-          isOrdered: Boolean
-        },
-        render: function (createElement, context) {
-          function appropriateListComponent () {
-            var items = context.props.items
-
-            if (items.length === 0)           return EmptyList
-            if (typeof items[0] === 'object') return TableList
-            if (context.props.isOrdered)      return OrderedList
-
-            return UnorderedList
-          }
-
-          return createElement(
-            appropriateListComponent(),
-            context.data,
-            context.children
-          )
-        }
-      })
-      `
+        <template>
+          <div v-for="color in colors">{{ color }}</div>
+        </template>
+        <script>
+          export default {
+            data () {
+              return {
+                colors: ["purple", "green"]
+              };
+            }
+          };
+        </script>
+      `,
+      options: allOptions
     },
+
+    // data used in v-html
     {
-      filename: 'test.js',
+      filename: 'test.vue',
       code: `
-      Vue.component('MyButton', {
-        functional: true,
-        props: ['foo'],
-        render: function (createElement, {props}) {
-          return createElement('button', props.foo)
-        }
-      })
-      `
+        <template>
+          <div v-html="message" />
+        </template>
+        <script>
+          export default {
+            data () {
+              return {
+                message: "<span>Hey!</span>"
+              };
+            }
+          };
+        </script>
+      `,
+      options: allOptions
     },
-    {
-      filename: 'test.js',
-      code: `
-      Vue.component('MyButton', {
-        functional: true,
-        props: ['foo'],
-        render: function (createElement, ctx) {
-          return createElement('button', fn(ctx.props))
-        }
-      })
 
-      function fn(props) {
-        return props.foo
-      }
-      `
-    },
+    // data used in v-model
     {
-      filename: 'test.js',
+      filename: 'test.vue',
       code: `
-      Vue.component('MyButton', {
-        functional: true,
-        props: ['foo'],
-        render: function (createElement, ctx) {
-          return createElement('button', fn(ctx))
-        }
-      })
-
+        <template>
+          <input v-model="count" />
+        </template>
+        <script>
+          export default {
+            data () {
+              return {
+                count: 2
+              };
+            }
+          };
+        </script>
+      `,
+      options: allOptions
+    },
+
+    // data passed in a component
+    {
+      filename: 'test.vue',
+      code: `
+        <template>
+          <counter :count="count" />
+        </template>
+        <script>
+          export default {
+            data () {
+              return {
+                count: 2
+              };
+            }
+          };
+        </script>
+      `,
+      options: allOptions
+    },
+
+    // data used in v-on
+    {
+      filename: 'test.vue',
+      code: `
+        <template>
+          <button @click="count++" />
+        </template>
+        <script>
+          export default {
+            data () {
+              return {
+                count: 2
+              };
+            }
+          };
+        </script>
+      `,
+      options: allOptions
+    },
+
+    // computed property used in a script expression
+    {
+      filename: 'test.vue',
+      code: `
+        <script>
+          export default {
+            computed: {
+              count() {
+                return 2;
+              }
+            },
+            created() {
+              const dummy = this.count + 1;
+            }
+          };
+        </script>
+      `,
+      options: allOptions
+    },
+
+    // computed property being watched
+    {
+      filename: 'test.vue',
+      code: `
+        <script>
+          export default {
+            computed: {
+              count() {
+                return 2;
+              }
+            },
+            watch: {
+              count() {
+                alert('Increased!');
+              },
+            },
+          };
+        </script>
+      `,
+      options: allOptions
+    },
+
+    // computed property used as a template identifier
+    {
+      filename: 'test.vue',
+      code: `
+        <template>
+          <div>{{ count }}</div>
+        </template>
+        <script>
+          export default {
+            computed: {
+              count() {
+                return 2;
+              }
+            }
+          }
+        </script>
+      `,
+      options: allOptions
+    },
+
+    // computed properties used in a template expression
+    {
+      filename: 'test.vue',
+      code: `
+        <template>
+          <div>{{ count1 + count2 }}</div>
+        </template>
+        <script>
+          export default {
+            computed: {
+              count1() {
+                return 1;
+              },
+              count2() {
+                return 2;
+              }
+            }
+          }
+        </script>
+      `,
+      options: allOptions
+    },
+
+    // computed property used in v-if
+    {
+      filename: 'test.vue',
+      code: `
+        <template>
+          <div v-if="count > 0"></div>
+        </template>
+        <script>
+          export default {
+            computed: {
+              count() {
+                return 2;
+              }
+            }
+          }
+        </script>
+      `,
+      options: allOptions
+    },
+
+    // computed property used in v-for
+    {
+      filename: 'test.vue',
+      code: `
+        <template>
+          <div v-for="color in colors">{{ color }}</div>
+        </template>
+        <script>
+          export default {
+            computed: {
+              colors() {
+                return ["purple", "green"];
+              }
+            }
+          };
+        </script>
+      `,
+      options: allOptions
+    },
+
+    // computed property used in v-html
+    {
+      filename: 'test.vue',
+      code: `
+        <template>
+          <div v-html="message" />
+        </template>
+        <script>
+          export default {
+            computed: {
+              message() {
+                return "<span>Hey!</span>";
+              }
+            }
+          };
+        </script>
+      `,
+      options: allOptions
+    },
+
+    // computed property used in v-model
+    {
+      filename: 'test.vue',
+      code: `
+        <template>
+          <input v-model="fullName" />
+        </template>
+        <script>
+          export default {
+            data() {
+              return {
+                firstName: "David",
+                lastName: "Attenborough"
+              }
+            },
+            computed: {
+              fullName: {
+                get() {
+                  return this.firstName + ' ' + this.lastName
+                },
+                set(newValue) {
+                  var names = newValue.split(' ')
+                  this.firstName = names[0]
+                  this.lastName = names[names.length - 1]
+                }
+              }
+            }
+          };
+        </script>
+      `,
+      options: allOptions
+    },
+
+    // computed property passed in a component
+    {
+      filename: 'test.vue',
+      code: `
+        <template>
+          <counter :count="count" />
+        </template>
+        <script>
+          export default {
+            computed: {
+              count() {
+                return 2;
+              }
+            }
+          }
+        </script>
+      `,
+      options: allOptions
+    },
+
+    // async data passed in a component
+    {
+      filename: 'test.vue',
+      code: `
+        <template>
+          <counter :count="foo" />
+        </template>
+        <script>
+          export default {
+            asyncData() {
+              return {
+                foo: 2
+              }
+            }
+          }
+        </script>
+      `,
+      options: allOptions
+    },
+
+    // ignores unused data when marked with eslint-disable
+    {
+      filename: 'test.vue',
+      code: `
+        <template>
+          <div>{{ cont }}</div>
+        </template>
+        <script>
+          export default {
+            data () {
+              return {
+                // eslint-disable-next-line
+                count: 2
+              };
+            }
+          };
+        </script>
+      `,
+      options: allOptions
+    },
+
+    // trace this
+    {
+      filename: 'test.vue',
+      code: `
+        <script>
+          export default {
+            props: ['count'],
+            methods: {
+              click () {
+                const vm = this
+                fn(vm.count)
+              }
+            }
+          };
+        </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <script>
+          export default {
+            props: ['count'],
+            methods: {
+              click () {
+                const vm = this
+                const {count} = vm
+              }
+            }
+          };
+        </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <script>
+          export default {
+            props: ['count'],
+            methods: {
+              click () {
+                const vm = this
+                let count;
+                ({count} = vm)
+              }
+            }
+          };
+        </script>
+      `
+    },
+    // use rest
+    {
+      filename: 'test.vue',
+      code: `
+        <template>
+          <div>{{ foo }}</div>
+        </template>
+        <script>
+          export default {
+            data () {
+              return {
+                count: 2
+              };
+            },
+            methods: {
+              click () {
+                const vm = this
+                const {...r} = vm
+              },
+              focus () {
+                fn(this.foo)
+              },
+              blur () {
+                const {foo} = this
+              },
+              keydown () {
+                let foo;
+                ({foo} = this)
+              }
+            }
+          };
+        </script>
+      `
+    },
+
+    // function trace
+    {
+      filename: 'test.vue',
+      code: `
+        <script>
+          export default {
+            props: ['foo', 'bar', 'baz'],
+            setup (props) {
+              fn(props)
+            }
+          }
+
+          function fn(p) {
+            return fn2(p)
+          }
+          function fn2(p2) {
+            const {...a} = p2
+          }
+        </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <script>
+          export default {
+            props: ['foo', 'bar', 'baz'],
+            setup (props) {
+              fn(props)
+            }
+          }
+
+          function fn(a) {
+            return a[foo]
+          }
+        </script>
+      `
+    },
+
+    // render & functional
+    {
+      filename: 'test.js',
+      code: `
+      Vue.component('smart-list', {
+        functional: true,
+        props: {
+          items: {
+            type: Array,
+            required: true
+          },
+          isOrdered: Boolean
+        },
+        render: function (createElement, context) {
+          function appropriateListComponent () {
+            var items = context.props.items
+
+            if (items.length === 0)           return EmptyList
+            if (typeof items[0] === 'object') return TableList
+            if (context.props.isOrdered)      return OrderedList
+
+            return UnorderedList
+          }
+
+          return createElement(
+            appropriateListComponent(),
+            context.data,
+            context.children
+          )
+        }
+      })
+      `
+    },
+    {
+      filename: 'test.js',
+      code: `
+      Vue.component('MyButton', {
+        functional: true,
+        props: ['foo'],
+        render: function (createElement, {props}) {
+          return createElement('button', props.foo)
+        }
+      })
+      `
+    },
+    {
+      filename: 'test.js',
+      code: `
+      Vue.component('MyButton', {
+        functional: true,
+        props: ['foo'],
+        render: function (createElement, ctx) {
+          return createElement('button', fn(ctx.props))
+        }
+      })
+
+      function fn(props) {
+        return props.foo
+      }
+      `
+    },
+    {
+      filename: 'test.js',
+      code: `
+      Vue.component('MyButton', {
+        functional: true,
+        props: ['foo'],
+        render: function (createElement, ctx) {
+          return createElement('button', fn(ctx))
+        }
+      })
+
       function fn({props}) {
         return props.foo
       }
@@ -925,811 +1312,1253 @@ tester.run('no-unused-properties', rule, {
       code: `
       Vue.component('MyButton', {
         functional: true,
-        props: ['foo'],
-        render: function (createElement, {props:{foo}}) {
-          return createElement('button', foo)
+        props: ['foo'],
+        render: function (createElement, {props:{foo}}) {
+          return createElement('button', foo)
+        }
+      })
+      `
+    },
+    {
+      filename: 'test.js',
+      code: `
+      Vue.component('MyButton', {
+        functional: true,
+        props: ['foo'],
+        render: function (createElement, {props:[bar]}) {
+          return createElement('button')
+        }
+      })
+      `
+    },
+    {
+      filename: 'test.js',
+      code: `
+      Vue.component('MyButton', {
+        functional: true,
+        props: ['foo'],
+        render: function (createElement, {props:bar={}}) {
+          return createElement('button', bar.foo)
+        }
+      })
+      `
+    },
+    {
+      filename: 'test.js',
+      code: `
+      Vue.component('MyButton', {
+        functional: true,
+        props: ['foo'],
+        render: function (createElement, {...foo}) {
+          return createElement('button')
+        }
+      })
+      `
+    },
+    {
+      filename: 'test.js',
+      code: `
+      Vue.component('MyButton', {
+        functional: true,
+        props: ['foo', 'bar'],
+        render: function (createElement, ctx) {
+          const a = ctx.props
+          const b = ctx.props
+          return createElement('button', a.foo + b.bar)
+        }
+      })
+      `
+    },
+    {
+      filename: 'test.js',
+      code: `
+      Vue.component('MyButton', {
+        functional: true,
+        props: ['foo', 'bar'],
+        render: function (createElement, {props: a, props: b}) {
+          return createElement('button', a.foo + b.bar)
+        }
+      })
+      `
+    },
+    // render for Vue 3.x
+    {
+      filename: 'test.vue',
+      code: `
+      export default {
+        props: ['foo'],
+        render (props) {
+          return h('button', props.foo)
+        }
+      })
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      export default {
+        props: ['foo'],
+        render ({foo}) {
+          return h('button', foo)
+        }
+      })
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      export default {
+        props: ['foo'],
+        render (bar) {
+          const {...baz} = bar
+          return h('button')
+        }
+      })
+      `
+    },
+    // Vue.js 3.x Template Refs
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div ref="root"></div>
+      </template>
+
+      <script>
+        import { ref, onMounted } from 'vue'
+
+        export default {
+          setup() {
+            const root = ref(null)
+
+            onMounted(() => {
+              // the DOM element will be assigned to the ref after initial render
+              console.log(root.value) // <div/>
+            })
+
+            return {
+              root
+            }
+          }
+        }
+      </script>`,
+      options: [{ groups: ['props', 'setup'] }]
+    },
+
+    // sparse array
+    {
+      filename: 'test.vue',
+      code: `
+        <template>
+          <div>{{ count }}</div>
+        </template>
+        <script>
+          export default {
+            props: [, 'count']
+          }
+        </script>
+        `
+    },
+    // optional chaining
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+        import { ref, onMounted } from 'vue'
+
+        export default {
+          props: ['foo', 'bar'],
+          methods: {
+            fn () {
+              fn(this)
+            }
+          }
+        }
+
+        function fn(a) {
+          return a?.foo + a?.bar
+        }
+      </script>`
+    },
+    {
+      filename: 'test.js',
+      code: `
+      Vue.component('MyButton', {
+        functional: true,
+        props: ['foo', 'bar'],
+        render: function (createElement, ctx) {
+          const a = ctx
+          const b = a?.props?.foo
+          const c = (a?.props)?.bar
         }
       })
       `
     },
+    // handlers
     {
-      filename: 'test.js',
+      filename: 'test.vue',
       code: `
-      Vue.component('MyButton', {
-        functional: true,
-        props: ['foo'],
-        render: function (createElement, {props:[bar]}) {
-          return createElement('button')
-        }
-      })
+      <script>
+        export default {
+          props: ['foo', 'bar'],
+          watch: {
+            foo: 'updateFoo',
+            bar: {
+              handler: 'updateBar',
+              immediate: true
+            }
+          },
+          methods: {
+            updateFoo() {},
+            updateBar() {}
+          }
+        };
+      </script>
+      `,
+      options: [{ groups: ['props', 'methods'] }]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+        export default {
+          props: ['foo', 'bar'],
+          data () {
+            return {
+              updateFoo() {},
+              updateBar() {}
+            }
+          },
+          watch: {
+            foo: 'updateFoo',
+            bar: {
+              handler: 'updateBar',
+              immediate: true
+            }
+          }
+        };
+      </script>
+      `,
+      options: [{ groups: ['props', 'data'] }]
+    },
+    // contexts
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+        export default {
+          props: ['x'],
+          data: ({ x }) => ({
+              y: x
+          })
+        };
+      </script>
       `
     },
     {
-      filename: 'test.js',
+      filename: 'test.vue',
       code: `
-      Vue.component('MyButton', {
-        functional: true,
-        props: ['foo'],
-        render: function (createElement, {props:bar={}}) {
-          return createElement('button', bar.foo)
-        }
-      })
+      <script>
+        export default {
+          props: ['x'],
+          computed: {
+            y: (vm) => vm.x * 2
+          }
+        };
+      </script>
       `
     },
     {
-      filename: 'test.js',
+      filename: 'test.vue',
       code: `
-      Vue.component('MyButton', {
-        functional: true,
-        props: ['foo'],
-        render: function (createElement, {...foo}) {
-          return createElement('button')
-        }
-      })
+      <script>
+        export default {
+          props: ['x'],
+          computed: {
+            y: {
+              get: () => this.x * 2
+            }
+          }
+        };
+      </script>
       `
     },
     {
-      filename: 'test.js',
+      filename: 'test.vue',
       code: `
-      Vue.component('MyButton', {
-        functional: true,
-        props: ['foo', 'bar'],
-        render: function (createElement, ctx) {
-          const a = ctx.props
-          const b = ctx.props
-          return createElement('button', a.foo + b.bar)
-        }
-      })
+      <script>
+        export default {
+          props: ['x'],
+          computed: {
+            y: {
+              get: () => this.$props.x * 2
+            }
+          }
+        };
+      </script>
       `
     },
+
+    // deep data
+    {
+      filename: 'test.vue',
+      code: `
+        <template>
+         {{ foo.baz.b }}
+        </template>
+        <script>
+          export default {
+            data() {
+              return {
+                foo: {
+                  bar: {
+                    a: 1
+                  },
+                  baz: {
+                    b: 1
+                  }
+                }
+              }
+            },
+            methods: {
+              a () {
+                return this.foo.bar.a
+              }
+            }
+          };
+        </script>
+      `,
+      options: deepDataOptions
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <script>
+          export default {
+            data() {
+              return {
+                foo: {
+                  bar: {
+                    a: 1
+                  },
+                  baz: {
+                    b: 1
+                  }
+                }
+              }
+            },
+            created() {
+              alert(this.foo)
+            }
+          };
+        </script>
+      `,
+      options: deepDataOptions
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <script>
+          export default {
+            data() {
+              return {
+                foo: {
+                  bar: {
+                    a: 1
+                  },
+                  baz: {
+                    b: 1
+                  }
+                }
+              }
+            },
+            created() {
+              fn(this.foo)
+            }
+          };
+          function fn(foo) {
+            return foo.bar.a + foo.baz.b
+          }
+        </script>
+      `,
+      options: deepDataOptions
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <template>
+          {{ foo.bar }}
+          {{ foo.baz }}
+        </template>
+        <script>
+          export default {
+            data() {
+              return {
+                foo: {
+                  bar: {
+                    a: 1
+                  },
+                  baz: {
+                    b: 1
+                  }
+                }
+              }
+            }
+          };
+        </script>
+      `,
+      options: deepDataOptions
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <script>
+          export default {
+            data() {
+              return {
+                foo: {
+                  bar: {
+                    a: 1
+                  },
+                  baz: {
+                    b: 1
+                  }
+                }
+              }
+            },
+            methods: {
+              a () {
+                return this.foo
+              }
+            }
+          };
+        </script>
+      `,
+      options: deepDataOptions
+    },
     {
-      filename: 'test.js',
+      filename: 'test.vue',
       code: `
-      Vue.component('MyButton', {
-        functional: true,
-        props: ['foo', 'bar'],
-        render: function (createElement, {props: a, props: b}) {
-          return createElement('button', a.foo + b.bar)
-        }
-      })
-      `
+        <template>
+          <Foo :param="{ a: foo.bar }"></Foo>
+        </template>
+
+        <script>
+          export default {
+            data() {
+              return {
+                foo: {
+                  bar: {
+                    a: 1
+                  },
+                  baz: {
+                    b: 1
+                  }
+                }
+              }
+            },
+            methods: {
+              fn () {
+                return {
+                  b: this.foo.baz
+                }
+              }
+            }
+          };
+        </script>
+      `,
+      options: deepDataOptions
     },
-    // render for Vue 3.x
     {
       filename: 'test.vue',
       code: `
-      export default {
-        props: ['foo'],
-        render (props) {
-          return h('button', props.foo)
-        }
-      })
-      `
+        <template>
+          <Foo :class="[ foo.bar ]"></Foo>
+        </template>
+
+        <script>
+          export default {
+            data() {
+              return {
+                foo: {
+                  bar: {
+                    a: 1
+                  },
+                  baz: {
+                    b: 1
+                  }
+                }
+              }
+            },
+            methods: {
+              fn () {
+                return [this.foo.baz]
+              }
+            }
+          };
+        </script>
+      `,
+      options: deepDataOptions
     },
     {
       filename: 'test.vue',
       code: `
-      export default {
-        props: ['foo'],
-        render ({foo}) {
-          return h('button', foo)
-        }
-      })
-      `
+        <template>
+          <Foo :param="[ {a: foo.bar} ]"></Foo>
+        </template>
+
+        <script>
+          export default {
+            data() {
+              return {
+                foo: {
+                  bar: {
+                    a: 1
+                  },
+                  baz: {
+                    b: 1
+                  }
+                }
+              }
+            },
+            methods: {
+              fn () {
+                return {
+                  b: [this.foo.baz]
+                }
+              }
+            }
+          };
+        </script>
+      `,
+      options: deepDataOptions
     },
     {
       filename: 'test.vue',
       code: `
-      export default {
-        props: ['foo'],
-        render (bar) {
-          const {...baz} = bar
-          return h('button')
-        }
-      })
-      `
+        <script>
+          export default {
+            data() {
+              return {
+                foo: {
+                  bar: {
+                    a: 1
+                  },
+                  baz: {
+                    b: 1
+                  }
+                }
+              }
+            },
+            methods: {
+              fn () {
+                unknown(this.foo.bar)
+                f( () => this.foo.baz )
+              }
+            }
+          };
+
+          function f (a) {
+
+          }
+        </script>
+      `,
+      options: deepDataOptions
     },
-    // Vue.js 3.x Template Refs
     {
       filename: 'test.vue',
       code: `
       <template>
-        <div ref="root"></div>
+        <div> {{ obj.num.toLocaleString() }} </div>
       </template>
-
       <script>
-        import { ref, onMounted } from 'vue'
-
-        export default {
-          setup() {
-            const root = ref(null)
-
-            onMounted(() => {
-              // the DOM element will be assigned to the ref after initial render
-              console.log(root.value) // <div/>
-            })
-
-            return {
-              root
-            }
+      export default {
+        data () {
+          return {
+            obj: { num: 42 }
           }
         }
+      }
       </script>`,
-      options: [{ groups: ['props', 'setup'] }]
+      options: deepDataOptions
     },
 
-    // sparse array
+    // ignore public members
     {
       filename: 'test.vue',
       code: `
-        <template>
-          <div>{{ count }}</div>
-        </template>
         <script>
-          export default {
-            props: [, 'count']
-          }
+        export default {
+          data: () => ({
+            /** @public */
+            publicData: 'foo'
+          })
+        }
         </script>
-        `
+      `,
+      options: [{ groups: ['data'], ignorePublicMembers: true }]
     },
-    // optional chaining
     {
       filename: 'test.vue',
       code: `
-      <script>
-        import { ref, onMounted } from 'vue'
-
+        <script>
         export default {
-          props: ['foo', 'bar'],
-          methods: {
-            fn () {
-              fn(this)
+          computed: {
+            /** @public */
+            unusedComputed() {
+              return 'foo'
             }
           }
         }
-
-        function fn(a) {
-          return a?.foo + a?.bar
-        }
-      </script>`
-    },
-    {
-      filename: 'test.js',
-      code: `
-      Vue.component('MyButton', {
-        functional: true,
-        props: ['foo', 'bar'],
-        render: function (createElement, ctx) {
-          const a = ctx
-          const b = a?.props?.foo
-          const c = (a?.props)?.bar
-        }
-      })
-      `
+        </script>
+      `,
+      options: [{ groups: ['computed'], ignorePublicMembers: true }]
     },
-    // handlers
     {
       filename: 'test.vue',
       code: `
-      <script>
+        <script>
         export default {
-          props: ['foo', 'bar'],
-          watch: {
-            foo: 'updateFoo',
-            bar: {
-              handler: 'updateBar',
-              immediate: true
-            }
-          },
           methods: {
-            updateFoo() {},
-            updateBar() {}
+            /** @public */
+            publicMethod() {}
           }
-        };
-      </script>
+        }
+        </script>
       `,
-      options: [{ groups: ['props', 'methods'] }]
+      options: [{ groups: ['methods'], ignorePublicMembers: true }]
     },
     {
       filename: 'test.vue',
       code: `
-      <script>
+        <script>
         export default {
-          props: ['foo', 'bar'],
-          data () {
+          setup() {
             return {
-              updateFoo() {},
-              updateBar() {}
-            }
-          },
-          watch: {
-            foo: 'updateFoo',
-            bar: {
-              handler: 'updateBar',
-              immediate: true
+              /** @public */
+              publicRef: 3
             }
           }
-        };
-      </script>
+        }
+        </script>
       `,
-      options: [{ groups: ['props', 'data'] }]
+      options: [{ groups: ['setup'], ignorePublicMembers: true }]
     },
-    // contexts
     {
       filename: 'test.vue',
       code: `
+      <template>
+      </template>
       <script>
-        export default {
-          props: ['x'],
-          data: ({ x }) => ({
-              y: x
-          })
-        };
-      </script>
-      `
+      export default {
+        props: {
+          a: String,
+        },
+        setup() {
+          return {
+            /** @public */
+            b: 42
+          }
+        },
+        data () {
+          return {
+            /** @public */
+            c: 42
+          }
+        },
+        computed: {
+          /** @public */
+          d () { return this.a }
+        },
+        methods: {
+          /** @public */
+          e () {},
+          f:
+            /** @public */
+            function () {},
+          g:
+            /** @public */
+            () => {},
+        },
+      }
+      </script>`,
+      options: [
+        {
+          groups: ['props', 'data', 'computed', 'methods', 'setup'],
+          ignorePublicMembers: true
+        }
+      ]
     },
     {
       filename: 'test.vue',
       code: `
+      <template>
+      </template>
       <script>
-        export default {
-          props: ['x'],
-          computed: {
-            y: (vm) => vm.x * 2
-          }
-        };
-      </script>
-      `
+      export default {
+        props: {
+          a: String,
+        },
+        computed: {
+          /** @public */
+          b () { return this.$props.a },
+        },
+      }
+      </script>`,
+      options: [
+        {
+          groups: ['props', 'computed'],
+          ignorePublicMembers: true
+        }
+      ]
     },
+
+    // expose
     {
       filename: 'test.vue',
       code: `
+      <template>
+        {{a}}
+      </template>
       <script>
-        export default {
-          props: ['x'],
-          computed: {
-            y: {
-              get: () => this.x * 2
-            }
+      export default {
+        expose: ['a', 'b', 'c', 'd'],
+        props: ['a'],
+        data() {
+          return {
+            b: 42,
           }
-        };
-      </script>
-      `
+        },
+        computed:{
+          c() {},
+        },
+        methods:{
+          d() {},
+        }
+      }
+      </script>`,
+      options: allOptions
     },
 
-    // deep data
+    //style vars
     {
       filename: 'test.vue',
       code: `
         <template>
-         {{ foo.baz.b }}
+          <div class="text">hello</div>
         </template>
+
         <script>
           export default {
             data() {
               return {
-                foo: {
-                  bar: {
-                    a: 1
-                  },
-                  baz: {
-                    b: 1
-                  }
-                }
+                color: 'red',
+                font: {
+                  size: '2em',
+                },
               }
             },
-            methods: {
-              a () {
-                return this.foo.bar.a
-              }
-            }
-          };
+          }
+        </script>
+
+        <style>
+          .text {
+            color: v-bind(color);
+
+            /* expressions (wrap in quotes) */
+            font-size: v-bind('font.size');
+          }
+        </style>
+        `
+    },
+
+    // toRefs
+    {
+      // https://github.com/vuejs/eslint-plugin-vue/issues/1643
+      filename: 'test.vue',
+      code: `
+      <template>
+        <span v-for="(item, index) of pages" :key="index" @click="changePage(item)">
+          {{ item }}
+        </span>
+      </template>
+
+      <script setup lang="ts">
+      import {
+        computed,
+        defineProps,
+        toRefs,
+        withDefaults,
+        defineEmits,
+        ComputedRef,
+      } from 'vue';
+      import { getPagesOnPagination } from '@/libs/pagination';
+
+      const props = withDefaults(
+        defineProps<{
+          currentPage: number;
+          totalRows: number;
+          rowsPerPage: number;
+        }>(),
+        {
+          currentPage: 1,
+          totalRows: 100,
+          rowsPerPage: 10,
+        }
+      );
+      const { currentPage, totalRows, rowsPerPage } = toRefs(props);
+
+      const totalPages = computed(() =>
+        Math.ceil(totalRows.value / rowsPerPage.value)
+      );
+
+      const pages: ComputedRef<(number | '...')[]> = computed(() =>
+        getPagesOnPagination(currentPage.value, totalPages.value)
+      );
+
+      const emit = defineEmits<{
+        (e: 'changePage', page: number): void;
+      }>();
+
+      function changePage(page: number | '...') {
+        if (page === '...') {
+          return;
+        }
+        emit('changePage', page);
+      }
+      </script>
+
+      <style lang="scss" scoped>
+      //
+      </style>`,
+      parserOptions: {
+        parser: '@typescript-eslint/parser'
+      }
+    },
+
+    // Vue2 functional component
+    {
+      filename: 'test.vue',
+      code: `
+      <template functional>
+        <div>{{props.a}} {{props.b}}</div>
+      </template>
+
+      <script>
+      export default {
+        props: {
+          a: String,
+          b: String,
+        },
+      };
+      </script>`
+    }
+  ],
+  invalid: [
+    {
+      filename: 'test.vue',
+      code: `
+        <script>
+          export default {
+            methods: mapMutations({
+              add2: 'increment',
+              add: 'iqwnei'
+            })
+          }
         </script>
+        <template>
+          {{ add() }}
+        </template>
       `,
-      options: deepDataOptions
+      errors: [
+        {
+          message: "'add2' of method found, but never used.",
+          line: 5
+        }
+      ]
     },
     {
       filename: 'test.vue',
       code: `
         <script>
           export default {
-            data() {
-              return {
-                foo: {
-                  bar: {
-                    a: 1
-                  },
-                  baz: {
-                    b: 1
-                  }
-                }
-              }
-            },
-            created() {
-              alert(this.foo)
+            methods: {
+              ...mapMutations({
+                add2: 'increment',
+                add: 'inrement'
+              })
             }
-          };
+          }
         </script>
+        <template>
+          {{ add() }}
+        </template>
       `,
-      options: deepDataOptions
+      errors: [
+        {
+          message: "'add2' of method found, but never used.",
+          line: 6
+        }
+      ]
     },
     {
       filename: 'test.vue',
       code: `
         <script>
           export default {
-            data() {
-              return {
-                foo: {
-                  bar: {
-                    a: 1
-                  },
-                  baz: {
-                    b: 1
-                  }
-                }
-              }
-            },
-            created() {
-              fn(this.foo)
-            }
-          };
-          function fn(foo) {
-            return foo.bar.a + foo.baz.b
+            methods: mapMutations(['add', 'add2'])
           }
         </script>
+        <template>
+          {{ add() }}
+        </template>
       `,
-      options: deepDataOptions
+      errors: [
+        {
+          message: "'add2' of method found, but never used.",
+          line: 4
+        }
+      ]
     },
     {
       filename: 'test.vue',
       code: `
-        <template>
-          {{ foo.bar }}
-          {{ foo.baz }}
-        </template>
         <script>
           export default {
-            data() {
-              return {
-                foo: {
-                  bar: {
-                    a: 1
-                  },
-                  baz: {
-                    b: 1
-                  }
-                }
+            methods: {
+              methods: {
+                ...mapMutations(['add', 'add2'])
               }
             }
-          };
+          }
         </script>
+        <template>
+          {{ add() }}
+        </template>
       `,
-      options: deepDataOptions
+      errors: [
+        {
+          message: "'add2' of method found, but never used.",
+          line: 6
+        }
+      ]
     },
     {
       filename: 'test.vue',
       code: `
         <script>
           export default {
-            data() {
-              return {
-                foo: {
-                  bar: {
-                    a: 1
-                  },
-                  baz: {
-                    b: 1
-                  }
-                }
-              }
-            },
             methods: {
-              a () {
-                return this.foo
-              }
+              ...mapActions({
+                add2: 'increment',
+                add: 'qweqwqewqwe'
+              })
             }
-          };
+          }
         </script>
+        <template>
+          {{ add() }}
+        </template>
       `,
-      options: deepDataOptions
+      errors: [
+        {
+          message: "'add2' of method found, but never used.",
+          line: 6
+        }
+      ]
     },
     {
       filename: 'test.vue',
       code: `
-        <template>
-          <Foo :param="{ a: foo.bar }"></Foo>
-        </template>
-
         <script>
           export default {
-            data() {
-              return {
-                foo: {
-                  bar: {
-                    a: 1
-                  },
-                  baz: {
-                    b: 1
-                  }
-                }
-              }
-            },
-            methods: {
-              fn () {
-                return {
-                  b: this.foo.baz
-                }
-              }
-            }
-          };
+            methods: mapActions({
+              add2: 'increment',
+              add: 'inasdasda'
+            })
+          }
         </script>
+        <template>
+          {{ add() }}
+        </template>
       `,
-      options: deepDataOptions
+      errors: [
+        {
+          message: "'add2' of method found, but never used.",
+          line: 5
+        }
+      ]
     },
     {
       filename: 'test.vue',
       code: `
-        <template>
-          <Foo :class="[ foo.bar ]"></Foo>
-        </template>
-
         <script>
           export default {
-            data() {
-              return {
-                foo: {
-                  bar: {
-                    a: 1
-                  },
-                  baz: {
-                    b: 1
-                  }
-                }
-              }
-            },
-            methods: {
-              fn () {
-                return [this.foo.baz]
-              }
-            }
-          };
+            methods: mapActions(['add', 'add2'])
+          }
         </script>
+        <template>
+          {{ add() }}
+        </template>
       `,
-      options: deepDataOptions
+      errors: [
+        {
+          message: "'add2' of method found, but never used.",
+          line: 4
+        }
+      ]
     },
     {
       filename: 'test.vue',
       code: `
-        <template>
-          <Foo :param="[ {a: foo.bar} ]"></Foo>
-        </template>
-
         <script>
           export default {
-            data() {
-              return {
-                foo: {
-                  bar: {
-                    a: 1
-                  },
-                  baz: {
-                    b: 1
-                  }
-                }
-              }
-            },
             methods: {
-              fn () {
-                return {
-                  b: [this.foo.baz]
-                }
-              }
+              ...mapActions(['add', 'add2'])
             }
-          };
+          }
         </script>
+        <template>
+          {{ add() }}
+        </template>
       `,
-      options: deepDataOptions
+      errors: [
+        {
+          message: "'add2' of method found, but never used.",
+          line: 5
+        }
+      ]
     },
     {
       filename: 'test.vue',
       code: `
         <script>
           export default {
-            data() {
-              return {
-                foo: {
-                  bar: {
-                    a: 1
-                  },
-                  baz: {
-                    b: 1
-                  }
-                }
-              }
-            },
-            methods: {
-              fn () {
-                unknown(this.foo.bar)
-                f( () => this.foo.baz )
-              }
-            }
-          };
-
-          function f (a) {
-
+            computed: mapState({
+              count2: state => state.todosCount,
+              count: state => state.todosCount
+            })
           }
         </script>
+        <template>
+          {{ count }}
+        </template>
       `,
-      options: deepDataOptions
+      errors: [
+        {
+          message: "'count2' of computed property found, but never used.",
+          line: 5
+        }
+      ]
     },
     {
       filename: 'test.vue',
       code: `
-      <template>
-        <div> {{ obj.num.toLocaleString() }} </div>
-      </template>
-      <script>
-      export default {
-        data () {
-          return {
-            obj: { num: 42 }
+        <script>
+          export default {
+            computed: {
+              ...mapState({
+                count2: state => state.todosCount,
+                count: state => state.todosCount
+              })
+            }
           }
+        </script>
+        <template>
+          {{ count }}
+        </template>
+      `,
+      errors: [
+        {
+          message: "'count2' of computed property found, but never used.",
+          line: 6
         }
-      }
-      </script>`,
-      options: deepDataOptions
+      ]
     },
-
-    // ignore public members
     {
       filename: 'test.vue',
       code: `
         <script>
-        export default {
-          data: () => ({
-            /** @public */
-            publicData: 'foo'
-          })
-        }
+          export default {
+            computed: mapState({
+              count2 (state) {
+                return state.todosCount
+              },
+
+              count (state) {
+                return state.todosCount
+              }
+            })
+          }
         </script>
+        <template>
+          {{ count }}
+        </template>
       `,
-      options: [{ groups: ['data'], ignorePublicMembers: true }]
+      errors: [
+        {
+          message: "'count2' of computed property found, but never used.",
+          line: 5
+        }
+      ]
     },
     {
       filename: 'test.vue',
       code: `
         <script>
-        export default {
-          computed: {
-            /** @public */
-            unusedComputed() {
-              return 'foo'
+          export default {
+            computed: {
+              ...mapGetters(['count', 'count2']),
             }
           }
-        }
         </script>
+        <template>
+          {{ count }}
+        </template>
       `,
-      options: [{ groups: ['computed'], ignorePublicMembers: true }]
+      errors: [
+        {
+          message: "'count2' of computed property found, but never used.",
+          line: 5
+        }
+      ]
     },
     {
       filename: 'test.vue',
       code: `
         <script>
-        export default {
-          methods: {
-            /** @public */
-            publicMethod() {}
+          export default {
+            computed: mapGetters(['count1', 'count2', 'count'])
           }
-        }
         </script>
+        <template>
+          {{ count1 }} {{ count2 }}
+        </template>
       `,
-      options: [{ groups: ['methods'], ignorePublicMembers: true }]
+      errors: [
+        {
+          message: "'count' of computed property found, but never used.",
+          line: 4
+        }
+      ]
     },
     {
       filename: 'test.vue',
       code: `
         <script>
-        export default {
-          setup() {
-            return {
-              /** @public */
-              publicRef: 3
-            }
+          export default {
+            computed: mapGetters({
+              count2: 'count',
+              count: 'count'
+            })
           }
-        }
         </script>
+        <template>
+          {{ count }}
+        </template>
       `,
-      options: [{ groups: ['setup'], ignorePublicMembers: true }]
+      errors: [
+        {
+          message: "'count2' of computed property found, but never used.",
+          line: 5
+        }
+      ]
     },
     {
       filename: 'test.vue',
       code: `
-      <template>
-      </template>
-      <script>
-      export default {
-        props: {
-          a: String,
-        },
-        setup() {
-          return {
-            /** @public */
-            b: 42
-          }
-        },
-        data () {
-          return {
-            /** @public */
-            c: 42
+        <script>
+          export default {
+            computed: {
+              ...mapGetters({
+                count: 'count',
+                count3: 'count2'
+              })
+            }
           }
-        },
-        computed: {
-          /** @public */
-          d () { return this.a }
-        },
-        methods: {
-          /** @public */
-          e () {},
-          f:
-            /** @public */
-            function () {},
-          g:
-            /** @public */
-            () => {},
-        },
-      }
-      </script>`,
-      options: [
+        </script>
+        <template>
+          {{ count }}
+        </template>
+      `,
+      errors: [
         {
-          groups: ['props', 'data', 'computed', 'methods', 'setup'],
-          ignorePublicMembers: true
+          message: "'count3' of computed property found, but never used.",
+          line: 7
         }
       ]
     },
 
-    // expose
+    // vuex unused state
     {
       filename: 'test.vue',
       code: `
-      <template>
-        {{a}}
-      </template>
-      <script>
-      export default {
-        expose: ['a', 'b', 'c', 'd'],
-        props: ['a'],
-        data() {
-          return {
-            b: 42,
+        <script>
+          export default {
+            computed: {
+              ...mapState(['count', 'count2', 'count3'])
+            }
           }
-        },
-        computed:{
-          c() {},
-        },
-        methods:{
-          d() {},
+        </script>
+        <template>
+          {{ count2 }} {{ count3 }}
+        </template>
+      `,
+      errors: [
+        {
+          message: "'count' of computed property found, but never used.",
+          line: 5
         }
-      }
-      </script>`,
-      options: allOptions
+      ]
     },
 
-    //style vars
+    // vuex unused state
     {
       filename: 'test.vue',
       code: `
-        <template>
-          <div class="text">hello</div>
-        </template>
-
         <script>
           export default {
-            data() {
-              return {
-                color: 'red',
-                font: {
-                  size: '2em',
-                },
-              }
-            },
+            computed: mapState({
+              count: 'todosCount',
+              count2: 'someCount'
+            })
           }
         </script>
-
-        <style>
-          .text {
-            color: v-bind(color);
-
-            /* expressions (wrap in quotes) */
-            font-size: v-bind('font.size');
-          }
-        </style>
-        `
+        <template>
+          {{ count }}
+        </template>
+      `,
+      errors: [
+        {
+          message: "'count2' of computed property found, but never used.",
+          line: 6
+        }
+      ]
     },
 
-    // toRefs
+    // vuex unused state
     {
-      // https://github.com/vuejs/eslint-plugin-vue/issues/1643
       filename: 'test.vue',
       code: `
-      <template>
-        <span v-for="(item, index) of pages" :key="index" @click="changePage(item)">
-          {{ item }}
-        </span>
-      </template>
-
-      <script setup lang="ts">
-      import {
-        computed,
-        defineProps,
-        toRefs,
-        withDefaults,
-        defineEmits,
-        ComputedRef,
-      } from 'vue';
-      import { getPagesOnPagination } from '@/libs/pagination';
-
-      const props = withDefaults(
-        defineProps<{
-          currentPage: number;
-          totalRows: number;
-          rowsPerPage: number;
-        }>(),
+        <script>
+          export default {
+            computed: {
+              ...mapState(['count', 'additional'])
+            }
+          }
+        </script>
+        <template>
+          {{ count }}
+        </template>
+      `,
+      errors: [
         {
-          currentPage: 1,
-          totalRows: 100,
-          rowsPerPage: 10,
-        }
-      );
-      const { currentPage, totalRows, rowsPerPage } = toRefs(props);
-
-      const totalPages = computed(() =>
-        Math.ceil(totalRows.value / rowsPerPage.value)
-      );
-
-      const pages: ComputedRef<(number | '...')[]> = computed(() =>
-        getPagesOnPagination(currentPage.value, totalPages.value)
-      );
-
-      const emit = defineEmits<{
-        (e: 'changePage', page: number): void;
-      }>();
-
-      function changePage(page: number | '...') {
-        if (page === '...') {
-          return;
+          message: "'additional' of computed property found, but never used.",
+          line: 5
         }
-        emit('changePage', page);
-      }
-      </script>
-
-      <style lang="scss" scoped>
-      //
-      </style>`,
-      parserOptions: {
-        parser: '@typescript-eslint/parser'
-      }
+      ]
     },
 
-    // Vue2 functional component
-    {
-      filename: 'test.vue',
-      code: `
-      <template functional>
-        <div>{{props.a}} {{props.b}}</div>
-      </template>
-
-      <script>
-      export default {
-        props: {
-          a: String,
-          b: String,
-        },
-      };
-      </script>`
-    }
-  ],
-  invalid: [
     // unused property
     {
       filename: 'test.vue',
diff --git a/tests/lib/rules/no-v-text-v-html-on-component.js b/tests/lib/rules/no-v-text-v-html-on-component.js
index c5007134..1dafc17d 100644
--- a/tests/lib/rules/no-v-text-v-html-on-component.js
+++ b/tests/lib/rules/no-v-text-v-html-on-component.js
@@ -40,6 +40,25 @@ tester.run('no-v-text-v-html-on-component', rule, {
         <MyComponent v-if="content" />
       </template>
       `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <router-link v-text="content" />
+      </template>
+      `,
+      options: [{ allow: ['router-link'] }]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <router-link v-html="content" />
+        <NuxtLink v-html="content" />
+      </template>
+      `,
+      options: [{ allow: ['RouterLink', 'nuxt-link'] }]
     }
   ],
   invalid: [
@@ -132,6 +151,22 @@ tester.run('no-v-text-v-html-on-component', rule, {
           column: 30
         }
       ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <router-link v-html="content" />
+      </template>
+      `,
+      options: [{ allow: ['nuxt-link'] }],
+      errors: [
+        {
+          message: "Using v-html on component may break component's content.",
+          line: 3,
+          column: 22
+        }
+      ]
     }
   ]
 })
diff --git a/tests/lib/rules/require-explicit-emits.js b/tests/lib/rules/require-explicit-emits.js
index af90569d..89ae4762 100644
--- a/tests/lib/rules/require-explicit-emits.js
+++ b/tests/lib/rules/require-explicit-emits.js
@@ -461,6 +461,19 @@ tester.run('require-explicit-emits', rule, {
       `,
       parserOptions: { parser: require.resolve('@typescript-eslint/parser') }
     },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div @click="emit('foo')"/>
+        <div @click="emit('bar')"/>
+      </template>
+      <script setup lang="ts">
+      const emit = defineEmits<(e: 'foo' | 'bar') => void>()
+      </script>
+      `,
+      parserOptions: { parser: require.resolve('@typescript-eslint/parser') }
+    },
 
     // unknown emits definition
     {
@@ -1983,6 +1996,25 @@ emits: {'foo': null}
         }
       ],
       ...getTypeScriptFixtureTestOptions()
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div @click="emit('bar')"/>
+      </template>
+      <script setup lang="ts">
+      const emit = defineEmits<(e: 'foo') => void>()
+      </script>
+      `,
+      parserOptions: { parser: require.resolve('@typescript-eslint/parser') },
+      errors: [
+        {
+          message:
+            'The "bar" event has been triggered but not declared on `defineEmits`.',
+          line: 3
+        }
+      ]
     }
   ]
 })