Repository: vuejs/eslint-plugin-vue
Pull request #2513 (2024-07-21 23:09:52)

Vuex and Pinia support for `no-undef-properties`
This PR adds support for Vuex and Pinia methods to the `no-undef-properties` rule, allowing properties defined by `mapState`, `mapActions`, `mapGetters`, `mapMutations`, and `mapWritableState` to be recognized as defined properties.

Test cases have been included for this enhancement.

kazupon: I think this rule should be supported by pinia or 3rd party plugin.
You can discuss in the issue
https://github.com/vuejs/pinia/issues/2612
marcjfj-vmlyr: > I think this rule should be supported by pinia or 3rd party plugin. You can discuss in the issue [vuejs/pinia#2612](https://github.com/vuejs/pinia/issues/2612)

Sure but it looks like the maintainers are welcoming these kinds of enhancements, as similar fix was added for `vue/no-unused-properties`.  See discussion here https://github.com/vuejs/eslint-plugin-vue/issues/1675.
nirinsanity: This is much required. Thanks to @marcjfj-vmlyr for working on this. Please merge as soon as possible.
marcjfj-vmlyr: @FloEdelmann Thanks for reviewing! I've made the requested changes.
marcjfj-vmlyr: Invalid test cases have been added.
horosin: this looks great, can't wait for it to ship!

Patch:
diff --git a/docs/rules/index.md b/docs/rules/index.md
index 46e0abaa..1ba5978d 100644
--- a/docs/rules/index.md
+++ b/docs/rules/index.md
@@ -229,6 +229,7 @@ For example:
 | [vue/next-tick-style](./next-tick-style.md) | enforce Promise or callback style in `nextTick` | :wrench: | :hammer: |
 | [vue/no-bare-strings-in-template](./no-bare-strings-in-template.md) | disallow the use of bare strings in `<template>` |  | :hammer: |
 | [vue/no-boolean-default](./no-boolean-default.md) | disallow boolean defaults |  | :hammer: |
+| [vue/no-deprecated-delete-set](./no-deprecated-delete-set.md) | disallow using deprecated `$delete` and `$set` (in Vue.js 3.0.0+) |  | :warning: |
 | [vue/no-deprecated-model-definition](./no-deprecated-model-definition.md) | disallow deprecated `model` definition (in Vue.js 3.0.0+) | :bulb: | :warning: |
 | [vue/no-duplicate-attr-inheritance](./no-duplicate-attr-inheritance.md) | enforce `inheritAttrs` to be set to `false` when using `v-bind="$attrs"` |  | :hammer: |
 | [vue/no-empty-component-block](./no-empty-component-block.md) | disallow the `<template>` `<script>` `<style>` block to be empty |  | :hammer: |
diff --git a/docs/rules/no-deprecated-delete-set.md b/docs/rules/no-deprecated-delete-set.md
new file mode 100644
index 00000000..6bd4ed82
--- /dev/null
+++ b/docs/rules/no-deprecated-delete-set.md
@@ -0,0 +1,52 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/no-deprecated-delete-set
+description: disallow using deprecated `$delete` and `$set` (in Vue.js 3.0.0+)
+---
+
+# vue/no-deprecated-delete-set
+
+> disallow using deprecated `$delete` and `$set` (in Vue.js 3.0.0+)
+
+- :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 use of deprecated `$delete` and `$set`. (in Vue.js 3.0.0+).
+
+<eslint-code-block :rules="{'vue/no-deprecated-delete-set': ['error']}">
+
+```vue
+<script>
+  import { set, del } from 'vue'
+  export default {
+    mounted () {
+      /* ✗ BAD */
+      this.$set(obj, key, value)
+      this.$delete(obj, key)
+
+      Vue.set(obj, key, value)
+      Vue.delete(obj, key)
+
+      set(obj, key, value)
+      del(obj, key)
+    }
+  }
+</script>
+```
+
+</eslint-code-block>
+
+## :wrench: Options
+
+Nothing.
+
+## :books: Further Reading
+
+- [Migration Guide - Removed APIs](https://v3-migration.vuejs.org/breaking-changes/#removed-apis)
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-deprecated-delete-set.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-deprecated-delete-set.js)
diff --git a/lib/index.d.ts b/lib/index.d.ts
new file mode 100644
index 00000000..22b1fbc6
--- /dev/null
+++ b/lib/index.d.ts
@@ -0,0 +1,43 @@
+type ConfigName =
+  | "base"
+  | "essential"
+  | "no-layout-rules"
+  | "recommended"
+  | "strongly-recommended"
+  | "vue3-essential"
+  | "vue3-recommended"
+  | "vue3-strongly-recommended"
+  | "flat/base"
+  | "flat/vue2-essential"
+  | "flat/vue2-recommended"
+  | "flat/vue2-strongly-recommended"
+  | "flat/essential"
+  | "flat/recommended"
+  | "flat/strongly-recommended"
+
+declare const vue: {
+  meta: any
+  configs: {
+    [name in keyof ConfigName]: any
+  }
+  rules: Record<string, any>
+  processors: {
+    ".vue": any
+    vue: any
+  }
+  environments: {
+    /**
+     * @deprecated
+     */
+    "setup-compiler-macros": {
+      globals: {
+        defineProps: "readonly"
+        defineEmits: "readonly"
+        defineExpose: "readonly"
+        withDefaults: "readonly"
+      }
+    }
+  }
+}
+
+export = vue
diff --git a/lib/index.js b/lib/index.js
index 6dbe5107..bb4abf40 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -97,6 +97,7 @@ const plugin = {
     'no-constant-condition': require('./rules/no-constant-condition'),
     'no-custom-modifiers-on-v-model': require('./rules/no-custom-modifiers-on-v-model'),
     'no-deprecated-data-object-declaration': require('./rules/no-deprecated-data-object-declaration'),
+    'no-deprecated-delete-set': require('./rules/no-deprecated-delete-set'),
     'no-deprecated-destroyed-lifecycle': require('./rules/no-deprecated-destroyed-lifecycle'),
     'no-deprecated-dollar-listeners-api': require('./rules/no-deprecated-dollar-listeners-api'),
     'no-deprecated-dollar-scopedslots-api': require('./rules/no-deprecated-dollar-scopedslots-api'),
diff --git a/lib/rules/no-deprecated-delete-set.js b/lib/rules/no-deprecated-delete-set.js
new file mode 100644
index 00000000..305dacf9
--- /dev/null
+++ b/lib/rules/no-deprecated-delete-set.js
@@ -0,0 +1,132 @@
+/**
+ * @author Wayne Zhang
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+const utils = require('../utils')
+const { ReferenceTracker } = require('@eslint-community/eslint-utils')
+
+/**
+ * @typedef {import('@eslint-community/eslint-utils').TYPES.TraceMap} TraceMap
+ */
+
+/** @type {TraceMap} */
+const deletedImportApisMap = {
+  set: {
+    [ReferenceTracker.CALL]: true
+  },
+  del: {
+    [ReferenceTracker.CALL]: true
+  }
+}
+const deprecatedApis = new Set(['set', 'delete'])
+const deprecatedDollarApis = new Set(['$set', '$delete'])
+
+/**
+ * @param {Expression|Super} node
+ */
+function isVue(node) {
+  return node.type === 'Identifier' && node.name === 'Vue'
+}
+
+module.exports = {
+  meta: {
+    type: 'problem',
+    docs: {
+      description:
+        'disallow using deprecated `$delete` and `$set` (in Vue.js 3.0.0+)',
+      categories: undefined,
+      url: 'https://eslint.vuejs.org/rules/no-deprecated-delete-set.html'
+    },
+    fixable: null,
+    schema: [],
+    messages: {
+      deprecated: 'The `$delete`, `$set` is deprecated.'
+    }
+  },
+  /** @param {RuleContext} context */
+  create(context) {
+    /**
+     * @param {Identifier} identifier
+     * @param {RuleContext} context
+     * @returns {CallExpression|undefined}
+     */
+    function getVueDeprecatedCallExpression(identifier, context) {
+      // Instance API: this.$set()
+      if (
+        deprecatedDollarApis.has(identifier.name) &&
+        identifier.parent.type === 'MemberExpression' &&
+        utils.isThis(identifier.parent.object, context) &&
+        identifier.parent.parent.type === 'CallExpression' &&
+        identifier.parent.parent.callee === identifier.parent
+      ) {
+        return identifier.parent.parent
+      }
+
+      // Vue 2 Global API: Vue.set()
+      if (
+        deprecatedApis.has(identifier.name) &&
+        identifier.parent.type === 'MemberExpression' &&
+        isVue(identifier.parent.object) &&
+        identifier.parent.parent.type === 'CallExpression' &&
+        identifier.parent.parent.callee === identifier.parent
+      ) {
+        return identifier.parent.parent
+      }
+
+      return undefined
+    }
+
+    const nodeVisitor = {
+      /** @param {Identifier} node */
+      Identifier(node) {
+        const callExpression = getVueDeprecatedCallExpression(node, context)
+        if (!callExpression) {
+          return
+        }
+
+        context.report({
+          node,
+          messageId: 'deprecated'
+        })
+      }
+    }
+
+    return utils.compositingVisitors(
+      utils.defineVueVisitor(context, nodeVisitor),
+      utils.defineScriptSetupVisitor(context, nodeVisitor),
+      {
+        /** @param {Program} node */
+        Program(node) {
+          const tracker = new ReferenceTracker(utils.getScope(context, node))
+
+          // import { set } from 'vue'; set()
+          const esmTraceMap = {
+            vue: {
+              [ReferenceTracker.ESM]: true,
+              ...deletedImportApisMap
+            }
+          }
+          // const { set } = require('vue'); set()
+          const cjsTraceMap = {
+            vue: {
+              ...deletedImportApisMap
+            }
+          }
+
+          for (const { node } of [
+            ...tracker.iterateEsmReferences(esmTraceMap),
+            ...tracker.iterateCjsReferences(cjsTraceMap)
+          ]) {
+            const refNode = /** @type {CallExpression} */ (node)
+            context.report({
+              node: refNode.callee,
+              messageId: 'deprecated'
+            })
+          }
+        }
+      }
+    )
+  }
+}
diff --git a/lib/rules/no-undef-properties.js b/lib/rules/no-undef-properties.js
index 29fbb714..711c2ed2 100644
--- a/lib/rules/no-undef-properties.js
+++ b/lib/rules/no-undef-properties.js
@@ -111,6 +111,11 @@ module.exports = {
     ).map(toRegExp)
     const propertyReferenceExtractor = definePropertyReferenceExtractor(context)
     const programNode = context.getSourceCode().ast
+    /**
+     * Property names identified as defined via a Vuex or Pinia helpers
+     * @type {Set<string>}
+     */
+    const propertiesDefinedByStoreHelpers = new Set()
 
     /**
      * @param {ASTNode} node
@@ -185,7 +190,8 @@ module.exports = {
       report(node, name, messageId = 'undef') {
         if (
           reserved.includes(name) ||
-          ignores.some((ignore) => ignore.test(name))
+          ignores.some((ignore) => ignore.test(name)) ||
+          propertiesDefinedByStoreHelpers.has(name)
         ) {
           return
         }
@@ -331,6 +337,51 @@ module.exports = {
         }
       }),
       utils.defineVueVisitor(context, {
+        /**
+         * @param {CallExpression} node
+         */
+        CallExpression(node) {
+          if (node.callee.type !== 'Identifier') return
+          /** @type {'methods'|'computed'|null} */
+          let groupName = null
+          if (/^mapMutations|mapActions$/u.test(node.callee.name)) {
+            groupName = GROUP_METHODS
+          } else if (
+            /^mapState|mapGetters|mapWritableState$/u.test(node.callee.name)
+          ) {
+            groupName = GROUP_COMPUTED_PROPERTY
+          }
+
+          if (!groupName || node.arguments.length === 0) return
+          // On Pinia the store is always the first argument
+          const arg =
+            node.arguments.length === 2 ? node.arguments[1] : node.arguments[0]
+          if (arg.type === 'ObjectExpression') {
+            // e.g.
+            // `mapMutations({ add: 'increment' })`
+            // `mapState({ count: state => state.todosCount })`
+            for (const prop of arg.properties) {
+              const name =
+                prop.type === 'SpreadElement'
+                  ? null
+                  : utils.getStaticPropertyName(prop)
+              if (name) {
+                propertiesDefinedByStoreHelpers.add(name)
+              }
+            }
+          } else if (arg.type === 'ArrayExpression') {
+            // e.g. `mapMutations(['add'])`
+            for (const element of arg.elements) {
+              if (!element || !utils.isStringLiteral(element)) {
+                continue
+              }
+              const name = utils.getStringLiteralValue(element)
+              if (name) {
+                propertiesDefinedByStoreHelpers.add(name)
+              }
+            }
+          }
+        },
         onVueObjectEnter(node) {
           const ctx = getVueComponentContext(node)
 
diff --git a/lib/rules/no-unused-refs.js b/lib/rules/no-unused-refs.js
index 9033d9c2..79d82a39 100644
--- a/lib/rules/no-unused-refs.js
+++ b/lib/rules/no-unused-refs.js
@@ -233,6 +233,19 @@ module.exports = {
               refsNode = id.parent
             }
             extractUsedForPattern(refsNode)
+          },
+          CallExpression(callExpression) {
+            const firstArgument = callExpression.arguments[0]
+            if (
+              callExpression.callee.name !== 'useTemplateRef' ||
+              !firstArgument
+            ) {
+              return
+            }
+            const name = utils.getStringLiteralValue(firstArgument)
+            if (name !== null) {
+              usedRefs.add(name)
+            }
           }
         }
       )
diff --git a/package.json b/package.json
index a10ad6b0..6ffc88fb 100644
--- a/package.json
+++ b/package.json
@@ -3,6 +3,7 @@
   "version": "9.28.0",
   "description": "Official ESLint plugin for Vue.js",
   "main": "lib/index.js",
+  "types": "lib/index.d.ts",
   "scripts": {
     "new": "node tools/new-rule.js",
     "start": "npm run test:base -- --watch --growl",


Test patch:
diff --git a/tests/lib/rules/no-deprecated-delete-set.js b/tests/lib/rules/no-deprecated-delete-set.js
new file mode 100644
index 00000000..a38b6ece
--- /dev/null
+++ b/tests/lib/rules/no-deprecated-delete-set.js
@@ -0,0 +1,379 @@
+/**
+ * @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/no-deprecated-delete-set')
+
+const tester = new RuleTester({
+  languageOptions: {
+    parser: require('vue-eslint-parser'),
+    ecmaVersion: 2020,
+    sourceType: 'module',
+    globals: {
+      require: 'readonly'
+    }
+  }
+})
+
+tester.run('no-deprecated-delete-set', rule, {
+  valid: [
+    {
+      filename: 'test.js',
+      code: `
+        const another = function () {
+          this.$set(obj, key, value)
+          this.$delete(obj, key)
+        }
+
+        createApp({
+          mounted () {
+            this.$emit('start')
+          },
+          methods: {
+            click () {
+              this.$emit('click')
+            }
+          }
+        })
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <script>
+          export default {
+            mounted () {
+              this.$nextTick()
+            }
+          }
+        </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <script>
+          export default {
+            mounted () {
+              a(this.$set)
+              a(this.$delete)
+            }
+          }
+        </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <script>
+        function set(obj, key, value) {}
+        function del(obj, key) {}
+        export default {
+          mounted () {
+            set(obj, key, value)
+            del(obj, key)
+          }
+        }
+        </script>
+      `
+    },
+    // from vue
+    {
+      filename: 'test.js',
+      code: `
+        app.component('some-comp', {
+          mounted () {
+            Vue.nextTick()
+          }
+        })
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <script>
+          import { nextTick as nt, provide } from 'vue'
+          export default {
+            mounted () {
+              nt()
+              provide(key, value)
+            }
+          }
+        </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <script>
+          const { nextTick: set, provide: del } = require('vue')
+          export default {
+            mounted () {
+              set()
+              del(key, value)
+            }
+          }
+        </script>
+      `
+    }
+  ],
+
+  invalid: [
+    {
+      filename: 'test.js',
+      code: `
+        app.component('some-comp', {
+          mounted () {
+            this.$set(obj, key, value)
+            this.$delete(obj, key)
+          }
+        })
+      `,
+      errors: [
+        {
+          messageId: 'deprecated',
+          line: 4
+        },
+        {
+          messageId: 'deprecated',
+          line: 5
+        }
+      ]
+    },
+    {
+      filename: 'test.js',
+      code: `
+        app.component('some-comp', {
+          mounted () {
+            const vm = this
+            vm.$set(obj, key, value)
+            vm.$delete(obj, key)
+          }
+        })
+      `,
+      errors: [
+        {
+          messageId: 'deprecated',
+          line: 5
+        },
+        {
+          messageId: 'deprecated',
+          line: 6
+        }
+      ]
+    },
+    {
+      filename: 'test.js',
+      code: `
+        app.component('some-comp', {
+          mounted () {
+            this?.$set(obj, key, value)
+            this?.$delete(obj, key)
+          }
+        })
+      `,
+      errors: [
+        { messageId: 'deprecated', line: 4 },
+        { messageId: 'deprecated', line: 5 }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <script>
+          export default {
+            mounted () {
+              this.$set(obj, key, value)
+              this.$delete(obj, key)
+            }
+          }
+        </script>
+      `,
+      errors: [
+        { messageId: 'deprecated', line: 5 },
+        { messageId: 'deprecated', line: 6 }
+      ]
+    },
+    // from vue
+    {
+      filename: 'test.js',
+      code: `
+        app.component('some-comp', {
+          mounted () {
+            Vue.set(obj, key, value)
+            Vue.delete(obj, key)
+          }
+        })
+      `,
+      errors: [
+        {
+          messageId: 'deprecated',
+          line: 4
+        },
+        {
+          messageId: 'deprecated',
+          line: 5
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <script>
+          import { set, del } from 'vue'
+          export default {
+            mounted () {
+              set(obj, key, value)
+              del(obj, key)
+            }
+          }
+        </script>
+      `,
+      errors: [
+        {
+          messageId: 'deprecated',
+          line: 6
+        },
+        {
+          messageId: 'deprecated',
+          line: 7
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <script setup>
+          import { set, del } from 'vue'
+
+          set(obj, key, value)
+          del(obj, key)
+        </script>
+      `,
+      errors: [
+        {
+          messageId: 'deprecated',
+          line: 5
+        },
+        {
+          messageId: 'deprecated',
+          line: 6
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <script setup>
+          import { set as s, del as d } from 'vue'
+
+          s(obj, key, value)
+          d(obj, key)
+        </script>
+      `,
+      errors: [
+        {
+          messageId: 'deprecated',
+          line: 5
+        },
+        {
+          messageId: 'deprecated',
+          line: 6
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <script>
+          const { set, del } = require('vue')
+          export default {
+            mounted () {
+              set(obj, key, value)
+              del(obj, key)
+            }
+          }
+        </script>
+      `,
+      errors: [
+        {
+          messageId: 'deprecated',
+          line: 6
+        },
+        {
+          messageId: 'deprecated',
+          line: 7
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <script>
+          const { set: s, del: d } = require('vue')
+          export default {
+            mounted () {
+              s(obj, key, value)
+              d(obj, key)
+            }
+          }
+        </script>
+      `,
+      errors: [
+        {
+          messageId: 'deprecated',
+          line: 6
+        },
+        {
+          messageId: 'deprecated',
+          line: 7
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <script setup>
+          const { set, del } = require('vue')
+
+          set(obj, key, value)
+          del(obj, key)
+        </script>
+      `,
+      errors: [
+        {
+          messageId: 'deprecated',
+          line: 5
+        },
+        {
+          messageId: 'deprecated',
+          line: 6
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <script setup>
+          const v = require('vue')
+
+          v.set(obj, key, value)
+          v.del(obj, key)
+        </script>
+      `,
+      errors: [
+        {
+          messageId: 'deprecated',
+          line: 5
+        },
+        {
+          messageId: 'deprecated',
+          line: 6
+        }
+      ]
+    }
+  ]
+})
diff --git a/tests/lib/rules/no-undef-properties.js b/tests/lib/rules/no-undef-properties.js
index 83812a24..63313b4b 100644
--- a/tests/lib/rules/no-undef-properties.js
+++ b/tests/lib/rules/no-undef-properties.js
@@ -561,7 +561,248 @@ tester.run('no-undef-properties', rule, {
         }
       }
     },
+    {
+      // Vuex
+      filename: 'test.vue',
+      code: `
+      <script>
+      import { mapState } from 'vuex';
+
+      export default {
+        computed: {
+          ...mapState({
+            a: (vm) => vm.a,
+            b: (vm) => vm.b,
+          })
+        },
+        methods: {
+          c() {return this.a * this.b}
+        }
+      }
+      </script>
+      <template>
+      {{ a }} {{ b }}
+      </template>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      import { mapActions } from 'vuex';
+
+      export default {
+          methods: {
+          ...mapActions({
+            a: 'a',
+            b: 'b',
+          }),
+          c() {return this.a()},
+          d() {return this.b()},
+        }
+      }
+      </script>
+      <template>
+      {{ a }} {{ b }}
+      </template>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      import { mapMutations } from 'vuex';
+
+      export default {
+          methods: {
+          ...mapMutations({
+            a: 'a',
+            b: 'b',
+          }),
+          c() {return this.a()},
+          d() {return this.b()},
+        }
+      }
+      </script>
+      <template>
+      {{ a }} {{ b }}
+      </template>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      import { mapActions } from 'vuex';
+
+      export default {
+          methods: {
+          ...mapActions(['a', 'b']),
+          c() {return this.a()},
+          d() {return this.b()},
+        }
+      }
+      </script>
+      <template>
+      {{ a }} {{ b }}
+      </template>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      import { mapMutations } from 'vuex';
+
+      export default {
+        methods: {
+          ...mapMutations(['a', 'b']),
+          c() {return this.a()},
+          d() {return this.b()},
+        }
+      }
+      </script>
+      <template>
+      {{ a }} {{ b }}
+      </template>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      import { mapGetters } from 'vuex';
+
+      export default {
+        computed: {
+          ...mapGetters(['a', 'b'])
+        },
+        methods: {
+          c() {return this.a},
+          d() {return this.b},
+        }
+      }
+      </script>
+      <template>
+      {{ a }} {{ b }}
+      </template>
+      `
+    },
+    {
+      // Pinia
+      filename: 'test.vue',
+      code: `
+      <script>
+      import { mapGetters } from 'pinia'
+      import { useStore } from '../store'
+
+      export default {
+        computed: {
+          ...mapGetters(useStore, ['a', 'b'])
+        },
+        methods: {
+          c() {return this.a},
+          d() {return this.b},
+        }
+      }
+      </script>
+      <template>
+      {{ a }} {{ b }}
+      </template>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      import { mapState } from 'pinia'
+      import { useStore } from '../store'
+
+      export default {
+        computed: {
+          ...mapState(useStore, {
+            a: 'a',
+            b: store => store.b,
+          })
+        },
+        methods: {
+          c() {return this.a},
+          d() {return this.b},
+        }
+      }
+      </script>
+      <template>
+      {{ a }} {{ b }}
+      </template>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      import { mapWritableState } from 'pinia'
+      import { useStore } from '../store'
+
+      export default {
+        computed: {
+          ...mapWritableState(useStore, {
+            a: 'a',
+            b: 'b',
+          })
+        },
+        methods: {
+          c() {return this.a},
+          d() {return this.b},
+        }
+      }
+      </script>
+      <template>
+      {{ a }} {{ b }}
+      </template>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      import { mapWritableState } from 'pinia'
+      import { useStore } from '../store'
+
+      export default {
+        computed: {
+          ...mapWritableState(useStore, ['a', 'b'])
+        },
+        methods: {
+          c() {return this.a},
+          d() {return this.b},
+        }
+      }
+      </script>
+      <template>
+      {{ a }} {{ b }}
+      </template>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      import { mapActions } from 'pinia'
+      import { useStore } from '../store'
 
+      export default {
+          methods: {
+          ...mapActions(useStore, ['a', 'b']),
+          c() {return this.a()},
+          d() {return this.b()},
+        }
+      }
+      </script>
+      <template>
+      {{ a() }} {{ b() }}
+      </template>
+      `
+    },
     `
     <script setup>
     const model = defineModel();
@@ -1214,6 +1455,321 @@ tester.run('no-undef-properties', rule, {
           line: 14
         }
       ]
+    },
+    {
+      // Vuex
+      filename: 'test.vue',
+      code: `
+      <script>
+      import { mapState } from 'vuex';
+
+      export default {
+        computed: {
+          ...mapState({
+            a: (vm) => vm.a,
+            b: (vm) => vm.b,
+          })
+        },
+        methods: {
+          c() {return this.a * this.g}
+        }
+      }
+      </script>
+      <template>
+      {{ a }} {{ b }} {{ c }}
+      </template>
+      `,
+      errors: [
+        {
+          message: "'g' is not defined.",
+          line: 13
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      import { mapActions } from 'vuex';
+
+      export default {
+          methods: {
+          ...mapActions({
+            a: 'a',
+            b: 'b',
+          }),
+          c() {return this.a()},
+          d() {return this.f()},
+        }
+      }
+      </script>
+      <template>
+      {{ a }} {{ b }}
+      </template>
+      `,
+      errors: [
+        {
+          message: "'f' is not defined.",
+          line: 12
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      import { mapMutations } from 'vuex';
+
+      export default {
+          methods: {
+          ...mapMutations({
+            a: 'a',
+            b: 'b',
+          }),
+          c() {return this.a()},
+          d() {return this.b()},
+          d() {return this.x()},
+        }
+      }
+      </script>
+      <template>
+      {{ a }} {{ b }}
+      </template>
+      `,
+      errors: [
+        {
+          message: "'x' is not defined.",
+          line: 13
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      import { mapActions } from 'vuex';
+
+      export default {
+          methods: {
+          ...mapActions(['a', 'b']),
+          c() {return this.a()},
+          d() {return this.b()},
+          d() {return this.f()},
+        }
+      }
+      </script>
+      <template>
+      {{ a }} {{ b }}
+      </template>
+      `,
+      errors: [
+        {
+          message: "'f' is not defined.",
+          line: 10
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      import { mapMutations } from 'vuex';
+
+      export default {
+        methods: {
+          ...mapMutations(['a', 'b']),
+          c() {return this.a()},
+          d() {return this.b()},
+        }
+      }
+      </script>
+      <template>
+      {{ a }} {{ b }} {{ q }}
+      </template>
+      `,
+      errors: [
+        {
+          message: "'q' is not defined.",
+          line: 14
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      import { mapGetters } from 'vuex';
+
+      export default {
+        computed: {
+          ...mapGetters(['a', 'b'])
+        },
+        methods: {
+          c() {return this.a},
+          d() {return this.b},
+          d() {return this.z},
+        }
+      }
+      </script>
+      <template>
+      {{ a }} {{ b }}
+      </template>
+      `,
+      errors: [
+        {
+          message: "'z' is not defined.",
+          line: 12
+        }
+      ]
+    },
+    {
+      // Pinia
+      filename: 'test.vue',
+      code: `
+      <script>
+      import { mapGetters } from 'pinia'
+      import { useStore } from '../store'
+
+      export default {
+        computed: {
+          ...mapGetters(useStore, ['a', 'b'])
+        },
+        methods: {
+          c() {return this.a},
+          d() {return this.b},
+          d() {return this.z},
+        }
+      }
+      </script>
+      <template>
+      {{ a }} {{ b }}
+      </template>
+      `,
+      errors: [
+        {
+          message: "'z' is not defined.",
+          line: 13
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      import { mapState } from 'pinia'
+      import { useStore } from '../store'
+
+      export default {
+        computed: {
+          ...mapState(useStore, {
+            a: 'a',
+            b: store => store.b,
+          })
+        },
+        methods: {
+          c() {return this.a},
+          d() {return this.b},
+        }
+      }
+      </script>
+      <template>
+      {{ a }} {{ b }} {{ q }}
+      </template>
+      `,
+      errors: [
+        {
+          message: "'q' is not defined.",
+          line: 20
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      import { mapWritableState } from 'pinia'
+      import { useStore } from '../store'
+
+      export default {
+        computed: {
+          ...mapWritableState(useStore, {
+            a: 'a',
+            b: 'b',
+          })
+        },
+        methods: {
+          c() {return this.a},
+          d() {return this.b},
+          d() {return this.z},
+        }
+      }
+      </script>
+      <template>
+      {{ a }} {{ b }}
+      </template>
+      `,
+      errors: [
+        {
+          message: "'z' is not defined.",
+          line: 16
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      import { mapWritableState } from 'pinia'
+      import { useStore } from '../store'
+
+      export default {
+        computed: {
+          ...mapWritableState(useStore, ['a', 'b'])
+        },
+        methods: {
+          c() {return this.a},
+          d() {return this.b},
+          d() {return this.z},
+        }
+      }
+      </script>
+      <template>
+      {{ a }} {{ b }}
+      </template>
+      `,
+      errors: [
+        {
+          message: "'z' is not defined.",
+          line: 13
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      import { mapActions } from 'pinia'
+      import { useStore } from '../store'
+
+      export default {
+          methods: {
+          ...mapActions(useStore, ['a', 'b']),
+          c() {return this.a()},
+          d() {return this.b()},
+          d() {return this.x()},
+        }
+      }
+      </script>
+      <template>
+      {{ a() }} {{ b() }}
+      </template>
+      `,
+      errors: [
+        {
+          message: "'x' is not defined.",
+          line: 11
+        }
+      ]
     }
   ]
 })
diff --git a/tests/lib/rules/no-unused-refs.js b/tests/lib/rules/no-unused-refs.js
index 7e092ee8..a4407a60 100644
--- a/tests/lib/rules/no-unused-refs.js
+++ b/tests/lib/rules/no-unused-refs.js
@@ -315,6 +315,20 @@ tester.run('no-unused-refs', rule, {
       const x = ref(null)
       </script>
       `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <input ref="x" />
+        <input ref="y" />
+      </template>
+      <script setup>
+      import {useTemplateRef} from 'vue'
+      const inputX = useTemplateRef('x')
+      const inputY = useTemplateRef(\`y\`)
+      </script>
+      `
     }
   ],