diff --git a/docs/rules/README.md b/docs/rules/README.md
index 68677d252..805dbd4ec 100644
--- a/docs/rules/README.md
+++ b/docs/rules/README.md
@@ -320,6 +320,7 @@ For example:
| [vue/sort-keys](./sort-keys.md) | enforce sort-keys in a manner that is compatible with order-in-components | |
| [vue/static-class-names-order](./static-class-names-order.md) | enforce static class names order | :wrench: |
| [vue/v-for-delimiter-style](./v-for-delimiter-style.md) | enforce `v-for` directive's delimiter style | :wrench: |
+| [vue/v-on-event-hyphenation](./v-on-event-hyphenation.md) | enforce v-on event naming style on custom components in template | :wrench: |
| [vue/v-on-function-call](./v-on-function-call.md) | enforce or forbid parentheses after method calls without arguments in `v-on` directives | :wrench: |
### Extension Rules
diff --git a/docs/rules/attribute-hyphenation.md b/docs/rules/attribute-hyphenation.md
index fc7bba756..7dd2feb21 100644
--- a/docs/rules/attribute-hyphenation.md
+++ b/docs/rules/attribute-hyphenation.md
@@ -47,6 +47,7 @@ Default casing is set to `always` with `['data-', 'aria-', 'slot-scope']` set to
- `"ignore"` ... Array of ignored names
### `"always"`
+
It errors on upper case letters.
@@ -64,6 +65,7 @@ It errors on upper case letters.
### `"never"`
+
It errors on hyphens except `data-`, `aria-` and `slot-scope`.
@@ -84,6 +86,7 @@ It errors on hyphens except `data-`, `aria-` and `slot-scope`.
### `"never", { "ignore": ["custom-prop"] }`
+
Don't use hyphenated name but allow custom attributes
diff --git a/docs/rules/v-on-event-hyphenation.md b/docs/rules/v-on-event-hyphenation.md
new file mode 100644
index 000000000..3b94b4529
--- /dev/null
+++ b/docs/rules/v-on-event-hyphenation.md
@@ -0,0 +1,108 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/v-on-event-hyphenation
+description: enforce v-on event naming style on custom components in template
+---
+# vue/v-on-event-hyphenation
+
+> enforce v-on event naming style on custom components in template
+
+- :exclamation: ***This rule has not been released yet.***
+- :wrench: The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) can automatically fix some of the problems reported by this rule.
+
+## :book: Rule Details
+
+This rule enforces using hyphenated v-on event names on custom components in Vue templates.
+
+
+
+```vue
+
+
+
+
+
+
+
+
+
+```
+
+
+
+## :wrench: Options
+
+```json
+{
+ "vue/v-on-event-hyphenation": ["error", "always" | "never", {
+ "autofix": false,
+ "ignore": []
+ }]
+}
+```
+
+- `"always"` (default) ... Use hyphenated name.
+- `"never"` ... Don't use hyphenated name.
+- `"ignore"` ... Array of ignored names
+- `"autofix"` ... If `true`, enable autofix. If you are using Vue 2, we recommend that you do not use it due to its side effects.
+
+### `"always"`
+
+It errors on upper case letters.
+
+
+
+```vue
+
+
+
+
+
+
+
+```
+
+
+
+### `"never"`
+
+It errors on hyphens.
+
+
+
+```vue
+
+
+
+
+
+
+
+```
+
+
+
+### `"never", { "ignore": ["custom-event"] }`
+
+Don't use hyphenated name but allow custom event names
+
+
+
+```vue
+
+
+
+
+
+
+
+
+```
+
+
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/v-on-event-hyphenation.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/v-on-event-hyphenation.js)
diff --git a/lib/index.js b/lib/index.js
index 95ffd1dd6..35a781e65 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -156,6 +156,7 @@ module.exports = {
'use-v-on-exact': require('./rules/use-v-on-exact'),
'v-bind-style': require('./rules/v-bind-style'),
'v-for-delimiter-style': require('./rules/v-for-delimiter-style'),
+ 'v-on-event-hyphenation': require('./rules/v-on-event-hyphenation'),
'v-on-function-call': require('./rules/v-on-function-call'),
'v-on-style': require('./rules/v-on-style'),
'v-slot-style': require('./rules/v-slot-style'),
diff --git a/lib/rules/attribute-hyphenation.js b/lib/rules/attribute-hyphenation.js
index 1fd7b5e9b..cba9dc241 100644
--- a/lib/rules/attribute-hyphenation.js
+++ b/lib/rules/attribute-hyphenation.js
@@ -87,7 +87,7 @@ module.exports = {
*/
function isIgnoredAttribute(value) {
const isIgnored = ignoredAttributes.some((attr) => {
- return value.indexOf(attr) !== -1
+ return value.includes(attr)
})
if (isIgnored) {
diff --git a/lib/rules/v-on-event-hyphenation.js b/lib/rules/v-on-event-hyphenation.js
new file mode 100644
index 000000000..60823e013
--- /dev/null
+++ b/lib/rules/v-on-event-hyphenation.js
@@ -0,0 +1,113 @@
+'use strict'
+
+const utils = require('../utils')
+const casing = require('../utils/casing')
+
+module.exports = {
+ meta: {
+ docs: {
+ description:
+ 'enforce v-on event naming style on custom components in template',
+ // TODO Change with major version.
+ // categories: ['vue3-strongly-recommended'],
+ categories: undefined,
+ url: 'https://eslint.vuejs.org/rules/v-on-event-hyphenation.html'
+ },
+ fixable: 'code',
+ schema: [
+ {
+ enum: ['always', 'never']
+ },
+ {
+ type: 'object',
+ properties: {
+ autofix: { type: 'boolean' },
+ ignore: {
+ type: 'array',
+ items: {
+ allOf: [
+ { type: 'string' },
+ { not: { type: 'string', pattern: ':exit$' } },
+ { not: { type: 'string', pattern: '^\\s*$' } }
+ ]
+ },
+ uniqueItems: true,
+ additionalItems: false
+ }
+ },
+ additionalProperties: false
+ }
+ ],
+ type: 'suggestion'
+ },
+
+ /** @param {RuleContext} context */
+ create(context) {
+ const sourceCode = context.getSourceCode()
+ const option = context.options[0]
+ const optionsPayload = context.options[1]
+ const useHyphenated = option !== 'never'
+ /** @type {string[]} */
+ const ignoredAttributes = (optionsPayload && optionsPayload.ignore) || []
+ const autofix = Boolean(optionsPayload && optionsPayload.autofix)
+
+ const caseConverter = casing.getExactConverter(
+ useHyphenated ? 'kebab-case' : 'camelCase'
+ )
+
+ /**
+ * @param {VDirective} node
+ * @param {string} name
+ */
+ function reportIssue(node, name) {
+ const text = sourceCode.getText(node.key)
+
+ context.report({
+ node: node.key,
+ loc: node.loc,
+ message: useHyphenated
+ ? "v-on event '{{text}}' must be hyphenated."
+ : "v-on event '{{text}}' can't be hyphenated.",
+ data: {
+ text
+ },
+ fix: autofix
+ ? (fixer) =>
+ fixer.replaceText(
+ node.key,
+ text.replace(name, caseConverter(name))
+ )
+ : null
+ })
+ }
+
+ /**
+ * @param {string} value
+ */
+ function isIgnoredAttribute(value) {
+ const isIgnored = ignoredAttributes.some((attr) => {
+ return value.includes(attr)
+ })
+
+ if (isIgnored) {
+ return true
+ }
+
+ return useHyphenated ? value.toLowerCase() === value : !/-/.test(value)
+ }
+
+ return utils.defineTemplateBodyVisitor(context, {
+ "VAttribute[directive=true][key.name.name='on']"(node) {
+ if (!utils.isCustomComponent(node.parent.parent)) return
+
+ const name =
+ node.key.argument &&
+ node.key.argument.type === 'VIdentifier' &&
+ node.key.argument.rawName
+ if (!name || isIgnoredAttribute(name)) return
+
+ reportIssue(node, name)
+ }
+ })
+ }
+}
diff --git a/tests/lib/rules/v-on-event-hyphenation.js b/tests/lib/rules/v-on-event-hyphenation.js
new file mode 100644
index 000000000..c77449971
--- /dev/null
+++ b/tests/lib/rules/v-on-event-hyphenation.js
@@ -0,0 +1,107 @@
+'use strict'
+
+const RuleTester = require('eslint').RuleTester
+const rule = require('../../../lib/rules/v-on-event-hyphenation.js')
+
+const tester = new RuleTester({
+ parser: require.resolve('vue-eslint-parser'),
+ parserOptions: {
+ ecmaVersion: 2019
+ }
+})
+
+tester.run('v-on-event-hyphenation', rule, {
+ valid: [
+ `
+
+
+
+ `,
+ `
+
+
+
+ `,
+ `
+
+
+
+ `,
+ `
+
+
+
+ `,
+ {
+ code: `
+
+
+
+ `,
+ options: ['never']
+ },
+ {
+ code: `
+
+
+
+ `,
+ options: ['never', { ignore: ['custom'] }]
+ }
+ ],
+ invalid: [
+ {
+ code: `
+
+
+
+ `,
+ output: null,
+ errors: [
+ {
+ message: "v-on event '@customEvent' must be hyphenated.",
+ line: 3,
+ column: 25,
+ endLine: 3,
+ endColumn: 47
+ }
+ ]
+ },
+ {
+ code: `
+
+
+
+ `,
+ options: ['always', { autofix: true }],
+ output: `
+
+
+
+ `,
+ errors: [
+ {
+ message: "v-on event '@customEvent' must be hyphenated.",
+ line: 3,
+ column: 25,
+ endLine: 3,
+ endColumn: 47
+ }
+ ]
+ },
+ {
+ code: `
+
+
+
+ `,
+ options: ['never', { autofix: true }],
+ output: `
+
+
+
+ `,
+ errors: ["v-on event 'v-on:custom-event' can't be hyphenated."]
+ }
+ ]
+})