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

Issue:
`vue/no-setup-props-destructure` is too restrictive after latest update
**What rule do you want to change?** 

`vue/no-setup-props-destructure`

**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.)?**

Revert #2244 and extract those rule changes into a new rule.

The changes now cause rule violations for example code directly from the vue documentation (see https://github.com/vuejs/eslint-plugin-vue/pull/2244#issuecomment-1656936626). 

It now enforces more or less subjective defaults. I mostly agree with the defaults, but the rule is active in `vue3-essential`, which shouldn't happen for subjective defaults.

In addition to that, the name and documentation no longer matches with the behaviour.


**Please provide some example code that this change will affect:**

```vue
<script setup>
  const props = defineProps({ count: Number })
  const count = ref(props.count)
                         ^^^^^^ Failing
</script>
```

**What does the rule currently do for this code?**

The code is violating the rule, even though this is not a destructure, but a non reactive access.

**What will the rule do after it's changed?**

It shouldn't violate. This is a valid use case for setting an initial value, even though it might not be required to do so often. Still, it shouldn't be necessary to disable the rule for every line where this occurs.

Please consider making this a new rule, which could be placed in `recommended` instead. This has nothing to do with destructuring anymore, which is indeed critical and should fail. 





Pull request:
Rename `vue/no-setup-props-destructure` to `vue/no-setup-props-reactivity-loss` and remove from config


close #2259

Patch:
diff --git a/docs/rules/index.md b/docs/rules/index.md
index 6b06a7c7..d5525a78 100644
--- a/docs/rules/index.md
+++ b/docs/rules/index.md
@@ -79,7 +79,6 @@ Rules in this category are enabled for all presets provided by eslint-plugin-vue
 | [vue/no-reserved-component-names](./no-reserved-component-names.md) | disallow the use of reserved names in component definitions |  | :three::two::hammer: |
 | [vue/no-reserved-keys](./no-reserved-keys.md) | disallow overwriting reserved keys |  | :three::two::hammer: |
 | [vue/no-reserved-props](./no-reserved-props.md) | disallow reserved names in props |  | :three::two::warning: |
-| [vue/no-setup-props-destructure](./no-setup-props-destructure.md) | disallow destructuring of `props` passed to `setup` |  | :three::two::hammer: |
 | [vue/no-shared-component-data](./no-shared-component-data.md) | enforce component's data property to be a function | :wrench: | :three::two::warning: |
 | [vue/no-side-effects-in-computed-properties](./no-side-effects-in-computed-properties.md) | disallow side effects in computed properties |  | :three::two::warning: |
 | [vue/no-template-key](./no-template-key.md) | disallow `key` attribute on `<template>` |  | :three::two::warning: |
@@ -245,6 +244,7 @@ For example:
 | [vue/no-restricted-static-attribute](./no-restricted-static-attribute.md) | disallow specific attribute |  | :hammer: |
 | [vue/no-restricted-v-bind](./no-restricted-v-bind.md) | disallow specific argument in `v-bind` |  | :hammer: |
 | [vue/no-root-v-if](./no-root-v-if.md) | disallow `v-if` directives on root element |  | :hammer: |
+| [vue/no-setup-props-reactivity-loss](./no-setup-props-reactivity-loss.md) | disallow usages that lose the reactivity of `props` passed to `setup` |  | :hammer: |
 | [vue/no-static-inline-styles](./no-static-inline-styles.md) | disallow static inline `style` attributes |  | :hammer: |
 | [vue/no-template-target-blank](./no-template-target-blank.md) | disallow target="_blank" attribute without rel="noopener noreferrer" | :bulb: | :warning: |
 | [vue/no-this-in-before-route-enter](./no-this-in-before-route-enter.md) | disallow `this` usage in a `beforeRouteEnter` method |  | :warning: |
@@ -339,6 +339,7 @@ The following rules extend the rules provided by ESLint itself and apply them to
 |:--------|:------------|
 | [vue/component-tags-order](./component-tags-order.md) | [vue/block-order](./block-order.md) |
 | [vue/no-invalid-model-keys](./no-invalid-model-keys.md) | [vue/valid-model-definition](./valid-model-definition.md) |
+| [vue/no-setup-props-destructure](./no-setup-props-destructure.md) | [vue/no-setup-props-reactivity-loss](./no-setup-props-reactivity-loss.md) |
 | [vue/script-setup-uses-vars](./script-setup-uses-vars.md) | (no replacement) |
 | [vue/v-on-function-call](./v-on-function-call.md) | [vue/v-on-handler-style](./v-on-handler-style.md) |
 
diff --git a/docs/rules/no-setup-props-destructure.md b/docs/rules/no-setup-props-destructure.md
index 8db33035..37fdcf8b 100644
--- a/docs/rules/no-setup-props-destructure.md
+++ b/docs/rules/no-setup-props-destructure.md
@@ -2,14 +2,14 @@
 pageClass: rule-details
 sidebarDepth: 0
 title: vue/no-setup-props-destructure
-description: disallow destructuring of `props` passed to `setup`
+description: disallow usages that lose the reactivity of `props` passed to `setup`
 since: v7.0.0
 ---
 # vue/no-setup-props-destructure
 
-> disallow destructuring of `props` passed to `setup`
+> disallow usages that lose the reactivity of `props` passed to `setup`
 
-- :gear: This rule is included in all of `"plugin:vue/vue3-essential"`, `"plugin:vue/essential"`, `"plugin:vue/vue3-strongly-recommended"`, `"plugin:vue/strongly-recommended"`, `"plugin:vue/vue3-recommended"` and `"plugin:vue/recommended"`.
+- :no_entry_sign: This rule was **deprecated** and replaced by [vue/no-setup-props-reactivity-loss](no-setup-props-reactivity-loss.md) rule.
 
 ## :book: Rule Details
 
diff --git a/docs/rules/no-setup-props-reactivity-loss.md b/docs/rules/no-setup-props-reactivity-loss.md
new file mode 100644
index 00000000..bc90415e
--- /dev/null
+++ b/docs/rules/no-setup-props-reactivity-loss.md
@@ -0,0 +1,102 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/no-setup-props-reactivity-loss
+description: disallow usages that lose the reactivity of `props` passed to `setup`
+---
+# vue/no-setup-props-reactivity-loss
+
+> disallow usages that lose the reactivity of `props` passed to `setup`
+
+- :exclamation: <badge text="This rule has not been released yet." vertical="middle" type="error"> ***This rule has not been released yet.*** </badge>
+
+## :book: Rule Details
+
+This rule reports the destructuring or member expression of `props` passed to `setup` causing the value to lose reactivity.
+
+<eslint-code-block :rules="{'vue/no-setup-props-reactivity-loss': ['error']}">
+
+```vue
+<script>
+export default {
+  /* ✓ GOOD */
+  setup(props) {
+    watch(() => props.count, () => {
+      console.log(props.count)
+    })
+
+    return () => {
+      return h('div', props.count)
+    }
+  }
+}
+</script>
+```
+
+</eslint-code-block>
+
+Destructuring the `props` passed to `setup` will cause the value to lose reactivity.
+
+<eslint-code-block :rules="{'vue/no-setup-props-reactivity-loss': ['error']}">
+
+```vue
+<script>
+export default {
+  /* ✗ BAD */
+  setup({ count }) {
+    watch(() => count, () => { // not going to detect changes
+      console.log(count)
+    })
+
+    return () => {
+      return h('div', count) // not going to update
+    }
+  }
+}
+</script>
+```
+
+</eslint-code-block>
+
+Also, destructuring in root scope of `setup()` should error, but ok inside nested callbacks or returned render functions:
+
+<eslint-code-block :rules="{'vue/no-setup-props-reactivity-loss': ['error']}">
+
+```vue
+<script>
+export default {
+  setup(props) {
+    /* ✗ BAD */
+    const { count } = props
+
+    watch(() => props.count, () => {
+      /* ✓ GOOD */
+      const { count } = props
+      console.log(count)
+    })
+
+    return () => {
+      /* ✓ GOOD */
+      const { count } = props
+      return h('div', count)
+    }
+  }
+}
+</script>
+```
+
+</eslint-code-block>
+
+## :wrench: Options
+
+Nothing.
+
+## :books: Further Reading
+
+- [Guide - Composition API - Setup](https://vuejs.org/api/composition-api-setup.html)
+- [Vue RFCs - 0013-composition-api](https://github.com/vuejs/rfcs/blob/master/active-rfcs/0013-composition-api.md)
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-setup-props-reactivity-loss.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-setup-props-reactivity-loss.js)
diff --git a/lib/configs/essential.js b/lib/configs/essential.js
index 944a162e..e2c66ceb 100644
--- a/lib/configs/essential.js
+++ b/lib/configs/essential.js
@@ -28,7 +28,6 @@ module.exports = {
         vueVersion: 2
       }
     ],
-    'vue/no-setup-props-destructure': 'error',
     'vue/no-shared-component-data': 'error',
     'vue/no-side-effects-in-computed-properties': 'error',
     'vue/no-template-key': 'error',
diff --git a/lib/configs/vue3-essential.js b/lib/configs/vue3-essential.js
index 5fe530be..f71508f7 100644
--- a/lib/configs/vue3-essential.js
+++ b/lib/configs/vue3-essential.js
@@ -42,7 +42,6 @@ module.exports = {
     'vue/no-reserved-component-names': 'error',
     'vue/no-reserved-keys': 'error',
     'vue/no-reserved-props': 'error',
-    'vue/no-setup-props-destructure': 'error',
     'vue/no-shared-component-data': 'error',
     'vue/no-side-effects-in-computed-properties': 'error',
     'vue/no-template-key': 'error',
diff --git a/lib/index.js b/lib/index.js
index ace37ce5..d69a03f4 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -131,6 +131,7 @@ module.exports = {
     'no-restricted-v-bind': require('./rules/no-restricted-v-bind'),
     'no-root-v-if': require('./rules/no-root-v-if'),
     'no-setup-props-destructure': require('./rules/no-setup-props-destructure'),
+    'no-setup-props-reactivity-loss': require('./rules/no-setup-props-reactivity-loss'),
     'no-shared-component-data': require('./rules/no-shared-component-data'),
     'no-side-effects-in-computed-properties': require('./rules/no-side-effects-in-computed-properties'),
     'no-spaces-around-equal-signs-in-attribute': require('./rules/no-spaces-around-equal-signs-in-attribute'),
diff --git a/lib/rules/no-deprecated-model-definition.js b/lib/rules/no-deprecated-model-definition.js
index 9540ffc0..d88a5028 100644
--- a/lib/rules/no-deprecated-model-definition.js
+++ b/lib/rules/no-deprecated-model-definition.js
@@ -12,7 +12,7 @@ const allowedEventNames = new Set(['update:modelValue', 'update:model-value'])
 /**
  * @param {ObjectExpression} node
  * @param {string} key
- * @returns {Literal | undefined}
+ * @returns {Literal | TemplateLiteral | undefined}
  */
 function findPropertyValue(node, key) {
   const property = node.properties.find(
@@ -24,7 +24,7 @@ function findPropertyValue(node, key) {
   if (
     !property ||
     property.type !== 'Property' ||
-    property.value.type !== 'Literal'
+    !utils.isStringLiteral(property.value)
   ) {
     return undefined
   }
@@ -33,7 +33,7 @@ function findPropertyValue(node, key) {
 
 /**
  * @param {RuleFixer} fixer
- * @param {Literal} node
+ * @param {Literal | TemplateLiteral} node
  * @param {string} text
  */
 function replaceLiteral(fixer, node, text) {
@@ -93,10 +93,12 @@ module.exports = {
       if (
         !propName ||
         !eventName ||
-        typeof propName.value !== 'string' ||
-        typeof eventName.value !== 'string' ||
-        !allowedPropNames.has(propName.value) ||
-        !allowedEventNames.has(eventName.value)
+        !allowedPropNames.has(
+          utils.getStringLiteralValue(propName, true) ?? ''
+        ) ||
+        !allowedEventNames.has(
+          utils.getStringLiteralValue(eventName, true) ?? ''
+        )
       ) {
         context.report({
           node: modelProperty,
diff --git a/lib/rules/no-setup-props-destructure.js b/lib/rules/no-setup-props-destructure.js
index 653a0b44..7b49b0a4 100644
--- a/lib/rules/no-setup-props-destructure.js
+++ b/lib/rules/no-setup-props-destructure.js
@@ -3,269 +3,22 @@
  * See LICENSE file in root directory for full license.
  */
 'use strict'
-const { findVariable } = require('@eslint-community/eslint-utils')
-const utils = require('../utils')
+const baseRule = require('./no-setup-props-reactivity-loss')
 
 module.exports = {
+  // eslint-disable-next-line eslint-plugin/require-meta-schema, eslint-plugin/prefer-message-ids, internal/no-invalid-meta, eslint-plugin/require-meta-type -- inherit schema from base rule
   meta: {
-    type: 'suggestion',
+    ...baseRule.meta,
+    // eslint-disable-next-line eslint-plugin/require-meta-docs-description, internal/no-invalid-meta-docs-categories, eslint-plugin/meta-property-ordering
     docs: {
-      description: 'disallow destructuring of `props` passed to `setup`',
-      categories: ['vue3-essential', 'essential'],
+      ...baseRule.meta.docs,
       url: 'https://eslint.vuejs.org/rules/no-setup-props-destructure.html'
     },
-    fixable: null,
-    schema: [],
-    messages: {
-      destructuring:
-        'Destructuring the `props` will cause the value to lose reactivity.',
-      getProperty:
-        'Getting a value from the `props` in root scope of `{{scopeName}}` will cause the value to lose reactivity.'
-    }
+    deprecated: true,
+    replacedBy: ['no-setup-props-reactivity-loss']
   },
-  /**
-   * @param {RuleContext} context
-   * @returns {RuleListener}
-   **/
+  /** @param {RuleContext} context */
   create(context) {
-    /**
-     * @typedef {object} ScopePropsReferences
-     * @property {Set<Identifier>} refs
-     * @property {string} scopeName
-     */
-    /** @type {Map<FunctionDeclaration | FunctionExpression | ArrowFunctionExpression | Program, ScopePropsReferences>} */
-    const setupScopePropsReferenceIds = new Map()
-    const wrapperExpressionTypes = new Set([
-      'ArrayExpression',
-      'ObjectExpression'
-    ])
-
-    /**
-     * @param {ESNode} node
-     * @param {string} messageId
-     * @param {string} scopeName
-     */
-    function report(node, messageId, scopeName) {
-      context.report({
-        node,
-        messageId,
-        data: {
-          scopeName
-        }
-      })
-    }
-
-    /**
-     * @param {Pattern} left
-     * @param {Expression | null} right
-     * @param {ScopePropsReferences} propsReferences
-     */
-    function verify(left, right, propsReferences) {
-      if (!right) {
-        return
-      }
-
-      const rightNode = utils.skipChainExpression(right)
-
-      if (
-        wrapperExpressionTypes.has(rightNode.type) &&
-        isPropsMemberAccessed(rightNode, propsReferences)
-      ) {
-        return report(rightNode, 'getProperty', propsReferences.scopeName)
-      }
-
-      if (
-        left.type !== 'ArrayPattern' &&
-        left.type !== 'ObjectPattern' &&
-        rightNode.type !== 'MemberExpression'
-      ) {
-        return
-      }
-
-      /** @type {Expression | Super} */
-      let rightId = rightNode
-      while (rightId.type === 'MemberExpression') {
-        rightId = utils.skipChainExpression(rightId.object)
-      }
-      if (rightId.type === 'Identifier' && propsReferences.refs.has(rightId)) {
-        report(left, 'getProperty', propsReferences.scopeName)
-      }
-    }
-
-    /**
-     * @param {Expression} node
-     * @param {ScopePropsReferences} propsReferences
-     */
-    function isPropsMemberAccessed(node, propsReferences) {
-      const propRefs = [...propsReferences.refs.values()]
-
-      return propRefs.some((props) => {
-        const isPropsInExpressionRange = utils.inRange(node.range, props)
-        const isPropsMemberExpression =
-          props.parent.type === 'MemberExpression' &&
-          props.parent.object === props
-
-        return isPropsInExpressionRange && isPropsMemberExpression
-      })
-    }
-
-    /**
-     * @typedef {object} ScopeStack
-     * @property {ScopeStack | null} upper
-     * @property {FunctionDeclaration | FunctionExpression | ArrowFunctionExpression | Program} scopeNode
-     */
-    /**
-     * @type {ScopeStack | null}
-     */
-    let scopeStack = null
-
-    /**
-     * @param {Pattern | null} node
-     * @param {FunctionDeclaration | FunctionExpression | ArrowFunctionExpression | Program} scopeNode
-     * @param {string} scopeName
-     */
-    function processPattern(node, scopeNode, scopeName) {
-      if (!node) {
-        // no arguments
-        return
-      }
-      if (
-        node.type === 'RestElement' ||
-        node.type === 'AssignmentPattern' ||
-        node.type === 'MemberExpression'
-      ) {
-        // cannot check
-        return
-      }
-      if (node.type === 'ArrayPattern' || node.type === 'ObjectPattern') {
-        report(node, 'destructuring', scopeName)
-        return
-      }
-
-      const variable = findVariable(context.getScope(), node)
-      if (!variable) {
-        return
-      }
-      const propsReferenceIds = new Set()
-      for (const reference of variable.references) {
-        // If reference is in another scope, we can't check it.
-        if (reference.from !== context.getScope()) {
-          continue
-        }
-
-        if (!reference.isRead()) {
-          continue
-        }
-
-        propsReferenceIds.add(reference.identifier)
-      }
-      setupScopePropsReferenceIds.set(scopeNode, {
-        refs: propsReferenceIds,
-        scopeName
-      })
-    }
-    return utils.compositingVisitors(
-      {
-        /**
-         * @param {FunctionExpression | FunctionDeclaration | ArrowFunctionExpression | Program} node
-         */
-        'Program, :function'(node) {
-          scopeStack = {
-            upper: scopeStack,
-            scopeNode: node
-          }
-        },
-        /**
-         * @param {FunctionExpression | FunctionDeclaration | ArrowFunctionExpression | Program} node
-         */
-        'Program, :function:exit'(node) {
-          scopeStack = scopeStack && scopeStack.upper
-
-          setupScopePropsReferenceIds.delete(node)
-        },
-        /**
-         * @param {CallExpression} node
-         */
-        CallExpression(node) {
-          if (!scopeStack) {
-            return
-          }
-
-          const propsReferenceIds = setupScopePropsReferenceIds.get(
-            scopeStack.scopeNode
-          )
-
-          if (!propsReferenceIds) {
-            return
-          }
-
-          if (isPropsMemberAccessed(node, propsReferenceIds)) {
-            report(node, 'getProperty', propsReferenceIds.scopeName)
-          }
-        },
-        /**
-         * @param {VariableDeclarator} node
-         */
-        VariableDeclarator(node) {
-          if (!scopeStack) {
-            return
-          }
-          const propsReferenceIds = setupScopePropsReferenceIds.get(
-            scopeStack.scopeNode
-          )
-          if (!propsReferenceIds) {
-            return
-          }
-          verify(node.id, node.init, propsReferenceIds)
-        },
-        /**
-         * @param {AssignmentExpression} node
-         */
-        AssignmentExpression(node) {
-          if (!scopeStack) {
-            return
-          }
-          const propsReferenceIds = setupScopePropsReferenceIds.get(
-            scopeStack.scopeNode
-          )
-          if (!propsReferenceIds) {
-            return
-          }
-          verify(node.left, node.right, propsReferenceIds)
-        }
-      },
-      utils.defineScriptSetupVisitor(context, {
-        onDefinePropsEnter(node) {
-          let target = node
-          if (
-            target.parent &&
-            target.parent.type === 'CallExpression' &&
-            target.parent.arguments[0] === target &&
-            target.parent.callee.type === 'Identifier' &&
-            target.parent.callee.name === 'withDefaults'
-          ) {
-            target = target.parent
-          }
-          if (!target.parent) {
-            return
-          }
-
-          /** @type {Pattern|null} */
-          let id = null
-          if (target.parent.type === 'VariableDeclarator') {
-            id = target.parent.init === target ? target.parent.id : null
-          } else if (target.parent.type === 'AssignmentExpression') {
-            id = target.parent.right === target ? target.parent.left : null
-          }
-          processPattern(id, context.getSourceCode().ast, '<script setup>')
-        }
-      }),
-      utils.defineVueVisitor(context, {
-        onSetupFunctionEnter(node) {
-          const propsParam = utils.skipDefaultParamValue(node.params[0])
-          processPattern(propsParam, node, 'setup()')
-        }
-      })
-    )
+    return baseRule.create(context)
   }
 }
diff --git a/lib/rules/no-setup-props-reactivity-loss.js b/lib/rules/no-setup-props-reactivity-loss.js
new file mode 100644
index 00000000..1292ace7
--- /dev/null
+++ b/lib/rules/no-setup-props-reactivity-loss.js
@@ -0,0 +1,272 @@
+/**
+ * @author Yosuke Ota
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+const { findVariable } = require('@eslint-community/eslint-utils')
+const utils = require('../utils')
+
+module.exports = {
+  meta: {
+    type: 'suggestion',
+    docs: {
+      description:
+        'disallow usages that lose the reactivity of `props` passed to `setup`',
+      categories: undefined,
+      url: 'https://eslint.vuejs.org/rules/no-setup-props-reactivity-loss.html'
+    },
+    fixable: null,
+    schema: [],
+    messages: {
+      destructuring:
+        'Destructuring the `props` will cause the value to lose reactivity.',
+      getProperty:
+        'Getting a value from the `props` in root scope of `{{scopeName}}` will cause the value to lose reactivity.'
+    }
+  },
+  /**
+   * @param {RuleContext} context
+   * @returns {RuleListener}
+   **/
+  create(context) {
+    /**
+     * @typedef {object} ScopePropsReferences
+     * @property {Set<Identifier>} refs
+     * @property {string} scopeName
+     */
+    /** @type {Map<FunctionDeclaration | FunctionExpression | ArrowFunctionExpression | Program, ScopePropsReferences>} */
+    const setupScopePropsReferenceIds = new Map()
+    const wrapperExpressionTypes = new Set([
+      'ArrayExpression',
+      'ObjectExpression'
+    ])
+
+    /**
+     * @param {ESNode} node
+     * @param {string} messageId
+     * @param {string} scopeName
+     */
+    function report(node, messageId, scopeName) {
+      context.report({
+        node,
+        messageId,
+        data: {
+          scopeName
+        }
+      })
+    }
+
+    /**
+     * @param {Pattern} left
+     * @param {Expression | null} right
+     * @param {ScopePropsReferences} propsReferences
+     */
+    function verify(left, right, propsReferences) {
+      if (!right) {
+        return
+      }
+
+      const rightNode = utils.skipChainExpression(right)
+
+      if (
+        wrapperExpressionTypes.has(rightNode.type) &&
+        isPropsMemberAccessed(rightNode, propsReferences)
+      ) {
+        return report(rightNode, 'getProperty', propsReferences.scopeName)
+      }
+
+      if (
+        left.type !== 'ArrayPattern' &&
+        left.type !== 'ObjectPattern' &&
+        rightNode.type !== 'MemberExpression'
+      ) {
+        return
+      }
+
+      /** @type {Expression | Super} */
+      let rightId = rightNode
+      while (rightId.type === 'MemberExpression') {
+        rightId = utils.skipChainExpression(rightId.object)
+      }
+      if (rightId.type === 'Identifier' && propsReferences.refs.has(rightId)) {
+        report(left, 'getProperty', propsReferences.scopeName)
+      }
+    }
+
+    /**
+     * @param {Expression} node
+     * @param {ScopePropsReferences} propsReferences
+     */
+    function isPropsMemberAccessed(node, propsReferences) {
+      const propRefs = [...propsReferences.refs.values()]
+
+      return propRefs.some((props) => {
+        const isPropsInExpressionRange = utils.inRange(node.range, props)
+        const isPropsMemberExpression =
+          props.parent.type === 'MemberExpression' &&
+          props.parent.object === props
+
+        return isPropsInExpressionRange && isPropsMemberExpression
+      })
+    }
+
+    /**
+     * @typedef {object} ScopeStack
+     * @property {ScopeStack | null} upper
+     * @property {FunctionDeclaration | FunctionExpression | ArrowFunctionExpression | Program} scopeNode
+     */
+    /**
+     * @type {ScopeStack | null}
+     */
+    let scopeStack = null
+
+    /**
+     * @param {Pattern | null} node
+     * @param {FunctionDeclaration | FunctionExpression | ArrowFunctionExpression | Program} scopeNode
+     * @param {string} scopeName
+     */
+    function processPattern(node, scopeNode, scopeName) {
+      if (!node) {
+        // no arguments
+        return
+      }
+      if (
+        node.type === 'RestElement' ||
+        node.type === 'AssignmentPattern' ||
+        node.type === 'MemberExpression'
+      ) {
+        // cannot check
+        return
+      }
+      if (node.type === 'ArrayPattern' || node.type === 'ObjectPattern') {
+        report(node, 'destructuring', scopeName)
+        return
+      }
+
+      const variable = findVariable(context.getScope(), node)
+      if (!variable) {
+        return
+      }
+      const propsReferenceIds = new Set()
+      for (const reference of variable.references) {
+        // If reference is in another scope, we can't check it.
+        if (reference.from !== context.getScope()) {
+          continue
+        }
+
+        if (!reference.isRead()) {
+          continue
+        }
+
+        propsReferenceIds.add(reference.identifier)
+      }
+      setupScopePropsReferenceIds.set(scopeNode, {
+        refs: propsReferenceIds,
+        scopeName
+      })
+    }
+    return utils.compositingVisitors(
+      {
+        /**
+         * @param {FunctionExpression | FunctionDeclaration | ArrowFunctionExpression | Program} node
+         */
+        'Program, :function'(node) {
+          scopeStack = {
+            upper: scopeStack,
+            scopeNode: node
+          }
+        },
+        /**
+         * @param {FunctionExpression | FunctionDeclaration | ArrowFunctionExpression | Program} node
+         */
+        'Program, :function:exit'(node) {
+          scopeStack = scopeStack && scopeStack.upper
+
+          setupScopePropsReferenceIds.delete(node)
+        },
+        /**
+         * @param {CallExpression} node
+         */
+        CallExpression(node) {
+          if (!scopeStack) {
+            return
+          }
+
+          const propsReferenceIds = setupScopePropsReferenceIds.get(
+            scopeStack.scopeNode
+          )
+
+          if (!propsReferenceIds) {
+            return
+          }
+
+          if (isPropsMemberAccessed(node, propsReferenceIds)) {
+            report(node, 'getProperty', propsReferenceIds.scopeName)
+          }
+        },
+        /**
+         * @param {VariableDeclarator} node
+         */
+        VariableDeclarator(node) {
+          if (!scopeStack) {
+            return
+          }
+          const propsReferenceIds = setupScopePropsReferenceIds.get(
+            scopeStack.scopeNode
+          )
+          if (!propsReferenceIds) {
+            return
+          }
+          verify(node.id, node.init, propsReferenceIds)
+        },
+        /**
+         * @param {AssignmentExpression} node
+         */
+        AssignmentExpression(node) {
+          if (!scopeStack) {
+            return
+          }
+          const propsReferenceIds = setupScopePropsReferenceIds.get(
+            scopeStack.scopeNode
+          )
+          if (!propsReferenceIds) {
+            return
+          }
+          verify(node.left, node.right, propsReferenceIds)
+        }
+      },
+      utils.defineScriptSetupVisitor(context, {
+        onDefinePropsEnter(node) {
+          let target = node
+          if (
+            target.parent &&
+            target.parent.type === 'CallExpression' &&
+            target.parent.arguments[0] === target &&
+            target.parent.callee.type === 'Identifier' &&
+            target.parent.callee.name === 'withDefaults'
+          ) {
+            target = target.parent
+          }
+          if (!target.parent) {
+            return
+          }
+
+          /** @type {Pattern|null} */
+          let id = null
+          if (target.parent.type === 'VariableDeclarator') {
+            id = target.parent.init === target ? target.parent.id : null
+          } else if (target.parent.type === 'AssignmentExpression') {
+            id = target.parent.right === target ? target.parent.left : null
+          }
+          processPattern(id, context.getSourceCode().ast, '<script setup>')
+        }
+      }),
+      utils.defineVueVisitor(context, {
+        onSetupFunctionEnter(node) {
+          const propsParam = utils.skipDefaultParamValue(node.params[0])
+          processPattern(propsParam, node, 'setup()')
+        }
+      })
+    )
+  }
+}


Test patch:
diff --git a/tests/lib/rules/no-deprecated-model-definition.js b/tests/lib/rules/no-deprecated-model-definition.js
index 908be654..ebdcb859 100644
--- a/tests/lib/rules/no-deprecated-model-definition.js
+++ b/tests/lib/rules/no-deprecated-model-definition.js
@@ -52,6 +52,20 @@ tester.run('no-deprecated-model-definition', rule, {
         </script>
       `,
       options: [{ allowVue3Compat: true }]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <script>
+        export default defineComponent({
+          model: {
+            prop: \`model-value\`,
+            event: \`update:model-value\`
+          }
+        })
+        </script>
+      `,
+      options: [{ allowVue3Compat: true }]
     }
   ],
   invalid: [
@@ -320,6 +334,58 @@ tester.run('no-deprecated-model-definition', rule, {
           ]
         }
       ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <script>
+        export default defineComponent({
+          model: {
+            prop: \`foo-bar\`,
+            event: \`update:foo-bar\`
+          }
+        })
+        </script>
+      `,
+      options: [{ allowVue3Compat: true }],
+      errors: [
+        {
+          message:
+            '`model` definition is deprecated. You may use the Vue 3-compatible `modelValue`/`update:modelValue` though.',
+          line: 4,
+          column: 11,
+          endLine: 7,
+          endColumn: 12,
+          suggestions: [
+            {
+              desc: 'Change to `modelValue`/`update:modelValue`.',
+              output: `
+        <script>
+        export default defineComponent({
+          model: {
+            prop: \`modelValue\`,
+            event: \`update:modelValue\`
+          }
+        })
+        </script>
+      `
+            },
+            {
+              desc: 'Change to `model-value`/`update:model-value`.',
+              output: `
+        <script>
+        export default defineComponent({
+          model: {
+            prop: \`model-value\`,
+            event: \`update:model-value\`
+          }
+        })
+        </script>
+      `
+            }
+          ]
+        }
+      ]
     }
   ]
 })
diff --git a/tests/lib/rules/no-setup-props-reactivity-loss.js b/tests/lib/rules/no-setup-props-reactivity-loss.js
new file mode 100644
index 00000000..2e248063
--- /dev/null
+++ b/tests/lib/rules/no-setup-props-reactivity-loss.js
@@ -0,0 +1,682 @@
+/**
+ * @author Yosuke Ota
+ */
+'use strict'
+
+const RuleTester = require('eslint').RuleTester
+const rule = require('../../../lib/rules/no-setup-props-reactivity-loss')
+
+const tester = new RuleTester({
+  parser: require.resolve('vue-eslint-parser'),
+  parserOptions: { ecmaVersion: 2020, sourceType: 'module' }
+})
+
+tester.run('no-setup-props-reactivity-loss', rule, {
+  valid: [
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      export default {
+        setup(props) {
+          watch(() => {
+            console.log(props.count) // ok
+          })
+
+          return () => {
+            return h('div', props.count) // ok
+          }
+        }
+      }
+      </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      export default {
+        setup(props) {
+          watch(() => {
+            const { count } = props // ok
+            console.log(count)
+          })
+
+          return () => {
+            const { count } = props // ok
+            return h('div', count)
+          }
+        }
+      }
+      </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      export default {
+        _setup({count}) {
+        }
+      }
+      </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      export default {
+        setup() {
+        }
+      }
+      </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      export default {
+        setup(...args) {
+        }
+      }
+      </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      export default {
+        setup(props) {
+          watch(() => {
+            ({ count } = props)
+          })
+        }
+      }
+      </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      var noVue = {
+        setup(props) {
+          const { count } = props
+        }
+      }
+      </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      export default {
+        setup(props) {
+          const {x} = noProps
+          ({y} = noProps)
+          const z = noProps.z
+        }
+      }
+      </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      export default {
+        setup(props) {
+          ({props} = x)
+        }
+      }
+      </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      export default {
+        watch: {
+          setup({val}) { }
+        }
+      }
+      </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      export default {
+        setup(props) {
+          const props2 = props
+        }
+      }
+      </script>
+      `
+    },
+    `
+      Vue.component('test', {
+        el: a = b
+      })
+    `,
+    {
+      filename: 'test.vue',
+      code: `
+      <script setup>
+      const props = defineProps({count:Number})
+      watch(() => {
+        const {count} = props
+      })
+      watch(() => {
+        const count = props.count
+      })
+      </script>
+      `,
+      errors: [
+        {
+          messageId: 'getProperty',
+          line: 4
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script setup>
+      const props = defineProps({count:Number})
+      watch(() => {
+        ({ count } = props)
+      })
+      watch(() => {
+        count = props.count
+      })
+      </script>
+      `,
+      errors: [
+        {
+          messageId: 'getProperty',
+          line: 4
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      export default {
+        setup: (props) => {
+          const count = computed(() => props.count)
+        }
+      }
+      </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script setup>
+      const props = defineProps({ count: Number })
+      const count = computed(() => props.count)
+      </script>
+      `
+    }
+  ],
+  invalid: [
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      export default {
+        setup({ count }) { // error
+          watch(() => {
+            console.log(count) // not going to detect changes
+          })
+
+          return () => {
+            return h('div', count) // not going to update
+          }
+        }
+      }
+      </script>
+      `,
+      errors: [
+        {
+          messageId: 'destructuring',
+          line: 4,
+          column: 15,
+          endLine: 4,
+          endColumn: 24
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      export default {
+        setup(props) {
+          const { count } = props // error
+        }
+      }
+      </script>
+      `,
+      errors: [
+        {
+          messageId: 'getProperty',
+          line: 5,
+          column: 17,
+          endLine: 5,
+          endColumn: 26
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      export default {
+        setup: (props) => {
+          const { count } = props
+        }
+      }
+      </script>
+      `,
+      errors: [
+        {
+          messageId: 'getProperty',
+          line: 5
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      export default {
+        setup(props) {
+          ({ count } = props)
+        }
+      }
+      </script>
+      `,
+      errors: [
+        {
+          messageId: 'getProperty',
+          line: 5
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      export default {
+        setup(p) {
+          const { count } = p
+        }
+      }
+
+      Vue.component('component', {
+        setup(p) {
+          const { count } = p
+        }
+      })
+      </script>
+      `,
+      errors: [
+        {
+          messageId: 'getProperty',
+          line: 5
+        },
+        {
+          messageId: 'getProperty',
+          line: 11
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      export default {
+        setup(p) {
+          const { count } = p
+        },
+        _setup(p) {
+          const { count } = p
+        },
+      }
+      </script>
+      `,
+      errors: [
+        {
+          messageId: 'getProperty',
+          line: 5
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      export default {
+        setup(p) {
+          const { x } = p
+          const { y } = p
+        }
+      }
+      </script>
+      `,
+      errors: [
+        {
+          messageId: 'getProperty',
+          line: 5
+        },
+        {
+          messageId: 'getProperty',
+          line: 6
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      export default {
+        setup(p) {
+          const foo = p.bar
+        }
+      }
+      </script>
+      `,
+      errors: [
+        {
+          messageId: 'getProperty',
+          line: 5
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      export default {
+        setup(p) {
+          const x = p.foo
+          const y = p?.bar
+          const z = (p?.baz).qux
+
+          const xc = p?.foo?.()
+          const yc = (p?.bar)?.()
+          const zc = (p?.baz.qux)?.()
+        }
+      }
+      </script>
+      `,
+      errors: [
+        {
+          messageId: 'getProperty',
+          line: 5
+        },
+        {
+          messageId: 'getProperty',
+          line: 6
+        },
+        {
+          messageId: 'getProperty',
+          line: 7
+        },
+        {
+          messageId: 'getProperty',
+          line: 9
+        },
+        {
+          messageId: 'getProperty',
+          line: 10
+        },
+        {
+          messageId: 'getProperty',
+          line: 11
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      export default {
+        setup(p) {
+          let foo
+          foo = p.bar
+        }
+      }
+      </script>
+      `,
+      errors: [
+        {
+          messageId: 'getProperty',
+          line: 6
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      export default {
+        setup(p) {
+          const {foo} = p.bar
+        }
+      }
+      </script>
+      `,
+      errors: [
+        {
+          messageId: 'getProperty',
+          line: 5
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      export default {
+        setup(p) {
+          foo.bar = p.bar
+        }
+      }
+      </script>
+      `,
+      errors: [
+        {
+          messageId: 'getProperty',
+          line: 5
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script setup>
+      const {count} = defineProps({count:Number})
+      </script>
+      `,
+      errors: [
+        {
+          message:
+            'Destructuring the `props` will cause the value to lose reactivity.',
+          line: 3
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script setup>
+      const props = defineProps({count:Number})
+      const {count} = props
+      ;({count} = props)
+      </script>
+      `,
+      errors: [
+        {
+          message:
+            'Getting a value from the `props` in root scope of `<script setup>` will cause the value to lose reactivity.',
+          line: 4
+        },
+        {
+          message:
+            'Getting a value from the `props` in root scope of `<script setup>` will cause the value to lose reactivity.',
+          line: 5
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script setup>
+      const props = defineProps({count:Number})
+      const count = props.count
+      count = props.count
+      </script>
+      `,
+      errors: [
+        {
+          messageId: 'getProperty',
+          line: 4
+        },
+        {
+          messageId: 'getProperty',
+          line: 5
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      export default {
+        setup: (props) => {
+          const count = ref(props.count)
+          count = fn(props.count)
+        }
+      }
+      </script>
+      `,
+      errors: [
+        {
+          messageId: 'getProperty',
+          line: 5
+        },
+        {
+          messageId: 'getProperty',
+          line: 6
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script setup>
+      const props = defineProps({ count: Number })
+      const count = ref(props.count)
+      count = fn(props.count)
+      </script>
+      `,
+      errors: [
+        {
+          messageId: 'getProperty',
+          line: 4
+        },
+        {
+          messageId: 'getProperty',
+          line: 5
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script setup>
+      const props = defineProps({ count: Number })
+      const newProps = ref({ count: props.count })
+      </script>
+      `,
+      errors: [
+        {
+          messageId: 'getProperty',
+          line: 4
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script setup>
+      const props = defineProps({ count: Number })
+      const counts = [props.count]
+      </script>
+      `,
+      errors: [
+        {
+          messageId: 'getProperty',
+          line: 4
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script setup>
+      const props = defineProps({ count: Number })
+      const counter = { count: props.count }
+      </script>
+      `,
+      errors: [
+        {
+          messageId: 'getProperty',
+          line: 4
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script setup>
+      const props = defineProps({ count: Number })
+      const counters = [{ count: [props.count] }]
+      </script>
+      `,
+      errors: [
+        {
+          messageId: 'getProperty',
+          line: 4
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script setup>
+      const props = defineProps({ count: Number })
+      const buildCounter = (count) => ({ count })
+
+      buildCounter(props.count)
+      </script>
+      `,
+      errors: [
+        {
+          messageId: 'getProperty',
+          line: 6
+        }
+      ]
+    }
+  ]
+})