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

Issue:
Memory leak in the "no-unsupported-features" rule
<!--
  ❗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:** 7.32.0
- **eslint-plugin-vue version:** 7.20.0
- **Node version:** 14.16.0
- **Operating System:** macOS Catalina 10.15.7

Some additional details:
- Our project is relatively big – around 3000 `*.vue` files and around 2000 of `*.js` files with `// @vue/component` comment

**Please show your full configuration:**
<!-- Paste content of your .eslintrc file -->
```json5
{
    "root": true,
    "env": { "es2021": true },
    "plugins": ["vue"],
    "extends": ["plugin:vue/recommended"],
    "rules": {
        "vue/no-unsupported-features": ["error", { "version": "^2.6.14" }]
    }
}
```
*Note this is not full `.eslintrc` file, but just relevant part with the configured rule in question*

**What did you do?**	
<!-- Please include the actual source code causing the issue. -->
*No code here because this issue can be reproduced with both valid and invalid code.*

**What did you expect to happen?**
Memory consumption does not significantly change when `no-unsupported-features` rule is enabled.

**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.
-->
*ESLint output is not relevant here, as we need to look into "Activity Monitor" instead.*

With the rule enabled, during the lint process (which we run as `eslint --ext .js,.vue .`) RAM consumption keeps raising and peaks at to 3.5G-4Gb level. When we disable the rule – it doesn't go more than 800-900Mb. I tried to disable other rules, and it seems this is the one which causes the issue.

I tried to debug it myself, and I narrowed it down to [this line](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/utils/style-variables/index.js#L35-L36):
```ts
/** @type {Map<VElement, StyleVariablesContext>} */
const cache = new Map()
```
This file is used in the [`style-css-vars-injection` syntax spec](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/syntaxes/style-css-vars-injection.js#L7), which is part of the [`no-unsupported-features` rule](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-unsupported-features.js#L28).
From my understanding, this `cache` is created globally per entire lint process, while it would be populated with values from each of the scanned files. This cache uses `VElement` objects as a key, which makes them "trapped" in this cache for the duration of the entire lint process. As a result Node.js cannot garbage collect those references when scan for individual file is done.

Simple change to `WeakMap` seems to be solving the problem:
```ts
/** @type {WeakMap<VElement, StyleVariablesContext>} */
const cache = new WeakMap()
```

Btw during my experiments, I noticed that [part of that code that reads from the `cache`](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/utils/style-variables/index.js#L55-L58) is **never executed**. I'm not sure what is the use case for this cache implementation, but for now it seems redundant for me and it could be removed altogether.

**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.
-->
I'm not sure if you still need it, but I can add it if you confirm.


Pull request:
Fix memory leak
close #1717

Patch:
diff --git a/.circleci/config.yml b/.circleci/config.yml
index 45637630..e695f422 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -25,7 +25,7 @@ jobs:
       #       - v2-npm-lock-{{ .Branch }}-{{ .Environment.CIRCLE_JOB }}-{{ checksum "package.json" }}
       - run:
           name: Install dependencies
-          command: npm install
+          command: npm install --legacy-peer-deps
       - run:
           name: Test
           command: npm test
diff --git a/docs/rules/README.md b/docs/rules/README.md
index 684731a9..5794cc3e 100644
--- a/docs/rules/README.md
+++ b/docs/rules/README.md
@@ -324,6 +324,7 @@ For example:
 | [vue/no-child-content](./no-child-content.md) | disallow element's child contents which would be overwritten by a directive like `v-html` or `v-text` | :bulb: |
 | [vue/no-duplicate-attr-inheritance](./no-duplicate-attr-inheritance.md) | enforce `inheritAttrs` to be set to `false` when using `v-bind="$attrs"` |  |
 | [vue/no-empty-component-block](./no-empty-component-block.md) | disallow the `<template>` `<script>` `<style>` block to be empty |  |
+| [vue/no-expose-after-await](./no-expose-after-await.md) | disallow asynchronously registered `expose` |  |
 | [vue/no-invalid-model-keys](./no-invalid-model-keys.md) | require valid keys in model option |  |
 | [vue/no-multiple-objects-in-class](./no-multiple-objects-in-class.md) | disallow to pass multiple objects into array to class |  |
 | [vue/no-potential-component-option-typo](./no-potential-component-option-typo.md) | disallow a potential typo in your component property | :bulb: |
diff --git a/docs/rules/no-expose-after-await.md b/docs/rules/no-expose-after-await.md
new file mode 100644
index 00000000..9b7bc8e3
--- /dev/null
+++ b/docs/rules/no-expose-after-await.md
@@ -0,0 +1,51 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/no-expose-after-await
+description: disallow asynchronously registered `expose`
+---
+# vue/no-expose-after-await
+
+> disallow asynchronously registered `expose`
+
+- :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 `expose()` after `await` expression.  
+In `setup()` function, `expose()` should be registered synchronously.
+
+<eslint-code-block :rules="{'vue/no-expose-after-await': ['error']}">
+
+```vue
+<script>
+import { watch } from 'vue'
+export default {
+  async setup(props, { expose }) {
+    /* ✓ GOOD */
+    expose({/* ... */})
+
+    await doSomething()
+
+    /* ✗ BAD */
+    expose({/* ... */})
+  }
+}
+</script>
+```
+
+</eslint-code-block>
+
+## :wrench: Options
+
+Nothing.
+
+## :books: Further Reading
+
+- [Vue RFCs - 0042-expose-api](https://github.com/vuejs/rfcs/blob/master/active-rfcs/0042-expose-api.md)
+- [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-expose-after-await.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-expose-after-await.js)
diff --git a/lib/index.js b/lib/index.js
index 437ed3e4..c9969917 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -89,6 +89,7 @@ module.exports = {
     'no-empty-component-block': require('./rules/no-empty-component-block'),
     'no-empty-pattern': require('./rules/no-empty-pattern'),
     'no-export-in-script-setup': require('./rules/no-export-in-script-setup'),
+    'no-expose-after-await': require('./rules/no-expose-after-await'),
     'no-extra-parens': require('./rules/no-extra-parens'),
     'no-invalid-model-keys': require('./rules/no-invalid-model-keys'),
     'no-irregular-whitespace': require('./rules/no-irregular-whitespace'),
diff --git a/lib/rules/no-export-in-script-setup.js b/lib/rules/no-export-in-script-setup.js
index b39231df..99b6ba11 100644
--- a/lib/rules/no-export-in-script-setup.js
+++ b/lib/rules/no-export-in-script-setup.js
@@ -6,6 +6,12 @@
 
 const utils = require('../utils')
 
+/**
+ * @typedef {import('@typescript-eslint/types').TSESTree.ExportAllDeclaration} TSESTreeExportAllDeclaration
+ * @typedef {import('@typescript-eslint/types').TSESTree.ExportDefaultDeclaration} TSESTreeExportDefaultDeclaration
+ * @typedef {import('@typescript-eslint/types').TSESTree.ExportNamedDeclaration} TSESTreeExportNamedDeclaration
+ */
+
 module.exports = {
   meta: {
     type: 'problem',
@@ -23,7 +29,22 @@ module.exports = {
   /** @param {RuleContext} context */
   create(context) {
     /** @param {ExportAllDeclaration | ExportDefaultDeclaration | ExportNamedDeclaration} node */
-    function report(node) {
+    function verify(node) {
+      const tsNode =
+        /** @type {TSESTreeExportAllDeclaration | TSESTreeExportDefaultDeclaration | TSESTreeExportNamedDeclaration} */ (
+          node
+        )
+      if (tsNode.exportKind === 'type') {
+        return
+      }
+      if (tsNode.type === 'ExportNamedDeclaration') {
+        if (
+          tsNode.specifiers.length > 0 &&
+          tsNode.specifiers.every((spec) => spec.exportKind === 'type')
+        ) {
+          return
+        }
+      }
       context.report({
         node,
         messageId: 'forbidden'
@@ -31,9 +52,9 @@ module.exports = {
     }
 
     return utils.defineScriptSetupVisitor(context, {
-      ExportAllDeclaration: report,
-      ExportDefaultDeclaration: report,
-      ExportNamedDeclaration: report
+      ExportAllDeclaration: verify,
+      ExportDefaultDeclaration: verify,
+      ExportNamedDeclaration: verify
     })
   }
 }
diff --git a/lib/rules/no-expose-after-await.js b/lib/rules/no-expose-after-await.js
new file mode 100644
index 00000000..e1e3104d
--- /dev/null
+++ b/lib/rules/no-expose-after-await.js
@@ -0,0 +1,202 @@
+/**
+ * @author Yosuke Ota
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const { findVariable } = require('eslint-utils')
+const utils = require('../utils')
+
+// ------------------------------------------------------------------------------
+// Helpers
+// ------------------------------------------------------------------------------
+
+/**
+ * Get the callee member node from the given CallExpression
+ * @param {CallExpression} node CallExpression
+ */
+function getCalleeMemberNode(node) {
+  const callee = utils.skipChainExpression(node.callee)
+
+  if (callee.type === 'MemberExpression') {
+    const name = utils.getStaticPropertyName(callee)
+    if (name) {
+      return { name, member: callee }
+    }
+  }
+  return null
+}
+
+// ------------------------------------------------------------------------------
+// Rule Definition
+// ------------------------------------------------------------------------------
+
+module.exports = {
+  meta: {
+    type: 'problem',
+    docs: {
+      description: 'disallow asynchronously registered `expose`',
+      categories: undefined,
+      // categories: ['vue3-essential'], TODO Change with the major version
+      url: 'https://eslint.vuejs.org/rules/no-expose-after-await.html'
+    },
+    fixable: null,
+    schema: [],
+    messages: {
+      forbidden: 'The `expose` after `await` expression are forbidden.'
+    }
+  },
+  /** @param {RuleContext} context */
+  create(context) {
+    /**
+     * @typedef {object} SetupScopeData
+     * @property {boolean} afterAwait
+     * @property {[number,number]} range
+     * @property {Set<Identifier>} exposeReferenceIds
+     * @property {Set<Identifier>} contextReferenceIds
+     */
+    /**
+     * @typedef {object} ScopeStack
+     * @property {ScopeStack | null} upper
+     * @property {FunctionDeclaration | FunctionExpression | ArrowFunctionExpression} scopeNode
+     */
+    /** @type {Map<FunctionDeclaration | FunctionExpression | ArrowFunctionExpression, SetupScopeData>} */
+    const setupScopes = new Map()
+
+    /** @type {ScopeStack | null} */
+    let scopeStack = null
+
+    return utils.defineVueVisitor(context, {
+      onSetupFunctionEnter(node) {
+        const contextParam = node.params[1]
+        if (!contextParam) {
+          // no arguments
+          return
+        }
+        if (contextParam.type === 'RestElement') {
+          // cannot check
+          return
+        }
+        if (contextParam.type === 'ArrayPattern') {
+          // cannot check
+          return
+        }
+        /** @type {Set<Identifier>} */
+        const contextReferenceIds = new Set()
+        /** @type {Set<Identifier>} */
+        const exposeReferenceIds = new Set()
+        if (contextParam.type === 'ObjectPattern') {
+          const exposeProperty = utils.findAssignmentProperty(
+            contextParam,
+            'expose'
+          )
+          if (!exposeProperty) {
+            return
+          }
+          const exposeParam = exposeProperty.value
+          // `setup(props, {emit})`
+          const variable =
+            exposeParam.type === 'Identifier'
+              ? findVariable(context.getScope(), exposeParam)
+              : null
+          if (!variable) {
+            return
+          }
+          for (const reference of variable.references) {
+            if (!reference.isRead()) {
+              continue
+            }
+            exposeReferenceIds.add(reference.identifier)
+          }
+        } else if (contextParam.type === 'Identifier') {
+          // `setup(props, context)`
+          const variable = findVariable(context.getScope(), contextParam)
+          if (!variable) {
+            return
+          }
+          for (const reference of variable.references) {
+            if (!reference.isRead()) {
+              continue
+            }
+            contextReferenceIds.add(reference.identifier)
+          }
+        }
+        setupScopes.set(node, {
+          afterAwait: false,
+          range: node.range,
+          exposeReferenceIds,
+          contextReferenceIds
+        })
+      },
+      /**
+       * @param {FunctionExpression | FunctionDeclaration | ArrowFunctionExpression} node
+       */
+      ':function'(node) {
+        scopeStack = {
+          upper: scopeStack,
+          scopeNode: node
+        }
+      },
+      ':function:exit'() {
+        scopeStack = scopeStack && scopeStack.upper
+      },
+      /** @param {AwaitExpression} node */
+      AwaitExpression(node) {
+        if (!scopeStack) {
+          return
+        }
+        const setupScope = setupScopes.get(scopeStack.scopeNode)
+        if (!setupScope || !utils.inRange(setupScope.range, node)) {
+          return
+        }
+        setupScope.afterAwait = true
+      },
+      /** @param {CallExpression} node */
+      CallExpression(node) {
+        if (!scopeStack) {
+          return
+        }
+        const setupScope = setupScopes.get(scopeStack.scopeNode)
+        if (
+          !setupScope ||
+          !setupScope.afterAwait ||
+          !utils.inRange(setupScope.range, node)
+        ) {
+          return
+        }
+        const { contextReferenceIds, exposeReferenceIds } = setupScope
+        if (
+          node.callee.type === 'Identifier' &&
+          exposeReferenceIds.has(node.callee)
+        ) {
+          // setup(props,{expose}) {expose()}
+          context.report({
+            node,
+            messageId: 'forbidden'
+          })
+        } else {
+          const expose = getCalleeMemberNode(node)
+          if (
+            expose &&
+            expose.name === 'expose' &&
+            expose.member.object.type === 'Identifier' &&
+            contextReferenceIds.has(expose.member.object)
+          ) {
+            // setup(props,context) {context.emit()}
+            context.report({
+              node,
+              messageId: 'forbidden'
+            })
+          }
+        }
+      },
+      onSetupFunctionExit(node) {
+        setupScopes.delete(node)
+      }
+    })
+  }
+}
diff --git a/lib/rules/no-mutating-props.js b/lib/rules/no-mutating-props.js
index 70559026..93855fd4 100644
--- a/lib/rules/no-mutating-props.js
+++ b/lib/rules/no-mutating-props.js
@@ -11,6 +11,34 @@ const { findVariable } = require('eslint-utils')
 // Rule Definition
 // ------------------------------------------------------------------------------
 
+// https://github.com/vuejs/vue-next/blob/7c11c58faf8840ab97b6449c98da0296a60dddd8/packages/shared/src/globalsWhitelist.ts
+const GLOBALS_WHITE_LISTED = new Set([
+  'Infinity',
+  'undefined',
+  'NaN',
+  'isFinite',
+  'isNaN',
+  'parseFloat',
+  'parseInt',
+  'decodeURI',
+  'decodeURIComponent',
+  'encodeURI',
+  'encodeURIComponent',
+  'Math',
+  'Number',
+  'Date',
+  'Array',
+  'Object',
+  'Boolean',
+  'String',
+  'RegExp',
+  'Map',
+  'Set',
+  'JSON',
+  'Intl',
+  'BigInt'
+])
+
 module.exports = {
   meta: {
     type: 'suggestion',
@@ -191,12 +219,43 @@ module.exports = {
       }
     }
 
+    function* extractDefineVariableNames() {
+      const globalScope = context.getSourceCode().scopeManager.globalScope
+      if (globalScope) {
+        for (const variable of globalScope.variables) {
+          if (variable.defs.length) {
+            yield variable.name
+          }
+        }
+        const moduleScope = globalScope.childScopes.find(
+          (scope) => scope.type === 'module'
+        )
+        for (const variable of (moduleScope && moduleScope.variables) || []) {
+          if (variable.defs.length) {
+            yield variable.name
+          }
+        }
+      }
+    }
+
     return utils.compositingVisitors(
       {},
       utils.defineScriptSetupVisitor(context, {
         onDefinePropsEnter(node, props) {
+          const defineVariableNames = new Set(extractDefineVariableNames())
+
           const propsSet = new Set(
-            props.map((p) => p.propName).filter(utils.isDef)
+            props
+              .map((p) => p.propName)
+              .filter(
+                /**
+                 * @returns {propName is string}
+                 */
+                (propName) =>
+                  utils.isDef(propName) &&
+                  !GLOBALS_WHITE_LISTED.has(propName) &&
+                  !defineVariableNames.has(propName)
+              )
           )
           propsMap.set(node, propsSet)
           vueObjectData = {
@@ -337,12 +396,22 @@ module.exports = {
           }
         },
         /** @param {ESNode} node */
-        "VAttribute[directive=true][key.name.name='model'] VExpressionContainer > *"(
+        "VAttribute[directive=true]:matches([key.name.name='model'], [key.name.name='bind']) VExpressionContainer > *"(
           node
         ) {
           if (!vueObjectData) {
             return
           }
+          let attr = node.parent
+          while (attr && attr.type !== 'VAttribute') {
+            attr = attr.parent
+          }
+          if (attr && attr.directive && attr.key.name.name === 'bind') {
+            if (!attr.key.modifiers.some((mod) => mod.name === 'sync')) {
+              return
+            }
+          }
+
           const nodes = utils.getMemberChaining(node)
           const first = nodes[0]
           let name
diff --git a/lib/utils/style-variables/index.js b/lib/utils/style-variables/index.js
index 3127166a..eca2f39d 100644
--- a/lib/utils/style-variables/index.js
+++ b/lib/utils/style-variables/index.js
@@ -32,8 +32,8 @@ module.exports = {
   StyleVariablesContext
 }
 
-/** @type {Map<VElement, StyleVariablesContext>} */
-const cache = new Map()
+/** @type {WeakMap<VElement, StyleVariablesContext>} */
+const cache = new WeakMap()
 /**
  * Get the style vars context
  * @param {RuleContext} context
diff --git a/package.json b/package.json
index c2eefdb3..f3abcbcd 100644
--- a/package.json
+++ b/package.json
@@ -65,7 +65,7 @@
     "@types/natural-compare": "^1.4.0",
     "@types/node": "^13.13.5",
     "@types/semver": "^7.2.0",
-    "@typescript-eslint/parser": "^5.0.0-0",
+    "@typescript-eslint/parser": "^5.4.0",
     "@vuepress/plugin-pwa": "^1.4.1",
     "acorn": "^8.5.0",
     "env-cmd": "^10.1.0",
@@ -82,7 +82,7 @@
     "mocha": "^7.1.2",
     "nyc": "^15.1.0",
     "prettier": "^2.4.1",
-    "typescript": "^4.4.3",
+    "typescript": "^4.5.0-0",
     "vue-eslint-editor": "^1.1.0",
     "vuepress": "^1.8.2"
   }


Test patch:
diff --git a/tests/lib/rules/no-export-in-script-setup.js b/tests/lib/rules/no-export-in-script-setup.js
index 5bc759f4..3ea91540 100644
--- a/tests/lib/rules/no-export-in-script-setup.js
+++ b/tests/lib/rules/no-export-in-script-setup.js
@@ -8,6 +8,7 @@
 // Requirements
 // ------------------------------------------------------------------------------
 
+const semver = require('semver')
 const eslint = require('eslint')
 const rule = require('../../../lib/rules/no-export-in-script-setup')
 
@@ -48,7 +49,43 @@ ruleTester.run('no-export-in-script-setup', rule, {
       let foo;
       </script>
       `
-    }
+    },
+    ...(semver.gte(
+      require('@typescript-eslint/parser/package.json').version,
+      '5.4.0'
+    ) &&
+    semver.satisfies(require('typescript/package.json').version, '>=4.5.0-0')
+      ? [
+          {
+            filename: 'test.vue',
+            code: `
+            <script setup lang="ts">
+            export { type Foo } from "foo"
+            export type Bar = {}
+            export interface Bar {}
+            </script>
+            `,
+            parser: require.resolve('vue-eslint-parser'),
+            parserOptions: {
+              parser: require.resolve('@typescript-eslint/parser')
+            }
+          }
+        ]
+      : [
+          {
+            filename: 'test.vue',
+            code: `
+            <script setup lang="ts">
+            export type Bar = {}
+            export interface Bar {}
+            </script>
+            `,
+            parser: require.resolve('vue-eslint-parser'),
+            parserOptions: {
+              parser: require.resolve('@typescript-eslint/parser')
+            }
+          }
+        ])
   ],
 
   invalid: [
@@ -102,6 +139,34 @@ ruleTester.run('no-export-in-script-setup', rule, {
           line: 8
         }
       ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script setup lang="ts">
+      export const Foo = {}
+      export enum Bar {}
+      export {}
+      </script>
+      `,
+      parser: require.resolve('vue-eslint-parser'),
+      parserOptions: {
+        parser: require.resolve('@typescript-eslint/parser')
+      },
+      errors: [
+        {
+          message: '`<script setup>` cannot contain ES module exports.',
+          line: 3
+        },
+        {
+          message: '`<script setup>` cannot contain ES module exports.',
+          line: 4
+        },
+        {
+          message: '`<script setup>` cannot contain ES module exports.',
+          line: 5
+        }
+      ]
     }
   ]
 })
diff --git a/tests/lib/rules/no-expose-after-await.js b/tests/lib/rules/no-expose-after-await.js
new file mode 100644
index 00000000..2f33c58e
--- /dev/null
+++ b/tests/lib/rules/no-expose-after-await.js
@@ -0,0 +1,152 @@
+/**
+ * @author Yosuke Ota
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+const RuleTester = require('eslint').RuleTester
+const rule = require('../../../lib/rules/no-expose-after-await')
+
+const tester = new RuleTester({
+  parser: require.resolve('vue-eslint-parser'),
+  parserOptions: {
+    ecmaVersion: 2020,
+    sourceType: 'module'
+  }
+})
+
+tester.run('no-expose-after-await', rule, {
+  valid: [
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      export default {
+        async setup(_, {expose}) {
+          expose({ /* ... */ }) // ok
+          await doSomething()
+        }
+      }
+      </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      export default {
+        async setup(_, ctx) {
+          ctx.expose({ /* ... */ }) // ok
+          await doSomething()
+        }
+      }
+      </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      export default {
+        async setup(_, {expose}) {
+          expose({ /* ... */ })
+        }
+      }
+      </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      export default {
+        async setup(_, ctx) {
+          ctx.expose({ /* ... */ })
+        }
+      }
+      </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      export default {
+        async _setup(_, {expose}) {
+          expose({ /* ... */ })
+          await doSomething()
+          expose({ /* ... */ })
+        }
+      }
+      </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      export default {
+        async _setup(_, ctx) {
+          ctx.expose({ /* ... */ })
+          await doSomething()
+          ctx.expose({ /* ... */ })
+        }
+      }
+      </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script setup>
+      defineExpose({ /* ... */ })
+      await doSomething()
+      defineExpose({ /* ... */ })
+      </script>
+      `,
+      parserOptions: { ecmaVersion: 2022 }
+    }
+  ],
+  invalid: [
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      export default {
+        async setup(_, {expose}) {
+          await doSomething()
+          expose({ /* ... */ })
+        }
+      }
+      </script>
+      `,
+      errors: [
+        {
+          message: 'The `expose` after `await` expression are forbidden.',
+          line: 6,
+          column: 11
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      export default {
+        async setup(_, ctx) {
+          await doSomething()
+          ctx.expose({ /* ... */ })
+        }
+      }
+      </script>
+      `,
+      errors: [
+        {
+          message: 'The `expose` after `await` expression are forbidden.',
+          line: 6,
+          column: 11
+        }
+      ]
+    }
+  ]
+})
diff --git a/tests/lib/rules/no-mutating-props.js b/tests/lib/rules/no-mutating-props.js
index c0d6c273..5c976baa 100644
--- a/tests/lib/rules/no-mutating-props.js
+++ b/tests/lib/rules/no-mutating-props.js
@@ -331,6 +331,28 @@ ruleTester.run('no-mutating-props', rule, {
           }
         }
       </script>`
+    },
+
+    {
+      // script setup with shadow
+      filename: 'test.vue',
+      code: `
+        <template>
+          <input v-model="foo">
+          <input v-model="bar">
+          <input v-model="Infinity">
+        </template>
+        <script setup>
+        import { ref } from 'vue'
+        import { bar } from './my-script'
+        defineProps({
+          foo: String,
+          bar: String,
+          Infinity: Number
+        })
+        const foo = ref('')
+        </script>
+      `
     }
   ],
 
@@ -582,6 +604,42 @@ ruleTester.run('no-mutating-props', rule, {
         }
       ]
     },
+    {
+      filename: 'test.vue',
+      code: `
+        <template>
+          <div>
+            <MyComponent :data.sync="this.prop" />
+            <MyComponent :data.sync="prop" />
+            <MyComponent :data="this.prop" />
+            <MyComponent :data="prop" />
+            <template v-for="prop of data">
+              <MyComponent :data.sync="prop" />
+              <MyComponent :data.sync="this.prop" />
+            </template>
+          </div>
+        </template>
+        <script>
+          export default {
+            props: ['prop']
+          }
+        </script>
+      `,
+      errors: [
+        {
+          message: 'Unexpected mutation of "prop" prop.',
+          line: 4
+        },
+        {
+          message: 'Unexpected mutation of "prop" prop.',
+          line: 5
+        },
+        {
+          message: 'Unexpected mutation of "prop" prop.',
+          line: 10
+        }
+      ]
+    },
 
     // setup
     {
@@ -817,6 +875,43 @@ ruleTester.run('no-mutating-props', rule, {
           line: 6
         }
       ]
+    },
+
+    {
+      // script setup with shadow
+      filename: 'test.vue',
+      code: `
+        <template>
+          <input v-model="foo">
+          <input v-model="bar">
+          <input v-model="window">
+          <input v-model="Infinity">
+        </template>
+        <script setup>
+        import { ref } from 'vue'
+        const { Infinity } = defineProps({
+          foo: String,
+          bar: String,
+          Infinity: String,
+          window: String,
+        })
+        const foo = ref('')
+        </script>
+      `,
+      errors: [
+        {
+          message: 'Unexpected mutation of "bar" prop.',
+          line: 4
+        },
+        {
+          message: 'Unexpected mutation of "window" prop.',
+          line: 5
+        },
+        {
+          message: 'Unexpected mutation of "Infinity" prop.',
+          line: 6
+        }
+      ]
     }
   ]
 })