Repository: vuejs/eslint-plugin-vue
Pull request #2327 (2023-11-27 14:26:47)

fix: avoid calling typeParameters getter
#2292

typeParameters was still being accessed when typeArguments is undefined


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/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/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()
+    }
+  ]
+})