Repository: vuejs/eslint-plugin-vue
Pull request #1124 (2020-05-13 05:52:52)

Add `vue/require-explicit-emits` rule
This PR adds `vue/require-explicit-emits` rule.

`vue/require-explicit-emits` rule reports event triggers not declared with the `emits` option. (The `emits` option is a new in Vue.js 3.0.0+)

[Vue RFCs - 0030-emits-option](https://github.com/vuejs/rfcs/blob/master/active-rfcs/0030-emits-option.md)

refs #1035 


Patch:
diff --git a/docs/rules/README.md b/docs/rules/README.md
index ad262e40..11a34773 100644
--- a/docs/rules/README.md
+++ b/docs/rules/README.md
@@ -282,10 +282,12 @@ For example:
 | [vue/no-restricted-syntax](./no-restricted-syntax.md) | disallow specified syntax |  |
 | [vue/no-static-inline-styles](./no-static-inline-styles.md) | disallow static inline `style` attributes |  |
 | [vue/no-template-target-blank](./no-template-target-blank.md) | disallow target="_blank" attribute without rel="noopener noreferrer" |  |
+| [vue/no-unregistered-components](./no-unregistered-components.md) | disallow using components that are not registered inside templates |  |
 | [vue/no-unsupported-features](./no-unsupported-features.md) | disallow unsupported Vue.js syntax on the specified version | :wrench: |
 | [vue/object-curly-spacing](./object-curly-spacing.md) | enforce consistent spacing inside braces | :wrench: |
 | [vue/padding-line-between-blocks](./padding-line-between-blocks.md) | require or disallow padding lines between blocks | :wrench: |
 | [vue/require-direct-export](./require-direct-export.md) | require the component to be directly exported |  |
+| [vue/require-explicit-emits](./require-explicit-emits.md) | require `emits` option with name triggered by `$emit()` |  |
 | [vue/require-name-property](./require-name-property.md) | require a name property in Vue components |  |
 | [vue/script-indent](./script-indent.md) | enforce consistent indentation in `<script>` | :wrench: |
 | [vue/sort-keys](./sort-keys.md) | enforce sort-keys in a manner that is compatible with order-in-components |  |
diff --git a/docs/rules/no-deprecated-vue-config-keycodes.md b/docs/rules/no-deprecated-vue-config-keycodes.md
index 16caa0ef..8311e8a2 100644
--- a/docs/rules/no-deprecated-vue-config-keycodes.md
+++ b/docs/rules/no-deprecated-vue-config-keycodes.md
@@ -13,7 +13,7 @@ description: disallow using deprecated `Vue.config.keyCodes` (in Vue.js 3.0.0+)
 
 This rule reports use of deprecated `Vue.config.keyCodes` (in Vue.js 3.0.0+)
 
-<eslint-code-block filename="a.js" language="javascript ":rules="{'vue/no-deprecated-vue-config-keycodes': ['error']}">
+<eslint-code-block filename="a.js" language="javascript" :rules="{'vue/no-deprecated-vue-config-keycodes': ['error']}">
 
 ```js
 /* ✗ BAD */
@@ -31,14 +31,16 @@ Nothing.
 ## :couple: Related rules
 
 - [vue/no-deprecated-v-on-number-modifiers]
-- [API - Global Config - keyCodes]
 
 [vue/no-deprecated-v-on-number-modifiers]: ./no-deprecated-v-on-number-modifiers.md
-[API - Global Config - keyCodes]: https://vuejs.org/v2/api/#keyCodes
 
 ## :books: Further reading
 
-- [Vue RFCs - 0014-drop-keycode-support](https://github.com/vuejs/rfcs/blob/master/active-rfcs/0014-drop-keycode-support.md)
+- [Vue RFCs - 0014-drop-keycode-support]
+- [API - Global Config - keyCodes]
+
+[Vue RFCs - 0014-drop-keycode-support]: https://github.com/vuejs/rfcs/blob/master/active-rfcs/0014-drop-keycode-support.md
+[API - Global Config - keyCodes]: https://vuejs.org/v2/api/#keyCodes
 
 ## :mag: Implementation
 
diff --git a/docs/rules/no-unregistered-components.md b/docs/rules/no-unregistered-components.md
new file mode 100644
index 00000000..c90b41c4
--- /dev/null
+++ b/docs/rules/no-unregistered-components.md
@@ -0,0 +1,137 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/no-unregistered-components
+description: disallow using components that are not registered inside templates
+---
+# vue/no-unregistered-components
+> disallow using components that are not registered inside templates
+
+## :book: Rule Details
+
+This rule reports components that haven't been registered and are being used in the template.
+
+::: warning Note
+This rule cannot check globally registered components and components registered in mixins 
+unless you add them as part of the ignored patterns. `component`, `suspense` and `teleport` 
+are ignored by default.
+:::
+
+<eslint-code-block :rules="{'vue/no-unregistered-components': ['error']}">
+
+```vue
+<!-- ✓ GOOD -->
+<template>
+  <div>
+    <h2>Lorem ipsum</h2>
+    <the-modal>
+      <component is="TheInput" />
+      <component :is="'TheDropdown'" />
+      <TheButton>CTA</TheButton>
+    </the-modal>
+  </div>
+</template>
+
+<script>
+  import TheButton from 'components/TheButton.vue'
+  import TheModal from 'components/TheModal.vue'
+  import TheInput from 'components/TheInput.vue'
+  import TheDropdown from 'components/TheDropdown.vue'
+
+  export default {
+    components: {
+      TheButton,
+      TheModal,
+      TheInput,
+      TheDropdown,
+    }
+  }
+</script>
+```
+
+</eslint-code-block>
+
+<eslint-code-block :rules="{'vue/no-unregistered-components': ['error']}">
+
+```vue
+<!-- ✗ BAD -->
+<template>
+  <div>
+    <h2>Lorem ipsum</h2>
+    <TheModal />
+  </div>
+</template>
+
+<script>
+  export default {
+    components: {
+
+    }
+  }
+</script>
+```
+
+</eslint-code-block>
+
+## :wrench: Options
+
+```json
+{
+  "vue/no-unregistered-components": ["error", {
+    "ignorePatterns": []
+  }]
+}
+```
+
+- `ignorePatterns` Suppresses all errors if component name matches one or more patterns.
+
+### `ignorePatterns: ['custom(\\-\\w+)+']`
+
+<eslint-code-block :rules="{'vue/no-unregistered-components': ['error', { 'ignorePatterns': ['custom(\\-\\w+)+'] }]}">
+
+```vue
+<!-- ✓ GOOD -->
+<template>
+  <div>
+    <h2>Lorem ipsum</h2>
+    <CustomComponent />
+  </div>
+</template>
+
+<script>
+  export default {
+    components: {
+
+    },
+  }
+</script>
+```
+
+</eslint-code-block>
+
+<eslint-code-block :rules="{'vue/no-unregistered-components': ['error', { 'ignorePatterns': ['custom(\\-\\w+)+'] }]}">
+
+```vue
+<!-- ✗ BAD -->
+<template>
+  <div>
+    <h2>Lorem ipsum</h2>
+    <WarmButton />
+  </div>
+</template>
+
+<script>
+  export default {
+    components: {
+
+    },
+  }
+</script>
+```
+
+</eslint-code-block>
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-unregistered-components.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-unregistered-components.js)
diff --git a/docs/rules/require-explicit-emits.md b/docs/rules/require-explicit-emits.md
new file mode 100644
index 00000000..542d23c6
--- /dev/null
+++ b/docs/rules/require-explicit-emits.md
@@ -0,0 +1,84 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/require-explicit-emits
+description: require `emits` option with name triggered by `$emit()`
+---
+# vue/require-explicit-emits
+> require `emits` option with name triggered by `$emit()`
+
+## :book: Rule Details
+
+This rule reports event triggers not declared with the `emits` option. (The `emits` option is a new in Vue.js 3.0.0+)
+
+Explicit `emits` declaration serves as self-documenting code. This can be useful for other developers to instantly understand what events the component is supposed to emit.
+Also,  with attribute fallthrough changes in Vue.js 3.0.0+, `v-on` listeners on components will fallthrough as native listeners by default. Declare it as a component-only event in `emits` to avoid unnecessary registration of native listeners.
+
+<eslint-code-block :rules="{'vue/require-explicit-emits': ['error']}">
+
+```vue
+<template>
+  <!-- ✓ GOOD -->
+  <div @click="$emit('good')"/>
+  <!-- ✗ BAD -->
+  <div @click="$emit('bad')"/>
+</template>
+<script>
+export default {
+  emits: ['good']
+}
+</script>
+```
+
+</eslint-code-block>
+
+<eslint-code-block :rules="{'vue/require-explicit-emits': ['error']}">
+
+```vue
+<script>
+export default {
+  emits: ['good'],
+  methods: {
+    foo () {
+      // ✓ GOOD
+      this.$emit('good')
+      // ✗ BAD
+      this.$emit('bad')
+    }
+  }
+}
+</script>
+```
+
+</eslint-code-block>
+
+<eslint-code-block :rules="{'vue/require-explicit-emits': ['error']}">
+
+```vue
+<script>
+export default {
+  emits: ['good'],
+  setup (props, context) {
+    // ✓ GOOD
+    context.emit('good')
+    // ✗ BAD
+    context.emit('bad')
+  }
+}
+</script>
+```
+
+</eslint-code-block>
+
+## :wrench: Options
+
+Nothing.
+
+## :books: Further reading
+
+- [Vue RFCs - 0030-emits-option](https://github.com/vuejs/rfcs/blob/master/active-rfcs/0030-emits-option.md)
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/require-explicit-emits.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/require-explicit-emits.js)
diff --git a/lib/index.js b/lib/index.js
index cd0691de..c4dbcb87 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -73,6 +73,7 @@ module.exports = {
     'no-template-shadow': require('./rules/no-template-shadow'),
     'no-template-target-blank': require('./rules/no-template-target-blank'),
     'no-textarea-mustache': require('./rules/no-textarea-mustache'),
+    'no-unregistered-components': require('./rules/no-unregistered-components'),
     'no-unsupported-features': require('./rules/no-unsupported-features'),
     'no-unused-components': require('./rules/no-unused-components'),
     'no-unused-vars': require('./rules/no-unused-vars'),
@@ -87,6 +88,7 @@ module.exports = {
     'require-component-is': require('./rules/require-component-is'),
     'require-default-prop': require('./rules/require-default-prop'),
     'require-direct-export': require('./rules/require-direct-export'),
+    'require-explicit-emits': require('./rules/require-explicit-emits'),
     'require-name-property': require('./rules/require-name-property'),
     'require-prop-type-constructor': require('./rules/require-prop-type-constructor'),
     'require-prop-types': require('./rules/require-prop-types'),
diff --git a/lib/rules/no-async-in-computed-properties.js b/lib/rules/no-async-in-computed-properties.js
index 42956550..35a785b4 100644
--- a/lib/rules/no-async-in-computed-properties.js
+++ b/lib/rules/no-async-in-computed-properties.js
@@ -72,7 +72,7 @@ module.exports = {
   },
 
   create (context) {
-    const forbiddenNodes = []
+    const computedPropertiesMap = new Map()
     let scopeStack = { upper: null, body: null }
 
     const expressionTypes = {
@@ -83,13 +83,9 @@ module.exports = {
       timed: 'timed function'
     }
 
-    function onFunctionEnter (node) {
+    function onFunctionEnter (node, { node: vueNode }) {
       if (node.async) {
-        forbiddenNodes.push({
-          node: node,
-          type: 'async',
-          targetBody: node.body
-        })
+        verify(node, node.body, 'async', computedPropertiesMap.get(vueNode))
       }
 
       scopeStack = { upper: scopeStack, body: node.body }
@@ -98,68 +94,53 @@ module.exports = {
     function onFunctionExit () {
       scopeStack = scopeStack.upper
     }
-    return Object.assign({},
-      {
-        ':function': onFunctionEnter,
-        ':function:exit': onFunctionExit,
-
-        NewExpression (node) {
-          if (node.callee.name === 'Promise') {
-            forbiddenNodes.push({
-              node: node,
-              type: 'new',
-              targetBody: scopeStack.body
-            })
-          }
-        },
-
-        CallExpression (node) {
-          if (isPromise(node)) {
-            forbiddenNodes.push({
-              node: node,
-              type: 'promise',
-              targetBody: scopeStack.body
-            })
-          } else if (isTimedFunction(node)) {
-            forbiddenNodes.push({
-              node: node,
-              type: 'timed',
-              targetBody: scopeStack.body
-            })
-          }
-        },
-
-        AwaitExpression (node) {
-          forbiddenNodes.push({
+
+    function verify (node, targetBody, type, computedProperties) {
+      computedProperties.forEach(cp => {
+        if (
+          cp.value &&
+          node.loc.start.line >= cp.value.loc.start.line &&
+          node.loc.end.line <= cp.value.loc.end.line &&
+          targetBody === cp.value
+        ) {
+          context.report({
             node: node,
-            type: 'await',
-            targetBody: scopeStack.body
-          })
-        }
-      },
-      utils.executeOnVue(context, (obj) => {
-        const computedProperties = utils.getComputedProperties(obj)
-
-        computedProperties.forEach(cp => {
-          forbiddenNodes.forEach(el => {
-            if (
-              cp.value &&
-              el.node.loc.start.line >= cp.value.loc.start.line &&
-              el.node.loc.end.line <= cp.value.loc.end.line &&
-              el.targetBody === cp.value
-            ) {
-              context.report({
-                node: el.node,
-                message: 'Unexpected {{expressionName}} in "{{propertyName}}" computed property.',
-                data: {
-                  expressionName: expressionTypes[el.type],
-                  propertyName: cp.key
-                }
-              })
+            message: 'Unexpected {{expressionName}} in "{{propertyName}}" computed property.',
+            data: {
+              expressionName: expressionTypes[type],
+              propertyName: cp.key
             }
           })
-        })
+        }
       })
-    )
+    }
+    return utils.defineVueVisitor(context, {
+      ObjectExpression (node, { node: vueNode }) {
+        if (node !== vueNode) {
+          return
+        }
+        computedPropertiesMap.set(node, utils.getComputedProperties(node))
+      },
+      ':function': onFunctionEnter,
+      ':function:exit': onFunctionExit,
+
+      NewExpression (node, { node: vueNode }) {
+        if (node.callee.name === 'Promise') {
+          verify(node, scopeStack.body, 'new', computedPropertiesMap.get(vueNode))
+        }
+      },
+
+      CallExpression (node, { node: vueNode }) {
+        if (isPromise(node)) {
+          verify(node, scopeStack.body, 'promise', computedPropertiesMap.get(vueNode))
+        } else if (isTimedFunction(node)) {
+          verify(node, scopeStack.body, 'timed', computedPropertiesMap.get(vueNode))
+        }
+      },
+
+      AwaitExpression (node, { node: vueNode }) {
+        verify(node, scopeStack.body, 'await', computedPropertiesMap.get(vueNode))
+      }
+    })
   }
 }
diff --git a/lib/rules/no-deprecated-events-api.js b/lib/rules/no-deprecated-events-api.js
index cc21edd4..8537469e 100644
--- a/lib/rules/no-deprecated-events-api.js
+++ b/lib/rules/no-deprecated-events-api.js
@@ -30,28 +30,17 @@ module.exports = {
   },
 
   create (context) {
-    const forbiddenNodes = []
-
-    return Object.assign(
+    return utils.defineVueVisitor(context,
       {
         'CallExpression > MemberExpression > ThisExpression' (node) {
           if (!['$on', '$off', '$once'].includes(node.parent.property.name)) return
-          forbiddenNodes.push(node.parent.parent)
+
+          context.report({
+            node: node.parent.parent,
+            messageId: 'noDeprecatedEventsApi'
+          })
         }
-      },
-      utils.executeOnVue(context, (obj) => {
-        forbiddenNodes.forEach(node => {
-          if (
-            node.loc.start.line >= obj.loc.start.line &&
-            node.loc.end.line <= obj.loc.end.line
-          ) {
-            context.report({
-              node,
-              messageId: 'noDeprecatedEventsApi'
-            })
-          }
-        })
-      })
+      }
     )
   }
 }
diff --git a/lib/rules/no-lifecycle-after-await.js b/lib/rules/no-lifecycle-after-await.js
index c4ae51a0..b1c3c243 100644
--- a/lib/rules/no-lifecycle-after-await.js
+++ b/lib/rules/no-lifecycle-after-await.js
@@ -25,16 +25,6 @@ module.exports = {
   create (context) {
     const lifecycleHookCallNodes = new Set()
     const setupFunctions = new Map()
-    const forbiddenNodes = new Map()
-
-    function addForbiddenNode (property, node) {
-      let list = forbiddenNodes.get(property)
-      if (!list) {
-        list = []
-        forbiddenNodes.set(property, list)
-      }
-      list.push(node)
-    }
 
     let scopeStack = { upper: null, functionNode: null }
 
@@ -56,56 +46,53 @@ module.exports = {
           for (const { node } of tracker.iterateEsmReferences(traceMap)) {
             lifecycleHookCallNodes.add(node)
           }
-        },
-        'Property[value.type=/^(Arrow)?FunctionExpression$/]' (node) {
-          if (utils.getStaticPropertyName(node) !== 'setup') {
-            return
-          }
-
-          setupFunctions.set(node.value, {
-            setupProperty: node,
-            afterAwait: false
-          })
-        },
-        ':function' (node) {
-          scopeStack = { upper: scopeStack, functionNode: node }
-        },
-        'AwaitExpression' () {
-          const setupFunctionData = setupFunctions.get(scopeStack.functionNode)
-          if (!setupFunctionData) {
-            return
-          }
-          setupFunctionData.afterAwait = true
-        },
-        'CallExpression' (node) {
-          const setupFunctionData = setupFunctions.get(scopeStack.functionNode)
-          if (!setupFunctionData || !setupFunctionData.afterAwait) {
-            return
-          }
-
-          if (lifecycleHookCallNodes.has(node)) {
-            addForbiddenNode(setupFunctionData.setupProperty, node)
-          }
-        },
-        ':function:exit' (node) {
-          scopeStack = scopeStack.upper
-
-          setupFunctions.delete(node)
         }
       },
-      utils.executeOnVue(context, obj => {
-        const reportsList = obj.properties
-          .map(item => forbiddenNodes.get(item))
-          .filter(reports => !!reports)
-        for (const reports of reportsList) {
-          for (const node of reports) {
-            context.report({
-              node,
-              messageId: 'forbidden'
+      utils.defineVueVisitor(context,
+        {
+          'Property[value.type=/^(Arrow)?FunctionExpression$/]' (node, { node: vueNode }) {
+            if (node.parent !== vueNode) {
+              return
+            }
+            if (utils.getStaticPropertyName(node) !== 'setup') {
+              return
+            }
+
+            setupFunctions.set(node.value, {
+              setupProperty: node,
+              afterAwait: false
             })
+          },
+          ':function' (node) {
+            scopeStack = { upper: scopeStack, functionNode: node }
+          },
+          'AwaitExpression' () {
+            const setupFunctionData = setupFunctions.get(scopeStack.functionNode)
+            if (!setupFunctionData) {
+              return
+            }
+            setupFunctionData.afterAwait = true
+          },
+          'CallExpression' (node) {
+            const setupFunctionData = setupFunctions.get(scopeStack.functionNode)
+            if (!setupFunctionData || !setupFunctionData.afterAwait) {
+              return
+            }
+
+            if (lifecycleHookCallNodes.has(node)) {
+              context.report({
+                node,
+                messageId: 'forbidden'
+              })
+            }
+          },
+          ':function:exit' (node) {
+            scopeStack = scopeStack.upper
+
+            setupFunctions.delete(node)
           }
-        }
-      })
+        },
+      )
     )
   }
 }
diff --git a/lib/rules/no-setup-props-destructure.js b/lib/rules/no-setup-props-destructure.js
index 7698ab9b..f167357d 100644
--- a/lib/rules/no-setup-props-destructure.js
+++ b/lib/rules/no-setup-props-destructure.js
@@ -22,115 +22,93 @@ module.exports = {
     }
   },
   create (context) {
-    const setupFunctions = new Map()
-    const forbiddenNodes = new Map()
+    const setupScopePropsReferenceIds = new Map()
 
-    function addForbiddenNode (property, node, messageId) {
-      let list = forbiddenNodes.get(property)
-      if (!list) {
-        list = []
-        forbiddenNodes.set(property, list)
-      }
-      list.push({
+    function report (node, messageId) {
+      context.report({
         node,
         messageId
       })
     }
 
-    function verify (left, right, { propsReferenceIds, setupProperty }) {
+    function verify (left, right, propsReferenceIds) {
       if (!right) {
         return
       }
 
-      if (left.type === 'ArrayPattern' || left.type === 'ObjectPattern') {
-        if (propsReferenceIds.has(right)) {
-          addForbiddenNode(setupProperty, left, 'getProperty')
-        }
-      } else if (left.type === 'Identifier' && right.type === 'MemberExpression') {
-        if (propsReferenceIds.has(right.object)) {
-          addForbiddenNode(setupProperty, right, 'getProperty')
-        }
+      if (left.type !== 'ArrayPattern' && left.type !== 'ObjectPattern' && right.type !== 'MemberExpression') {
+        return
+      }
+
+      let rightId = right
+      while (rightId.type === 'MemberExpression') {
+        rightId = rightId.object
+      }
+      if (propsReferenceIds.has(rightId)) {
+        report(left, 'getProperty')
       }
     }
 
-    let scopeStack = { upper: null, functionNode: null }
+    let scopeStack = null
 
-    return Object.assign(
-      {
-        'Property[value.type=/^(Arrow)?FunctionExpression$/]' (node) {
-          if (utils.getStaticPropertyName(node) !== 'setup') {
-            return
-          }
-          const param = node.value.params[0]
-          if (!param) {
-            // no arguments
-            return
-          }
-          if (param.type === 'RestElement') {
-            // cannot check
-            return
-          }
-          if (param.type === 'ArrayPattern' || param.type === 'ObjectPattern') {
-            addForbiddenNode(node, param, 'destructuring')
-            return
-          }
-          setupFunctions.set(node.value, {
-            setupProperty: node,
-            propsParam: param,
-            propsReferenceIds: new Set()
-          })
-        },
-        ':function' (node) {
-          scopeStack = { upper: scopeStack, functionNode: node }
-        },
-        ':function>*' (node) {
-          const setupFunctionData = setupFunctions.get(node.parent)
-          if (!setupFunctionData || setupFunctionData.propsParam !== node) {
-            return
-          }
-          const variable = findVariable(context.getScope(), node)
-          if (!variable) {
-            return
-          }
-          const { propsReferenceIds } = setupFunctionData
-          for (const reference of variable.references) {
-            if (!reference.isRead()) {
-              continue
-            }
+    return utils.defineVueVisitor(context, {
+      'Property[value.type=/^(Arrow)?FunctionExpression$/]' (node, { node: vueNode }) {
+        if (node.parent !== vueNode) {
+          return
+        }
+        if (utils.getStaticPropertyName(node) !== 'setup') {
+          return
+        }
+        const propsParam = node.value.params[0]
+        if (!propsParam) {
+          // no arguments
+          return
+        }
+        if (propsParam.type === 'RestElement') {
+          // cannot check
+          return
+        }
+        if (propsParam.type === 'ArrayPattern' || propsParam.type === 'ObjectPattern') {
+          report(propsParam, 'destructuring')
+          return
+        }
 
-            propsReferenceIds.add(reference.identifier)
-          }
-        },
-        'VariableDeclarator' (node) {
-          const setupFunctionData = setupFunctions.get(scopeStack.functionNode)
-          if (!setupFunctionData) {
-            return
-          }
-          verify(node.id, node.init, setupFunctionData)
-        },
-        'AssignmentExpression' (node) {
-          const setupFunctionData = setupFunctions.get(scopeStack.functionNode)
-          if (!setupFunctionData) {
-            return
+        const variable = findVariable(context.getScope(), propsParam)
+        if (!variable) {
+          return
+        }
+        const propsReferenceIds = new Set()
+        for (const reference of variable.references) {
+          if (!reference.isRead()) {
+            continue
           }
-          verify(node.left, node.right, setupFunctionData)
-        },
-        ':function:exit' (node) {
-          scopeStack = scopeStack.upper
 
-          setupFunctions.delete(node)
+          propsReferenceIds.add(reference.identifier)
         }
+        setupScopePropsReferenceIds.set(node.value, propsReferenceIds)
       },
-      utils.executeOnVue(context, obj => {
-        const reportsList = obj.properties
-          .map(item => forbiddenNodes.get(item))
-          .filter(reports => !!reports)
-        for (const reports of reportsList) {
-          for (const report of reports) {
-            context.report(report)
-          }
+      ':function' (node) {
+        scopeStack = { upper: scopeStack, functionNode: node }
+      },
+      'VariableDeclarator' (node) {
+        const propsReferenceIds = setupScopePropsReferenceIds.get(scopeStack.functionNode)
+        if (!propsReferenceIds) {
+          return
         }
-      })
-    )
+        verify(node.id, node.init, propsReferenceIds)
+      },
+      'AssignmentExpression' (node) {
+        const propsReferenceIds = setupScopePropsReferenceIds.get(scopeStack.functionNode)
+        if (!propsReferenceIds) {
+          return
+        }
+        verify(node.left, node.right, propsReferenceIds)
+      },
+      ':function:exit' (node) {
+        scopeStack = scopeStack.upper
+
+        setupScopePropsReferenceIds.delete(node)
+      }
+    })
   }
 }
diff --git a/lib/rules/no-side-effects-in-computed-properties.js b/lib/rules/no-side-effects-in-computed-properties.js
index a11c3032..eab6e0fa 100644
--- a/lib/rules/no-side-effects-in-computed-properties.js
+++ b/lib/rules/no-side-effects-in-computed-properties.js
@@ -23,7 +23,7 @@ module.exports = {
   },
 
   create (context) {
-    const forbiddenNodes = []
+    const computedPropertiesMap = new Map()
     let scopeStack = { upper: null, body: null }
 
     function onFunctionEnter (node) {
@@ -34,63 +34,56 @@ module.exports = {
       scopeStack = scopeStack.upper
     }
 
-    return Object.assign({},
-      {
-        ':function': onFunctionEnter,
-        ':function:exit': onFunctionExit,
+    function verify (node, targetBody, computedProperties) {
+      computedProperties.forEach(cp => {
+        if (
+          cp.value &&
+          node.loc.start.line >= cp.value.loc.start.line &&
+          node.loc.end.line <= cp.value.loc.end.line &&
+          targetBody === cp.value
+        ) {
+          context.report({
+            node: node,
+            message: 'Unexpected side effect in "{{key}}" computed property.',
+            data: { key: cp.key }
+          })
+        }
+      })
+    }
 
-        // this.xxx <=|+=|-=>
-        'AssignmentExpression' (node) {
-          if (node.left.type !== 'MemberExpression') return
-          if (utils.parseMemberExpression(node.left)[0] === 'this') {
-            forbiddenNodes.push({
-              node,
-              targetBody: scopeStack.body
-            })
-          }
-        },
-        // this.xxx <++|-->
-        'UpdateExpression > MemberExpression' (node) {
-          if (utils.parseMemberExpression(node)[0] === 'this') {
-            forbiddenNodes.push({
-              node,
-              targetBody: scopeStack.body
-            })
-          }
-        },
-        // this.xxx.func()
-        'CallExpression' (node) {
-          const code = utils.parseMemberOrCallExpression(node)
-          const MUTATION_REGEX = /(this.)((?!(concat|slice|map|filter)\().)[^\)]*((push|pop|shift|unshift|reverse|splice|sort|copyWithin|fill)\()/g
+    return utils.defineVueVisitor(context, {
+      ObjectExpression (node, { node: vueNode }) {
+        if (node !== vueNode) {
+          return
+        }
+        computedPropertiesMap.set(node, utils.getComputedProperties(node))
+      },
+      ':function': onFunctionEnter,
+      ':function:exit': onFunctionExit,
 
-          if (MUTATION_REGEX.test(code)) {
-            forbiddenNodes.push({
-              node,
-              targetBody: scopeStack.body
-            })
-          }
+      // this.xxx <=|+=|-=>
+      'AssignmentExpression' (node, { node: vueNode }) {
+        if (node.left.type !== 'MemberExpression') return
+        if (utils.parseMemberExpression(node.left)[0] === 'this') {
+          verify(node, scopeStack.body, computedPropertiesMap.get(vueNode))
         }
       },
-      utils.executeOnVue(context, (obj) => {
-        const computedProperties = utils.getComputedProperties(obj)
+      // this.xxx <++|-->
+      'UpdateExpression > MemberExpression' (node, { node: vueNode }) {
+        if (utils.parseMemberExpression(node)[0] === 'this') {
+          verify(node, scopeStack.body, computedPropertiesMap.get(vueNode))
+        }
+      },
+      // this.xxx.func()
+      'CallExpression' (node, { node: vueNode }) {
+        const code = utils.parseMemberOrCallExpression(node)
+        const MUTATION_REGEX = /(this.)((?!(concat|slice|map|filter)\().)[^\)]*((push|pop|shift|unshift|reverse|splice|sort|copyWithin|fill)\()/g
 
-        computedProperties.forEach(cp => {
-          forbiddenNodes.forEach(({ node, targetBody }) => {
-            if (
-              cp.value &&
-              node.loc.start.line >= cp.value.loc.start.line &&
-              node.loc.end.line <= cp.value.loc.end.line &&
-              targetBody === cp.value
-            ) {
-              context.report({
-                node: node,
-                message: 'Unexpected side effect in "{{key}}" computed property.',
-                data: { key: cp.key }
-              })
-            }
-          })
-        })
-      })
+        if (MUTATION_REGEX.test(code)) {
+          verify(node, scopeStack.body, computedPropertiesMap.get(vueNode))
+        }
+      }
+    }
     )
   }
 }
diff --git a/lib/rules/no-unregistered-components.js b/lib/rules/no-unregistered-components.js
new file mode 100644
index 00000000..f387248a
--- /dev/null
+++ b/lib/rules/no-unregistered-components.js
@@ -0,0 +1,153 @@
+/**
+ * @fileoverview Report used components that are not registered
+ * @author Jesús Ángel González Novez
+ */
+'use strict'
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const utils = require('eslint-plugin-vue/lib/utils')
+const casing = require('eslint-plugin-vue/lib/utils/casing')
+
+// ------------------------------------------------------------------------------
+// Rule helpers
+// ------------------------------------------------------------------------------
+
+const VUE_BUILT_IN_COMPONENTS = [
+  'component',
+  'suspense',
+  'teleport',
+  'transition',
+  'transition-group',
+  'keep-alive',
+  'slot'
+]
+/**
+ * Check whether the given node is a built-in component or not.
+ *
+ * Includes `suspense` and `teleport` from Vue 3.
+ *
+ * @param {ASTNode} node The start tag node to check.
+ * @returns {boolean} `true` if the node is a built-in component.
+ */
+const isBuiltInComponent = (node) => {
+  const rawName = node && casing.kebabCase(node.rawName)
+  return utils.isHtmlElementNode(node) &&
+    !utils.isHtmlWellKnownElementName(node.rawName) &&
+    VUE_BUILT_IN_COMPONENTS.indexOf(rawName) > -1
+}
+
+// ------------------------------------------------------------------------------
+// Rule Definition
+// ------------------------------------------------------------------------------
+
+module.exports = {
+  meta: {
+    type: 'suggestion',
+    docs: {
+      description: 'disallow using components that are not registered inside templates',
+      categories: null,
+      recommended: false,
+      url: 'https://eslint.vuejs.org/rules/no-unregistered-components.html'
+    },
+    fixable: null,
+    schema: [{
+      type: 'object',
+      properties: {
+        ignorePatterns: {
+          type: 'array'
+        }
+      },
+      additionalProperties: false
+    }]
+  },
+
+  create (context) {
+    const options = context.options[0] || {}
+    const ignorePatterns = options.ignorePatterns || []
+    const usedComponentNodes = []
+    const registeredComponents = []
+
+    return utils.defineTemplateBodyVisitor(context, {
+      VElement (node) {
+        if (
+          (!utils.isHtmlElementNode(node) && !utils.isSvgElementNode(node)) ||
+          utils.isHtmlWellKnownElementName(node.rawName) ||
+          utils.isSvgWellKnownElementName(node.rawName) ||
+          isBuiltInComponent(node)
+        ) {
+          return
+        }
+
+        usedComponentNodes.push({ node, name: node.rawName })
+      },
+      "VAttribute[directive=true][key.name.name='bind'][key.argument.name='is']" (node) {
+        if (
+          !node.value ||
+          node.value.type !== 'VExpressionContainer' ||
+          !node.value.expression
+        ) return
+
+        if (node.value.expression.type === 'Literal') {
+          if (utils.isHtmlWellKnownElementName(node.value.expression.value)) return
+          usedComponentNodes.push({ node, name: node.value.expression.value })
+        }
+      },
+      "VAttribute[directive=false][key.name='is']" (node) {
+        if (
+          !node.value || // `<component is />`
+          utils.isHtmlWellKnownElementName(node.value.value)
+        ) return
+        usedComponentNodes.push({ node, name: node.value.value })
+      },
+      "VElement[name='template']:exit" () {
+        // All registered components, transformed to kebab-case
+        const registeredComponentNames = registeredComponents
+          .map(({ name }) => casing.kebabCase(name))
+
+        // All registered components using kebab-case syntax
+        const componentsRegisteredAsKebabCase = registeredComponents
+          .filter(({ name }) => name === casing.kebabCase(name))
+          .map(({ name }) => name)
+
+        usedComponentNodes
+          .filter(({ name }) => {
+            const kebabCaseName = casing.kebabCase(name)
+
+            // Check ignored patterns in first place
+            if (ignorePatterns.find(pattern => {
+              const regExp = new RegExp(pattern)
+              return regExp.test(kebabCaseName) ||
+                regExp.test(casing.pascalCase(name)) ||
+                regExp.test(casing.camelCase(name)) ||
+                regExp.test(casing.snakeCase(name)) ||
+                regExp.test(name)
+            })) return false
+
+            // Component registered as `foo-bar` cannot be used as `FooBar`
+            if (
+              name.indexOf('-') === -1 &&
+              name === casing.pascalCase(name) &&
+              componentsRegisteredAsKebabCase.indexOf(kebabCaseName) !== -1
+            ) {
+              return true
+            }
+
+            // Otherwise
+            return registeredComponentNames.indexOf(kebabCaseName) === -1
+          })
+          .forEach(({ node, name }) => context.report({
+            node,
+            message: 'The "{{name}}" component has been used but not registered.',
+            data: {
+              name
+            }
+          }))
+      }
+    }, utils.executeOnVue(context, (obj) => {
+      registeredComponents.push(...utils.getRegisteredComponents(obj))
+    }))
+  }
+}
diff --git a/lib/rules/no-watch-after-await.js b/lib/rules/no-watch-after-await.js
index f65a459d..e666b861 100644
--- a/lib/rules/no-watch-after-await.js
+++ b/lib/rules/no-watch-after-await.js
@@ -50,16 +50,6 @@ module.exports = {
   create (context) {
     const watchCallNodes = new Set()
     const setupFunctions = new Map()
-    const forbiddenNodes = new Map()
-
-    function addForbiddenNode (property, node) {
-      let list = forbiddenNodes.get(property)
-      if (!list) {
-        list = []
-        forbiddenNodes.set(property, list)
-      }
-      list.push(node)
-    }
 
     let scopeStack = { upper: null, functionNode: null }
 
@@ -82,56 +72,53 @@ module.exports = {
           for (const { node } of tracker.iterateEsmReferences(traceMap)) {
             watchCallNodes.add(node)
           }
-        },
-        'Property[value.type=/^(Arrow)?FunctionExpression$/]' (node) {
-          if (utils.getStaticPropertyName(node) !== 'setup') {
-            return
-          }
-
-          setupFunctions.set(node.value, {
-            setupProperty: node,
-            afterAwait: false
-          })
-        },
-        ':function' (node) {
-          scopeStack = { upper: scopeStack, functionNode: node }
-        },
-        'AwaitExpression' () {
-          const setupFunctionData = setupFunctions.get(scopeStack.functionNode)
-          if (!setupFunctionData) {
-            return
-          }
-          setupFunctionData.afterAwait = true
-        },
-        'CallExpression' (node) {
-          const setupFunctionData = setupFunctions.get(scopeStack.functionNode)
-          if (!setupFunctionData || !setupFunctionData.afterAwait) {
-            return
-          }
-
-          if (watchCallNodes.has(node) && !isMaybeUsedStopHandle(node)) {
-            addForbiddenNode(setupFunctionData.setupProperty, node)
-          }
-        },
-        ':function:exit' (node) {
-          scopeStack = scopeStack.upper
-
-          setupFunctions.delete(node)
         }
       },
-      utils.executeOnVue(context, obj => {
-        const reportsList = obj.properties
-          .map(item => forbiddenNodes.get(item))
-          .filter(reports => !!reports)
-        for (const reports of reportsList) {
-          for (const node of reports) {
-            context.report({
-              node,
-              messageId: 'forbidden'
+      utils.defineVueVisitor(context,
+        {
+          'Property[value.type=/^(Arrow)?FunctionExpression$/]' (node, { node: vueNode }) {
+            if (node.parent !== vueNode) {
+              return
+            }
+            if (utils.getStaticPropertyName(node) !== 'setup') {
+              return
+            }
+
+            setupFunctions.set(node.value, {
+              setupProperty: node,
+              afterAwait: false
             })
+          },
+          ':function' (node) {
+            scopeStack = { upper: scopeStack, functionNode: node }
+          },
+          'AwaitExpression' () {
+            const setupFunctionData = setupFunctions.get(scopeStack.functionNode)
+            if (!setupFunctionData) {
+              return
+            }
+            setupFunctionData.afterAwait = true
+          },
+          'CallExpression' (node) {
+            const setupFunctionData = setupFunctions.get(scopeStack.functionNode)
+            if (!setupFunctionData || !setupFunctionData.afterAwait) {
+              return
+            }
+
+            if (watchCallNodes.has(node) && !isMaybeUsedStopHandle(node)) {
+              context.report({
+                node,
+                messageId: 'forbidden'
+              })
+            }
+          },
+          ':function:exit' (node) {
+            scopeStack = scopeStack.upper
+
+            setupFunctions.delete(node)
           }
-        }
-      })
+        },
+      )
     )
   }
 }
diff --git a/lib/rules/require-explicit-emits.js b/lib/rules/require-explicit-emits.js
new file mode 100644
index 00000000..2addf847
--- /dev/null
+++ b/lib/rules/require-explicit-emits.js
@@ -0,0 +1,390 @@
+/**
+ * @author Yosuke Ota
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+// @ts-check
+
+/**
+ * @typedef {import('vue-eslint-parser').AST.ESLintLiteral} Literal
+ * @typedef {import('vue-eslint-parser').AST.ESLintProperty} Property
+ * @typedef {import('vue-eslint-parser').AST.ESLintObjectExpression} ObjectExpression
+ * @typedef {import('../utils').ComponentArrayEmit} ComponentArrayEmit
+ * @typedef {import('../utils').ComponentObjectEmit} ComponentObjectEmit
+ */
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const { findVariable } = require('eslint-utils')
+const utils = require('../utils')
+
+// ------------------------------------------------------------------------------
+// Helpers
+// ------------------------------------------------------------------------------
+
+const FIX_EMITS_AFTER_OPTIONS = [
+  'props',
+  'propsData',
+  'setup',
+  'data',
+  'computed',
+  'watch',
+  'methods',
+  'template', 'render',
+  'renderError',
+
+  // lifecycle hooks
+  'beforeCreate',
+  'created',
+  'beforeMount',
+  'mounted',
+  'beforeUpdate',
+  'updated',
+  'activated',
+  'deactivated',
+  'beforeDestroy',
+  'destroyed'
+]
+
+/**
+ * Check whether the given token is a left brace.
+ * @param {Token} token The token to check.
+ * @returns {boolean} `true` if the token is a left brace.
+ */
+function isLeftBrace (token) {
+  return token != null && token.type === 'Punctuator' && token.value === '{'
+}
+
+/**
+ * Check whether the given token is a right brace.
+ * @param {Token} token The token to check.
+ * @returns {boolean} `true` if the token is a right brace.
+ */
+function isRightBrace (token) {
+  return token != null && token.type === 'Punctuator' && token.value === '}'
+}
+
+/**
+ * Check whether the given token is a left bracket.
+ * @param {Token} token The token to check.
+ * @returns {boolean} `true` if the token is a left bracket.
+ */
+function isLeftBracket (token) {
+  return token != null && token.type === 'Punctuator' && token.value === '['
+}
+
+// ------------------------------------------------------------------------------
+// Rule Definition
+// ------------------------------------------------------------------------------
+
+module.exports = {
+  meta: {
+    type: 'suggestion',
+    docs: {
+      description: 'require `emits` option with name triggered by `$emit()`',
+      categories: undefined,
+      url: 'https://eslint.vuejs.org/rules/require-explicit-emits.html'
+    },
+    fixable: null,
+    schema: [],
+    messages: {
+      missing: 'The "{{name}}" event has been triggered but not declared on `emits` option.',
+      addOneOption: 'Add the "{{name}}" to `emits` option.',
+      addArrayEmitsOption: 'Add the `emits` option with array syntax and define "{{name}}" event.',
+      addObjectEmitsOption: 'Add the `emits` option with object syntax and define "{{name}}" event.'
+    }
+  },
+
+  create (context) {
+    /** @typedef { { node: Literal, name: string } } EmitCellName */
+
+    const setupContexts = new Map()
+    const vueEmitsDeclarations = new Map()
+
+    /** @type {EmitCellName[]} */
+    const templateEmitCellNames = []
+    /** @type { { type: 'export' | 'mark' | 'definition', object: ObjectExpression, emits: (ComponentArrayEmit | ComponentObjectEmit)[] } | null } */
+    let vueObjectData = null
+
+    function addTemplateEmitCellName (nameLiteralNode) {
+      templateEmitCellNames.push({
+        node: nameLiteralNode,
+        name: nameLiteralNode.value
+      })
+    }
+
+    function verify (emitsDeclarations, nameLiteralNode, vueObjectNode) {
+      const name = nameLiteralNode.value
+      if (emitsDeclarations.some(e => e.emitName === name)) {
+        return
+      }
+      context.report({
+        node: nameLiteralNode,
+        messageId: 'missing',
+        data: {
+          name
+        },
+        suggest: buildSuggest(vueObjectNode, emitsDeclarations, nameLiteralNode, context)
+      })
+    }
+
+    return utils.defineTemplateBodyVisitor(context,
+      {
+        'CallExpression[arguments.0.type=Literal]' (node) {
+          const callee = node.callee
+          const nameLiteralNode = node.arguments[0]
+          if (!nameLiteralNode || typeof nameLiteralNode.value !== 'string') {
+            // cannot check
+            return
+          }
+          if (callee.type === 'Identifier' && callee.name === '$emit') {
+            addTemplateEmitCellName(nameLiteralNode)
+          }
+        },
+        "VElement[parent.type!='VElement']:exit" () {
+          if (!vueObjectData) {
+            return
+          }
+          const emitsDeclarationNames = new Set(vueObjectData.emits.map(e => e.emitName))
+
+          for (const { name, node } of templateEmitCellNames) {
+            if (emitsDeclarationNames.has(name)) {
+              continue
+            }
+            context.report({
+              node,
+              messageId: 'missing',
+              data: {
+                name
+              },
+              suggest: buildSuggest(vueObjectData.object, vueObjectData.emits, node, context)
+            })
+          }
+        }
+      },
+      utils.defineVueVisitor(context, {
+        ObjectExpression (node, { node: vueNode }) {
+          if (node !== vueNode) {
+            return
+          }
+          vueEmitsDeclarations.set(node, utils.getComponentEmits(node))
+
+          const setupProperty = node.properties.find(p => utils.getStaticPropertyName(p) === 'setup')
+          if (!setupProperty) {
+            return
+          }
+          if (!/^(Arrow)?FunctionExpression$/.test(setupProperty.value.type)) {
+            return
+          }
+          const contextParam = setupProperty.value.params[1]
+          if (!contextParam) {
+            // no arguments
+            return
+          }
+          if (contextParam.type === 'RestElement') {
+            // cannot check
+            return
+          }
+          if (contextParam.type === 'ArrayPattern') {
+            // cannot check
+            return
+          }
+          const contextReferenceIds = new Set()
+          const emitReferenceIds = new Set()
+          if (contextParam.type === 'ObjectPattern') {
+            const emitProperty = contextParam.properties.find(p => p.type === 'Property' && utils.getStaticPropertyName(p) === 'emit')
+            if (!emitProperty) {
+              return
+            }
+            const emitParam = emitProperty.value
+            // `setup(props, {emit})`
+            const variable = findVariable(context.getScope(), emitParam)
+            if (!variable) {
+              return
+            }
+            for (const reference of variable.references) {
+              if (!reference.isRead()) {
+                continue
+              }
+
+              emitReferenceIds.add(reference.identifier)
+            }
+          } else {
+            // `setup(props, context)`
+            const variable = findVariable(context.getScope(), contextParam)
+            if (!variable) {
+              return
+            }
+            for (const reference of variable.references) {
+              if (!reference.isRead()) {
+                continue
+              }
+
+              contextReferenceIds.add(reference.identifier)
+            }
+          }
+          setupContexts.set(node, {
+            contextReferenceIds,
+            emitReferenceIds
+          })
+        },
+        'CallExpression[arguments.0.type=Literal]' (node, { node: vueNode }) {
+          const callee = node.callee
+          const nameLiteralNode = node.arguments[0]
+          if (!nameLiteralNode || typeof nameLiteralNode.value !== 'string') {
+            // cannot check
+            return
+          }
+          const emitsDeclarations = vueEmitsDeclarations.get(vueNode)
+
+          let emit
+          if (callee.type === 'MemberExpression') {
+            const name = utils.getStaticPropertyName(callee)
+            if (name === 'emit' || name === '$emit') {
+              emit = { name, member: callee }
+            }
+          }
+
+          // verify setup context
+          const setupContext = setupContexts.get(vueNode)
+          if (setupContext) {
+            const { contextReferenceIds, emitReferenceIds } = setupContext
+            if (emitReferenceIds.has(callee)) {
+            // verify setup(props,{emit}) {emit()}
+              verify(emitsDeclarations, nameLiteralNode, vueNode)
+            } else if (emit && emit.name === 'emit' && contextReferenceIds.has(emit.member.object)) {
+            // verify setup(props,context) {context.emit()}
+              verify(emitsDeclarations, nameLiteralNode, vueNode)
+            }
+          }
+
+          // verify $emit
+          if (emit && emit.name === '$emit') {
+            const objectType = emit.member.object.type
+            if (objectType === 'Identifier' || objectType === 'ThisExpression') {
+              // verify this.$emit()
+              verify(emitsDeclarations, nameLiteralNode, vueNode)
+            }
+          }
+        },
+        'ObjectExpression:exit' (node, { node: vueNode, type }) {
+          if (node !== vueNode) {
+            return
+          }
+          if (!vueObjectData || vueObjectData.type !== 'export') {
+            vueObjectData = {
+              type,
+              object: node,
+              emits: vueEmitsDeclarations.get(node)
+            }
+          }
+          setupContexts.delete(node)
+          vueEmitsDeclarations.delete(node)
+        }
+      }),
+    )
+  }
+}
+
+/**
+ * @param {ObjectExpression} object
+ * @param {(ComponentArrayEmit | ComponentObjectEmit)[]} emits
+ * @param {Literal} nameNode
+ * @param {RuleContext} context
+ */
+function buildSuggest (object, emits, nameNode, context) {
+  const certainEmits = emits.filter(e => e.key)
+  if (certainEmits.length) {
+    const last = certainEmits[certainEmits.length - 1]
+    return [
+      {
+        messageId: 'addOneOption',
+        data: { name: nameNode.value },
+        fix (fixer) {
+          if (last.value === null) {
+            // Array
+            return fixer.insertTextAfter(last.node, `, '${nameNode.value}'`)
+          } else {
+            // Object
+            return fixer.insertTextAfter(last.node, `, '${nameNode.value}': null`)
+          }
+        }
+      }
+    ]
+  }
+
+  const propertyNodes = object.properties
+    .filter(p =>
+      p.type === 'Property' &&
+      p.key.type === 'Identifier'
+    )
+
+  const emitsOption = propertyNodes.find(p => utils.getStaticPropertyName(p) === 'emits')
+  if (emitsOption) {
+    const sourceCode = context.getSourceCode()
+    const emitsOptionValue = emitsOption.value
+    if (emitsOptionValue.type === 'ArrayExpression') {
+      const leftBracket = sourceCode.getFirstToken(emitsOptionValue, isLeftBracket)
+      return [
+        {
+          messageId: 'addOneOption',
+          data: { name: nameNode.value },
+          fix (fixer) {
+            return fixer.insertTextAfter(leftBracket, `'${nameNode.value}'${emitsOptionValue.elements.length ? ',' : ''}`)
+          }
+        }
+      ]
+    } else if (emitsOptionValue.type === 'ObjectExpression') {
+      const leftBrace = sourceCode.getFirstToken(emitsOptionValue, isLeftBrace)
+      return [
+        {
+          messageId: 'addOneOption',
+          data: { name: nameNode.value },
+          fix (fixer) {
+            return fixer.insertTextAfter(leftBrace, `'${nameNode.value}': null${emitsOptionValue.properties.length ? ',' : ''}`)
+          }
+        }
+      ]
+    }
+    return []
+  }
+
+  const sourceCode = context.getSourceCode()
+  const afterOptionNode = propertyNodes.find(p => FIX_EMITS_AFTER_OPTIONS.includes(utils.getStaticPropertyName(p)))
+  return [
+    {
+      messageId: 'addArrayEmitsOption',
+      data: { name: nameNode.value },
+      fix (fixer) {
+        if (afterOptionNode) {
+          return fixer.insertTextAfter(sourceCode.getTokenBefore(afterOptionNode), `\nemits: ['${nameNode.value}'],`)
+        } else if (object.properties.length) {
+          const before = propertyNodes[propertyNodes.length - 1] || object.properties[object.properties.length - 1]
+          return fixer.insertTextAfter(before, `,\nemits: ['${nameNode.value}']`)
+        } else {
+          const objectLeftBrace = sourceCode.getFirstToken(object, isLeftBrace)
+          const objectRightBrace = sourceCode.getLastToken(object, isRightBrace)
+          return fixer.insertTextAfter(objectLeftBrace, `\nemits: ['${nameNode.value}']${objectLeftBrace.loc.end.line < objectRightBrace.loc.start.line ? '' : '\n'}`)
+        }
+      }
+    },
+    {
+      messageId: 'addObjectEmitsOption',
+      data: { name: nameNode.value },
+      fix (fixer) {
+        if (afterOptionNode) {
+          return fixer.insertTextAfter(sourceCode.getTokenBefore(afterOptionNode), `\nemits: {'${nameNode.value}': null},`)
+        } else if (object.properties.length) {
+          const before = propertyNodes[propertyNodes.length - 1] || object.properties[object.properties.length - 1]
+          return fixer.insertTextAfter(before, `,\nemits: {'${nameNode.value}': null}`)
+        } else {
+          const objectLeftBrace = sourceCode.getFirstToken(object, isLeftBrace)
+          const objectRightBrace = sourceCode.getLastToken(object, isRightBrace)
+          return fixer.insertTextAfter(objectLeftBrace, `\nemits: {'${nameNode.value}': null}${objectLeftBrace.loc.end.line < objectRightBrace.loc.start.line ? '' : '\n'}`)
+        }
+      }
+    }
+  ]
+}
diff --git a/lib/rules/require-render-return.js b/lib/rules/require-render-return.js
index 3ae1623d..17cfcb35 100644
--- a/lib/rules/require-render-return.js
+++ b/lib/rules/require-render-return.js
@@ -23,32 +23,36 @@ module.exports = {
   },
 
   create (context) {
-    const forbiddenNodes = []
+    const renderFunctions = new Map()
 
     // ----------------------------------------------------------------------
     // Public
     // ----------------------------------------------------------------------
 
     return Object.assign({},
+      utils.defineVueVisitor(context,
+        {
+          ObjectExpression (obj, { node: vueNode }) {
+            if (obj !== vueNode) {
+              return
+            }
+            const node = obj.properties.find(item => item.type === 'Property' &&
+              utils.getStaticPropertyName(item) === 'render' &&
+              (item.value.type === 'ArrowFunctionExpression' || item.value.type === 'FunctionExpression')
+            )
+            if (!node) return
+            renderFunctions.set(node.value, node.key)
+          }
+        }
+      ),
       utils.executeOnFunctionsWithoutReturn(true, node => {
-        forbiddenNodes.push(node)
+        if (renderFunctions.has(node)) {
+          context.report({
+            node: renderFunctions.get(node),
+            message: 'Expected to return a value in render function.'
+          })
+        }
       }),
-      utils.executeOnVue(context, obj => {
-        const node = obj.properties.find(item => item.type === 'Property' &&
-          utils.getStaticPropertyName(item) === 'render' &&
-          (item.value.type === 'ArrowFunctionExpression' || item.value.type === 'FunctionExpression')
-        )
-        if (!node) return
-
-        forbiddenNodes.forEach(el => {
-          if (node.value === el) {
-            context.report({
-              node: node.key,
-              message: 'Expected to return a value in render function.'
-            })
-          }
-        })
-      })
     )
   }
 }
diff --git a/lib/rules/return-in-computed-property.js b/lib/rules/return-in-computed-property.js
index 00a43a5c..c62f5984 100644
--- a/lib/rules/return-in-computed-property.js
+++ b/lib/rules/return-in-computed-property.js
@@ -36,33 +36,38 @@ module.exports = {
     const options = context.options[0] || {}
     const treatUndefinedAsUnspecified = !(options.treatUndefinedAsUnspecified === false)
 
-    const forbiddenNodes = []
+    const computedProperties = new Set()
 
     // ----------------------------------------------------------------------
     // Public
     // ----------------------------------------------------------------------
 
     return Object.assign({},
+      utils.defineVueVisitor(context,
+        {
+          ObjectExpression (obj, { node: vueNode }) {
+            if (obj !== vueNode) {
+              return
+            }
+            for (const computedProperty of utils.getComputedProperties(obj)) {
+              computedProperties.add(computedProperty)
+            }
+          }
+        }
+      ),
       utils.executeOnFunctionsWithoutReturn(treatUndefinedAsUnspecified, node => {
-        forbiddenNodes.push(node)
-      }),
-      utils.executeOnVue(context, properties => {
-        const computedProperties = utils.getComputedProperties(properties)
-
         computedProperties.forEach(cp => {
-          forbiddenNodes.forEach(el => {
-            if (cp.value && cp.value.parent === el) {
-              context.report({
-                node: el,
-                message: 'Expected to return a value in "{{name}}" computed property.',
-                data: {
-                  name: cp.key
-                }
-              })
-            }
-          })
+          if (cp.value && cp.value.parent === node) {
+            context.report({
+              node,
+              message: 'Expected to return a value in "{{name}}" computed property.',
+              data: {
+                name: cp.key
+              }
+            })
+          }
         })
-      })
+      }),
     )
   }
 }
diff --git a/lib/utils/index.js b/lib/utils/index.js
index b302cd6c..3e1ce4c5 100644
--- a/lib/utils/index.js
+++ b/lib/utils/index.js
@@ -17,10 +17,19 @@
  * @typedef {import('vue-eslint-parser').AST.ESLintTemplateLiteral} TemplateLiteral
  */
 
+/**
+ * @typedef {import('eslint').Rule.RuleContext} RuleContext
+ * @typedef {import('vue-eslint-parser').AST.Token} Token
+ */
+
 /**
  * @typedef { {key: Literal | null, value: null, node: ArrayExpression['elements'][0], propName: string} } ComponentArrayProp
  * @typedef { {key: Property['key'], value: Property['value'], node: Property, propName: string} } ComponentObjectProp
  */
+/**
+ * @typedef { {key: Literal | null, value: null, node: ArrayExpression['elements'][0], emitName: string} } ComponentArrayEmit
+ * @typedef { {key: Property['key'], value: Property['value'], node: Property, emitName: string} } ComponentObjectEmit
+ */
 
 // ------------------------------------------------------------------------------
 // Helpers
@@ -33,6 +42,11 @@ const assert = require('assert')
 const path = require('path')
 const vueEslintParser = require('vue-eslint-parser')
 
+/**
+ * @type { WeakMap<RuleContext, Token[]> }
+ */
+const componentComments = new WeakMap()
+
 /**
  * Wrap the rule context object to override methods which access to tokens (such as getTokenAfter).
  * @param {RuleContext} context The rule context object.
@@ -422,10 +436,8 @@ module.exports = {
       return []
     }
 
-    let props
-
     if (propsNode.value.type === 'ObjectExpression') {
-      props = propsNode.value.properties
+      return propsNode.value.properties
         .filter(prop => prop.type === 'Property')
         .map(prop => {
           return {
@@ -434,15 +446,50 @@ module.exports = {
           }
         })
     } else {
-      props = propsNode.value.elements
+      return propsNode.value.elements
         .filter(prop => prop)
         .map(prop => {
           const key = prop.type === 'Literal' && typeof prop.value === 'string' ? prop : null
           return { key, value: null, node: prop, propName: key != null ? prop.value : null }
         })
     }
+  },
+
+  /**
+   * Get all emits by looking at all component's properties
+   * @param {ObjectExpression} componentObject Object with component definition
+   * @return {(ComponentArrayEmit | ComponentObjectEmit)[]} Array of component emits in format: [{key?: String, value?: ASTNode, node: ASTNod}]
+   */
+  getComponentEmits (componentObject) {
+    const emitsNode = componentObject.properties
+      .find(p =>
+        p.type === 'Property' &&
+        p.key.type === 'Identifier' &&
+        p.key.name === 'emits' &&
+        (p.value.type === 'ObjectExpression' || p.value.type === 'ArrayExpression')
+      )
+
+    if (!emitsNode) {
+      return []
+    }
 
-    return props
+    if (emitsNode.value.type === 'ObjectExpression') {
+      return emitsNode.value.properties
+        .filter(prop => prop.type === 'Property')
+        .map(prop => {
+          return {
+            key: prop.key, value: unwrapTypes(prop.value), node: prop,
+            emitName: getStaticPropertyName(prop)
+          }
+        })
+    } else {
+      return emitsNode.value.elements
+        .filter(prop => prop)
+        .map(prop => {
+          const key = prop.type === 'Literal' && typeof prop.value === 'string' ? prop : null
+          return { key, value: null, node: prop, emitName: key != null ? prop.value : null }
+        })
+    }
   },
 
   /**
@@ -484,115 +531,66 @@ module.exports = {
       })
   },
 
-  isVueFile (path) {
-    return path.endsWith('.vue') || path.endsWith('.jsx')
-  },
+  isVueFile,
 
   /**
-   * Check whether the given node is a Vue component based
-   * on the filename and default export type
-   * export default {} in .vue || .jsx
-   * @param {ASTNode} node Node to check
-   * @param {string} path File name with extension
-   * @returns {boolean}
+   * Check if current file is a Vue instance or component and call callback
+   * @param {RuleContext} context The ESLint rule context object.
+   * @param {Function} cb Callback function
    */
-  isVueComponentFile (node, path) {
-    return this.isVueFile(path) &&
-      node.type === 'ExportDefaultDeclaration' &&
-      node.declaration.type === 'ObjectExpression'
+  executeOnVue (context, cb) {
+    return compositingVisitors(
+      this.executeOnVueComponent(context, cb),
+      this.executeOnVueInstance(context, cb)
+    )
   },
 
   /**
-   * Check whether given node is Vue component
-   * Vue.component('xxx', {}) || component('xxx', {})
-   * @param {ASTNode} node Node to check
-   * @returns {boolean}
+   * Define handlers to traverse the Vue Objects.
+   * @param {RuleContext} context The ESLint rule context object.
+   * @param {Object} visitor The visitor to traverse the Vue Objects.
+   * @param {Function} cb Callback function
    */
-  isVueComponent (node) {
-    if (node.type === 'CallExpression') {
-      const callee = node.callee
-
-      if (callee.type === 'MemberExpression') {
-        const calleeObject = unwrapTypes(callee.object)
-
-        if (calleeObject.type === 'Identifier') {
-          const propName = getStaticPropertyName(callee.property)
-          if (calleeObject.name === 'Vue') {
-            // for Vue.js 2.x
-            // Vue.component('xxx', {}) || Vue.mixin({}) || Vue.extend('xxx', {})
-            const isFullVueComponentForVue2 =
-              ['component', 'mixin', 'extend'].includes(propName) &&
-              isObjectArgument(node)
+  defineVueVisitor (context, visitor) {
+    let vueStack = null
 
-            return isFullVueComponentForVue2
-          }
-
-          // for Vue.js 3.x
-          // app.component('xxx', {}) || app.mixin({})
-          const isFullVueComponent =
-            ['component', 'mixin'].includes(propName) &&
-            isObjectArgument(node)
-
-          return isFullVueComponent
-        }
+    function callVisitor (key, node) {
+      if (visitor[key] && vueStack) {
+        visitor[key](node, vueStack)
       }
-
-      if (callee.type === 'Identifier') {
-        if (callee.name === 'component') {
-          // for Vue.js 2.x
-          // component('xxx', {})
-          const isDestructedVueComponent = isObjectArgument(node)
-          return isDestructedVueComponent
-        }
-        if (callee.name === 'createApp') {
-          // for Vue.js 3.x
-          // createApp({})
-          const isAppVueComponent = isObjectArgument(node)
-          return isAppVueComponent
-        }
-        if (callee.name === 'defineComponent') {
-          // for Vue.js 3.x
-          // defineComponent({})
-          const isDestructedVueComponent = isObjectArgument(node)
-          return isDestructedVueComponent
-        }
+    }
+    function objectEnter (node) {
+      const type = getVueObjectType(context, node)
+      if (type) {
+        vueStack = { node, type, parent: vueStack }
+      }
+    }
+    function objectExit (node) {
+      if (vueStack && vueStack.node === node) {
+        vueStack = vueStack.parent
       }
     }
 
-    return false
+    const vueVisitor = {}
+    for (const key in visitor) {
+      vueVisitor[key] = (node) => callVisitor(key, node)
+    }
 
-    function isObjectArgument (node) {
-      return node.arguments.length > 0 &&
-        unwrapTypes(node.arguments.slice(-1)[0]).type === 'ObjectExpression'
+    return {
+      ...vueVisitor,
+      ObjectExpression: vueVisitor.ObjectExpression ? (node) => {
+        objectEnter(node)
+        vueVisitor.ObjectExpression(node)
+      } : objectEnter,
+      'ObjectExpression:exit': vueVisitor['ObjectExpression:exit'] ? (node) => {
+        vueVisitor['ObjectExpression:exit'](node)
+        objectExit(node)
+      } : objectExit
     }
   },
 
-  /**
-   * Check whether given node is new Vue instance
-   * new Vue({})
-   * @param {ASTNode} node Node to check
-   * @returns {boolean}
-   */
-  isVueInstance (node) {
-    const callee = node.callee
-    return node.type === 'NewExpression' &&
-      callee.type === 'Identifier' &&
-      callee.name === 'Vue' &&
-      node.arguments.length &&
-      unwrapTypes(node.arguments[0]).type === 'ObjectExpression'
-  },
-
-  /**
-   * Check if current file is a Vue instance or component and call callback
-   * @param {RuleContext} context The ESLint rule context object.
-   * @param {Function} cb Callback function
-   */
-  executeOnVue (context, cb) {
-    return Object.assign(
-      this.executeOnVueComponent(context, cb),
-      this.executeOnVueInstance(context, cb)
-    )
-  },
+  getVueObjectType,
+  compositingVisitors,
 
   /**
    * Check if current file is a Vue instance (new Vue) and call callback
@@ -600,13 +598,11 @@ module.exports = {
    * @param {Function} cb Callback function
    */
   executeOnVueInstance (context, cb) {
-    const _this = this
-
     return {
-      'NewExpression:exit' (node) {
-        // new Vue({})
-        if (!_this.isVueInstance(node)) return
-        cb(node.arguments[0])
+      'ObjectExpression:exit' (node) {
+        const type = getVueObjectType(context, node)
+        if (!type || type !== 'instance') return
+        cb(node, type)
       }
     }
   },
@@ -617,32 +613,11 @@ module.exports = {
    * @param {Function} cb Callback function
    */
   executeOnVueComponent (context, cb) {
-    const filePath = context.getFilename()
-    const sourceCode = context.getSourceCode()
-    const _this = this
-    const componentComments = sourceCode.getAllComments().filter(comment => /@vue\/component/g.test(comment.value))
-    const foundNodes = []
-
-    const isDuplicateNode = (node) => {
-      if (foundNodes.some(el => el.loc.start.line === node.loc.start.line)) return true
-      foundNodes.push(node)
-      return false
-    }
-
     return {
       'ObjectExpression:exit' (node) {
-        if (!componentComments.some(el => el.loc.end.line === node.loc.start.line - 1) || isDuplicateNode(node)) return
-        cb(node)
-      },
-      'ExportDefaultDeclaration:exit' (node) {
-        // export default {} in .vue || .jsx
-        if (!_this.isVueComponentFile(node, filePath) || isDuplicateNode(node.declaration)) return
-        cb(node.declaration)
-      },
-      'CallExpression:exit' (node) {
-        // Vue.component('xxx', {}) || component('xxx', {})
-        if (!_this.isVueComponent(node) || isDuplicateNode(node.arguments.slice(-1)[0])) return
-        cb(unwrapTypes(node.arguments.slice(-1)[0]))
+        const type = getVueObjectType(context, node)
+        if (!type || (type !== 'mark' && type !== 'export' && type !== 'definition')) return
+        cb(node, type)
       }
     }
   },
@@ -882,14 +857,15 @@ module.exports = {
    */
   unwrapTypes
 }
+
 /**
-* Unwrap typescript types like "X as F"
-* @template T
-* @param {T} node
-* @return {T}
-*/
+ * Unwrap typescript types like "X as F"
+ * @template T
+ * @param {T} node
+ * @return {T}
+ */
 function unwrapTypes (node) {
-  return node.type === 'TSAsExpression' ? node.expression : node
+  return !node ? node : node.type === 'TSAsExpression' ? unwrapTypes(node.expression) : node
 }
 
 /**
@@ -933,3 +909,174 @@ function getStaticPropertyName (node) {
 
   return null
 }
+
+function isVueFile (path) {
+  return path.endsWith('.vue') || path.endsWith('.jsx')
+}
+
+/**
+ * Check whether the given node is a Vue component based
+ * on the filename and default export type
+ * export default {} in .vue || .jsx
+ * @param {ASTNode} node Node to check
+ * @param {string} path File name with extension
+ * @returns {boolean}
+ */
+function isVueComponentFile (node, path) {
+  return isVueFile(path) &&
+      node.type === 'ExportDefaultDeclaration' &&
+      node.declaration.type === 'ObjectExpression'
+}
+
+/**
+ * Check whether given node is Vue component
+ * Vue.component('xxx', {}) || component('xxx', {})
+ * @param {ASTNode} node Node to check
+ * @returns {boolean}
+ */
+function isVueComponent (node) {
+  if (node.type === 'CallExpression') {
+    const callee = node.callee
+
+    if (callee.type === 'MemberExpression') {
+      const calleeObject = unwrapTypes(callee.object)
+
+      if (calleeObject.type === 'Identifier') {
+        const propName = getStaticPropertyName(callee.property)
+        if (calleeObject.name === 'Vue') {
+          // for Vue.js 2.x
+          // Vue.component('xxx', {}) || Vue.mixin({}) || Vue.extend('xxx', {})
+          const isFullVueComponentForVue2 =
+              ['component', 'mixin', 'extend'].includes(propName) &&
+              isObjectArgument(node)
+
+          return isFullVueComponentForVue2
+        }
+
+        // for Vue.js 3.x
+        // app.component('xxx', {}) || app.mixin({})
+        const isFullVueComponent =
+            ['component', 'mixin'].includes(propName) &&
+            isObjectArgument(node)
+
+        return isFullVueComponent
+      }
+    }
+
+    if (callee.type === 'Identifier') {
+      if (callee.name === 'component') {
+        // for Vue.js 2.x
+        // component('xxx', {})
+        const isDestructedVueComponent = isObjectArgument(node)
+        return isDestructedVueComponent
+      }
+      if (callee.name === 'createApp') {
+        // for Vue.js 3.x
+        // createApp({})
+        const isAppVueComponent = isObjectArgument(node)
+        return isAppVueComponent
+      }
+      if (callee.name === 'defineComponent') {
+        // for Vue.js 3.x
+        // defineComponent({})
+        const isDestructedVueComponent = isObjectArgument(node)
+        return isDestructedVueComponent
+      }
+    }
+  }
+
+  return false
+
+  function isObjectArgument (node) {
+    return node.arguments.length > 0 &&
+        unwrapTypes(node.arguments.slice(-1)[0]).type === 'ObjectExpression'
+  }
+}
+
+/**
+ * Check whether given node is new Vue instance
+ * new Vue({})
+ * @param {ASTNode} node Node to check
+ * @returns {boolean}
+ */
+function isVueInstance (node) {
+  const callee = node.callee
+  return node.type === 'NewExpression' &&
+      callee.type === 'Identifier' &&
+      callee.name === 'Vue' &&
+      node.arguments.length &&
+      unwrapTypes(node.arguments[0]).type === 'ObjectExpression'
+}
+
+/**
+ * If the given object is a Vue component or instance, returns the Vue definition type.
+ * @param {RuleContext} context The ESLint rule context object.
+ * @param {ObjectExpression} node Node to check
+ * @returns { 'mark' | 'export' | 'definition' | 'instance' | null } The Vue definition type.
+ */
+function getVueObjectType (context, node) {
+  if (node.type !== 'ObjectExpression') {
+    return null
+  }
+  let parent = node.parent
+  while (parent && parent.type === 'TSAsExpression') {
+    parent = parent.parent
+  }
+  if (parent) {
+    if (parent.type === 'ExportDefaultDeclaration') {
+      // export default {} in .vue || .jsx
+      const filePath = context.getFilename()
+      if (isVueComponentFile(parent, filePath) && unwrapTypes(parent.declaration) === node) {
+        return 'export'
+      }
+    } else if (parent.type === 'CallExpression') {
+      // Vue.component('xxx', {}) || component('xxx', {})
+      if (isVueComponent(parent) && unwrapTypes(parent.arguments.slice(-1)[0]) === node) {
+        return 'definition'
+      }
+    } else if (parent.type === 'NewExpression') {
+      // new Vue({})
+      if (isVueInstance(parent) && unwrapTypes(parent.arguments[0]) === node) {
+        return 'instance'
+      }
+    }
+  }
+  if (getComponentComments(context).some(el => el.loc.end.line === node.loc.start.line - 1)) {
+    return 'mark'
+  }
+  return null
+}
+
+/**
+ * Gets the component comments of a given context.
+ * @param {RuleContext} context The ESLint rule context object.
+ * @return {Token[]} The the component comments.
+ */
+function getComponentComments (context) {
+  let tokens = componentComments.get(context)
+  if (tokens) {
+    return tokens
+  }
+  const sourceCode = context.getSourceCode()
+  tokens = sourceCode.getAllComments().filter(comment => /@vue\/component/g.test(comment.value))
+  componentComments.set(context, tokens)
+  return tokens
+}
+
+function compositingVisitors (...visitors) {
+  const visitor = {}
+  for (const v of visitors) {
+    for (const key in v) {
+      if (visitor[key]) {
+        const o = visitor[key]
+        visitor[key] = (node) => {
+          o(node)
+          v[key](node)
+        }
+      } else {
+        visitor[key] = v[key]
+      }
+    }
+  }
+  return visitor
+}


Test patch:
diff --git a/tests/lib/rules/no-setup-props-destructure.js b/tests/lib/rules/no-setup-props-destructure.js
index 2f3bccc5..e81d680f 100644
--- a/tests/lib/rules/no-setup-props-destructure.js
+++ b/tests/lib/rules/no-setup-props-destructure.js
@@ -135,6 +135,30 @@ tester.run('no-setup-props-destructure', rule, {
       }
       </script>
       `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      export default {
+        watch: {
+          setup({val}) { }
+        }
+      }
+      </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      export default {
+        setup(props) {
+          const props2 = props
+        }
+      }
+      </script>
+      `
     }
   ],
   invalid: [
@@ -330,6 +354,42 @@ tester.run('no-setup-props-destructure', rule, {
           line: 6
         }
       ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      export default {
+        setup(p) {
+          const {foo} = p.bar
+        }
+      }
+      </script>
+      `,
+      errors: [
+        {
+          messageId: 'getProperty',
+          line: 5
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      export default {
+        setup(p) {
+          foo.bar = p.bar
+        }
+      }
+      </script>
+      `,
+      errors: [
+        {
+          messageId: 'getProperty',
+          line: 5
+        }
+      ]
     }
   ]
 })
diff --git a/tests/lib/rules/no-unregistered-components.js b/tests/lib/rules/no-unregistered-components.js
new file mode 100644
index 00000000..2f7dd263
--- /dev/null
+++ b/tests/lib/rules/no-unregistered-components.js
@@ -0,0 +1,566 @@
+/**
+ * @fileoverview Report used components that are not registered
+ * @author Jesús Ángel González Novez
+ */
+'use strict'
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const RuleTester = require('eslint').RuleTester
+const rule = require('../../../lib/rules/no-unregistered-components')
+
+// ------------------------------------------------------------------------------
+// Tests
+// ------------------------------------------------------------------------------
+
+const tester = new RuleTester({
+  parser: require.resolve('vue-eslint-parser'),
+  parserOptions: {
+    ecmaVersion: 2018,
+    sourceType: 'module'
+  }
+})
+
+tester.run('no-unregistered-components', rule, {
+  valid: [
+    {
+      filename: 'test.vue',
+      code: `
+        <template>
+          <div>Lorem ipsum</div>
+        </template>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <template>
+          <CustomComponent />
+        </template>
+      `,
+      options: [
+        {
+          ignorePatterns: [
+            'custom(\\-\\w+)+'
+          ]
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <template>
+          <CustomComponent>
+            Some text
+          </CustomComponent>
+        </template>
+      `,
+      options: [
+        {
+          ignorePatterns: [
+            'custom(\\-\\w+)+'
+          ]
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <template>
+          <custom-component>
+            Some text
+          </custom-component>
+        </template>
+      `,
+      options: [
+        {
+          ignorePatterns: [
+            'custom(\\-\\w+)+'
+          ]
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <template>
+          <custom-component>
+            Some text
+          </custom-component>
+        </template>
+      `,
+      options: [
+        {
+          ignorePatterns: [
+            'Custom(\\w+)+'
+          ]
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <template>
+          <custom_component>
+            Some text
+          </custom_component>
+        </template>
+      `,
+      options: [
+        {
+          ignorePatterns: [
+            'Custom(\\w+)+'
+          ]
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <template>
+          <customComponent>
+            Some text
+          </customComponent>
+        </template>
+      `,
+      options: [
+        {
+          ignorePatterns: [
+            'Custom(\\w+)+'
+          ]
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <template>
+          <customComponent>
+            <warm-code>Text</warm-code>
+            <InfoBtnPrimary value="text" />
+          </customComponent>
+        </template>
+      `,
+      options: [
+        {
+          ignorePatterns: [
+            'Custom(\\w+)+',
+            'Warm(\\w+)+',
+            'InfoBtn(\\w+)+'
+          ]
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <template>
+          <CustomComponent />
+        </template>
+        <script>
+        export default {
+          components: {
+            CustomComponent
+          }
+        }
+        </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <template>
+          <CustomComponent>
+            Some text
+          </CustomComponent>
+        </template>
+        <script>
+        export default {
+          components: {
+            CustomComponent
+          }
+        }
+        </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <template>
+          <component :is="'CustomComponent'" />
+        </template>
+        <script>
+        export default {
+          components: {
+            CustomComponent
+          }
+        }
+        </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <template>
+          <component :is="'CustomComponent'">
+            Some text
+          </component>
+        </template>
+        <script>
+        export default {
+          components: {
+            CustomComponent
+          }
+        }
+        </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <template>
+          <component v-if="showComponent" :is="'CustomComponent'" />
+        </template>
+        <script>
+        export default {
+          data: () => ({
+            showComponent: true
+          }),
+          components: {
+            CustomComponent
+          }
+        }
+        </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <template>
+          <component v-if="showComponent" :is="'CustomComponent'" />
+        </template>
+        <script>
+        export default {
+          data: () => ({
+            showComponent: false
+          }),
+          components: {
+            CustomComponent
+          }
+        }
+        </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <template>
+          <component :is="'div'" />
+        </template>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <template>
+          <component is="div" />
+        </template>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <template>
+          <component is="span">
+            Text
+          </component>
+        </template>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <template>
+          <Component is="div" />
+        </template>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <template>
+          <Component is="span">
+            Text
+          </Component>
+        </template>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <template>
+          <component :is="name" />
+        </template>
+        <script>
+        export default {
+          data: () => ({ name: 'div' })
+        }
+        </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <template>
+          <component :is="name" />
+        </template>
+        <script>
+        export default {
+          data: () => ({ name: 'warm-button' }),
+          components: { WarmButton }
+        }
+        </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <template>
+          <teleport />
+          <suspense />
+          <suspense>
+            <div>Text</div>
+          </suspense>
+          <Teleport />
+          <Suspense />
+          <Suspense>
+            <div>Text</div>
+          </Suspense>
+        </template>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <template>
+          <transition />
+          <transition-group />
+          <keep-alive />
+          <transition>
+            <div>Text</div>
+          </transition>
+          <TransitionGroup />
+          <KeepAlive />
+          <Transition>
+            <div>Text</div>
+          </Transition>
+          <Slot />
+          <slot />
+          <slot>
+            foo
+          </slot>
+        </template>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <template>
+          <Custom-Component />
+        </template>
+        <script>
+        export default {
+          components: {
+            'custom-component': InfoPrimaryWrapper
+          }
+        }
+        </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <template>
+          <component is />
+        </template>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <template>
+          <Component is />
+        </template>
+      `
+    }
+  ],
+  invalid: [
+    {
+      filename: 'test.vue',
+      code: `
+        <template>
+          <CustomComponent />
+        </template>
+      `,
+      errors: [{
+        message: 'The "CustomComponent" component has been used but not registered.',
+        line: 3
+      }]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <template>
+          <WarmButton />
+        </template>
+      `,
+      options: [
+        {
+          ignorePatterns: [
+            'custom(\\-\\w+)+'
+          ]
+        }
+      ],
+      errors: [{
+        message: 'The "WarmButton" component has been used but not registered.',
+        line: 3
+      }]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <template>
+          <CustomComponent>
+            Some text
+          </CustomComponent>
+        </template>
+      `,
+      errors: [{
+        message: 'The "CustomComponent" component has been used but not registered.',
+        line: 3
+      }]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <template>
+          <WarmButton>
+            Some text
+          </WarmButton>
+        </template>
+      `,
+      options: [
+        {
+          ignorePatterns: [
+            'custom(\\-\\w+)+'
+          ]
+        }
+      ],
+      errors: [{
+        message: 'The "WarmButton" component has been used but not registered.',
+        line: 3
+      }]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <template>
+          <CustomComponent />
+        </template>
+        <script>
+        export default {
+          components: {
+            WarmButton
+          }
+        }
+        </script>
+      `,
+      errors: [{
+        message: 'The "CustomComponent" component has been used but not registered.',
+        line: 3
+      }]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <template>
+          <CustomComponent>
+            Some text
+          </CustomComponent>
+        </template>
+        <script>
+        export default {
+          components: {
+            WarmButton
+          }
+        }
+        </script>
+      `,
+      errors: [{
+        message: 'The "CustomComponent" component has been used but not registered.',
+        line: 3
+      }]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <template>
+          <component :is="'CustomComponent'" />
+        </template>
+        <script>
+        export default {
+          components: {
+            WarmButton
+          }
+        }
+        </script>
+      `,
+      errors: [{
+        message: 'The "CustomComponent" component has been used but not registered.',
+        line: 3
+      }]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <template>
+          <component :is="'CustomComponent'">
+            Some text
+          </component>
+        </template>
+        <script>
+        export default {
+          components: {
+            WarmButton
+          }
+        }
+        </script>
+      `,
+      errors: [{
+        message: 'The "CustomComponent" component has been used but not registered.',
+        line: 3
+      }]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <template>
+          <CustomComponent />
+        </template>
+        <script>
+        export default {
+          components: {
+            'custom-component': InfoPrimaryWrapper
+          }
+        }
+        </script>
+      `,
+      errors: [{
+        message: 'The "CustomComponent" component has been used but not registered.',
+        line: 3
+      }]
+    }
+  ]
+})
diff --git a/tests/lib/rules/require-explicit-emits.js b/tests/lib/rules/require-explicit-emits.js
new file mode 100644
index 00000000..0a084ab4
--- /dev/null
+++ b/tests/lib/rules/require-explicit-emits.js
@@ -0,0 +1,1433 @@
+/**
+ * @author Yosuke Ota
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+const RuleTester = require('eslint').RuleTester
+const rule = require('../../../lib/rules/require-explicit-emits')
+
+const tester = new RuleTester({
+  parser: require.resolve('vue-eslint-parser'),
+  parserOptions: {
+    ecmaVersion: 2019,
+    sourceType: 'module'
+  }
+})
+
+tester.run('require-explicit-emits', rule, {
+  valid: [
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div @click="$emit('welcome')"/>
+      </template>
+      <script>
+      export default {
+        emits: ['welcome']
+      }
+      </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div @click="$emit('welcome')"/>
+      </template>
+      <script>
+      export default {
+        emits: {welcome:null}
+      }
+      </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div @click="$emit('welcome')"/>
+      </template>
+      <script>
+      export default {
+        emits: {welcome(){}}
+      }
+      </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      export default {
+        emits: ['welcome'],
+        methods: {
+          onClick() {
+            this.$emit('welcome')
+          }
+        }
+      }
+      </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      export default {
+        emits: ['welcome'],
+        methods: {
+          onClick() {
+            const vm = this
+            vm.$emit('welcome')
+          }
+        }
+      }
+      </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      export default {
+        emits: ['welcome'],
+        setup(p, context) {
+          context.emit('welcome')
+        }
+      }
+      </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      export default {
+        emits: ['welcome'],
+        setup(p, {emit}) {
+          emit('welcome')
+        }
+      }
+      </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      export default {
+        emits: ['welcome'],
+        setup(props, context) {
+          context.emit = 'foo'
+          context='foo'
+        }
+      }
+      </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      export default {
+        emits: ['welcome'],
+        setup(props, {emit}) {
+          emit='foo'
+        }
+      }
+      </script>
+      `
+    },
+    // unknown
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div @click="emit('foo')"/>
+      </template>
+      <script>
+      export default {
+        emits: ['welcome']
+      }
+      </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div @click="$emit('foo')"/>
+      </template>
+      <script>
+      </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      export default {
+        emits: ['welcome'],
+        methods: {
+          onClick() {
+            $emit('foo')
+          }
+        }
+      }
+      </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      export default {
+        emits: ['welcome'],
+        methods: {
+          onClick() {
+            this.bar.$emit('foo')
+          }
+        }
+      }
+      </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      export default {
+        emits: ['welcome'],
+        setup(p, ...context) {
+          context.emit('foo')
+        }
+      }
+      </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      export default {
+        emits: ['welcome'],
+        setup(p, [emit]) {
+          emit('foo')
+        }
+      }
+      </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      export default {
+        emits: ['welcome'],
+        setup(p, {$emit}) {
+          $emit('foo')
+        }
+      }
+      </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      export default {
+        emits: ['welcome'],
+        setup(context) {
+          context.emit('foo')
+        }
+      }
+      </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div @click="$emit(foo)"/>
+        <div @click="$emit()"/>
+        <div @click="$emit(1)"/>
+        <div @click="$emit(true)"/>
+        <div @click="$emit(null)"/>
+        <div @click="$emit(/regex/)"/>
+      </template>
+      <script>
+      export default {
+        emits: ['welcome']
+      }
+      </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      export default {
+        emits: ['welcome'],
+        setup(p, context) {
+          context.emit(foo)
+          context.emit()
+          context.emit(1)
+          context.emit(true)
+          context.emit(null)
+          context.emit(/regex/)
+        }
+      }
+      </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      export default {
+        emits: ['welcome'],
+        methods: {
+          onClick() {
+            this.$emit(foo)
+            this.$emit()
+            this.$emit(1)
+            this.$emit(true)
+            this.$emit(null)
+            this.$emit(/regex/)
+          }
+        }
+      }
+      </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      export default {
+        emits: ['welcome'],
+        setup(p, context) {
+          context.fire('foo')
+        }
+      }
+      </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      export default {
+        emits: ['welcome'],
+        methods: {
+          onClick() {
+            this.$fire('foo')
+          }
+        }
+      }
+      </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      function fn() {
+        this.$emit('foo')
+      }
+      export default {
+        emits: ['welcome'],
+        methods: {
+          onClick() {
+          }
+        }
+      }
+      </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      export default {
+        emits: ['welcome'],
+        setup: ((p, context) => {
+          context.emit('foo')
+        })()
+      }
+      </script>
+      `
+    }
+  ],
+  invalid: [
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div @click="$emit('foo')"/>
+      </template>
+      <script>
+      export default {
+      }
+      </script>
+      `,
+      errors: [
+        {
+          line: 3,
+          column: 28,
+          messageId: 'missing',
+          endLine: 3,
+          endColumn: 33,
+          suggestions: [
+            {
+              desc: 'Add the `emits` option with array syntax and define "foo" event.',
+              output: `
+      <template>
+        <div @click="$emit('foo')"/>
+      </template>
+      <script>
+      export default {
+emits: ['foo']
+      }
+      </script>
+      `
+            },
+            {
+              desc: 'Add the `emits` option with object syntax and define "foo" event.',
+              output: `
+      <template>
+        <div @click="$emit('foo')"/>
+      </template>
+      <script>
+      export default {
+emits: {'foo': null}
+      }
+      </script>
+      `
+            }
+          ]
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div @click="$emit('foo')"/>
+      </template>
+      <script>
+      export default {
+        emits: ['welcome'],
+      }
+      </script>
+      `,
+      errors: [
+        {
+          line: 3,
+          column: 28,
+          messageId: 'missing',
+          endLine: 3,
+          endColumn: 33,
+          suggestions: [
+            {
+              desc: 'Add the "foo" to `emits` option.',
+              output: `
+      <template>
+        <div @click="$emit('foo')"/>
+      </template>
+      <script>
+      export default {
+        emits: ['welcome', 'foo'],
+      }
+      </script>
+      `
+            }
+          ]
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div @click="$emit('foo')"/>
+      </template>
+      <script>
+      export default {
+        emits: {welcome:null}
+      }
+      </script>
+      `,
+      errors: [
+        {
+          line: 3,
+          column: 28,
+          messageId: 'missing',
+          endLine: 3,
+          endColumn: 33,
+          suggestions: [
+            {
+              desc: 'Add the "foo" to `emits` option.',
+              output: `
+      <template>
+        <div @click="$emit('foo')"/>
+      </template>
+      <script>
+      export default {
+        emits: {welcome:null, 'foo': null}
+      }
+      </script>
+      `
+            }
+          ]
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div @click="$emit('foo')"/>
+      </template>
+      <script>
+      export default {
+        emits: {welcome(){}}
+      }
+      </script>
+      `,
+      errors: [
+        {
+          line: 3,
+          column: 28,
+          messageId: 'missing',
+          endLine: 3,
+          endColumn: 33,
+          suggestions: [
+            {
+              desc: 'Add the "foo" to `emits` option.',
+              output: `
+      <template>
+        <div @click="$emit('foo')"/>
+      </template>
+      <script>
+      export default {
+        emits: {welcome(){}, 'foo': null}
+      }
+      </script>
+      `
+            }
+          ]
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      export default {
+        emits: ['welcome'],
+        methods: {
+          onClick() {
+            this.$emit('foo')
+          }
+        }
+      }
+      </script>
+      `,
+      errors: [
+        {
+          line: 7,
+          column: 24,
+          messageId: 'missing',
+          endLine: 7,
+          endColumn: 29,
+          suggestions: [
+            {
+              desc: 'Add the "foo" to `emits` option.',
+              output: `
+      <script>
+      export default {
+        emits: ['welcome', 'foo'],
+        methods: {
+          onClick() {
+            this.$emit('foo')
+          }
+        }
+      }
+      </script>
+      `
+            }
+          ]
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      export default {
+        emits: ['welcome'],
+        methods: {
+          onClick() {
+            const vm = this
+            vm.$emit('foo')
+          }
+        }
+      }
+      </script>
+      `,
+      errors: [
+        {
+          line: 8,
+          column: 22,
+          messageId: 'missing',
+          endLine: 8,
+          endColumn: 27,
+          suggestions: [
+            {
+              desc: 'Add the "foo" to `emits` option.',
+              output: `
+      <script>
+      export default {
+        emits: ['welcome', 'foo'],
+        methods: {
+          onClick() {
+            const vm = this
+            vm.$emit('foo')
+          }
+        }
+      }
+      </script>
+      `
+            }
+          ]
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      export default {
+        emits: ['welcome'],
+        setup(p, context) {
+          context.emit('foo')
+        }
+      }
+      </script>
+      `,
+      errors: [
+        {
+          line: 6,
+          column: 24,
+          messageId: 'missing',
+          endLine: 6,
+          endColumn: 29,
+          suggestions: [
+            {
+              desc: 'Add the "foo" to `emits` option.',
+              output: `
+      <script>
+      export default {
+        emits: ['welcome', 'foo'],
+        setup(p, context) {
+          context.emit('foo')
+        }
+      }
+      </script>
+      `
+            }
+          ]
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      export default {
+        emits: ['welcome'],
+        setup(p, {emit}) {
+          emit('foo')
+        }
+      }
+      </script>
+      `,
+      errors: [
+        {
+          line: 6,
+          column: 16,
+          messageId: 'missing',
+          endLine: 6,
+          endColumn: 21,
+          suggestions: [
+            {
+              desc: 'Add the "foo" to `emits` option.',
+              output: `
+      <script>
+      export default {
+        emits: ['welcome', 'foo'],
+        setup(p, {emit}) {
+          emit('foo')
+        }
+      }
+      </script>
+      `
+            }
+          ]
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      export default {
+        emits: ['welcome'],
+        setup(p, {emit:fire}) {
+          fire('foo')
+          fire('bar')
+        }
+      }
+      </script>
+      `,
+      errors: [
+        {
+          line: 6,
+          column: 16,
+          messageId: 'missing',
+          endLine: 6,
+          endColumn: 21,
+          suggestions: [
+            {
+              desc: 'Add the "foo" to `emits` option.',
+              output: `
+      <script>
+      export default {
+        emits: ['welcome', 'foo'],
+        setup(p, {emit:fire}) {
+          fire('foo')
+          fire('bar')
+        }
+      }
+      </script>
+      `
+            }
+          ]
+        },
+        {
+          line: 7,
+          column: 16,
+          messageId: 'missing',
+          endLine: 7,
+          endColumn: 21,
+          suggestions: [
+            {
+              desc: 'Add the "bar" to `emits` option.',
+              output: `
+      <script>
+      export default {
+        emits: ['welcome', 'bar'],
+        setup(p, {emit:fire}) {
+          fire('foo')
+          fire('bar')
+        }
+      }
+      </script>
+      `
+            }
+          ]
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div @click="$emit('foo')"/>
+      </template>
+      <script>
+      // @vue/component
+      const Foo = {}
+      export default Foo
+      </script>
+      `,
+      errors: [
+        {
+          message: 'The "foo" event has been triggered but not declared on `emits` option.',
+          suggestions: [
+            {
+              desc: 'Add the `emits` option with array syntax and define "foo" event.',
+              output: `
+      <template>
+        <div @click="$emit('foo')"/>
+      </template>
+      <script>
+      // @vue/component
+      const Foo = {
+emits: ['foo']
+}
+      export default Foo
+      </script>
+      `
+            },
+            {
+              desc: 'Add the `emits` option with object syntax and define "foo" event.',
+              output: `
+      <template>
+        <div @click="$emit('foo')"/>
+      </template>
+      <script>
+      // @vue/component
+      const Foo = {
+emits: {'foo': null}
+}
+      export default Foo
+      </script>
+      `
+            }
+          ]
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div @click="$emit('foo')"/>
+      </template>
+      <script>
+      // @vue/component
+      const Foo = {emits:{}}
+      export default {
+        emits: {}
+      }
+      </script>
+      `,
+      errors: [
+        {
+          message: 'The "foo" event has been triggered but not declared on `emits` option.',
+          suggestions: [
+            {
+              desc: 'Add the "foo" to `emits` option.',
+              output: `
+      <template>
+        <div @click="$emit('foo')"/>
+      </template>
+      <script>
+      // @vue/component
+      const Foo = {emits:{}}
+      export default {
+        emits: {'foo': null}
+      }
+      </script>
+      `
+            }
+          ]
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div @click="$emit('foo')"/>
+      </template>
+      <script>
+      // @vue/component
+      const Foo = {emits: {}}
+      export default { emits: {} }
+      // @vue/component
+      const Bar = {emits: {}}
+      </script>
+      `,
+      errors: [
+        {
+          line: 3,
+          column: 28,
+          messageId: 'missing',
+          endLine: 3,
+          endColumn: 33,
+          suggestions: [
+            {
+              desc: 'Add the "foo" to `emits` option.',
+              output: `
+      <template>
+        <div @click="$emit('foo')"/>
+      </template>
+      <script>
+      // @vue/component
+      const Foo = {emits: {}}
+      export default { emits: {'foo': null} }
+      // @vue/component
+      const Bar = {emits: {}}
+      </script>
+      `
+            }
+          ]
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div @click="$emit('foo')"/>
+      </template>
+      <script>
+      export default {
+        components: {
+          // @vue/component
+          Foo: {
+            emits: ['foo'],
+            setup(p, {emit}) {
+              emit('bar')
+            }
+          }
+        },
+        emits: ['bar'],
+        setup(p, context) {
+          context.emit('foo')
+        }
+      }
+      </script>
+      `,
+      errors: [
+        {
+          message: 'The "foo" event has been triggered but not declared on `emits` option.',
+          suggestions: [
+            {
+              desc: 'Add the "foo" to `emits` option.',
+              output: `
+      <template>
+        <div @click="$emit('foo')"/>
+      </template>
+      <script>
+      export default {
+        components: {
+          // @vue/component
+          Foo: {
+            emits: ['foo'],
+            setup(p, {emit}) {
+              emit('bar')
+            }
+          }
+        },
+        emits: ['bar', 'foo'],
+        setup(p, context) {
+          context.emit('foo')
+        }
+      }
+      </script>
+      `
+            }
+          ]
+        },
+        {
+          message: 'The "bar" event has been triggered but not declared on `emits` option.',
+          suggestions: [
+            {
+              desc: 'Add the "bar" to `emits` option.',
+              output: `
+      <template>
+        <div @click="$emit('foo')"/>
+      </template>
+      <script>
+      export default {
+        components: {
+          // @vue/component
+          Foo: {
+            emits: ['foo', 'bar'],
+            setup(p, {emit}) {
+              emit('bar')
+            }
+          }
+        },
+        emits: ['bar'],
+        setup(p, context) {
+          context.emit('foo')
+        }
+      }
+      </script>
+      `
+            }
+          ]
+        },
+        {
+          message: 'The "foo" event has been triggered but not declared on `emits` option.',
+          suggestions: [
+            {
+              desc: 'Add the "foo" to `emits` option.',
+              output: `
+      <template>
+        <div @click="$emit('foo')"/>
+      </template>
+      <script>
+      export default {
+        components: {
+          // @vue/component
+          Foo: {
+            emits: ['foo'],
+            setup(p, {emit}) {
+              emit('bar')
+            }
+          }
+        },
+        emits: ['bar', 'foo'],
+        setup(p, context) {
+          context.emit('foo')
+        }
+      }
+      </script>
+      `
+            }
+          ]
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div @click="$emit('foo')"/>
+      </template>
+      <script>
+      export default {
+        components: {
+          // @vue/component
+          Foo: {
+            emits: ['foo'],
+            methods: {
+              onClick() {
+                this.$emit('bar')
+              }
+            }
+          }
+        },
+        emits: ['bar'],
+        methods: {
+          onClick() {
+            this.$emit('foo')
+          }
+        }
+      }
+      </script>
+      `,
+      errors: [
+        {
+          message: 'The "foo" event has been triggered but not declared on `emits` option.',
+          suggestions: [
+            {
+              desc: 'Add the "foo" to `emits` option.',
+              output: `
+      <template>
+        <div @click="$emit('foo')"/>
+      </template>
+      <script>
+      export default {
+        components: {
+          // @vue/component
+          Foo: {
+            emits: ['foo'],
+            methods: {
+              onClick() {
+                this.$emit('bar')
+              }
+            }
+          }
+        },
+        emits: ['bar', 'foo'],
+        methods: {
+          onClick() {
+            this.$emit('foo')
+          }
+        }
+      }
+      </script>
+      `
+            }
+          ]
+        },
+        {
+          message: 'The "bar" event has been triggered but not declared on `emits` option.',
+          suggestions: [
+            {
+              desc: 'Add the "bar" to `emits` option.',
+              output: `
+      <template>
+        <div @click="$emit('foo')"/>
+      </template>
+      <script>
+      export default {
+        components: {
+          // @vue/component
+          Foo: {
+            emits: ['foo', 'bar'],
+            methods: {
+              onClick() {
+                this.$emit('bar')
+              }
+            }
+          }
+        },
+        emits: ['bar'],
+        methods: {
+          onClick() {
+            this.$emit('foo')
+          }
+        }
+      }
+      </script>
+      `
+            }
+          ]
+        },
+        {
+          message: 'The "foo" event has been triggered but not declared on `emits` option.',
+          suggestions: [
+            {
+              desc: 'Add the "foo" to `emits` option.',
+              output: `
+      <template>
+        <div @click="$emit('foo')"/>
+      </template>
+      <script>
+      export default {
+        components: {
+          // @vue/component
+          Foo: {
+            emits: ['foo'],
+            methods: {
+              onClick() {
+                this.$emit('bar')
+              }
+            }
+          }
+        },
+        emits: ['bar', 'foo'],
+        methods: {
+          onClick() {
+            this.$emit('foo')
+          }
+        }
+      }
+      </script>
+      `
+            }
+          ]
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div @click="$emit('foo')"/>
+      </template>
+      <script>
+      export default {
+        emits: {...foo}
+      }
+      </script>
+      `,
+      errors: [
+        {
+          message: 'The "foo" event has been triggered but not declared on `emits` option.',
+          suggestions: [
+            {
+              desc: 'Add the "foo" to `emits` option.',
+              output: `
+      <template>
+        <div @click="$emit('foo')"/>
+      </template>
+      <script>
+      export default {
+        emits: {'foo': null,...foo}
+      }
+      </script>
+      `
+            }
+          ]
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div @click="$emit('foo')"/>
+      </template>
+      <script>
+      export default {
+        emits: []
+      }
+      </script>
+      `,
+      errors: [
+        {
+          message: 'The "foo" event has been triggered but not declared on `emits` option.',
+          suggestions: [
+            {
+              desc: 'Add the "foo" to `emits` option.',
+              output: `
+      <template>
+        <div @click="$emit('foo')"/>
+      </template>
+      <script>
+      export default {
+        emits: ['foo']
+      }
+      </script>
+      `
+            }
+          ]
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div @click="$emit('foo')"/>
+      </template>
+      <script>
+      export default {
+        emits: [...foo]
+      }
+      </script>
+      `,
+      errors: [
+        {
+          message: 'The "foo" event has been triggered but not declared on `emits` option.',
+          suggestions: [
+            {
+              desc: 'Add the "foo" to `emits` option.',
+              output: `
+      <template>
+        <div @click="$emit('foo')"/>
+      </template>
+      <script>
+      export default {
+        emits: ['foo',...foo]
+      }
+      </script>
+      `
+            }
+          ]
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div @click="$emit('foo')"/>
+      </template>
+      <script>
+      export default {
+        emits: foo
+      }
+      </script>
+      `,
+      errors: [
+        {
+          message: 'The "foo" event has been triggered but not declared on `emits` option.',
+          suggestions: []
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div @click="$emit('foo')"/>
+      </template>
+      <script>
+      export default {
+        name: '',
+        props: {}
+      }
+      </script>
+      `,
+      errors: [
+        {
+          message: 'The "foo" event has been triggered but not declared on `emits` option.',
+          suggestions: [
+            {
+              desc: 'Add the `emits` option with array syntax and define "foo" event.',
+              output: `
+      <template>
+        <div @click="$emit('foo')"/>
+      </template>
+      <script>
+      export default {
+        name: '',
+emits: ['foo'],
+        props: {}
+      }
+      </script>
+      `
+            }, {
+              desc: 'Add the `emits` option with object syntax and define "foo" event.',
+              output: `
+      <template>
+        <div @click="$emit('foo')"/>
+      </template>
+      <script>
+      export default {
+        name: '',
+emits: {'foo': null},
+        props: {}
+      }
+      </script>
+      `
+            }
+          ]
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div @click="$emit('foo')"/>
+      </template>
+      <script>
+      export default {
+        name: '',
+      }
+      </script>
+      `,
+      errors: [
+        {
+          message: 'The "foo" event has been triggered but not declared on `emits` option.',
+          suggestions: [
+            {
+              desc: 'Add the `emits` option with array syntax and define "foo" event.',
+              output: `
+      <template>
+        <div @click="$emit('foo')"/>
+      </template>
+      <script>
+      export default {
+        name: '',
+emits: ['foo'],
+      }
+      </script>
+      `
+            }, {
+              desc: 'Add the `emits` option with object syntax and define "foo" event.',
+              output: `
+      <template>
+        <div @click="$emit('foo')"/>
+      </template>
+      <script>
+      export default {
+        name: '',
+emits: {'foo': null},
+      }
+      </script>
+      `
+            }
+          ]
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div @click="$emit('foo')"/>
+      </template>
+      <script>
+      export default {
+        name: ''
+      }
+      </script>
+      `,
+      errors: [
+        {
+          message: 'The "foo" event has been triggered but not declared on `emits` option.',
+          suggestions: [
+            {
+              desc: 'Add the `emits` option with array syntax and define "foo" event.',
+              output: `
+      <template>
+        <div @click="$emit('foo')"/>
+      </template>
+      <script>
+      export default {
+        name: '',
+emits: ['foo']
+      }
+      </script>
+      `
+            }, {
+              desc: 'Add the `emits` option with object syntax and define "foo" event.',
+              output: `
+      <template>
+        <div @click="$emit('foo')"/>
+      </template>
+      <script>
+      export default {
+        name: '',
+emits: {'foo': null}
+      }
+      </script>
+      `
+            }
+          ]
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div @click="$emit('foo')"/>
+      </template>
+      <script>
+      export default {
+        ...foo
+      }
+      </script>
+      `,
+      errors: [
+        {
+          message: 'The "foo" event has been triggered but not declared on `emits` option.',
+          suggestions: [
+            {
+              desc: 'Add the `emits` option with array syntax and define "foo" event.',
+              output: `
+      <template>
+        <div @click="$emit('foo')"/>
+      </template>
+      <script>
+      export default {
+        ...foo,
+emits: ['foo']
+      }
+      </script>
+      `
+            }, {
+              desc: 'Add the `emits` option with object syntax and define "foo" event.',
+              output: `
+      <template>
+        <div @click="$emit('foo')"/>
+      </template>
+      <script>
+      export default {
+        ...foo,
+emits: {'foo': null}
+      }
+      </script>
+      `
+            }
+          ]
+        }
+      ]
+    }
+  ]
+})