Repository: vuejs/eslint-plugin-vue
Pull request #1068 (2020-03-07 08:56:46)

New: Add `vue/no-watch-after-await` rule
This PR adds the `vue/no-watch-after-await` rule.

The `vue/no-watch-after-await` rule reports the `watch()` after `await` expression. 

---

ref #1035

ota-meshi: I was unable to reproduce this asynchronous issue with Vue3 "setup".
I keep the status "WIP" until I see the actual problem.
ota-meshi: I have found that registering a watch asynchronously has the problem that the watch remains after the instance of the Vue component is destroyed.
ota-meshi: Hmm... but, the remaining watches can be deleted with StopHandle.

Patch:
diff --git a/docs/rules/README.md b/docs/rules/README.md
index cc2a9731..bb8d92e8 100644
--- a/docs/rules/README.md
+++ b/docs/rules/README.md
@@ -39,11 +39,14 @@ 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: |
 | [vue/no-deprecated-slot-attribute](./no-deprecated-slot-attribute.md) | disallow deprecated `slot` attribute (in Vue.js 2.6.0+) | :wrench: |
 | [vue/no-deprecated-slot-scope-attribute](./no-deprecated-slot-scope-attribute.md) | disallow deprecated `slot-scope` attribute (in Vue.js 2.6.0+) | :wrench: |
 | [vue/no-deprecated-v-bind-sync](./no-deprecated-v-bind-sync.md) | disallow use of deprecated `.sync` modifier on `v-bind` directive (in Vue.js 3.0.0+) | :wrench: |
+| [vue/no-deprecated-v-on-number-modifiers](./no-deprecated-v-on-number-modifiers.md) | disallow using deprecated number (keycode) modifiers | :wrench: |
 | [vue/no-dupe-keys](./no-dupe-keys.md) | disallow duplication of field names |  |
 | [vue/no-duplicate-attributes](./no-duplicate-attributes.md) | disallow duplication of attributes |  |
 | [vue/no-lifecycle-after-await](./no-lifecycle-after-await.md) | disallow asynchronously registered lifecycle hooks |  |
@@ -58,6 +61,7 @@ Enforce all the rules in this category, as well as all higher priority rules, wi
 | [vue/no-unused-components](./no-unused-components.md) | disallow registering components that are not used inside templates |  |
 | [vue/no-unused-vars](./no-unused-vars.md) | disallow unused variable definitions of v-for directives or scope attributes |  |
 | [vue/no-use-v-if-with-v-for](./no-use-v-if-with-v-for.md) | disallow use v-if on the same element as v-for |  |
+| [vue/no-watch-after-await](./no-watch-after-await.md) | disallow asynchronously registered `watch` |  |
 | [vue/require-component-is](./require-component-is.md) | require `v-bind:is` of `<component>` elements |  |
 | [vue/require-prop-type-constructor](./require-prop-type-constructor.md) | require prop type to be a constructor | :wrench: |
 | [vue/require-render-return](./require-render-return.md) | enforce render function to always return value |  |
@@ -272,6 +276,7 @@ For example:
 | [vue/no-reserved-component-names](./no-reserved-component-names.md) | disallow the use of reserved names in component definitions |  |
 | [vue/no-restricted-syntax](./no-restricted-syntax.md) | disallow specified syntax |  |
 | [vue/no-static-inline-styles](./no-static-inline-styles.md) | disallow static inline `style` attributes |  |
+| [vue/no-template-target-blank](./no-template-target-blank.md) | disallow target="_blank" attribute without rel="noopener noreferrer" |  |
 | [vue/no-unsupported-features](./no-unsupported-features.md) | disallow unsupported Vue.js syntax on the specified version | :wrench: |
 | [vue/object-curly-spacing](./object-curly-spacing.md) | enforce consistent spacing inside braces | :wrench: |
 | [vue/padding-line-between-blocks](./padding-line-between-blocks.md) | require or disallow padding lines between blocks | :wrench: |
diff --git a/docs/rules/no-deprecated-data-object-declaration.md b/docs/rules/no-deprecated-data-object-declaration.md
new file mode 100644
index 00000000..c095fadb
--- /dev/null
+++ b/docs/rules/no-deprecated-data-object-declaration.md
@@ -0,0 +1,65 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/no-deprecated-data-object-declaration
+description: disallow using deprecated object declaration on data
+---
+# vue/no-deprecated-data-object-declaration
+> disallow using deprecated object declaration on data
+
+- :gear: This rule is included in all of `"plugin:vue/vue3-essential"`, `"plugin:vue/vue3-strongly-recommended"` and `"plugin:vue/vue3-recommended"`.
+- :wrench: The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) can automatically fix some of the problems reported by this rule.
+
+## :book: Rule Details
+
+This rule reports use of deprecated object declaration on `data` property (in Vue.js 3.0.0+).
+The different from `vue/no-shared-component-data` is the root instance being also disallowed.
+
+<eslint-code-block fix :rules="{'vue/no-deprecated-data-object-declaration': ['error']}">
+
+```vue
+<script>
+/* ✗ BAD */
+createApp({
+  data: {
+    foo: null
+  }
+}).mount('#app')
+export default {
+  data: {
+    foo: null
+  }
+}
+
+/* ✓ GOOD */
+export default {
+  data () {
+    return {
+      foo: null
+    }
+  }
+}
+createApp({
+  data () {
+    return {
+      foo: null
+    }
+  }
+}).mount('#app')
+</script>
+```
+
+</eslint-code-block>
+
+## :wrench: Options
+
+Nothing.
+
+## :books: Further reading
+
+- [RFC: remove data object declaration](https://github.com/vuejs/rfcs/blob/master/active-rfcs/0019-remove-data-object-declaration.md)
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-deprecated-data-object-declaration.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-deprecated-data-object-declaration.js)
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/docs/rules/no-deprecated-v-on-number-modifiers.md b/docs/rules/no-deprecated-v-on-number-modifiers.md
new file mode 100644
index 00000000..5ecba24a
--- /dev/null
+++ b/docs/rules/no-deprecated-v-on-number-modifiers.md
@@ -0,0 +1,52 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/no-deprecated-v-on-number-modifiers
+description: disallow using deprecated number (keycode) modifiers (in Vue.js 3.0.0+)
+---
+# vue/no-deprecated-v-on-number-modifiers
+> disallow using deprecated number (keycode) modifiers (in Vue.js 3.0.0+)
+
+- :gear: This rule is included in all of `"plugin:vue/vue3-essential"`, `"plugin:vue/vue3-strongly-recommended"` and `"plugin:vue/vue3-recommended"`.
+- :wrench: The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) can automatically fix some of the problems reported by this rule.
+
+## :book: Rule Details
+
+This rule reports use of deprecated `KeyboardEvent.keyCode` modifier on `v-on` directive (in Vue.js 3.0.0+)
+
+<eslint-code-block fix :rules="{'vue/no-deprecated-v-on-number-modifiers': ['error']}">
+
+```vue
+<template>
+  <!-- ✓ GOOD -->
+  <input v-on:keyup.page-down="onArrowUp">
+  <input @keyup.page-down="onArrowUp">
+  <input @keyup.9="onArrowUp"> <!-- 9 is KeyboardEvent.key -->
+
+
+  <!-- ✗ BAD -->
+  <input v-on:keyup.34="onArrowUp">
+  <input @keyup.34="onArrowUp">
+</template>
+```
+
+</eslint-code-block>
+
+## :wrench: Options
+
+Nothing.
+
+## :couple: Related rules
+
+- [valid-v-on]
+
+[valid-v-on]: valid-v-on.md
+
+## :books: Further reading
+
+- [RFC: drop keycode support](https://github.com/vuejs/rfcs/blob/master/active-rfcs/0014-drop-keycode-support.md)
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-deprecated-v-on-number-modifiers.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-deprecated-v-on-number-modifiers.js)
diff --git a/docs/rules/no-template-target-blank.md b/docs/rules/no-template-target-blank.md
new file mode 100644
index 00000000..67dfcc99
--- /dev/null
+++ b/docs/rules/no-template-target-blank.md
@@ -0,0 +1,99 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/no-template-target-blank
+description: disallow target="_blank" attribute without rel="noopener noreferrer"
+---
+# vue/no-template-target-blank
+> disallow target="_blank" attribute without rel="noopener noreferrer"
+
+## :book: Rule Details
+
+This rule disallows using `target="_blank"` attribute without `rel="noopener noreferrer"` to avoid a security vulnerability([see here for more details](https://mathiasbynens.github.io/rel-noopener/)).
+
+<eslint-code-block :rules="{'vue/no-template-target-blank': ['error']}">
+
+```vue
+<template>
+  <!-- ✓ Good -->
+  <a link="http://example.com" target="_blank" rel="noopener noreferrer">link</a>
+
+  <!-- ✗ BAD -->
+  <a link="http://example.com" target="_blank" >link</a>
+</temlate>
+```
+
+## :wrench: Options
+
+```json
+{
+  "vue/no-template-target-blank": ["error", {
+    "allowReferrer": true,
+    "enforceDynamicLinks": "always"
+  }]
+}
+```
+
+- `allowReferrer` ... If `true`, does not require noreferrer.default `false`
+- `enforceDynamicLinks ("always" | "never")` ... If `always`, enforces the rule if the href is a dynamic link. default `always`.
+
+### `{ allowReferrer: false }` (default)
+
+<eslint-code-block :rules="{'vue/no-template-target-blank': ['error', { allowReferrer: false }]}">
+
+```vue
+<template>
+  <!-- ✓ Good -->
+  <a link="http://example.com" target="_blank" rel="noopener noreferrer">link</a>
+
+  <!-- ✗ BAD -->
+  <a link="http://example.com" target="_blank" rel="noopener">link</a>
+</temlate>
+```
+
+### `{ allowReferrer: true }`
+
+<eslint-code-block :rules="{'vue/no-template-target-blank': ['error', { allowReferrer: true }]}">
+
+```vue
+<template>
+  <!-- ✓ Good -->
+  <a link="http://example.com" target="_blank" rel="noopener">link</a>
+
+  <!-- ✗ BAD -->
+  <a link="http://example.com" target="_blank" >link</a>
+</temlate>
+```
+
+### `{ "enforceDynamicLinks": "always" }` (default)
+
+<eslint-code-block :rules="{'vue/no-template-target-blank': ['error', { enforceDynamicLinks: 'never' }]}">
+
+```vue
+<template>
+  <!-- ✓ Good -->
+  <a :link="link" target="_blank" rel="noopener noreferrer">link</a>
+
+  <!-- ✗ BAD -->
+  <a :link="link" target="_blank">link</a>
+</temlate>
+```
+
+### `{ "enforceDynamicLinks": "never" }`
+
+<eslint-code-block :rules="{'vue/no-template-target-blank': ['error', { enforceDynamicLinks: 'never' }]}">
+
+```vue
+<template>
+  <!-- ✓ Good -->
+  <a :link="link" target="_blank">link</a>
+
+  <!-- ✗ BAD -->
+  <a link="http://example.com" target="_blank" >link</a>
+</temlate>
+```
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-template-target-blank.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-template-target-blank.js)
diff --git a/docs/rules/no-watch-after-await.md b/docs/rules/no-watch-after-await.md
new file mode 100644
index 00000000..679dbcbf
--- /dev/null
+++ b/docs/rules/no-watch-after-await.md
@@ -0,0 +1,73 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/no-watch-after-await
+description: disallow asynchronously registered `watch`
+---
+# vue/no-watch-after-await
+> disallow asynchronously registered `watch`
+
+- :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 the `watch()` after `await` expression.  
+In `setup()` function, `watch()` should be registered synchronously.
+
+<eslint-code-block :rules="{'vue/no-watch-after-await': ['error']}">
+
+```vue
+<script>
+import { watch } from 'vue'
+export default {
+  async setup() {
+    /* ✓ GOOD */
+    watch(watchSource, () => { /* ... */ })
+
+    await doSomething()
+
+    /* ✗ BAD */
+    watch(watchSource, () => { /* ... */ })
+  }
+}
+</script>
+```
+
+</eslint-code-block>
+
+This rule is not reported when using the stop handle.
+
+<eslint-code-block :rules="{'vue/no-watch-after-await': ['error']}">
+
+```vue
+<script>
+import { watch } from 'vue'
+export default {
+  async setup() {
+    await doSomething()
+
+    /* ✓ GOOD */
+    const stopHandle = watch(watchSource, () => { /* ... */ })
+
+    // later
+    stopHandle()
+  }
+}
+</script>
+```
+
+</eslint-code-block>
+
+## :wrench: Options
+
+Nothing.
+
+## :books: Further reading
+
+- [Vue RFCs - 0013-composition-api](https://github.com/vuejs/rfcs/blob/master/active-rfcs/0013-composition-api.md)
+- [Vue Composition API - API Reference - Stopping the Watcher](https://composition-api.vuejs.org/api.html#stopping-the-watcher)
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-watch-after-await.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-watch-after-await.js)
diff --git a/lib/configs/vue3-essential.js b/lib/configs/vue3-essential.js
index 3e45ddd2..d668791c 100644
--- a/lib/configs/vue3-essential.js
+++ b/lib/configs/vue3-essential.js
@@ -7,11 +7,14 @@ 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',
     'vue/no-deprecated-slot-attribute': 'error',
     'vue/no-deprecated-slot-scope-attribute': 'error',
     'vue/no-deprecated-v-bind-sync': 'error',
+    'vue/no-deprecated-v-on-number-modifiers': 'error',
     'vue/no-dupe-keys': 'error',
     'vue/no-duplicate-attributes': 'error',
     'vue/no-lifecycle-after-await': 'error',
@@ -26,6 +29,7 @@ module.exports = {
     'vue/no-unused-components': 'error',
     'vue/no-unused-vars': 'error',
     'vue/no-use-v-if-with-v-for': 'error',
+    'vue/no-watch-after-await': 'error',
     'vue/require-component-is': 'error',
     'vue/require-prop-type-constructor': 'error',
     'vue/require-render-return': 'error',
diff --git a/lib/index.js b/lib/index.js
index 10e2e876..fec19283 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -40,11 +40,14 @@ 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'),
     'no-deprecated-slot-attribute': require('./rules/no-deprecated-slot-attribute'),
     'no-deprecated-slot-scope-attribute': require('./rules/no-deprecated-slot-scope-attribute'),
     'no-deprecated-v-bind-sync': require('./rules/no-deprecated-v-bind-sync'),
+    'no-deprecated-v-on-number-modifiers': require('./rules/no-deprecated-v-on-number-modifiers'),
     'no-dupe-keys': require('./rules/no-dupe-keys'),
     'no-duplicate-attributes': require('./rules/no-duplicate-attributes'),
     'no-empty-pattern': require('./rules/no-empty-pattern'),
@@ -64,6 +67,7 @@ module.exports = {
     'no-static-inline-styles': require('./rules/no-static-inline-styles'),
     'no-template-key': require('./rules/no-template-key'),
     'no-template-shadow': require('./rules/no-template-shadow'),
+    'no-template-target-blank': require('./rules/no-template-target-blank'),
     'no-textarea-mustache': require('./rules/no-textarea-mustache'),
     'no-unsupported-features': require('./rules/no-unsupported-features'),
     'no-unused-components': require('./rules/no-unused-components'),
@@ -71,6 +75,7 @@ module.exports = {
     'no-use-v-if-with-v-for': require('./rules/no-use-v-if-with-v-for'),
     'no-v-html': require('./rules/no-v-html'),
     'no-v-model-argument': require('./rules/no-v-model-argument'),
+    'no-watch-after-await': require('./rules/no-watch-after-await'),
     'object-curly-spacing': require('./rules/object-curly-spacing'),
     'order-in-components': require('./rules/order-in-components'),
     'padding-line-between-blocks': require('./rules/padding-line-between-blocks'),
diff --git a/lib/rules/no-deprecated-data-object-declaration.js b/lib/rules/no-deprecated-data-object-declaration.js
new file mode 100644
index 00000000..4c8399d7
--- /dev/null
+++ b/lib/rules/no-deprecated-data-object-declaration.js
@@ -0,0 +1,86 @@
+/**
+ * @fileoverview disallow using deprecated object declaration on data
+ * @author yoyo930021
+ */
+'use strict'
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const utils = require('../utils')
+
+function isOpenParen (token) {
+  return token.type === 'Punctuator' && token.value === '('
+}
+
+function isCloseParen (token) {
+  return token.type === 'Punctuator' && token.value === ')'
+}
+
+function getFirstAndLastTokens (node, sourceCode) {
+  let first = sourceCode.getFirstToken(node)
+  let last = sourceCode.getLastToken(node)
+
+  // If the value enclosed by parentheses, update the 'first' and 'last' by the parentheses.
+  while (true) {
+    const prev = sourceCode.getTokenBefore(first)
+    const next = sourceCode.getTokenAfter(last)
+    if (isOpenParen(prev) && isCloseParen(next)) {
+      first = prev
+      last = next
+    } else {
+      return { first, last }
+    }
+  }
+}
+
+// ------------------------------------------------------------------------------
+// Rule Definition
+// ------------------------------------------------------------------------------
+
+module.exports = {
+  meta: {
+    type: 'problem',
+    docs: {
+      description: 'disallow using deprecated object declaration on data',
+      categories: ['vue3-essential'],
+      url: 'https://eslint.vuejs.org/rules/no-deprecated-data-object-declaration.html'
+    },
+    fixable: 'code',
+    schema: [],
+    messages: {
+      objectDeclarationIsDeprecated: "Object declaration on \'data\' property is deprecated. Using function declaration instead."
+    }
+  },
+
+  create (context) {
+    const sourceCode = context.getSourceCode()
+
+    return utils.executeOnVue(context, (obj) => {
+      obj.properties
+        .filter(p =>
+          p.type === 'Property' &&
+          p.key.type === 'Identifier' &&
+          p.key.name === 'data' &&
+          p.value.type !== 'FunctionExpression' &&
+          p.value.type !== 'ArrowFunctionExpression' &&
+          p.value.type !== 'Identifier'
+        )
+        .forEach(p => {
+          context.report({
+            node: p,
+            messageId: 'objectDeclarationIsDeprecated',
+            fix (fixer) {
+              const tokens = getFirstAndLastTokens(p.value, sourceCode)
+
+              return [
+                fixer.insertTextBefore(tokens.first, 'function() {\nreturn '),
+                fixer.insertTextAfter(tokens.last, ';\n}')
+              ]
+            }
+          })
+        })
+    })
+  }
+}
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'
+            })
+          }
+        })
+      })
+    )
+  }
+}
diff --git a/lib/rules/no-deprecated-v-on-number-modifiers.js b/lib/rules/no-deprecated-v-on-number-modifiers.js
new file mode 100644
index 00000000..651991cd
--- /dev/null
+++ b/lib/rules/no-deprecated-v-on-number-modifiers.js
@@ -0,0 +1,58 @@
+/**
+ * @fileoverview disallow using deprecated number (keycode) modifiers
+ * @author yoyo930021
+ */
+'use strict'
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const utils = require('../utils')
+const keyCodeToKey = require('../utils/keycode-to-key.json')
+
+// ------------------------------------------------------------------------------
+// Rule Definition
+// ------------------------------------------------------------------------------
+
+module.exports = {
+  meta: {
+    type: 'problem',
+    docs: {
+      description: 'disallow using deprecated number (keycode) modifiers',
+      categories: ['vue3-essential'],
+      url: 'https://eslint.vuejs.org/rules/no-deprecated-v-on-number-modifiers.html'
+    },
+    fixable: 'code',
+    schema: [],
+    messages: {
+      numberModifierIsDeprecated: "'KeyboardEvent.keyCode' modifier on 'v-on' directive is deprecated. Using 'KeyboardEvent.key' instead."
+    }
+  },
+
+  create (context) {
+    return utils.defineTemplateBodyVisitor(context, {
+      "VAttribute[directive=true][key.name.name='on']" (node) {
+        const modifier = node.key.modifiers.find(mod => Number.isInteger(parseInt(mod.name, 10)))
+        if (!modifier) return
+
+        const keyCodes = parseInt(modifier.name, 10)
+        if (
+          keyCodes > 9 || keyCodes < 0
+        ) {
+          context.report({
+            node,
+            loc: node.loc,
+            messageId: 'numberModifierIsDeprecated',
+            fix: (fixer) => {
+              const key = keyCodeToKey[keyCodes]
+              if (!key) return
+
+              return fixer.replaceTextRange(modifier.range, `${key}`)
+            }
+          })
+        }
+      }
+    })
+  }
+}
diff --git a/lib/rules/no-dupe-keys.js b/lib/rules/no-dupe-keys.js
index 1bee4d94..28018e44 100644
--- a/lib/rules/no-dupe-keys.js
+++ b/lib/rules/no-dupe-keys.js
@@ -10,7 +10,7 @@ const utils = require('../utils')
 // Rule Definition
 // ------------------------------------------------------------------------------
 
-const GROUP_NAMES = ['props', 'computed', 'data', 'methods']
+const GROUP_NAMES = ['props', 'computed', 'data', 'methods', 'setup']
 
 module.exports = {
   meta: {
diff --git a/lib/rules/no-reserved-keys.js b/lib/rules/no-reserved-keys.js
index 80ecada3..249f1054 100644
--- a/lib/rules/no-reserved-keys.js
+++ b/lib/rules/no-reserved-keys.js
@@ -11,7 +11,7 @@ const utils = require('../utils')
 // ------------------------------------------------------------------------------
 
 const RESERVED_KEYS = require('../utils/vue-reserved.json')
-const GROUP_NAMES = ['props', 'computed', 'data', 'methods']
+const GROUP_NAMES = ['props', 'computed', 'data', 'methods', 'setup']
 
 module.exports = {
   meta: {
diff --git a/lib/rules/no-template-target-blank.js b/lib/rules/no-template-target-blank.js
new file mode 100644
index 00000000..bf43e19e
--- /dev/null
+++ b/lib/rules/no-template-target-blank.js
@@ -0,0 +1,111 @@
+/**
+ * @fileoverview disallow target="_blank" attribute without rel="noopener noreferrer"
+ * @author Sosukesuzuki
+ */
+'use strict'
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const utils = require('../utils')
+
+// ------------------------------------------------------------------------------
+// Helpers
+// ------------------------------------------------------------------------------
+function isTargetBlank (node) {
+  return node.key &&
+    node.key.name === 'target' &&
+    node.value &&
+    node.value.value === '_blank'
+}
+
+function hasSecureRel (node, allowReferrer) {
+  return node.attributes.some(attr => {
+    if (attr.key && attr.key.name === 'rel') {
+      const tags = attr.value && attr.value.value.toLowerCase().split(' ')
+      return tags &&
+        tags.includes('noopener') &&
+        (allowReferrer || tags.includes('noreferrer'))
+    } else {
+      return false
+    }
+  })
+}
+
+function hasExternalLink (node) {
+  return node.attributes.some(attr =>
+    attr.key &&
+    attr.key.name === 'href' &&
+    attr.value && /^(?:\w+:|\/\/)/.test(attr.value.value)
+  )
+}
+
+function hasDynamicLink (node) {
+  return node.attributes.some(attr =>
+    attr.key &&
+    attr.key.type === 'VDirectiveKey' &&
+    attr.key.name &&
+    attr.key.name.name === 'bind' &&
+    attr.key.argument &&
+    attr.key.argument.name === 'href'
+  )
+}
+
+// ------------------------------------------------------------------------------
+// Rule Definition
+// ------------------------------------------------------------------------------
+
+module.exports = {
+  meta: {
+    type: 'problem',
+    docs: {
+      description:
+        'disallow target="_blank" attribute without rel="noopener noreferrer"',
+      categories: undefined,
+      url: 'https://eslint.vuejs.org/rules/no-template-target-blank.html'
+    },
+    schema: [{
+      type: 'object',
+      properties: {
+        allowReferrer: {
+          type: 'boolean'
+        },
+        enforceDynamicLinks: {
+          enum: ['always', 'never']
+        }
+      },
+      additionalProperties: false
+    }]
+  },
+
+  /**
+   * Creates AST event handlers for no-template-target-blank
+   *
+   * @param {RuleContext} context - The rule context.
+   * @returns {Object} AST event handlers.
+   */
+  create (context) {
+    const configuration = context.options[0] || {}
+    const allowReferrer = configuration.allowReferrer || false
+    const enforceDynamicLinks = configuration.enforceDynamicLinks || 'always'
+
+    return utils.defineTemplateBodyVisitor(context, {
+      'VAttribute' (node) {
+        if (!isTargetBlank(node) || hasSecureRel(node.parent, allowReferrer)) {
+          return
+        }
+
+        const hasDangerHref = hasExternalLink(node.parent) ||
+          (enforceDynamicLinks === 'always' && hasDynamicLink(node.parent))
+
+        if (hasDangerHref) {
+          context.report({
+            node,
+            message: 'Using target="_blank" without rel="noopener noreferrer" is a security risk.'
+          })
+        }
+      }
+    })
+  }
+}
diff --git a/lib/rules/no-watch-after-await.js b/lib/rules/no-watch-after-await.js
new file mode 100644
index 00000000..f65a459d
--- /dev/null
+++ b/lib/rules/no-watch-after-await.js
@@ -0,0 +1,137 @@
+/**
+ * @author Yosuke Ota
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+const { ReferenceTracker } = require('eslint-utils')
+const utils = require('../utils')
+
+function isMaybeUsedStopHandle (node) {
+  const parent = node.parent
+  if (parent) {
+    if (parent.type === 'VariableDeclarator') {
+      // var foo = watch()
+      return true
+    }
+    if (parent.type === 'AssignmentExpression') {
+      // foo = watch()
+      return true
+    }
+    if (parent.type === 'CallExpression') {
+      // foo(watch())
+      return true
+    }
+    if (parent.type === 'Property') {
+      // {foo: watch()}
+      return true
+    }
+    if (parent.type === 'ArrayExpression') {
+      // [watch()]
+      return true
+    }
+  }
+  return false
+}
+
+module.exports = {
+  meta: {
+    type: 'suggestion',
+    docs: {
+      description: 'disallow asynchronously registered `watch`',
+      categories: ['vue3-essential'],
+      url: 'https://eslint.vuejs.org/rules/no-watch-after-await.html'
+    },
+    fixable: null,
+    schema: [],
+    messages: {
+      forbidden: 'The `watch` after `await` expression are forbidden.'
+    }
+  },
+  create (context) {
+    const watchCallNodes = new Set()
+    const setupFunctions = new Map()
+    const forbiddenNodes = new Map()
+
+    function addForbiddenNode (property, node) {
+      let list = forbiddenNodes.get(property)
+      if (!list) {
+        list = []
+        forbiddenNodes.set(property, list)
+      }
+      list.push(node)
+    }
+
+    let scopeStack = { upper: null, functionNode: null }
+
+    return Object.assign(
+      {
+        'Program' () {
+          const tracker = new ReferenceTracker(context.getScope())
+          const traceMap = {
+            vue: {
+              [ReferenceTracker.ESM]: true,
+              watch: {
+                [ReferenceTracker.CALL]: true
+              },
+              watchEffect: {
+                [ReferenceTracker.CALL]: true
+              }
+            }
+          }
+
+          for (const { node } of tracker.iterateEsmReferences(traceMap)) {
+            watchCallNodes.add(node)
+          }
+        },
+        'Property[value.type=/^(Arrow)?FunctionExpression$/]' (node) {
+          if (utils.getStaticPropertyName(node) !== 'setup') {
+            return
+          }
+
+          setupFunctions.set(node.value, {
+            setupProperty: node,
+            afterAwait: false
+          })
+        },
+        ':function' (node) {
+          scopeStack = { upper: scopeStack, functionNode: node }
+        },
+        'AwaitExpression' () {
+          const setupFunctionData = setupFunctions.get(scopeStack.functionNode)
+          if (!setupFunctionData) {
+            return
+          }
+          setupFunctionData.afterAwait = true
+        },
+        'CallExpression' (node) {
+          const setupFunctionData = setupFunctions.get(scopeStack.functionNode)
+          if (!setupFunctionData || !setupFunctionData.afterAwait) {
+            return
+          }
+
+          if (watchCallNodes.has(node) && !isMaybeUsedStopHandle(node)) {
+            addForbiddenNode(setupFunctionData.setupProperty, node)
+          }
+        },
+        ':function:exit' (node) {
+          scopeStack = scopeStack.upper
+
+          setupFunctions.delete(node)
+        }
+      },
+      utils.executeOnVue(context, obj => {
+        const reportsList = obj.properties
+          .map(item => forbiddenNodes.get(item))
+          .filter(reports => !!reports)
+        for (const reports of reportsList) {
+          for (const node of reports) {
+            context.report({
+              node,
+              messageId: 'forbidden'
+            })
+          }
+        }
+      })
+    )
+  }
+}
diff --git a/lib/utils/index.js b/lib/utils/index.js
index e784f2ab..b302cd6c 100644
--- a/lib/utils/index.js
+++ b/lib/utils/index.js
@@ -550,6 +550,12 @@ module.exports = {
           const isAppVueComponent = isObjectArgument(node)
           return isAppVueComponent
         }
+        if (callee.name === 'defineComponent') {
+          // for Vue.js 3.x
+          // defineComponent({})
+          const isDestructedVueComponent = isObjectArgument(node)
+          return isDestructedVueComponent
+        }
       }
     }
 
diff --git a/lib/utils/keycode-to-key.json b/lib/utils/keycode-to-key.json
new file mode 100644
index 00000000..43f48030
--- /dev/null
+++ b/lib/utils/keycode-to-key.json
@@ -0,0 +1,100 @@
+{
+  "8": "backspace",
+  "9": "tab",
+  "13": "enter",
+  "16": "shift",
+  "17": "ctrl",
+  "18": "alt",
+  "19": "pause-break",
+  "20": "caps-lock",
+  "27": "escape",
+  "33": "page-up",
+  "34": "page-down",
+  "35": "end",
+  "36": "home",
+  "37": "left-arrow",
+  "38": "up-arrow",
+  "39": "right-arrow",
+  "40": "down-arrow",
+  "45": "insert",
+  "46": "delete",
+  "48": "0",
+  "49": "1",
+  "50": "2",
+  "51": "3",
+  "52": "4",
+  "53": "5",
+  "54": "6",
+  "55": "7",
+  "56": "8",
+  "57": "9",
+  "65": "a",
+  "66": "b",
+  "67": "c",
+  "68": "d",
+  "69": "e",
+  "70": "f",
+  "71": "g",
+  "72": "h",
+  "73": "i",
+  "74": "j",
+  "75": "k",
+  "76": "l",
+  "77": "m",
+  "78": "n",
+  "79": "o",
+  "80": "p",
+  "81": "q",
+  "82": "r",
+  "83": "s",
+  "84": "t",
+  "85": "u",
+  "86": "v",
+  "87": "w",
+  "88": "x",
+  "89": "y",
+  "90": "z",
+  "91": "left-window-key",
+  "92": "right-window-key",
+  "93": "select-key",
+  "96": "numpad-0",
+  "97": "numpad-1",
+  "98": "numpad-2",
+  "99": "numpad-3",
+  "100": "numpad-4",
+  "101": "numpad-5",
+  "102": "numpad-6",
+  "103": "numpad-7",
+  "104": "numpad-8",
+  "105": "numpad-9",
+  "106": "multiply",
+  "107": "add",
+  "109": "subtract",
+  "110": "decimal-point",
+  "111": "divide",
+  "112": "f1",
+  "113": "f2",
+  "114": "f3",
+  "115": "f4",
+  "116": "f5",
+  "117": "f6",
+  "118": "f7",
+  "119": "f8",
+  "120": "f9",
+  "121": "f10",
+  "122": "f11",
+  "123": "f12",
+  "144": "num-lock",
+  "145": "scroll-lock",
+  "186": "semi-colon",
+  "187": "equal-sign",
+  "188": "comma",
+  "189": "dash",
+  "190": "period",
+  "191": "forward-slash",
+  "192": "grave-accent",
+  "219": "open-bracket",
+  "220": "back-slash",
+  "221": "close-braket",
+  "222": "single-quote"
+}


Test patch:
diff --git a/tests/lib/rules/no-deprecated-data-object-declaration.js b/tests/lib/rules/no-deprecated-data-object-declaration.js
new file mode 100644
index 00000000..1ced275c
--- /dev/null
+++ b/tests/lib/rules/no-deprecated-data-object-declaration.js
@@ -0,0 +1,289 @@
+/**
+ * @fileoverview disallow using deprecated object declaration on data
+ * @author yoyo930021
+ */
+'use strict'
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const rule = require('../../../lib/rules/no-deprecated-data-object-declaration')
+
+const RuleTester = require('eslint').RuleTester
+
+const parserOptions = {
+  ecmaVersion: 2018,
+  sourceType: 'module'
+}
+
+// ------------------------------------------------------------------------------
+// Tests
+// ------------------------------------------------------------------------------
+
+const ruleTester = new RuleTester()
+ruleTester.run('no-deprecated-data-object-declaration', rule, {
+
+  valid: [
+    {
+      filename: 'test.js',
+      code: `
+        createApp({
+          data: function () {
+            return {
+              foo: 'bar'
+            }
+          }
+        }).mount('#app')
+      `,
+      parserOptions
+    },
+    {
+      filename: 'test.js',
+      code: `
+        createApp({
+          ...data,
+          data () {
+            return {
+              foo: 'bar'
+            }
+          }
+        }).mount('#app')
+      `,
+      parserOptions
+    },
+    {
+      filename: 'test.js',
+      code: `
+        const app = createApp(App)
+        app.component('some-comp', {
+          data: function () {
+            return {
+              foo: 'bar'
+            }
+          }
+        })
+      `,
+      parserOptions
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        export default {
+          data: function () {
+            return {
+              foo: 'bar'
+            }
+          }
+        }
+      `,
+      parserOptions
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        export default {
+          ...foo
+        }
+      `,
+      parserOptions
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        export default {
+          data
+        }
+      `,
+      parserOptions
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        export default {
+          data: () => {
+
+          }
+        }
+      `,
+      parserOptions
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        export default {
+          data () {
+
+          },
+          methods: {
+
+          }
+        }
+      `,
+      parserOptions
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        export default {
+          data () {
+
+          },
+          computed: {
+
+          }
+        }
+      `,
+      parserOptions
+    }
+  ],
+
+  invalid: [
+    {
+      filename: 'test.js',
+      code: `
+        const app = createApp(App)
+        app.component('some-comp', {
+          data: {
+            foo: 'bar'
+          }
+        })
+      `,
+      output: `
+        const app = createApp(App)
+        app.component('some-comp', {
+          data: function() {
+return {
+            foo: 'bar'
+          };
+}
+        })
+      `,
+      parserOptions,
+      errors: [{
+        message: "Object declaration on \'data\' property is deprecated. Using function declaration instead.",
+        line: 4
+      }]
+    },
+    {
+      filename: 'test.js',
+      code: `
+        app.component('some-comp', {
+          data: {
+            foo: 'bar'
+          }
+        })
+      `,
+      output: `
+        app.component('some-comp', {
+          data: function() {
+return {
+            foo: 'bar'
+          };
+}
+        })
+      `,
+      parserOptions,
+      errors: [{
+        message: "Object declaration on \'data\' property is deprecated. Using function declaration instead.",
+        line: 3
+      }]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        export default {
+          data: {
+            foo: 'bar'
+          }
+        }
+      `,
+      output: `
+        export default {
+          data: function() {
+return {
+            foo: 'bar'
+          };
+}
+        }
+      `,
+      parserOptions,
+      errors: [{
+        message: "Object declaration on \'data\' property is deprecated. Using function declaration instead.",
+        line: 3
+      }]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        export default {
+          data: /*a*/ (/*b*/{
+            foo: 'bar'
+          })
+        }
+      `,
+      output: `
+        export default {
+          data: /*a*/ function() {
+return (/*b*/{
+            foo: 'bar'
+          });
+}
+        }
+      `,
+      parserOptions,
+      errors: [{
+        message: "Object declaration on \'data\' property is deprecated. Using function declaration instead.",
+        line: 3
+      }]
+    },
+    {
+      filename: 'test.js',
+      code: `
+        createApp({
+          data: {
+            foo: 'bar'
+          }
+        })
+      `,
+      output: `
+        createApp({
+          data: function() {
+return {
+            foo: 'bar'
+          };
+}
+        })
+      `,
+      parserOptions,
+      errors: [{
+        message: "Object declaration on \'data\' property is deprecated. Using function declaration instead.",
+        line: 3
+      }]
+    },
+    {
+      filename: 'test.js',
+      code: `
+        createApp({
+          data: {
+            foo: 'bar'
+          }
+        }).mount('#app')
+      `,
+      output: `
+        createApp({
+          data: function() {
+return {
+            foo: 'bar'
+          };
+}
+        }).mount('#app')
+      `,
+      parserOptions,
+      errors: [{
+        message: "Object declaration on \'data\' property is deprecated. Using function declaration instead.",
+        line: 3
+      }]
+    }
+  ]
+})
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
+      }]
+    }
+  ]
+})
diff --git a/tests/lib/rules/no-deprecated-v-on-number-modifiers.js b/tests/lib/rules/no-deprecated-v-on-number-modifiers.js
new file mode 100644
index 00000000..8c6d7269
--- /dev/null
+++ b/tests/lib/rules/no-deprecated-v-on-number-modifiers.js
@@ -0,0 +1,174 @@
+/**
+ * @fileoverview disallow using deprecated number (keyCodes) modifiers
+ * @author yoyo930021
+ */
+'use strict'
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const rule = require('../../../lib/rules/no-deprecated-v-on-number-modifiers')
+const RuleTester = require('eslint').RuleTester
+
+// ------------------------------------------------------------------------------
+// Tests
+// ------------------------------------------------------------------------------
+
+const ruleTester = new RuleTester({
+  parser: require.resolve('vue-eslint-parser'),
+  parserOptions: { ecmaVersion: 2015 }
+})
+
+ruleTester.run('no-deprecated-v-bind-sync', rule, {
+
+  valid: [
+    {
+      filename: 'test.vue',
+      code: "<template><input v-on:keyup.page-down='onArrowUp'></template>"
+    },
+    {
+      filename: 'test.vue',
+      code: "<template><input @keyup.page-down='onArrowUp'></template>"
+    },
+    {
+      filename: 'test.vue',
+      code: "<template><input v-on:keyup.9='onArrowUp'></template>"
+    },
+    {
+      filename: 'test.vue',
+      code: "<template><input @keyup.9='onArrowUp'></template>"
+    },
+    {
+      filename: 'test.vue',
+      code: "<template><input v-on:keyup.0='onArrowUp'></template>"
+    },
+    {
+      filename: 'test.vue',
+      code: "<template><input @keyup.0='onArrowUp'></template>"
+    },
+    {
+      filename: 'test.vue',
+      code: "<template><input v-on:keyup.4='onArrowUp'></template>"
+    },
+    {
+      filename: 'test.vue',
+      code: "<template><input @keyup.4='onArrowUp'></template>"
+    },
+    {
+      filename: 'test.vue',
+      code: "<template><input v-on:keyup.page-down.native='onArrowUp'></template>"
+    },
+    {
+      filename: 'test.vue',
+      code: "<template><input @keyup.page-down.native='onArrowUp'></template>"
+    },
+    {
+      filename: 'test.vue',
+      code: "<template><input v-on:keyup.0.native='onArrowUp'></template>"
+    },
+    {
+      filename: 'test.vue',
+      code: "<template><input @keyup.0.native='onArrowUp'></template>"
+    }
+  ],
+
+  invalid: [
+    {
+      filename: 'test.vue',
+      code: "<template><input v-on:keyup.34='onArrowUp'></template>",
+      output: "<template><input v-on:keyup.page-down='onArrowUp'></template>",
+      errors: ["'KeyboardEvent.keyCode' modifier on 'v-on' directive is deprecated. Using 'KeyboardEvent.key' instead."]
+    },
+    {
+      filename: 'test.vue',
+      code: "<template><input v-on:keyup.34.native='onArrowUp'></template>",
+      output: "<template><input v-on:keyup.page-down.native='onArrowUp'></template>",
+      errors: ["'KeyboardEvent.keyCode' modifier on 'v-on' directive is deprecated. Using 'KeyboardEvent.key' instead."]
+    },
+    {
+      filename: 'test.vue',
+      code: "<template><input v-on:keyup.unknown.34='onArrowUp'></template>",
+      output: "<template><input v-on:keyup.unknown.page-down='onArrowUp'></template>",
+      errors: ["'KeyboardEvent.keyCode' modifier on 'v-on' directive is deprecated. Using 'KeyboardEvent.key' instead."]
+    },
+    {
+      filename: 'test.vue',
+      code: "<template><input v-on:[dynamicArg].34='onArrowUp'></template>",
+      output: "<template><input v-on:[dynamicArg].page-down='onArrowUp'></template>",
+      errors: ["'KeyboardEvent.keyCode' modifier on 'v-on' directive is deprecated. Using 'KeyboardEvent.key' instead."]
+    },
+    {
+      filename: 'test.vue',
+      code: "<template><input v-on:[dynamicArg].unknown.34='onArrowUp'></template>",
+      output: "<template><input v-on:[dynamicArg].unknown.page-down='onArrowUp'></template>",
+      errors: ["'KeyboardEvent.keyCode' modifier on 'v-on' directive is deprecated. Using 'KeyboardEvent.key' instead."]
+    },
+    {
+      filename: 'test.vue',
+      code: "<template><input v-on:[dynamicArg].34.unknown='onArrowUp'></template>",
+      output: "<template><input v-on:[dynamicArg].page-down.unknown='onArrowUp'></template>",
+      errors: ["'KeyboardEvent.keyCode' modifier on 'v-on' directive is deprecated. Using 'KeyboardEvent.key' instead."]
+    },
+    {
+      filename: 'test.vue',
+      code: "<template><input @keyup.34='onArrowUp'></template>",
+      output: "<template><input @keyup.page-down='onArrowUp'></template>",
+      errors: ["'KeyboardEvent.keyCode' modifier on 'v-on' directive is deprecated. Using 'KeyboardEvent.key' instead."]
+    },
+    {
+      filename: 'test.vue',
+      code: "<template><input @keyup.34.native='onArrowUp'></template>",
+      output: "<template><input @keyup.page-down.native='onArrowUp'></template>",
+      errors: ["'KeyboardEvent.keyCode' modifier on 'v-on' directive is deprecated. Using 'KeyboardEvent.key' instead."]
+    },
+    {
+      filename: 'test.vue',
+      code: "<template><input @keyup.unknown.34='onArrowUp'></template>",
+      output: "<template><input @keyup.unknown.page-down='onArrowUp'></template>",
+      errors: ["'KeyboardEvent.keyCode' modifier on 'v-on' directive is deprecated. Using 'KeyboardEvent.key' instead."]
+    },
+    {
+      filename: 'test.vue',
+      code: "<template><input @[dynamicArg].34='onArrowUp'></template>",
+      output: "<template><input @[dynamicArg].page-down='onArrowUp'></template>",
+      errors: ["'KeyboardEvent.keyCode' modifier on 'v-on' directive is deprecated. Using 'KeyboardEvent.key' instead."]
+    },
+    {
+      filename: 'test.vue',
+      code: "<template><input @[dynamicArg].unknown.34='onArrowUp'></template>",
+      output: "<template><input @[dynamicArg].unknown.page-down='onArrowUp'></template>",
+      errors: ["'KeyboardEvent.keyCode' modifier on 'v-on' directive is deprecated. Using 'KeyboardEvent.key' instead."]
+    },
+    {
+      filename: 'test.vue',
+      code: "<template><input @[dynamicArg].34.unknown='onArrowUp'></template>",
+      output: "<template><input @[dynamicArg].page-down.unknown='onArrowUp'></template>",
+      errors: ["'KeyboardEvent.keyCode' modifier on 'v-on' directive is deprecated. Using 'KeyboardEvent.key' instead."]
+    },
+    {
+      filename: 'test.vue',
+      code: "<template><input @keyup.10='onArrowUp'></template>",
+      output: null,
+      errors: ["'KeyboardEvent.keyCode' modifier on 'v-on' directive is deprecated. Using 'KeyboardEvent.key' instead."]
+    },
+    {
+      filename: 'test.vue',
+      code: "<template><input @keyup.10.native='onArrowUp'></template>",
+      output: null,
+      errors: ["'KeyboardEvent.keyCode' modifier on 'v-on' directive is deprecated. Using 'KeyboardEvent.key' instead."]
+    },
+    {
+      filename: 'test.vue',
+      code: "<template><input @keyup.unknown.10='onArrowUp'></template>",
+      output: null,
+      errors: ["'KeyboardEvent.keyCode' modifier on 'v-on' directive is deprecated. Using 'KeyboardEvent.key' instead."]
+    },
+    {
+      filename: 'test.vue',
+      code: "<template><input @[dynamicArg].unknown.10='onArrowUp'></template>",
+      output: null,
+      errors: ["'KeyboardEvent.keyCode' modifier on 'v-on' directive is deprecated. Using 'KeyboardEvent.key' instead."]
+    }
+  ]
+})
diff --git a/tests/lib/rules/no-dupe-keys.js b/tests/lib/rules/no-dupe-keys.js
index f1a35568..a3bc06e7 100644
--- a/tests/lib/rules/no-dupe-keys.js
+++ b/tests/lib/rules/no-dupe-keys.js
@@ -44,6 +44,37 @@ ruleTester.run('no-dupe-keys', rule, {
       `,
       parserOptions: { ecmaVersion: 6, sourceType: 'module' }
     },
+    {
+      filename: 'test.vue',
+      code: `
+        export default {
+          props: ['foo'],
+          data () {
+            return {
+              dat: null
+            }
+          },
+          data () {
+            return
+          },
+          methods: {
+            _foo () {},
+            test () {
+            }
+          },
+          setup () {
+            const _foo = () => {}
+            const dat = ref(null)
+            const bar = computed(() => 'bar')
+
+            return {
+              bar
+            }
+          }
+        }
+      `,
+      parserOptions: { ecmaVersion: 6, sourceType: 'module' }
+    },
 
     {
       filename: 'test.vue',
@@ -134,6 +165,51 @@ ruleTester.run('no-dupe-keys', rule, {
       parserOptions: { ecmaVersion: 2018, sourceType: 'module' }
     },
 
+    {
+      filename: 'test.vue',
+      code: `
+        export default {
+          ...foo(),
+          props: {
+            ...foo(),
+            foo: String
+          },
+          computed: {
+            ...mapGetters({
+              test: 'getTest'
+            }),
+            bar: {
+              get () {
+              }
+            }
+          },
+          data: {
+            ...foo(),
+            dat: null
+          },
+          methods: {
+            ...foo(),
+            test () {
+            }
+          },
+          data () {
+            return {
+              ...dat
+            }
+          },
+          setup () {
+            const com = computed(() => 1)
+
+            return {
+              ...foo(),
+              com
+            }
+          }
+        }
+      `,
+      parserOptions: { ecmaVersion: 2018, sourceType: 'module' }
+    },
+
     {
       filename: 'test.vue',
       code: `
@@ -224,6 +300,39 @@ ruleTester.run('no-dupe-keys', rule, {
         }
       `,
       parserOptions: { ecmaVersion: 6, sourceType: 'module' }
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        // @vue/component
+        export const compA = {
+          props: {
+            propA: String
+          },
+          setup (props) {
+            const com = computed(() => props.propA)
+
+            return {
+              com
+            }
+          }
+        }
+
+        // @vue/component
+        export const compB = {
+          props: {
+            propA: String
+          },
+          setup (props) {
+            const com = computed(() => props.propA)
+
+            return {
+              com
+            }
+          }
+        }
+      `,
+      parserOptions: { ecmaVersion: 6, sourceType: 'module' }
     }
   ],
 
@@ -245,6 +354,13 @@ ruleTester.run('no-dupe-keys', rule, {
           methods: {
             foo () {
             }
+          },
+          setup () {
+            const foo = ref(1)
+
+            return {
+              foo
+            }
           }
         }
       `,
@@ -258,6 +374,9 @@ ruleTester.run('no-dupe-keys', rule, {
       }, {
         message: 'Duplicated key \'foo\'.',
         line: 14
+      }, {
+        message: 'Duplicated key \'foo\'.',
+        line: 21
       }]
     },
     {
@@ -277,6 +396,13 @@ ruleTester.run('no-dupe-keys', rule, {
           methods: {
             foo () {
             }
+          },
+          setup: () => {
+            const foo = computed(() => 0)
+
+            return {
+              foo
+            }
           }
         }
       `,
@@ -290,6 +416,9 @@ ruleTester.run('no-dupe-keys', rule, {
       }, {
         message: 'Duplicated key \'foo\'.',
         line: 14
+      }, {
+        message: 'Duplicated key \'foo\'.',
+        line: 21
       }]
     },
     {
@@ -341,6 +470,13 @@ ruleTester.run('no-dupe-keys', rule, {
           methods: {
             foo () {
             }
+          },
+          setup (props) {
+            const foo = computed(() => props.foo)
+
+            return {
+              foo
+            }
           }
         }
       `,
@@ -354,6 +490,9 @@ ruleTester.run('no-dupe-keys', rule, {
       }, {
         message: 'Duplicated key \'foo\'.',
         line: 16
+      }, {
+        message: 'Duplicated key \'foo\'.',
+        line: 23
       }]
     },
     {
@@ -374,6 +513,170 @@ ruleTester.run('no-dupe-keys', rule, {
         message: 'Duplicated key \'bar\'.',
         line: 7
       }]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        export default {
+          methods: {
+            foo () {
+              return 0
+            }
+          },
+          setup () {
+            const foo = () => 0
+
+            return {
+              foo
+            }
+          }
+        }
+      `,
+      parserOptions: { ecmaVersion: 6, sourceType: 'module' },
+      errors: [{
+        message: 'Duplicated key \'foo\'.',
+        line: 12
+      }]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        export default {
+          methods: {
+            foo () {
+              return 0
+            }
+          },
+          setup () {
+            return {
+              foo: () => 0
+            }
+          }
+        }
+      `,
+      parserOptions: { ecmaVersion: 6, sourceType: 'module' },
+      errors: [{
+        message: 'Duplicated key \'foo\'.',
+        line: 10
+      }]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        export default {
+          methods: {
+            foo () {
+              return 0
+            }
+          },
+          setup: () => ({
+            foo: () => 0
+          })
+        }
+      `,
+      parserOptions: { ecmaVersion: 6, sourceType: 'module' },
+      errors: [{
+        message: 'Duplicated key \'foo\'.',
+        line: 9
+      }]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        export default {
+          computed: {
+            foo () {
+              return 0
+            }
+          },
+          setup: () => ({
+            foo: computed(() => 0)
+          })
+        }
+      `,
+      parserOptions: { ecmaVersion: 6, sourceType: 'module' },
+      errors: [{
+        message: 'Duplicated key \'foo\'.',
+        line: 9
+      }]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        export default {
+          data() {
+            return {
+              foo: 0
+            }
+          },
+          setup: () => ({
+            foo: ref(0)
+          })
+        }
+      `,
+      parserOptions: { ecmaVersion: 6, sourceType: 'module' },
+      errors: [{
+        message: 'Duplicated key \'foo\'.',
+        line: 9
+      }]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        export default {
+          data() {
+            return {
+              foo: 0
+            }
+          },
+          setup: () => ({
+            foo: 0
+          })
+        }
+      `,
+      parserOptions: { ecmaVersion: 6, sourceType: 'module' },
+      errors: [{
+        message: 'Duplicated key \'foo\'.',
+        line: 9
+      }]
+    },
+    {
+      filename: 'test.js',
+      code: `
+        defineComponent({
+          foo: {
+            bar: String
+          },
+          data: {
+            bar: null
+          },
+        })
+      `,
+      options: [{ groups: ['foo'] }],
+      parserOptions: { ecmaVersion: 6 },
+      errors: [{
+        message: 'Duplicated key \'bar\'.',
+        line: 7
+      }]
+    },
+    {
+      filename: 'test.js',
+      code: `
+        export default defineComponent({
+          foo: {
+            bar: String
+          },
+          data: {
+            bar: null
+          },
+        })
+      `,
+      options: [{ groups: ['foo'] }],
+      parserOptions: { ecmaVersion: 6, sourceType: 'module' },
+      errors: [{
+        message: 'Duplicated key \'bar\'.',
+        line: 7
+      }]
     }
   ]
 })
diff --git a/tests/lib/rules/no-reserved-keys.js b/tests/lib/rules/no-reserved-keys.js
index 05836bc4..f3bc64ac 100644
--- a/tests/lib/rules/no-reserved-keys.js
+++ b/tests/lib/rules/no-reserved-keys.js
@@ -89,6 +89,32 @@ ruleTester.run('no-reserved-keys', rule, {
         }
       `,
       parserOptions
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        export default {
+          props: ['foo'],
+          computed: {
+            bar () {
+            }
+          },
+          data: () => ({
+            dat: null
+          }),
+          methods: {
+            _foo () {},
+            test () {
+            }
+          },
+          setup () {
+            return {
+              _bar: () => {}
+            }
+          }
+        }
+      `,
+      parserOptions
     }
   ],
 
@@ -108,6 +134,23 @@ ruleTester.run('no-reserved-keys', rule, {
         line: 4
       }]
     },
+    {
+      filename: 'test.js',
+      code: `
+        new Vue({
+          setup () {
+            return {
+              $el: ''
+            }
+          }
+        })
+      `,
+      parserOptions: { ecmaVersion: 6 },
+      errors: [{
+        message: "Key '$el' is reserved.",
+        line: 5
+      }]
+    },
     {
       filename: 'test.js',
       code: `
diff --git a/tests/lib/rules/no-template-target-blank.js b/tests/lib/rules/no-template-target-blank.js
new file mode 100644
index 00000000..41b5c1c4
--- /dev/null
+++ b/tests/lib/rules/no-template-target-blank.js
@@ -0,0 +1,68 @@
+/**
+ * @fileoverview disallow target="_blank" attribute without rel="noopener noreferrer"
+ * @author Sosukesuzuki
+ */
+'use strict'
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const rule = require('../../../lib/rules/no-template-target-blank')
+
+const RuleTester = require('eslint').RuleTester
+
+// ------------------------------------------------------------------------------
+// Tests
+// ------------------------------------------------------------------------------
+
+const ruleTester = new RuleTester({
+  parser: require.resolve('vue-eslint-parser'),
+  parserOptions: { ecmaVersion: 2015 }
+})
+
+ruleTester.run('no-template-target-blank', rule, {
+  valid: [
+    { code: '<template><a>link</a></template>' },
+    { code: '<template><a attr>link</a></template>' },
+    { code: '<template><a target>link</a></template>' },
+    { code: '<template><a href="https://eslint.vuejs.org">link</a></template>' },
+    { code: '<template><a :href="link">link</a></template>' },
+    { code: '<template><a :href="link" target="_blank" rel="noopener noreferrer">link</a></template>' },
+    { code: '<template><a href="https://eslint.vuejs.org" target="_blank" rel="noopener noreferrer">link</a></template>' },
+    {
+      code: '<template><a href="https://eslint.vuejs.org" target="_blank" rel="noopener">link</a></template>',
+      options: [{ allowReferrer: true }]
+    },
+    { code: '<template><a href="/foo" target="_blank">link</a></template>' },
+    { code: '<template><a href="/foo" target="_blank" rel="noopener noreferrer">link</a></template>' },
+    { code: '<template><a href="foo/bar" target="_blank">link</a></template>' },
+    { code: '<template><a href="foo/bar" target="_blank" rel="noopener noreferrer">link</a></template>' },
+    {
+      code: '<template><a :href="link" target="_blank">link</a></template>',
+      options: [{ enforceDynamicLinks: 'never' }]
+    }
+  ],
+  invalid: [
+    {
+      code: '<template><a href="https://eslint.vuejs.org" target="_blank">link</a></template>',
+      errors: ['Using target="_blank" without rel="noopener noreferrer" is a security risk.']
+    },
+    {
+      code: '<template><a href="https://eslint.vuejs.org" target="_blank" rel="noopenernoreferrer">link</a></template>',
+      errors: ['Using target="_blank" without rel="noopener noreferrer" is a security risk.']
+    },
+    {
+      code: '<template><a :href="link" target="_blank" rel=3>link</a></template>',
+      errors: ['Using target="_blank" without rel="noopener noreferrer" is a security risk.']
+    },
+    {
+      code: '<template><a :href="link" target="_blank">link</a></template>',
+      errors: ['Using target="_blank" without rel="noopener noreferrer" is a security risk.']
+    },
+    {
+      code: '<template><a href="https://eslint.vuejs.org" target="_blank" rel="noopener">link</a></template>',
+      errors: ['Using target="_blank" without rel="noopener noreferrer" is a security risk.']
+    }
+  ]
+})
diff --git a/tests/lib/rules/no-watch-after-await.js b/tests/lib/rules/no-watch-after-await.js
new file mode 100644
index 00000000..9d91fbb2
--- /dev/null
+++ b/tests/lib/rules/no-watch-after-await.js
@@ -0,0 +1,177 @@
+/**
+ * @author Yosuke Ota
+ */
+'use strict'
+
+const RuleTester = require('eslint').RuleTester
+const rule = require('../../../lib/rules/no-watch-after-await')
+
+const tester = new RuleTester({
+  parser: require.resolve('vue-eslint-parser'),
+  parserOptions: { ecmaVersion: 2019, sourceType: 'module' }
+})
+
+tester.run('no-watch-after-await', rule, {
+  valid: [
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      import {watch} from 'vue'
+      export default {
+        async setup() {
+          watch(foo, () => { /* ... */ }) // ok
+
+          await doSomething()
+        }
+      }
+      </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      import {watch} from 'vue'
+      export default {
+        async setup() {
+          watch(foo, () => { /* ... */ })
+        }
+      }
+      </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      import {watch, watchEffect} from 'vue'
+      export default {
+        async setup() {
+          watchEffect(() => { /* ... */ })
+          watch(foo, () => { /* ... */ })
+          await doSomething()
+        }
+      }
+      </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      import {onMounted} from 'vue'
+      export default {
+        async _setup() {
+          await doSomething()
+
+          onMounted(() => { /* ... */ }) // error
+        }
+      }
+      </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      import {watch, watchEffect} from 'vue'
+      export default {
+        async setup() {
+          await doSomething()
+          const a = watchEffect(() => { /* ... */ })
+          const b = watch(foo, () => { /* ... */ })
+          c = watch()
+          d(watch())
+          e = {
+            foo: watch()
+          }
+          f = [watch()]
+        }
+      }
+      </script>
+      `
+    }
+  ],
+  invalid: [
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      import {watch} from 'vue'
+      export default {
+        async setup() {
+          await doSomething()
+
+          watch(foo, () => { /* ... */ }) // error
+        }
+      }
+      </script>
+      `,
+      errors: [
+        {
+          message: 'The `watch` after `await` expression are forbidden.',
+          line: 8,
+          column: 11,
+          endLine: 8,
+          endColumn: 42
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      import {watch, watchEffect} from 'vue'
+      export default {
+        async setup() {
+          await doSomething()
+
+          watchEffect(() => { /* ... */ })
+          watch(foo, () => { /* ... */ })
+        }
+      }
+      </script>
+      `,
+      errors: [
+        {
+          messageId: 'forbidden',
+          line: 8
+        },
+        {
+          messageId: 'forbidden',
+          line: 9
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      import {watch} from 'vue'
+      export default {
+        async setup() {
+          await doSomething()
+
+          watch(foo, () => { /* ... */ })
+
+          await doSomething()
+
+          watch(foo, () => { /* ... */ })
+        }
+      }
+      </script>
+      `,
+      errors: [
+        {
+          messageId: 'forbidden',
+          line: 8
+        },
+        {
+          messageId: 'forbidden',
+          line: 12
+        }
+      ]
+    }
+  ]
+})
diff --git a/tests/lib/utils/vue-component.js b/tests/lib/utils/vue-component.js
index 69d663e9..794075d4 100644
--- a/tests/lib/utils/vue-component.js
+++ b/tests/lib/utils/vue-component.js
@@ -311,6 +311,12 @@ function invalidTests (ext) {
       `,
       parserOptions,
       errors: (ext === 'js' ? [] : [makeError(2)]).concat([makeError(8)])
+    },
+    {
+      filename: `test.${ext}`,
+      code: `export default defineComponent({})`,
+      parserOptions,
+      errors: [makeError(1)]
     }
   ]
 }