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

Issue:
Namespace handling makes it impossible to lint recursive SVG / MathML components
**What rule do you want to change?**

Not exactly a specific rule, but I have been motivated by `vue/no-v-text-v-html-on-component` being falsely triggered on MathML tags. `isCustomComponent()` is being called from 17 rules. The problem lies with the fact that every composition component starts with a top-level namespace of HTML, which makes it impossible to create partial, especially recursive components within another namespace without confusing the linter.

**Does this change cause the rule to produce more or fewer warnings?**

Fewer.

**How will the change be implemented? (New option, new default behavior, etc.)?**

There are multiple options:

1. Stop examining namespaces when determining whether something is an element or a component.
2. Treat the HTML namespace as valid namespace for SVG and MathML tags.
3. Looks for `xmlns` on the top-level `<template/>` and adjust namespace accordingly.
4. Implement (1) or (2), but gate it behind a configuration option.

**Please provide some example code that this change will affect:**

<!-- Put your code examples here -->
```vue
<script setup type="ts">
// This lives inside MathExpression.vue

import { Expression } from './expression.ts'

defineProps<{
  expression: Expression
}>()
</script>

<template>
  <mfrac v-if="expression instanceof Fraction">
    <mrow>
      <MathExpression v-text="expression.numerator" />
    </mrow>
    <mrow>
      <MathExpression v-text="expression.denominator" />
    </mrow>
  </mfrac>
  <!-- more cases omitted for brevity -->
  <mn v-else v-text="expression.value" />
</template>
```

**What does the rule currently do for this code?**

Triggers `vue/no-v-text-v-html-on-component` because it does not see `<mfrac/>` as being inside MathML namespace. Which is impossible to realize, because `<math/>` cannot be nested and is only specified at the top level. This component recurses into the data, rendering the full MathML structure.

**What will the rule do after it's changed?**

In case of (1), (2), (4) it won't consider using `<mfrac/>` in this context as an error.
In case of (3):

```vue
<template xmlns="http://www.w3.org/1998/Math/MathML">
  <mi v-text="'x'" />
</template>
```

It will accept this otherwise invalid, but (probably) harmless syntax to indicate that we are inside MathML namespace and proceed from there on, possibly marking HTML and SVG elements as out of place.

**Additional context**

This obviously applies to SVG as well, although there is not that high probability somebody will be doing anything recursive in there. Math is a different story, though.

Pull request description:
feat(no-v-text-v-html-on-component): add ignore namespace option
resolve #2458

Patch:
diff --git a/docs/rules/index.md b/docs/rules/index.md
index e359f47a..51515e23 100644
--- a/docs/rules/index.md
+++ b/docs/rules/index.md
@@ -270,7 +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` for template refs |  | :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-v-text-v-html-on-component.md b/docs/rules/no-v-text-v-html-on-component.md
index 8e504d85..7d75eb9c 100644
--- a/docs/rules/no-v-text-v-html-on-component.md
+++ b/docs/rules/no-v-text-v-html-on-component.md
@@ -25,11 +25,15 @@ If you use v-text / v-html on a component, it will overwrite the component's con
   <!-- ✓ GOOD -->
   <div v-text="content"></div>
   <div v-html="html"></div>
+  <svg><g v-text="content" /></svg>
+  <math><mi v-text="content" /></math>
   <MyComponent>{{ content }}</MyComponent>
 
   <!-- ✗ BAD -->
   <MyComponent v-text="content"></MyComponent>
   <MyComponent v-html="html"></MyComponent>
+  <g v-text="content" />
+  <mi v-text="content" />
 </template>
 ```
 
@@ -39,14 +43,15 @@ If you use v-text / v-html on a component, it will overwrite the component's con
 
 ```json
 {
-  "vue/no-v-text-v-html-on-component": [
-    "error",
-    { "allow": ["router-link", "nuxt-link"] }
-  ]
+  "vue/no-v-text-v-html-on-component": ["error", {
+    "allow": ["router-link", "nuxt-link"],
+    "ignoreElementNamespaces": false
+  }]
 }
 ```
 
 - `allow` (`string[]`) ... Specify a list of custom components for which the rule should not apply.
+- `ignoreElementNamespaces` (`boolean`) ... If `true`, always treat SVG and MathML tag names as HTML elements, even if they are not used inside a SVG/MathML root element. Default is `false`.
 
 ### `{ "allow": ["router-link", "nuxt-link"] }`
 
@@ -65,6 +70,20 @@ If you use v-text / v-html on a component, it will overwrite the component's con
 
 </eslint-code-block>
 
+### `{ "ignoreElementNamespaces": true }`
+
+<eslint-code-block :rules="{'vue/no-v-text-v-html-on-component': ['error', { ignoreElementNamespaces: true }]}">
+
+```vue
+<template>
+  <!-- ✓ GOOD -->
+  <g v-text="content" /> <!-- SVG element not inside of <svg> -->
+  <mi v-text="content" /> <!-- MathML element not inside of <math> -->
+</template>
+```
+
+</eslint-code-block>
+
 ## :rocket: Version
 
 This rule was introduced in eslint-plugin-vue v8.4.0
diff --git a/docs/rules/prefer-use-template-ref.md b/docs/rules/prefer-use-template-ref.md
index 553e99bf..1b1b4038 100644
--- a/docs/rules/prefer-use-template-ref.md
+++ b/docs/rules/prefer-use-template-ref.md
@@ -2,31 +2,32 @@
 pageClass: rule-details
 sidebarDepth: 0
 title: vue/prefer-use-template-ref
-description: require using `useTemplateRef` instead of `ref` for template refs
+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` for template refs
+> 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` for template refs.
+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, useTemplateRef } from 'vue';
+  import { ref, shallowRef, useTemplateRef } from 'vue';
 
   /* ✓ GOOD */
   const submitRef = useTemplateRef('submitRef');
@@ -35,6 +36,7 @@ This rule enforces using the new `useTemplateRef` function instead of `ref` for
 
   /* ✗ BAD */
   const closeRef = ref();
+  const cancelRef = shallowRef();
 </script>
 ```
 
@@ -47,14 +49,16 @@ This rule skips `ref` template function refs as these should be used to allow cu
 
 ```vue
 <template>
-  <button :ref="ref => button = ref">Content</button>
+  <button :ref="ref => submitRef = ref">Submit</button>
+  <button :ref="ref => cancelRef = ref">Cancel</button>
 </template>
 
 <script setup>
-  import { ref } from 'vue';
+  import { ref, shallowRef } from 'vue';
   
   /* ✓ GOOD */
-  const button = ref();
+  const submitRef = ref();
+  const cancelRef = shallowRef();
 </script>
 ```
 
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-v-text-v-html-on-component.js b/lib/rules/no-v-text-v-html-on-component.js
index 50ef9c76..e3f1f540 100644
--- a/lib/rules/no-v-text-v-html-on-component.js
+++ b/lib/rules/no-v-text-v-html-on-component.js
@@ -26,6 +26,9 @@ module.exports = {
               type: 'string'
             },
             uniqueItems: true
+          },
+          ignoreElementNamespaces: {
+            type: 'boolean'
           }
         },
         additionalProperties: false
@@ -41,6 +44,8 @@ module.exports = {
     const options = context.options[0] || {}
     /** @type {Set<string>} */
     const allow = new Set(options.allow)
+    /** @type {boolean} */
+    const ignoreElementNamespaces = options.ignoreElementNamespaces === true
 
     /**
      * Check whether the given node is an allowed component or not.
@@ -62,7 +67,10 @@ module.exports = {
      */
     function verify(node) {
       const element = node.parent.parent
-      if (utils.isCustomComponent(element) && !isAllowedComponent(element)) {
+      if (
+        utils.isCustomComponent(element, ignoreElementNamespaces) &&
+        !isAllowedComponent(element)
+      ) {
         context.report({
           node,
           loc: node.loc,
diff --git a/lib/rules/prefer-use-template-ref.js b/lib/rules/prefer-use-template-ref.js
index 8dcdccb3..916583e9 100644
--- a/lib/rules/prefer-use-template-ref.js
+++ b/lib/rules/prefer-use-template-ref.js
@@ -8,8 +8,12 @@ const utils = require('../utils')
 
 /** @param expression {Expression | null} */
 function expressionIsRef(expression) {
-  // @ts-ignore
-  return expression?.callee?.name === 'ref'
+  return (
+    // @ts-ignore
+    expression?.callee?.name === 'ref' ||
+    // @ts-ignore
+    expression?.callee?.name === 'shallowRef'
+  )
 }
 
 /** @type {import("eslint").Rule.RuleModule} */
@@ -18,13 +22,13 @@ module.exports = {
     type: 'suggestion',
     docs: {
       description:
-        'require using `useTemplateRef` instead of `ref` for template refs',
+        '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 'ref' with 'useTemplateRef'."
+      preferUseTemplateRef: "Replace '{{name}}' with 'useTemplateRef'."
     }
   },
   /** @param {RuleContext} context */
@@ -79,7 +83,11 @@ module.exports = {
 
             context.report({
               node: scriptRef.node,
-              messageId: 'preferUseTemplateRef'
+              messageId: 'preferUseTemplateRef',
+              data: {
+                // @ts-ignore
+                name: scriptRef.node?.callee?.name
+              }
             })
           }
         }
diff --git a/lib/utils/index.js b/lib/utils/index.js
index 58cd3268..eb84c127 100644
--- a/lib/utils/index.js
+++ b/lib/utils/index.js
@@ -941,19 +941,30 @@ module.exports = {
   /**
    * Check whether the given node is a custom component or not.
    * @param {VElement} node The start tag node to check.
+   * @param {boolean} [ignoreElementNamespaces=false] If `true`, ignore element namespaces.
    * @returns {boolean} `true` if the node is a custom component.
    */
-  isCustomComponent(node) {
-    return (
-      (this.isHtmlElementNode(node) &&
-        !this.isHtmlWellKnownElementName(node.rawName)) ||
-      (this.isSvgElementNode(node) &&
-        !this.isSvgWellKnownElementName(node.rawName)) ||
-      (this.isMathElementNode(node) &&
-        !this.isMathWellKnownElementName(node.rawName)) ||
+  isCustomComponent(node, ignoreElementNamespaces = false) {
+    if (
       hasAttribute(node, 'is') ||
       hasDirective(node, 'bind', 'is') ||
       hasDirective(node, 'is')
+    ) {
+      return true
+    }
+
+    const isHtmlName = this.isHtmlWellKnownElementName(node.rawName)
+    const isSvgName = this.isSvgWellKnownElementName(node.rawName)
+    const isMathName = this.isMathWellKnownElementName(node.rawName)
+
+    if (ignoreElementNamespaces) {
+      return !isHtmlName && !isSvgName && !isMathName
+    }
+
+    return (
+      (this.isHtmlElementNode(node) && !isHtmlName) ||
+      (this.isSvgElementNode(node) && !isSvgName) ||
+      (this.isMathElementNode(node) && !isMathName)
     )
   },
 


Test patch:
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-v-text-v-html-on-component.js b/tests/lib/rules/no-v-text-v-html-on-component.js
index ebf2901b..bb403489 100644
--- a/tests/lib/rules/no-v-text-v-html-on-component.js
+++ b/tests/lib/rules/no-v-text-v-html-on-component.js
@@ -59,6 +59,26 @@ tester.run('no-v-text-v-html-on-component', rule, {
       </template>
       `,
       options: [{ allow: ['RouterLink', 'nuxt-link'] }]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <svg><g v-text="content" /></svg>
+        <math><mspace v-text="content" /></math>
+      </template>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <h1 v-html="content" />
+        <g v-text="content" />
+        <mi v-text="content" />
+      </template>
+      `,
+      options: [{ ignoreElementNamespaces: true }]
     }
   ],
   invalid: [
@@ -167,6 +187,28 @@ tester.run('no-v-text-v-html-on-component', rule, {
           column: 22
         }
       ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <g v-text="content" />
+        <mi v-text="content" />
+      </template>
+      `,
+      options: [{ ignoreElementNamespaces: false }],
+      errors: [
+        {
+          message: "Using v-text on component may break component's content.",
+          line: 3,
+          column: 12
+        },
+        {
+          message: "Using v-text on component may break component's content.",
+          line: 4,
+          column: 13
+        }
+      ]
     }
   ]
 })
diff --git a/tests/lib/rules/prefer-use-template-ref.js b/tests/lib/rules/prefer-use-template-ref.js
index 49a2f075..fe1ee2d2 100644
--- a/tests/lib/rules/prefer-use-template-ref.js
+++ b/tests/lib/rules/prefer-use-template-ref.js
@@ -214,6 +214,9 @@ tester.run('prefer-use-template-ref', rule, {
       errors: [
         {
           messageId: 'preferUseTemplateRef',
+          data: {
+            name: 'ref'
+          },
           line: 7,
           column: 22
         }
@@ -235,6 +238,9 @@ tester.run('prefer-use-template-ref', rule, {
       errors: [
         {
           messageId: 'preferUseTemplateRef',
+          data: {
+            name: 'ref'
+          },
           line: 9,
           column: 22
         }
@@ -256,11 +262,17 @@ tester.run('prefer-use-template-ref', rule, {
       errors: [
         {
           messageId: 'preferUseTemplateRef',
+          data: {
+            name: 'ref'
+          },
           line: 8,
           column: 25
         },
         {
           messageId: 'preferUseTemplateRef',
+          data: {
+            name: 'ref'
+          },
           line: 9,
           column: 22
         }
@@ -288,6 +300,9 @@ tester.run('prefer-use-template-ref', rule, {
       errors: [
         {
           messageId: 'preferUseTemplateRef',
+          data: {
+            name: 'ref'
+          },
           line: 14,
           column: 33
         }
@@ -314,10 +329,35 @@ tester.run('prefer-use-template-ref', rule, {
       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
+        }
+      ]
     }
   ]
 })