Skip to content

Commit 872c0b8

Browse files
authored
Add allowProps option to vue/require-explicit-emits rule. (#1259)
Even if you declare it in props, a warning message and fallthrough can be stopped, so I add an option to allow this. https://github.com/vuejs/vue-next/blob/00ab9e2e8506d108958895cda4e977dfb16b53f9/packages/runtime-core/src/componentEmits.ts#L50 By default this option remains disabled.
1 parent 0a6f0f2 commit 872c0b8

File tree

6 files changed

+168
-73
lines changed

6 files changed

+168
-73
lines changed

docs/rules/require-explicit-emits.md

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,37 @@ export default {
7474

7575
## :wrench: Options
7676

77-
Nothing.
77+
```json
78+
{
79+
"vue/require-explicit-emits": ["error", {
80+
"allowProps": false
81+
}]
82+
}
83+
```
84+
85+
- `"allowProps"` ... If `true`, allow event names defined in `props`. default `false`
86+
87+
### `"allowProps": true`
88+
89+
<eslint-code-block fix :rules="{'vue/require-explicit-emits': ['error', {allowProps: true}]}">
90+
91+
```vue
92+
<script>
93+
export default {
94+
props: ['onGood', 'bad'],
95+
methods: {
96+
foo () {
97+
// ✓ GOOD
98+
this.$emit('good')
99+
// ✗ BAD
100+
this.$emit('bad')
101+
}
102+
}
103+
}
104+
</script>
105+
```
106+
107+
</eslint-code-block>
78108

79109
## :books: Further Reading
80110

lib/rules/no-reserved-component-names.js

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -37,14 +37,10 @@ const vue3BuiltInComponents = ['teleport', 'suspense']
3737
function isLowercase(word) {
3838
return /^[a-z]*$/.test(word)
3939
}
40-
/** @param {string} word */
41-
function capitalizeFirstLetter(word) {
42-
return word[0].toUpperCase() + word.substring(1, word.length)
43-
}
4440

4541
const RESERVED_NAMES_IN_HTML = new Set([
4642
...htmlElements,
47-
...htmlElements.map(capitalizeFirstLetter)
43+
...htmlElements.map(casing.capitalize)
4844
])
4945
const RESERVED_NAMES_IN_VUE = new Set([
5046
...vueBuiltInComponents,
@@ -57,11 +53,11 @@ const RESERVED_NAMES_IN_VUE3 = new Set([
5753
])
5854
const RESERVED_NAMES_IN_OTHERS = new Set([
5955
...deprecatedHtmlElements,
60-
...deprecatedHtmlElements.map(capitalizeFirstLetter),
56+
...deprecatedHtmlElements.map(casing.capitalize),
6157
...kebabCaseElements,
6258
...kebabCaseElements.map(casing.pascalCase),
6359
...svgElements,
64-
...svgElements.filter(isLowercase).map(capitalizeFirstLetter)
60+
...svgElements.filter(isLowercase).map(casing.capitalize)
6561
])
6662

6763
// ------------------------------------------------------------------------------

lib/rules/require-explicit-emits.js

Lines changed: 74 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
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

@@ -16,6 +18,7 @@
1618

1719
const { findVariable } = require('eslint-utils')
1820
const 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
)

lib/rules/require-valid-default-prop.js

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
*/
55
'use strict'
66
const utils = require('../utils')
7+
const { capitalize } = require('../utils/casing')
78

89
/**
910
* @typedef {import('../utils').ComponentObjectProp} ComponentObjectProp
@@ -68,13 +69,6 @@ function getTypes(node) {
6869
return []
6970
}
7071

71-
/**
72-
* @param {string} text
73-
*/
74-
function capitalize(text) {
75-
return text[0].toUpperCase() + text.slice(1)
76-
}
77-
7872
// ------------------------------------------------------------------------------
7973
// Rule Definition
8074
// ------------------------------------------------------------------------------

lib/utils/casing.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,5 +197,7 @@ module.exports = {
197197
isCamelCase,
198198
isPascalCase,
199199
isKebabCase,
200-
isSnakeCase
200+
isSnakeCase,
201+
202+
capitalize
201203
}

0 commit comments

Comments
 (0)