Repository: vuejs/eslint-plugin-vue (js)

Issue:
Enable the autofix of `vue/v-on-event-hyphenation` in `vue/vue3-recommended`
<!--
  ❗Please don't ignore this template.❗

  If you ignore it, we're just going to respond asking you to fill it out, which wastes everyone's time.
  The more relevant information you can include, the faster we can find the issue and fix it without asking you for more info.
-->

<!--
  Before posting the issue, please confirm that the problem you're getting
  is not related with your code editor configuration.
  To make sure it's not, run: yarn eslint src/your-file.vue
-->

**Checklist**

- [x] I have tried restarting my IDE and the issue persists.
- [x] I have read the [FAQ](https://eslint.vuejs.org/user-guide/#faq) and my problem is not listed.
<!-- If you do not read the FAQ and open an issue that is listed in the FAQ, we may silently close the issue. -->

**Tell us about your environment**

- **ESLint version:** 8.30.0
- **eslint-plugin-vue version:** 9.8.0
- **Node version:** 19.2.0
- **Operating System:** Windows 11

**Please show your full configuration:**
<!-- Paste content of your .eslintrc file -->
```json5

module.exports = {
	root: true,
	env: {
		'node': false,
	},
	parser: 'vue-eslint-parser',
	parserOptions: {
		'parser': '@typescript-eslint/parser',
		tsconfigRootDir: __dirname,
		project: ['./tsconfig.json'],
		extraFileExtensions: ['.vue'],
	},
	extends: [
		'../shared/.eslintrc.js',
		'plugin:vue/vue3-recommended',
	],
	rules: {
		'@typescript-eslint/no-empty-interface': [
			'error',
			{
				'allowSingleExtends': true,
			},
		],
		'@typescript-eslint/prefer-nullish-coalescing': [
			'error',
		],
		// window の禁止理由: グローバルスコープと衝突し、予期せぬ結果を招くため
		// e の禁止理由: error や event など、複数のキーワードの頭文字であり分かりにくいため
		'id-denylist': ['error', 'window', 'e'],
		'no-shadow': ['warn'],
		'vue/attributes-order': ['error', {
			'alphabetical': false,
		}],
		'vue/no-use-v-if-with-v-for': ['error', {
			'allowUsingIterationVar': false,
		}],
		'vue/no-ref-as-operand': 'error',
		'vue/no-multi-spaces': ['error', {
			'ignoreProperties': false,
		}],
		'vue/no-v-html': 'warn',
		'vue/order-in-components': 'error',
		'vue/html-indent': ['warn', 'tab', {
			'attribute': 1,
			'baseIndent': 0,
			'closeBracket': 0,
			'alignAttributesVertically': true,
			'ignores': [],
		}],
		'vue/html-closing-bracket-spacing': ['warn', {
			'startTag': 'never',
			'endTag': 'never',
			'selfClosingTag': 'never',
		}],
		'vue/multi-word-component-names': 'warn',
		'vue/require-v-for-key': 'warn',
		'vue/no-unused-components': 'warn',
		'vue/valid-v-for': 'warn',
		'vue/return-in-computed-property': 'warn',
		'vue/no-setup-props-destructure': 'warn',
		'vue/max-attributes-per-line': 'off',
		'vue/html-self-closing': 'off',
		'vue/singleline-html-element-content-newline': 'off',
	},
	globals: {
		// Node.js
		'module': false,
		'require': false,
		'__dirname': false,

		// Vue
		'$$': false,
		'$ref': false,
		'$shallowRef': false,
		'$computed': false,

		// Misskey
		'_DEV_': false,
		'_LANGS_': false,
		'_VERSION_': false,
		'_ENV_': false,
		'_PERF_PREFIX_': false,
		'_DATA_TRANSFER_DRIVE_FILE_': false,
		'_DATA_TRANSFER_DRIVE_FOLDER_': false,
		'_DATA_TRANSFER_DECK_COLUMN_': false,
	},
};
```

**What did you do?**	
<!-- Please include the actual source code causing the issue. -->
```vue
<FormSwitch v-model="$i.injectFeaturedNote" class="_formBlock" @update:model-value="onChangeInjectFeaturedNote">
```

**What did you expect to happen?**

An autofixable warning

**What actually happened?**
<!--
  Please include the actual, raw output from ESLint.
  If you are only looking at the results of your editor extension, also check the CLI results.
-->

Non-autofixable warning

**Repository to reproduce this issue**
<!--
  ❗ Please share a repository that can reproduce your issue.
  ❗ If you don't share it, we just label it as requesting a repository that can reproduce your issue.
  ❗ If you don't share it for a few days, we silently close this issue.
-->

https://github.com/misskey-dev/misskey/tree/f1fd1d2585bd5230caea4c7b8814c2cf007e57cc

Docs imply the autofix is disabled by default for Vue 2 compatibility, but `vue/vue3-recommended` has no reason to keep it disabled as it's for Vue 3. 

https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/configs/vue3-strongly-recommended.js is autogenerated with no options, it should somehow pass an option.

Pull request description:
Make `vue/v-on-event-hyphenation` autofixable by default in Vue 3
Fixes #2065.

Patch:
diff --git a/docs/rules/define-emits-declaration.md b/docs/rules/define-emits-declaration.md
index 2fd4b478..7a11524c 100644
--- a/docs/rules/define-emits-declaration.md
+++ b/docs/rules/define-emits-declaration.md
@@ -5,13 +5,15 @@ title: vue/define-emits-declaration
 description: enforce declaration style of `defineEmits`
 since: v9.5.0
 ---
+
 # vue/define-emits-declaration
 
 > enforce declaration style of `defineEmits`
 
 ## :book: Rule Details
 
-This rule enforces `defineEmits` typing style which you should use `type-based` or `runtime` declaration.
+This rule enforces `defineEmits` typing style which you should use `type-based`, strict `type-literal`
+(introduced in Vue 3.3), or `runtime` declaration.
 
 This rule only works in setup script and `lang="ts"`.
 
@@ -25,6 +27,12 @@ const emit = defineEmits<{
   (e: 'update', value: string): void
 }>()
 
+/* ✓ GOOD */
+const emit = defineEmits<{
+  change: [id: number]
+  update: [value: string]
+}>()
+
 /* ✗ BAD */
 const emit = defineEmits({
   change: (id) => typeof id == 'number',
@@ -41,10 +49,11 @@ const emit = defineEmits(['change', 'update'])
 ## :wrench: Options
 
 ```json
-  "vue/define-emits-declaration": ["error", "type-based" | "runtime"]
+  "vue/define-emits-declaration": ["error", "type-based" | "type-literal" | "runtime"]
 ```
 
-- `type-based` (default) enforces type-based declaration
+- `type-based` (default) enforces type based declaration
+- `type-literal` enforces strict "type literal" type based declaration
 - `runtime` enforces runtime declaration
 
 ### `runtime`
@@ -72,6 +81,37 @@ const emit = defineEmits(['change', 'update'])
 
 </eslint-code-block>
 
+### `type-literal`
+
+<eslint-code-block :rules="{'vue/define-emits-declaration': ['error', 'type-literal']}">
+
+```vue
+<script setup lang="ts">
+/* ✗ BAD */
+const emit = defineEmits(['change', 'update'])
+
+/* ✗ BAD */
+const emit = defineEmits({
+  change: (id) => typeof id == 'number',
+  update: (value) => typeof value == 'string'
+})
+
+/* ✗ BAD */
+const emit = defineEmits<{
+  (e: 'change', id: number): void
+  (e: 'update', value: string): void
+}>()
+
+/* ✓ GOOD */
+const emit = defineEmits<{
+  change: [id: number]
+  update: [value: string]
+}>()
+</script>
+```
+
+</eslint-code-block>
+
 ## :couple: Related Rules
 
 - [vue/define-props-declaration](./define-props-declaration.md)
diff --git a/docs/rules/index.md b/docs/rules/index.md
index 224cddc7..a7d3047a 100644
--- a/docs/rules/index.md
+++ b/docs/rules/index.md
@@ -251,6 +251,7 @@ For example:
 | [vue/no-undef-components](./no-undef-components.md) | disallow use of undefined components in `<template>` |  | :hammer: |
 | [vue/no-undef-properties](./no-undef-properties.md) | disallow undefined properties |  | :hammer: |
 | [vue/no-unsupported-features](./no-unsupported-features.md) | disallow unsupported Vue.js syntax on the specified version | :wrench: | :hammer: |
+| [vue/no-unused-emit-declarations](./no-unused-emit-declarations.md) | disallow unused emit declarations |  | :hammer: |
 | [vue/no-unused-properties](./no-unused-properties.md) | disallow unused properties |  | :hammer: |
 | [vue/no-unused-refs](./no-unused-refs.md) | disallow unused refs |  | :hammer: |
 | [vue/no-use-v-else-with-v-for](./no-use-v-else-with-v-for.md) | disallow using `v-else-if`/`v-else` on the same element as `v-for` |  | :hammer: |
diff --git a/docs/rules/no-unused-emit-declarations.md b/docs/rules/no-unused-emit-declarations.md
new file mode 100644
index 00000000..a6c93862
--- /dev/null
+++ b/docs/rules/no-unused-emit-declarations.md
@@ -0,0 +1,59 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/no-unused-emit-declarations
+description: disallow unused emit declarations
+---
+# vue/no-unused-emit-declarations
+
+> disallow unused emit declarations
+
+- :exclamation: <badge text="This rule has not been released yet." vertical="middle" type="error"> ***This rule has not been released yet.*** </badge>
+
+## :book: Rule Details
+
+This rule is aimed at eliminating unused emit declarations.
+
+<eslint-code-block :rules="{'vue/no-unused-emit-declarations': ['error']}">
+
+```vue
+<!-- ✗ BAD -->
+<script>
+  export default {
+    emits: ['foo'],
+  }
+</script>
+```
+
+</eslint-code-block>
+
+<eslint-code-block :rules="{'vue/no-unused-emit-declarations': ['error']}">
+
+```vue
+<!-- ✓ GOOD -->
+<script>
+  export default {
+    emits: ['foo'],
+    methods: {
+      foo() {
+        this.$emit('foo')
+      },
+    },
+  }
+</script>
+```
+
+</eslint-code-block>
+
+## :wrench: Options
+
+Nothing.
+
+## :couple: Related Rules
+
+- [vue/require-explicit-emits](./require-explicit-emits.md)
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-unused-emit-declarations.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-unused-emit-declarations.js)
diff --git a/docs/rules/require-explicit-emits.md b/docs/rules/require-explicit-emits.md
index 6d681c55..f7ce60dd 100644
--- a/docs/rules/require-explicit-emits.md
+++ b/docs/rules/require-explicit-emits.md
@@ -109,6 +109,10 @@ export default {
 
 </eslint-code-block>
 
+## :couple: Related Rules
+
+- [vue/no-unused-emit-declarations](./no-unused-emit-declarations.md)
+
 ## :books: Further Reading
 
 - [Guide - Custom Events / Defining Custom Events](https://v3.vuejs.org/guide/component-custom-events.html#defining-custom-events)
diff --git a/lib/configs/vue3-strongly-recommended.js b/lib/configs/vue3-strongly-recommended.js
index 9da67f39..564e386b 100644
--- a/lib/configs/vue3-strongly-recommended.js
+++ b/lib/configs/vue3-strongly-recommended.js
@@ -28,7 +28,12 @@ module.exports = {
     'vue/require-prop-types': 'warn',
     'vue/singleline-html-element-content-newline': 'warn',
     'vue/v-bind-style': 'warn',
-    'vue/v-on-event-hyphenation': 'warn',
+    'vue/v-on-event-hyphenation': [
+      'warn',
+      {
+        autofix: true
+      }
+    ],
     'vue/v-on-style': 'warn',
     'vue/v-slot-style': 'warn'
   }
diff --git a/lib/index.js b/lib/index.js
index edbe34a2..0f3dcba2 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -147,6 +147,7 @@ module.exports = {
     'no-undef-properties': require('./rules/no-undef-properties'),
     'no-unsupported-features': require('./rules/no-unsupported-features'),
     'no-unused-components': require('./rules/no-unused-components'),
+    'no-unused-emit-declarations': require('./rules/no-unused-emit-declarations'),
     'no-unused-properties': require('./rules/no-unused-properties'),
     'no-unused-refs': require('./rules/no-unused-refs'),
     'no-unused-vars': require('./rules/no-unused-vars'),
diff --git a/lib/rules/define-emits-declaration.js b/lib/rules/define-emits-declaration.js
index 478e1d84..3d12e0bc 100644
--- a/lib/rules/define-emits-declaration.js
+++ b/lib/rules/define-emits-declaration.js
@@ -6,6 +6,11 @@
 
 const utils = require('../utils')
 
+/**
+ * @typedef {import('@typescript-eslint/types').TSESTree.TSTypeLiteral} TSTypeLiteral
+ *
+ */
+
 module.exports = {
   meta: {
     type: 'suggestion',
@@ -17,12 +22,14 @@ module.exports = {
     fixable: null,
     schema: [
       {
-        enum: ['type-based', 'runtime']
+        enum: ['type-based', 'type-literal', 'runtime']
       }
     ],
     messages: {
-      hasArg: 'Use type-based declaration instead of runtime declaration.',
-      hasTypeArg: 'Use runtime declaration instead of type-based declaration.'
+      hasArg: 'Use type based declaration instead of runtime declaration.',
+      hasTypeArg: 'Use runtime declaration instead of type based declaration.',
+      hasTypeCallArg:
+        'Use new type literal declaration instead of the old call signature declaration.'
     }
   },
   /** @param {RuleContext} context */
@@ -46,8 +53,31 @@ module.exports = {
             break
           }
 
-          case 'runtime': {
+          case 'type-literal': {
+            if (node.arguments.length > 0) {
+              context.report({
+                node,
+                messageId: 'hasArg'
+              })
+              return
+            }
+
             const typeArguments = node.typeArguments || node.typeParameters
+            const param = /** @type {TSTypeLiteral} */ (typeArguments.params[0])
+            for (const memberNode of param.members) {
+              if (memberNode.type !== 'TSPropertySignature') {
+                context.report({
+                  node: memberNode,
+                  messageId: 'hasTypeCallArg'
+                })
+              }
+            }
+            break
+          }
+
+          case 'runtime': {
+            const typeArguments =
+              'typeArguments' in node ? node.typeArguments : node.typeParameters
             if (typeArguments && typeArguments.params.length > 0) {
               context.report({
                 node,
diff --git a/lib/rules/define-props-declaration.js b/lib/rules/define-props-declaration.js
index d355bb2f..3862bb22 100644
--- a/lib/rules/define-props-declaration.js
+++ b/lib/rules/define-props-declaration.js
@@ -47,7 +47,8 @@ module.exports = {
           }
 
           case 'runtime': {
-            const typeArguments = node.typeArguments || node.typeParameters
+            const typeArguments =
+              'typeArguments' in node ? node.typeArguments : node.typeParameters
             if (typeArguments && typeArguments.params.length > 0) {
               context.report({
                 node,
diff --git a/lib/rules/no-unused-emit-declarations.js b/lib/rules/no-unused-emit-declarations.js
new file mode 100644
index 00000000..1414b50c
--- /dev/null
+++ b/lib/rules/no-unused-emit-declarations.js
@@ -0,0 +1,330 @@
+/**
+ * @author ItMaga
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+const { findVariable } = require('@eslint-community/eslint-utils')
+const utils = require('../utils')
+
+/**
+ * @typedef {import('../utils').ComponentEmit} ComponentEmit
+ * @typedef {import('../utils').VueObjectData} VueObjectData
+ */
+
+/**
+ * @typedef {object} SetupContext
+ * @property {Set<Identifier>} contextReferenceIds
+ * @property {Set<Identifier>} emitReferenceIds
+ */
+
+/**
+ * @typedef {object} NameWithLoc
+ * @property {string} name
+ * @property {SourceLocation} loc
+ * @property {Range} range
+ */
+
+/**
+ * Get the name param node from the given CallExpression
+ * @param {CallExpression} node CallExpression
+ * @returns {NameWithLoc | null}
+ */
+function getNameParamNode(node) {
+  const nameLiteralNode = node.arguments[0]
+  if (nameLiteralNode && utils.isStringLiteral(nameLiteralNode)) {
+    const name = utils.getStringLiteralValue(nameLiteralNode)
+    if (name != null) {
+      return { name, loc: nameLiteralNode.loc, range: nameLiteralNode.range }
+    }
+  }
+
+  return null
+}
+
+/**
+ * Check if the given node is a reference of setup context
+ * @param {Expression | Super | SpreadElement} value
+ * @param {SetupContext} setupContext
+ * @returns {boolean}
+ * */
+function hasReferenceId(value, setupContext) {
+  const { contextReferenceIds, emitReferenceIds } = setupContext
+  return (
+    value.type === 'Identifier' &&
+    (emitReferenceIds.has(value) || contextReferenceIds.has(value))
+  )
+}
+
+module.exports = {
+  meta: {
+    type: 'suggestion',
+    docs: {
+      description: 'disallow unused emit declarations',
+      categories: undefined,
+      url: 'https://eslint.vuejs.org/rules/no-unused-emit-declarations.html'
+    },
+    fixable: null,
+    schema: [],
+    messages: {
+      unused: '`{{name}}` is defined as emit but never used.'
+    }
+  },
+  /** @param {RuleContext} context */
+  create(context) {
+    /** @type {Map<string, ComponentEmit>} */
+    const emitDeclarations = new Map()
+    /** @type {Map<string, Expression>} */
+    const emitCalls = new Map()
+    /** @type {Map<ObjectExpression | Program, SetupContext>} */
+    const setupContexts = new Map()
+    const programNode = context.getSourceCode().ast
+
+    /**
+     * @param {CallExpression} node
+     * */
+    function addEmitCall(node) {
+      const nameParamNode = getNameParamNode(node)
+      if (nameParamNode) {
+        emitCalls.set(nameParamNode.name, node)
+      }
+    }
+    function clearEmits() {
+      emitCalls.clear()
+      emitDeclarations.clear()
+    }
+
+    /**
+     * @param {Expression | SpreadElement} expression
+     * @param {SetupContext} setupContext
+     * @returns {boolean}
+     * */
+    function checkExpressionReference(expression, setupContext) {
+      if (expression.type === 'MemberExpression') {
+        const memObject = utils.skipChainExpression(expression.object)
+        if (hasReferenceId(memObject, setupContext)) {
+          clearEmits()
+          return true
+        }
+      }
+      if (hasReferenceId(expression, setupContext)) {
+        clearEmits()
+        return true
+      }
+      return false
+    }
+
+    /**
+     *
+     * @param {Array<Expression | SpreadElement>} args
+     * @param {SetupContext} setupContext
+     * @returns {boolean}
+     */
+    function verifyArgumentsReferences(args, setupContext) {
+      for (const argument of args) {
+        if (argument.type === 'ObjectExpression') {
+          for (const property of argument.properties) {
+            if (
+              property.type === 'Property' &&
+              checkExpressionReference(property.value, setupContext)
+            ) {
+              return true
+            }
+          }
+        }
+
+        if (argument.type === 'ArrayExpression') {
+          for (const element of argument.elements) {
+            if (!element) {
+              continue
+            }
+            if (checkExpressionReference(element, setupContext)) {
+              return true
+            }
+          }
+        }
+
+        if (checkExpressionReference(argument, setupContext)) {
+          return true
+        }
+      }
+      return false
+    }
+
+    /**
+     * @param {Expression | Super} callee
+     * @param {Set<Identifier>} referenceIds
+     * @param {CallExpression} node
+     * */
+    function addEmitCallByReference(callee, referenceIds, node) {
+      if (callee.type === 'Identifier' && referenceIds.has(callee)) {
+        addEmitCall(node)
+      }
+    }
+
+    const callVisitor = {
+      /**
+       * @param {CallExpression} node
+       * @param {VueObjectData} [info]
+       */
+      CallExpression(node, info) {
+        const callee = utils.skipChainExpression(node.callee)
+
+        let emit = null
+        if (callee.type === 'MemberExpression') {
+          const name = utils.getStaticPropertyName(callee)
+          if (name === 'emit' || name === '$emit') {
+            emit = { name, member: callee }
+          }
+        }
+
+        const vueDefineNode = info ? info.node : programNode
+        const setupContext = setupContexts.get(vueDefineNode)
+        if (setupContext) {
+          if (
+            callee.parent.type === 'CallExpression' &&
+            callee.parent.arguments &&
+            verifyArgumentsReferences(callee.parent.arguments, setupContext)
+          ) {
+            // skip if the emit passed as argument
+            return
+          }
+
+          const { contextReferenceIds, emitReferenceIds } = setupContext
+          // verify setup(props,{emit}) {emit()}
+          addEmitCallByReference(callee, emitReferenceIds, node)
+          if (emit && emit.name === 'emit') {
+            const memObject = utils.skipChainExpression(emit.member.object)
+            // verify setup(props,context) {context.emit()}
+            addEmitCallByReference(memObject, contextReferenceIds, node)
+          }
+        }
+
+        if (emit && emit.name === '$emit') {
+          const memObject = utils.skipChainExpression(emit.member.object)
+          // verify this.$emit()
+          if (utils.isThis(memObject, context)) {
+            addEmitCall(node)
+          }
+        }
+
+        // verify $emit() in template
+        if (callee.type === 'Identifier' && callee.name === '$emit') {
+          addEmitCall(node)
+        }
+      }
+    }
+
+    return utils.compositingVisitors(
+      utils.defineTemplateBodyVisitor(context, callVisitor),
+      utils.defineVueVisitor(context, {
+        ...callVisitor,
+        onVueObjectEnter(node) {
+          for (const emit of utils.getComponentEmitsFromOptions(node)) {
+            if (emit.emitName) {
+              emitDeclarations.set(emit.emitName, emit)
+            }
+          }
+        },
+        onSetupFunctionEnter(node, { node: vueNode }) {
+          const contextParam = node.params[1]
+          if (
+            !contextParam ||
+            contextParam.type === 'RestElement' ||
+            contextParam.type === 'ArrayPattern'
+          ) {
+            // no arguments or cannot check
+            return
+          }
+
+          /** @type {Set<Identifier>} */
+          const contextReferenceIds = new Set()
+          /** @type {Set<Identifier>} */
+          const emitReferenceIds = new Set()
+          if (contextParam.type === 'ObjectPattern') {
+            const emitProperty = utils.findAssignmentProperty(
+              contextParam,
+              'emit'
+            )
+            if (!emitProperty) {
+              return
+            }
+            const emitParam = emitProperty.value
+            // `setup(props, {emit})`
+            const variable =
+              emitParam.type === 'Identifier'
+                ? findVariable(context.getScope(), emitParam)
+                : null
+            if (!variable) {
+              return
+            }
+            for (const reference of variable.references) {
+              emitReferenceIds.add(reference.identifier)
+            }
+          } else if (contextParam.type === 'Identifier') {
+            // `setup(props, context)`
+            const variable = findVariable(context.getScope(), contextParam)
+            for (const reference of variable.references) {
+              contextReferenceIds.add(reference.identifier)
+            }
+          }
+
+          setupContexts.set(vueNode, {
+            contextReferenceIds,
+            emitReferenceIds
+          })
+        }
+      }),
+      utils.defineScriptSetupVisitor(context, {
+        onDefineEmitsEnter(node, emits) {
+          for (const emit of emits) {
+            if (emit.emitName) {
+              emitDeclarations.set(emit.emitName, emit)
+            }
+          }
+
+          if (
+            !node.parent ||
+            node.parent.type !== 'VariableDeclarator' ||
+            node.parent.init !== node
+          ) {
+            return
+          }
+
+          const emitParam = node.parent.id
+          const variable =
+            emitParam.type === 'Identifier'
+              ? findVariable(context.getScope(), emitParam)
+              : null
+          if (!variable) {
+            return
+          }
+          /** @type {Set<Identifier>} */
+          const emitReferenceIds = new Set()
+          for (const reference of variable.references) {
+            emitReferenceIds.add(reference.identifier)
+          }
+          setupContexts.set(programNode, {
+            contextReferenceIds: new Set(),
+            emitReferenceIds
+          })
+        },
+        ...callVisitor
+      }),
+      {
+        'Program:exit'() {
+          for (const [name, emit] of emitDeclarations) {
+            if (!emitCalls.has(name) && emit.node) {
+              context.report({
+                node: emit.node,
+                loc: emit.node.loc,
+                messageId: 'unused',
+                data: { name }
+              })
+            }
+          }
+        }
+      }
+    )
+  }
+}
diff --git a/lib/rules/require-typed-ref.js b/lib/rules/require-typed-ref.js
index 2f691fec..e26ac133 100644
--- a/lib/rules/require-typed-ref.js
+++ b/lib/rules/require-typed-ref.js
@@ -84,7 +84,9 @@ module.exports = {
           }
 
           const typeArguments =
-            ref.node.typeArguments || ref.node.typeParameters
+            'typeArguments' in ref.node
+              ? ref.node.typeArguments
+              : ref.node.typeParameters
           if (typeArguments == null) {
             if (
               ref.node.parent.type === 'VariableDeclarator' &&
diff --git a/lib/rules/v-on-event-hyphenation.js b/lib/rules/v-on-event-hyphenation.js
index 5a3e023f..b797b3b7 100644
--- a/lib/rules/v-on-event-hyphenation.js
+++ b/lib/rules/v-on-event-hyphenation.js
@@ -10,7 +10,10 @@ module.exports = {
       description:
         'enforce v-on event naming style on custom components in template',
       categories: ['vue3-strongly-recommended'],
-      url: 'https://eslint.vuejs.org/rules/v-on-event-hyphenation.html'
+      url: 'https://eslint.vuejs.org/rules/v-on-event-hyphenation.html',
+      defaultOptions: {
+        vue3: [{ autofix: true }]
+      }
     },
     fixable: 'code',
     schema: [
diff --git a/lib/rules/valid-define-emits.js b/lib/rules/valid-define-emits.js
index 994302c6..2c83faa2 100644
--- a/lib/rules/valid-define-emits.js
+++ b/lib/rules/valid-define-emits.js
@@ -47,7 +47,8 @@ module.exports = {
         onDefineEmitsEnter(node) {
           defineEmitsNodes.push(node)
 
-          const typeArguments = node.typeArguments || node.typeParameters
+          const typeArguments =
+            'typeArguments' in node ? node.typeArguments : node.typeParameters
           if (node.arguments.length > 0) {
             if (typeArguments && typeArguments.params.length > 0) {
               // `defineEmits` has both a literal type and an argument.
diff --git a/lib/rules/valid-define-options.js b/lib/rules/valid-define-options.js
index c1cd5b99..fad893b4 100644
--- a/lib/rules/valid-define-options.js
+++ b/lib/rules/valid-define-options.js
@@ -74,7 +74,8 @@ module.exports = {
             })
           }
 
-          const typeArguments = node.typeArguments || node.typeParameters
+          const typeArguments =
+            'typeArguments' in node ? node.typeArguments : node.typeParameters
           if (typeArguments) {
             context.report({
               node: typeArguments,
diff --git a/lib/rules/valid-define-props.js b/lib/rules/valid-define-props.js
index ecde56f1..7e3b0a34 100644
--- a/lib/rules/valid-define-props.js
+++ b/lib/rules/valid-define-props.js
@@ -48,7 +48,8 @@ module.exports = {
         onDefinePropsEnter(node) {
           definePropsNodes.push(node)
 
-          const typeArguments = node.typeArguments || node.typeParameters
+          const typeArguments =
+            'typeArguments' in node ? node.typeArguments : node.typeParameters
           if (node.arguments.length > 0) {
             if (typeArguments && typeArguments.params.length > 0) {
               // `defineProps` has both a literal type and an argument.
diff --git a/lib/utils/indent-common.js b/lib/utils/indent-common.js
index 6119406e..bacf43e1 100644
--- a/lib/utils/indent-common.js
+++ b/lib/utils/indent-common.js
@@ -1168,7 +1168,8 @@ module.exports.defineVisitor = function create(
     },
     /** @param {CallExpression} node */
     CallExpression(node) {
-      const typeArguments = node.typeArguments || node.typeParameters
+      const typeArguments =
+        'typeArguments' in node ? node.typeArguments : node.typeParameters
       const firstToken = tokenStore.getFirstToken(node)
       const rightToken = tokenStore.getLastToken(node)
       const leftToken = /** @type {Token} */ (
@@ -1695,7 +1696,8 @@ module.exports.defineVisitor = function create(
     },
     /** @param {NewExpression} node */
     NewExpression(node) {
-      const typeArguments = node.typeArguments || node.typeParameters
+      const typeArguments =
+        'typeArguments' in node ? node.typeArguments : node.typeParameters
       const newToken = tokenStore.getFirstToken(node)
       const calleeToken = tokenStore.getTokenAfter(newToken)
       const rightToken = tokenStore.getLastToken(node)
diff --git a/lib/utils/indent-ts.js b/lib/utils/indent-ts.js
index a4323b38..f021e8a1 100644
--- a/lib/utils/indent-ts.js
+++ b/lib/utils/indent-ts.js
@@ -332,7 +332,10 @@ function defineVisitor({
      * @param {TSTypeReference | TSInstantiationExpression} node
      */
     'TSTypeReference, TSInstantiationExpression'(node) {
-      const typeArguments = node.typeArguments || node.typeParameters
+      const typeArguments =
+        'typeArguments' in node
+          ? node.typeArguments
+          : /** @type {any} typescript-eslint v5 */ (node).typeParameters
       if (typeArguments) {
         const firstToken = tokenStore.getFirstToken(node)
         setOffset(tokenStore.getFirstToken(typeArguments), 1, firstToken)
@@ -1183,8 +1186,9 @@ function defineVisitor({
         setOffset([dotToken, propertyToken], 1, firstToken)
       }
       const typeArguments =
-        node.typeArguments ||
-        /** @type {any} typescript-eslint v5 */ (node).typeParameters
+        'typeArguments' in node
+          ? node.typeArguments
+          : /** @type {any} typescript-eslint v5 */ (node).typeParameters
       if (typeArguments) {
         setOffset(tokenStore.getFirstToken(typeArguments), 1, firstToken)
       }
diff --git a/lib/utils/index.js b/lib/utils/index.js
index 22119ae9..4073b691 100644
--- a/lib/utils/index.js
+++ b/lib/utils/index.js
@@ -3000,7 +3000,8 @@ function getComponentPropsFromDefineProps(context, node) {
       }
     ]
   }
-  const typeArguments = node.typeArguments || node.typeParameters
+  const typeArguments =
+    'typeArguments' in node ? node.typeArguments : node.typeParameters
   if (typeArguments && typeArguments.params.length > 0) {
     return getComponentPropsFromTypeDefine(context, typeArguments.params[0])
   }
@@ -3033,7 +3034,8 @@ function getComponentEmitsFromDefineEmits(context, node) {
       }
     ]
   }
-  const typeArguments = node.typeArguments || node.typeParameters
+  const typeArguments =
+    'typeArguments' in node ? node.typeArguments : node.typeParameters
   if (typeArguments && typeArguments.params.length > 0) {
     return getComponentEmitsFromTypeDefine(context, typeArguments.params[0])
   }
diff --git a/lib/utils/ts-utils/ts-ast.js b/lib/utils/ts-utils/ts-ast.js
index 8a95dd9b..3a7356a2 100644
--- a/lib/utils/ts-utils/ts-ast.js
+++ b/lib/utils/ts-utils/ts-ast.js
@@ -430,7 +430,10 @@ function inferRuntimeType(context, node, checked = new Set()) {
             return ['Array']
           }
           case 'NonNullable': {
-            const typeArguments = node.typeArguments || node.typeParameters
+            const typeArguments =
+              'typeArguments' in node
+                ? node.typeArguments
+                : /** @type {any} typescript-eslint v5 */ (node).typeParameters
             if (typeArguments && typeArguments.params[0]) {
               return inferRuntimeType(
                 context,
@@ -441,7 +444,10 @@ function inferRuntimeType(context, node, checked = new Set()) {
             break
           }
           case 'Extract': {
-            const typeArguments = node.typeArguments || node.typeParameters
+            const typeArguments =
+              'typeArguments' in node
+                ? node.typeArguments
+                : /** @type {any} typescript-eslint v5 */ (node).typeParameters
             if (typeArguments && typeArguments.params[1]) {
               return inferRuntimeType(context, typeArguments.params[1], checked)
             }
@@ -449,7 +455,10 @@ function inferRuntimeType(context, node, checked = new Set()) {
           }
           case 'Exclude':
           case 'OmitThisParameter': {
-            const typeArguments = node.typeArguments || node.typeParameters
+            const typeArguments =
+              'typeArguments' in node
+                ? node.typeArguments
+                : /** @type {any} typescript-eslint v5 */ (node).typeParameters
             if (typeArguments && typeArguments.params[0]) {
               return inferRuntimeType(context, typeArguments.params[0], checked)
             }


Test patch:
diff --git a/tests/lib/rules/define-emits-declaration.js b/tests/lib/rules/define-emits-declaration.js
index 372fa1a4..43ecb385 100644
--- a/tests/lib/rules/define-emits-declaration.js
+++ b/tests/lib/rules/define-emits-declaration.js
@@ -63,6 +63,36 @@ tester.run('define-emits-declaration', rule, {
        `,
       options: ['runtime']
     },
+    {
+      filename: 'test.vue',
+      code: `
+        <script setup lang="ts">
+        const emit = defineEmits<{
+          change: [id: number]
+          update: [value: string]
+        }>()
+        </script>
+       `,
+      options: ['type-based'],
+      parserOptions: {
+        parser: require.resolve('@typescript-eslint/parser')
+      }
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <script setup lang="ts">
+        const emit = defineEmits<{
+          change: [id: number]
+          update: [value: string]
+        }>()
+        </script>
+       `,
+      options: ['type-literal'],
+      parserOptions: {
+        parser: require.resolve('@typescript-eslint/parser')
+      }
+    },
     {
       filename: 'test.vue',
       // ignore code without defineEmits
@@ -82,7 +112,7 @@ tester.run('define-emits-declaration', rule, {
       code: `
          <script lang="ts">
          import { PropType } from 'vue'
- 
+
          export default {
            props: {
              kind: { type: String as PropType<'primary' | 'secondary'> },
@@ -106,7 +136,7 @@ tester.run('define-emits-declaration', rule, {
        `,
       errors: [
         {
-          message: 'Use type-based declaration instead of runtime declaration.',
+          message: 'Use type based declaration instead of runtime declaration.',
           line: 3
         }
       ]
@@ -121,7 +151,25 @@ tester.run('define-emits-declaration', rule, {
       options: ['type-based'],
       errors: [
         {
-          message: 'Use type-based declaration instead of runtime declaration.',
+          message: 'Use type based declaration instead of runtime declaration.',
+          line: 3
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+       <script setup lang="ts">
+       const emit = defineEmits(['change', 'update'])
+       </script>
+       `,
+      options: ['type-literal'],
+      parserOptions: {
+        parser: require.resolve('@typescript-eslint/parser')
+      },
+      errors: [
+        {
+          message: 'Use type based declaration instead of runtime declaration.',
           line: 3
         }
       ]
@@ -142,10 +190,59 @@ tester.run('define-emits-declaration', rule, {
       },
       errors: [
         {
-          message: 'Use runtime declaration instead of type-based declaration.',
+          message: 'Use runtime declaration instead of type based declaration.',
           line: 3
         }
       ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <script setup lang="ts">
+        const emit = defineEmits<{
+          (e: 'change', id: number): void
+          (e: 'update', value: string): void
+        }>()
+        </script>
+       `,
+      options: ['type-literal'],
+      parserOptions: {
+        parser: require.resolve('@typescript-eslint/parser')
+      },
+      errors: [
+        {
+          message:
+            'Use new type literal declaration instead of the old call signature declaration.',
+          line: 4
+        },
+        {
+          message:
+            'Use new type literal declaration instead of the old call signature declaration.',
+          line: 5
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <script setup lang="ts">
+        const emit = defineEmits<{
+          'change': [id: number]
+          (e: 'update', value: string): void
+        }>()
+        </script>
+       `,
+      options: ['type-literal'],
+      parserOptions: {
+        parser: require.resolve('@typescript-eslint/parser')
+      },
+      errors: [
+        {
+          message:
+            'Use new type literal declaration instead of the old call signature declaration.',
+          line: 5
+        }
+      ]
     }
   ]
 })
diff --git a/tests/lib/rules/no-unused-emit-declarations.js b/tests/lib/rules/no-unused-emit-declarations.js
new file mode 100644
index 00000000..0667ad46
--- /dev/null
+++ b/tests/lib/rules/no-unused-emit-declarations.js
@@ -0,0 +1,693 @@
+/**
+ * @author ItMaga
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+const RuleTester = require('eslint').RuleTester
+const rule = require('../../../lib/rules/no-unused-emit-declarations')
+const {
+  getTypeScriptFixtureTestOptions
+} = require('../../test-utils/typescript')
+
+const tester = new RuleTester({
+  parser: require.resolve('vue-eslint-parser'),
+  parserOptions: {
+    ecmaVersion: 2020,
+    sourceType: 'module'
+  }
+})
+
+tester.run('no-unused-emit-declarations', rule, {
+  valid: [
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+        export default {
+          emits: ['count', 'foo'],
+          methods: {
+              setCount () {
+                  this.$emit('count', newCount)
+              },
+              setFoo () {
+                  this.$emit('foo', newFoo)
+              }
+          }
+        }
+      </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      export default {
+        emits: ['foo'],
+        methods: {
+          click () {
+            const vm = this
+            vm?.$emit?.('foo')
+          }
+        }
+      }
+      </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <button @click="$emit('bar')">Bar</button>
+      </template>
+      <script>
+        export default {
+          emits: ['foo', 'bar'],
+          methods: {
+              setFoo () {
+                  this.$emit('foo', newFoo)
+              }
+          }
+        }
+      </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <button @click="this.$emit('bar')">Bar</button>
+      </template>
+      <script>
+        export default {
+          emits: ['bar'],
+        }
+      </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <button @click="$emit('bar')">Bar</button>
+      </template>
+      <script setup>
+        const emit = defineEmits(['foo', 'bar'])
+        const setFoo = () => {
+          emit('foo', newFoo)
+        }
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+        export default {
+          emits: ['foo'],
+          setup(_, { emit }) {
+            const setFoo = () => {
+              emit('foo', newFoo)
+            }
+          }
+        }
+      </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+        export default {
+          emits: ['foo'],
+          setup(_, { emit: customName }) {
+            const setFoo = () => customName('foo')
+          }
+        }
+      </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+        export default {
+          emits: { foo: null },
+          setup(_, { emit }) {
+            const setFoo = () => {
+              emit('foo', newFoo)
+            }
+          }
+        }
+      </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+        export default {
+          setup(_, { emit }) {
+            const setFoo = () => {
+              emit('foo', newFoo)
+            }
+          }
+        }
+      </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <button @click="$emit('bar')">Bar</button>
+      </template>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+        export default {
+          emits: ['foo'],
+          setup(_, context) {
+            const setFoo = () => {
+              context.emit('foo', newFoo)
+            }
+          }
+        }
+      </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+        export default {
+          emits: ['foo'],
+          setup(_, { emit }) {
+            useCustomComposable(emit)
+          }
+        }
+      </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+        export default {
+          emits: ['foo'],
+          setup(_, context) {
+            useCustomComposable(context.emit)
+          }
+        }
+      </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+        export default {
+          emits: ['foo'],
+          setup(_, { emit }) {
+            useCustomComposable([emit])
+          }
+        }
+      </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+        export default {
+          emits: ['foo'],
+          setup(_, context) {
+            useCustomComposable([context.emit])
+          }
+        }
+      </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+        export default {
+          emits: ['foo'],
+          setup(_, context) {
+            useCustomComposable({ emit: context.emit }) 
+          }
+        }
+      </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+        export default {
+          emits: [...foo],
+        }
+      </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+        const emits = ['foo'];
+        export default {
+          emits: [...emits],
+        }
+      </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+        const emits = ['foo'];
+        export default {
+          emits: emits,
+        }
+      </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+        export default {
+          emits: ['foo'],
+          setup(_, { emit }) {
+            useCustomComposable({ emit: emit }) 
+          }
+        }
+      </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+        export default {
+          emits: ['foo', 'bar'],
+          setup(_, { emit }) {
+            const emitBar = () => {
+              emit('bar', newBar)
+            }
+          },
+          methods: {
+            setFoo() {
+              this.$emit('foo', newFoo)
+            }
+          }
+        }
+      </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script setup>
+        defineEmits(...foo)
+      </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script setup>
+        const emits = ['foo'];
+        defineEmits([...emits])
+      </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script setup lang="ts">
+      const emit = defineEmits<{
+        (e: 'foo'): void
+      }>()
+      const change = () => emit('foo');
+      `,
+      parserOptions: { parser: require.resolve('@typescript-eslint/parser') }
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script setup lang="ts">
+      const emit = defineEmits<{
+        foo: [string, number]
+      }>()
+      const change = () => emit('foo');
+      `,
+      ...getTypeScriptFixtureTestOptions()
+    }
+  ],
+  invalid: [
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+        export default {
+          emits: ['foo', 'count'],
+          methods: {
+            setFoo () {
+              this.$emit('foo', newFoo)
+            }
+          }
+        }
+      </script>
+      `,
+      errors: [
+        {
+          messageId: 'unused',
+          line: 4,
+          column: 26,
+          endColumn: 33
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      export default {
+        emits: ['foo', 'bar'],
+        methods: {
+          click () {
+            const vm = this
+            vm?.$emit?.('foo')
+          }
+        }
+      }
+      </script>
+      `,
+      errors: [
+        {
+          messageId: 'unused',
+          line: 4,
+          column: 24,
+          endColumn: 29
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <button @click="$emit('bar')">Foo</button>
+      </template>
+      <script>
+        export default {
+          emits: ['foo', 'bar'],
+        }
+      </script>
+      `,
+      errors: [
+        {
+          messageId: 'unused',
+          line: 7,
+          column: 19,
+          endColumn: 24
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+        export default {
+          emits: { foo: null },
+        }
+      </script>
+      `,
+      errors: [
+        {
+          messageId: 'unused',
+          line: 4,
+          column: 20,
+          endColumn: 29
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <button @click="$emit('bar')">Bar</button>
+      </template>
+      <script setup>
+        const emit = defineEmits(['foo', 'bar'])
+      `,
+      errors: [
+        {
+          messageId: 'unused',
+          line: 6,
+          column: 35,
+          endColumn: 40
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+        export default {
+          emits: ['foo'],
+          methods: {
+              setFoo () {
+                  custom.$emit('foo', newFoo)
+              }
+          }
+        }
+      </script>
+      `,
+      errors: [
+        {
+          messageId: 'unused',
+          line: 4,
+          column: 19,
+          endColumn: 24
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+        export default {
+          emits: ['foo', 'bar'],
+          setup(_, { emit }) {
+            const setFoo = () => {
+              emit('foo', newFoo)
+            }
+          }
+        }
+      </script>
+      `,
+      errors: [
+        {
+          messageId: 'unused',
+          line: 4,
+          column: 26,
+          endColumn: 31
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <button @click="$emit('bar')">Bar</button>
+      </template>
+      <script>
+        // @vue/component
+        const Foo = {
+          emits: ['foo', 'bar'],
+        }
+        export default Foo
+      </script>
+      `,
+      errors: [
+        {
+          messageId: 'unused',
+          line: 8,
+          column: 19,
+          endColumn: 24
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+        export default {
+          emits: ['foo', 'bar'],
+          setup(_, { emit: customName }) {
+            const setFoo = () => customName('foo')
+          }
+        }
+      </script>
+      `,
+      errors: [
+        {
+          messageId: 'unused',
+          line: 4,
+          column: 26,
+          endColumn: 31
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+        export default {
+          emits: ['foo'],
+          setup() {
+            const setFoo = () => foo.$emit('foo')
+          }
+        }
+      </script>
+      `,
+      errors: [
+        {
+          messageId: 'unused',
+          line: 4,
+          column: 19,
+          endColumn: 24
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+        export default {
+          emits: ['foo', 'bar'],
+          setup(_, context) {
+            const setFoo = () => {
+              context.emit('foo', newFoo)
+            }
+          }
+        }
+      </script>
+      `,
+      errors: [
+        {
+          messageId: 'unused',
+          line: 4,
+          column: 26,
+          endColumn: 31
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+        export default {
+          emits: ['foo', 'bar', 'baz'],
+          setup(_, { emit }) {
+            const emitBar = () => {
+              emit('bar', newBar)
+            }
+          },
+          methods: {
+            setFoo() {
+              this.$emit('foo', newFoo)
+            }
+          }
+        }
+      </script>
+      `,
+      errors: [
+        {
+          messageId: 'unused',
+          line: 4,
+          endLine: 4,
+          column: 33,
+          endColumn: 38
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script setup lang="ts">
+        const emit = defineEmits<{
+          (e: 'foo' | 'bar'): void;
+        }>()
+        const change = () => emit('foo');
+      `,
+      parserOptions: { parser: require.resolve('@typescript-eslint/parser') },
+      errors: [
+        {
+          messageId: 'unused',
+          line: 4,
+          column: 11,
+          endColumn: 36
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script setup>
+        const emits = ['foo'];
+        defineEmits(emits)
+      </script>
+      `,
+      errors: [
+        {
+          messageId: 'unused',
+          line: 3,
+          column: 24,
+          endColumn: 29
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script setup lang="ts">
+        defineEmits<{(e: 'foo'): void}>()
+      </script>
+      `,
+      parserOptions: { parser: require.resolve('@typescript-eslint/parser') },
+      errors: [
+        {
+          messageId: 'unused',
+          line: 3,
+          column: 22,
+          endColumn: 38
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script setup lang="ts">
+      const emit = defineEmits<{
+        foo: [string, number],
+        bar: []
+      }>()
+      const change = () => emit('foo');
+      `,
+      errors: [
+        {
+          messageId: 'unused',
+          line: 5,
+          column: 9,
+          endColumn: 16
+        }
+      ],
+      ...getTypeScriptFixtureTestOptions()
+    }
+  ]
+})