From 9fd6139acfafa73945bbb0777ebcc7d7bf35e2ea Mon Sep 17 00:00:00 2001 From: ota-meshi Date: Fri, 25 Dec 2020 21:21:09 +0900 Subject: [PATCH] Add vue/v-on-event-hyphenation rule --- docs/rules/README.md | 1 + docs/rules/attribute-hyphenation.md | 3 + docs/rules/v-on-event-hyphenation.md | 108 +++++++++++++++++++++ lib/index.js | 1 + lib/rules/attribute-hyphenation.js | 2 +- lib/rules/v-on-event-hyphenation.js | 113 ++++++++++++++++++++++ tests/lib/rules/v-on-event-hyphenation.js | 107 ++++++++++++++++++++ 7 files changed, 334 insertions(+), 1 deletion(-) create mode 100644 docs/rules/v-on-event-hyphenation.md create mode 100644 lib/rules/v-on-event-hyphenation.js create mode 100644 tests/lib/rules/v-on-event-hyphenation.js 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."] + } + ] +})