Repository: vuejs/eslint-plugin-vue
Pull request #1387 (2020-12-25 06:42:46)

Add `deepData` option to `vue/no-unused-properties` rule
This PR adds `deepData` option to `vue/no-unused-properties` rule.

If `true`, the object of the property defined in `data` will be searched deeply. Include `"data"` in `groups` to use this option.

e.g. 


```vue
<template>
  <Foo :param="state.used">
</template>
<script>
  export default {
    data() {
      return {
        state: {
          /* ✓ GOOD */
          used: null,
          /* ✗ BAD (`state.unused` data not used) */
          unused: null
        }
      }
    }
  }
</script>
```


Patch:
diff --git a/docs/rules/README.md b/docs/rules/README.md
index 68677d25..5f848467 100644
--- a/docs/rules/README.md
+++ b/docs/rules/README.md
@@ -301,6 +301,8 @@ For example:
 | [vue/no-multiple-objects-in-class](./no-multiple-objects-in-class.md) | disallow to pass multiple objects into array to class |  |
 | [vue/no-potential-component-option-typo](./no-potential-component-option-typo.md) | disallow a potential typo in your component property |  |
 | [vue/no-reserved-component-names](./no-reserved-component-names.md) | disallow the use of reserved names in component definitions |  |
+| [vue/no-restricted-block](./no-restricted-block.md) | disallow specific block |  |
+| [vue/no-restricted-call-after-await](./no-restricted-call-after-await.md) | disallow asynchronously called restricted methods |  |
 | [vue/no-restricted-component-options](./no-restricted-component-options.md) | disallow specific component option |  |
 | [vue/no-restricted-custom-event](./no-restricted-custom-event.md) | disallow specific custom event |  |
 | [vue/no-restricted-props](./no-restricted-props.md) | disallow specific props |  |
@@ -320,6 +322,7 @@ For example:
 | [vue/sort-keys](./sort-keys.md) | enforce sort-keys in a manner that is compatible with order-in-components |  |
 | [vue/static-class-names-order](./static-class-names-order.md) | enforce static class names order | :wrench: |
 | [vue/v-for-delimiter-style](./v-for-delimiter-style.md) | enforce `v-for` directive's delimiter style | :wrench: |
+| [vue/v-on-event-hyphenation](./v-on-event-hyphenation.md) | enforce v-on event naming style on custom components in template | :wrench: |
 | [vue/v-on-function-call](./v-on-function-call.md) | enforce or forbid parentheses after method calls without arguments in `v-on` directives | :wrench: |
 
 ### Extension Rules
diff --git a/docs/rules/attribute-hyphenation.md b/docs/rules/attribute-hyphenation.md
index fc7bba75..cc639183 100644
--- a/docs/rules/attribute-hyphenation.md
+++ b/docs/rules/attribute-hyphenation.md
@@ -47,6 +47,7 @@ Default casing is set to `always` with `['data-', 'aria-', 'slot-scope']` set to
 - `"ignore"` ... Array of ignored names
 
 ### `"always"`
+
 It errors on upper case letters.
 
 <eslint-code-block fix :rules="{'vue/attribute-hyphenation': ['error', 'always']}">
@@ -64,6 +65,7 @@ It errors on upper case letters.
 </eslint-code-block>
 
 ### `"never"`
+
 It errors on hyphens except `data-`, `aria-` and `slot-scope`.
 
 <eslint-code-block fix :rules="{'vue/attribute-hyphenation': ['error', 'never']}">
@@ -84,6 +86,7 @@ It errors on hyphens except `data-`, `aria-` and `slot-scope`.
 </eslint-code-block>
 
 ### `"never", { "ignore": ["custom-prop"] }`
+
 Don't use hyphenated name but allow custom attributes
 
 <eslint-code-block fix :rules="{'vue/attribute-hyphenation': ['error', 'never', { ignore: ['custom-prop']}]}">
@@ -104,6 +107,11 @@ Don't use hyphenated name but allow custom attributes
 
 </eslint-code-block>
 
+## :couple: Related Rules
+
+- [vue/v-on-event-hyphenation](./v-on-event-hyphenation.md)
+- [vue/prop-name-casing](./prop-name-casing.md)
+
 ## :rocket: Version
 
 This rule was introduced in eslint-plugin-vue v3.9.0
diff --git a/docs/rules/attributes-order.md b/docs/rules/attributes-order.md
index e5c8a91f..8bcb8b8e 100644
--- a/docs/rules/attributes-order.md
+++ b/docs/rules/attributes-order.md
@@ -91,7 +91,33 @@ This rule aims to enforce ordering of component attributes. The default order is
 
 </eslint-code-block>
 
+Note that `v-bind="object"` syntax is considered to be the same as the next or previous attribute categories.
+
+<eslint-code-block fix :rules="{'vue/attributes-order': ['error']}">
+
+```vue
+<template>
+  <!-- ✓ GOOD (`v-bind="object"` is considered GLOBAL category) -->
+  <MyComponent
+    v-bind="object"
+    id="x"
+    v-model="x"
+    v-bind:foo="x">
+  </MyComponent>
+
+  <!-- ✗ BAD (`v-bind="object"` is considered UNIQUE category) -->
+  <MyComponent
+    key="x"
+    v-model="x"
+    v-bind="object">
+  </MyComponent>
+</template>
+```
+
+</eslint-code-block>
+
 ## :wrench: Options
+
 ```json
 {
   "vue/attributes-order": ["error", {
@@ -113,7 +139,7 @@ This rule aims to enforce ordering of component attributes. The default order is
 }
 ```
 
-### `"alphabetical": true` 
+### `"alphabetical": true`
 
 <eslint-code-block fix :rules="{'vue/attributes-order': ['error', {alphabetical: true}]}">
 
diff --git a/docs/rules/custom-event-name-casing.md b/docs/rules/custom-event-name-casing.md
index 16a46dfd..ae08fc25 100644
--- a/docs/rules/custom-event-name-casing.md
+++ b/docs/rules/custom-event-name-casing.md
@@ -23,9 +23,9 @@ Vue 2 recommends using kebab-case for custom event names.
 
 See [Guide (for v2) - Custom Events] for more details.
 
-Vue 3 recommends using camelCase for custom event names.
+In Vue 3, using either camelCase or kebab-case for your custom event name does not limit its use in v-on. However, following JavaScript conventions, camelCase is more natural.
 
-See [vuejs/docs-next#656](https://github.com/vuejs/docs-next/issues/656) for more details.
+See [Guide - Custom Events] for more details.
 
 This rule enforces kebab-case by default.
 
@@ -171,6 +171,11 @@ export default {
 [Guide - Custom Events]: https://v3.vuejs.org/guide/component-custom-events.html
 [Guide (for v2) - Custom Events]: https://vuejs.org/v2/guide/components-custom-events.html
 
+## :couple: Related Rules
+
+- [vue/v-on-event-hyphenation](./v-on-event-hyphenation.md)
+- [vue/prop-name-casing](./prop-name-casing.md)
+
 ## :rocket: Version
 
 This rule was introduced in eslint-plugin-vue v7.0.0
diff --git a/docs/rules/no-restricted-block.md b/docs/rules/no-restricted-block.md
new file mode 100644
index 00000000..f84b1374
--- /dev/null
+++ b/docs/rules/no-restricted-block.md
@@ -0,0 +1,88 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/no-restricted-block
+description: disallow specific block
+---
+# vue/no-restricted-block
+
+> disallow specific block
+
+- :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 allows you to specify block names that you don't want to use in your application.
+
+## :wrench: Options
+
+This rule takes a list of strings, where each string is a block name or pattern to be restricted:
+
+```json
+{
+  "vue/no-restricted-block": ["error", "style", "foo", "bar"]
+}
+```
+
+<eslint-code-block :rules="{'vue/no-restricted-block': ['error', 'style', 'foo', 'bar']}">
+
+```vue
+<!-- ✗ BAD -->
+<foo>
+  Custom block
+</foo>
+<bar>
+  Custom block
+</bar>
+<style>
+.foo {}
+</style>
+```
+
+</eslint-code-block>
+
+Alternatively, the rule also accepts objects.
+
+```json
+{
+  "vue/no-restricted-block": ["error",
+    {
+      "element": "style",
+      "message": "Do not use <style> block in this project."
+    },
+    {
+      "element": "foo",
+      "message": "Do not use <foo> block in this project."
+    },
+    {
+      "element": "/forbidden/",
+      "message": "Do not use blocks that include `forbidden` in their name."
+    }
+  ]
+}
+```
+
+The following properties can be specified for the object.
+
+- `element` ... Specify the block element name or pattern.
+- `message` ... Specify an optional custom message.
+
+### `{ "element": "foo"  }, { "element": "/forbidden/"  }`
+
+<eslint-code-block :rules="{'vue/no-restricted-block': ['error', { element: 'foo' }, { element: '/forbidden/' }]}">
+
+```vue
+<!-- ✗ BAD -->
+<foo>
+  ✗ BAD
+</foo>
+<forbidden-block></forbidden-block>
+<block-forbidden></block-forbidden>
+```
+
+</eslint-code-block>
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-restricted-block.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-restricted-block.js)
diff --git a/docs/rules/no-restricted-call-after-await.md b/docs/rules/no-restricted-call-after-await.md
new file mode 100644
index 00000000..735fc87b
--- /dev/null
+++ b/docs/rules/no-restricted-call-after-await.md
@@ -0,0 +1,98 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/no-restricted-call-after-await
+description: disallow asynchronously called restricted methods
+---
+# vue/no-restricted-call-after-await
+
+> disallow asynchronously called restricted methods
+
+- :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 your restricted calls after the `await` expression.
+In `setup()` function, you need to call your restricted functions synchronously.
+
+## :wrench: Options
+
+This rule takes a list of objects, where each object specifies a restricted module name and an exported name:
+
+```json5
+{
+  "vue/no-restricted-call-after-await": ["error",
+    { "module": "vue-i18n", "path": "useI18n" },
+    { ... } // You can specify more...
+  ]
+}
+```
+
+<eslint-code-block :rules="{'vue/no-restricted-call-after-await': ['error', { module: 'vue-i18n', path: ['useI18n'] }]}">
+
+```vue
+<script>
+import { useI18n } from 'vue-i18n'
+export default {
+  async setup() {
+    /* ✓ GOOD */
+    useI18n({})
+
+    await doSomething()
+
+    /* ✗ BAD */
+    useI18n({})
+  }
+}
+</script>
+```
+
+</eslint-code-block>
+
+The following properties can be specified for the object.
+
+- `module` ... Specify the module name.
+- `path` ... Specify the imported name or the path that points to the method.
+- `message` ... Specify an optional custom message.
+
+For examples:
+
+```json5
+{
+  "vue/no-restricted-call-after-await": ["error",
+    { "module": "a", "path": "foo" },
+    { "module": "b", "path": ["bar", "baz"] },
+    { "module": "c" }, // Checks the default import.
+    { "module": "d", "path": "default" }, // Checks the default import.
+  ]
+}
+```
+
+<eslint-code-block :rules="{'vue/no-restricted-call-after-await': ['error', { module: 'a', path: 'foo' }, { module: 'b', path: ['bar', 'baz'] }, { module: 'c' }, { module: 'd', path: 'default' }]}">
+
+```vue
+<script>
+import { foo as fooOfA } from 'a'
+import { bar as barOfB } from 'b'
+import defaultOfC from 'c'
+import defaultOfD from 'd'
+export default {
+  async setup() {
+    await doSomething()
+
+    /* ✗ BAD */
+    fooOfA()
+    barOfB.baz()
+    defaultOfC()
+    defaultOfD()
+  }
+}
+</script>
+```
+
+</eslint-code-block>
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-restricted-call-after-await.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-restricted-call-after-await.js)
diff --git a/docs/rules/no-unused-properties.md b/docs/rules/no-unused-properties.md
index e0e39eed..932fb956 100644
--- a/docs/rules/no-unused-properties.md
+++ b/docs/rules/no-unused-properties.md
@@ -54,7 +54,8 @@ This rule cannot be checked for use in other components (e.g. `mixins`, Property
 ```json
 {
   "vue/no-unused-properties": ["error", {
-    "groups": ["props"]
+    "groups": ["props"],
+    "deepData": false
   }]
 }
 ```
@@ -65,6 +66,7 @@ This rule cannot be checked for use in other components (e.g. `mixins`, Property
   - `"computed"`
   - `"methods"`
   - `"setup"`
+- `"deepData"` (`boolean`) If `true`, the object of the property defined in `data` will be searched deeply. Default is `false`. Include `"data"` in `groups` to use this option.
 
 ### `"groups": ["props", "data"]`
 
@@ -108,6 +110,32 @@ This rule cannot be checked for use in other components (e.g. `mixins`, Property
 
 </eslint-code-block>
 
+### `{ "groups": ["props", "data"], "deepData": true }`
+
+<eslint-code-block :rules="{'vue/no-unused-properties': ['error', {groups: ['props', 'data'], deepData: true}]}">
+
+```vue
+<template>
+  <Foo :param="state.used">
+</template>
+<script>
+  export default {
+    data() {
+      return {
+        state: {
+          /* ✓ GOOD */
+          used: null,
+          /* ✗ BAD (`state.unused` data not used) */
+          unused: null
+        }
+      }
+    }
+  }
+</script>
+```
+
+</eslint-code-block>
+
 ### `"groups": ["props", "computed"]`
 
 <eslint-code-block :rules="{'vue/no-unused-properties': ['error', {groups: ['props', 'computed']}]}">
diff --git a/docs/rules/prop-name-casing.md b/docs/rules/prop-name-casing.md
index 3cfee5b0..81f19518 100644
--- a/docs/rules/prop-name-casing.md
+++ b/docs/rules/prop-name-casing.md
@@ -70,6 +70,11 @@ export default {
 
 - [Style guide - Prop name casing](https://v3.vuejs.org/style-guide/#prop-name-casing-strongly-recommended)
 
+## :couple: Related Rules
+
+- [vue/attribute-hyphenation](./attribute-hyphenation.md)
+- [vue/custom-event-name-casing](./custom-event-name-casing.md)
+
 ## :rocket: Version
 
 This rule was introduced in eslint-plugin-vue v4.3.0
diff --git a/docs/rules/v-on-event-hyphenation.md b/docs/rules/v-on-event-hyphenation.md
new file mode 100644
index 00000000..92caed69
--- /dev/null
+++ b/docs/rules/v-on-event-hyphenation.md
@@ -0,0 +1,119 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/v-on-event-hyphenation
+description: enforce v-on event naming style on custom components in template
+---
+# vue/v-on-event-hyphenation
+
+> enforce v-on event naming style on custom components in template
+
+- :exclamation: <badge text="This rule has not been released yet." vertical="middle" type="error"> ***This rule has not been released yet.*** </badge>
+- :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 enforces using hyphenated v-on event names on custom components in Vue templates.
+
+<eslint-code-block fix :rules="{'vue/v-on-event-hyphenation': ['error', 'always', { autofix: true }]}">
+
+```vue
+<template>
+  <!-- ✓ GOOD -->
+  <MyComponent v-on:custom-event="handleEvent"/>
+  <MyComponent @custom-event="handleEvent"/>
+
+  <!-- ✗ BAD -->
+  <MyComponent v-on:customEvent="handleEvent"/>
+  <MyComponent @customEvent="handleEvent"/>
+</template>
+```
+
+</eslint-code-block>
+
+## :wrench: Options
+
+```json
+{
+  "vue/v-on-event-hyphenation": ["error", "always" | "never", {
+    "autofix": false,
+    "ignore": []
+  }]
+}
+```
+
+- `"always"` (default) ... Use hyphenated name.
+- `"never"` ... Don't use hyphenated name.
+- `"ignore"` ... Array of ignored names
+- `"autofix"` ... If `true`, enable autofix. If you are using Vue 2, we recommend that you do not use it due to its side effects.
+
+### `"always"`
+
+It errors on upper case letters.
+
+<eslint-code-block fix :rules="{'vue/v-on-event-hyphenation': ['error', 'always', { autofix: true }]}">
+
+```vue
+<template>
+  <!-- ✓ GOOD -->
+  <MyComponent v-on:custom-event="handleEvent"/>
+
+  <!-- ✗ BAD -->
+  <MyComponent v-on:customEvent="handleEvent"/>
+</template>
+```
+
+</eslint-code-block>
+
+### `"never"`
+
+It errors on hyphens.
+
+<eslint-code-block fix :rules="{'vue/v-on-event-hyphenation': ['error', 'never', { autofix: true }]}">
+
+```vue
+<template>
+  <!-- ✓ GOOD -->
+  <MyComponent v-on:customEvent="handleEvent"/>
+
+  <!-- ✗ BAD -->
+  <MyComponent v-on:custom-event="handleEvent"/>
+</template>
+```
+
+</eslint-code-block>
+
+### `"never", { "ignore": ["custom-event"] }`
+
+Don't use hyphenated name but allow custom event names
+
+<eslint-code-block fix :rules="{'vue/v-on-event-hyphenation': ['error', 'never', { ignore: ['custom-event'], autofix: true }]}">
+
+```vue
+<template>
+  <!-- ✓ GOOD -->
+  <MyComponent v-on:custom-event="handleEvent"/>
+  <MyComponent v-on:myEvent="handleEvent"/>
+
+  <!-- ✗ BAD -->
+  <MyComponent v-on:my-event="handleEvent"/>
+</template>
+```
+
+</eslint-code-block>
+
+## :books: Further Reading
+
+- [Guide - Custom Events]
+
+[Guide - Custom Events]: https://v3.vuejs.org/guide/component-custom-events.html
+
+## :couple: Related Rules
+
+- [vue/custom-event-name-casing](./custom-event-name-casing.md)
+- [vue/attribute-hyphenation](./attribute-hyphenation.md)
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/v-on-event-hyphenation.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/v-on-event-hyphenation.js)
diff --git a/lib/index.js b/lib/index.js
index 95ffd1dd..21091a9f 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -91,6 +91,8 @@ module.exports = {
     'no-ref-as-operand': require('./rules/no-ref-as-operand'),
     'no-reserved-component-names': require('./rules/no-reserved-component-names'),
     'no-reserved-keys': require('./rules/no-reserved-keys'),
+    'no-restricted-block': require('./rules/no-restricted-block'),
+    'no-restricted-call-after-await': require('./rules/no-restricted-call-after-await'),
     'no-restricted-component-options': require('./rules/no-restricted-component-options'),
     'no-restricted-custom-event': require('./rules/no-restricted-custom-event'),
     'no-restricted-props': require('./rules/no-restricted-props'),
@@ -156,6 +158,7 @@ module.exports = {
     'use-v-on-exact': require('./rules/use-v-on-exact'),
     'v-bind-style': require('./rules/v-bind-style'),
     'v-for-delimiter-style': require('./rules/v-for-delimiter-style'),
+    'v-on-event-hyphenation': require('./rules/v-on-event-hyphenation'),
     'v-on-function-call': require('./rules/v-on-function-call'),
     'v-on-style': require('./rules/v-on-style'),
     'v-slot-style': require('./rules/v-slot-style'),
diff --git a/lib/rules/attribute-hyphenation.js b/lib/rules/attribute-hyphenation.js
index 1fd7b5e9..cba9dc24 100644
--- a/lib/rules/attribute-hyphenation.js
+++ b/lib/rules/attribute-hyphenation.js
@@ -87,7 +87,7 @@ module.exports = {
      */
     function isIgnoredAttribute(value) {
       const isIgnored = ignoredAttributes.some((attr) => {
-        return value.indexOf(attr) !== -1
+        return value.includes(attr)
       })
 
       if (isIgnored) {
diff --git a/lib/rules/attributes-order.js b/lib/rules/attributes-order.js
index 34a9d7d3..2441ee49 100644
--- a/lib/rules/attributes-order.js
+++ b/lib/rules/attributes-order.js
@@ -8,6 +8,11 @@ const utils = require('../utils')
 // ------------------------------------------------------------------------------
 // Rule Definition
 // ------------------------------------------------------------------------------
+
+/**
+ * @typedef { VDirective & { key: VDirectiveKey & { name: VIdentifier & { name: 'bind' } } } } VBindDirective
+ */
+
 const ATTRS = {
   DEFINITION: 'DEFINITION',
   LIST_RENDERING: 'LIST_RENDERING',
@@ -22,13 +27,47 @@ const ATTRS = {
   CONTENT: 'CONTENT'
 }
 
+/**
+ * Check whether the given attribute is `v-bind` directive.
+ * @param {VAttribute | VDirective | undefined | null} node
+ * @returns { node is VBindDirective }
+ */
+function isVBind(node) {
+  return Boolean(node && node.directive && node.key.name.name === 'bind')
+}
+/**
+ * Check whether the given attribute is plain attribute.
+ * @param {VAttribute | VDirective | undefined | null} node
+ * @returns { node is VAttribute }
+ */
+function isVAttribute(node) {
+  return Boolean(node && !node.directive)
+}
+/**
+ * Check whether the given attribute is plain attribute or `v-bind` directive.
+ * @param {VAttribute | VDirective | undefined | null} node
+ * @returns { node is VAttribute }
+ */
+function isVAttributeOrVBind(node) {
+  return isVAttribute(node) || isVBind(node)
+}
+
+/**
+ * Check whether the given attribute is `v-bind="..."` directive.
+ * @param {VAttribute | VDirective | undefined | null} node
+ * @returns { node is VBindDirective }
+ */
+function isVBindObject(node) {
+  return isVBind(node) && node.key.argument == null
+}
+
 /**
  * @param {VAttribute | VDirective} attribute
  * @param {SourceCode} sourceCode
  */
 function getAttributeName(attribute, sourceCode) {
   if (attribute.directive) {
-    if (attribute.key.name.name === 'bind') {
+    if (isVBind(attribute)) {
       return attribute.key.argument
         ? sourceCode.getText(attribute.key.argument)
         : ''
@@ -62,7 +101,7 @@ function getDirectiveKeyName(directiveKey, sourceCode) {
 function getAttributeType(attribute, sourceCode) {
   let propName
   if (attribute.directive) {
-    if (attribute.key.name.name !== 'bind') {
+    if (!isVBind(attribute)) {
       const name = attribute.key.name.name
       if (name === 'for') {
         return ATTRS.LIST_RENDERING
@@ -130,24 +169,14 @@ function getPosition(attribute, attributePosition, sourceCode) {
  * @param {SourceCode} sourceCode
  */
 function isAlphabetical(prevNode, currNode, sourceCode) {
-  const isSameType =
-    getAttributeType(prevNode, sourceCode) ===
-    getAttributeType(currNode, sourceCode)
-  if (isSameType) {
-    const prevName = getAttributeName(prevNode, sourceCode)
-    const currName = getAttributeName(currNode, sourceCode)
-    if (prevName === currName) {
-      const prevIsBind = Boolean(
-        prevNode.directive && prevNode.key.name.name === 'bind'
-      )
-      const currIsBind = Boolean(
-        currNode.directive && currNode.key.name.name === 'bind'
-      )
-      return prevIsBind <= currIsBind
-    }
-    return prevName < currName
+  const prevName = getAttributeName(prevNode, sourceCode)
+  const currName = getAttributeName(currNode, sourceCode)
+  if (prevName === currName) {
+    const prevIsBind = isVBind(prevNode)
+    const currIsBind = isVBind(currNode)
+    return prevIsBind <= currIsBind
   }
-  return true
+  return prevName < currName
 }
 
 /**
@@ -186,16 +215,6 @@ function create(context) {
     } else attributePosition[item] = i
   })
 
-  /**
-   * @typedef {object} State
-   * @property {number} currentPosition
-   * @property {VAttribute | VDirective} previousNode
-   */
-  /**
-   * @type {State | null}
-   */
-  let state
-
   /**
    * @param {VAttribute | VDirective} node
    * @param {VAttribute | VDirective} previousNode
@@ -213,43 +232,112 @@ function create(context) {
 
       fix(fixer) {
         const attributes = node.parent.attributes
-        const shiftAttrs = attributes.slice(
+
+        /** @type { (node: VAttribute | VDirective | undefined) => boolean } */
+        let isMoveUp
+
+        if (isVBindObject(node)) {
+          // prev, v-bind:foo, v-bind -> v-bind:foo, v-bind, prev
+          isMoveUp = isVAttributeOrVBind
+        } else if (isVAttributeOrVBind(node)) {
+          // prev, v-bind, v-bind:foo -> v-bind, v-bind:foo, prev
+          isMoveUp = isVBindObject
+        } else {
+          isMoveUp = () => false
+        }
+
+        const previousNodes = attributes.slice(
           attributes.indexOf(previousNode),
-          attributes.indexOf(node) + 1
+          attributes.indexOf(node)
         )
+        const moveUpNodes = [node]
+        const moveDownNodes = []
+        let index = 0
+        while (previousNodes[index]) {
+          const node = previousNodes[index++]
+          if (isMoveUp(node)) {
+            moveUpNodes.unshift(node)
+          } else {
+            moveDownNodes.push(node)
+          }
+        }
+        const moveNodes = [...moveUpNodes, ...moveDownNodes]
 
-        return shiftAttrs.map((attr, i) => {
-          const text =
-            attr === previousNode
-              ? sourceCode.getText(node)
-              : sourceCode.getText(shiftAttrs[i - 1])
-          return fixer.replaceText(attr, text)
+        return moveNodes.map((moveNode, index) => {
+          const text = sourceCode.getText(moveNode)
+          return fixer.replaceText(previousNodes[index] || node, text)
         })
       }
     })
   }
 
   return utils.defineTemplateBodyVisitor(context, {
-    VStartTag() {
-      state = null
-    },
-    VAttribute(node) {
-      let inAlphaOrder = true
-      if (state && alphabetical) {
-        inAlphaOrder = isAlphabetical(state.previousNode, node, sourceCode)
+    VStartTag(node) {
+      const attributes = node.attributes.filter((node, index, attributes) => {
+        if (
+          isVBindObject(node) &&
+          (isVAttributeOrVBind(attributes[index - 1]) ||
+            isVAttributeOrVBind(attributes[index + 1]))
+        ) {
+          // In Vue 3, ignore the `v-bind:foo=" ... "` and `v-bind ="object"` syntax
+          // as they behave differently if you change the order.
+          return false
+        }
+        return true
+      })
+      if (attributes.length <= 1) {
+        return
       }
-      if (
-        !state ||
-        (state.currentPosition <=
-          getPosition(node, attributePosition, sourceCode) &&
-          inAlphaOrder)
-      ) {
-        state = {
-          currentPosition: getPosition(node, attributePosition, sourceCode),
-          previousNode: node
+
+      let previousNode = attributes[0]
+      let previousPosition = getPositionFromAttrIndex(0)
+      for (let index = 1; index < attributes.length; index++) {
+        const node = attributes[index]
+        const position = getPositionFromAttrIndex(index)
+
+        let valid = previousPosition <= position
+        if (valid && alphabetical && previousPosition === position) {
+          valid = isAlphabetical(previousNode, node, sourceCode)
         }
-      } else {
-        reportIssue(node, state.previousNode)
+        if (valid) {
+          previousNode = node
+          previousPosition = position
+        } else {
+          reportIssue(node, previousNode)
+        }
+      }
+
+      /**
+       * @param {number} index
+       * @returns {number}
+       */
+      function getPositionFromAttrIndex(index) {
+        const node = attributes[index]
+        if (isVBindObject(node)) {
+          // node is `v-bind ="object"` syntax
+
+          // In Vue 3, if change the order of `v-bind:foo=" ... "` and `v-bind ="object"`,
+          // the behavior will be different, so adjust so that there is no change in behavior.
+
+          const len = attributes.length
+          for (let nextIndex = index + 1; nextIndex < len; nextIndex++) {
+            const next = attributes[nextIndex]
+
+            if (isVAttributeOrVBind(next) && !isVBindObject(next)) {
+              // It is considered to be in the same order as the next bind prop node.
+              return getPositionFromAttrIndex(nextIndex)
+            }
+          }
+          for (let prevIndex = index - 1; prevIndex >= 0; prevIndex--) {
+            const prev = attributes[prevIndex]
+
+            if (isVAttributeOrVBind(prev) && !isVBindObject(prev)) {
+              // It is considered to be in the same order as the prev bind prop node.
+              return getPositionFromAttrIndex(prevIndex)
+            }
+          }
+        }
+        return getPosition(node, attributePosition, sourceCode)
       }
     }
   })
diff --git a/lib/rules/no-restricted-block.js b/lib/rules/no-restricted-block.js
new file mode 100644
index 00000000..b94ce0c1
--- /dev/null
+++ b/lib/rules/no-restricted-block.js
@@ -0,0 +1,126 @@
+/**
+ * @author Yosuke Ota
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+const utils = require('../utils')
+const regexp = require('../utils/regexp')
+/**
+ * @typedef {object} ParsedOption
+ * @property { (block: VElement) => boolean } test
+ * @property {string} [message]
+ */
+
+/**
+ * @param {string} str
+ * @returns {(str: string) => boolean}
+ */
+function buildMatcher(str) {
+  if (regexp.isRegExp(str)) {
+    const re = regexp.toRegExp(str)
+    return (s) => {
+      re.lastIndex = 0
+      return re.test(s)
+    }
+  }
+  return (s) => s === str
+}
+/**
+ * @param {any} option
+ * @returns {ParsedOption}
+ */
+function parseOption(option) {
+  if (typeof option === 'string') {
+    const matcher = buildMatcher(option)
+    return {
+      test(block) {
+        return matcher(block.rawName)
+      }
+    }
+  }
+  const parsed = parseOption(option.element)
+  parsed.message = option.message
+  return parsed
+}
+
+module.exports = {
+  meta: {
+    type: 'suggestion',
+    docs: {
+      description: 'disallow specific block',
+      categories: undefined,
+      url: 'https://eslint.vuejs.org/rules/no-restricted-block.html'
+    },
+    fixable: null,
+    schema: {
+      type: 'array',
+      items: {
+        oneOf: [
+          { type: 'string' },
+          {
+            type: 'object',
+            properties: {
+              element: { type: 'string' },
+              message: { type: 'string', minLength: 1 }
+            },
+            required: ['element'],
+            additionalProperties: false
+          }
+        ]
+      },
+      uniqueItems: true,
+      minItems: 0
+    },
+
+    messages: {
+      // eslint-disable-next-line eslint-plugin/report-message-format
+      restrictedBlock: '{{message}}'
+    }
+  },
+  /** @param {RuleContext} context */
+  create(context) {
+    /** @type {ParsedOption[]} */
+    const options = context.options.map(parseOption)
+
+    const documentFragment =
+      context.parserServices.getDocumentFragment &&
+      context.parserServices.getDocumentFragment()
+
+    function getTopLevelHTMLElements() {
+      if (documentFragment) {
+        return documentFragment.children.filter(utils.isVElement)
+      }
+      return []
+    }
+
+    return {
+      /** @param {Program} node */
+      Program(node) {
+        if (utils.hasInvalidEOF(node)) {
+          return
+        }
+        for (const block of getTopLevelHTMLElements()) {
+          for (const option of options) {
+            if (option.test(block)) {
+              const message = option.message || defaultMessage(block)
+              context.report({
+                node: block.startTag,
+                messageId: 'restrictedBlock',
+                data: { message }
+              })
+              break
+            }
+          }
+        }
+      }
+    }
+
+    /**
+     * @param {VElement} block
+     */
+    function defaultMessage(block) {
+      return `Using \`<${block.rawName}>\` is not allowed.`
+    }
+  }
+}
diff --git a/lib/rules/no-restricted-call-after-await.js b/lib/rules/no-restricted-call-after-await.js
new file mode 100644
index 00000000..6723d91c
--- /dev/null
+++ b/lib/rules/no-restricted-call-after-await.js
@@ -0,0 +1,227 @@
+/**
+ * @author Yosuke Ota
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+const fs = require('fs')
+const path = require('path')
+const { ReferenceTracker } = require('eslint-utils')
+const utils = require('../utils')
+
+/**
+ * @typedef {import('eslint-utils').TYPES.TraceMap} TraceMap
+ * @typedef {import('eslint-utils').TYPES.TraceKind} TraceKind
+ */
+
+module.exports = {
+  meta: {
+    type: 'suggestion',
+    docs: {
+      description: 'disallow asynchronously called restricted methods',
+      categories: undefined,
+      url: 'https://eslint.vuejs.org/rules/no-restricted-call-after-await.html'
+    },
+    fixable: null,
+    schema: {
+      type: 'array',
+      items: {
+        type: 'object',
+        properties: {
+          module: { type: 'string' },
+          path: {
+            anyOf: [
+              { type: 'string' },
+              {
+                type: 'array',
+                items: {
+                  type: 'string'
+                }
+              }
+            ]
+          },
+          message: { type: 'string', minLength: 1 }
+        },
+        required: ['module'],
+        additionalProperties: false
+      },
+      uniqueItems: true,
+      minItems: 0
+    },
+    messages: {
+      // eslint-disable-next-line eslint-plugin/report-message-format
+      restricted: '{{message}}'
+    }
+  },
+  /** @param {RuleContext} context */
+  create(context) {
+    /** @type {Map<ESNode, string>} */
+    const restrictedCallNodes = new Map()
+    /** @type {Map<FunctionExpression | ArrowFunctionExpression | FunctionDeclaration, { setupProperty: Property, afterAwait: boolean }>} */
+    const setupFunctions = new Map()
+
+    /**x
+     * @typedef {object} ScopeStack
+     * @property {ScopeStack | null} upper
+     * @property {FunctionExpression | ArrowFunctionExpression | FunctionDeclaration} functionNode
+     */
+    /** @type {ScopeStack | null} */
+    let scopeStack = null
+
+    /** @type {Record<string, string[]> | null} */
+    let allLocalImports = null
+    /**
+     * @param {string} id
+     */
+    function safeRequireResolve(id) {
+      try {
+        if (fs.statSync(id).isDirectory()) {
+          return require.resolve(id)
+        }
+      } catch (_e) {
+        // ignore
+      }
+      return id
+    }
+    /**
+     * @param {Program} ast
+     */
+    function getAllLocalImports(ast) {
+      if (!allLocalImports) {
+        allLocalImports = {}
+        const dir = path.dirname(context.getFilename())
+        for (const body of ast.body) {
+          if (body.type !== 'ImportDeclaration') {
+            continue
+          }
+          const source = String(body.source.value)
+          if (!source.startsWith('.')) {
+            continue
+          }
+          const modulePath = safeRequireResolve(path.join(dir, source))
+          const list =
+            allLocalImports[modulePath] || (allLocalImports[modulePath] = [])
+          list.push(source)
+        }
+      }
+
+      return allLocalImports
+    }
+
+    function getCwd() {
+      if (context.getCwd) {
+        return context.getCwd()
+      }
+      return path.resolve('')
+    }
+
+    /**
+     * @param {string} moduleName
+     * @param {Program} ast
+     * @returns {string[]}
+     */
+    function normalizeModules(moduleName, ast) {
+      /** @type {string} */
+      let modulePath
+      if (moduleName.startsWith('.')) {
+        modulePath = safeRequireResolve(path.join(getCwd(), moduleName))
+      } else if (path.isAbsolute(moduleName)) {
+        modulePath = safeRequireResolve(moduleName)
+      } else {
+        return [moduleName]
+      }
+      return getAllLocalImports(ast)[modulePath] || []
+    }
+
+    return utils.compositingVisitors(
+      {
+        /** @param {Program} node */
+        Program(node) {
+          const tracker = new ReferenceTracker(context.getScope())
+
+          for (const option of context.options) {
+            const modules = normalizeModules(option.module, node)
+
+            for (const module of modules) {
+              /** @type {TraceMap} */
+              const traceMap = {
+                [module]: {
+                  [ReferenceTracker.ESM]: true
+                }
+              }
+
+              /** @type {TraceKind & TraceMap} */
+              const mod = traceMap[module]
+              let local = mod
+              const paths = Array.isArray(option.path)
+                ? option.path
+                : [option.path || 'default']
+              for (const path of paths) {
+                local = local[path] || (local[path] = {})
+              }
+              local[ReferenceTracker.CALL] = true
+              const message =
+                option.message ||
+                `The \`${[`import("${module}")`, ...paths].join(
+                  '.'
+                )}\` after \`await\` expression are forbidden.`
+
+              for (const { node } of tracker.iterateEsmReferences(traceMap)) {
+                restrictedCallNodes.set(node, message)
+              }
+            }
+          }
+        }
+      },
+      utils.defineVueVisitor(context, {
+        /** @param {FunctionExpression | ArrowFunctionExpression | FunctionDeclaration} node */
+        ':function'(node) {
+          scopeStack = {
+            upper: scopeStack,
+            functionNode: node
+          }
+        },
+        onSetupFunctionEnter(node) {
+          setupFunctions.set(node, {
+            setupProperty: node.parent,
+            afterAwait: false
+          })
+        },
+        AwaitExpression() {
+          if (!scopeStack) {
+            return
+          }
+          const setupFunctionData = setupFunctions.get(scopeStack.functionNode)
+          if (!setupFunctionData) {
+            return
+          }
+          setupFunctionData.afterAwait = true
+        },
+        /** @param {CallExpression} node */
+        CallExpression(node) {
+          if (!scopeStack) {
+            return
+          }
+          const setupFunctionData = setupFunctions.get(scopeStack.functionNode)
+          if (!setupFunctionData || !setupFunctionData.afterAwait) {
+            return
+          }
+
+          const message = restrictedCallNodes.get(node)
+          if (message) {
+            context.report({
+              node,
+              messageId: 'restricted',
+              data: { message }
+            })
+          }
+        },
+        /** @param {FunctionExpression | ArrowFunctionExpression | FunctionDeclaration} node */
+        ':function:exit'(node) {
+          scopeStack = scopeStack && scopeStack.upper
+
+          setupFunctions.delete(node)
+        }
+      })
+    )
+  }
+}
diff --git a/lib/rules/no-unused-properties.js b/lib/rules/no-unused-properties.js
index a405c8e6..61ac8357 100644
--- a/lib/rules/no-unused-properties.js
+++ b/lib/rules/no-unused-properties.js
@@ -17,17 +17,12 @@ const eslintUtils = require('eslint-utils')
  */
 /**
  * @typedef {object} TemplatePropertiesContainer
- * @property {Set<string>} usedNames
+ * @property {UsedProperties} usedProperties
  * @property {Set<string>} refNames
  * @typedef {object} VueComponentPropertiesContainer
  * @property {ComponentPropertyData[]} properties
- * @property {Set<string>} usedNames
- * @property {boolean} unknown
- * @property {Set<string>} usedPropsNames
- * @property {boolean} unknownProps
- * @typedef { { node: FunctionExpression | ArrowFunctionExpression | FunctionDeclaration, index: number } } CallIdAndParamIndex
- * @typedef { { usedNames: UsedNames, unknown: boolean } } UsedProperties
- * @typedef { (context: RuleContext) => UsedProps } UsedPropsTracker
+ * @property {UsedProperties} usedProperties
+ * @property {UsedProperties} usedPropertiesForProps
  */
 
 // ------------------------------------------------------------------------------
@@ -94,171 +89,290 @@ function getScope(context, currentNode) {
  * Extract names from references objects.
  * @param {VReference[]} references
  */
-function getReferencesNames(references) {
-  return references
-    .filter((ref) => ref.variable == null)
-    .map((ref) => ref.id.name)
+function getReferences(references) {
+  return references.filter((ref) => ref.variable == null).map((ref) => ref.id)
 }
 
-class UsedNames {
-  constructor() {
-    /** @type {Map<string, UsedPropsTracker[]>} */
-    this.map = new Map()
+/**
+ * @param {RuleContext} context
+ * @param {Identifier} id
+ * @returns {FunctionExpression | ArrowFunctionExpression | FunctionDeclaration | null}
+ */
+function findFunction(context, id) {
+  const calleeVariable = findVariable(context, id)
+  if (!calleeVariable) {
+    return null
+  }
+  if (calleeVariable.defs.length === 1) {
+    const def = calleeVariable.defs[0]
+    if (def.node.type === 'FunctionDeclaration') {
+      return def.node
+    }
+    if (
+      def.type === 'Variable' &&
+      def.parent.kind === 'const' &&
+      def.node.init
+    ) {
+      if (
+        def.node.init.type === 'FunctionExpression' ||
+        def.node.init.type === 'ArrowFunctionExpression'
+      ) {
+        return def.node.init
+      }
+      if (def.node.init.type === 'Identifier') {
+        return findFunction(context, def.node.init)
+      }
+    }
+  }
+  return null
+}
+
+/**
+ * @param {RuleContext} context
+ * @param {Identifier} id
+ * @returns {Expression}
+ */
+function findExpression(context, id) {
+  const variable = findVariable(context, id)
+  if (!variable) {
+    return id
   }
+  if (variable.defs.length === 1) {
+    const def = variable.defs[0]
+    if (
+      def.type === 'Variable' &&
+      def.parent.kind === 'const' &&
+      def.node.init
+    ) {
+      if (def.node.init.type === 'Identifier') {
+        return findExpression(context, def.node.init)
+      }
+      return def.node.init
+    }
+  }
+  return id
+}
+
+/**
+ * @typedef { (context: RuleContext) => UsedProperties } UsedPropertiesTracker
+ * @typedef { { node: CallExpression, index: number } } CallAndParamIndex
+ */
+
+/**
+ * Collects the used property names.
+ */
+class UsedProperties {
   /**
-   * @returns {IterableIterator<string>}
+   * @param {object} [option]
+   * @param {boolean} [option.unknown]
    */
-  names() {
-    return this.map.keys()
+  constructor(option) {
+    /** @type {Record<string, UsedPropertiesTracker[]>} */
+    this.map = {}
+    /** @type {CallAndParamIndex[]} */
+    this.calls = []
+    this.unknown = (option && option.unknown) || false
   }
+
   /**
    * @param {string} name
-   * @returns {UsedPropsTracker[]}
    */
-  get(name) {
-    return this.map.get(name) || []
+  isUsed(name) {
+    if (this.unknown) {
+      // If it is unknown, it is considered used.
+      return true
+    }
+    return Boolean(this.map[name])
   }
+
   /**
    * @param {string} name
-   * @param {UsedPropsTracker} tracker
+   * @param {UsedPropertiesTracker | null} tracker
    */
-  add(name, tracker) {
-    const list = this.map.get(name)
-    if (list) {
-      list.push(tracker)
-    } else {
-      this.map.set(name, [tracker])
-    }
+  addUsed(name, tracker) {
+    const trackers = this.map[name] || (this.map[name] = [])
+    if (tracker) trackers.push(tracker)
   }
+
   /**
-   * @param {UsedNames} other
+   * @param {string} name
+   * @returns {UsedPropertiesTracker}
    */
-  addAll(other) {
-    other.map.forEach((trackers, name) => {
-      const list = this.map.get(name)
-      if (list) {
-        list.push(...trackers)
-      } else {
-        this.map.set(name, trackers)
+  getPropsTracker(name) {
+    if (this.unknown) {
+      return () => new UsedProperties({ unknown: true })
+    }
+    const trackers = this.map[name] || []
+    return (context) => {
+      const result = new UsedProperties()
+      for (const tracker of trackers) {
+        result.merge(tracker(context))
       }
-    })
+      return result
+    }
+  }
+
+  /**
+   * @param {UsedProperties | null} other
+   */
+  merge(other) {
+    if (!other) {
+      return
+    }
+    this.unknown = this.unknown || other.unknown
+    if (this.unknown) {
+      return
+    }
+    for (const [name, otherTrackers] of Object.entries(other.map)) {
+      const trackers = this.map[name] || (this.map[name] = [])
+      trackers.push(...otherTrackers)
+    }
+    this.calls.push(...other.calls)
   }
 }
 
 /**
+ * Collects the used property names for parameters of the function.
+ */
+class ParamsUsedProperties {
+  /**
+   * @param {FunctionDeclaration | FunctionExpression | ArrowFunctionExpression} node
+   * @param {RuleContext} context
+   */
+  constructor(node, context) {
+    this.node = node
+    this.context = context
+    /** @type {UsedProperties[]} */
+    this.params = []
+  }
+
+  /**
+   * @param {number} index
+   * @returns {UsedProperties | null}
+   */
+  getParam(index) {
+    const param = this.params[index]
+    if (param != null) {
+      return param
+    }
+    if (this.node.params[index]) {
+      return (this.params[index] = extractParamProperties(
+        this.node.params[index],
+        this.context
+      ))
+    }
+    return null
+  }
+}
+/**
+ * Extract the used property name from one parameter of the function.
+ * @param {Pattern} node
+ * @param {RuleContext} context
+ * @returns {UsedProperties}
+ */
+function extractParamProperties(node, context) {
+  const result = new UsedProperties()
+
+  while (node.type === 'AssignmentPattern') {
+    node = node.left
+  }
+  if (node.type === 'RestElement' || node.type === 'ArrayPattern') {
+    // cannot check
+    return result
+  }
+  if (node.type === 'ObjectPattern') {
+    result.merge(extractObjectPatternProperties(node))
+    return result
+  }
+  if (node.type !== 'Identifier') {
+    return result
+  }
+  const variable = findVariable(context, node)
+  if (!variable) {
+    return result
+  }
+  for (const reference of variable.references) {
+    const id = reference.identifier
+    result.merge(extractPatternOrThisProperties(id, context, false))
+  }
+
+  return result
+}
+
+/**
+ * Extract the used property name from ObjectPattern.
  * @param {ObjectPattern} node
  * @returns {UsedProperties}
  */
 function extractObjectPatternProperties(node) {
-  const usedNames = new UsedNames()
+  const result = new UsedProperties()
   for (const prop of node.properties) {
     if (prop.type === 'Property') {
       const name = utils.getStaticPropertyName(prop)
       if (name) {
-        usedNames.add(name, getObjectPatternPropertyPatternTracker(prop.value))
+        result.addUsed(name, getObjectPatternPropertyPatternTracker(prop.value))
       } else {
         // If cannot trace name, everything is used!
-        return {
-          usedNames,
-          unknown: true
-        }
+        result.unknown = true
+        return result
       }
     } else {
       // If use RestElement, everything is used!
-      return {
-        usedNames,
-        unknown: true
-      }
+      result.unknown = true
+      return result
     }
   }
-  return {
-    usedNames,
-    unknown: false
-  }
+  return result
 }
 
 /**
- * @param {Pattern} pattern
- * @returns {UsedPropsTracker}
+ * Extract the used property name from id.
+ * @param {Identifier} node
+ * @param {RuleContext} context
+ * @returns {UsedProperties}
  */
-function getObjectPatternPropertyPatternTracker(pattern) {
-  if (pattern.type === 'ObjectPattern') {
-    return () => {
-      const result = new UsedProps()
-      const { usedNames, unknown } = extractObjectPatternProperties(pattern)
-      result.usedNames.addAll(usedNames)
-      result.unknown = unknown
-      return result
-    }
-  }
-  if (pattern.type === 'Identifier') {
-    return (context) => {
-      const result = new UsedProps()
-      const variable = findVariable(context, pattern)
-      if (!variable) {
-        return result
-      }
-      for (const reference of variable.references) {
-        const id = reference.identifier
-        const { usedNames, unknown, calls } = extractPatternOrThisProperties(
-          id,
-          context
-        )
-        result.usedNames.addAll(usedNames)
-        result.unknown = result.unknown || unknown
-        result.calls.push(...calls)
-      }
-      return result
-    }
-  } else if (pattern.type === 'AssignmentPattern') {
-    return getObjectPatternPropertyPatternTracker(pattern.left)
-  }
-  return () => {
-    const result = new UsedProps()
-    result.unknown = true
+function extractIdentifierProperties(node, context) {
+  const result = new UsedProperties()
+  const variable = findVariable(context, node)
+  if (!variable) {
     return result
   }
+  for (const reference of variable.references) {
+    const id = reference.identifier
+    result.merge(extractPatternOrThisProperties(id, context, false))
+  }
+  return result
 }
-
 /**
+ * Extract the used property name from pattern or `this`.
  * @param {Identifier | MemberExpression | ChainExpression | ThisExpression} node
  * @param {RuleContext} context
- * @returns {UsedProps}
+ * @param {boolean} withInTemplate
+ * @returns {UsedProperties}
  */
-function extractPatternOrThisProperties(node, context) {
-  const result = new UsedProps()
+function extractPatternOrThisProperties(node, context, withInTemplate) {
+  const result = new UsedProperties()
   const parent = node.parent
   if (parent.type === 'AssignmentExpression') {
+    if (withInTemplate) {
+      return result
+    }
     if (parent.right === node && parent.left.type === 'ObjectPattern') {
       // `({foo} = arg)`
-      const { usedNames, unknown } = extractObjectPatternProperties(parent.left)
-      result.usedNames.addAll(usedNames)
-      result.unknown = result.unknown || unknown
+      result.merge(extractObjectPatternProperties(parent.left))
     }
     return result
   } else if (parent.type === 'VariableDeclarator') {
+    if (withInTemplate) {
+      return result
+    }
     if (parent.init === node) {
       if (parent.id.type === 'ObjectPattern') {
         // `const {foo} = arg`
-        const { usedNames, unknown } = extractObjectPatternProperties(parent.id)
-        result.usedNames.addAll(usedNames)
-        result.unknown = result.unknown || unknown
+        result.merge(extractObjectPatternProperties(parent.id))
       } else if (parent.id.type === 'Identifier') {
         // `const foo = arg`
-        const variable = findVariable(context, parent.id)
-        if (!variable) {
-          return result
-        }
-        for (const reference of variable.references) {
-          const id = reference.identifier
-          const { usedNames, unknown, calls } = extractPatternOrThisProperties(
-            id,
-            context
-          )
-          result.usedNames.addAll(usedNames)
-          result.unknown = result.unknown || unknown
-          result.calls.push(...calls)
-        }
+        result.merge(extractIdentifierProperties(parent.id, context))
       }
     }
     return result
@@ -267,8 +381,8 @@ function extractPatternOrThisProperties(node, context) {
       // `arg.foo`
       const name = utils.getStaticPropertyName(parent)
       if (name) {
-        result.usedNames.add(name, () =>
-          extractPatternOrThisProperties(parent, context)
+        result.addUsed(name, () =>
+          extractPatternOrThisProperties(parent, context, withInTemplate)
         )
       } else {
         result.unknown = true
@@ -276,133 +390,73 @@ function extractPatternOrThisProperties(node, context) {
     }
     return result
   } else if (parent.type === 'CallExpression') {
+    if (withInTemplate) {
+      return result
+    }
     const argIndex = parent.arguments.indexOf(node)
-    if (argIndex > -1 && parent.callee.type === 'Identifier') {
+    if (argIndex > -1) {
       // `foo(arg)`
-      const calleeVariable = findVariable(context, parent.callee)
-      if (!calleeVariable) {
-        return result
-      }
-      if (calleeVariable.defs.length === 1) {
-        const def = calleeVariable.defs[0]
-        if (
-          def.type === 'Variable' &&
-          def.parent.kind === 'const' &&
-          def.node.init &&
-          (def.node.init.type === 'FunctionExpression' ||
-            def.node.init.type === 'ArrowFunctionExpression')
-        ) {
-          result.calls.push({
-            node: def.node.init,
-            index: argIndex
-          })
-        } else if (def.node.type === 'FunctionDeclaration') {
-          result.calls.push({
-            node: def.node,
-            index: argIndex
-          })
-        }
-      }
+      result.calls.push({
+        node: parent,
+        index: argIndex
+      })
     }
   } else if (parent.type === 'ChainExpression') {
-    const { usedNames, unknown, calls } = extractPatternOrThisProperties(
-      parent,
-      context
+    result.merge(
+      extractPatternOrThisProperties(parent, context, withInTemplate)
     )
-    result.usedNames.addAll(usedNames)
-    result.unknown = result.unknown || unknown
-    result.calls.push(...calls)
+  } else if (
+    parent.type === 'ArrowFunctionExpression' ||
+    parent.type === 'ReturnStatement' ||
+    parent.type === 'VExpressionContainer' ||
+    parent.type === 'Property' ||
+    parent.type === 'ArrayExpression'
+  ) {
+    // Maybe used externally.
+    if (maybeExternalUsed(parent)) {
+      result.unknown = true
+    }
   }
   return result
-}
 
-/**
- * Collects the property names used.
- */
-class UsedProps {
-  constructor() {
-    this.usedNames = new UsedNames()
-    /** @type {CallIdAndParamIndex[]} */
-    this.calls = []
-    this.unknown = false
-  }
-}
-
-/**
- * Collects the property names used for one parameter of the function.
- */
-class ParamUsedProps extends UsedProps {
   /**
-   * @param {Pattern} paramNode
-   * @param {RuleContext} context
+   * @param {ASTNode} parentTarget
+   * @returns {boolean}
    */
-  constructor(paramNode, context) {
-    super()
-    while (paramNode.type === 'AssignmentPattern') {
-      paramNode = paramNode.left
+  function maybeExternalUsed(parentTarget) {
+    if (
+      parentTarget.type === 'ReturnStatement' ||
+      parentTarget.type === 'VExpressionContainer'
+    ) {
+      return true
     }
-    if (paramNode.type === 'RestElement' || paramNode.type === 'ArrayPattern') {
-      // cannot check
-      return
+    if (parentTarget.type === 'ArrayExpression') {
+      return maybeExternalUsed(parentTarget.parent)
     }
-    if (paramNode.type === 'ObjectPattern') {
-      const { usedNames, unknown } = extractObjectPatternProperties(paramNode)
-      this.usedNames.addAll(usedNames)
-      this.unknown = this.unknown || unknown
-      return
+    if (parentTarget.type === 'Property') {
+      return maybeExternalUsed(parentTarget.parent.parent)
     }
-    if (paramNode.type !== 'Identifier') {
-      return
-    }
-    const variable = findVariable(context, paramNode)
-    if (!variable) {
-      return
-    }
-    for (const reference of variable.references) {
-      const id = reference.identifier
-      const { usedNames, unknown, calls } = extractPatternOrThisProperties(
-        id,
-        context
-      )
-      this.usedNames.addAll(usedNames)
-      this.unknown = this.unknown || unknown
-      this.calls.push(...calls)
+    if (parentTarget.type === 'ArrowFunctionExpression') {
+      return parentTarget.body === node
     }
+    return false
   }
 }
 
 /**
- * Collects the property names used for parameters of the function.
+ * @param {Pattern} pattern
+ * @returns {UsedPropertiesTracker}
  */
-class ParamsUsedProps {
-  /**
-   * @param {FunctionDeclaration | FunctionExpression | ArrowFunctionExpression} node
-   * @param {RuleContext} context
-   */
-  constructor(node, context) {
-    this.node = node
-    this.context = context
-    /** @type {ParamUsedProps[]} */
-    this.params = []
+function getObjectPatternPropertyPatternTracker(pattern) {
+  if (pattern.type === 'ObjectPattern') {
+    return () => extractObjectPatternProperties(pattern)
   }
-
-  /**
-   * @param {number} index
-   * @returns {ParamUsedProps | null}
-   */
-  getParam(index) {
-    const param = this.params[index]
-    if (param != null) {
-      return param
-    }
-    if (this.node.params[index]) {
-      return (this.params[index] = new ParamUsedProps(
-        this.node.params[index],
-        this.context
-      ))
-    }
-    return null
+  if (pattern.type === 'Identifier') {
+    return (context) => extractIdentifierProperties(pattern, context)
+  } else if (pattern.type === 'AssignmentPattern') {
+    return getObjectPatternPropertyPatternTracker(pattern.left)
   }
+  return () => new UsedProperties({ unknown: true })
 }
 
 // ------------------------------------------------------------------------------
@@ -435,7 +489,8 @@ module.exports = {
             },
             additionalItems: false,
             uniqueItems: true
-          }
+          },
+          deepData: { type: 'boolean' }
         },
         additionalProperties: false
       }
@@ -448,12 +503,13 @@ module.exports = {
   create(context) {
     const options = context.options[0] || {}
     const groups = new Set(options.groups || [GROUP_PROPERTY])
+    const deepData = Boolean(options.deepData)
 
-    /** @type {Map<FunctionDeclaration | FunctionExpression | ArrowFunctionExpression, ParamsUsedProps>} */
-    const paramsUsedPropsMap = new Map()
+    /** @type {Map<FunctionDeclaration | FunctionExpression | ArrowFunctionExpression, ParamsUsedProperties>} */
+    const paramsUsedPropertiesMap = new Map()
     /** @type {TemplatePropertiesContainer} */
     const templatePropertiesContainer = {
-      usedNames: new Set(),
+      usedProperties: new UsedProperties(),
       refNames: new Set()
     }
     /** @type {Map<ASTNode, VueComponentPropertiesContainer>} */
@@ -461,13 +517,13 @@ module.exports = {
 
     /**
      * @param {FunctionDeclaration | FunctionExpression | ArrowFunctionExpression} node
-     * @returns {ParamsUsedProps}
+     * @returns {ParamsUsedProperties}
      */
-    function getParamsUsedProps(node) {
-      let usedProps = paramsUsedPropsMap.get(node)
+    function getParamsUsedProperties(node) {
+      let usedProps = paramsUsedPropertiesMap.get(node)
       if (!usedProps) {
-        usedProps = new ParamsUsedProps(node, context)
-        paramsUsedPropsMap.set(node, usedProps)
+        usedProps = new ParamsUsedProperties(node, context)
+        paramsUsedPropertiesMap.set(node, usedProps)
       }
       return usedProps
     }
@@ -481,37 +537,84 @@ module.exports = {
       if (!container) {
         container = {
           properties: [],
-          usedNames: new Set(),
-          usedPropsNames: new Set(),
-          unknown: false,
-          unknownProps: false
+          usedProperties: new UsedProperties(),
+          usedPropertiesForProps: new UsedProperties()
         }
         vueComponentPropertiesContainerMap.set(node, container)
       }
       return container
     }
+    /**
+     * @param {string[]} segments
+     * @param {Expression} propertyValue
+     * @param {UsedProperties} baseUsedProperties
+     */
+    function verifyDataOptionDeepProperties(
+      segments,
+      propertyValue,
+      baseUsedProperties
+    ) {
+      let targetExpr = propertyValue
+      if (targetExpr.type === 'Identifier') {
+        targetExpr = findExpression(context, targetExpr)
+      }
+      if (targetExpr.type === 'ObjectExpression') {
+        const usedProperties = resolvedUsedProperties(baseUsedProperties, {
+          allowUnknownCall: true
+        })
+        if (usedProperties.unknown) {
+          return
+        }
+
+        for (const prop of targetExpr.properties) {
+          if (prop.type !== 'Property') {
+            continue
+          }
+          const name = utils.getStaticPropertyName(prop)
+          if (name == null) {
+            continue
+          }
+          if (!usedProperties.isUsed(name)) {
+            // report
+            context.report({
+              node: prop.key,
+              messageId: 'unused',
+              data: {
+                group: PROPERTY_LABEL.data,
+                name: [...segments, name].join('.')
+              }
+            })
+            continue
+          }
+          // next
+          verifyDataOptionDeepProperties(
+            [...segments, name],
+            prop.value,
+            usedProperties.getPropsTracker(name)(context)
+          )
+        }
+      }
+    }
 
     /**
      * Report all unused properties.
      */
     function reportUnusedProperties() {
       for (const container of vueComponentPropertiesContainerMap.values()) {
-        if (container.unknown) {
-          // unknown
+        const usedProperties = resolvedUsedProperties(container.usedProperties)
+        usedProperties.merge(templatePropertiesContainer.usedProperties)
+        if (usedProperties.unknown) {
           continue
         }
+
+        const usedPropertiesForProps = resolvedUsedProperties(
+          container.usedPropertiesForProps
+        )
+
         for (const property of container.properties) {
-          if (
-            container.usedNames.has(property.name) ||
-            templatePropertiesContainer.usedNames.has(property.name)
-          ) {
-            // used
-            continue
-          }
           if (
             property.groupName === 'props' &&
-            (container.unknownProps ||
-              container.usedPropsNames.has(property.name))
+            usedPropertiesForProps.isUsed(property.name)
           ) {
             // used props
             continue
@@ -523,6 +626,22 @@ module.exports = {
             // used template refs
             continue
           }
+          if (usedProperties.isUsed(property.name)) {
+            // used
+            if (
+              deepData &&
+              property.groupName === 'data' &&
+              property.type === 'object'
+            ) {
+              // Check the deep properties of the data option.
+              verifyDataOptionDeepProperties(
+                [property.name],
+                property.property.value,
+                usedProperties.getPropsTracker(property.name)(context)
+              )
+            }
+            continue
+          }
           context.report({
             node: property.node,
             messageId: 'unused',
@@ -536,60 +655,60 @@ module.exports = {
     }
 
     /**
-     * @param {UsedProps} usedProps
-     * @param {Map<ASTNode,Set<number>>} already
-     * @returns {IterableIterator<UsedProps>}
+     * @param {UsedProperties | null} usedProps
+     * @param {object} [options]
+     * @param {boolean} [options.allowUnknownCall]
+     * @returns {UsedProperties}
      */
-    function* iterateUsedProps(usedProps, already = new Map()) {
-      yield usedProps
-      for (const call of usedProps.calls) {
-        let alreadyIndexes = already.get(call.node)
-        if (!alreadyIndexes) {
-          alreadyIndexes = new Set()
-          already.set(call.node, alreadyIndexes)
+    function resolvedUsedProperties(usedProps, options) {
+      const allowUnknownCall = options && options.allowUnknownCall
+      const already = new Map()
+
+      const result = new UsedProperties()
+      for (const up of iterate(usedProps)) {
+        result.merge(up)
+        if (result.unknown) {
+          break
         }
-        if (alreadyIndexes.has(call.index)) {
-          continue
-        }
-        alreadyIndexes.add(call.index)
-        const paramsUsedProps = getParamsUsedProps(call.node)
-        const paramUsedProps = paramsUsedProps.getParam(call.index)
-        if (!paramUsedProps) {
-          continue
-        }
-        yield paramUsedProps
-        yield* iterateUsedProps(paramUsedProps, already)
       }
-    }
+      return result
 
-    /**
-     * @param {VueComponentPropertiesContainer} container
-     * @param {UsedProps} baseUseProps
-     */
-    function processParamPropsUsed(container, baseUseProps) {
-      for (const { usedNames, unknown } of iterateUsedProps(baseUseProps)) {
-        if (unknown) {
-          container.unknownProps = true
+      /**
+       * @param {UsedProperties | null} usedProps
+       * @returns {IterableIterator<UsedProperties>}
+       */
+      function* iterate(usedProps) {
+        if (!usedProps) {
           return
         }
-        for (const name of usedNames.names()) {
-          container.usedPropsNames.add(name)
-        }
-      }
-    }
+        yield usedProps
+        for (const call of usedProps.calls) {
+          if (call.node.callee.type !== 'Identifier') {
+            if (allowUnknownCall) {
+              yield new UsedProperties({ unknown: true })
+            }
+            continue
+          }
+          const fnNode = findFunction(context, call.node.callee)
+          if (!fnNode) {
+            if (allowUnknownCall) {
+              yield new UsedProperties({ unknown: true })
+            }
+            continue
+          }
 
-    /**
-     * @param {VueComponentPropertiesContainer} container
-     * @param {UsedProps} baseUseProps
-     */
-    function processUsed(container, baseUseProps) {
-      for (const { usedNames, unknown } of iterateUsedProps(baseUseProps)) {
-        if (unknown) {
-          container.unknown = true
-          return
-        }
-        for (const name of usedNames.names()) {
-          container.usedNames.add(name)
+          let alreadyIndexes = already.get(fnNode)
+          if (!alreadyIndexes) {
+            alreadyIndexes = new Set()
+            already.set(fnNode, alreadyIndexes)
+          }
+          if (alreadyIndexes.has(call.index)) {
+            continue
+          }
+          alreadyIndexes.add(call.index)
+          const paramsUsedProps = getParamsUsedProperties(fnNode)
+          const paramUsedProps = paramsUsedProps.getParam(call.index)
+          yield* iterate(paramUsedProps)
         }
       }
     }
@@ -607,28 +726,44 @@ module.exports = {
         return null
       }
       const property = node.parent
-      if (!property.parent || property.parent.type !== 'ObjectExpression') {
+      if (!utils.isProperty(property)) {
         return null
       }
-      return /** @type {Property} */ (property)
+      return property
     }
 
-    const scriptVisitor = Object.assign(
+    const scriptVisitor = utils.compositingVisitors(
       {},
       utils.defineVueVisitor(context, {
         onVueObjectEnter(node) {
           const container = getVueComponentPropertiesContainer(node)
-          const watcherUsedProperties = new Set()
+
           for (const watcher of utils.iterateProperties(
             node,
             new Set([GROUP_WATCHER])
           )) {
             // Process `watch: { foo /* <- this */ () {} }`
-            let path
-            for (const seg of watcher.name.split('.')) {
-              path = path ? `${path}.${seg}` : seg
-              watcherUsedProperties.add(path)
-            }
+            const segments = watcher.name.split('.')
+            container.usedProperties.addUsed(segments[0], (context) => {
+              return buildChainTracker(segments)(context)
+              /**
+               * @param {string[]} baseSegments
+               * @returns {UsedPropertiesTracker}
+               */
+              function buildChainTracker(baseSegments) {
+                return () => {
+                  const subSegments = baseSegments.slice(1)
+                  const usedProps = new UsedProperties()
+                  if (subSegments.length) {
+                    usedProps.addUsed(
+                      subSegments[0],
+                      buildChainTracker(subSegments)
+                    )
+                  }
+                  return usedProps
+                }
+              }
+            })
 
             // Process `watch: { x: 'foo' /* <- this */  }`
             if (watcher.type === 'object') {
@@ -643,19 +778,14 @@ module.exports = {
                   ) {
                     const name = utils.getStringLiteralValue(handlerValueNode)
                     if (name != null) {
-                      watcherUsedProperties.add(name)
+                      container.usedProperties.addUsed(name, null)
                     }
                   }
                 }
               }
             }
           }
-          for (const prop of utils.iterateProperties(node, groups)) {
-            if (watcherUsedProperties.has(prop.name)) {
-              continue
-            }
-            container.properties.push(prop)
-          }
+          container.properties.push(...utils.iterateProperties(node, groups))
         },
         /** @param { (FunctionExpression | ArrowFunctionExpression) & { parent: Property }} node */
         'ObjectExpression > Property > :function[params.length>0]'(
@@ -696,70 +826,44 @@ module.exports = {
                 ) {
                   return
                 }
-                // check { computed: { foo: { get: () => vm.prop } } }
+                // check { computed: { foo: { get: (vm) => vm.prop } } }
               } else {
                 return
               }
             }
           }
 
-          const paramsUsedProps = getParamsUsedProps(node)
-          const usedProps = /** @type {ParamUsedProps} */ (paramsUsedProps.getParam(
-            0
-          ))
-          processUsed(
-            getVueComponentPropertiesContainer(vueData.node),
-            usedProps
-          )
+          const paramsUsedProps = getParamsUsedProperties(node)
+          const usedProps = paramsUsedProps.getParam(0)
+          const container = getVueComponentPropertiesContainer(vueData.node)
+          container.usedProperties.merge(usedProps)
         },
         onSetupFunctionEnter(node, vueData) {
           const container = getVueComponentPropertiesContainer(vueData.node)
-          if (node.params[0]) {
-            const paramsUsedProps = getParamsUsedProps(node)
-            const paramUsedProps = /** @type {ParamUsedProps} */ (paramsUsedProps.getParam(
-              0
-            ))
-
-            processParamPropsUsed(container, paramUsedProps)
-          }
+          const paramsUsedProps = getParamsUsedProperties(node)
+          const paramUsedProps = paramsUsedProps.getParam(0)
+          container.usedPropertiesForProps.merge(paramUsedProps)
         },
         onRenderFunctionEnter(node, vueData) {
           const container = getVueComponentPropertiesContainer(vueData.node)
-          if (node.params[0]) {
-            // for Vue 3.x render
-            const paramsUsedProps = getParamsUsedProps(node)
-            const paramUsedProps = /** @type {ParamUsedProps} */ (paramsUsedProps.getParam(
-              0
-            ))
-
-            processParamPropsUsed(container, paramUsedProps)
-            if (container.unknownProps) {
-              return
-            }
+
+          // Check for Vue 3.x render
+          const paramsUsedProps = getParamsUsedProperties(node)
+          const ctxUsedProps = paramsUsedProps.getParam(0)
+
+          container.usedPropertiesForProps.merge(ctxUsedProps)
+          if (container.usedPropertiesForProps.unknown) {
+            return
           }
 
-          if (vueData.functional && node.params[1]) {
-            // for Vue 2.x render & functional
-            const paramsUsedProps = getParamsUsedProps(node)
-            const paramUsedProps = /** @type {ParamUsedProps} */ (paramsUsedProps.getParam(
-              1
-            ))
-
-            for (const { usedNames, unknown } of iterateUsedProps(
-              paramUsedProps
-            )) {
-              if (unknown) {
-                container.unknownProps = true
-                return
-              }
-              for (const usedPropsTracker of usedNames.get('props')) {
-                const propUsedProps = usedPropsTracker(context)
-                processParamPropsUsed(container, propUsedProps)
-                if (container.unknownProps) {
-                  return
-                }
-              }
-            }
+          if (vueData.functional) {
+            // Check for Vue 2.x render & functional
+            const contextUsedProps = resolvedUsedProperties(
+              paramsUsedProps.getParam(1)
+            )
+            const tracker = contextUsedProps.getPropsTracker('props')
+            const propUsedProps = tracker(context)
+            container.usedPropertiesForProps.merge(propUsedProps)
           }
         },
         /**
@@ -771,9 +875,8 @@ module.exports = {
             return
           }
           const container = getVueComponentPropertiesContainer(vueData.node)
-          const usedProps = extractPatternOrThisProperties(node, context)
-
-          processUsed(container, usedProps)
+          const usedProps = extractPatternOrThisProperties(node, context, false)
+          container.usedProperties.merge(usedProps)
         }
       }),
       {
@@ -791,8 +894,11 @@ module.exports = {
        * @param {VExpressionContainer} node
        */
       VExpressionContainer(node) {
-        for (const name of getReferencesNames(node.references)) {
-          templatePropertiesContainer.usedNames.add(name)
+        for (const id of getReferences(node.references)) {
+          templatePropertiesContainer.usedProperties.addUsed(
+            id.name,
+            (context) => extractPatternOrThisProperties(id, context, true)
+          )
         }
       },
       /**
diff --git a/lib/rules/v-on-event-hyphenation.js b/lib/rules/v-on-event-hyphenation.js
new file mode 100644
index 00000000..60823e01
--- /dev/null
+++ b/lib/rules/v-on-event-hyphenation.js
@@ -0,0 +1,113 @@
+'use strict'
+
+const utils = require('../utils')
+const casing = require('../utils/casing')
+
+module.exports = {
+  meta: {
+    docs: {
+      description:
+        'enforce v-on event naming style on custom components in template',
+      // TODO Change with major version.
+      // categories: ['vue3-strongly-recommended'],
+      categories: undefined,
+      url: 'https://eslint.vuejs.org/rules/v-on-event-hyphenation.html'
+    },
+    fixable: 'code',
+    schema: [
+      {
+        enum: ['always', 'never']
+      },
+      {
+        type: 'object',
+        properties: {
+          autofix: { type: 'boolean' },
+          ignore: {
+            type: 'array',
+            items: {
+              allOf: [
+                { type: 'string' },
+                { not: { type: 'string', pattern: ':exit$' } },
+                { not: { type: 'string', pattern: '^\\s*$' } }
+              ]
+            },
+            uniqueItems: true,
+            additionalItems: false
+          }
+        },
+        additionalProperties: false
+      }
+    ],
+    type: 'suggestion'
+  },
+
+  /** @param {RuleContext} context */
+  create(context) {
+    const sourceCode = context.getSourceCode()
+    const option = context.options[0]
+    const optionsPayload = context.options[1]
+    const useHyphenated = option !== 'never'
+    /** @type {string[]} */
+    const ignoredAttributes = (optionsPayload && optionsPayload.ignore) || []
+    const autofix = Boolean(optionsPayload && optionsPayload.autofix)
+
+    const caseConverter = casing.getExactConverter(
+      useHyphenated ? 'kebab-case' : 'camelCase'
+    )
+
+    /**
+     * @param {VDirective} node
+     * @param {string} name
+     */
+    function reportIssue(node, name) {
+      const text = sourceCode.getText(node.key)
+
+      context.report({
+        node: node.key,
+        loc: node.loc,
+        message: useHyphenated
+          ? "v-on event '{{text}}' must be hyphenated."
+          : "v-on event '{{text}}' can't be hyphenated.",
+        data: {
+          text
+        },
+        fix: autofix
+          ? (fixer) =>
+              fixer.replaceText(
+                node.key,
+                text.replace(name, caseConverter(name))
+              )
+          : null
+      })
+    }
+
+    /**
+     * @param {string} value
+     */
+    function isIgnoredAttribute(value) {
+      const isIgnored = ignoredAttributes.some((attr) => {
+        return value.includes(attr)
+      })
+
+      if (isIgnored) {
+        return true
+      }
+
+      return useHyphenated ? value.toLowerCase() === value : !/-/.test(value)
+    }
+
+    return utils.defineTemplateBodyVisitor(context, {
+      "VAttribute[directive=true][key.name.name='on']"(node) {
+        if (!utils.isCustomComponent(node.parent.parent)) return
+
+        const name =
+          node.key.argument &&
+          node.key.argument.type === 'VIdentifier' &&
+          node.key.argument.rawName
+        if (!name || isIgnoredAttribute(name)) return
+
+        reportIssue(node, name)
+      }
+    })
+  }
+}
diff --git a/lib/utils/index.js b/lib/utils/index.js
index 3df90418..b6ed0590 100644
--- a/lib/utils/index.js
+++ b/lib/utils/index.js
@@ -1652,11 +1652,14 @@ function findAssignmentProperty(node, name, filter) {
 
 /**
  * Checks whether the given node is Property.
- * @param {Property | SpreadElement} node
+ * @param {Property | SpreadElement | AssignmentProperty} node
  * @returns {node is Property}
  */
 function isProperty(node) {
-  return node.type === 'Property'
+  if (node.type !== 'Property') {
+    return false
+  }
+  return !node.parent || node.parent.type === 'ObjectExpression'
 }
 /**
  * Checks whether the given node is AssignmentProperty.
diff --git a/package.json b/package.json
index cca34e8c..72b4d697 100644
--- a/package.json
+++ b/package.json
@@ -18,6 +18,7 @@
     "version": "npm run lint -- --fix && git add .",
     "update": "node ./tools/update.js",
     "docs:watch": "vuepress dev docs",
+    "predocs:build": "npm run update",
     "docs:build": "vuepress build docs"
   },
   "files": [
diff --git a/typings/eslint/index.d.ts b/typings/eslint/index.d.ts
index f977b45a..fdbca850 100644
--- a/typings/eslint/index.d.ts
+++ b/typings/eslint/index.d.ts
@@ -327,6 +327,9 @@ export namespace Rule {
     getSourceCode(): SourceCode
     markVariableAsUsed(name: string): boolean
     report(descriptor: ReportDescriptor): void
+
+    // eslint@6 does not have this method.
+    getCwd?: () => string
   }
 
   type ReportDescriptor =


Test patch:
diff --git a/tests/lib/rules/attributes-order.js b/tests/lib/rules/attributes-order.js
index 87e72b56..399a859b 100644
--- a/tests/lib/rules/attributes-order.js
+++ b/tests/lib/rules/attributes-order.js
@@ -373,6 +373,54 @@ tester.run('attributes-order', rule, {
           </div>
         </template>`,
       options: [{ alphabetical: true }]
+    },
+
+    // v-bind="..."
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div
+          v-bind:id="a"
+          v-bind="b">
+        </div>
+      </template>`,
+      options: [{ alphabetical: true }]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div
+          v-bind="b"
+          v-bind:id="a">
+        </div>
+      </template>`,
+      options: [{ alphabetical: true }]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div
+          v-if="x"
+          v-bind:id="a"
+          v-bind="b">
+        </div>
+      </template>`,
+      options: [{ alphabetical: true }]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div
+          v-if="x"
+          v-bind="b"
+          v-bind:id="a">
+        </div>
+      </template>`,
+      options: [{ alphabetical: true }]
     }
   ],
 
@@ -943,6 +991,228 @@ tester.run('attributes-order', rule, {
           message: 'Attribute "v-is" should go before "v-cloak".'
         }
       ]
+    },
+
+    // v-bind="..."
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div
+          v-bind:id="a"
+          v-bind="b"
+          v-if="x">
+        </div>
+      </template>`,
+      output: `
+      <template>
+        <div
+          v-if="x"
+          v-bind:id="a"
+          v-bind="b">
+        </div>
+      </template>`,
+      errors: ['Attribute "v-if" should go before "v-bind:id".']
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div
+          v-bind:id="a"
+          v-if="x"
+          v-bind="b">
+        </div>
+      </template>`,
+      output: `
+      <template>
+        <div
+          v-if="x"
+          v-bind:id="a"
+          v-bind="b">
+        </div>
+      </template>`,
+      errors: ['Attribute "v-if" should go before "v-bind:id".']
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div
+          v-bind="b"
+          v-bind:id="a"
+          v-if="x">
+        </div>
+      </template>`,
+      output: `
+      <template>
+        <div
+          v-bind="b"
+          v-if="x"
+          v-bind:id="a">
+        </div>
+      </template>`,
+      errors: ['Attribute "v-if" should go before "v-bind:id".']
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div
+          v-on:click="x"
+          v-bind:id="a"
+          v-bind="b">
+        </div>
+      </template>`,
+      options: [{ alphabetical: true }],
+      output: `
+      <template>
+        <div
+          v-bind:id="a"
+          v-on:click="x"
+          v-bind="b">
+        </div>
+      </template>`,
+      errors: ['Attribute "v-bind:id" should go before "v-on:click".']
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div
+          v-bind:id="a"
+          v-on:click="x"
+          v-bind="b">
+        </div>
+      </template>`,
+      options: [{ alphabetical: true }],
+      output: `
+      <template>
+        <div
+          v-bind:id="a"
+          v-bind="b"
+          v-on:click="x">
+        </div>
+      </template>`,
+      errors: ['Attribute "v-bind" should go before "v-on:click".']
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div
+          v-on:click="x"
+          v-bind="b"
+          v-bind:id="a">
+        </div>
+      </template>`,
+      options: [{ alphabetical: true }],
+      output: `
+      <template>
+        <div
+          v-bind="b"
+          v-bind:id="a"
+          v-on:click="x">
+        </div>
+      </template>`,
+      errors: ['Attribute "v-bind:id" should go before "v-on:click".']
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div
+          v-on:click="x"
+          v-bind:a="x"
+          v-bind="x"
+          v-if="x">
+        </div>
+      </template>`,
+      options: [{ alphabetical: true }],
+      output: `
+      <template>
+        <div
+          v-bind:a="x"
+          v-on:click="x"
+          v-bind="x"
+          v-if="x">
+        </div>
+      </template>`,
+      errors: [
+        'Attribute "v-bind:a" should go before "v-on:click".',
+        'Attribute "v-if" should go before "v-on:click".'
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div
+          v-bind:a="x"
+          v-on:click="x"
+          v-bind="x"
+          v-if="x">
+        </div>
+      </template>`,
+      options: [{ alphabetical: true }],
+      output: `
+      <template>
+        <div
+          v-bind:a="x"
+          v-bind="x"
+          v-on:click="x"
+          v-if="x">
+        </div>
+      </template>`,
+      errors: [
+        'Attribute "v-bind" should go before "v-on:click".',
+        'Attribute "v-if" should go before "v-on:click".'
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div
+          a="x"
+          v-bind="x"
+          v-if="x">
+        </div>
+      </template>`,
+      options: [{ alphabetical: true }],
+      output: `
+      <template>
+        <div
+          v-if="x"
+          a="x"
+          v-bind="x">
+        </div>
+      </template>`,
+      errors: ['Attribute "v-if" should go before "a".']
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div
+          v-on:click="x"
+          v-bind="x"
+          v-if="x">
+        </div>
+      </template>`,
+      options: [{ alphabetical: true }],
+      output: `
+      <template>
+        <div
+          v-bind="x"
+          v-on:click="x"
+          v-if="x">
+        </div>
+      </template>`,
+      errors: [
+        'Attribute "v-bind" should go before "v-on:click".',
+        'Attribute "v-if" should go before "v-on:click".'
+      ]
     }
   ]
 })
diff --git a/tests/lib/rules/no-restricted-block.js b/tests/lib/rules/no-restricted-block.js
new file mode 100644
index 00000000..82b0b3b6
--- /dev/null
+++ b/tests/lib/rules/no-restricted-block.js
@@ -0,0 +1,96 @@
+/**
+ * @author Yosuke Ota
+ */
+'use strict'
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const RuleTester = require('eslint').RuleTester
+const rule = require('../../../lib/rules/no-restricted-block')
+
+// ------------------------------------------------------------------------------
+// Tests
+// ------------------------------------------------------------------------------
+
+const tester = new RuleTester({
+  parser: require.resolve('vue-eslint-parser'),
+  parserOptions: { ecmaVersion: 2020 }
+})
+
+tester.run('no-restricted-block', rule, {
+  valid: [
+    {
+      filename: 'test.vue',
+      code: ''
+    },
+    {
+      filename: 'test.vue',
+      code: '<style>.foo {}</style>',
+      options: ['foo']
+    }
+  ],
+  invalid: [
+    {
+      filename: 'test.vue',
+      code: `<style>.foo {}</style><foo></foo>`,
+      options: ['style', 'foo'],
+      errors: [
+        {
+          message: 'Using `<style>` is not allowed.',
+          line: 1,
+          column: 1
+        },
+        {
+          message: 'Using `<foo>` is not allowed.',
+          line: 1,
+          column: 23
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `<forbidden-block></forbidden-block>
+      <block-forbidden></block-forbidden>`,
+      options: ['/forbidden/'],
+      errors: [
+        'Using `<forbidden-block>` is not allowed.',
+        'Using `<block-forbidden>` is not allowed.'
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `<style>.foo {}</style>
+      <forbidden-block></forbidden-block>
+      <block-forbidden></block-forbidden>`,
+      options: [
+        {
+          element: 'style',
+          message: 'Do not use <style> block in this project.'
+        },
+        {
+          element: '/forbidden/',
+          message: 'Do not use blocks that include `forbidden` in their name.'
+        }
+      ],
+      errors: [
+        {
+          message: 'Do not use <style> block in this project.',
+          line: 1,
+          column: 1
+        },
+        {
+          message: 'Do not use blocks that include `forbidden` in their name.',
+          line: 2,
+          column: 7
+        },
+        {
+          message: 'Do not use blocks that include `forbidden` in their name.',
+          line: 3,
+          column: 7
+        }
+      ]
+    }
+  ]
+})
diff --git a/tests/lib/rules/no-restricted-call-after-await.js b/tests/lib/rules/no-restricted-call-after-await.js
new file mode 100644
index 00000000..530e7cd3
--- /dev/null
+++ b/tests/lib/rules/no-restricted-call-after-await.js
@@ -0,0 +1,382 @@
+/**
+ * @author Yosuke Ota
+ */
+'use strict'
+
+const path = require('path')
+const RuleTester = require('eslint').RuleTester
+const rule = require('../../../lib/rules/no-restricted-call-after-await')
+
+const tester = new RuleTester({
+  parser: require.resolve('vue-eslint-parser'),
+  parserOptions: { ecmaVersion: 2020, sourceType: 'module' }
+})
+
+tester.run('no-restricted-call-after-await', rule, {
+  valid: [
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      import { useI18n } from 'vue-i18n'
+      export default {
+        async setup() {
+          const { t } = useI18n({ })
+          return { t }
+        }
+      }
+      </script>
+      `,
+      options: [{ module: 'vue-i18n', path: 'useI18n' }]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      import { useI18n } from 'vue-i18n'
+      export default {
+        async foo() {
+          await doSomething()
+          const { t } = useI18n({ })
+          return { t }
+        }
+      }
+      </script>
+      `,
+      options: [{ module: 'vue-i18n', path: 'useI18n' }]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      import { bar } from 'foo'
+      export default {
+        async setup() {
+          const qux = bar.baz({ })
+          await doSomething()
+        }
+      }
+      </script>
+      `,
+      options: [{ module: 'foo', path: ['bar', 'baz'] }]
+    },
+    {
+      filename: path.join(process.cwd(), 'test.vue'),
+      code: `
+      <script>
+      export default {
+        async setup() {
+          await doSomething()
+          const v = bar({ })
+          qux()
+          useI18n()
+        }
+      }
+      </script>
+      `,
+      options: [
+        { module: './foo', path: 'bar' },
+        { module: './baz', path: 'qux' },
+        { module: 'vue-i18n', path: 'useI18n' }
+      ]
+    },
+    {
+      filename: path.join(process.cwd(), 'test.vue'),
+      code: `
+      <script>
+      import {useI18n} from 'vue-i18n'
+      import { bar } from './foo'
+      import { qux } from './baz'
+      export default {
+        async setup() {
+          const v = bar({ })
+          qux()
+          useI18n()
+          await doSomething()
+        }
+      }
+      </script>
+      `,
+      options: [
+        { module: './foo', path: 'bar' },
+        { module: './baz', path: 'qux' },
+        { module: 'vue-i18n', path: 'useI18n' }
+      ]
+    },
+    {
+      filename: path.join(process.cwd(), 'test.vue'),
+      code: `
+      <script>
+      import {useI18n} from 'vue-i18n'
+      import { bar } from './foo'
+      import { qux } from './baz'
+      const v = bar({ })
+      qux()
+      useI18n()
+      export default {
+        async setup() {
+          await doSomething()
+        }
+      }
+      </script>
+      `,
+      options: [
+        { module: './foo', path: 'bar' },
+        { module: './baz', path: 'qux' },
+        { module: 'vue-i18n', path: 'useI18n' }
+      ]
+    }
+  ],
+  invalid: [
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      import { useI18n } from 'vue-i18n'
+      export default {
+        async setup() {
+          await doSomething()
+          const { t } = useI18n({ })
+          return { t }
+        }
+      }
+      </script>
+      `,
+      options: [{ module: 'vue-i18n', path: 'useI18n' }],
+      errors: [
+        {
+          message:
+            'The `import("vue-i18n").useI18n` after `await` expression are forbidden.',
+          line: 7,
+          column: 25,
+          endLine: 7,
+          endColumn: 37
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      import { bar } from 'foo'
+      export default {
+        async setup() {
+          await doSomething()
+          const qux = bar.baz({ })
+        }
+      }
+      </script>
+      `,
+      options: [{ module: 'foo', path: ['bar', 'baz'] }],
+      errors: [
+        {
+          message:
+            'The `import("foo").bar.baz` after `await` expression are forbidden.',
+          line: 7
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      import { bar } from 'foo'
+      import { useI18n } from 'vue-i18n'
+      export default {
+        async setup() {
+          await doSomething()
+          const {t} = useI18n({ })
+          const qux = bar.baz({ })
+        }
+      }
+      </script>
+      `,
+      options: [
+        { module: 'vue-i18n', path: 'useI18n' },
+        { module: 'foo', path: ['bar', 'baz'] }
+      ],
+      errors: [
+        {
+          message:
+            'The `import("vue-i18n").useI18n` after `await` expression are forbidden.',
+          line: 8
+        },
+        {
+          message:
+            'The `import("foo").bar.baz` after `await` expression are forbidden.',
+          line: 9
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      import def from 'foo'
+      export default {
+        async setup() {
+          await doSomething()
+          const v = def({ })
+        }
+      }
+      </script>
+      `,
+      options: [{ module: 'foo', path: 'default' }],
+      errors: [
+        {
+          message:
+            'The `import("foo").default` after `await` expression are forbidden.',
+          line: 7
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      import def from 'foo'
+      export default {
+        async setup() {
+          await doSomething()
+          const v = def({ })
+        }
+      }
+      </script>
+      `,
+      options: [{ module: 'foo' }],
+      errors: [
+        {
+          message:
+            'The `import("foo").default` after `await` expression are forbidden.',
+          line: 7
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      import * as i18n from 'vue-i18n'
+      export default {
+        async setup() {
+          await doSomething()
+          const v = i18n.useI18n({ })
+        }
+      }
+      </script>
+      `,
+      options: [{ module: 'vue-i18n', path: 'useI18n' }],
+      errors: [
+        {
+          message:
+            'The `import("vue-i18n").useI18n` after `await` expression are forbidden.',
+          line: 7
+        }
+      ]
+    },
+    {
+      filename: path.join(process.cwd(), 'test.vue'),
+      code: `
+      <script>
+      import {useI18n} from 'vue-i18n'
+      import { bar } from './foo'
+      import { qux } from './baz'
+      export default {
+        async setup() {
+          useI18n()
+          await doSomething()
+          const v = bar({ })
+          qux()
+          useI18n()
+        }
+      }
+      </script>
+      `,
+      options: [
+        { module: './foo', path: 'bar' },
+        { module: './baz', path: 'qux' },
+        { module: 'vue-i18n', path: 'useI18n' }
+      ],
+      errors: [
+        {
+          message:
+            'The `import("./foo").bar` after `await` expression are forbidden.',
+          line: 10
+        },
+        {
+          message:
+            'The `import("./baz").qux` after `await` expression are forbidden.',
+          line: 11
+        },
+        {
+          message:
+            'The `import("vue-i18n").useI18n` after `await` expression are forbidden.',
+          line: 12
+        }
+      ]
+    },
+    {
+      filename: path.join(process.cwd(), 'test.vue'),
+      code: `
+      <script>
+      import {useI18n} from 'vue-i18n'
+      import { bar } from './foo'
+      import { qux } from './baz'
+      export default {
+        async setup() {
+          useI18n()
+          await doSomething()
+          const v = bar({ })
+          qux()
+          useI18n()
+        }
+      }
+      </script>
+      `,
+      options: [
+        { module: path.join(process.cwd(), './foo'), path: 'bar' },
+        { module: path.join(process.cwd(), './baz'), path: 'qux' },
+        { module: 'vue-i18n', path: 'useI18n' }
+      ],
+      errors: [
+        {
+          message:
+            'The `import("./foo").bar` after `await` expression are forbidden.',
+          line: 10
+        },
+        {
+          message:
+            'The `import("./baz").qux` after `await` expression are forbidden.',
+          line: 11
+        },
+        {
+          message:
+            'The `import("vue-i18n").useI18n` after `await` expression are forbidden.',
+          line: 12
+        }
+      ]
+    },
+    {
+      filename: path.join(__dirname, '../../../test/test.vue'),
+      code: `
+      <script>
+      import { foo } from '..'
+      export default {
+        async setup() {
+          await doSomething()
+          foo({})
+        }
+      }
+      </script>
+      `,
+      options: [{ module: require.resolve('../../..'), path: 'foo' }],
+      errors: [
+        {
+          message:
+            'The `import("..").foo` after `await` expression are forbidden.',
+          line: 7
+        }
+      ]
+    }
+  ]
+})
diff --git a/tests/lib/rules/no-unused-properties.js b/tests/lib/rules/no-unused-properties.js
index 1419add6..89478e06 100644
--- a/tests/lib/rules/no-unused-properties.js
+++ b/tests/lib/rules/no-unused-properties.js
@@ -18,6 +18,7 @@ const tester = new RuleTester({
 const allOptions = [
   { groups: ['props', 'computed', 'data', 'methods', 'setup'] }
 ]
+const deepDataOptions = [{ groups: ['data'], deepData: true }]
 
 tester.run('no-unused-properties', rule, {
   valid: [
@@ -1143,6 +1144,273 @@ tester.run('no-unused-properties', rule, {
         };
       </script>
       `
+    },
+
+    // deep data
+    {
+      filename: 'test.vue',
+      code: `
+        <template>
+         {{ foo.baz.b }}
+        </template>
+        <script>
+          export default {
+            data() {
+              return {
+                foo: {
+                  bar: {
+                    a: 1
+                  },
+                  baz: {
+                    b: 1
+                  }
+                }
+              }
+            },
+            methods: {
+              a () {
+                return this.foo.bar.a
+              }
+            }
+          };
+        </script>
+      `,
+      options: deepDataOptions
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <script>
+          export default {
+            data() {
+              return {
+                foo: {
+                  bar: {
+                    a: 1
+                  },
+                  baz: {
+                    b: 1
+                  }
+                }
+              }
+            },
+            created() {
+              alert(this.foo)
+            }
+          };
+        </script>
+      `,
+      options: deepDataOptions
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <script>
+          export default {
+            data() {
+              return {
+                foo: {
+                  bar: {
+                    a: 1
+                  },
+                  baz: {
+                    b: 1
+                  }
+                }
+              }
+            },
+            created() {
+              fn(this.foo)
+            }
+          };
+          function fn(foo) {
+            return foo.bar.a + foo.baz.b
+          }
+        </script>
+      `,
+      options: deepDataOptions
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <template>
+          {{ foo.bar }}
+          {{ foo.baz }}
+        </template>
+        <script>
+          export default {
+            data() {
+              return {
+                foo: {
+                  bar: {
+                    a: 1
+                  },
+                  baz: {
+                    b: 1
+                  }
+                }
+              }
+            }
+          };
+        </script>
+      `,
+      options: deepDataOptions
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <script>
+          export default {
+            data() {
+              return {
+                foo: {
+                  bar: {
+                    a: 1
+                  },
+                  baz: {
+                    b: 1
+                  }
+                }
+              }
+            },
+            methods: {
+              a () {
+                return this.foo
+              }
+            }
+          };
+        </script>
+      `,
+      options: deepDataOptions
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <template>
+          <Foo :param="{ a: foo.bar }"></Foo>
+        </template>
+
+        <script>
+          export default {
+            data() {
+              return {
+                foo: {
+                  bar: {
+                    a: 1
+                  },
+                  baz: {
+                    b: 1
+                  }
+                }
+              }
+            },
+            methods: {
+              fn () {
+                return {
+                  b: this.foo.baz
+                }
+              }
+            }
+          };
+        </script>
+      `,
+      options: deepDataOptions
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <template>
+          <Foo :class="[ foo.bar ]"></Foo>
+        </template>
+
+        <script>
+          export default {
+            data() {
+              return {
+                foo: {
+                  bar: {
+                    a: 1
+                  },
+                  baz: {
+                    b: 1
+                  }
+                }
+              }
+            },
+            methods: {
+              fn () {
+                return [this.foo.baz]
+              }
+            }
+          };
+        </script>
+      `,
+      options: deepDataOptions
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <template>
+          <Foo :param="[ {a: foo.bar} ]"></Foo>
+        </template>
+
+        <script>
+          export default {
+            data() {
+              return {
+                foo: {
+                  bar: {
+                    a: 1
+                  },
+                  baz: {
+                    b: 1
+                  }
+                }
+              }
+            },
+            methods: {
+              fn () {
+                return {
+                  b: [this.foo.baz]
+                }
+              }
+            }
+          };
+        </script>
+      `,
+      options: deepDataOptions
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <script>
+          export default {
+            data() {
+              return {
+                foo: {
+                  bar: {
+                    a: 1
+                  },
+                  baz: {
+                    b: 1
+                  }
+                }
+              }
+            },
+            methods: {
+              fn () {
+                unknown(this.foo.bar)
+                f( () => this.foo.baz )
+              }
+            }
+          };
+
+          function f (a) {
+
+          }
+        </script>
+      `,
+      options: deepDataOptions
     }
   ],
 
@@ -1686,6 +1954,229 @@ tester.run('no-unused-properties', rule, {
       </script>
       `,
       errors: ["'x' of property found, but never used."]
+    },
+
+    // deep data
+    {
+      filename: 'test.vue',
+      code: `
+        <template>
+         {{ foo.baz.a }}
+        </template>
+        <script>
+          export default {
+            data() {
+              return {
+                foo: {
+                  bar: {
+                    a: 1
+                  },
+                  baz: {
+                    b: 1
+                  }
+                }
+              }
+            },
+            methods: {
+              a () {
+                return this.foo.bar
+              }
+            }
+          };
+        </script>
+      `,
+      options: deepDataOptions,
+      errors: [
+        {
+          message: "'foo.baz.b' of data found, but never used.",
+          line: 14,
+          column: 21
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <script>
+          export default {
+            data() {
+              return {
+                foo: {
+                  bar: {
+                    a: 1
+                  },
+                  baz: {
+                    b: 1
+                  }
+                }
+              }
+            },
+            created() {
+              fn(this.foo)
+            }
+          };
+          function fn(b) {
+            b.baz()
+          }
+        </script>
+      `,
+      options: deepDataOptions,
+      errors: [
+        "'foo.bar' of data found, but never used.",
+        "'foo.baz.b' of data found, but never used."
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <script>
+          export default {
+            data() {
+              return {
+                foo: {
+                  bar: {
+                    a: 1
+                  },
+                  baz: {
+                    b: 1
+                  }
+                }
+              }
+            },
+            created() {
+              fn(this.foo)
+            }
+          };
+          function fn(foo) {
+            return foo.bar + foo.baz.b
+          }
+        </script>
+      `,
+      options: deepDataOptions,
+      errors: ["'foo.bar.a' of data found, but never used."]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <script>
+          export default {
+            data() {
+              return {
+                foo: {
+                  bar: {
+                    a: 1
+                  },
+                  baz: {
+                    b: 1
+                  }
+                }
+              }
+            },
+            created() {
+              fn(this.foo)
+              this.foo.baz
+            }
+          };
+          function fn(foo) {
+            foo.bar
+          }
+        </script>
+      `,
+      options: deepDataOptions,
+      errors: [
+        "'foo.bar.a' of data found, but never used.",
+        "'foo.baz.b' of data found, but never used."
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <script>
+          export default {
+            data() {
+              return {
+                foo: {
+                  bar: {
+                    a: 1
+                  },
+                }
+              }
+            },
+            mounted () {
+              if (this.foo.bar) {
+              }
+            }
+          };
+        </script>
+      `,
+      options: deepDataOptions,
+      errors: ["'foo.bar.a' of data found, but never used."]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <script>
+          export default {
+            data() {
+              return {
+                foo: {
+                  bar: {
+                    a: 1
+                  },
+                }
+              }
+            },
+            methods: {
+              fn () {
+                return this.foo.bar ? 1 : 2
+              }
+            }
+          };
+        </script>
+      `,
+      options: deepDataOptions,
+      errors: ["'foo.bar.a' of data found, but never used."]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <script>
+          export default {
+            data() {
+              const bar = {
+                a: 1
+              }
+              const baz = {
+                a: 1
+              }
+              return {
+                foo: {
+                  bar,
+                  baz,
+                }
+              }
+            },
+            methods: {
+              fn () {
+                console.log(this.foo.bar + this.foo.baz)
+              }
+            }
+          };
+        </script>
+      `,
+      options: deepDataOptions,
+      errors: [
+        {
+          message: "'foo.bar.a' of data found, but never used.",
+          line: 6,
+          column: 17
+        },
+        {
+          message: "'foo.baz.a' of data found, but never used.",
+          line: 9,
+          column: 17
+        }
+      ]
     }
   ]
 })
diff --git a/tests/lib/rules/v-on-event-hyphenation.js b/tests/lib/rules/v-on-event-hyphenation.js
new file mode 100644
index 00000000..c7744997
--- /dev/null
+++ b/tests/lib/rules/v-on-event-hyphenation.js
@@ -0,0 +1,107 @@
+'use strict'
+
+const RuleTester = require('eslint').RuleTester
+const rule = require('../../../lib/rules/v-on-event-hyphenation.js')
+
+const tester = new RuleTester({
+  parser: require.resolve('vue-eslint-parser'),
+  parserOptions: {
+    ecmaVersion: 2019
+  }
+})
+
+tester.run('v-on-event-hyphenation', rule, {
+  valid: [
+    `
+    <template>
+        <VueComponent @custom-event="onEvent"/>
+    </template>
+    `,
+    `
+    <template>
+        <VueComponent :customEvent="onEvent"/>
+    </template>
+    `,
+    `
+    <template>
+        <VueComponent v-on="events"/>
+    </template>
+    `,
+    `
+    <template>
+        <div v-on:unknownEvent="onEvent"/>
+    </template>
+    `,
+    {
+      code: `
+      <template>
+          <VueComponent v-on:customEvent="events"/>
+      </template>
+      `,
+      options: ['never']
+    },
+    {
+      code: `
+      <template>
+          <VueComponent v-on:customEvent="events"/>
+      </template>
+      `,
+      options: ['never', { ignore: ['custom'] }]
+    }
+  ],
+  invalid: [
+    {
+      code: `
+      <template>
+          <VueComponent @customEvent="onEvent"/>
+      </template>
+      `,
+      output: null,
+      errors: [
+        {
+          message: "v-on event '@customEvent' must be hyphenated.",
+          line: 3,
+          column: 25,
+          endLine: 3,
+          endColumn: 47
+        }
+      ]
+    },
+    {
+      code: `
+      <template>
+          <VueComponent @customEvent="onEvent"/>
+      </template>
+      `,
+      options: ['always', { autofix: true }],
+      output: `
+      <template>
+          <VueComponent @custom-event="onEvent"/>
+      </template>
+      `,
+      errors: [
+        {
+          message: "v-on event '@customEvent' must be hyphenated.",
+          line: 3,
+          column: 25,
+          endLine: 3,
+          endColumn: 47
+        }
+      ]
+    },
+    {
+      code: `
+      <template>
+          <VueComponent v-on:custom-event="events"/>
+      </template>
+      `,
+      options: ['never', { autofix: true }],
+      output: `
+      <template>
+          <VueComponent v-on:customEvent="events"/>
+      </template>
+      `,
+      errors: ["v-on event 'v-on:custom-event' can't be hyphenated."]
+    }
+  ]
+})