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

Issue:
Rule Proposal: `vue/no-deprecated-delete-set`
<!--
  Before proposing new rule, please make sure it hasn't been posted already.
  You can see all open propositions here:
  https://github.com/vuejs/eslint-plugin-vue/issues?q=is%3Aopen+is%3Aissue+label%3A%22new+rule+proposition%22
-->

**Please describe what the rule should do:**
<!-- A clear and concise description of what the rule should do. -->
Since `$delete` and `$set` APIs are [removed in Vue 3](https://v3-migration.vuejs.org/breaking-changes/#removed-apis); it would be helpful to add a warning to help people transition to Vue 3 by discouraging deprecated APIs

`Global functions set and delete, and the instance methods $set and $delete. They are no longer required with proxy-based change detection.`

**What category should the rule belong to?**
<!-- (place an "X" next to just one item) -->
[ ] Enforces code style (layout)
[ ] Warns about a potential error (problem)
[ ] Suggests an alternate way of doing something (suggestion)
[x] Other (please specify: Vue 3 transition)

**Provide 2-3 code examples that this rule should warn about:**

```vue
<!-- BAD -->
this.$set(...)
Vue.set(...)
this.$delete(...)
Vue.delete(...)
```

```vue
<!-- BAD -->
Vue.set(col, 'value', usage[col.id]);

<!-- GOOD -->
Object.assign(col, { value: usage[col.id] });
```

```vue
<!-- BAD -->
this.$set(targetObj, lastKey, value);

<!-- GOOD -->
targetObj[lastKey] = value;
```

```vue
<!-- BAD -->
Vue.delete(targetObj, lastKey);
this.$delete(targetObj, lastKey);

<!-- GOOD -->
delete targetObj[lastKey];
```

**Additional context**
<!-- Add any other context or screenshots about the feature request here. -->
- [Vue 3 docs](https://v3-migration.vuejs.org/breaking-changes/#removed-apis)
- [Custom ESLint rules for preventing $delete in Vue files](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/162079)
- [GitLab: Remove Vue.set occurrences epic](https://gitlab.com/gitlab-org/gitlab/-/issues/452467)
- [GitLab: Remove Vue.delete occurrences epic](https://gitlab.com/gitlab-org/gitlab/-/issues/451128)



Discussion:
Additionally, these cases should be reported:

```ts
import { set, del } from 'vue'

set(obj, key, val)
del(obj, key)
```

```ts
const { set, del } = require('vue')

set(obj, key, val)
del(obj, key)
```



Patch:
diff --git a/docs/rules/index.md b/docs/rules/index.md
index 113ff416b..c5efced00 100644
--- a/docs/rules/index.md
+++ b/docs/rules/index.md
@@ -228,6 +228,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 000000000..6bd4ed829
--- /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.js b/lib/index.js
index ff3f69c6e..60bccf38c 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -96,6 +96,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 000000000..305dacf91
--- /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'
+            })
+          }
+        }
+      }
+    )
+  }
+}


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 000000000..a38b6ecea
--- /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
+        }
+      ]
+    }
+  ]
+})