1717// https://sonarsource.github.io/rspec/#/rspec/S6598/javascript
1818
1919import type { Rule } from 'eslint' ;
20+ import type estree from 'estree' ;
21+ import type { TSESTree } from '@typescript-eslint/utils' ;
2022import { generateMeta } from '../helpers/generate-meta.js' ;
2123import { interceptReport } from '../helpers/decorators/interceptor.js' ;
24+ import { isInsideVueSetupScript } from '../helpers/vue.js' ;
2225import * as meta from './generated-meta.js' ;
2326
27+ function getTopLevelCallExprs ( context : Rule . RuleContext ) : TSESTree . CallExpression [ ] {
28+ return context . sourceCode . ast . body . flatMap ( stmt => {
29+ if ( stmt . type === 'ExpressionStatement' && stmt . expression . type === 'CallExpression' ) {
30+ return [ stmt . expression as unknown as TSESTree . CallExpression ] ;
31+ }
32+ if ( stmt . type === 'VariableDeclaration' ) {
33+ return stmt . declarations
34+ . filter ( decl => decl . init ?. type === 'CallExpression' )
35+ . map ( decl => decl . init as unknown as TSESTree . CallExpression ) ;
36+ }
37+ return [ ] ;
38+ } ) ;
39+ }
40+
41+ function isReferencedByDefineEmits ( name : string , context : Rule . RuleContext ) : boolean {
42+ return getTopLevelCallExprs ( context ) . some ( call => {
43+ if ( call . callee . type !== 'Identifier' || call . callee . name !== 'defineEmits' ) {
44+ return false ;
45+ }
46+ return (
47+ call . typeArguments ?. params ?. some (
48+ p =>
49+ p . type === 'TSTypeReference' &&
50+ p . typeName . type === 'Identifier' &&
51+ p . typeName . name === name ,
52+ ) ?? false
53+ ) ;
54+ } ) ;
55+ }
56+
57+ function isDefineEmitsTypeArg (
58+ node : TSESTree . TSCallSignatureDeclaration ,
59+ context : Rule . RuleContext ,
60+ ) : boolean {
61+ const { parent } = node ;
62+ if ( parent . type === 'TSTypeLiteral' ) {
63+ const grandParent = parent . parent ;
64+ // Inline type literal used directly as a defineEmits type argument, e.g.:
65+ // defineEmits<{ ...function signature... }>()
66+ if ( grandParent . type === 'TSTypeParameterInstantiation' ) {
67+ const greatGrandParent = grandParent . parent ;
68+ if ( greatGrandParent . type === 'CallExpression' ) {
69+ const { callee } = greatGrandParent ;
70+ return callee . type === 'Identifier' && callee . name === 'defineEmits' ;
71+ }
72+ return false ;
73+ }
74+ // Type alias whose body contains the call signature, referenced by defineEmits, e.g.:
75+ // type Emits = { ...function signature... }
76+ // defineEmits<Emits>()
77+ if ( grandParent . type === 'TSTypeAliasDeclaration' ) {
78+ return isReferencedByDefineEmits ( grandParent . id . name , context ) ;
79+ }
80+ return false ;
81+ }
82+ // Interface whose body contains the call signature, referenced by defineEmits, e.g.:
83+ // interface Emits { ...function signature... }
84+ // defineEmits<Emits>()
85+ if ( parent . type === 'TSInterfaceBody' ) {
86+ return isReferencedByDefineEmits ( parent . parent . id . name , context ) ;
87+ }
88+ return false ;
89+ }
90+
2491// Rewording one of the issue messages reported by the core implementation.
92+ // Suppresses false positives for defineEmits type arguments in Vue <script setup>.
2593export function decorate ( rule : Rule . RuleModule ) : Rule . RuleModule {
2694 return interceptReport (
2795 {
@@ -37,9 +105,18 @@ export function decorate(rule: Rule.RuleModule): Rule.RuleModule {
37105 } ) ,
38106 } ,
39107 ( context , reportDescriptor ) => {
40- context . report ( {
41- ...reportDescriptor ,
42- } ) ;
108+ const node = ( reportDescriptor as unknown as { node : TSESTree . Node } ) . node ;
109+ if ( node . type !== 'TSCallSignatureDeclaration' ) {
110+ context . report ( { ...reportDescriptor } ) ;
111+ return ;
112+ }
113+ if ( ! isInsideVueSetupScript ( node as unknown as estree . Node , context ) ) {
114+ context . report ( { ...reportDescriptor } ) ;
115+ return ;
116+ }
117+ if ( ! isDefineEmitsTypeArg ( node , context ) ) {
118+ context . report ( { ...reportDescriptor } ) ;
119+ }
43120 } ,
44121 ) ;
45122}
0 commit comments