diff --git a/docs/rules/README.md b/docs/rules/README.md
index fe113336e..83ed350d1 100644
--- a/docs/rules/README.md
+++ b/docs/rules/README.md
@@ -301,6 +301,7 @@ For example:
| [vue/no-potential-component-option-typo](./no-potential-component-option-typo.md) | disallow a potential typo in your component property | |
| [vue/no-reserved-component-names](./no-reserved-component-names.md) | disallow the use of reserved names in component definitions | |
| [vue/no-restricted-component-options](./no-restricted-component-options.md) | disallow specific component option | |
+| [vue/no-restricted-custom-event](./no-restricted-custom-event.md) | disallow specific custom event | |
| [vue/no-restricted-static-attribute](./no-restricted-static-attribute.md) | disallow specific attribute | |
| [vue/no-restricted-v-bind](./no-restricted-v-bind.md) | disallow specific argument in `v-bind` | |
| [vue/no-static-inline-styles](./no-static-inline-styles.md) | disallow static inline `style` attributes | |
diff --git a/docs/rules/no-restricted-custom-event.md b/docs/rules/no-restricted-custom-event.md
new file mode 100644
index 000000000..73cf09679
--- /dev/null
+++ b/docs/rules/no-restricted-custom-event.md
@@ -0,0 +1,97 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/no-restricted-custom-event
+description: disallow specific custom event
+---
+# vue/no-restricted-custom-event
+> disallow specific custom event
+
+## :book: Rule Details
+
+This rule allows you to specify custom event that you don't want to use in your application.
+
+## :wrench: Options
+
+This rule takes a list of strings, where each string is a custom event name or pattern to be restricted:
+
+```json
+{
+ "vue/no-restricted-custom-event": ["error", "input", "/^forbidden/"]
+}
+```
+
+
+
+```vue
+
+
+
+
+
+
+
+```
+
+
+
+
+Alternatively, the rule also accepts objects.
+
+```json
+{
+ "vue/no-restricted-custom-event": ["error",
+ {
+ "event": "input",
+ "message": "If you intend a prop for v-model, it should be 'update:modelValue' in Vue 3.",
+ "suggest": "update:modelValue"
+ },
+ ]
+}
+```
+
+The following properties can be specified for the object.
+
+- `event` ... Specify the event name or pattern.
+- `message` ... Specify an optional custom message.
+- `suggest` ... Specify an optional name to suggest changes.
+
+
+
+```vue
+
+
+
+
+
+```
+
+
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-restricted-custom-event.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-restricted-custom-event.js)
diff --git a/lib/index.js b/lib/index.js
index 7755e9f1e..ab6549408 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -91,6 +91,7 @@ module.exports = {
'no-reserved-component-names': require('./rules/no-reserved-component-names'),
'no-reserved-keys': require('./rules/no-reserved-keys'),
'no-restricted-component-options': require('./rules/no-restricted-component-options'),
+ 'no-restricted-custom-event': require('./rules/no-restricted-custom-event'),
'no-restricted-static-attribute': require('./rules/no-restricted-static-attribute'),
'no-restricted-syntax': require('./rules/no-restricted-syntax'),
'no-restricted-v-bind': require('./rules/no-restricted-v-bind'),
diff --git a/lib/rules/no-restricted-custom-event.js b/lib/rules/no-restricted-custom-event.js
new file mode 100644
index 000000000..72835ddd0
--- /dev/null
+++ b/lib/rules/no-restricted-custom-event.js
@@ -0,0 +1,293 @@
+/**
+ * @author Yosuke Ota
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const { findVariable } = require('eslint-utils')
+const utils = require('../utils')
+const regexp = require('../utils/regexp')
+
+// ------------------------------------------------------------------------------
+// Helpers
+// ------------------------------------------------------------------------------
+
+/**
+ * @typedef {object} ParsedOption
+ * @property { (name: string) => boolean } test
+ * @property {string} [message]
+ * @property {string} [suggest]
+ */
+
+/**
+ * @param {string} str
+ * @returns {(str: string) => boolean}
+ */
+function buildMatcher(str) {
+ if (regexp.isRegExp(str)) {
+ const re = regexp.toRegExp(str)
+ return (s) => {
+ re.lastIndex = 0
+ return re.test(s)
+ }
+ }
+ return (s) => s === str
+}
+/**
+ * @param {string|{event: string, message?: string, suggest?: string}} option
+ * @returns {ParsedOption}
+ */
+function parseOption(option) {
+ if (typeof option === 'string') {
+ const matcher = buildMatcher(option)
+ return {
+ test(name) {
+ return matcher(name)
+ }
+ }
+ }
+ const parsed = parseOption(option.event)
+ parsed.message = option.message
+ parsed.suggest = option.suggest
+ return parsed
+}
+
+/**
+ * Get the name param node from the given CallExpression
+ * @param {CallExpression} node CallExpression
+ * @returns { Literal & { value: string } | null }
+ */
+function getNameParamNode(node) {
+ const nameLiteralNode = node.arguments[0]
+ if (
+ !nameLiteralNode ||
+ nameLiteralNode.type !== 'Literal' ||
+ typeof nameLiteralNode.value !== 'string'
+ ) {
+ // cannot check
+ return null
+ }
+
+ return /** @type {Literal & { value: string }} */ (nameLiteralNode)
+}
+/**
+ * Get the callee member node from the given CallExpression
+ * @param {CallExpression} node CallExpression
+ */
+function getCalleeMemberNode(node) {
+ const callee = utils.skipChainExpression(node.callee)
+
+ if (callee.type === 'MemberExpression') {
+ const name = utils.getStaticPropertyName(callee)
+ if (name) {
+ return { name, member: callee }
+ }
+ }
+ return null
+}
+module.exports = {
+ meta: {
+ type: 'suggestion',
+ docs: {
+ description: 'disallow specific custom event',
+ categories: undefined,
+ url: 'https://eslint.vuejs.org/rules/no-restricted-custom-event.html'
+ },
+ fixable: null,
+ schema: {
+ type: 'array',
+ items: {
+ oneOf: [
+ { type: ['string'] },
+ {
+ type: 'object',
+ properties: {
+ event: { type: 'string' },
+ message: { type: 'string', minLength: 1 },
+ suggest: { type: 'string' }
+ },
+ required: ['event'],
+ additionalProperties: false
+ }
+ ]
+ },
+ uniqueItems: true,
+ minItems: 0
+ },
+
+ messages: {
+ // eslint-disable-next-line eslint-plugin/report-message-format
+ restrictedEvent: '{{message}}',
+ instead: 'Instead, change to `{{suggest}}`.'
+ }
+ },
+ /** @param {RuleContext} context */
+ create(context) {
+ /** @type {Map,emitReferenceIds:Set}>} */
+ const setupContexts = new Map()
+ /** @type {ParsedOption[]} */
+ const options = context.options.map(parseOption)
+
+ /**
+ * @param { Literal & { value: string } } nameLiteralNode
+ */
+ function verify(nameLiteralNode) {
+ const name = nameLiteralNode.value
+
+ for (const option of options) {
+ if (option.test(name)) {
+ const message =
+ option.message || `Using \`${name}\` event is not allowed.`
+ context.report({
+ node: nameLiteralNode,
+ messageId: 'restrictedEvent',
+ data: { message },
+ suggest: option.suggest
+ ? [
+ {
+ fix(fixer) {
+ const sourceCode = context.getSourceCode()
+ return fixer.replaceText(
+ nameLiteralNode,
+ `${
+ sourceCode.text[nameLiteralNode.range[0]]
+ }${JSON.stringify(option.suggest)
+ .slice(1, -1)
+ .replace(/'/gu, "\\'")}${
+ sourceCode.text[nameLiteralNode.range[1] - 1]
+ }`
+ )
+ },
+ messageId: 'instead',
+ data: { suggest: option.suggest }
+ }
+ ]
+ : []
+ })
+ break
+ }
+ }
+ }
+
+ return utils.defineTemplateBodyVisitor(
+ context,
+ {
+ CallExpression(node) {
+ const callee = node.callee
+ const nameLiteralNode = getNameParamNode(node)
+ if (!nameLiteralNode) {
+ // cannot check
+ return
+ }
+ if (callee.type === 'Identifier' && callee.name === '$emit') {
+ verify(nameLiteralNode)
+ }
+ }
+ },
+ utils.compositingVisitors(
+ utils.defineVueVisitor(context, {
+ onSetupFunctionEnter(node, { node: vueNode }) {
+ const contextParam = utils.skipDefaultParamValue(node.params[1])
+ if (!contextParam) {
+ // no arguments
+ return
+ }
+ if (
+ contextParam.type === 'RestElement' ||
+ contextParam.type === 'ArrayPattern'
+ ) {
+ // cannot check
+ return
+ }
+ const contextReferenceIds = new Set()
+ const emitReferenceIds = new Set()
+ if (contextParam.type === 'ObjectPattern') {
+ const emitProperty = utils.findAssignmentProperty(
+ contextParam,
+ 'emit'
+ )
+ if (!emitProperty || emitProperty.value.type !== 'Identifier') {
+ return
+ }
+ const emitParam = emitProperty.value
+ // `setup(props, {emit})`
+ const variable = findVariable(context.getScope(), emitParam)
+ if (!variable) {
+ return
+ }
+ for (const reference of variable.references) {
+ emitReferenceIds.add(reference.identifier)
+ }
+ } else {
+ // `setup(props, context)`
+ const variable = findVariable(context.getScope(), contextParam)
+ if (!variable) {
+ return
+ }
+ for (const reference of variable.references) {
+ contextReferenceIds.add(reference.identifier)
+ }
+ }
+ setupContexts.set(vueNode, {
+ contextReferenceIds,
+ emitReferenceIds
+ })
+ },
+ CallExpression(node, { node: vueNode }) {
+ const nameLiteralNode = getNameParamNode(node)
+ if (!nameLiteralNode) {
+ // cannot check
+ return
+ }
+
+ // verify setup context
+ const setupContext = setupContexts.get(vueNode)
+ if (setupContext) {
+ const { contextReferenceIds, emitReferenceIds } = setupContext
+ if (
+ node.callee.type === 'Identifier' &&
+ emitReferenceIds.has(node.callee)
+ ) {
+ // verify setup(props,{emit}) {emit()}
+ verify(nameLiteralNode)
+ } else {
+ const emit = getCalleeMemberNode(node)
+ if (
+ emit &&
+ emit.name === 'emit' &&
+ emit.member.object.type === 'Identifier' &&
+ contextReferenceIds.has(emit.member.object)
+ ) {
+ // verify setup(props,context) {context.emit()}
+ verify(nameLiteralNode)
+ }
+ }
+ }
+ },
+ onVueObjectExit(node) {
+ setupContexts.delete(node)
+ }
+ }),
+ {
+ CallExpression(node) {
+ const nameLiteralNode = getNameParamNode(node)
+ if (!nameLiteralNode) {
+ // cannot check
+ return
+ }
+ const emit = getCalleeMemberNode(node)
+ // verify $emit
+ if (emit && emit.name === '$emit') {
+ // verify this.$emit()
+ verify(nameLiteralNode)
+ }
+ }
+ }
+ )
+ )
+ }
+}
diff --git a/tests/lib/rules/no-restricted-custom-event.js b/tests/lib/rules/no-restricted-custom-event.js
new file mode 100644
index 000000000..447f8122c
--- /dev/null
+++ b/tests/lib/rules/no-restricted-custom-event.js
@@ -0,0 +1,296 @@
+/**
+ * @author Yosuke Ota
+ */
+'use strict'
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const RuleTester = require('eslint').RuleTester
+const rule = require('../../../lib/rules/no-restricted-custom-event')
+
+// ------------------------------------------------------------------------------
+// Tests
+// ------------------------------------------------------------------------------
+
+const tester = new RuleTester({
+ parser: require.resolve('vue-eslint-parser'),
+ parserOptions: { ecmaVersion: 2020, sourceType: 'module' }
+})
+
+tester.run('no-restricted-custom-event', rule, {
+ valid: [
+ {
+ filename: 'test.vue',
+ code: `
+
+
+
+
+ `
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+
+
+
+ `,
+ options: ['bad']
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+
+
+
+
+ `,
+ options: ['ignore', '0']
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ options: ['ignore', '0']
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ options: ['ignore', '0']
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ options: ['ignore', '0']
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ options: ['ignore', '0']
+ }
+ ],
+ invalid: [
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ options: ['bad'],
+ errors: [
+ {
+ message: 'Using `bad` event is not allowed.',
+ line: 7,
+ column: 24
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ options: [
+ { event: 'foo' },
+ { event: 'bar', message: 'Use Bar instead', suggest: 'Bar' }
+ ],
+ errors: [
+ {
+ message: 'Using `foo` event is not allowed.',
+ line: 6
+ },
+ {
+ message: 'Use Bar instead',
+ line: 7,
+ suggestions: [
+ {
+ desc: 'Instead, change to `Bar`.',
+ output: `
+
+ `
+ }
+ ]
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ options: ['/^regexp/'],
+ errors: [
+ {
+ message: 'Using `regexp1` event is not allowed.',
+ line: 6
+ },
+ {
+ message: 'Using `regexp2` event is not allowed.',
+ line: 7
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+
+
+ `,
+ options: [{ event: 'bad', suggest: "foo'" }],
+ errors: [
+ {
+ message: 'Using `bad` event is not allowed.',
+ line: 3,
+ suggestions: [
+ {
+ desc: "Instead, change to `foo'`.",
+ output: `
+
+
+
+ `
+ }
+ ]
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ options: ['foo'],
+ errors: [
+ {
+ message: 'Using `foo` event is not allowed.',
+ line: 5
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ options: ['foo'],
+ errors: [
+ {
+ message: 'Using `foo` event is not allowed.',
+ line: 5
+ }
+ ]
+ }
+ ]
+})