diff --git a/docs/rules/README.md b/docs/rules/README.md
index df3441d3e..3577668f4 100644
--- a/docs/rules/README.md
+++ b/docs/rules/README.md
@@ -332,6 +332,7 @@ For example:
| [vue/v-for-delimiter-style](./v-for-delimiter-style.md) | enforce `v-for` directive's delimiter style | :wrench: |
| [vue/v-on-event-hyphenation](./v-on-event-hyphenation.md) | enforce v-on event naming style on custom components in template | :wrench: |
| [vue/v-on-function-call](./v-on-function-call.md) | enforce or forbid parentheses after method calls without arguments in `v-on` directives | :wrench: |
+| [vue/valid-define-emits](./valid-define-emits.md) | enforce valid `defineEmits` compiler macro | |
| [vue/valid-define-props](./valid-define-props.md) | enforce valid `defineProps` compiler macro | |
| [vue/valid-next-tick](./valid-next-tick.md) | enforce valid `nextTick` function calls | :wrench: |
diff --git a/docs/rules/valid-define-emits.md b/docs/rules/valid-define-emits.md
new file mode 100644
index 000000000..cf6ea6c15
--- /dev/null
+++ b/docs/rules/valid-define-emits.md
@@ -0,0 +1,133 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/valid-define-emits
+description: enforce valid `defineEmits` compiler macro
+---
+# vue/valid-define-emits
+
+> enforce valid `defineEmits` compiler macro
+
+- :exclamation: ***This rule has not been released yet.***
+
+This rule checks whether `defineEmits` compiler macro is valid.
+
+## :book: Rule Details
+
+This rule reports `defineEmits` compiler macros in the following cases:
+
+- `defineEmits` are referencing locally declared variables.
+- `defineEmits` has both a literal type and an argument. e.g. `defineEmits<(e: 'foo')=>void>(['bar'])`
+- `defineEmits` has been called multiple times.
+- Custom events are defined in both `defineEmits` and `export default {}`.
+- Custom events are not defined in either `defineEmits` or `export default {}`.
+
+
+
+```vue
+
+```
+
+
+
+
+
+```vue
+
+```
+
+
+
+```vue
+
+```
+
+
+
+```vue
+
+
+```
+
+
+
+
+
+```vue
+
+```
+
+
+
+```vue
+
+```
+
+
+
+```vue
+
+```
+
+
+
+
+
+```vue
+
+
+```
+
+
+
+
+
+```vue
+
+```
+
+
+
+## :wrench: Options
+
+Nothing.
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/valid-define-emits.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/valid-define-emits.js)
diff --git a/lib/index.js b/lib/index.js
index c321450f1..8bf6cad45 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -172,6 +172,7 @@ module.exports = {
'v-on-function-call': require('./rules/v-on-function-call'),
'v-on-style': require('./rules/v-on-style'),
'v-slot-style': require('./rules/v-slot-style'),
+ 'valid-define-emits': require('./rules/valid-define-emits'),
'valid-define-props': require('./rules/valid-define-props'),
'valid-next-tick': require('./rules/valid-next-tick'),
'valid-template-root': require('./rules/valid-template-root'),
diff --git a/lib/rules/valid-define-emits.js b/lib/rules/valid-define-emits.js
new file mode 100644
index 000000000..6b79e83cc
--- /dev/null
+++ b/lib/rules/valid-define-emits.js
@@ -0,0 +1,144 @@
+/**
+ * @author Yosuke Ota
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+const { findVariable } = require('eslint-utils')
+const utils = require('../utils')
+
+module.exports = {
+ meta: {
+ type: 'problem',
+ docs: {
+ description: 'enforce valid `defineEmits` compiler macro',
+ // TODO Switch in the major version.
+ // categories: ['vue3-essential'],
+ categories: undefined,
+ url: 'https://eslint.vuejs.org/rules/valid-define-emits.html'
+ },
+ fixable: null,
+ schema: [],
+ messages: {
+ hasTypeAndArg: '`defineEmits` has both a type-only emit and an argument.',
+ referencingLocally:
+ '`defineEmits` are referencing locally declared variables.',
+ multiple: '`defineEmits` has been called multiple times.',
+ notDefined: 'Custom events are not defined.',
+ definedInBoth:
+ 'Custom events are defined in both `defineEmits` and `export default {}`.'
+ }
+ },
+ /** @param {RuleContext} context */
+ create(context) {
+ const scriptSetup = utils.getScriptSetupElement(context)
+ if (!scriptSetup) {
+ return {}
+ }
+
+ /** @type {Set} */
+ const emitsDefExpressions = new Set()
+ let hasDefaultExport = false
+ /** @type {CallExpression[]} */
+ const defineEmitsNodes = []
+ /** @type {CallExpression | null} */
+ let emptyDefineEmits = null
+
+ return utils.compositingVisitors(
+ utils.defineScriptSetupVisitor(context, {
+ onDefineEmitsEnter(node) {
+ defineEmitsNodes.push(node)
+
+ if (node.arguments.length >= 1) {
+ if (node.typeParameters && node.typeParameters.params.length >= 1) {
+ // `defineEmits` has both a literal type and an argument.
+ context.report({
+ node,
+ messageId: 'hasTypeAndArg'
+ })
+ return
+ }
+
+ emitsDefExpressions.add(node.arguments[0])
+ } else {
+ if (
+ !node.typeParameters ||
+ node.typeParameters.params.length === 0
+ ) {
+ emptyDefineEmits = node
+ }
+ }
+ },
+ Identifier(node) {
+ for (const def of emitsDefExpressions) {
+ if (utils.inRange(def.range, node)) {
+ const variable = findVariable(context.getScope(), node)
+ if (
+ variable &&
+ variable.references.some((ref) => ref.identifier === node)
+ ) {
+ if (
+ variable.defs.length &&
+ variable.defs.every((def) =>
+ utils.inRange(scriptSetup.range, def.name)
+ )
+ ) {
+ //`defineEmits` are referencing locally declared variables.
+ context.report({
+ node,
+ messageId: 'referencingLocally'
+ })
+ }
+ }
+ }
+ }
+ }
+ }),
+ utils.defineVueVisitor(context, {
+ onVueObjectEnter(node, { type }) {
+ if (type !== 'export' || utils.inRange(scriptSetup.range, node)) {
+ return
+ }
+
+ hasDefaultExport = Boolean(utils.findProperty(node, 'emits'))
+ }
+ }),
+ {
+ 'Program:exit'() {
+ if (!defineEmitsNodes.length) {
+ return
+ }
+ if (defineEmitsNodes.length > 1) {
+ // `defineEmits` has been called multiple times.
+ for (const node of defineEmitsNodes) {
+ context.report({
+ node,
+ messageId: 'multiple'
+ })
+ }
+ return
+ }
+ if (emptyDefineEmits) {
+ if (!hasDefaultExport) {
+ // Custom events are not defined.
+ context.report({
+ node: emptyDefineEmits,
+ messageId: 'notDefined'
+ })
+ }
+ } else {
+ if (hasDefaultExport) {
+ // Custom events are defined in both `defineEmits` and `export default {}`.
+ for (const node of defineEmitsNodes) {
+ context.report({
+ node,
+ messageId: 'definedInBoth'
+ })
+ }
+ }
+ }
+ }
+ }
+ )
+ }
+}
diff --git a/tests/lib/rules/valid-define-emits.js b/tests/lib/rules/valid-define-emits.js
new file mode 100644
index 000000000..309375e94
--- /dev/null
+++ b/tests/lib/rules/valid-define-emits.js
@@ -0,0 +1,156 @@
+/**
+ * @author Yosuke Ota
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const RuleTester = require('eslint').RuleTester
+const rule = require('../../../lib/rules/valid-define-emits')
+
+// ------------------------------------------------------------------------------
+// Tests
+// ------------------------------------------------------------------------------
+
+const tester = new RuleTester({
+ parser: require.resolve('vue-eslint-parser'),
+ parserOptions: { ecmaVersion: 2015, sourceType: 'module' }
+})
+
+tester.run('valid-define-emits', rule, {
+ valid: [
+ {
+ filename: 'test.vue',
+ code: `
+
+ `
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ parserOptions: { parser: require.resolve('@typescript-eslint/parser') }
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+
+ `
+ }
+ ],
+ invalid: [
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ errors: [
+ {
+ message: '`defineEmits` are referencing locally declared variables.',
+ line: 5
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ parserOptions: { parser: require.resolve('@typescript-eslint/parser') },
+ errors: [
+ {
+ message: '`defineEmits` has both a type-only emit and an argument.',
+ line: 4
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ errors: [
+ {
+ message: '`defineEmits` has been called multiple times.',
+ line: 4
+ },
+ {
+ message: '`defineEmits` has been called multiple times.',
+ line: 5
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+
+ `,
+ errors: [
+ {
+ message:
+ 'Custom events are defined in both `defineEmits` and `export default {}`.',
+ line: 9
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ errors: [
+ {
+ message: 'Custom events are not defined.',
+ line: 4
+ }
+ ]
+ }
+ ]
+})