Repository: vuejs/eslint-plugin-vue
Pull request #1097 (2020-04-13 13:10:15)

New: Add `vue/no-deprecated-events-api` rule
This PR adds the vue/no-deprecated-events-api rule.

This rule reports use of deprecated `$on`, `$off` `$once` api. (in Vue.js 3.0.0+).

[RFC0020](https://github.com/vuejs/rfcs/blob/master/active-rfcs/0020-events-api-change.md)
ref #1035


Patch:
diff --git a/docs/rules/README.md b/docs/rules/README.md
index e229c19f..3e37fe70 100644
--- a/docs/rules/README.md
+++ b/docs/rules/README.md
@@ -39,6 +39,7 @@ Enforce all the rules in this category, as well as all higher priority rules, wi
 | Rule ID | Description |    |
 |:--------|:------------|:---|
 | [vue/no-async-in-computed-properties](./no-async-in-computed-properties.md) | disallow asynchronous actions in computed properties |  |
+| [vue/no-deprecated-events-api](./no-deprecated-events-api.md) | disallow using deprecated events api |  |
 | [vue/no-deprecated-data-object-declaration](./no-deprecated-data-object-declaration.md) | disallow using deprecated object declaration on data | :wrench: |
 | [vue/no-deprecated-filter](./no-deprecated-filter.md) | disallow using deprecated filters syntax |  |
 | [vue/no-deprecated-scope-attribute](./no-deprecated-scope-attribute.md) | disallow deprecated `scope` attribute (in Vue.js 2.5.0+) | :wrench: |
diff --git a/docs/rules/no-deprecated-events-api.md b/docs/rules/no-deprecated-events-api.md
new file mode 100644
index 00000000..e855a5a4
--- /dev/null
+++ b/docs/rules/no-deprecated-events-api.md
@@ -0,0 +1,57 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/no-deprecated-events-api
+description: disallow using deprecated events api
+---
+# vue/no-deprecated-events-api
+> disallow using deprecated events api
+
+- :gear: This rule is included in all of `"plugin:vue/vue3-essential"`, `"plugin:vue/vue3-strongly-recommended"` and `"plugin:vue/vue3-recommended"`.
+
+## :book: Rule Details
+
+This rule reports use of deprecated `$on`, `$off` `$once` api. (in Vue.js 3.0.0+).
+
+<eslint-code-block :rules="{'vue/no-deprecated-events-api': ['error']}">
+
+```vue
+<script>
+/* ✗ BAD */
+export default {
+  mounted () {
+    this.$on('start', function(args) {
+      console.log('start')
+    })
+    this.$emit('start')
+  }
+}
+
+/* ✓ GOOD */
+import mitt from 'mitt'
+const emitter = mitt()
+export default {
+  mounted () {
+    emitter.on('start', function(args) {
+      console.log('start')
+    })
+    emitter.emit('start')
+  }
+}
+</script>
+```
+
+</eslint-code-block>
+
+## :wrench: Options
+
+Nothing.
+
+## :books: Further reading
+
+- [RFC: events api change](https://github.com/vuejs/rfcs/blob/master/active-rfcs/0020-events-api-change.md)
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-deprecated-events-api.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-deprecated-events-api.js)
diff --git a/lib/configs/vue3-essential.js b/lib/configs/vue3-essential.js
index 3f2865df..f7c35fec 100644
--- a/lib/configs/vue3-essential.js
+++ b/lib/configs/vue3-essential.js
@@ -7,6 +7,7 @@ module.exports = {
   extends: require.resolve('./base'),
   rules: {
     'vue/no-async-in-computed-properties': 'error',
+    'vue/no-deprecated-events-api': 'error',
     'vue/no-deprecated-data-object-declaration': 'error',
     'vue/no-deprecated-filter': 'error',
     'vue/no-deprecated-scope-attribute': 'error',
diff --git a/lib/index.js b/lib/index.js
index 040965de..03fd799a 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -40,6 +40,7 @@ module.exports = {
     'no-boolean-default': require('./rules/no-boolean-default'),
     'no-confusing-v-for-v-if': require('./rules/no-confusing-v-for-v-if'),
     'no-custom-modifiers-on-v-model': require('./rules/no-custom-modifiers-on-v-model'),
+    'no-deprecated-events-api': require('./rules/no-deprecated-events-api'),
     'no-deprecated-data-object-declaration': require('./rules/no-deprecated-data-object-declaration'),
     'no-deprecated-filter': require('./rules/no-deprecated-filter'),
     'no-deprecated-scope-attribute': require('./rules/no-deprecated-scope-attribute'),
diff --git a/lib/rules/no-deprecated-events-api.js b/lib/rules/no-deprecated-events-api.js
new file mode 100644
index 00000000..488b893d
--- /dev/null
+++ b/lib/rules/no-deprecated-events-api.js
@@ -0,0 +1,57 @@
+/**
+ * @fileoverview disallow using deprecated events api
+ * @author yoyo930021
+ */
+'use strict'
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const utils = require('../utils')
+
+// ------------------------------------------------------------------------------
+// Rule Definition
+// ------------------------------------------------------------------------------
+
+module.exports = {
+  meta: {
+    type: 'problem',
+    docs: {
+      description: 'disallow using deprecated events api',
+      categories: ['vue3-essential'],
+      url: 'https://eslint.vuejs.org/rules/no-deprecated-events-api.html'
+    },
+    fixable: null,
+    schema: [],
+    messages: {
+      noDeprecatedEventsApi: 'The Events api `$on`, `$off` `$once` is deprecated. Using external library instead, for example mitt.'
+    }
+  },
+
+  create (context) {
+    const forbiddenNodes = []
+
+    return Object.assign(
+      {
+        'CallExpression > MemberExpression > ThisExpression' (node) {
+          if (!['$on', '$off', '$once'].includes(node.parent.property.name)) return
+          forbiddenNodes.push(node.parent.parent)
+        }
+      },
+      utils.executeOnVue(context, (obj) => {
+        forbiddenNodes.forEach(node => {
+          if (
+            node.loc.start.line >= obj.loc.start.line &&
+            node.loc.end.line <= obj.loc.end.line
+          ) {
+            context.report({
+              node,
+              messageId: 'noDeprecatedEventsApi'
+            })
+          }
+        })
+      })
+    )
+  }
+}


Test patch:
diff --git a/tests/lib/rules/no-deprecated-events-api.js b/tests/lib/rules/no-deprecated-events-api.js
new file mode 100644
index 00000000..59a6c685
--- /dev/null
+++ b/tests/lib/rules/no-deprecated-events-api.js
@@ -0,0 +1,161 @@
+/* eslint-disable eslint-plugin/consistent-output */
+/**
+ * @fileoverview disallow using deprecated events api
+ * @author yoyo930021
+ */
+'use strict'
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const rule = require('../../../lib/rules/no-deprecated-events-api')
+
+const RuleTester = require('eslint').RuleTester
+
+const parserOptions = {
+  ecmaVersion: 2018,
+  sourceType: 'module'
+}
+
+// ------------------------------------------------------------------------------
+// Tests
+// ------------------------------------------------------------------------------
+
+const ruleTester = new RuleTester()
+ruleTester.run('no-deprecated-events-api', rule, {
+
+  valid: [
+    {
+      filename: 'test.js',
+      code: `
+        createApp({
+          mounted () {
+            this.$emit('start')
+          }
+        })
+      `,
+      parserOptions
+    },
+    {
+      filename: 'test.js',
+      code: `
+        createApp({
+          methods: {
+            click () {
+              this.$emit('click')
+            }
+          }
+        })
+      `,
+      parserOptions
+    },
+    {
+      filename: 'test.js',
+      code: `
+        const another = function () {
+          this.$on('start', args => {
+            console.log('start')
+          })
+        }
+
+        createApp({
+          mounted () {
+            this.$emit('start')
+          }
+        })
+      `,
+      parserOptions
+    },
+    {
+      filename: 'test.js',
+      code: `
+        app.component('some-comp', {
+          mounted () {
+            this.$emit('start')
+          }
+        })
+      `,
+      parserOptions
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        export default {
+          mounted () {
+            this.$emit('start')
+          }
+        }
+      `,
+      parserOptions
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        import mitt from 'mitt'
+        const emitter = mitt()
+
+        export default {
+          setup () {
+            emitter.on('foo', e => console.log('foo', e))
+            emitter.emit('foo', { a: 'b' })
+            emitter.off('foo', onFoo)
+          }
+        }
+      `,
+      parserOptions
+    }
+  ],
+
+  invalid: [
+    {
+      filename: 'test.js',
+      code: `
+        app.component('some-comp', {
+          mounted () {
+            this.$on('start', function (args) {
+              console.log('start', args)
+            })
+          }
+        })
+      `,
+      parserOptions,
+      errors: [{
+        message: 'The Events api `$on`, `$off` `$once` is deprecated. Using external library instead, for example mitt.',
+        line: 4
+      }]
+    },
+    {
+      filename: 'test.js',
+      code: `
+        app.component('some-comp', {
+          mounted () {
+            this.$off('start')
+          }
+        })
+      `,
+      parserOptions,
+      errors: [{
+        message: 'The Events api `$on`, `$off` `$once` is deprecated. Using external library instead, for example mitt.',
+        line: 4
+      }]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        export default {
+          mounted () {
+            this.$once('start', function () {
+              console.log('start')
+            })
+          }
+        }
+      `,
+      parserOptions,
+      errors: [{
+        message: 'The Events api `$on`, `$off` `$once` is deprecated. Using external library instead, for example mitt.',
+        line: 4
+      }]
+    }
+  ]
+})