diff --git a/docs/rules/require-explicit-emits.md b/docs/rules/require-explicit-emits.md
index 2a165ec19..ba33eadf0 100644
--- a/docs/rules/require-explicit-emits.md
+++ b/docs/rules/require-explicit-emits.md
@@ -74,7 +74,37 @@ export default {
## :wrench: Options
-Nothing.
+```json
+{
+ "vue/require-explicit-emits": ["error", {
+ "allowProps": false
+ }]
+}
+```
+
+- `"allowProps"` ... If `true`, allow event names defined in `props`. default `false`
+
+### `"allowProps": true`
+
+
+
+```vue
+
+```
+
+
## :books: Further Reading
diff --git a/lib/rules/no-reserved-component-names.js b/lib/rules/no-reserved-component-names.js
index e21bf7cd8..aecbe6020 100644
--- a/lib/rules/no-reserved-component-names.js
+++ b/lib/rules/no-reserved-component-names.js
@@ -37,14 +37,10 @@ const vue3BuiltInComponents = ['teleport', 'suspense']
function isLowercase(word) {
return /^[a-z]*$/.test(word)
}
-/** @param {string} word */
-function capitalizeFirstLetter(word) {
- return word[0].toUpperCase() + word.substring(1, word.length)
-}
const RESERVED_NAMES_IN_HTML = new Set([
...htmlElements,
- ...htmlElements.map(capitalizeFirstLetter)
+ ...htmlElements.map(casing.capitalize)
])
const RESERVED_NAMES_IN_VUE = new Set([
...vueBuiltInComponents,
@@ -57,11 +53,11 @@ const RESERVED_NAMES_IN_VUE3 = new Set([
])
const RESERVED_NAMES_IN_OTHERS = new Set([
...deprecatedHtmlElements,
- ...deprecatedHtmlElements.map(capitalizeFirstLetter),
+ ...deprecatedHtmlElements.map(casing.capitalize),
...kebabCaseElements,
...kebabCaseElements.map(casing.pascalCase),
...svgElements,
- ...svgElements.filter(isLowercase).map(capitalizeFirstLetter)
+ ...svgElements.filter(isLowercase).map(casing.capitalize)
])
// ------------------------------------------------------------------------------
diff --git a/lib/rules/require-explicit-emits.js b/lib/rules/require-explicit-emits.js
index fdadc2332..ec356364e 100644
--- a/lib/rules/require-explicit-emits.js
+++ b/lib/rules/require-explicit-emits.js
@@ -7,6 +7,8 @@
/**
* @typedef {import('../utils').ComponentArrayEmit} ComponentArrayEmit
* @typedef {import('../utils').ComponentObjectEmit} ComponentObjectEmit
+ * @typedef {import('../utils').ComponentArrayProp} ComponentArrayProp
+ * @typedef {import('../utils').ComponentObjectProp} ComponentObjectProp
* @typedef {import('../utils').VueObjectData} VueObjectData
*/
@@ -16,6 +18,7 @@
const { findVariable } = require('eslint-utils')
const utils = require('../utils')
+const { capitalize } = require('../utils/casing')
// ------------------------------------------------------------------------------
// Helpers
@@ -89,7 +92,17 @@ module.exports = {
url: 'https://eslint.vuejs.org/rules/require-explicit-emits.html'
},
fixable: null,
- schema: [],
+ schema: [
+ {
+ type: 'object',
+ properties: {
+ allowProps: {
+ type: 'boolean'
+ }
+ },
+ additionalProperties: false
+ }
+ ],
messages: {
missing:
'The "{{name}}" event has been triggered but not declared on `emits` option.',
@@ -102,49 +115,49 @@ module.exports = {
},
/** @param {RuleContext} context */
create(context) {
- /** @typedef { { node: Literal, name: string } } EmitCellName */
+ const options = context.options[0] || {}
+ const allowProps = !!options.allowProps
/** @type {Map, emitReferenceIds: Set }>} */
const setupContexts = new Map()
/** @type {Map} */
const vueEmitsDeclarations = new Map()
-
- /** @type {EmitCellName[]} */
- const templateEmitCellNames = []
- /** @type { { type: 'export' | 'mark' | 'definition', object: ObjectExpression, emits: (ComponentArrayEmit | ComponentObjectEmit)[] } | null } */
- let vueObjectData = null
+ /** @type {Map} */
+ const vuePropsDeclarations = new Map()
/**
- * @param {Literal} nameLiteralNode
+ * @typedef {object} VueTemplateObjectData
+ * @property {'export' | 'mark' | 'definition'} type
+ * @property {ObjectExpression} object
+ * @property {(ComponentArrayEmit | ComponentObjectEmit)[]} emits
+ * @property {(ComponentArrayProp | ComponentObjectProp)[]} props
*/
- function addTemplateEmitCellName(nameLiteralNode) {
- templateEmitCellNames.push({
- node: nameLiteralNode,
- name: `${nameLiteralNode.value}`
- })
- }
+ /** @type {VueTemplateObjectData | null} */
+ let vueTemplateObjectData = null
/**
- * @param {(ComponentArrayEmit | ComponentObjectEmit)[]} emitsDeclarations
+ * @param {(ComponentArrayEmit | ComponentObjectEmit)[]} emits
+ * @param {(ComponentArrayProp | ComponentObjectProp)[]} props
* @param {Literal} nameLiteralNode
* @param {ObjectExpression} vueObjectNode
*/
- function verify(emitsDeclarations, nameLiteralNode, vueObjectNode) {
+ function verifyEmit(emits, props, nameLiteralNode, vueObjectNode) {
const name = `${nameLiteralNode.value}`
- if (emitsDeclarations.some((e) => e.emitName === name)) {
+ if (emits.some((e) => e.emitName === name)) {
return
}
+ if (allowProps) {
+ const key = `on${capitalize(name)}`
+ if (props.some((e) => e.propName === key)) {
+ return
+ }
+ }
context.report({
node: nameLiteralNode,
messageId: 'missing',
data: {
name
},
- suggest: buildSuggest(
- vueObjectNode,
- emitsDeclarations,
- nameLiteralNode,
- context
- )
+ suggest: buildSuggest(vueObjectNode, emits, nameLiteralNode, context)
})
}
@@ -153,47 +166,31 @@ module.exports = {
{
/** @param { CallExpression & { argument: [Literal, ...Expression] } } node */
'CallExpression[arguments.0.type=Literal]'(node) {
- const callee = node.callee
+ const callee = utils.skipChainExpression(node.callee)
const nameLiteralNode = /** @type {Literal} */ (node.arguments[0])
if (!nameLiteralNode || typeof nameLiteralNode.value !== 'string') {
// cannot check
return
}
- if (callee.type === 'Identifier' && callee.name === '$emit') {
- addTemplateEmitCellName(nameLiteralNode)
- }
- },
- "VElement[parent.type!='VElement']:exit"() {
- if (!vueObjectData) {
+ if (!vueTemplateObjectData) {
return
}
- const emitsDeclarationNames = new Set(
- vueObjectData.emits.map((e) => e.emitName)
- )
-
- for (const { name, node } of templateEmitCellNames) {
- if (emitsDeclarationNames.has(name)) {
- continue
- }
- context.report({
- node,
- messageId: 'missing',
- data: {
- name
- },
- suggest: buildSuggest(
- vueObjectData.object,
- vueObjectData.emits,
- node,
- context
- )
- })
+ if (callee.type === 'Identifier' && callee.name === '$emit') {
+ verifyEmit(
+ vueTemplateObjectData.emits,
+ vueTemplateObjectData.props,
+ nameLiteralNode,
+ vueTemplateObjectData.object
+ )
}
}
},
utils.defineVueVisitor(context, {
onVueObjectEnter(node) {
vueEmitsDeclarations.set(node, utils.getComponentEmits(node))
+ if (allowProps) {
+ vuePropsDeclarations.set(node, utils.getComponentProps(node))
+ }
},
onSetupFunctionEnter(node, { node: vueNode }) {
const contextParam = node.params[1]
@@ -286,7 +283,12 @@ module.exports = {
const { contextReferenceIds, emitReferenceIds } = setupContext
if (callee.type === 'Identifier' && emitReferenceIds.has(callee)) {
// verify setup(props,{emit}) {emit()}
- verify(emitsDeclarations, nameLiteralNode, vueNode)
+ verifyEmit(
+ emitsDeclarations,
+ vuePropsDeclarations.get(vueNode) || [],
+ nameLiteralNode,
+ vueNode
+ )
} else if (emit && emit.name === 'emit') {
const memObject = utils.skipChainExpression(emit.member.object)
if (
@@ -294,7 +296,12 @@ module.exports = {
contextReferenceIds.has(memObject)
) {
// verify setup(props,context) {context.emit()}
- verify(emitsDeclarations, nameLiteralNode, vueNode)
+ verifyEmit(
+ emitsDeclarations,
+ vuePropsDeclarations.get(vueNode) || [],
+ nameLiteralNode,
+ vueNode
+ )
}
}
}
@@ -304,26 +311,36 @@ module.exports = {
const memObject = utils.skipChainExpression(emit.member.object)
if (utils.isThis(memObject, context)) {
// verify this.$emit()
- verify(emitsDeclarations, nameLiteralNode, vueNode)
+ verifyEmit(
+ emitsDeclarations,
+ vuePropsDeclarations.get(vueNode) || [],
+ nameLiteralNode,
+ vueNode
+ )
}
}
},
onVueObjectExit(node, { type }) {
const emits = vueEmitsDeclarations.get(node)
- if (!vueObjectData || vueObjectData.type !== 'export') {
+ if (
+ !vueTemplateObjectData ||
+ vueTemplateObjectData.type !== 'export'
+ ) {
if (
emits &&
(type === 'mark' || type === 'export' || type === 'definition')
) {
- vueObjectData = {
+ vueTemplateObjectData = {
type,
object: node,
- emits
+ emits,
+ props: vuePropsDeclarations.get(node) || []
}
}
}
setupContexts.delete(node)
vueEmitsDeclarations.delete(node)
+ vuePropsDeclarations.delete(node)
}
})
)
diff --git a/lib/rules/require-valid-default-prop.js b/lib/rules/require-valid-default-prop.js
index ffebef07f..ef9453b23 100644
--- a/lib/rules/require-valid-default-prop.js
+++ b/lib/rules/require-valid-default-prop.js
@@ -4,6 +4,7 @@
*/
'use strict'
const utils = require('../utils')
+const { capitalize } = require('../utils/casing')
/**
* @typedef {import('../utils').ComponentObjectProp} ComponentObjectProp
@@ -68,13 +69,6 @@ function getTypes(node) {
return []
}
-/**
- * @param {string} text
- */
-function capitalize(text) {
- return text[0].toUpperCase() + text.slice(1)
-}
-
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
diff --git a/lib/utils/casing.js b/lib/utils/casing.js
index f6dea6d6e..b2f2e89b5 100644
--- a/lib/utils/casing.js
+++ b/lib/utils/casing.js
@@ -197,5 +197,7 @@ module.exports = {
isCamelCase,
isPascalCase,
isKebabCase,
- isSnakeCase
+ isSnakeCase,
+
+ capitalize
}
diff --git a/tests/lib/rules/require-explicit-emits.js b/tests/lib/rules/require-explicit-emits.js
index d59d78600..4f27563cd 100644
--- a/tests/lib/rules/require-explicit-emits.js
+++ b/tests/lib/rules/require-explicit-emits.js
@@ -359,6 +359,27 @@ tester.run('require-explicit-emits', rule, {
}
`
+ },
+ // allowProps
+ {
+ filename: 'test.vue',
+ code: `
+
+
+
+
+ `,
+ options: [{ allowProps: true }]
}
],
invalid: [
@@ -1551,6 +1572,41 @@ emits: {'foo': null}
'The "foo" event has been triggered but not declared on `emits` option.',
'The "bar" event has been triggered but not declared on `emits` option.'
]
+ },
+ // allowProps
+ {
+ filename: 'test.vue',
+ code: `
+
+
+
+
+ `,
+ options: [{ allowProps: true }],
+ errors: [
+ {
+ line: 3,
+ messageId: 'missing'
+ },
+ {
+ line: 9,
+ messageId: 'missing'
+ },
+ {
+ line: 12,
+ messageId: 'missing'
+ }
+ ]
}
]
})