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

Issue:
`vue/no-multiple-template-root` false negative with comment
<!--
  ❗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:**  8.50.0
- **eslint-plugin-vue version:** 9.15.1
- **Node version:** 18.13.0
- **Operating System:** macOS

**Please show your full configuration:**
<!-- Paste content of your .eslintrc file -->
```json5
module.exports = {
  extends: ['../../../.eslintrc.js', 'plugin:storybook/recommended'],
  root: true,
  env: {
    node: true,
    browser: true
  },
  overrides: [
    {
      files: ['*.ts', '*.tsx', '*.vue'],
      parser: 'vue-eslint-parser',
      parserOptions: {
        parser: '@typescript-eslint/parser',
        project: './tsconfig.json',
        extraFileExtensions: ['.vue']
      },
      globals: {
        definePageMeta: 'readonly',
        $fetch: 'readonly',
        Stripe: 'readonly'
      },
      rules: {
        /**
         * This has unexpected behavior when used with other TS settings which produce
         * unexpected undefines, such as a map.get after testing for map.has
         */
        '@typescript-eslint/no-non-null-assertion': 'off'
      }
    },
    {
      files: ['**/pages/**/*.vue'],
      rules: {
        /**
         * Even though this is allowed in Vue3,
         * both Nuxt and Knockout need a single
         * root element.
         */
        'vue/no-multiple-template-root': 'error'
      }
    }
  ]
}

```

**What did you do?**	
<!-- Please include the actual source code causing the issue. -->
```vue
<template>
  <!-- this is a comment -->
   <div></div>
</template>
```

**_IMPORTANT CONTEXT:  the Vue compiler has `comments: true` turned on (as a necessity). What this means is that the root template in this mode actually has multiple roots, including a blank text node, a comment node, and the div. I want to prevent developers from making this mistake, so I need a way to flag this as an error._**

**What did you expect to happen?**

I expected an ESLint error when there are multiple possible roots.

**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.
-->
There is no error, hence no output.

**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.
-->
https://stackblitz.com/edit/vitejs-vite-mhdbqk?file=src%2FApp.vue


Pull request:
feat(no-multiple-template-root): support disallowComments property
# Issue

close #2316


Patch:
diff --git a/.github/workflows/check-for-resources-update.yml b/.github/workflows/check-for-resources-update.yml
index 613125fc..31f881b1 100644
--- a/.github/workflows/check-for-resources-update.yml
+++ b/.github/workflows/check-for-resources-update.yml
@@ -5,12 +5,13 @@ on:
     - cron: 0 0 * * 0 # At 00:00 on Sunday, see https://crontab.guru/#0_0_*_*_0
 
 permissions:
-    contents: write
-    pull-requests: write
+  contents: write
+  pull-requests: write
 
 jobs:
   check-for-resources-update:
     runs-on: ubuntu-latest
+    if: ${{ github.repository == 'vuejs/eslint-plugin-vue' }}
     steps:
       - name: Checkout
         uses: actions/checkout@v4
diff --git a/README.md b/README.md
index 95e0c08b..bdeb5e16 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
 
 [![NPM version](https://img.shields.io/npm/v/eslint-plugin-vue.svg?style=flat)](https://npmjs.org/package/eslint-plugin-vue)
 [![NPM downloads](https://img.shields.io/npm/dm/eslint-plugin-vue.svg?style=flat)](https://npmjs.org/package/eslint-plugin-vue)
-[![CircleCI](https://img.shields.io/circleci/project/github/vuejs/eslint-plugin-vue/master.svg?style=flat)](https://circleci.com/gh/vuejs/eslint-plugin-vue)
+[![CI](https://img.shields.io/github/actions/workflow/status/vuejs/eslint-plugin-vue/CI.yml?style=flat&label=CI)](https://github.com/vuejs/eslint-plugin-vue/actions/workflows/CI.yml)
 [![License](https://img.shields.io/github/license/vuejs/eslint-plugin-vue.svg?style=flat)](https://github.com/vuejs/eslint-plugin-vue/blob/master/LICENSE)
 
 > Official ESLint plugin for Vue.js
diff --git a/docs/rules/define-emits-declaration.md b/docs/rules/define-emits-declaration.md
index 7a11524c..c24f5ec6 100644
--- a/docs/rules/define-emits-declaration.md
+++ b/docs/rules/define-emits-declaration.md
@@ -35,8 +35,8 @@ const emit = defineEmits<{
 
 /* ✗ BAD */
 const emit = defineEmits({
-  change: (id) => typeof id == 'number',
-  update: (value) => typeof value == 'string'
+  change: (id) => typeof id === 'number',
+  update: (value) => typeof value === 'string'
 })
 
 /* ✗ BAD */
@@ -70,8 +70,8 @@ const emit = defineEmits<{
 
 /* ✓ GOOD */
 const emit = defineEmits({
-  change: (id) => typeof id == 'number',
-  update: (value) => typeof value == 'string'
+  change: (id) => typeof id === 'number',
+  update: (value) => typeof value === 'string'
 })
 
 /* ✓ GOOD */
@@ -92,8 +92,8 @@ const emit = defineEmits(['change', 'update'])
 
 /* ✗ BAD */
 const emit = defineEmits({
-  change: (id) => typeof id == 'number',
-  update: (value) => typeof value == 'string'
+  change: (id) => typeof id === 'number',
+  update: (value) => typeof value === 'string'
 })
 
 /* ✗ BAD */
diff --git a/docs/rules/define-props-destructuring.md b/docs/rules/define-props-destructuring.md
new file mode 100644
index 00000000..7f3bc784
--- /dev/null
+++ b/docs/rules/define-props-destructuring.md
@@ -0,0 +1,95 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/define-props-destructuring
+description: enforce consistent style for props destructuring
+---
+
+# vue/define-props-destructuring
+
+> enforce consistent style for props destructuring
+
+- :exclamation: <badge text="This rule has not been released yet." vertical="middle" type="error"> _**This rule has not been released yet.**_ </badge>
+
+## :book: Rule Details
+
+This rule enforces a consistent style for handling Vue 3 Composition API props, allowing you to choose between requiring destructuring or prohibiting it.
+
+By default, the rule requires you to use destructuring syntax when using `defineProps` instead of storing props in a variable and warns against combining `withDefaults` with destructuring.
+
+<eslint-code-block :rules="{'vue/define-props-destructuring': ['error']}">
+
+```vue
+<script setup>
+  // ✓ GOOD
+  const { foo } = defineProps(['foo'])
+  const { bar = 'default' } = defineProps(['bar'])
+
+  // ✗ BAD
+  const props = defineProps(['foo'])
+  const propsWithDefaults = withDefaults(defineProps(['foo']), { foo: 'default' })
+
+  // ✗ BAD
+  const { baz } = withDefaults(defineProps(['baz']), { baz: 'default' })
+</script>
+```
+
+</eslint-code-block>
+
+The rule applies to both JavaScript and TypeScript props:
+
+<eslint-code-block :rules="{'vue/define-props-destructuring': ['error']}">
+
+```vue
+<script setup lang="ts">
+  // ✓ GOOD
+  const { foo } = defineProps<{ foo?: string }>()
+  const { bar = 'default' } = defineProps<{ bar?: string }>()
+
+  // ✗ BAD
+  const props = defineProps<{ foo?: string }>()
+  const propsWithDefaults = withDefaults(defineProps<{ foo?: string }>(), { foo: 'default' })
+</script>
+```
+
+</eslint-code-block>
+
+## :wrench: Options
+
+```js
+{
+  "vue/define-props-destructuring": ["error", {
+    "destructure": "always" | "never"
+  }]
+}
+```
+
+- `destructure` - Sets the destructuring preference for props
+  - `"always"` (default) - Requires destructuring when using `defineProps` and warns against using `withDefaults` with destructuring
+  - `"never"` - Requires using a variable to store props and prohibits destructuring
+
+### `"destructure": "never"`
+
+<eslint-code-block :rules="{'vue/define-props-destructuring': ['error', { destructure: 'never' }]}">
+
+```vue
+<script setup>
+  // ✓ GOOD
+  const props = defineProps(['foo'])
+  const propsWithDefaults = withDefaults(defineProps(['foo']), { foo: 'default' })
+
+  // ✗ BAD
+  const { foo } = defineProps(['foo'])
+</script>
+```
+
+</eslint-code-block>
+
+## :books: Further Reading
+
+- [Reactive Props Destructure](https://vuejs.org/guide/components/props.html#reactive-props-destructure)
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/define-props-destructuring.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/define-props-destructuring.js)
diff --git a/docs/rules/index.md b/docs/rules/index.md
index 55c5b96c..7828b58b 100644
--- a/docs/rules/index.md
+++ b/docs/rules/index.md
@@ -218,6 +218,7 @@ For example:
 | [vue/define-emits-declaration] | enforce declaration style of `defineEmits` |  | :hammer: |
 | [vue/define-macros-order] | enforce order of compiler macros (`defineProps`, `defineEmits`, etc.) | :wrench::bulb: | :lipstick: |
 | [vue/define-props-declaration] | enforce declaration style of `defineProps` |  | :hammer: |
+| [vue/define-props-destructuring] | enforce consistent style for props destructuring |  | :hammer: |
 | [vue/enforce-style-attribute] | enforce or forbid the use of the `scoped` and `module` attributes in SFC top level style tags |  | :hammer: |
 | [vue/html-button-has-type] | disallow usage of button without an explicit type attribute |  | :hammer: |
 | [vue/html-comment-content-newline] | enforce unified line break in HTML comments | :wrench: | :lipstick: |
@@ -398,6 +399,7 @@ The following rules extend the rules provided by ESLint itself and apply them to
 [vue/define-emits-declaration]: ./define-emits-declaration.md
 [vue/define-macros-order]: ./define-macros-order.md
 [vue/define-props-declaration]: ./define-props-declaration.md
+[vue/define-props-destructuring]: ./define-props-destructuring.md
 [vue/dot-location]: ./dot-location.md
 [vue/dot-notation]: ./dot-notation.md
 [vue/enforce-style-attribute]: ./enforce-style-attribute.md
diff --git a/docs/rules/no-bare-strings-in-template.md b/docs/rules/no-bare-strings-in-template.md
index df1fae12..23a23c11 100644
--- a/docs/rules/no-bare-strings-in-template.md
+++ b/docs/rules/no-bare-strings-in-template.md
@@ -12,7 +12,7 @@ since: v7.0.0
 
 ## :book: Rule Details
 
-This rule disallows the use of bare strings in `<template>`.  
+This rule disallows the use of bare strings in `<template>`.
 In order to be able to internationalize your application, you will need to avoid using plain strings in your templates. Instead, you would need to use a template helper specializing in translation.
 
 This rule was inspired by [no-bare-strings rule in ember-template-lint](https://github.com/ember-template-lint/ember-template-lint/blob/master/docs/rule/no-bare-strings.md).
@@ -50,7 +50,7 @@ This rule was inspired by [no-bare-strings rule in ember-template-lint](https://
 </eslint-code-block>
 
 :::tip
-This rule does not check for string literals, in bindings and mustaches interpolation. This is because it looks like a conscious decision.  
+This rule does not check for string literals, in bindings and mustaches interpolation. This is because it looks like a conscious decision.
 If you want to report these string literals, enable the [vue/no-useless-v-bind] and [vue/no-useless-mustaches] rules and fix the useless string literals.
 :::
 
@@ -72,7 +72,7 @@ If you want to report these string literals, enable the [vue/no-useless-v-bind]
 }
 ```
 
-- `allowlist` ... An array of allowed strings.
+- `allowlist` ... An array of allowed strings or regular expression patterns (e.g. `/\d+/` to allow numbers).
 - `attributes` ... An object whose keys are tag name or patterns and value is an array of attributes to check for that tag name.
 - `directives` ... An array of directive names to check literal value.
 
diff --git a/docs/rules/no-multiple-template-root.md b/docs/rules/no-multiple-template-root.md
index 90b5488f..8a85c920 100644
--- a/docs/rules/no-multiple-template-root.md
+++ b/docs/rules/no-multiple-template-root.md
@@ -61,7 +61,32 @@ This rule checks whether template contains single root element valid for Vue 2.
 
 ## :wrench: Options
 
-Nothing.
+```json
+{
+  "vue/no-multiple-template-root": ["error", {
+    "disallowComments": false
+  }]
+}
+```
+
+- "disallowComments" (`boolean`) Enables there should not be any comments in the template root. Default is `false`.
+
+### "disallowComments": true
+
+<eslint-code-block :rules="{'vue/no-multiple-template-root': ['error', {disallowComments: true}]}">
+
+```vue
+/* ✗ BAD */
+<template>
+  <!-- root comment -->
+  <div>
+    vue eslint plugin
+  </div>
+  <!-- root comment -->
+</template>
+```
+
+</eslint-code-block>
 
 ## :rocket: Version
 
diff --git a/docs/user-guide/index.md b/docs/user-guide/index.md
index 30432d51..103471f6 100644
--- a/docs/user-guide/index.md
+++ b/docs/user-guide/index.md
@@ -166,8 +166,8 @@ module.exports = {
   extends: [
     // add more generic rulesets here, such as:
     // 'eslint:recommended',
-    'plugin:vue/vue3-recommended',
-    // 'plugin:vue/recommended' // Use this if you are using Vue.js 2.x.
+    'plugin:vue/recommended',
+    // 'plugin:vue/vue2-recommended' // Use this if you are using Vue.js 2.x.
   ],
   rules: {
     // override/add rules settings here, such as:
@@ -185,13 +185,13 @@ You can use the following configs by adding them to `extends`.
 
 - `"plugin:vue/base"` ... Settings and rules to enable correct ESLint parsing.
 - Configurations for using Vue.js 3.x:
-  - `"plugin:vue/vue3-essential"` ... `base`, plus rules to prevent errors or unintended behavior.
-  - `"plugin:vue/vue3-strongly-recommended"` ... Above, plus rules to considerably improve code readability and/or dev experience.
-  - `"plugin:vue/vue3-recommended"` ... Above, plus rules to enforce subjective community defaults to ensure consistency.
-- Configurations for using Vue.js 2.x:
   - `"plugin:vue/essential"` ... `base`, plus rules to prevent errors or unintended behavior.
   - `"plugin:vue/strongly-recommended"` ... Above, plus rules to considerably improve code readability and/or dev experience.
-  - `"plugin:vue/recommended"` ... Above, plus rules to enforce subjective community defaults to ensure consistency
+  - `"plugin:vue/recommended"` ... Above, plus rules to enforce subjective community defaults to ensure consistency.
+- Configurations for using Vue.js 2.x:
+  - `"plugin:vue/vue2-essential"` ... `base`, plus rules to prevent errors or unintended behavior.
+  - `"plugin:vue/vue2-strongly-recommended"` ... Above, plus rules to considerably improve code readability and/or dev experience.
+  - `"plugin:vue/vue2-recommended"` ... Above, plus rules to enforce subjective community defaults to ensure consistency.
 
 :::warning Reporting rules
 By default, all rules from **base** and **essential** categories report ESLint errors. Other rules - because they're not covering potential bugs in the application - report warnings. What does it mean? By default - nothing, but if you want - you can set up a threshold and break the build after a certain amount of warnings, instead of any. More information [here](https://eslint.org/docs/user-guide/command-line-interface#handling-warnings).
@@ -264,7 +264,7 @@ Full example:
   "extends": [
     "eslint:recommended",
     "plugin:@typescript-eslint/recommended",
-    "plugin:vue/vue3-recommended"
+    "plugin:vue/recommended"
   ],
   "parser": "vue-eslint-parser",
   "parserOptions": {
@@ -436,7 +436,7 @@ Most `eslint-plugin-vue` rules require `vue-eslint-parser` to check `<template>`
 
 Make sure you have one of the following settings in your **.eslintrc**:
 
-- `"extends": ["plugin:vue/vue3-recommended"]`
+- `"extends": ["plugin:vue/recommended"]`
 - `"extends": ["plugin:vue/base"]`
 
 If you already use another parser (e.g. `"parser": "@typescript-eslint/parser"`), please move it into `parserOptions`, so it doesn't collide with the `vue-eslint-parser` used by this plugin's configuration:
@@ -482,7 +482,7 @@ module.exports = {
     // ...
     // 'eslint:recommended',
     // ...
-    'plugin:vue/vue3-recommended',
+    'plugin:vue/recommended',
     // ...
     'prettier'
     // Make sure "prettier" is the last element in this list.
diff --git a/lib/index.js b/lib/index.js
index 834e5f28..e511536f 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -58,6 +58,7 @@ const plugin = {
     'define-emits-declaration': require('./rules/define-emits-declaration'),
     'define-macros-order': require('./rules/define-macros-order'),
     'define-props-declaration': require('./rules/define-props-declaration'),
+    'define-props-destructuring': require('./rules/define-props-destructuring'),
     'dot-location': require('./rules/dot-location'),
     'dot-notation': require('./rules/dot-notation'),
     'enforce-style-attribute': require('./rules/enforce-style-attribute'),
diff --git a/lib/rules/define-props-destructuring.js b/lib/rules/define-props-destructuring.js
new file mode 100644
index 00000000..65ec1dcd
--- /dev/null
+++ b/lib/rules/define-props-destructuring.js
@@ -0,0 +1,79 @@
+/**
+ * @author Wayne Zhang
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+const utils = require('../utils')
+
+module.exports = {
+  meta: {
+    type: 'suggestion',
+    docs: {
+      description: 'enforce consistent style for props destructuring',
+      categories: undefined,
+      url: 'https://eslint.vuejs.org/rules/define-props-destructuring.html'
+    },
+    fixable: null,
+    schema: [
+      {
+        type: 'object',
+        properties: {
+          destructure: {
+            enum: ['always', 'never']
+          }
+        },
+        additionalProperties: false
+      }
+    ],
+    messages: {
+      preferDestructuring: 'Prefer destructuring from `defineProps` directly.',
+      avoidDestructuring: 'Avoid destructuring from `defineProps`.',
+      avoidWithDefaults: 'Avoid using `withDefaults` with destructuring.'
+    }
+  },
+  /** @param {RuleContext} context */
+  create(context) {
+    const options = context.options[0] || {}
+    const destructurePreference = options.destructure || 'always'
+
+    return utils.compositingVisitors(
+      utils.defineScriptSetupVisitor(context, {
+        onDefinePropsEnter(node, props) {
+          const hasNoArgs = props.filter((prop) => prop.propName).length === 0
+          if (hasNoArgs) {
+            return
+          }
+
+          const hasDestructure = utils.isUsingPropsDestructure(node)
+          const hasWithDefaults = utils.hasWithDefaults(node)
+
+          if (destructurePreference === 'never') {
+            if (hasDestructure) {
+              context.report({
+                node,
+                messageId: 'avoidDestructuring'
+              })
+            }
+            return
+          }
+
+          if (!hasDestructure) {
+            context.report({
+              node,
+              messageId: 'preferDestructuring'
+            })
+            return
+          }
+
+          if (hasWithDefaults) {
+            context.report({
+              node: node.parent.callee,
+              messageId: 'avoidWithDefaults'
+            })
+          }
+        }
+      })
+    )
+  }
+}
diff --git a/lib/rules/html-self-closing.js b/lib/rules/html-self-closing.js
index 5b98d8cf..c31c9ab7 100644
--- a/lib/rules/html-self-closing.js
+++ b/lib/rules/html-self-closing.js
@@ -150,8 +150,7 @@ module.exports = {
             isEmpty(node, sourceCode)
           ) {
             context.report({
-              node,
-              loc: node.loc,
+              node: node.endTag || node,
               messageId: 'requireSelfClosing',
               data: {
                 elementType: ELEMENT_TYPE_MESSAGES[elementType],
@@ -175,7 +174,13 @@ module.exports = {
           if (mode === 'never' && node.startTag.selfClosing) {
             context.report({
               node,
-              loc: node.loc,
+              loc: {
+                start: {
+                  line: node.loc.end.line,
+                  column: node.loc.end.column - 2
+                },
+                end: node.loc.end
+              },
               messageId: 'disallowSelfClosing',
               data: {
                 elementType: ELEMENT_TYPE_MESSAGES[elementType],
diff --git a/lib/rules/no-bare-strings-in-template.js b/lib/rules/no-bare-strings-in-template.js
index 1c75f509..8e4acc96 100644
--- a/lib/rules/no-bare-strings-in-template.js
+++ b/lib/rules/no-bare-strings-in-template.js
@@ -149,17 +149,33 @@ module.exports = {
      */
     const opts = context.options[0] || {}
     /** @type {string[]} */
-    const allowlist = opts.allowlist || DEFAULT_ALLOWLIST
+    const rawAllowlist = opts.allowlist || DEFAULT_ALLOWLIST
     const attributes = parseTargetAttrs(opts.attributes || DEFAULT_ATTRIBUTES)
     const directives = opts.directives || DEFAULT_DIRECTIVES
 
-    const allowlistRe = new RegExp(
-      allowlist
-        .map((w) => regexp.escape(w))
-        .sort((a, b) => b.length - a.length)
-        .join('|'),
-      'gu'
-    )
+    /** @type {string[]} */
+    const stringAllowlist = []
+    /** @type {RegExp[]} */
+    const regexAllowlist = []
+
+    for (const item of rawAllowlist) {
+      if (regexp.isRegExp(item)) {
+        regexAllowlist.push(regexp.toRegExp(item))
+      } else {
+        stringAllowlist.push(item)
+      }
+    }
+
+    const allowlistRe =
+      stringAllowlist.length > 0
+        ? new RegExp(
+            stringAllowlist
+              .map((w) => regexp.escape(w))
+              .sort((a, b) => b.length - a.length)
+              .join('|'),
+            'gu'
+          )
+        : null
 
     /** @type {ElementStack | null} */
     let elementStack = null
@@ -168,7 +184,21 @@ module.exports = {
      * @param {string} str
      */
     function getBareString(str) {
-      return str.trim().replace(allowlistRe, '').trim()
+      let result = str.trim()
+
+      if (allowlistRe) {
+        result = result.replace(allowlistRe, '')
+      }
+
+      for (const regex of regexAllowlist) {
+        const flags = regex.flags.includes('g')
+          ? regex.flags
+          : `${regex.flags}g`
+        const globalRegex = new RegExp(regex.source, flags)
+        result = result.replace(globalRegex, '')
+      }
+
+      return result.trim()
     }
 
     /**
diff --git a/lib/rules/no-dupe-keys.js b/lib/rules/no-dupe-keys.js
index 01b85d9f..ecfa787c 100644
--- a/lib/rules/no-dupe-keys.js
+++ b/lib/rules/no-dupe-keys.js
@@ -58,6 +58,33 @@ function isInsideInitializer(node, references) {
   )
 }
 
+/**
+ * Collects all renamed props from a pattern
+ * @param {Pattern | null} pattern - The destructuring pattern
+ * @returns {Set<string>} - Set of prop names that have been renamed
+ */
+function collectRenamedProps(pattern) {
+  const renamedProps = new Set()
+
+  if (!pattern || pattern.type !== 'ObjectPattern') {
+    return renamedProps
+  }
+
+  for (const prop of pattern.properties) {
+    if (prop.type !== 'Property') continue
+
+    if (
+      prop.key.type === 'Identifier' &&
+      prop.value.type === 'Identifier' &&
+      prop.key.name !== prop.value.name
+    ) {
+      renamedProps.add(prop.key.name)
+    }
+  }
+
+  return renamedProps
+}
+
 module.exports = {
   meta: {
     type: 'problem',
@@ -115,9 +142,15 @@ module.exports = {
             node
           ]
 
+          const renamedProps = collectRenamedProps(propsNode)
+
           for (const prop of props) {
             if (!prop.propName) continue
 
+            if (renamedProps.has(prop.propName)) {
+              continue
+            }
+
             const variable = findVariable(
               utils.getScope(context, node),
               prop.propName
diff --git a/lib/rules/no-duplicate-attr-inheritance.js b/lib/rules/no-duplicate-attr-inheritance.js
index 1f0fb0fd..31aef7e4 100644
--- a/lib/rules/no-duplicate-attr-inheritance.js
+++ b/lib/rules/no-duplicate-attr-inheritance.js
@@ -63,7 +63,7 @@ module.exports = {
     const options = context.options[0] || {}
     const checkMultiRootNodes = options.checkMultiRootNodes === true
 
-    /** @type {string | number | boolean | RegExp | BigInt | null} */
+    /** @type {Literal['value']} */
     let inheritsAttrs = true
     /** @type {VReference[]} */
     const attrsRefs = []
diff --git a/lib/rules/no-multiple-template-root.js b/lib/rules/no-multiple-template-root.js
index 45c22389..524b7a2b 100644
--- a/lib/rules/no-multiple-template-root.js
+++ b/lib/rules/no-multiple-template-root.js
@@ -6,6 +6,21 @@
 
 const utils = require('../utils')
 
+/**
+ * Get all comments that need to be reported
+ * @param {(HTMLComment | HTMLBogusComment | Comment)[]} comments
+ * @param {Range[]} elementRanges
+ * @returns {(HTMLComment | HTMLBogusComment | Comment)[]}
+ */
+function getReportComments(comments, elementRanges) {
+  return comments.filter(
+    (comment) =>
+      !elementRanges.some(
+        (range) => range[0] <= comment.range[0] && comment.range[1] <= range[1]
+      )
+  )
+}
+
 module.exports = {
   meta: {
     type: 'problem',
@@ -15,8 +30,19 @@ module.exports = {
       url: 'https://eslint.vuejs.org/rules/no-multiple-template-root.html'
     },
     fixable: null,
-    schema: [],
+    schema: [
+      {
+        type: 'object',
+        properties: {
+          disallowComments: {
+            type: 'boolean'
+          }
+        },
+        additionalProperties: false
+      }
+    ],
     messages: {
+      commentRoot: 'The template root disallows comments.',
       multipleRoot: 'The template root requires exactly one element.',
       textRoot: 'The template root requires an element rather than texts.',
       disallowedElement: "The template root disallows '<{{name}}>' elements.",
@@ -28,6 +54,8 @@ module.exports = {
    * @returns {RuleListener} AST event handlers.
    */
   create(context) {
+    const options = context.options[0] || {}
+    const disallowComments = options.disallowComments
     const sourceCode = context.getSourceCode()
 
     return {
@@ -37,6 +65,18 @@ module.exports = {
           return
         }
 
+        const comments = element.comments
+        const elementRanges = element.children.map((child) => child.range)
+        if (disallowComments && comments.length > 0) {
+          for (const comment of getReportComments(comments, elementRanges)) {
+            context.report({
+              node: comment,
+              loc: comment.loc,
+              messageId: 'commentRoot'
+            })
+          }
+        }
+
         const rootElements = []
         let extraText = null
         let extraElement = null
diff --git a/lib/rules/no-ref-as-operand.js b/lib/rules/no-ref-as-operand.js
index db92f19e..b1a9e12a 100644
--- a/lib/rules/no-ref-as-operand.js
+++ b/lib/rules/no-ref-as-operand.js
@@ -233,7 +233,7 @@ module.exports = {
         },
         // `${refValue}`
         /** @param {Identifier} node */
-        'TemplateLiteral>Identifier'(node) {
+        ':not(TaggedTemplateExpression)>TemplateLiteral>Identifier'(node) {
           reportIfRefWrapped(node)
         },
         // refValue.x
diff --git a/lib/rules/no-unused-refs.js b/lib/rules/no-unused-refs.js
index 4896ce25..6c5407e0 100644
--- a/lib/rules/no-unused-refs.js
+++ b/lib/rules/no-unused-refs.js
@@ -237,8 +237,10 @@ module.exports = {
           CallExpression(callExpression) {
             const firstArgument = callExpression.arguments[0]
             if (
+              callExpression.callee.type !== 'Identifier' ||
               callExpression.callee.name !== 'useTemplateRef' ||
-              !firstArgument
+              !firstArgument ||
+              !utils.isStringLiteral(firstArgument)
             ) {
               return
             }
diff --git a/lib/rules/require-default-prop.js b/lib/rules/require-default-prop.js
index b5d2564e..acceacf0 100644
--- a/lib/rules/require-default-prop.js
+++ b/lib/rules/require-default-prop.js
@@ -200,8 +200,8 @@ module.exports = {
 
           processProps(props, (prop) => {
             if (prop.type === 'type') {
-              if (!hasWithDefaults) {
-                // If don't use withDefaults(), exclude it from the report.
+              if (!hasWithDefaults && !isUsingPropsDestructure) {
+                // If don't use withDefaults() and props destructure, exclude it from the report.
                 return true
               }
               if (defaultsByWithDefaults[prop.propName]) {
diff --git a/lib/utils/html-elements.json b/lib/utils/html-elements.json
index 829b6f84..ff9cdf31 100644
--- a/lib/utils/html-elements.json
+++ b/lib/utils/html-elements.json
@@ -74,7 +74,6 @@
   "output",
   "p",
   "picture",
-  "portal",
   "pre",
   "progress",
   "q",
@@ -87,6 +86,7 @@
   "search",
   "section",
   "select",
+  "selectedcontent",
   "slot",
   "small",
   "source",
diff --git a/lib/utils/svg-elements.json b/lib/utils/svg-elements.json
index f214aad2..eedbf0d0 100644
--- a/lib/utils/svg-elements.json
+++ b/lib/utils/svg-elements.json
@@ -7,6 +7,7 @@
   "clipPath",
   "defs",
   "desc",
+  "discard",
   "ellipse",
   "feBlend",
   "feColorMatrix",
diff --git a/package.json b/package.json
index 475c24e6..5eb6e811 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "eslint-plugin-vue",
-  "version": "10.0.0",
+  "version": "10.0.1",
   "description": "Official ESLint plugin for Vue.js",
   "main": "lib/index.js",
   "types": "lib/index.d.ts",
@@ -69,7 +69,6 @@
     "@ota-meshi/site-kit-eslint-editor-vue": "^0.2.4",
     "@stylistic/eslint-plugin": "^2.12.1",
     "@types/eslint": "^8.56.2",
-    "@types/eslint-visitor-keys": "^3.3.2",
     "@types/natural-compare": "^1.4.3",
     "@types/node": "^14.18.63",
     "@types/semver": "^7.5.8",
@@ -88,6 +87,7 @@
     "eslint-plugin-prettier": "^5.2.1",
     "eslint-plugin-unicorn": "^56.0.0",
     "eslint-plugin-vue": "file:.",
+    "eslint-visitor-keys": "^4.2.0",
     "espree": "^9.6.1",
     "events": "^3.3.0",
     "globals": "^15.14.0",


Test patch:
diff --git a/tests/lib/rules/define-props-destructuring.js b/tests/lib/rules/define-props-destructuring.js
new file mode 100644
index 00000000..ec24b432
--- /dev/null
+++ b/tests/lib/rules/define-props-destructuring.js
@@ -0,0 +1,212 @@
+/**
+ * @author Wayne Zhang
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+const RuleTester = require('../../eslint-compat').RuleTester
+const rule = require('../../../lib/rules/define-props-destructuring')
+
+const tester = new RuleTester({
+  languageOptions: {
+    parser: require('vue-eslint-parser'),
+    ecmaVersion: 2015,
+    sourceType: 'module'
+  }
+})
+
+tester.run('define-props-destructuring', rule, {
+  valid: [
+    {
+      filename: 'test.vue',
+      code: `
+      <script setup>
+      const props = defineProps()
+      </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script setup>
+      const { foo = 'default' } = defineProps(['foo'])
+      </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script setup lang="ts">
+      const { foo = 'default' } = defineProps<{ foo?: string }>()
+      </script>
+      `,
+      languageOptions: {
+        parserOptions: { parser: require.resolve('@typescript-eslint/parser') }
+      }
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script setup>
+      const props = defineProps(['foo'])
+      </script>
+      `,
+      options: [{ destructure: 'never' }]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script setup>
+      const props = withDefaults(defineProps(['foo']), { foo: 'default' })
+      </script>
+      `,
+      options: [{ destructure: 'never' }]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script setup lang="ts">
+      const props = defineProps<{ foo?: string }>()
+      </script>
+      `,
+      options: [{ destructure: 'never' }],
+      languageOptions: {
+        parserOptions: { parser: require.resolve('@typescript-eslint/parser') }
+      }
+    }
+  ],
+  invalid: [
+    {
+      filename: 'test.vue',
+      code: `
+      <script setup>
+      const props = defineProps(['foo'])
+      </script>
+      `,
+      errors: [
+        {
+          messageId: 'preferDestructuring',
+          line: 3,
+          column: 21
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script setup>
+      const props = withDefaults(defineProps(['foo']), { foo: 'default' })
+      </script>
+      `,
+      errors: [
+        {
+          messageId: 'preferDestructuring',
+          line: 3,
+          column: 34
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script setup>
+      const { foo } = withDefaults(defineProps(['foo']), { foo: 'default' })
+      </script>
+      `,
+      errors: [
+        {
+          messageId: 'avoidWithDefaults',
+          line: 3,
+          column: 23
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script setup lang="ts">
+      const props = withDefaults(defineProps<{ foo?: string }>(), { foo: 'default' })
+      </script>
+      `,
+      languageOptions: {
+        parserOptions: { parser: require.resolve('@typescript-eslint/parser') }
+      },
+      errors: [
+        {
+          messageId: 'preferDestructuring',
+          line: 3,
+          column: 34
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script setup lang="ts">
+      const { foo } = withDefaults(defineProps<{ foo?: string }>(), { foo: 'default' })
+      </script>
+      `,
+      languageOptions: {
+        parserOptions: { parser: require.resolve('@typescript-eslint/parser') }
+      },
+      errors: [
+        {
+          messageId: 'avoidWithDefaults',
+          line: 3,
+          column: 23
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script setup>
+      const { foo } = defineProps(['foo'])
+      </script>
+      `,
+      options: [{ destructure: 'never' }],
+      errors: [
+        {
+          messageId: 'avoidDestructuring',
+          line: 3,
+          column: 23
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script setup>
+      const { foo } = withDefaults(defineProps(['foo']), { foo: 'default' })
+      </script>
+      `,
+      options: [{ destructure: 'never' }],
+      errors: [
+        {
+          messageId: 'avoidDestructuring',
+          line: 3,
+          column: 36
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script setup lang="ts">
+      const { foo } = defineProps<{ foo?: string }>()
+      </script>
+      `,
+      options: [{ destructure: 'never' }],
+      languageOptions: {
+        parserOptions: { parser: require.resolve('@typescript-eslint/parser') }
+      },
+      errors: [
+        {
+          messageId: 'avoidDestructuring',
+          line: 3,
+          column: 23
+        }
+      ]
+    }
+  ]
+})
diff --git a/tests/lib/rules/no-bare-strings-in-template.js b/tests/lib/rules/no-bare-strings-in-template.js
index 4c6c89c7..6706aaae 100644
--- a/tests/lib/rules/no-bare-strings-in-template.js
+++ b/tests/lib/rules/no-bare-strings-in-template.js
@@ -132,6 +132,32 @@ tester.run('no-bare-strings-in-template', rule, {
       </template>
       `,
       options: [{ allowlist: ['@@'] }]
+    },
+    // regex
+    {
+      code: `
+      <template>
+        <h1>123 321</h1>
+      </template>
+      `,
+      options: [{ allowlist: [String.raw`/\d+/g`] }]
+    },
+    {
+      code: `
+      <template>
+        <h1>$foo</h1>
+        <h1>$bar</h1>
+      </template>
+      `,
+      options: [{ allowlist: [String.raw`/\$\w+/`] }]
+    },
+    {
+      code: `
+      <template>
+        <h1>foo123foo</h1>
+      </template>
+      `,
+      options: [{ allowlist: [String.raw`/\d+/`, 'foo'] }]
     }
   ],
   invalid: [
@@ -316,6 +342,40 @@ tester.run('no-bare-strings-in-template', rule, {
           endColumn: 34
         }
       ]
+    },
+    {
+      code: `
+      <template>
+        <h1>123, foo is invalid, 321</h1>
+      </template>
+      `,
+      options: [{ allowlist: [String.raw`/^\d+$/g`] }],
+      errors: [
+        {
+          messageId: 'unexpected',
+          line: 3,
+          column: 13,
+          endLine: 3,
+          endColumn: 37
+        }
+      ]
+    },
+    {
+      code: `
+      <template>
+        <h1>foo123bar</h1>
+      </template>
+      `,
+      options: [{ allowlist: [String.raw`/\d+/`, 'foo'] }],
+      errors: [
+        {
+          messageId: 'unexpected',
+          line: 3,
+          column: 13,
+          endLine: 3,
+          endColumn: 22
+        }
+      ]
     }
   ]
 })
diff --git a/tests/lib/rules/no-dupe-keys.js b/tests/lib/rules/no-dupe-keys.js
index 124442ec..2df95908 100644
--- a/tests/lib/rules/no-dupe-keys.js
+++ b/tests/lib/rules/no-dupe-keys.js
@@ -466,7 +466,7 @@ ruleTester.run('no-dupe-keys', rule, {
     {
       filename: 'test.vue',
       code: `
-      <script setup></script>
+      <script setup>
       const {foo,bar} = defineProps(['foo', 'bar'])
       </script>
       `,
@@ -475,7 +475,7 @@ ruleTester.run('no-dupe-keys', rule, {
     {
       filename: 'test.vue',
       code: `
-      <script setup></script>
+      <script setup>
       const {foo=42,bar='abc'} = defineProps(['foo', 'bar'])
       </script>
       `,
@@ -500,6 +500,17 @@ ruleTester.run('no-dupe-keys', rule, {
         parser: require('vue-eslint-parser'),
         parserOptions: { parser: require.resolve('@typescript-eslint/parser') }
       }
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script setup>
+      const { foo: renamedFoo, bar: renamedBar } = defineProps(['foo', 'bar'])
+      const foo = 42
+      const bar = 'hello'
+      </script>
+      `,
+      languageOptions: { parser: require('vue-eslint-parser') }
     }
   ],
 
@@ -1105,6 +1116,24 @@ ruleTester.run('no-dupe-keys', rule, {
           line: 5
         }
       ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script setup>
+      const { foo: renamedFoo } = defineProps(['foo', 'bar'])
+      const foo = 'foo'
+      const bar = 'bar'
+      </script>
+      `,
+      languageOptions: { parser: require('vue-eslint-parser') },
+      errors: [
+        {
+          message:
+            "Duplicate key 'bar'. May cause name collision in script or template tag.",
+          line: 5
+        }
+      ]
     }
   ]
 })
diff --git a/tests/lib/rules/no-multiple-template-root.js b/tests/lib/rules/no-multiple-template-root.js
index 7ab80af7..c52c1c20 100644
--- a/tests/lib/rules/no-multiple-template-root.js
+++ b/tests/lib/rules/no-multiple-template-root.js
@@ -62,6 +62,66 @@ ruleTester.run('no-multiple-template-root', rule, {
         </Link>
       </template>
       `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <!-- comments -->
+        <div>12333</div>
+        <!-- comments -->
+      </template>
+      `,
+      options: [{ disallowComments: false }]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <!-- comments -->
+        <div>
+          <!-- comments -->
+          12333
+        </div>
+      </template>
+      `,
+      options: [{ disallowComments: false }]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div>
+          <!-- comments -->
+          12333
+          <span>
+            <!-- comments -->
+            12333
+          </span>
+        </div>
+      </template>
+      `,
+      options: [{ disallowComments: true }]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div v-if="for">
+          <!-- comments -->
+          12333
+          <span>
+            <!-- comments -->
+            12333
+          </span>
+        </div>
+        <div v-else>
+          <!-- comments -->
+          12333
+        </div>
+      </template>
+      `,
+      options: [{ disallowComments: true }]
     }
   ],
   invalid: [
@@ -104,6 +164,132 @@ ruleTester.run('no-multiple-template-root', rule, {
       filename: 'test.vue',
       code: '<template><template></template></template>',
       errors: ["The template root disallows '<template>' elements."]
+    },
+    {
+      code: `
+      <template>
+        <!-- comments -->
+        <div>12333</div>
+        <!-- comments -->
+      </template>
+      `,
+      options: [{ disallowComments: true }],
+      errors: [
+        {
+          message: 'The template root disallows comments.',
+          line: 3
+        },
+        {
+          message: 'The template root disallows comments.',
+          line: 5
+        }
+      ]
+    },
+    {
+      code: `
+      <template>
+        <!-- comments -->
+        <div>
+          12333
+          <!-- comments -->
+        </div>
+      </template>
+      `,
+      options: [{ disallowComments: true }],
+      errors: [
+        {
+          message: 'The template root disallows comments.',
+          line: 3
+        }
+      ]
+    },
+    {
+      code: `
+      <template>
+        <!-- comments -->
+        <div v-if="for">
+          <!-- comments -->
+          12333
+          <span>
+            <!-- comments -->
+            12333
+          </span>
+        </div>
+        <!-- comments -->
+        <div v-else>
+          <!-- comments -->
+          12333
+        </div>
+        <!-- comments -->
+      </template>
+      `,
+      options: [{ disallowComments: true }],
+      errors: [
+        {
+          message: 'The template root disallows comments.',
+          line: 3
+        },
+        {
+          message: 'The template root disallows comments.',
+          line: 12
+        },
+        {
+          message: 'The template root disallows comments.',
+          line: 17
+        }
+      ]
+    },
+    {
+      code: `
+      <template>
+        <div>
+          12333
+          <!-- comments -->
+        </div>
+        <!-- comments -->
+      </template>
+      `,
+      options: [{ disallowComments: true }],
+      errors: [
+        {
+          message: 'The template root disallows comments.',
+          line: 7
+        }
+      ]
+    },
+    {
+      code: `
+      <template>
+        <div />
+        <!-- comments -->
+        <div />
+      </template>
+      `,
+      options: [{ disallowComments: true }],
+      errors: [
+        {
+          message: 'The template root disallows comments.',
+          line: 4
+        },
+        {
+          message: 'The template root requires exactly one element.',
+          line: 5
+        }
+      ]
+    },
+    {
+      code: `
+      <template>
+        <!-- When you have a comment in the root of your template in vue 3, 
+        using $el will point to the first text comment instead of the actual DOM element.   -->
+        <div>
+          12333
+          <!-- comments -->
+        </div>
+      </template>
+      `,
+      options: [{ disallowComments: true }],
+      errors: ['The template root disallows comments.']
     }
   ]
 })
diff --git a/tests/lib/rules/no-ref-as-operand.js b/tests/lib/rules/no-ref-as-operand.js
index 04234eff..9db2ab3a 100644
--- a/tests/lib/rules/no-ref-as-operand.js
+++ b/tests/lib/rules/no-ref-as-operand.js
@@ -168,6 +168,22 @@ tester.run('no-ref-as-operand', rule, {
       }
       </script>
     `,
+    `
+      <script>
+      import { ref } from 'vue'
+      const foo = ref(0)
+      func(foo)
+      function func(foo) {}
+      </script>
+    `,
+    `
+      <script>
+      import { ref } from 'vue'
+      const foo = ref(0)
+      tag\`\${foo}\`
+      function tag(arr, ...args) {}
+      </script>
+    `,
     `
     <script setup>
     const model = defineModel();
@@ -196,7 +212,7 @@ tester.run('no-ref-as-operand', rule, {
     <script setup>
     const emit = defineEmits(['test'])
     const [model, mod] = defineModel();
-    
+
     function update() {
       emit('test', model.value)
     }
diff --git a/tests/lib/rules/require-default-prop.js b/tests/lib/rules/require-default-prop.js
index e352eddf..43160dfd 100644
--- a/tests/lib/rules/require-default-prop.js
+++ b/tests/lib/rules/require-default-prop.js
@@ -388,6 +388,31 @@ ruleTester.run('require-default-prop', rule, {
         parser: require('vue-eslint-parser'),
         ...languageOptions
       }
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script setup>
+      const {bar=42} = defineProps({foo: {type: Number, required: true}, bar: {type: Number, required: false}})
+      </script>
+      `,
+      languageOptions: {
+        parser: require('vue-eslint-parser'),
+        ...languageOptions
+      }
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script setup lang="ts">
+      const {foo = 42, bar} = defineProps<{foo?: number; bar: number}>()
+      </script>
+      `,
+      languageOptions: {
+        parser: require('vue-eslint-parser'),
+        ...languageOptions,
+        parserOptions: { parser: require.resolve('@typescript-eslint/parser') }
+      }
     }
   ],
 
@@ -700,6 +725,26 @@ ruleTester.run('require-default-prop', rule, {
           line: 3
         }
       ]
+    },
+    {
+      // https://github.com/vuejs/eslint-plugin-vue/issues/2725
+      filename: 'type-with-props-destructure.vue',
+      code: `
+      <script setup lang="ts">
+      const {foo, bar} = defineProps<{foo?: number; bar: number}>()
+      </script>
+      `,
+      languageOptions: {
+        parser: require('vue-eslint-parser'),
+        ...languageOptions,
+        parserOptions: { parser: require.resolve('@typescript-eslint/parser') }
+      },
+      errors: [
+        {
+          message: "Prop 'foo' requires default value to be set.",
+          line: 3
+        }
+      ]
     }
   ]
 })