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

Issue:
Exclude some tags from vue/v-on-event-hyphenation and vue/attribute-hyphenation
**What rule do you want to change?**
vue/v-on-event-hyphenation and vue/attribute-hyphenation

**Does this change cause the rule to produce more or fewer warnings?**
The same

**How will the change be implemented? (New option, new default behavior, etc.)?**
New option to exclude some tags from applying rules.
For example
```
{
  rules: {
    "vue/v-on-event-hyphenation": ["always", {
      exclude: ["custom-element-tag", /custom-element-.+/]
    }]
  }
}
```


**Please provide some example code that this change will affect:**

<!-- Put your code examples here -->
```vue
<template>
  <!-- error -->
  <vue-component fooBar="baz"></button>
  <!-- ok -->
  <vue-component foo-bar="baz"></button>
  <custom-element-tag barFoo="foo" @fooBar="baz"></custom-element-tag>
</template>
```

**What does the rule currently do for this code?**

**What will the rule do after it's changed?**

**Additional context**
I'm working on a project that includes some custom elements that don't follow kebab case rule, so when applying the rule these elements are broken. 
This would help keep kebab-case in the vue component tags and camelCase in the custom elements

Not to be confused with [ignore](https://eslint.vuejs.org/rules/v-on-event-hyphenation#never-ignore-custom-event), which ignores attributes, not tags.


Pull request:
feat: add `ignoreTags` option
resolve #2605 

Add an `exclude` option to exclude specified tag names in the `vue/attribute-hyphenation` and `vue/v-on-event-hyphenation` rules.

Patch:
diff --git a/docs/rules/attribute-hyphenation.md b/docs/rules/attribute-hyphenation.md
index d5fba2e3..89442fce 100644
--- a/docs/rules/attribute-hyphenation.md
+++ b/docs/rules/attribute-hyphenation.md
@@ -36,7 +36,8 @@ This rule enforces using hyphenated attribute names on custom components in Vue
 ```json
 {
   "vue/attribute-hyphenation": ["error", "always" | "never", {
-    "ignore": []
+    "ignore": [],
+    "ignoreTags": []
   }]
 }
 ```
@@ -44,9 +45,10 @@ This rule enforces using hyphenated attribute names on custom components in Vue
 Default casing is set to `always`. By default the following attributes are ignored: `data-`, `aria-`, `slot-scope`,
 and all the [SVG attributes](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute) with either an upper case letter or an hyphen.
 
-- `"always"` (default) ... Use hyphenated name.
-- `"never"` ... Don't use hyphenated name except the ones that are ignored.
-- `"ignore"` ... Array of ignored names
+- `"always"` (default) ... Use hyphenated attribute name.
+- `"never"` ... Don't use hyphenated attribute name.
+- `"ignore"` ... Array of attribute names that don't need to follow the specified casing.
+- `"ignoreTags"` ... Array of tag names whose attributes don't need to follow the specified casing.
 
 ### `"always"`
 
@@ -109,6 +111,22 @@ Don't use hyphenated name but allow custom attributes
 
 </eslint-code-block>
 
+### `"never", { "ignoreTags": ["/^custom-/"] }`
+
+<eslint-code-block fix :rules="{'vue/attribute-hyphenation': ['error', 'never', { ignoreTags: ['/^custom-/'] }]}">
+
+```vue
+<template>
+  <!-- ✓ GOOD -->
+  <custom-component my-prop="prop" />
+
+  <!-- ✗ BAD -->
+  <my-component my-prop="prop" />
+</template>
+```
+
+</eslint-code-block>
+
 ## :couple: Related Rules
 
 - [vue/v-on-event-hyphenation](./v-on-event-hyphenation.md)
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/docs/rules/v-on-event-hyphenation.md b/docs/rules/v-on-event-hyphenation.md
index 811b3743..493a9dac 100644
--- a/docs/rules/v-on-event-hyphenation.md
+++ b/docs/rules/v-on-event-hyphenation.md
@@ -39,14 +39,16 @@ This rule enforces using hyphenated v-on event names on custom components in Vue
 {
   "vue/v-on-event-hyphenation": ["error", "always" | "never", {
     "autofix": false,
-    "ignore": []
+    "ignore": [],
+    "ignoreTags": []
   }]
 }
 ```
 
-- `"always"` (default) ... Use hyphenated name.
-- `"never"` ... Don't use hyphenated name.
-- `"ignore"` ... Array of ignored names
+- `"always"` (default) ... Use hyphenated event name.
+- `"never"` ... Don't use hyphenated event name.
+- `"ignore"` ... Array of event names that don't need to follow the specified casing.
+- `"ignoreTags"` ... Array of tag names whose events don't need to follow the specified casing.
 - `"autofix"` ... If `true`, enable autofix. If you are using Vue 2, we recommend that you do not use it due to its side effects.
 
 ### `"always"`
@@ -104,6 +106,22 @@ Don't use hyphenated name but allow custom event names
 
 </eslint-code-block>
 
+### `"never", { "ignoreTags": ["/^custom-/"] }`
+
+<eslint-code-block fix :rules="{'vue/v-on-event-hyphenation': ['error', 'never', { ignoreTags: ['/^custom-/'], autofix: true }]}">
+
+```vue
+<template>
+  <!-- ✓ GOOD -->
+  <custom-component v-on:my-event="handleEvent" />
+
+  <!-- ✗ BAD -->
+  <my-component v-on:my-event="handleEvent" />
+</template>
+```
+
+</eslint-code-block>
+
 ## :couple: Related Rules
 
 - [vue/custom-event-name-casing](./custom-event-name-casing.md)
diff --git a/lib/rules/attribute-hyphenation.js b/lib/rules/attribute-hyphenation.js
index 35519d23..65d096cd 100644
--- a/lib/rules/attribute-hyphenation.js
+++ b/lib/rules/attribute-hyphenation.js
@@ -6,6 +6,7 @@
 
 const utils = require('../utils')
 const casing = require('../utils/casing')
+const { toRegExp } = require('../utils/regexp')
 const svgAttributes = require('../utils/svg-attributes-weird-case.json')
 
 /**
@@ -56,6 +57,12 @@ module.exports = {
             },
             uniqueItems: true,
             additionalItems: false
+          },
+          ignoreTags: {
+            type: 'array',
+            items: { type: 'string' },
+            uniqueItems: true,
+            additionalItems: false
           }
         },
         additionalProperties: false
@@ -72,6 +79,11 @@ module.exports = {
     const option = context.options[0]
     const optionsPayload = context.options[1]
     const useHyphenated = option !== 'never'
+    /** @type {RegExp[]} */
+    const ignoredTagsRegexps = (
+      (optionsPayload && optionsPayload.ignoreTags) ||
+      []
+    ).map(toRegExp)
     const ignoredAttributes = ['data-', 'aria-', 'slot-scope', ...svgAttributes]
 
     if (optionsPayload && optionsPayload.ignore) {
@@ -130,11 +142,17 @@ module.exports = {
       return useHyphenated ? value.toLowerCase() === value : !/-/.test(value)
     }
 
+    /** @param {string} name */
+    function isIgnoredTagName(name) {
+      return ignoredTagsRegexps.some((re) => re.test(name))
+    }
+
     return utils.defineTemplateBodyVisitor(context, {
       VAttribute(node) {
+        const element = node.parent.parent
         if (
-          !utils.isCustomComponent(node.parent.parent) &&
-          node.parent.parent.name !== 'slot'
+          (!utils.isCustomComponent(element) && element.name !== 'slot') ||
+          isIgnoredTagName(element.rawName)
         )
           return
 
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/rules/v-on-event-hyphenation.js b/lib/rules/v-on-event-hyphenation.js
index f99a45fd..c9fac76e 100644
--- a/lib/rules/v-on-event-hyphenation.js
+++ b/lib/rules/v-on-event-hyphenation.js
@@ -2,6 +2,7 @@
 
 const utils = require('../utils')
 const casing = require('../utils/casing')
+const { toRegExp } = require('../utils/regexp')
 
 module.exports = {
   meta: {
@@ -35,6 +36,12 @@ module.exports = {
             },
             uniqueItems: true,
             additionalItems: false
+          },
+          ignoreTags: {
+            type: 'array',
+            items: { type: 'string' },
+            uniqueItems: true,
+            additionalItems: false
           }
         },
         additionalProperties: false
@@ -56,6 +63,11 @@ module.exports = {
     const useHyphenated = option !== 'never'
     /** @type {string[]} */
     const ignoredAttributes = (optionsPayload && optionsPayload.ignore) || []
+    /** @type {RegExp[]} */
+    const ignoredTagsRegexps = (
+      (optionsPayload && optionsPayload.ignoreTags) ||
+      []
+    ).map(toRegExp)
     const autofix = Boolean(optionsPayload && optionsPayload.autofix)
 
     const caseConverter = casing.getConverter(
@@ -99,9 +111,20 @@ module.exports = {
       return useHyphenated ? value.toLowerCase() === value : !/-/.test(value)
     }
 
+    /** @param {string} name */
+    function isIgnoredTagName(name) {
+      return ignoredTagsRegexps.some((re) => re.test(name))
+    }
+
     return utils.defineTemplateBodyVisitor(context, {
       "VAttribute[directive=true][key.name.name='on']"(node) {
-        if (!utils.isCustomComponent(node.parent.parent)) return
+        const element = node.parent.parent
+        if (
+          !utils.isCustomComponent(element) ||
+          isIgnoredTagName(element.rawName)
+        ) {
+          return
+        }
         if (!node.key.argument || node.key.argument.type !== 'VIdentifier') {
           return
         }
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/attribute-hyphenation.js b/tests/lib/rules/attribute-hyphenation.js
index 18d60e19..738d59ae 100644
--- a/tests/lib/rules/attribute-hyphenation.js
+++ b/tests/lib/rules/attribute-hyphenation.js
@@ -85,6 +85,26 @@ ruleTester.run('attribute-hyphenation', rule, {
       filename: 'test.vue',
       code: '<template><div><custom :myName.sync="prop"></custom></div></template>',
       options: ['never']
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <VueComponent my-prop></VueComponent>
+        <custom-component my-prop></custom-component>
+      </template>
+      `,
+      options: ['never', { ignoreTags: ['VueComponent', '/^custom-/'] }]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <VueComponent myProp="prop"></VueComponent>
+        <custom-component myProp="prop"></custom-component>
+      </template>
+      `,
+      options: ['always', { ignoreTags: ['VueComponent', '/^custom-/'] }]
     }
   ],
 
@@ -450,6 +470,52 @@ ruleTester.run('attribute-hyphenation', rule, {
           line: 1
         }
       ]
+    },
+    {
+      code: `
+      <template>
+        <custom my-prop/>
+        <CustomComponent my-prop/>
+      </template>
+      `,
+      output: `
+      <template>
+        <custom myProp/>
+        <CustomComponent my-prop/>
+      </template>
+      `,
+      options: ['never', { ignoreTags: ['CustomComponent'] }],
+      errors: [
+        {
+          message: "Attribute 'my-prop' can't be hyphenated.",
+          type: 'VIdentifier',
+          line: 3,
+          column: 17
+        }
+      ]
+    },
+    {
+      code: `
+      <template>
+        <custom myProp/>
+        <CustomComponent myProp/>
+      </template>
+      `,
+      output: `
+      <template>
+        <custom my-prop/>
+        <CustomComponent myProp/>
+      </template>
+      `,
+      options: ['always', { ignoreTags: ['CustomComponent'] }],
+      errors: [
+        {
+          message: "Attribute 'myProp' must be hyphenated.",
+          type: 'VIdentifier',
+          line: 3,
+          column: 17
+        }
+      ]
     }
   ]
 })
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
+        }
+      ]
     }
   ]
 })
diff --git a/tests/lib/rules/v-on-event-hyphenation.js b/tests/lib/rules/v-on-event-hyphenation.js
index 54d2ec43..3f58ce1f 100644
--- a/tests/lib/rules/v-on-event-hyphenation.js
+++ b/tests/lib/rules/v-on-event-hyphenation.js
@@ -44,6 +44,32 @@ tester.run('v-on-event-hyphenation', rule, {
       </template>
       `,
       options: ['never', { ignore: ['custom'] }]
+    },
+    {
+      code: `
+      <template>
+          <VueComponent v-on:custom-event="events"/>
+      </template>
+      `,
+      options: ['never', { ignore: ['custom-event'] }]
+    },
+    {
+      code: `
+      <template>
+          <VueComponent v-on:custom-event="events"/>
+          <custom-component v-on:custom-event="events"/>
+      </template>
+      `,
+      options: ['never', { ignoreTags: ['/^Vue/', 'custom-component'] }]
+    },
+    {
+      code: `
+      <template>
+          <VueComponent v-on:customEvent="events"/>
+          <custom-component v-on:customEvent="events"/>
+      </template>
+      `,
+      options: ['always', { ignoreTags: ['/^Vue/', 'custom-component'] }]
     }
   ],
   invalid: [
@@ -179,6 +205,50 @@ tester.run('v-on-event-hyphenation', rule, {
         "v-on event '@upDate:model-value' can't be hyphenated.",
         "v-on event '@up-date:model-value' can't be hyphenated."
       ]
+    },
+    {
+      code: `
+      <template>
+        <VueComponent v-on:custom-event="events"/>
+        <CustomComponent v-on:custom-event="events"/>
+      </template>
+      `,
+      output: `
+      <template>
+        <VueComponent v-on:customEvent="events"/>
+        <CustomComponent v-on:custom-event="events"/>
+      </template>
+      `,
+      options: ['never', { autofix: true, ignoreTags: ['CustomComponent'] }],
+      errors: [
+        {
+          message: "v-on event 'v-on:custom-event' can't be hyphenated.",
+          line: 3,
+          column: 23
+        }
+      ]
+    },
+    {
+      code: `
+      <template>
+        <VueComponent v-on:customEvent="events"/>
+        <CustomComponent v-on:customEvent="events"/>
+      </template>
+      `,
+      output: `
+      <template>
+        <VueComponent v-on:custom-event="events"/>
+        <CustomComponent v-on:customEvent="events"/>
+      </template>
+      `,
+      options: ['always', { autofix: true, ignoreTags: ['CustomComponent'] }],
+      errors: [
+        {
+          message: "v-on event 'v-on:customEvent' must be hyphenated.",
+          line: 3,
+          column: 23
+        }
+      ]
     }
   ]
 })