Repository: vuejs/eslint-plugin-vue
Pull request #2541 (2024-09-02 04:07:50)

feat(no-unused-refs): support template ref api

FloEdelmann: Thanks! But can you please elaborate on the change? Where is `useTemplateRef` documented? Is it a native Vue feature?
KazariEX: It is a new feature of Vue 3.5 and there is no documentation yet.

https://github.com/vuejs/core/blob/main/packages/runtime-core/src/helpers/useTemplateRef.ts

Patch:
diff --git a/docs/rules/define-macros-order.md b/docs/rules/define-macros-order.md
index 5d8dda56..6dbe5ef0 100644
--- a/docs/rules/define-macros-order.md
+++ b/docs/rules/define-macros-order.md
@@ -2,20 +2,20 @@
 pageClass: rule-details
 sidebarDepth: 0
 title: vue/define-macros-order
-description: enforce order of `defineEmits` and `defineProps` compiler macros
+description: enforce order of compiler macros (`defineProps`, `defineEmits`, etc.)
 since: v8.7.0
 ---
 
 # vue/define-macros-order
 
-> enforce order of `defineEmits` and `defineProps` compiler macros
+> enforce order of compiler macros (`defineProps`, `defineEmits`, etc.)
 
 - :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.
 - :bulb: Some problems reported by this rule are manually fixable by editor [suggestions](https://eslint.org/docs/developer-guide/working-with-rules#providing-suggestions).
 
 ## :book: Rule Details
 
-This rule reports the `defineProps` and `defineEmits` compiler macros when they are not the first statements in `<script setup>` (after any potential import statements or type definitions) or when they are not in the correct order.
+This rule reports compiler macros (like `defineProps` or `defineEmits` but also custom ones) when they are not the first statements in `<script setup>` (after any potential import statements or type definitions) or when they are not in the correct order.
 
 ## :wrench: Options
 
@@ -28,7 +28,7 @@ This rule reports the `defineProps` and `defineEmits` compiler macros when they
 }
 ```
 
-- `order` (`string[]`) ... The order of defineEmits and defineProps macros. You can also add `"defineOptions"`, `"defineSlots"`, and `"defineModel"`.
+- `order` (`string[]`) ... The order in which the macros should appear. The default is `["defineProps", "defineEmits"]`.
 - `defineExposeLast` (`boolean`) ... Force `defineExpose` at the end.
 
 ### `{ "order": ["defineProps", "defineEmits"] }` (default)
@@ -118,6 +118,39 @@ const slots = defineSlots()
 
 </eslint-code-block>
 
+### `{ "order": ["definePage", "defineModel", "defineCustom", "defineEmits", "defineSlots"] }`
+
+<eslint-code-block fix :rules="{'vue/define-macros-order': ['error', {order: ['definePage', 'defineModel', 'defineCustom', 'defineEmits', 'defineSlots']}]}">
+
+```vue
+<!-- ✓ GOOD -->
+<script setup>
+definePage()
+const model = defineModel()
+defineCustom()
+defineEmits(/* ... */)
+const slots = defineSlots()
+</script>
+```
+
+</eslint-code-block>
+
+<eslint-code-block fix :rules="{'vue/define-macros-order': ['error', {order: ['definePage', 'defineModel', 'defineCustom', 'defineEmits', 'defineSlots']}]}">
+
+```vue
+<!-- ✗ BAD -->
+<script setup>
+defineEmits(/* ... */)
+const slots = defineSlots()
+defineProps(/* ... */)
+defineCustom({/* ... */})
+const model = defineModel()
+definePage()
+</script>
+```
+
+</eslint-code-block>
+
 ### `{ "defineExposeLast": true }`
 
 <eslint-code-block fix :rules="{'vue/define-macros-order': ['error', {defineExposeLast: true}]}">
diff --git a/docs/rules/index.md b/docs/rules/index.md
index 113ff416..1ba5978d 100644
--- a/docs/rules/index.md
+++ b/docs/rules/index.md
@@ -213,7 +213,7 @@ For example:
 | [vue/component-options-name-casing](./component-options-name-casing.md) | enforce the casing of component name in `components` options | :wrench::bulb: | :hammer: |
 | [vue/custom-event-name-casing](./custom-event-name-casing.md) | enforce specific casing for custom event name |  | :hammer: |
 | [vue/define-emits-declaration](./define-emits-declaration.md) | enforce declaration style of `defineEmits` |  | :hammer: |
-| [vue/define-macros-order](./define-macros-order.md) | enforce order of `defineEmits` and `defineProps` compiler macros | :wrench::bulb: | :lipstick: |
+| [vue/define-macros-order](./define-macros-order.md) | enforce order of compiler macros (`defineProps`, `defineEmits`, etc.) | :wrench::bulb: | :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: |
@@ -224,10 +224,12 @@ For example:
 | [vue/match-component-import-name](./match-component-import-name.md) | require the registered component name to match the imported component name |  | :warning: |
 | [vue/max-lines-per-block](./max-lines-per-block.md) | enforce maximum number of lines in Vue SFC blocks |  | :warning: |
 | [vue/max-props](./max-props.md) | enforce maximum number of props in Vue component |  | :warning: |
+| [vue/max-template-depth](./max-template-depth.md) | enforce maximum depth of template |  | :warning: |
 | [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 |  | :hammer: |
+| [vue/no-deprecated-delete-set](./no-deprecated-delete-set.md) | disallow using deprecated `$delete` and `$set` (in Vue.js 3.0.0+) |  | :warning: |
 | [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: |
@@ -310,7 +312,7 @@ The following rules extend the rules provided by ESLint itself and apply them to
 | [vue/dot-notation](./dot-notation.md) | Enforce dot notation whenever possible in `<template>` | :wrench: | :hammer: |
 | [vue/eqeqeq](./eqeqeq.md) | Require the use of `===` and `!==` in `<template>` | :wrench: | :hammer: |
 | [vue/func-call-spacing](./func-call-spacing.md) | Require or disallow spacing between function identifiers and their invocations in `<template>` | :wrench: | :lipstick: |
-| [vue/key-spacing](./key-spacing.md) | Enforce consistent spacing between keys and values in object literal properties in `<template>` | :wrench: | :lipstick: |
+| [vue/key-spacing](./key-spacing.md) | Enforce consistent spacing between property names and type annotations in types and interfaces in `<template>` | :wrench: | :lipstick: |
 | [vue/keyword-spacing](./keyword-spacing.md) | Enforce consistent spacing before and after keywords in `<template>` | :wrench: | :lipstick: |
 | [vue/max-len](./max-len.md) | enforce a maximum line length in `.vue` files |  | :lipstick: |
 | [vue/multiline-ternary](./multiline-ternary.md) | Enforce newlines between operands of ternary expressions in `<template>` | :wrench: | :lipstick: |
@@ -329,7 +331,7 @@ The following rules extend the rules provided by ESLint itself and apply them to
 | [vue/object-shorthand](./object-shorthand.md) | Require or disallow method and property shorthand syntax for object literals in `<template>` | :wrench: | :hammer: |
 | [vue/operator-linebreak](./operator-linebreak.md) | Enforce consistent linebreak style for operators in `<template>` | :wrench: | :lipstick: |
 | [vue/prefer-template](./prefer-template.md) | Require template literals instead of string concatenation in `<template>` | :wrench: | :hammer: |
-| [vue/quote-props](./quote-props.md) | Require quotes around object literal property names in `<template>` | :wrench: | :lipstick: |
+| [vue/quote-props](./quote-props.md) | Require quotes around object literal, type literal, interfaces and enums property names in `<template>` | :wrench: | :lipstick: |
 | [vue/space-in-parens](./space-in-parens.md) | Enforce consistent spacing inside parentheses in `<template>` | :wrench: | :lipstick: |
 | [vue/space-infix-ops](./space-infix-ops.md) | Require spacing around infix operators in `<template>` | :wrench: | :lipstick: |
 | [vue/space-unary-ops](./space-unary-ops.md) | Enforce consistent spacing before or after unary operators in `<template>` | :wrench: | :lipstick: |
diff --git a/docs/rules/key-spacing.md b/docs/rules/key-spacing.md
index 679b26c9..bad77240 100644
--- a/docs/rules/key-spacing.md
+++ b/docs/rules/key-spacing.md
@@ -2,13 +2,13 @@
 pageClass: rule-details
 sidebarDepth: 0
 title: vue/key-spacing
-description: Enforce consistent spacing between keys and values in object literal properties in `<template>`
+description: Enforce consistent spacing between property names and type annotations in types and interfaces in `<template>`
 since: v5.2.0
 ---
 
 # vue/key-spacing
 
-> Enforce consistent spacing between keys and values in object literal properties in `<template>`
+> Enforce consistent spacing between property names and type annotations in types and interfaces in `<template>`
 
 - :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.
 
diff --git a/docs/rules/max-props.md b/docs/rules/max-props.md
index b6979660..918c6729 100644
--- a/docs/rules/max-props.md
+++ b/docs/rules/max-props.md
@@ -3,14 +3,13 @@ pageClass: rule-details
 sidebarDepth: 0
 title: vue/max-props
 description: enforce maximum number of props in Vue component
+since: v9.28.0
 ---
 
 # vue/max-props
 
 > enforce maximum number of props in Vue component
 
-- :exclamation: <badge text="This rule has not been released yet." vertical="middle" type="error"> _**This rule has not been released yet.**_ </badge>
-
 ## :book: Rule Details
 
 This rule enforces a maximum number of props in a Vue SFC, in order to aid in maintainability and reduce complexity.
@@ -56,6 +55,10 @@ defineProps({
 
 </eslint-code-block>
 
+## :rocket: Version
+
+This rule was introduced in eslint-plugin-vue v9.28.0
+
 ## :mag: Implementation
 
 - [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/max-props.js)
diff --git a/docs/rules/max-template-depth.md b/docs/rules/max-template-depth.md
new file mode 100644
index 00000000..42ee4da8
--- /dev/null
+++ b/docs/rules/max-template-depth.md
@@ -0,0 +1,70 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/max-template-depth
+description: enforce maximum depth of template
+since: v9.28.0
+---
+
+# vue/max-template-depth
+
+> enforce maximum depth of template
+
+## :book: Rule Details
+
+This rule enforces a maximum depth of the template in a Vue SFC, in order to aid in maintainability and reduce complexity.
+
+## :wrench: Options
+
+This rule takes an object, where you can specify the maximum depth allowed in a Vue SFC template block.
+There is one property that can be specified for the object.
+
+- `maxDepth` ... Specify the maximum template depth `template` block.
+
+### `{ maxDepth: 3 }`
+
+<eslint-code-block :rules="{'vue/max-template-depth': ['error', { maxDepth: 3 }]}">
+
+```vue
+<!-- ✗ BAD -->
+<template>
+  <main-container>
+    <child-component>
+      <div />
+    </child-component>
+    <child-component>
+      <nested-component>
+        <div>
+          <div />
+        </div>
+      </nested-component>
+    </child-component>
+  </main-container>
+</template>
+```
+
+</eslint-code-block>
+
+<eslint-code-block :rules="{'vue/max-template-depth': ['error', { maxDepth: 3 }]}">
+
+```vue
+<!-- ✓ GOOD -->
+<template>
+  <main-container>
+    <child-component>
+      <div />
+    </child-component>
+  </main-container>
+</template>
+```
+
+</eslint-code-block>
+
+## :rocket: Version
+
+This rule was introduced in eslint-plugin-vue v9.28.0
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/max-template-depth.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/max-template-depth.js)
diff --git a/docs/rules/no-deprecated-delete-set.md b/docs/rules/no-deprecated-delete-set.md
new file mode 100644
index 00000000..6bd4ed82
--- /dev/null
+++ b/docs/rules/no-deprecated-delete-set.md
@@ -0,0 +1,52 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/no-deprecated-delete-set
+description: disallow using deprecated `$delete` and `$set` (in Vue.js 3.0.0+)
+---
+
+# vue/no-deprecated-delete-set
+
+> disallow using deprecated `$delete` and `$set` (in Vue.js 3.0.0+)
+
+- :exclamation: <badge text="This rule has not been released yet." vertical="middle" type="error"> _**This rule has not been released yet.**_ </badge>
+
+## :book: Rule Details
+
+This rule reports use of deprecated `$delete` and `$set`. (in Vue.js 3.0.0+).
+
+<eslint-code-block :rules="{'vue/no-deprecated-delete-set': ['error']}">
+
+```vue
+<script>
+  import { set, del } from 'vue'
+  export default {
+    mounted () {
+      /* ✗ BAD */
+      this.$set(obj, key, value)
+      this.$delete(obj, key)
+
+      Vue.set(obj, key, value)
+      Vue.delete(obj, key)
+
+      set(obj, key, value)
+      del(obj, key)
+    }
+  }
+</script>
+```
+
+</eslint-code-block>
+
+## :wrench: Options
+
+Nothing.
+
+## :books: Further Reading
+
+- [Migration Guide - Removed APIs](https://v3-migration.vuejs.org/breaking-changes/#removed-apis)
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-deprecated-delete-set.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-deprecated-delete-set.js)
diff --git a/docs/rules/quote-props.md b/docs/rules/quote-props.md
index d568a952..4d2b62d0 100644
--- a/docs/rules/quote-props.md
+++ b/docs/rules/quote-props.md
@@ -2,13 +2,13 @@
 pageClass: rule-details
 sidebarDepth: 0
 title: vue/quote-props
-description: Require quotes around object literal property names in `<template>`
+description: Require quotes around object literal, type literal, interfaces and enums property names in `<template>`
 since: v8.4.0
 ---
 
 # vue/quote-props
 
-> Require quotes around object literal property names in `<template>`
+> Require quotes around object literal, type literal, interfaces and enums property names in `<template>`
 
 - :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.
 
diff --git a/docs/rules/require-default-export.md b/docs/rules/require-default-export.md
index 1760c25f..9266eee8 100644
--- a/docs/rules/require-default-export.md
+++ b/docs/rules/require-default-export.md
@@ -3,14 +3,13 @@ pageClass: rule-details
 sidebarDepth: 0
 title: vue/require-default-export
 description: require components to be the default export
+since: v9.28.0
 ---
 
 # vue/require-default-export
 
 > require components to be the default export
 
-- :exclamation: <badge text="This rule has not been released yet." vertical="middle" type="error"> _**This rule has not been released yet.**_ </badge>
-
 ## :book: Rule Details
 
 This rule reports when a Vue component does not have a default export, if the component is not defined as `<script setup>`.
@@ -51,6 +50,10 @@ Nothing.
 
 - [vue/one-component-per-file](./one-component-per-file.md)
 
+## :rocket: Version
+
+This rule was introduced in eslint-plugin-vue v9.28.0
+
 ## :mag: Implementation
 
 - [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/require-default-export.js)
diff --git a/docs/rules/require-toggle-inside-transition.md b/docs/rules/require-toggle-inside-transition.md
index c62de95c..c0760e38 100644
--- a/docs/rules/require-toggle-inside-transition.md
+++ b/docs/rules/require-toggle-inside-transition.md
@@ -33,7 +33,34 @@ This rule reports elements inside `<transition>` that do not control the display
 
 ## :wrench: Options
 
-Nothing.
+```json
+{
+  "vue/require-toggle-inside-transition": ["error", {
+    "additionalDirectives": []
+  }]
+}
+```
+
+- `additionalDirectives` (`string[]`) ... Custom directives which will satisfy this rule in addition to `v-show` and `v-if`. Should be added without the `v-` prefix.
+
+### `additionalDirectives: ["dialog"]`
+
+<eslint-code-block :rules="{'vue/require-toggle-inside-transition': ['error', {additionalDirectives: ['dialog']}]}">
+
+```vue
+<template>
+  <!-- ✓ GOOD -->
+  <transition><div v-if="show" /></transition>
+  <transition><div v-show="show" /></transition>
+  <transition><dialog v-dialog="show" /></transition>
+
+  <!-- ✗ BAD -->
+  <transition><div /></transition>
+  <transition><div v-custom="show" /></transition>
+<template>
+```
+
+</eslint-code-block>
 
 ## :books: Further Reading
 
diff --git a/lib/index.js b/lib/index.js
index ff3f69c6..bb4abf40 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -80,6 +80,7 @@ const plugin = {
     'max-len': require('./rules/max-len'),
     'max-lines-per-block': require('./rules/max-lines-per-block'),
     'max-props': require('./rules/max-props'),
+    'max-template-depth': require('./rules/max-template-depth'),
     'multi-word-component-names': require('./rules/multi-word-component-names'),
     'multiline-html-element-content-newline': require('./rules/multiline-html-element-content-newline'),
     'multiline-ternary': require('./rules/multiline-ternary'),
@@ -96,6 +97,7 @@ const plugin = {
     'no-constant-condition': require('./rules/no-constant-condition'),
     'no-custom-modifiers-on-v-model': require('./rules/no-custom-modifiers-on-v-model'),
     'no-deprecated-data-object-declaration': require('./rules/no-deprecated-data-object-declaration'),
+    'no-deprecated-delete-set': require('./rules/no-deprecated-delete-set'),
     'no-deprecated-destroyed-lifecycle': require('./rules/no-deprecated-destroyed-lifecycle'),
     'no-deprecated-dollar-listeners-api': require('./rules/no-deprecated-dollar-listeners-api'),
     'no-deprecated-dollar-scopedslots-api': require('./rules/no-deprecated-dollar-scopedslots-api'),
diff --git a/lib/rules/attribute-hyphenation.js b/lib/rules/attribute-hyphenation.js
index 0ef2a042..35519d23 100644
--- a/lib/rules/attribute-hyphenation.js
+++ b/lib/rules/attribute-hyphenation.js
@@ -18,7 +18,7 @@ function getAttributeName(node) {
   }
 
   if (
-    node.key.name.name === 'bind' &&
+    (node.key.name.name === 'bind' || node.key.name.name === 'model') &&
     node.key.argument &&
     node.key.argument.type === 'VIdentifier'
   ) {
@@ -101,6 +101,10 @@ module.exports = {
             return null
           }
 
+          if (text.endsWith('.sync')) {
+            return null
+          }
+
           if (/^[A-Z]/.test(name)) {
             return null
           }
diff --git a/lib/rules/define-macros-order.js b/lib/rules/define-macros-order.js
index f24c58af..920b5c29 100644
--- a/lib/rules/define-macros-order.js
+++ b/lib/rules/define-macros-order.js
@@ -11,13 +11,15 @@ const MACROS_PROPS = 'defineProps'
 const MACROS_OPTIONS = 'defineOptions'
 const MACROS_SLOTS = 'defineSlots'
 const MACROS_MODEL = 'defineModel'
-const ORDER_SCHEMA = [
+const MACROS_EXPOSE = 'defineExpose'
+const KNOWN_MACROS = new Set([
   MACROS_EMITS,
   MACROS_PROPS,
   MACROS_OPTIONS,
   MACROS_SLOTS,
-  MACROS_MODEL
-]
+  MACROS_MODEL,
+  MACROS_EXPOSE
+])
 const DEFAULT_ORDER = [MACROS_PROPS, MACROS_EMITS]
 
 /**
@@ -109,6 +111,12 @@ function create(context) {
   /** @type {ASTNode} */
   let defineExposeNode
 
+  if (order.includes(MACROS_EXPOSE) && defineExposeLast) {
+    throw new Error(
+      "`defineExpose` macro can't be in the `order` array if `defineExposeLast` is true."
+    )
+  }
+
   return utils.compositingVisitors(
     utils.defineScriptSetupVisitor(context, {
       onDefinePropsExit(node) {
@@ -130,6 +138,20 @@ function create(context) {
       },
       onDefineExposeExit(node) {
         defineExposeNode = getDefineMacrosStatement(node)
+      },
+
+      /** @param {CallExpression} node */
+      'Program > ExpressionStatement > CallExpression, Program > VariableDeclaration > VariableDeclarator > CallExpression'(
+        node
+      ) {
+        if (
+          node.callee &&
+          node.callee.type === 'Identifier' &&
+          order.includes(node.callee.name) &&
+          !KNOWN_MACROS.has(node.callee.name)
+        ) {
+          macrosNodes.set(node.callee.name, [getDefineMacrosStatement(node)])
+        }
       }
     }),
     {
@@ -333,7 +355,7 @@ module.exports = {
     type: 'layout',
     docs: {
       description:
-        'enforce order of `defineEmits` and `defineProps` compiler macros',
+        'enforce order of compiler macros (`defineProps`, `defineEmits`, etc.)',
       categories: undefined,
       url: 'https://eslint.vuejs.org/rules/define-macros-order.html'
     },
@@ -346,7 +368,8 @@ module.exports = {
           order: {
             type: 'array',
             items: {
-              enum: ORDER_SCHEMA
+              type: 'string',
+              minLength: 1
             },
             uniqueItems: true,
             additionalItems: false
diff --git a/lib/rules/max-template-depth.js b/lib/rules/max-template-depth.js
new file mode 100644
index 00000000..899ec75c
--- /dev/null
+++ b/lib/rules/max-template-depth.js
@@ -0,0 +1,82 @@
+/**
+ * @author kevsommer Kevin Sommer
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+module.exports = {
+  meta: {
+    type: 'problem',
+    docs: {
+      description: 'enforce maximum depth of template',
+      categories: undefined,
+      url: 'https://eslint.vuejs.org/rules/max-template-depth.html'
+    },
+    fixable: null,
+    schema: [
+      {
+        type: 'object',
+        properties: {
+          maxDepth: {
+            type: 'integer',
+            minimum: 1
+          }
+        },
+        additionalProperties: false,
+        minProperties: 1
+      }
+    ],
+    messages: {
+      templateTooDeep:
+        'Element is nested too deeply (depth of {{depth}}, maximum allowed is {{limit}}).'
+    }
+  },
+  /** @param {RuleContext} context */
+  create(context) {
+    const option = context.options[0] || {}
+
+    /**
+     * @param {VElement} element
+     * @param {number} curDepth
+     */
+    function checkMaxDepth(element, curDepth) {
+      if (curDepth > option.maxDepth) {
+        context.report({
+          node: element,
+          messageId: 'templateTooDeep',
+          data: {
+            depth: curDepth,
+            limit: option.maxDepth
+          }
+        })
+      }
+
+      if (!element.children) {
+        return
+      }
+
+      for (const child of element.children) {
+        if (child.type === 'VElement') {
+          checkMaxDepth(child, curDepth + 1)
+        }
+      }
+    }
+
+    return {
+      /** @param {Program} program */
+      Program(program) {
+        const element = program.templateBody
+
+        if (element == null) {
+          return
+        }
+
+        if (element.type !== 'VElement') {
+          return
+        }
+
+        checkMaxDepth(element, 0)
+      }
+    }
+  }
+}
diff --git a/lib/rules/no-deprecated-delete-set.js b/lib/rules/no-deprecated-delete-set.js
new file mode 100644
index 00000000..305dacf9
--- /dev/null
+++ b/lib/rules/no-deprecated-delete-set.js
@@ -0,0 +1,132 @@
+/**
+ * @author Wayne Zhang
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+const utils = require('../utils')
+const { ReferenceTracker } = require('@eslint-community/eslint-utils')
+
+/**
+ * @typedef {import('@eslint-community/eslint-utils').TYPES.TraceMap} TraceMap
+ */
+
+/** @type {TraceMap} */
+const deletedImportApisMap = {
+  set: {
+    [ReferenceTracker.CALL]: true
+  },
+  del: {
+    [ReferenceTracker.CALL]: true
+  }
+}
+const deprecatedApis = new Set(['set', 'delete'])
+const deprecatedDollarApis = new Set(['$set', '$delete'])
+
+/**
+ * @param {Expression|Super} node
+ */
+function isVue(node) {
+  return node.type === 'Identifier' && node.name === 'Vue'
+}
+
+module.exports = {
+  meta: {
+    type: 'problem',
+    docs: {
+      description:
+        'disallow using deprecated `$delete` and `$set` (in Vue.js 3.0.0+)',
+      categories: undefined,
+      url: 'https://eslint.vuejs.org/rules/no-deprecated-delete-set.html'
+    },
+    fixable: null,
+    schema: [],
+    messages: {
+      deprecated: 'The `$delete`, `$set` is deprecated.'
+    }
+  },
+  /** @param {RuleContext} context */
+  create(context) {
+    /**
+     * @param {Identifier} identifier
+     * @param {RuleContext} context
+     * @returns {CallExpression|undefined}
+     */
+    function getVueDeprecatedCallExpression(identifier, context) {
+      // Instance API: this.$set()
+      if (
+        deprecatedDollarApis.has(identifier.name) &&
+        identifier.parent.type === 'MemberExpression' &&
+        utils.isThis(identifier.parent.object, context) &&
+        identifier.parent.parent.type === 'CallExpression' &&
+        identifier.parent.parent.callee === identifier.parent
+      ) {
+        return identifier.parent.parent
+      }
+
+      // Vue 2 Global API: Vue.set()
+      if (
+        deprecatedApis.has(identifier.name) &&
+        identifier.parent.type === 'MemberExpression' &&
+        isVue(identifier.parent.object) &&
+        identifier.parent.parent.type === 'CallExpression' &&
+        identifier.parent.parent.callee === identifier.parent
+      ) {
+        return identifier.parent.parent
+      }
+
+      return undefined
+    }
+
+    const nodeVisitor = {
+      /** @param {Identifier} node */
+      Identifier(node) {
+        const callExpression = getVueDeprecatedCallExpression(node, context)
+        if (!callExpression) {
+          return
+        }
+
+        context.report({
+          node,
+          messageId: 'deprecated'
+        })
+      }
+    }
+
+    return utils.compositingVisitors(
+      utils.defineVueVisitor(context, nodeVisitor),
+      utils.defineScriptSetupVisitor(context, nodeVisitor),
+      {
+        /** @param {Program} node */
+        Program(node) {
+          const tracker = new ReferenceTracker(utils.getScope(context, node))
+
+          // import { set } from 'vue'; set()
+          const esmTraceMap = {
+            vue: {
+              [ReferenceTracker.ESM]: true,
+              ...deletedImportApisMap
+            }
+          }
+          // const { set } = require('vue'); set()
+          const cjsTraceMap = {
+            vue: {
+              ...deletedImportApisMap
+            }
+          }
+
+          for (const { node } of [
+            ...tracker.iterateEsmReferences(esmTraceMap),
+            ...tracker.iterateCjsReferences(cjsTraceMap)
+          ]) {
+            const refNode = /** @type {CallExpression} */ (node)
+            context.report({
+              node: refNode.callee,
+              messageId: 'deprecated'
+            })
+          }
+        }
+      }
+    )
+  }
+}
diff --git a/lib/rules/no-unused-refs.js b/lib/rules/no-unused-refs.js
index 9033d9c2..79d82a39 100644
--- a/lib/rules/no-unused-refs.js
+++ b/lib/rules/no-unused-refs.js
@@ -233,6 +233,19 @@ module.exports = {
               refsNode = id.parent
             }
             extractUsedForPattern(refsNode)
+          },
+          CallExpression(callExpression) {
+            const firstArgument = callExpression.arguments[0]
+            if (
+              callExpression.callee.name !== 'useTemplateRef' ||
+              !firstArgument
+            ) {
+              return
+            }
+            const name = utils.getStringLiteralValue(firstArgument)
+            if (name !== null) {
+              usedRefs.add(name)
+            }
           }
         }
       )
diff --git a/lib/rules/require-toggle-inside-transition.js b/lib/rules/require-toggle-inside-transition.js
index 423d4beb..bfb267ca 100644
--- a/lib/rules/require-toggle-inside-transition.js
+++ b/lib/rules/require-toggle-inside-transition.js
@@ -20,6 +20,25 @@ function isValidBindAppear(vBindAppear) {
   return true
 }
 
+/**
+ * @param {string[]} directives
+ */
+function createDirectiveList(directives) {
+  let str = ''
+
+  for (const [index, directive] of directives.entries()) {
+    if (index === 0) {
+      str += `\`v-${directive}\``
+    } else if (index < directives.length - 1) {
+      str += `, \`v-${directive}\``
+    } else {
+      str += ` or \`v-${directive}\``
+    }
+  }
+
+  return str
+}
+
 module.exports = {
   meta: {
     type: 'problem',
@@ -30,14 +49,34 @@ module.exports = {
       url: 'https://eslint.vuejs.org/rules/require-toggle-inside-transition.html'
     },
     fixable: null,
-    schema: [],
+    schema: [
+      {
+        type: 'object',
+        properties: {
+          additionalDirectives: {
+            type: 'array',
+            items: {
+              type: 'string'
+            },
+            uniqueItems: true
+          }
+        },
+        additionalProperties: false
+      }
+    ],
     messages: {
       expected:
-        'The element inside `<transition>` is expected to have a `v-if` or `v-show` directive.'
+        'The element inside `<transition>` is expected to have a {{allowedDirectives}} directive.'
     }
   },
   /** @param {RuleContext} context */
   create(context) {
+    /** @type {Array<string>} */
+    const additionalDirectives =
+      (context.options[0] && context.options[0].additionalDirectives) || []
+    const allowedDirectives = ['if', 'show', ...additionalDirectives]
+    const allowedDirectivesString = createDirectiveList(allowedDirectives)
+
     /**
      * Check if the given element has display control.
      * @param {VElement} element The element node to check.
@@ -59,14 +98,18 @@ module.exports = {
 
       if (
         element.name !== 'slot' &&
-        !utils.hasDirective(element, 'if') &&
-        !utils.hasDirective(element, 'show') &&
+        !allowedDirectives.some((directive) =>
+          utils.hasDirective(element, directive)
+        ) &&
         !utils.hasDirective(element, 'bind', 'key')
       ) {
         context.report({
           node: element.startTag,
           loc: element.startTag.loc,
-          messageId: 'expected'
+          messageId: 'expected',
+          data: {
+            allowedDirectives: allowedDirectivesString
+          }
         })
       }
     }
diff --git a/lib/rules/syntaxes/slot-attribute.js b/lib/rules/syntaxes/slot-attribute.js
index 61891df3..27087cb3 100644
--- a/lib/rules/syntaxes/slot-attribute.js
+++ b/lib/rules/syntaxes/slot-attribute.js
@@ -56,10 +56,8 @@ module.exports = {
         // parse error or empty expression
         return false
       }
-      const slotName = sourceCode.getText(slotAttr.value.expression).trim()
-      // If non-Latin characters are included it can not be converted.
-      // It does not check the space only because `a>b?c:d` should be rejected.
-      return !/[^a-z]/i.test(slotName)
+
+      return slotAttr.value.expression.type === 'Identifier'
     }
 
     /**
@@ -102,7 +100,18 @@ module.exports = {
           yield fixer.remove(scopeAttr)
         }
 
-        yield fixer.insertTextBefore(element, `<template ${replaceText}>\n`)
+        const vFor = startTag.attributes.find(
+          (attr) => attr.directive && attr.key.name.name === 'for'
+        )
+        const vForText = vFor ? `${sourceCode.getText(vFor)} ` : ''
+        if (vFor) {
+          yield fixer.remove(vFor)
+        }
+
+        yield fixer.insertTextBefore(
+          element,
+          `<template ${vForText}${replaceText}>\n`
+        )
         yield fixer.insertTextAfter(element, `\n</template>`)
       }
     }
diff --git a/lib/rules/syntaxes/utils/can-convert-to-v-slot.js b/lib/rules/syntaxes/utils/can-convert-to-v-slot.js
index f4e66778..e0e51053 100644
--- a/lib/rules/syntaxes/utils/can-convert-to-v-slot.js
+++ b/lib/rules/syntaxes/utils/can-convert-to-v-slot.js
@@ -28,7 +28,8 @@ module.exports = function canConvertToVSlot(element, sourceCode, tokenStore) {
   const ownerElement = element.parent
   if (
     ownerElement.type === 'VDocumentFragment' ||
-    !utils.isCustomComponent(ownerElement)
+    !utils.isCustomComponent(ownerElement) ||
+    ownerElement.name === 'component'
   ) {
     return false
   }
diff --git a/package.json b/package.json
index 153ebe94..a10ad6b0 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "eslint-plugin-vue",
-  "version": "9.27.0",
+  "version": "9.28.0",
   "description": "Official ESLint plugin for Vue.js",
   "main": "lib/index.js",
   "scripts": {


Test patch:
diff --git a/tests/lib/rules/attribute-hyphenation.js b/tests/lib/rules/attribute-hyphenation.js
index 4d867d15..18d60e19 100644
--- a/tests/lib/rules/attribute-hyphenation.js
+++ b/tests/lib/rules/attribute-hyphenation.js
@@ -26,6 +26,11 @@ ruleTester.run('attribute-hyphenation', rule, {
       code: '<template><div><custom data-id="foo" aria-test="bar" slot-scope="{ data }" my-prop="prop"></custom></div></template>',
       options: ['always']
     },
+    {
+      filename: 'test.vue',
+      code: '<template><div><custom :my-prop="prop" v-model:foo-bar="fooBar"></custom></div></template>',
+      options: ['always']
+    },
     {
       filename: 'test.vue',
       code: '<template><div><custom data-id="foo" aria-test="bar" slot-scope="{ data }" myProp="prop"></custom></div></template>',
@@ -70,6 +75,16 @@ ruleTester.run('attribute-hyphenation', rule, {
       filename: 'test.vue',
       code: '<template><div><custom :attr_ff="prop"></custom></div></template>',
       options: ['never']
+    },
+    {
+      filename: 'test.vue',
+      code: '<template><div><custom :my-name.sync="prop"></custom></div></template>',
+      options: ['always']
+    },
+    {
+      filename: 'test.vue',
+      code: '<template><div><custom :myName.sync="prop"></custom></div></template>',
+      options: ['never']
     }
   ],
 
@@ -209,6 +224,48 @@ ruleTester.run('attribute-hyphenation', rule, {
         }
       ]
     },
+    {
+      // https://github.com/vuejs/eslint-plugin-vue/issues/2510
+      filename: 'test.vue',
+      code: '<template><div><custom v-model:my-prop="prop"></custom></div></template>',
+      output:
+        '<template><div><custom v-model:myProp="prop"></custom></div></template>',
+      options: ['never'],
+      errors: [
+        {
+          message: "Attribute 'v-model:my-prop' can't be hyphenated.",
+          type: 'VDirectiveKey',
+          line: 1
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: '<template><div><custom v-model:myProp="prop"></custom></div></template>',
+      output:
+        '<template><div><custom v-model:my-prop="prop"></custom></div></template>',
+      options: ['always'],
+      errors: [
+        {
+          message: "Attribute 'v-model:myProp' must be hyphenated.",
+          type: 'VDirectiveKey',
+          line: 1
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: '<template><div><custom v-model:MyProp="prop"></custom></div></template>',
+      output: null,
+      options: ['always'],
+      errors: [
+        {
+          message: "Attribute 'v-model:MyProp' must be hyphenated.",
+          type: 'VDirectiveKey',
+          line: 1
+        }
+      ]
+    },
     {
       filename: 'test.vue',
       code: `
@@ -367,6 +424,32 @@ ruleTester.run('attribute-hyphenation', rule, {
           line: 1
         }
       ]
+    },
+    {
+      filename: 'test.vue',
+      code: '<template><div><custom :myAge.sync="prop"></custom></div></template>',
+      output: null,
+      options: ['always'],
+      errors: [
+        {
+          message: "Attribute ':myAge.sync' must be hyphenated.",
+          type: 'VDirectiveKey',
+          line: 1
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: '<template><div><custom :my-age.sync="prop"></custom></div></template>',
+      output: null,
+      options: ['never'],
+      errors: [
+        {
+          message: "Attribute ':my-age.sync' can't be hyphenated.",
+          type: 'VDirectiveKey',
+          line: 1
+        }
+      ]
     }
   ]
 })
diff --git a/tests/lib/rules/define-macros-order.js b/tests/lib/rules/define-macros-order.js
index 3aa2e434..72b35986 100644
--- a/tests/lib/rules/define-macros-order.js
+++ b/tests/lib/rules/define-macros-order.js
@@ -195,6 +195,65 @@ tester.run('define-macros-order', rule, {
         }
       ]
     },
+    {
+      filename: 'test.vue',
+      code: `
+        <script setup>
+          import Foo from 'foo'
+          /** Page */
+          definePage()
+          /** model */
+          const model = defineModel()
+          /** emits */
+          defineEmits(['update:foo'])
+        </script>
+      `,
+      options: [
+        {
+          order: ['definePage', 'defineModel', 'defineEmits']
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <script setup>
+          import Foo from 'foo'
+          /** model */
+          const model = defineModel()
+          /** emits */
+          defineEmits(['update:foo'])
+
+          function fn() {
+            definePage()
+          }
+        </script>
+      `,
+      options: [
+        {
+          order: ['definePage', 'defineModel', 'defineEmits']
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <script setup>
+          import Foo from 'foo'
+          /** model */
+          const model = defineModel()
+          /** emits */
+          defineEmits(['update:foo'])
+
+          const val = () => definePage()
+        </script>
+      `,
+      options: [
+        {
+          order: ['definePage', 'defineModel', 'defineEmits']
+        }
+      ]
+    },
     {
       filename: 'test.vue',
       code: `
@@ -254,6 +313,22 @@ tester.run('define-macros-order', rule, {
           order: ['defineModel', 'defineSlots']
         }
       ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <script setup>
+        const page = definePage()
+
+        const first = defineModel('first')
+        const second = defineModel('second')
+        </script>
+      `,
+      options: [
+        {
+          order: ['definePage', 'defineModel']
+        }
+      ]
     }
   ],
   invalid: [
@@ -382,6 +457,40 @@ tester.run('define-macros-order', rule, {
         }
       ]
     },
+    {
+      filename: 'test.vue',
+      code: `
+        <script setup>
+          console.log('test1')
+          const props = defineProps({
+            test: Boolean
+          })
+          console.log('test2')
+          const page = definePage({
+            name: 'hello'
+          })
+        </script>
+      `,
+      output: `
+        <script setup>
+          const page = definePage({
+            name: 'hello'
+          })
+          const props = defineProps({
+            test: Boolean
+          })
+          console.log('test1')
+          console.log('test2')
+        </script>
+      `,
+      options: [{ order: ['definePage', 'defineProps'] }],
+      errors: [
+        {
+          message: message('definePage'),
+          line: 8
+        }
+      ]
+    },
     {
       filename: 'test.vue',
       code: `
@@ -425,6 +534,61 @@ tester.run('define-macros-order', rule, {
         }
       ]
     },
+    {
+      filename: 'test.vue',
+      code: `
+        <script lang="ts" setup>
+          interface Props {
+            msg?: string
+            labels?: string[]
+          }
+
+          const props = defineProps<{
+            msg?: string
+            labels?: string[]
+          }>()
+          defineCustom()
+          const emit = defineEmits<{(e: 'update:test'): void}>()
+
+          const page = definePage({
+            name: 'hello'
+          })
+        </script>
+      `,
+      output: `
+        <script lang="ts" setup>
+          interface Props {
+            msg?: string
+            labels?: string[]
+          }
+
+          const page = definePage({
+            name: 'hello'
+          })
+          defineCustom()
+          const props = defineProps<{
+            msg?: string
+            labels?: string[]
+          }>()
+          const emit = defineEmits<{(e: 'update:test'): void}>()
+
+        </script>
+      `,
+      options: [
+        { order: ['definePage', 'defineCustom', 'defineProps', 'defineEmits'] }
+      ],
+      languageOptions: {
+        parserOptions: {
+          parser: require.resolve('@typescript-eslint/parser')
+        }
+      },
+      errors: [
+        {
+          message: message('definePage'),
+          line: 15
+        }
+      ]
+    },
     {
       filename: 'test.vue',
       code: `
@@ -537,6 +701,25 @@ tester.run('define-macros-order', rule, {
         }
       ]
     },
+    {
+      filename: 'test.vue',
+      code: `
+        <script setup>
+          const props = defineProps({ test: Boolean });definePage({name: 'hello'})
+        </script>
+      `,
+      output: `
+        <script setup>
+          definePage({name: 'hello'});const props = defineProps({ test: Boolean });        </script>
+      `,
+      options: [{ order: ['definePage', 'defineProps'] }],
+      errors: [
+        {
+          message: message('definePage'),
+          line: 3
+        }
+      ]
+    },
     {
       filename: 'test.vue',
       code: `
@@ -633,6 +816,54 @@ tester.run('define-macros-order', rule, {
         }
       ]
     },
+    {
+      filename: 'test.vue',
+      code: `
+        <script setup>
+          definePage({name: 'hello'})
+          console.log('test1')
+          defineCustom({ test: Boolean })
+        </script>
+      `,
+      output: `
+        <script setup>
+          defineCustom({ test: Boolean })
+          definePage({name: 'hello'})
+          console.log('test1')
+        </script>
+      `,
+      options: [{ order: ['defineCustom', 'definePage'] }],
+      errors: [
+        {
+          message: message('defineCustom'),
+          line: 5
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <script setup>
+          definePage({name: 'hello'})
+          console.log('test1')
+          const custom = defineCustom({ test: Boolean })
+        </script>
+      `,
+      output: `
+        <script setup>
+          const custom = defineCustom({ test: Boolean })
+          definePage({name: 'hello'})
+          console.log('test1')
+        </script>
+      `,
+      options: [{ order: ['defineCustom', 'definePage'] }],
+      errors: [
+        {
+          message: message('defineCustom'),
+          line: 5
+        }
+      ]
+    },
     {
       filename: 'test.vue',
       code: `
@@ -895,6 +1126,44 @@ tester.run('define-macros-order', rule, {
         }
       ]
     },
+    {
+      filename: 'test.vue',
+      code: `
+        <script setup>
+          defineOptions({})
+          defineCustom('second')
+          const something = defineSomething('first')
+          definePage()
+          const model = defineModel('second')
+        </script>
+      `,
+      output: `
+        <script setup>
+          const something = defineSomething('first')
+          defineCustom('second')
+          const model = defineModel('second')
+          defineOptions({})
+          definePage()
+        </script>
+      `,
+      options: [
+        {
+          order: [
+            'defineSomething',
+            'defineCustom',
+            'defineModel',
+            'defineOptions',
+            'definePage'
+          ]
+        }
+      ],
+      errors: [
+        {
+          message: message('defineSomething'),
+          line: 5
+        }
+      ]
+    },
     {
       filename: 'test.vue',
       code: `
diff --git a/tests/lib/rules/max-template-depth.js b/tests/lib/rules/max-template-depth.js
new file mode 100644
index 00000000..4ef9e95d
--- /dev/null
+++ b/tests/lib/rules/max-template-depth.js
@@ -0,0 +1,168 @@
+/**
+ * @author kevsommer Kevin Sommer
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+const RuleTester = require('../../eslint-compat').RuleTester
+const rule = require('../../../lib/rules/max-template-depth')
+
+const tester = new RuleTester({
+  languageOptions: {
+    parser: require('vue-eslint-parser'),
+    ecmaVersion: 2020,
+    sourceType: 'module'
+  }
+})
+
+tester.run('max-template-depth', rule, {
+  valid: [
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+      <main-container>
+        <child-component>
+          <div />
+        </child-component>
+      </main-container>
+      </template>
+      `,
+      options: [{ maxDepth: 3 }]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+      <main-container>
+        <ul>
+          <li>Item 1</li>
+        </ul>
+      </main-container>
+      </template>
+      `,
+      options: [{ maxDepth: 4 }]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+      <main-container>
+        <child-component>
+          <sub-child-component>
+            <div />
+          </sub-child-component>
+        </child-component>
+      </main-container>
+      </template>
+      `,
+      options: [{ maxDepth: 4 }]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+      <div>
+      </div>
+      <main-container>
+        <child-component>
+          <ul>
+            <li />
+          </ul>
+        </child-component>
+      </main-container>
+      </template>
+      `,
+      options: [{ maxDepth: 4 }]
+    }
+  ],
+  invalid: [
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+      <div>
+        <div>
+          <div>
+            <div />
+          </div>
+        </div>
+      </div>
+      </template>
+      `,
+      options: [{ maxDepth: 3 }],
+      errors: [
+        {
+          message:
+            'Element is nested too deeply (depth of 4, maximum allowed is 3).',
+          line: 6
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+      <side-container>
+        <child-component />
+      </side-container>
+      <main-container>
+        <child-component>
+          <nested-component>
+            <h1 />
+          </nested-component>
+        </child-component>
+      </main-container>
+      </template>
+      `,
+      options: [{ maxDepth: 3 }],
+      errors: [
+        {
+          message:
+            'Element is nested too deeply (depth of 4, maximum allowed is 3).',
+          line: 9
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+      <main-container>
+        <nav-bar>
+          <div />
+        </nav-bar>
+        <child-component>
+          <nested-component>
+            <div>
+              <div />
+            <div>
+          </nested-component>
+        </child-component>
+      </main-container>
+      </template>
+      `,
+      options: [{ maxDepth: 3 }],
+      errors: [
+        {
+          message:
+            'Element is nested too deeply (depth of 4, maximum allowed is 3).',
+          line: 9,
+          endLine: 12
+        },
+        {
+          message:
+            'Element is nested too deeply (depth of 5, maximum allowed is 3).',
+          line: 10,
+          endLine: 10
+        },
+        {
+          message:
+            'Element is nested too deeply (depth of 5, maximum allowed is 3).',
+          line: 11,
+          endLine: 12
+        }
+      ]
+    }
+  ]
+})
diff --git a/tests/lib/rules/no-deprecated-delete-set.js b/tests/lib/rules/no-deprecated-delete-set.js
new file mode 100644
index 00000000..a38b6ece
--- /dev/null
+++ b/tests/lib/rules/no-deprecated-delete-set.js
@@ -0,0 +1,379 @@
+/**
+ * @author Wayne Zhang
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+const RuleTester = require('../../eslint-compat').RuleTester
+const rule = require('../../../lib/rules/no-deprecated-delete-set')
+
+const tester = new RuleTester({
+  languageOptions: {
+    parser: require('vue-eslint-parser'),
+    ecmaVersion: 2020,
+    sourceType: 'module',
+    globals: {
+      require: 'readonly'
+    }
+  }
+})
+
+tester.run('no-deprecated-delete-set', rule, {
+  valid: [
+    {
+      filename: 'test.js',
+      code: `
+        const another = function () {
+          this.$set(obj, key, value)
+          this.$delete(obj, key)
+        }
+
+        createApp({
+          mounted () {
+            this.$emit('start')
+          },
+          methods: {
+            click () {
+              this.$emit('click')
+            }
+          }
+        })
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <script>
+          export default {
+            mounted () {
+              this.$nextTick()
+            }
+          }
+        </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <script>
+          export default {
+            mounted () {
+              a(this.$set)
+              a(this.$delete)
+            }
+          }
+        </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <script>
+        function set(obj, key, value) {}
+        function del(obj, key) {}
+        export default {
+          mounted () {
+            set(obj, key, value)
+            del(obj, key)
+          }
+        }
+        </script>
+      `
+    },
+    // from vue
+    {
+      filename: 'test.js',
+      code: `
+        app.component('some-comp', {
+          mounted () {
+            Vue.nextTick()
+          }
+        })
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <script>
+          import { nextTick as nt, provide } from 'vue'
+          export default {
+            mounted () {
+              nt()
+              provide(key, value)
+            }
+          }
+        </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <script>
+          const { nextTick: set, provide: del } = require('vue')
+          export default {
+            mounted () {
+              set()
+              del(key, value)
+            }
+          }
+        </script>
+      `
+    }
+  ],
+
+  invalid: [
+    {
+      filename: 'test.js',
+      code: `
+        app.component('some-comp', {
+          mounted () {
+            this.$set(obj, key, value)
+            this.$delete(obj, key)
+          }
+        })
+      `,
+      errors: [
+        {
+          messageId: 'deprecated',
+          line: 4
+        },
+        {
+          messageId: 'deprecated',
+          line: 5
+        }
+      ]
+    },
+    {
+      filename: 'test.js',
+      code: `
+        app.component('some-comp', {
+          mounted () {
+            const vm = this
+            vm.$set(obj, key, value)
+            vm.$delete(obj, key)
+          }
+        })
+      `,
+      errors: [
+        {
+          messageId: 'deprecated',
+          line: 5
+        },
+        {
+          messageId: 'deprecated',
+          line: 6
+        }
+      ]
+    },
+    {
+      filename: 'test.js',
+      code: `
+        app.component('some-comp', {
+          mounted () {
+            this?.$set(obj, key, value)
+            this?.$delete(obj, key)
+          }
+        })
+      `,
+      errors: [
+        { messageId: 'deprecated', line: 4 },
+        { messageId: 'deprecated', line: 5 }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <script>
+          export default {
+            mounted () {
+              this.$set(obj, key, value)
+              this.$delete(obj, key)
+            }
+          }
+        </script>
+      `,
+      errors: [
+        { messageId: 'deprecated', line: 5 },
+        { messageId: 'deprecated', line: 6 }
+      ]
+    },
+    // from vue
+    {
+      filename: 'test.js',
+      code: `
+        app.component('some-comp', {
+          mounted () {
+            Vue.set(obj, key, value)
+            Vue.delete(obj, key)
+          }
+        })
+      `,
+      errors: [
+        {
+          messageId: 'deprecated',
+          line: 4
+        },
+        {
+          messageId: 'deprecated',
+          line: 5
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <script>
+          import { set, del } from 'vue'
+          export default {
+            mounted () {
+              set(obj, key, value)
+              del(obj, key)
+            }
+          }
+        </script>
+      `,
+      errors: [
+        {
+          messageId: 'deprecated',
+          line: 6
+        },
+        {
+          messageId: 'deprecated',
+          line: 7
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <script setup>
+          import { set, del } from 'vue'
+
+          set(obj, key, value)
+          del(obj, key)
+        </script>
+      `,
+      errors: [
+        {
+          messageId: 'deprecated',
+          line: 5
+        },
+        {
+          messageId: 'deprecated',
+          line: 6
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <script setup>
+          import { set as s, del as d } from 'vue'
+
+          s(obj, key, value)
+          d(obj, key)
+        </script>
+      `,
+      errors: [
+        {
+          messageId: 'deprecated',
+          line: 5
+        },
+        {
+          messageId: 'deprecated',
+          line: 6
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <script>
+          const { set, del } = require('vue')
+          export default {
+            mounted () {
+              set(obj, key, value)
+              del(obj, key)
+            }
+          }
+        </script>
+      `,
+      errors: [
+        {
+          messageId: 'deprecated',
+          line: 6
+        },
+        {
+          messageId: 'deprecated',
+          line: 7
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <script>
+          const { set: s, del: d } = require('vue')
+          export default {
+            mounted () {
+              s(obj, key, value)
+              d(obj, key)
+            }
+          }
+        </script>
+      `,
+      errors: [
+        {
+          messageId: 'deprecated',
+          line: 6
+        },
+        {
+          messageId: 'deprecated',
+          line: 7
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <script setup>
+          const { set, del } = require('vue')
+
+          set(obj, key, value)
+          del(obj, key)
+        </script>
+      `,
+      errors: [
+        {
+          messageId: 'deprecated',
+          line: 5
+        },
+        {
+          messageId: 'deprecated',
+          line: 6
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <script setup>
+          const v = require('vue')
+
+          v.set(obj, key, value)
+          v.del(obj, key)
+        </script>
+      `,
+      errors: [
+        {
+          messageId: 'deprecated',
+          line: 5
+        },
+        {
+          messageId: 'deprecated',
+          line: 6
+        }
+      ]
+    }
+  ]
+})
diff --git a/tests/lib/rules/no-deprecated-slot-attribute.js b/tests/lib/rules/no-deprecated-slot-attribute.js
index 51d10935..2fd5fa40 100644
--- a/tests/lib/rules/no-deprecated-slot-attribute.js
+++ b/tests/lib/rules/no-deprecated-slot-attribute.js
@@ -643,6 +643,44 @@ tester.run('no-deprecated-slot-attribute', rule, {
         }
       ],
       errors: ['`slot` attributes are deprecated.']
+    },
+    {
+      code: `
+      <template>
+        <my-component>
+          <slot
+            v-for="slot in Object.keys($slots)"
+            :slot="slot"
+            :name="slot"
+          ></slot>
+        </my-component>
+      </template>`,
+      output: `
+      <template>
+        <my-component>
+          <template v-for="slot in Object.keys($slots)" v-slot:[slot]>
+<slot
+            
+            
+            :name="slot"
+          ></slot>
+</template>
+        </my-component>
+      </template>`,
+      errors: ['`slot` attributes are deprecated.']
+    },
+    {
+      code: `
+      <template>
+        <component :is="toggle ? 'my-component' : 'div'">
+          <div slot="named">
+            Passing in a named slot to a div worked with old syntax
+            But not with new syntax
+          </div>
+        </component>
+      </template>`,
+      output: null,
+      errors: ['`slot` attributes are deprecated.']
     }
   ]
 })
diff --git a/tests/lib/rules/no-unused-refs.js b/tests/lib/rules/no-unused-refs.js
index 7e092ee8..a4407a60 100644
--- a/tests/lib/rules/no-unused-refs.js
+++ b/tests/lib/rules/no-unused-refs.js
@@ -315,6 +315,20 @@ tester.run('no-unused-refs', rule, {
       const x = ref(null)
       </script>
       `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <input ref="x" />
+        <input ref="y" />
+      </template>
+      <script setup>
+      import {useTemplateRef} from 'vue'
+      const inputX = useTemplateRef('x')
+      const inputY = useTemplateRef(\`y\`)
+      </script>
+      `
     }
   ],
 
diff --git a/tests/lib/rules/require-toggle-inside-transition.js b/tests/lib/rules/require-toggle-inside-transition.js
index d40ad474..cdd48c49 100644
--- a/tests/lib/rules/require-toggle-inside-transition.js
+++ b/tests/lib/rules/require-toggle-inside-transition.js
@@ -77,6 +77,15 @@ tester.run('require-toggle-inside-transition', rule, {
     {
       filename: 'test.vue',
       code: '<template><transition :appear="true"><div /></transition></template>'
+    },
+    {
+      filename: 'test.vue',
+      code: '<template><transition><dialog v-dialog="show" /></transition></template>',
+      options: [
+        {
+          additionalDirectives: ['dialog']
+        }
+      ]
     }
   ],
   invalid: [
@@ -132,6 +141,36 @@ tester.run('require-toggle-inside-transition', rule, {
       filename: 'test.vue',
       code: '<template><transition @appear="isLoaded"><div /></transition></template>',
       errors: [{ messageId: 'expected' }]
+    },
+    {
+      filename: 'test.vue',
+      code: '<template><transition><dialog v-dialog="show" /></transition></template>',
+      options: [
+        {
+          additionalDirectives: []
+        }
+      ],
+      errors: [
+        {
+          messageId: 'expected',
+          data: { allowedDirectives: '`v-if` or `v-show`' }
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: '<template><transition><div v-custom="show" /></transition></template>',
+      options: [
+        {
+          additionalDirectives: ['dialog']
+        }
+      ],
+      errors: [
+        {
+          messageId: 'expected',
+          data: { allowedDirectives: '`v-if`, `v-show` or `v-dialog`' }
+        }
+      ]
     }
   ]
 })