7
7
/**
8
8
* @typedef {import('../utils').ComponentArrayEmit } ComponentArrayEmit
9
9
* @typedef {import('../utils').ComponentObjectEmit } ComponentObjectEmit
10
+ * @typedef {import('../utils').ComponentArrayProp } ComponentArrayProp
11
+ * @typedef {import('../utils').ComponentObjectProp } ComponentObjectProp
10
12
* @typedef {import('../utils').VueObjectData } VueObjectData
11
13
*/
12
14
16
18
17
19
const { findVariable } = require ( 'eslint-utils' )
18
20
const utils = require ( '../utils' )
21
+ const { capitalize } = require ( '../utils/casing' )
19
22
20
23
// ------------------------------------------------------------------------------
21
24
// Helpers
@@ -89,7 +92,17 @@ module.exports = {
89
92
url : 'https://eslint.vuejs.org/rules/require-explicit-emits.html'
90
93
} ,
91
94
fixable : null ,
92
- schema : [ ] ,
95
+ schema : [
96
+ {
97
+ type : 'object' ,
98
+ properties : {
99
+ allowProps : {
100
+ type : 'boolean'
101
+ }
102
+ } ,
103
+ additionalProperties : false
104
+ }
105
+ ] ,
93
106
messages : {
94
107
missing :
95
108
'The "{{name}}" event has been triggered but not declared on `emits` option.' ,
@@ -102,49 +115,49 @@ module.exports = {
102
115
} ,
103
116
/** @param {RuleContext } context */
104
117
create ( context ) {
105
- /** @typedef { { node: Literal, name: string } } EmitCellName */
118
+ const options = context . options [ 0 ] || { }
119
+ const allowProps = ! ! options . allowProps
106
120
/** @type {Map<ObjectExpression, { contextReferenceIds: Set<Identifier>, emitReferenceIds: Set<Identifier> }> } */
107
121
const setupContexts = new Map ( )
108
122
/** @type {Map<ObjectExpression, (ComponentArrayEmit | ComponentObjectEmit)[]> } */
109
123
const vueEmitsDeclarations = new Map ( )
110
-
111
- /** @type {EmitCellName[] } */
112
- const templateEmitCellNames = [ ]
113
- /** @type { { type: 'export' | 'mark' | 'definition', object: ObjectExpression, emits: (ComponentArrayEmit | ComponentObjectEmit)[] } | null } */
114
- let vueObjectData = null
124
+ /** @type {Map<ObjectExpression, (ComponentArrayProp | ComponentObjectProp)[]> } */
125
+ const vuePropsDeclarations = new Map ( )
115
126
116
127
/**
117
- * @param {Literal } nameLiteralNode
128
+ * @typedef {object } VueTemplateObjectData
129
+ * @property {'export' | 'mark' | 'definition' } type
130
+ * @property {ObjectExpression } object
131
+ * @property {(ComponentArrayEmit | ComponentObjectEmit)[] } emits
132
+ * @property {(ComponentArrayProp | ComponentObjectProp)[] } props
118
133
*/
119
- function addTemplateEmitCellName ( nameLiteralNode ) {
120
- templateEmitCellNames . push ( {
121
- node : nameLiteralNode ,
122
- name : `${ nameLiteralNode . value } `
123
- } )
124
- }
134
+ /** @type {VueTemplateObjectData | null } */
135
+ let vueTemplateObjectData = null
125
136
126
137
/**
127
- * @param {(ComponentArrayEmit | ComponentObjectEmit)[] } emitsDeclarations
138
+ * @param {(ComponentArrayEmit | ComponentObjectEmit)[] } emits
139
+ * @param {(ComponentArrayProp | ComponentObjectProp)[] } props
128
140
* @param {Literal } nameLiteralNode
129
141
* @param {ObjectExpression } vueObjectNode
130
142
*/
131
- function verify ( emitsDeclarations , nameLiteralNode , vueObjectNode ) {
143
+ function verifyEmit ( emits , props , nameLiteralNode , vueObjectNode ) {
132
144
const name = `${ nameLiteralNode . value } `
133
- if ( emitsDeclarations . some ( ( e ) => e . emitName === name ) ) {
145
+ if ( emits . some ( ( e ) => e . emitName === name ) ) {
134
146
return
135
147
}
148
+ if ( allowProps ) {
149
+ const key = `on${ capitalize ( name ) } `
150
+ if ( props . some ( ( e ) => e . propName === key ) ) {
151
+ return
152
+ }
153
+ }
136
154
context . report ( {
137
155
node : nameLiteralNode ,
138
156
messageId : 'missing' ,
139
157
data : {
140
158
name
141
159
} ,
142
- suggest : buildSuggest (
143
- vueObjectNode ,
144
- emitsDeclarations ,
145
- nameLiteralNode ,
146
- context
147
- )
160
+ suggest : buildSuggest ( vueObjectNode , emits , nameLiteralNode , context )
148
161
} )
149
162
}
150
163
@@ -153,47 +166,31 @@ module.exports = {
153
166
{
154
167
/** @param { CallExpression & { argument: [Literal, ...Expression] } } node */
155
168
'CallExpression[arguments.0.type=Literal]' ( node ) {
156
- const callee = node . callee
169
+ const callee = utils . skipChainExpression ( node . callee )
157
170
const nameLiteralNode = /** @type {Literal } */ ( node . arguments [ 0 ] )
158
171
if ( ! nameLiteralNode || typeof nameLiteralNode . value !== 'string' ) {
159
172
// cannot check
160
173
return
161
174
}
162
- if ( callee . type === 'Identifier' && callee . name === '$emit' ) {
163
- addTemplateEmitCellName ( nameLiteralNode )
164
- }
165
- } ,
166
- "VElement[parent.type!='VElement']:exit" ( ) {
167
- if ( ! vueObjectData ) {
175
+ if ( ! vueTemplateObjectData ) {
168
176
return
169
177
}
170
- const emitsDeclarationNames = new Set (
171
- vueObjectData . emits . map ( ( e ) => e . emitName )
172
- )
173
-
174
- for ( const { name, node } of templateEmitCellNames ) {
175
- if ( emitsDeclarationNames . has ( name ) ) {
176
- continue
177
- }
178
- context . report ( {
179
- node,
180
- messageId : 'missing' ,
181
- data : {
182
- name
183
- } ,
184
- suggest : buildSuggest (
185
- vueObjectData . object ,
186
- vueObjectData . emits ,
187
- node ,
188
- context
189
- )
190
- } )
178
+ if ( callee . type === 'Identifier' && callee . name === '$emit' ) {
179
+ verifyEmit (
180
+ vueTemplateObjectData . emits ,
181
+ vueTemplateObjectData . props ,
182
+ nameLiteralNode ,
183
+ vueTemplateObjectData . object
184
+ )
191
185
}
192
186
}
193
187
} ,
194
188
utils . defineVueVisitor ( context , {
195
189
onVueObjectEnter ( node ) {
196
190
vueEmitsDeclarations . set ( node , utils . getComponentEmits ( node ) )
191
+ if ( allowProps ) {
192
+ vuePropsDeclarations . set ( node , utils . getComponentProps ( node ) )
193
+ }
197
194
} ,
198
195
onSetupFunctionEnter ( node , { node : vueNode } ) {
199
196
const contextParam = node . params [ 1 ]
@@ -286,15 +283,25 @@ module.exports = {
286
283
const { contextReferenceIds, emitReferenceIds } = setupContext
287
284
if ( callee . type === 'Identifier' && emitReferenceIds . has ( callee ) ) {
288
285
// verify setup(props,{emit}) {emit()}
289
- verify ( emitsDeclarations , nameLiteralNode , vueNode )
286
+ verifyEmit (
287
+ emitsDeclarations ,
288
+ vuePropsDeclarations . get ( vueNode ) || [ ] ,
289
+ nameLiteralNode ,
290
+ vueNode
291
+ )
290
292
} else if ( emit && emit . name === 'emit' ) {
291
293
const memObject = utils . skipChainExpression ( emit . member . object )
292
294
if (
293
295
memObject . type === 'Identifier' &&
294
296
contextReferenceIds . has ( memObject )
295
297
) {
296
298
// verify setup(props,context) {context.emit()}
297
- verify ( emitsDeclarations , nameLiteralNode , vueNode )
299
+ verifyEmit (
300
+ emitsDeclarations ,
301
+ vuePropsDeclarations . get ( vueNode ) || [ ] ,
302
+ nameLiteralNode ,
303
+ vueNode
304
+ )
298
305
}
299
306
}
300
307
}
@@ -304,26 +311,36 @@ module.exports = {
304
311
const memObject = utils . skipChainExpression ( emit . member . object )
305
312
if ( utils . isThis ( memObject , context ) ) {
306
313
// verify this.$emit()
307
- verify ( emitsDeclarations , nameLiteralNode , vueNode )
314
+ verifyEmit (
315
+ emitsDeclarations ,
316
+ vuePropsDeclarations . get ( vueNode ) || [ ] ,
317
+ nameLiteralNode ,
318
+ vueNode
319
+ )
308
320
}
309
321
}
310
322
} ,
311
323
onVueObjectExit ( node , { type } ) {
312
324
const emits = vueEmitsDeclarations . get ( node )
313
- if ( ! vueObjectData || vueObjectData . type !== 'export' ) {
325
+ if (
326
+ ! vueTemplateObjectData ||
327
+ vueTemplateObjectData . type !== 'export'
328
+ ) {
314
329
if (
315
330
emits &&
316
331
( type === 'mark' || type === 'export' || type === 'definition' )
317
332
) {
318
- vueObjectData = {
333
+ vueTemplateObjectData = {
319
334
type,
320
335
object : node ,
321
- emits
336
+ emits,
337
+ props : vuePropsDeclarations . get ( node ) || [ ]
322
338
}
323
339
}
324
340
}
325
341
setupContexts . delete ( node )
326
342
vueEmitsDeclarations . delete ( node )
343
+ vuePropsDeclarations . delete ( node )
327
344
}
328
345
} )
329
346
)
0 commit comments