Repository: vuejs/eslint-plugin-vue (js)

Issue:
`no-duplicate-attr-inheritance`: should ignore components with multiple root nodes?
<!--
  ❗Please don't ignore this template.❗

  If you ignore it, we're just going to respond asking you to fill it out, which wastes everyone's time.
  The more relevant information you can include, the faster we can find the issue and fix it without asking you for more info.
-->

<!--
  Before posting the issue, please confirm that the problem you're getting
  is not related with your code editor configuration.
  To make sure it's not, run: yarn eslint src/your-file.vue
-->

**Checklist**

- [x] I have tried restarting my IDE and the issue persists.
- [x] I have read the [FAQ](https://eslint.vuejs.org/user-guide/#faq) and my problem is not listed.
<!-- If you do not read the FAQ and open an issue that is listed in the FAQ, we may silently close the issue. -->

**Tell us about your environment**

- **ESLint version:** 9.13.0
- **eslint-plugin-vue version:** 9.30.3
- **Vue version:** 3.5.12
- **Node version:** 
- **Operating System:** 

**Please show your full configuration:**
<!-- Paste content of your .eslintrc file -->
```json5

```

**What did you do?**	

If a component has multiple root nodes, Vue [encourages the presence of `v-bind="$attrs"`](https://vuejs.org/guide/components/attrs.html#attribute-inheritance-on-multiple-root-nodes) because essentially `inheritAttrs` is implicitly set to `false`. I suggest that this rule ignores such components as setting `inheritAttrs: false` manually becomes redundant. Or perhaps have an option to ignore or not ignore for the sake of explicitness?

Example of SFCs that shouldn't be reported:

```vue
<template>
  <div v-bind="$attrs"></div>
  Some text
</template>

<template>
  <div v-bind="$attrs"></div>
  <div>Another div</div>
</template>
```

[Test on the rule page](https://eslint.vuejs.org/rules/no-duplicate-attr-inheritance.html)

**What did you expect to happen?**


**What actually happened?**
<!--
  Please include the actual, raw output from ESLint.
  If you are only looking at the results of your editor extension, also check the CLI results.
-->

**Repository to reproduce this issue**
<!--
  ❗ Please share a repository that can reproduce your issue.
  ❗ If you don't share it, we just label it as requesting a repository that can reproduce your issue.
  ❗ If you don't share it for a few days, we silently close this issue.
-->



Pull request description:
feat(no-duplicate-attr-inheritance): ignore multi root
resolve #2596.

- since this will result in fewer errors, should we still consider adding an option to control this?
- multi root could also be a group of `v-if` / `v-else` / `v-else` nodes. Should we consider treating it as a "single" root?




Patch:
diff --git a/docs/rules/index.md b/docs/rules/index.md
index 1ba5978d..51515e23 100644
--- a/docs/rules/index.md
+++ b/docs/rules/index.md
@@ -232,7 +232,7 @@ For example:
 | [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: |
+| [vue/no-empty-component-block](./no-empty-component-block.md) | disallow the `<template>` `<script>` `<style>` block to be empty | :wrench: | :hammer: |
 | [vue/no-multiple-objects-in-class](./no-multiple-objects-in-class.md) | disallow to pass multiple objects into array to class |  | :hammer: |
 | [vue/no-potential-component-option-typo](./no-potential-component-option-typo.md) | disallow a potential typo in your component property | :bulb: | :hammer: |
 | [vue/no-ref-object-reactivity-loss](./no-ref-object-reactivity-loss.md) | disallow usages of ref objects that can lead to loss of reactivity |  | :warning: |
@@ -270,6 +270,7 @@ For example:
 | [vue/prefer-prop-type-boolean-first](./prefer-prop-type-boolean-first.md) | enforce `Boolean` comes first in component prop types | :bulb: | :warning: |
 | [vue/prefer-separate-static-class](./prefer-separate-static-class.md) | require static class names in template to be in a separate `class` attribute | :wrench: | :hammer: |
 | [vue/prefer-true-attribute-shorthand](./prefer-true-attribute-shorthand.md) | require shorthand form attribute when `v-bind` value is `true` | :bulb: | :hammer: |
+| [vue/prefer-use-template-ref](./prefer-use-template-ref.md) | require using `useTemplateRef` instead of `ref`/`shallowRef` for template refs |  | :hammer: |
 | [vue/require-default-export](./require-default-export.md) | require components to be the default export |  | :warning: |
 | [vue/require-direct-export](./require-direct-export.md) | require the component to be directly exported |  | :hammer: |
 | [vue/require-emit-validator](./require-emit-validator.md) | require type definitions in emits | :bulb: | :hammer: |
diff --git a/docs/rules/no-duplicate-attr-inheritance.md b/docs/rules/no-duplicate-attr-inheritance.md
index 0f1b6034..fe3cd37b 100644
--- a/docs/rules/no-duplicate-attr-inheritance.md
+++ b/docs/rules/no-duplicate-attr-inheritance.md
@@ -13,9 +13,9 @@ since: v7.0.0
 ## :book: Rule Details
 
 This rule aims to prevent duplicate attribute inheritance.  
-This rule to warn to apply `inheritAttrs: false` when it detects `v-bind="$attrs"` being used.
+This rule suggests applying `inheritAttrs: false` when it detects `v-bind="$attrs"` being used.
 
-<eslint-code-block :rules="{'vue/no-duplicate-attr-inheritance': ['error']}">
+<eslint-code-block :rules="{'vue/no-duplicate-attr-inheritance': ['error', { checkMultiRootNodes: false }]}">
 
 ```vue
 <template>
@@ -26,11 +26,12 @@ export default {
   /* ✓ GOOD */
   inheritAttrs: false
 }
+</script>
 ```
 
 </eslint-code-block>
 
-<eslint-code-block :rules="{'vue/no-duplicate-attr-inheritance': ['error']}">
+<eslint-code-block :rules="{'vue/no-duplicate-attr-inheritance': ['error', { checkMultiRootNodes: false }]}">
 
 ```vue
 <template>
@@ -41,17 +42,46 @@ export default {
   /* ✗ BAD */
   // inheritAttrs: true (default)
 }
+</script>
 ```
 
 </eslint-code-block>
 
 ## :wrench: Options
 
-Nothing.
+```json
+{
+  "vue/no-duplicate-attr-inheritance": ["error", {
+    "checkMultiRootNodes": false,
+  }]
+}
+```
+
+- `"checkMultiRootNodes"`: If set to `true`, also suggest applying `inheritAttrs: false` to components with multiple root nodes (where `inheritAttrs: false` is the implicit default, see [attribute inheritance on multiple root nodes](https://vuejs.org/guide/components/attrs.html#attribute-inheritance-on-multiple-root-nodes)), whenever it detects `v-bind="$attrs"` being used. Default is `false`, which will ignore components with multiple root nodes.
+
+### `"checkMultiRootNodes": true`
+
+<eslint-code-block :rules="{'vue/no-duplicate-attr-inheritance': ['error', { checkMultiRootNodes: true }]}">
+
+```vue
+<template>
+  <div v-bind="$attrs" />
+  <div />
+</template>
+<script>
+export default {
+  /* ✗ BAD */
+  // inheritAttrs: true (default)
+}
+</script>
+```
+
+</eslint-code-block>
 
 ## :books: Further Reading
 
 - [API - inheritAttrs](https://vuejs.org/api/options-misc.html#inheritattrs)
+- [Fallthrough Attributes](https://vuejs.org/guide/components/attrs.html#attribute-inheritance-on-multiple-root-nodes)
 
 ## :rocket: Version
 
diff --git a/docs/rules/no-empty-component-block.md b/docs/rules/no-empty-component-block.md
index 36ea4c07..d0e3dbd1 100644
--- a/docs/rules/no-empty-component-block.md
+++ b/docs/rules/no-empty-component-block.md
@@ -10,6 +10,8 @@ since: v7.0.0
 
 > disallow the `<template>` `<script>` `<style>` block to be empty
 
+- :wrench: The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) can automatically fix some of the problems reported by this rule.
+
 ## :book: Rule Details
 
 This rule disallows the `<template>` `<script>` `<style>` block to be empty.
@@ -17,7 +19,7 @@ This rule disallows the `<template>` `<script>` `<style>` block to be empty.
 This rule also checks block what has attribute `src`.
 See [Vue Single-File Component (SFC) Spec](https://vue-loader.vuejs.org/spec.html#src-imports).
 
-<eslint-code-block :rules="{'vue/no-empty-component-block': ['error']}">
+<eslint-code-block fix :rules="{'vue/no-empty-component-block': ['error']}">
 
 ```vue
 <!-- ✓ GOOD -->
diff --git a/docs/rules/no-reserved-component-names.md b/docs/rules/no-reserved-component-names.md
index a5d85cde..31b74ab0 100644
--- a/docs/rules/no-reserved-component-names.md
+++ b/docs/rules/no-reserved-component-names.md
@@ -35,13 +35,15 @@ export default {
 {
   "vue/no-reserved-component-names": ["error", {
     "disallowVueBuiltInComponents": false,
-    "disallowVue3BuiltInComponents": false
+    "disallowVue3BuiltInComponents": false,
+    "htmlElementCaseSensitive": false,
   }]
 }
 ```
 
 - `disallowVueBuiltInComponents` (`boolean`) ... If `true`, disallow Vue.js 2.x built-in component names. Default is `false`.
 - `disallowVue3BuiltInComponents` (`boolean`) ... If `true`, disallow Vue.js 3.x built-in component names. Default is `false`.
+- `htmlElementCaseSensitive` (`boolean`) ... If `true`, component names must exactly match the case of an HTML element to be considered conflicting. Default is `false` (i.e. case-insensitve comparison).
 
 ### `"disallowVueBuiltInComponents": true`
 
@@ -73,6 +75,34 @@ export default {
 
 </eslint-code-block>
 
+### `"htmlElementCaseSensitive": true`
+
+<eslint-code-block :rules="{'vue/no-reserved-component-names': ['error', {htmlElementCaseSensitive: true}]}">
+
+```vue
+<script>
+/* ✓ GOOD */
+export default {
+  name: 'Button'
+}
+</script>
+```
+
+</eslint-code-block>
+
+<eslint-code-block :rules="{'vue/no-reserved-component-names': ['error', {htmlElementCaseSensitive: true}]}">
+
+```vue
+<script>
+/* ✗ BAD */
+export default {
+  name: 'button'
+}
+</script>
+```
+
+</eslint-code-block>
+
 ## :couple: Related Rules
 
 - [vue/multi-word-component-names](./multi-word-component-names.md)
diff --git a/docs/rules/prefer-use-template-ref.md b/docs/rules/prefer-use-template-ref.md
new file mode 100644
index 00000000..1b1b4038
--- /dev/null
+++ b/docs/rules/prefer-use-template-ref.md
@@ -0,0 +1,78 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/prefer-use-template-ref
+description: require using `useTemplateRef` instead of `ref`/`shallowRef` for template refs
+since: v9.31.0
+---
+
+# vue/prefer-use-template-ref
+
+> require using `useTemplateRef` instead of `ref`/`shallowRef` for template refs
+
+## :book: Rule Details
+
+Vue 3.5 introduced a new way of obtaining template refs via
+the [`useTemplateRef()`](https://vuejs.org/guide/essentials/template-refs.html#accessing-the-refs) API.
+
+This rule enforces using the new `useTemplateRef` function instead of `ref`/`shallowRef` for template refs.
+
+<eslint-code-block :rules="{'vue/prefer-use-template-ref': ['error']}">
+
+```vue
+<template>
+  <button ref="submitRef">Submit</button>
+  <button ref="cancelRef">Cancel</button>
+  <button ref="closeRef">Close</button>
+</template>
+
+<script setup>
+  import { ref, shallowRef, useTemplateRef } from 'vue';
+
+  /* ✓ GOOD */
+  const submitRef = useTemplateRef('submitRef');
+  const submitButton = useTemplateRef('submitRef');
+  const closeButton = useTemplateRef('closeRef');
+
+  /* ✗ BAD */
+  const closeRef = ref();
+  const cancelRef = shallowRef();
+</script>
+```
+
+</eslint-code-block>
+
+This rule skips `ref` template function refs as these should be used to allow custom implementation of storing `ref`. If you prefer
+`useTemplateRef`, you have to change the value of the template `ref` to a string.
+
+<eslint-code-block :rules="{'vue/prefer-use-template-ref': ['error']}">
+
+```vue
+<template>
+  <button :ref="ref => submitRef = ref">Submit</button>
+  <button :ref="ref => cancelRef = ref">Cancel</button>
+</template>
+
+<script setup>
+  import { ref, shallowRef } from 'vue';
+  
+  /* ✓ GOOD */
+  const submitRef = ref();
+  const cancelRef = shallowRef();
+</script>
+```
+
+</eslint-code-block>
+
+## :wrench: Options
+
+Nothing.
+
+## :rocket: Version
+
+This rule was introduced in eslint-plugin-vue v9.31.0
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/prefer-use-template-ref.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/prefer-use-template-ref.js)
diff --git a/lib/index.js b/lib/index.js
index bb4abf40..b09e247d 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -208,6 +208,7 @@ const plugin = {
     'prefer-separate-static-class': require('./rules/prefer-separate-static-class'),
     'prefer-template': require('./rules/prefer-template'),
     'prefer-true-attribute-shorthand': require('./rules/prefer-true-attribute-shorthand'),
+    'prefer-use-template-ref': require('./rules/prefer-use-template-ref'),
     'prop-name-casing': require('./rules/prop-name-casing'),
     'quote-props': require('./rules/quote-props'),
     'require-component-is': require('./rules/require-component-is'),
diff --git a/lib/rules/define-macros-order.js b/lib/rules/define-macros-order.js
index 55e6140c..0bdd5f21 100644
--- a/lib/rules/define-macros-order.js
+++ b/lib/rules/define-macros-order.js
@@ -53,6 +53,7 @@ function isUseStrictStatement(node) {
 function getTargetStatementPosition(scriptSetup, program) {
   const skipStatements = new Set([
     'ImportDeclaration',
+    'TSModuleDeclaration',
     'TSInterfaceDeclaration',
     'TSTypeAliasDeclaration',
     'DebuggerStatement',
diff --git a/lib/rules/no-duplicate-attr-inheritance.js b/lib/rules/no-duplicate-attr-inheritance.js
index 654929bd..ba20a52e 100644
--- a/lib/rules/no-duplicate-attr-inheritance.js
+++ b/lib/rules/no-duplicate-attr-inheritance.js
@@ -6,6 +6,33 @@
 
 const utils = require('../utils')
 
+/** @param {VElement[]} elements */
+function isConditionalGroup(elements) {
+  if (elements.length < 2) {
+    return false
+  }
+
+  const firstElement = elements[0]
+  const lastElement = elements[elements.length - 1]
+  const inBetweenElements = elements.slice(1, -1)
+
+  return (
+    utils.hasDirective(firstElement, 'if') &&
+    (utils.hasDirective(lastElement, 'else-if') ||
+      utils.hasDirective(lastElement, 'else')) &&
+    inBetweenElements.every((element) => utils.hasDirective(element, 'else-if'))
+  )
+}
+
+/** @param {VElement[]} elements */
+function isMultiRootNodes(elements) {
+  if (elements.length > 1 && !isConditionalGroup(elements)) {
+    return true
+  }
+
+  return false
+}
+
 module.exports = {
   meta: {
     type: 'suggestion',
@@ -17,15 +44,30 @@ module.exports = {
       url: 'https://eslint.vuejs.org/rules/no-duplicate-attr-inheritance.html'
     },
     fixable: null,
-    schema: [],
+    schema: [
+      {
+        type: 'object',
+        properties: {
+          checkMultiRootNodes: {
+            type: 'boolean'
+          }
+        },
+        additionalProperties: false
+      }
+    ],
     messages: {
       noDuplicateAttrInheritance: 'Set "inheritAttrs" to false.'
     }
   },
   /** @param {RuleContext} context */
   create(context) {
+    const options = context.options[0] || {}
+    const checkMultiRootNodes = options.checkMultiRootNodes === true
+
     /** @type {string | number | boolean | RegExp | BigInt | null} */
     let inheritsAttrs = true
+    /** @type {VReference[]} */
+    const attrsRefs = []
 
     /** @param {ObjectExpression} node */
     function processOptions(node) {
@@ -54,7 +96,7 @@ module.exports = {
           if (!inheritsAttrs) {
             return
           }
-          const attrsRef = node.references.find((reference) => {
+          const reference = node.references.find((reference) => {
             if (reference.variable != null) {
               // Not vm reference
               return false
@@ -62,14 +104,32 @@ module.exports = {
             return reference.id.name === '$attrs'
           })
 
-          if (attrsRef) {
-            context.report({
-              node: attrsRef.id,
-              messageId: 'noDuplicateAttrInheritance'
-            })
+          if (reference) {
+            attrsRefs.push(reference)
           }
         }
-      })
+      }),
+      {
+        'Program:exit'(program) {
+          const element = program.templateBody
+          if (element == null) {
+            return
+          }
+
+          const rootElements = element.children.filter(utils.isVElement)
+
+          if (!checkMultiRootNodes && isMultiRootNodes(rootElements)) return
+
+          if (attrsRefs.length > 0) {
+            for (const attrsRef of attrsRefs) {
+              context.report({
+                node: attrsRef.id,
+                messageId: 'noDuplicateAttrInheritance'
+              })
+            }
+          }
+        }
+      }
     )
   }
 }
diff --git a/lib/rules/no-empty-component-block.js b/lib/rules/no-empty-component-block.js
index b2e428bc..e7afc59f 100644
--- a/lib/rules/no-empty-component-block.js
+++ b/lib/rules/no-empty-component-block.js
@@ -45,7 +45,7 @@ module.exports = {
       categories: undefined,
       url: 'https://eslint.vuejs.org/rules/no-empty-component-block.html'
     },
-    fixable: null,
+    fixable: 'code',
     schema: [],
     messages: {
       unexpected: '`<{{ blockName }}>` is empty. Empty block is not allowed.'
@@ -91,6 +91,9 @@ module.exports = {
               messageId: 'unexpected',
               data: {
                 blockName: componentBlock.name
+              },
+              fix(fixer) {
+                return fixer.remove(componentBlock)
               }
             })
           }
diff --git a/lib/rules/no-reserved-component-names.js b/lib/rules/no-reserved-component-names.js
index 90e53604..56621b42 100644
--- a/lib/rules/no-reserved-component-names.js
+++ b/lib/rules/no-reserved-component-names.js
@@ -34,19 +34,6 @@ function isLowercase(word) {
   return /^[a-z]*$/.test(word)
 }
 
-const RESERVED_NAMES_IN_HTML = new Set([
-  ...htmlElements,
-  ...htmlElements.map(casing.capitalize)
-])
-const RESERVED_NAMES_IN_OTHERS = new Set([
-  ...deprecatedHtmlElements,
-  ...deprecatedHtmlElements.map(casing.capitalize),
-  ...kebabCaseElements,
-  ...kebabCaseElements.map(casing.pascalCase),
-  ...svgElements,
-  ...svgElements.filter(isLowercase).map(casing.capitalize)
-])
-
 /**
  * @param {Expression | SpreadElement} node
  * @returns {node is (Literal | TemplateLiteral)}
@@ -61,14 +48,14 @@ function canVerify(node) {
 }
 
 /**
- * @param {string} name
- * @returns {string}
+ * @template T
+ * @param {Set<T>} set
+ * @param {Iterable<T>} iterable
  */
-function getMessageId(name) {
-  if (RESERVED_NAMES_IN_HTML.has(name)) return 'reservedInHtml'
-  if (RESERVED_NAMES_IN_VUE.has(name)) return 'reservedInVue'
-  if (RESERVED_NAMES_IN_VUE3.has(name)) return 'reservedInVue3'
-  return 'reserved'
+function addAll(set, iterable) {
+  for (const element of iterable) {
+    set.add(element)
+  }
 }
 
 module.exports = {
@@ -90,6 +77,9 @@ module.exports = {
           },
           disallowVue3BuiltInComponents: {
             type: 'boolean'
+          },
+          htmlElementCaseSensitive: {
+            type: 'boolean'
           }
         },
         additionalProperties: false
@@ -109,6 +99,23 @@ module.exports = {
       options.disallowVueBuiltInComponents === true
     const disallowVue3BuiltInComponents =
       options.disallowVue3BuiltInComponents === true
+    const htmlElementCaseSensitive = options.htmlElementCaseSensitive === true
+
+    const RESERVED_NAMES_IN_HTML = new Set(htmlElements)
+    const RESERVED_NAMES_IN_OTHERS = new Set([
+      ...deprecatedHtmlElements,
+      ...kebabCaseElements,
+      ...svgElements
+    ])
+
+    if (!htmlElementCaseSensitive) {
+      addAll(RESERVED_NAMES_IN_HTML, htmlElements.map(casing.capitalize))
+      addAll(RESERVED_NAMES_IN_OTHERS, [
+        ...deprecatedHtmlElements.map(casing.capitalize),
+        ...kebabCaseElements.map(casing.pascalCase),
+        ...svgElements.filter(isLowercase).map(casing.capitalize)
+      ])
+    }
 
     const reservedNames = new Set([
       ...RESERVED_NAMES_IN_HTML,
@@ -117,6 +124,17 @@ module.exports = {
       ...RESERVED_NAMES_IN_OTHERS
     ])
 
+    /**
+     * @param {string} name
+     * @returns {string}
+     */
+    function getMessageId(name) {
+      if (RESERVED_NAMES_IN_HTML.has(name)) return 'reservedInHtml'
+      if (RESERVED_NAMES_IN_VUE.has(name)) return 'reservedInVue'
+      if (RESERVED_NAMES_IN_VUE3.has(name)) return 'reservedInVue3'
+      return 'reserved'
+    }
+
     /**
      * @param {Literal | TemplateLiteral} node
      */
diff --git a/lib/rules/no-unused-emit-declarations.js b/lib/rules/no-unused-emit-declarations.js
index 14643893..f13784d8 100644
--- a/lib/rules/no-unused-emit-declarations.js
+++ b/lib/rules/no-unused-emit-declarations.js
@@ -56,18 +56,6 @@ 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) {
-  if (emitReferenceIds.size === 0) return false
-  const emitVariable = emitReferenceIds.values().next().value.name
-  return emitVariable === name
-}
-
 module.exports = {
   meta: {
     type: 'suggestion',
@@ -91,6 +79,7 @@ module.exports = {
     /** @type {Map<ObjectExpression | Program, SetupContext>} */
     const setupContexts = new Map()
     const programNode = context.getSourceCode().ast
+    let emitParamName = ''
 
     /**
      * @param {CallExpression} node
@@ -204,14 +193,6 @@ 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') {
@@ -229,8 +210,11 @@ module.exports = {
           }
         }
 
-        // verify $emit() in template
-        if (callee.type === 'Identifier' && callee.name === '$emit') {
+        // verify $emit() and defineEmits variable in template
+        if (
+          callee.type === 'Identifier' &&
+          (callee.name === '$emit' || callee.name === emitParamName)
+        ) {
           addEmitCall(node)
         }
       }
@@ -316,10 +300,15 @@ module.exports = {
           }
 
           const emitParam = node.parent.id
-          const variable =
-            emitParam.type === 'Identifier'
-              ? findVariable(utils.getScope(context, emitParam), emitParam)
-              : null
+          if (emitParam.type !== 'Identifier') {
+            return
+          }
+
+          emitParamName = emitParam.name
+          const variable = findVariable(
+            utils.getScope(context, emitParam),
+            emitParam
+          )
           if (!variable) {
             return
           }
diff --git a/lib/rules/prefer-use-template-ref.js b/lib/rules/prefer-use-template-ref.js
new file mode 100644
index 00000000..916583e9
--- /dev/null
+++ b/lib/rules/prefer-use-template-ref.js
@@ -0,0 +1,97 @@
+/**
+ * @author Thomasan1999
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+const utils = require('../utils')
+
+/** @param expression {Expression | null} */
+function expressionIsRef(expression) {
+  return (
+    // @ts-ignore
+    expression?.callee?.name === 'ref' ||
+    // @ts-ignore
+    expression?.callee?.name === 'shallowRef'
+  )
+}
+
+/** @type {import("eslint").Rule.RuleModule} */
+module.exports = {
+  meta: {
+    type: 'suggestion',
+    docs: {
+      description:
+        'require using `useTemplateRef` instead of `ref`/`shallowRef` for template refs',
+      categories: undefined,
+      url: 'https://eslint.vuejs.org/rules/prefer-use-template-ref.html'
+    },
+    schema: [],
+    messages: {
+      preferUseTemplateRef: "Replace '{{name}}' with 'useTemplateRef'."
+    }
+  },
+  /** @param {RuleContext} context */
+  create(context) {
+    /** @type Set<string> */
+    const templateRefs = new Set()
+
+    /**
+     * @typedef ScriptRef
+     * @type {{node: Expression, ref: string}}
+     */
+
+    /**
+     * @type ScriptRef[] */
+    const scriptRefs = []
+
+    return utils.compositingVisitors(
+      utils.defineTemplateBodyVisitor(
+        context,
+        {
+          'VAttribute[directive=false]'(node) {
+            if (node.key.name === 'ref' && node.value?.value) {
+              templateRefs.add(node.value.value)
+            }
+          }
+        },
+        {
+          VariableDeclarator(declarator) {
+            if (!expressionIsRef(declarator.init)) {
+              return
+            }
+
+            scriptRefs.push({
+              // @ts-ignore
+              node: declarator.init,
+              // @ts-ignore
+              ref: declarator.id.name
+            })
+          }
+        }
+      ),
+      {
+        'Program:exit'() {
+          for (const templateRef of templateRefs) {
+            const scriptRef = scriptRefs.find(
+              (scriptRef) => scriptRef.ref === templateRef
+            )
+
+            if (!scriptRef) {
+              continue
+            }
+
+            context.report({
+              node: scriptRef.node,
+              messageId: 'preferUseTemplateRef',
+              data: {
+                // @ts-ignore
+                name: scriptRef.node?.callee?.name
+              }
+            })
+          }
+        }
+      }
+    )
+  }
+}
diff --git a/lib/rules/require-explicit-emits.js b/lib/rules/require-explicit-emits.js
index ba5c406b..e912549e 100644
--- a/lib/rules/require-explicit-emits.js
+++ b/lib/rules/require-explicit-emits.js
@@ -71,22 +71,6 @@ 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',
@@ -128,6 +112,7 @@ module.exports = {
     const vueEmitsDeclarations = new Map()
     /** @type {Map<ObjectExpression | Program, ComponentProp[]>} */
     const vuePropsDeclarations = new Map()
+    let emitParamName = ''
 
     /**
      * @typedef {object} VueTemplateDefineData
@@ -271,11 +256,7 @@ module.exports = {
           // e.g. $emit() / emit() in template
           if (
             callee.type === 'Identifier' &&
-            (callee.name === '$emit' ||
-              isEmitVariableName(
-                callee.name,
-                vueTemplateDefineData.defineEmits
-              ))
+            (callee.name === '$emit' || callee.name === emitParamName)
           ) {
             verifyEmit(
               vueTemplateDefineData.emits,
@@ -308,10 +289,15 @@ module.exports = {
             }
 
             const emitParam = node.parent.id
-            const variable =
-              emitParam.type === 'Identifier'
-                ? findVariable(utils.getScope(context, emitParam), emitParam)
-                : null
+            if (emitParam.type !== 'Identifier') {
+              return
+            }
+
+            emitParamName = emitParam.name
+            const variable = findVariable(
+              utils.getScope(context, emitParam),
+              emitParam
+            )
             if (!variable) {
               return
             }
diff --git a/lib/rules/require-explicit-slots.js b/lib/rules/require-explicit-slots.js
index 0ae77b59..f87503bb 100644
--- a/lib/rules/require-explicit-slots.js
+++ b/lib/rules/require-explicit-slots.js
@@ -35,6 +35,21 @@ function getSlotsName(node) {
   return null
 }
 
+/**
+ * @param {VElement} node
+ * @return {VAttribute | VDirective | undefined}
+ */
+function getSlotNameNode(node) {
+  return node.startTag.attributes.find(
+    (node) =>
+      (!node.directive && node.key.name === 'name') ||
+      (node.directive &&
+        node.key.name.name === 'bind' &&
+        node.key.argument?.type === 'VIdentifier' &&
+        node.key.argument?.name === 'name')
+  )
+}
+
 module.exports = {
   meta: {
     type: 'problem',
@@ -68,6 +83,19 @@ module.exports = {
     }
     const slotsDefined = new Set()
 
+    /**
+     * @param {VElement} node
+     * @param {string | undefined} slotName
+     */
+    function reportMissingSlot(node, slotName) {
+      if (!slotsDefined.has(slotName)) {
+        context.report({
+          node,
+          messageId: 'requireExplicitSlots'
+        })
+      }
+    }
+
     return utils.compositingVisitors(
       utils.defineScriptSetupVisitor(context, {
         onDefineSlotsEnter(node) {
@@ -137,20 +165,27 @@ module.exports = {
         }
       }),
       utils.defineTemplateBodyVisitor(context, {
+        /** @param {VElement} node */
         "VElement[name='slot']"(node) {
-          let slotName = 'default'
-
-          const slotNameAttr = utils.getAttribute(node, 'name')
+          const nameNode = getSlotNameNode(node)
 
-          if (slotNameAttr?.value) {
-            slotName = slotNameAttr.value.value
+          // if no slot name is declared, default to 'default'
+          if (!nameNode) {
+            reportMissingSlot(node, 'default')
+            return
           }
 
-          if (!slotsDefined.has(slotName)) {
-            context.report({
-              node,
-              messageId: 'requireExplicitSlots'
-            })
+          if (nameNode.directive) {
+            const expression = nameNode.value?.expression
+            // ignore attribute binding except string literal
+            if (!expression || !utils.isStringLiteral(expression)) {
+              return
+            }
+
+            const name = utils.getStringLiteralValue(expression) || undefined
+            reportMissingSlot(node, name)
+          } else {
+            reportMissingSlot(node, nameNode.value?.value)
           }
         }
       })
diff --git a/package.json b/package.json
index 47a4381a..75d83695 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "eslint-plugin-vue",
-  "version": "9.30.0",
+  "version": "9.31.0",
   "description": "Official ESLint plugin for Vue.js",
   "main": "lib/index.js",
   "types": "lib/index.d.ts",


Test patch:
diff --git a/tests/lib/rules/define-macros-order.js b/tests/lib/rules/define-macros-order.js
index 72b35986..3def7cf1 100644
--- a/tests/lib/rules/define-macros-order.js
+++ b/tests/lib/rules/define-macros-order.js
@@ -126,6 +126,8 @@ tester.run('define-macros-order', rule, {
       code: `
         <script setup lang="ts">
           import { bar } from 'foo'
+          declare global {}
+          declare namespace Namespace {}
           export interface Props {
             msg?: string
             labels?: string[]
diff --git a/tests/lib/rules/no-duplicate-attr-inheritance.js b/tests/lib/rules/no-duplicate-attr-inheritance.js
index e38711a5..41e9f152 100644
--- a/tests/lib/rules/no-duplicate-attr-inheritance.js
+++ b/tests/lib/rules/no-duplicate-attr-inheritance.js
@@ -43,6 +43,57 @@ ruleTester.run('no-duplicate-attr-inheritance', rule, {
         </script>
       `
     },
+    // ignore multi root by default
+    {
+      filename: 'test.vue',
+      code: `
+      <script setup>
+      defineOptions({ inheritAttrs: true })
+      </script>
+      <template><div v-bind="$attrs"/><div/></template>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div v-if="condition1"></div>
+        <div v-if="condition2" v-bind="$attrs"></div>
+        <div v-else></div>
+      </template>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div v-if="condition1"></div>
+        <div v-else-if="condition2"></div>
+        <div v-bind="$attrs"></div>
+      </template>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div v-bind="$attrs"></div>
+        <div v-if="condition1"></div>
+        <div v-else></div>
+      </template>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div v-if="condition1"></div>
+        <div v-else-if="condition2"></div>
+        <div v-if="condition3" v-bind="$attrs"></div>
+      </template>
+      `,
+      options: [{ checkMultiRootNodes: false }]
+    },
     {
       filename: 'test.vue',
       code: `
@@ -151,6 +202,67 @@ ruleTester.run('no-duplicate-attr-inheritance', rule, {
           line: 5
         }
       ]
+    },
+    {
+      filename: 'test.vue',
+      code: `<template><div v-bind="$attrs"></div><div></div></template>`,
+      options: [{ checkMultiRootNodes: true }],
+      errors: [{ message: 'Set "inheritAttrs" to false.' }]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div v-if="condition1" v-bind="$attrs"></div>
+        <div v-else></div>
+        <div v-if="condition2"></div>
+      </template>
+      `,
+      options: [{ checkMultiRootNodes: true }],
+      errors: [{ message: 'Set "inheritAttrs" to false.' }]
+    },
+    // condition group as a single root node
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div v-if="condition1" v-bind="$attrs"></div>
+        <div v-else-if="condition2"></div>
+        <div v-else></div>
+      </template>
+      `,
+      errors: [{ message: 'Set "inheritAttrs" to false.' }]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div v-if="condition1" v-bind="$attrs"></div>
+        <div v-else-if="condition2"></div>
+        <div v-else-if="condition3"></div>
+        <div v-else></div>
+      </template>
+      `,
+      errors: [{ message: 'Set "inheritAttrs" to false.' }]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div v-if="condition1" v-bind="$attrs"></div>
+        <div v-else></div>
+      </template>
+      `,
+      errors: [{ message: 'Set "inheritAttrs" to false.' }]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div v-if="condition1" v-bind="$attrs"></div>
+      </template>
+      `,
+      errors: [{ message: 'Set "inheritAttrs" to false.' }]
     }
   ]
 })
diff --git a/tests/lib/rules/no-empty-component-block.js b/tests/lib/rules/no-empty-component-block.js
index e54fd9df..0a014099 100644
--- a/tests/lib/rules/no-empty-component-block.js
+++ b/tests/lib/rules/no-empty-component-block.js
@@ -27,6 +27,7 @@ tester.run('no-empty-component-block', rule, {
   invalid: [
     {
       code: `<template></template>`,
+      output: '',
       errors: [
         {
           message: '`<template>` is empty. Empty block is not allowed.'
@@ -35,6 +36,7 @@ tester.run('no-empty-component-block', rule, {
     },
     {
       code: `<template> </template>`,
+      output: '',
       errors: [
         {
           message: '`<template>` is empty. Empty block is not allowed.'
@@ -44,6 +46,7 @@ tester.run('no-empty-component-block', rule, {
     {
       code: `<template>
 </template>`,
+      output: '',
       errors: [
         {
           message: '`<template>` is empty. Empty block is not allowed.'
@@ -52,6 +55,7 @@ tester.run('no-empty-component-block', rule, {
     },
     {
       code: '<template />',
+      output: '',
       errors: [
         {
           message: '`<template>` is empty. Empty block is not allowed.'
@@ -60,6 +64,7 @@ tester.run('no-empty-component-block', rule, {
     },
     {
       code: '<template src="" />',
+      output: '',
       errors: [
         {
           message: '`<template>` is empty. Empty block is not allowed.'
@@ -68,6 +73,7 @@ tester.run('no-empty-component-block', rule, {
     },
     {
       code: '<template></template><script></script>',
+      output: '<script></script>',
       errors: [
         {
           message: '`<template>` is empty. Empty block is not allowed.'
@@ -79,6 +85,7 @@ tester.run('no-empty-component-block', rule, {
     },
     {
       code: '<template /><script />',
+      output: '<script />',
       errors: [
         {
           message: '`<template>` is empty. Empty block is not allowed.'
@@ -90,6 +97,7 @@ tester.run('no-empty-component-block', rule, {
     },
     {
       code: '<template src="" /><script src="" />',
+      output: '<script src="" />',
       errors: [
         {
           message: '`<template>` is empty. Empty block is not allowed.'
@@ -101,6 +109,7 @@ tester.run('no-empty-component-block', rule, {
     },
     {
       code: '<template></template><script></script><style></style>',
+      output: '<script></script>',
       errors: [
         {
           message: '`<template>` is empty. Empty block is not allowed.'
@@ -115,6 +124,7 @@ tester.run('no-empty-component-block', rule, {
     },
     {
       code: '<template /><script /><style />',
+      output: '<script />',
       errors: [
         {
           message: '`<template>` is empty. Empty block is not allowed.'
@@ -129,6 +139,7 @@ tester.run('no-empty-component-block', rule, {
     },
     {
       code: '<template src="" /><script src="" /><style src="" />',
+      output: '<script src="" />',
       errors: [
         {
           message: '`<template>` is empty. Empty block is not allowed.'
@@ -140,6 +151,64 @@ tester.run('no-empty-component-block', rule, {
           message: '`<style>` is empty. Empty block is not allowed.'
         }
       ]
+    },
+    // auto fix with whitespace
+    {
+      code: '<template></template> <script></script> <style></style>',
+      output: '  ',
+      errors: [
+        {
+          message: '`<template>` is empty. Empty block is not allowed.'
+        },
+        {
+          message: '`<script>` is empty. Empty block is not allowed.'
+        },
+        {
+          message: '`<style>` is empty. Empty block is not allowed.'
+        }
+      ]
+    },
+    {
+      code: '<template /> <script /> <style />',
+      output: '  ',
+      errors: [
+        {
+          message: '`<template>` is empty. Empty block is not allowed.'
+        },
+        {
+          message: '`<script>` is empty. Empty block is not allowed.'
+        },
+        {
+          message: '`<style>` is empty. Empty block is not allowed.'
+        }
+      ]
+    },
+    {
+      code: '<template src="" /> <script src="" /> <style src="" />',
+      output: '  ',
+      errors: [
+        {
+          message: '`<template>` is empty. Empty block is not allowed.'
+        },
+        {
+          message: '`<script>` is empty. Empty block is not allowed.'
+        },
+        {
+          message: '`<style>` is empty. Empty block is not allowed.'
+        }
+      ]
+    },
+    {
+      code: '<template><p></p></template> <script src="" /> <style src="" />',
+      output: '<template><p></p></template>  ',
+      errors: [
+        {
+          message: '`<script>` is empty. Empty block is not allowed.'
+        },
+        {
+          message: '`<style>` is empty. Empty block is not allowed.'
+        }
+      ]
     }
   ]
 })
diff --git a/tests/lib/rules/no-reserved-component-names.js b/tests/lib/rules/no-reserved-component-names.js
index 41cddcfd..231e1f98 100644
--- a/tests/lib/rules/no-reserved-component-names.js
+++ b/tests/lib/rules/no-reserved-component-names.js
@@ -410,6 +410,16 @@ const invalidElements = [
   'xmp',
   'Xmp'
 ]
+const invalidLowerCaseElements = []
+const invalidUpperCaseElements = []
+
+for (const element of invalidElements) {
+  if (element[0] === element[0].toLowerCase()) {
+    invalidLowerCaseElements.push(element)
+  } else {
+    invalidUpperCaseElements.push(element)
+  }
+}
 
 const vue2BuiltInComponents = [
   'component',
@@ -559,6 +569,16 @@ ruleTester.run('no-reserved-component-names', rule, {
       languageOptions,
       options: [{ disallowVueBuiltInComponents: true }]
     })),
+    ...invalidUpperCaseElements.map((name) => ({
+      filename: `${name}.vue`,
+      code: `
+          export default {
+            name: '${name}'
+          }
+        `,
+      languageOptions,
+      options: [{ htmlElementCaseSensitive: true }]
+    })),
     {
       filename: 'test.vue',
       code: `<script setup> defineOptions({}) </script>`,
@@ -701,6 +721,24 @@ ruleTester.run('no-reserved-component-names', rule, {
         }
       ]
     })),
+    ...invalidLowerCaseElements.map((name) => ({
+      filename: `${name}.vue`,
+      code: `<script setup> defineOptions({name: '${name}'}) </script>`,
+      languageOptions: {
+        parser: require('vue-eslint-parser'),
+        ...languageOptions
+      },
+      options: [{ htmlElementCaseSensitive: true }],
+      errors: [
+        {
+          messageId: RESERVED_NAMES_IN_HTML.has(name)
+            ? 'reservedInHtml'
+            : 'reserved',
+          data: { name },
+          line: 1
+        }
+      ]
+    })),
     ...vue2BuiltInComponents.map((name) => ({
       filename: `${name}.vue`,
       code: `
diff --git a/tests/lib/rules/prefer-use-template-ref.js b/tests/lib/rules/prefer-use-template-ref.js
new file mode 100644
index 00000000..fe1ee2d2
--- /dev/null
+++ b/tests/lib/rules/prefer-use-template-ref.js
@@ -0,0 +1,363 @@
+/**
+ * @author Thomasan1999
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+const RuleTester = require('../../eslint-compat').RuleTester
+const rule = require('../../../lib/rules/prefer-use-template-ref')
+
+const tester = new RuleTester({
+  languageOptions: {
+    parser: require('vue-eslint-parser'),
+    ecmaVersion: 2020,
+    sourceType: 'module'
+  }
+})
+
+tester.run('prefer-use-template-ref', rule, {
+  valid: [
+    {
+      filename: 'single-use-template-ref.vue',
+      code: `
+      <template>
+          <div ref="root" />
+      </template>
+      <script setup>
+        import { useTemplateRef } from 'vue';
+        const root = useTemplateRef('root');
+      </script>
+      `
+    },
+    {
+      filename: 'multiple-use-template-refs.vue',
+      code: `
+      <template>
+          <button ref="button">Content</button>
+          <a href="" ref="link">Link</a>
+      </template>
+      <script setup>
+        import { useTemplateRef } from 'vue';
+        const buttonRef = useTemplateRef('button');
+        const link = useTemplateRef('link');
+      </script>
+      `
+    },
+    {
+      filename: 'use-template-ref-in-block.vue',
+      code: `
+      <template>
+          <div>
+            <ul>
+              <li ref="firstListItem">Morning</li>
+              <li>Afternoon</li>
+              <li>Evening</li>
+            </ul>
+          </div>
+      </template>
+      <script setup>
+        import { useTemplateRef } from 'vue';
+        function getFirstListItemElement() {
+          const firstListItem = useTemplateRef('firstListItem')
+          console.log(firstListItem)
+        }
+      </script>
+      `
+    },
+    {
+      filename: 'non-template-ref.vue',
+      code: `
+      <template>
+          <div>
+            <ul>
+              <li v-for="food in foods" :key="food">{{food}}</li>
+            </ul>
+          </div>
+      </template>
+      <script setup>
+        import { ref } from 'vue';
+        const foods = ref(['Spaghetti', 'Pizza', 'Cake']);
+      </script>
+      `
+    },
+    {
+      filename: 'counter.js',
+      code: `
+      import { ref } from 'vue';
+      const counter = ref(0);
+      const names = ref(new Set());
+      function incrementCounter() {
+        counter.value++;
+        return counter.value;
+      }
+      function storeName(name) {
+        names.value.add(name)
+      }
+      `
+    },
+    {
+      filename: 'setup-function.vue',
+      code: `
+      <template>
+        <p>Button clicked {{counter}} times.</p>
+        <button ref="button" @click="counter++">Click</button>
+      </template>
+      <script>
+        import { ref, useTemplateRef } from 'vue';
+        export default {
+          name: 'Counter',
+          setup() {
+            const counter = ref(0);
+            const button = useTemplateRef('button');
+          }
+        }
+      </script>
+      `
+    },
+    {
+      filename: 'options-api-no-refs.vue',
+      code: `
+      <template>
+        <label ref="label">
+          Name:
+          <input v-model="name" />
+        </label>
+        <p ref="textRef">{{niceName}}</p>
+        <button
+      </template>
+      <script>
+        import { ref } from 'vue';
+        export default {
+          name: 'NameRow',
+          methods: {
+            someFunction() {
+              return {
+                label: ref(5),
+              }
+            }
+          }
+          data() {
+            return {
+              label: 'label',
+              name: ''
+            }
+          },
+          computed: {
+            niceName() {
+              return 'Nice ' + this.name;
+            }
+          }
+        }
+      </script>
+      `
+    },
+    {
+      filename: 'options-api-mixed.vue',
+      code: `
+      <template>
+        <label ref="labelElem">
+          Name:
+          <input v-model="name" />
+        </label>
+        {{ loremIpsum }}
+      </template>
+      <script>
+        import { ref } from 'vue';
+        export default {
+          name: 'NameRow',
+          props: {
+            defaultLabel: {
+              type: String,
+            },
+          },
+          data() {
+            return {
+              label: ref(this.defaultLabel),
+              labelElem: ref(),
+              name: ''
+            }
+          },
+          computed: {
+            loremIpsum() {
+              return this.name + ' lorem ipsum'
+            }
+          }
+        }
+      </script>
+      `
+    },
+    {
+      filename: 'template-ref-function.vue',
+      code: `
+      <template>
+        <button :ref="ref => button = ref">Content</button>
+      </template>
+      <script setup>
+        import { ref } from 'vue';
+        const button = ref();
+        </script>
+      `
+    }
+  ],
+  invalid: [
+    {
+      filename: 'single-ref.vue',
+      code: `
+      <template>
+          <div ref="root"/>
+      </template>
+      <script setup>
+        import { ref } from 'vue';
+        const root = ref();
+      </script>
+      `,
+      errors: [
+        {
+          messageId: 'preferUseTemplateRef',
+          data: {
+            name: 'ref'
+          },
+          line: 7,
+          column: 22
+        }
+      ]
+    },
+    {
+      filename: 'one-ref-unused-in-script.vue',
+      code: `
+      <template>
+          <button ref="button">Content</button>
+          <a href="" ref="link">Link</a>
+      </template>
+      <script setup>
+        import { ref } from 'vue';
+        const buttonRef = ref();
+        const link = ref();
+      </script>
+      `,
+      errors: [
+        {
+          messageId: 'preferUseTemplateRef',
+          data: {
+            name: 'ref'
+          },
+          line: 9,
+          column: 22
+        }
+      ]
+    },
+    {
+      filename: 'multiple-refs.vue',
+      code: `
+      <template>
+          <h1 ref="heading">Heading</h1>
+          <a href="" ref="link">Link</a>
+      </template>
+      <script setup>
+        import { ref } from 'vue';
+        const heading = ref();
+        const link = ref();
+      </script>
+      `,
+      errors: [
+        {
+          messageId: 'preferUseTemplateRef',
+          data: {
+            name: 'ref'
+          },
+          line: 8,
+          column: 25
+        },
+        {
+          messageId: 'preferUseTemplateRef',
+          data: {
+            name: 'ref'
+          },
+          line: 9,
+          column: 22
+        }
+      ]
+    },
+    {
+      filename: 'ref-in-block.vue',
+      code: `
+      <template>
+          <div>
+            <ul>
+              <li ref="firstListItem">Morning</li>
+              <li>Afternoon</li>
+              <li>Evening</li>
+            </ul>
+          </div>
+      </template>
+      <script setup>
+        import { ref } from 'vue';
+        function getFirstListItemElement() {
+          const firstListItem = ref();
+        }
+      </script>
+      `,
+      errors: [
+        {
+          messageId: 'preferUseTemplateRef',
+          data: {
+            name: 'ref'
+          },
+          line: 14,
+          column: 33
+        }
+      ]
+    },
+    {
+      filename: 'setup-function-only-refs.vue',
+      code: `
+      <template>
+        <p>Button clicked {{counter}} times.</p>
+        <button ref="button">Click</button>
+      </template>
+      <script>
+        import { ref } from 'vue';
+        export default {
+          name: 'Counter',
+          setup() {
+            const counter = ref(0);
+            const button = ref();
+          }
+        }
+      </script>
+      `,
+      errors: [
+        {
+          messageId: 'preferUseTemplateRef',
+          data: {
+            name: 'ref'
+          },
+          line: 12,
+          column: 28
+        }
+      ]
+    },
+    {
+      filename: 'single-shallowRef.vue',
+      code: `
+      <template>
+          <div ref="root"/>
+      </template>
+      <script setup>
+        import { shallowRef } from 'vue';
+        const root = shallowRef();
+      </script>
+      `,
+      errors: [
+        {
+          messageId: 'preferUseTemplateRef',
+          data: {
+            name: 'shallowRef'
+          },
+          line: 7,
+          column: 22
+        }
+      ]
+    }
+  ]
+})
diff --git a/tests/lib/rules/require-explicit-slots.js b/tests/lib/rules/require-explicit-slots.js
index ebbc2881..92d1a133 100644
--- a/tests/lib/rules/require-explicit-slots.js
+++ b/tests/lib/rules/require-explicit-slots.js
@@ -160,6 +160,37 @@ tester.run('require-explicit-slots', rule, {
           parser: null
         }
       }
+    },
+    // attribute binding
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div>
+          <slot :name="'foo'"></slot>
+          <slot :name="\`bar\`"></slot>
+        </div>
+      </template>
+      <script setup lang="ts">
+      defineSlots<{
+        foo(props: { msg: string }): any
+        bar(props: { msg: string }): any
+      }>()
+      </script>`
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div>
+          <slot :name="bar"></slot>
+        </div>
+      </template>
+      <script setup lang="ts">
+      defineSlots<{
+        default(props: { msg: string }): any
+      }>()
+      </script>`
     }
   ],
   invalid: [
@@ -291,6 +322,26 @@ tester.run('require-explicit-slots', rule, {
         }
       ]
     },
+    {
+      // ignore attribute binding except string literal
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div>
+          <slot :name="'foo'"></slot>
+        </div>
+      </template>
+      <script setup lang="ts">
+      defineSlots<{
+        default(props: { msg: string }): any
+      }>()
+      </script>`,
+      errors: [
+        {
+          message: 'Slots must be explicitly defined.'
+        }
+      ]
+    },
     {
       filename: 'test.vue',
       code: `