77/**
88 * @typedef {import('../utils').ComponentArrayEmit } ComponentArrayEmit
99 * @typedef {import('../utils').ComponentObjectEmit } ComponentObjectEmit
10+ * @typedef {import('../utils').ComponentArrayProp } ComponentArrayProp
11+ * @typedef {import('../utils').ComponentObjectProp } ComponentObjectProp
1012 * @typedef {import('../utils').VueObjectData } VueObjectData
1113 */
1214
1618
1719const { findVariable } = require ( 'eslint-utils' )
1820const utils = require ( '../utils' )
21+ const { capitalize } = require ( '../utils/casing' )
1922
2023// ------------------------------------------------------------------------------
2124// Helpers
@@ -89,7 +92,17 @@ module.exports = {
8992 url : 'https://eslint.vuejs.org/rules/require-explicit-emits.html'
9093 } ,
9194 fixable : null ,
92- schema : [ ] ,
95+ schema : [
96+ {
97+ type : 'object' ,
98+ properties : {
99+ allowProps : {
100+ type : 'boolean'
101+ }
102+ } ,
103+ additionalProperties : false
104+ }
105+ ] ,
93106 messages : {
94107 missing :
95108 'The "{{name}}" event has been triggered but not declared on `emits` option.' ,
@@ -102,49 +115,49 @@ module.exports = {
102115 } ,
103116 /** @param {RuleContext } context */
104117 create ( context ) {
105- /** @typedef { { node: Literal, name: string } } EmitCellName */
118+ const options = context . options [ 0 ] || { }
119+ const allowProps = ! ! options . allowProps
106120 /** @type {Map<ObjectExpression, { contextReferenceIds: Set<Identifier>, emitReferenceIds: Set<Identifier> }> } */
107121 const setupContexts = new Map ( )
108122 /** @type {Map<ObjectExpression, (ComponentArrayEmit | ComponentObjectEmit)[]> } */
109123 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 ( )
115126
116127 /**
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
118133 */
119- function addTemplateEmitCellName ( nameLiteralNode ) {
120- templateEmitCellNames . push ( {
121- node : nameLiteralNode ,
122- name : `${ nameLiteralNode . value } `
123- } )
124- }
134+ /** @type {VueTemplateObjectData | null } */
135+ let vueTemplateObjectData = null
125136
126137 /**
127- * @param {(ComponentArrayEmit | ComponentObjectEmit)[] } emitsDeclarations
138+ * @param {(ComponentArrayEmit | ComponentObjectEmit)[] } emits
139+ * @param {(ComponentArrayProp | ComponentObjectProp)[] } props
128140 * @param {Literal } nameLiteralNode
129141 * @param {ObjectExpression } vueObjectNode
130142 */
131- function verify ( emitsDeclarations , nameLiteralNode , vueObjectNode ) {
143+ function verifyEmit ( emits , props , nameLiteralNode , vueObjectNode ) {
132144 const name = `${ nameLiteralNode . value } `
133- if ( emitsDeclarations . some ( ( e ) => e . emitName === name ) ) {
145+ if ( emits . some ( ( e ) => e . emitName === name ) ) {
134146 return
135147 }
148+ if ( allowProps ) {
149+ const key = `on${ capitalize ( name ) } `
150+ if ( props . some ( ( e ) => e . propName === key ) ) {
151+ return
152+ }
153+ }
136154 context . report ( {
137155 node : nameLiteralNode ,
138156 messageId : 'missing' ,
139157 data : {
140158 name
141159 } ,
142- suggest : buildSuggest (
143- vueObjectNode ,
144- emitsDeclarations ,
145- nameLiteralNode ,
146- context
147- )
160+ suggest : buildSuggest ( vueObjectNode , emits , nameLiteralNode , context )
148161 } )
149162 }
150163
@@ -153,47 +166,31 @@ module.exports = {
153166 {
154167 /** @param { CallExpression & { argument: [Literal, ...Expression] } } node */
155168 'CallExpression[arguments.0.type=Literal]' ( node ) {
156- const callee = node . callee
169+ const callee = utils . skipChainExpression ( node . callee )
157170 const nameLiteralNode = /** @type {Literal } */ ( node . arguments [ 0 ] )
158171 if ( ! nameLiteralNode || typeof nameLiteralNode . value !== 'string' ) {
159172 // cannot check
160173 return
161174 }
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 ) {
168176 return
169177 }
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+ )
191185 }
192186 }
193187 } ,
194188 utils . defineVueVisitor ( context , {
195189 onVueObjectEnter ( node ) {
196190 vueEmitsDeclarations . set ( node , utils . getComponentEmits ( node ) )
191+ if ( allowProps ) {
192+ vuePropsDeclarations . set ( node , utils . getComponentProps ( node ) )
193+ }
197194 } ,
198195 onSetupFunctionEnter ( node , { node : vueNode } ) {
199196 const contextParam = node . params [ 1 ]
@@ -286,15 +283,25 @@ module.exports = {
286283 const { contextReferenceIds, emitReferenceIds } = setupContext
287284 if ( callee . type === 'Identifier' && emitReferenceIds . has ( callee ) ) {
288285 // verify setup(props,{emit}) {emit()}
289- verify ( emitsDeclarations , nameLiteralNode , vueNode )
286+ verifyEmit (
287+ emitsDeclarations ,
288+ vuePropsDeclarations . get ( vueNode ) || [ ] ,
289+ nameLiteralNode ,
290+ vueNode
291+ )
290292 } else if ( emit && emit . name === 'emit' ) {
291293 const memObject = utils . skipChainExpression ( emit . member . object )
292294 if (
293295 memObject . type === 'Identifier' &&
294296 contextReferenceIds . has ( memObject )
295297 ) {
296298 // verify setup(props,context) {context.emit()}
297- verify ( emitsDeclarations , nameLiteralNode , vueNode )
299+ verifyEmit (
300+ emitsDeclarations ,
301+ vuePropsDeclarations . get ( vueNode ) || [ ] ,
302+ nameLiteralNode ,
303+ vueNode
304+ )
298305 }
299306 }
300307 }
@@ -304,26 +311,36 @@ module.exports = {
304311 const memObject = utils . skipChainExpression ( emit . member . object )
305312 if ( utils . isThis ( memObject , context ) ) {
306313 // verify this.$emit()
307- verify ( emitsDeclarations , nameLiteralNode , vueNode )
314+ verifyEmit (
315+ emitsDeclarations ,
316+ vuePropsDeclarations . get ( vueNode ) || [ ] ,
317+ nameLiteralNode ,
318+ vueNode
319+ )
308320 }
309321 }
310322 } ,
311323 onVueObjectExit ( node , { type } ) {
312324 const emits = vueEmitsDeclarations . get ( node )
313- if ( ! vueObjectData || vueObjectData . type !== 'export' ) {
325+ if (
326+ ! vueTemplateObjectData ||
327+ vueTemplateObjectData . type !== 'export'
328+ ) {
314329 if (
315330 emits &&
316331 ( type === 'mark' || type === 'export' || type === 'definition' )
317332 ) {
318- vueObjectData = {
333+ vueTemplateObjectData = {
319334 type,
320335 object : node ,
321- emits
336+ emits,
337+ props : vuePropsDeclarations . get ( node ) || [ ]
322338 }
323339 }
324340 }
325341 setupContexts . delete ( node )
326342 vueEmitsDeclarations . delete ( node )
343+ vuePropsDeclarations . delete ( node )
327344 }
328345 } )
329346 )
0 commit comments