From 6d6203fe85d58003844f84776d60816fbe591ef1 Mon Sep 17 00:00:00 2001 From: Yosuke Ota Date: Fri, 4 Dec 2020 13:08:44 +0900 Subject: [PATCH] Add casing option to vue/custom-event-name-casing rule & remove from configs. (#1364) * Add casing option to vue/custom-event-name-casing rule & remove from configs. * update --- docs/rules/README.md | 3 +- docs/rules/custom-event-name-casing.md | 88 ++++++++++++++++++-- lib/configs/essential.js | 1 - lib/configs/vue3-essential.js | 1 - lib/rules/custom-event-name-casing.js | 92 ++++++++++++++------- tests/lib/rules/custom-event-name-casing.js | 88 +++++++++++++++++++- 6 files changed, 227 insertions(+), 46 deletions(-) diff --git a/docs/rules/README.md b/docs/rules/README.md index f2d9b8476..fe113336e 100644 --- a/docs/rules/README.md +++ b/docs/rules/README.md @@ -39,7 +39,6 @@ Enforce all the rules in this category, as well as all higher priority rules, wi | Rule ID | Description | | |:--------|:------------|:---| -| [vue/custom-event-name-casing](./custom-event-name-casing.md) | enforce custom event names always use "kebab-case" | | | [vue/no-arrow-functions-in-watch](./no-arrow-functions-in-watch.md) | disallow using arrow functions to define watcher | | | [vue/no-async-in-computed-properties](./no-async-in-computed-properties.md) | disallow asynchronous actions in computed properties | | | [vue/no-deprecated-data-object-declaration](./no-deprecated-data-object-declaration.md) | disallow using deprecated object declaration on data (in Vue.js 3.0.0+) | :wrench: | @@ -172,7 +171,6 @@ Enforce all the rules in this category, as well as all higher priority rules, wi | Rule ID | Description | | |:--------|:------------|:---| -| [vue/custom-event-name-casing](./custom-event-name-casing.md) | enforce custom event names always use "kebab-case" | | | [vue/no-arrow-functions-in-watch](./no-arrow-functions-in-watch.md) | disallow using arrow functions to define watcher | | | [vue/no-async-in-computed-properties](./no-async-in-computed-properties.md) | disallow asynchronous actions in computed properties | | | [vue/no-custom-modifiers-on-v-model](./no-custom-modifiers-on-v-model.md) | disallow custom modifiers on v-model used on the component | | @@ -290,6 +288,7 @@ For example: |:--------|:------------|:---| | [vue/block-tag-newline](./block-tag-newline.md) | enforce line breaks after opening and before closing block-level tags | :wrench: | | [vue/component-name-in-template-casing](./component-name-in-template-casing.md) | enforce specific casing for the component naming style in template | :wrench: | +| [vue/custom-event-name-casing](./custom-event-name-casing.md) | enforce specific casing for custom event name | | | [vue/html-comment-content-newline](./html-comment-content-newline.md) | enforce unified line brake in HTML comments | :wrench: | | [vue/html-comment-content-spacing](./html-comment-content-spacing.md) | enforce unified spacing in HTML comments | :wrench: | | [vue/html-comment-indent](./html-comment-indent.md) | enforce consistent indentation in HTML comments | :wrench: | diff --git a/docs/rules/custom-event-name-casing.md b/docs/rules/custom-event-name-casing.md index 6890c771e..3a5f339cf 100644 --- a/docs/rules/custom-event-name-casing.md +++ b/docs/rules/custom-event-name-casing.md @@ -2,22 +2,30 @@ pageClass: rule-details sidebarDepth: 0 title: vue/custom-event-name-casing -description: enforce custom event names always use "kebab-case" +description: enforce specific casing for custom event name --- # vue/custom-event-name-casing -> enforce custom event names always use "kebab-case" +> enforce specific casing for custom event name -- :gear: This rule is included in all of `"plugin:vue/vue3-essential"`, `"plugin:vue/essential"`, `"plugin:vue/vue3-strongly-recommended"`, `"plugin:vue/strongly-recommended"`, `"plugin:vue/vue3-recommended"` and `"plugin:vue/recommended"`. +Define a style for custom event name casing for consistency purposes. ## :book: Rule Details -This rule enforces using kebab-case custom event names. +This rule aims to warn the custom event names other than the configured casing. + +Vue 2 recommends using kebab-case for custom event names. > Event names will never be used as variable or property names in JavaScript, so there’s no reason to use camelCase or PascalCase. Additionally, `v-on` event listeners inside DOM templates will be automatically transformed to lowercase (due to HTML’s case-insensitivity), so `v-on:myEvent` would become `v-on:myevent` – making `myEvent` impossible to listen to. > > For these reasons, we recommend you **always use kebab-case for event names**. -See [Guide - Custom Events] for more details. +See [Guide (for v2) - Custom Events] for more details. + +Vue 3 recommends using camelCase for custom event names. + +See [vuejs/docs-next#656](https://github.com/vuejs/docs-next/issues/656) for more details. + +This rule enforces kebab-case by default. @@ -51,14 +59,77 @@ export default { ```json { - "vue/custom-event-name-casing": ["error", { - "ignores": [] - }] + "vue/custom-event-name-casing": ["error", + "kebab-case" | "camelCase", + { + "ignores": [] + } + ] } ``` +- `"kebab-case"` (default) ... Enforce custom event names to kebab-case. +- `"camelCase"` ... Enforce custom event names to camelCase. - `ignores` (`string[]`) ... The event names to ignore. Sets the event name to allow. For example, custom event names, Vue components event with special name, or Vue library component event name. You can set the regexp by writing it like `"/^name/"` or `click:row` or `fooBar`. +### `"kebab-case"` + + + +```vue + + +``` + + + +### `"camelCase"` + + + +```vue + + +``` + + + ### `"ignores": ["fooBar", "/^[a-z]+(?:-[a-z]+)*:[a-z]+(?:-[a-z]+)*$/u"]` @@ -93,6 +164,7 @@ export default { ## :books: Further Reading - [Guide - Custom Events] +- [Guide (for v2) - Custom Events] [Guide - Custom Events]: https://v3.vuejs.org/guide/component-custom-events.html [Guide (for v2) - Custom Events]: https://vuejs.org/v2/guide/components-custom-events.html diff --git a/lib/configs/essential.js b/lib/configs/essential.js index 7b7a58733..dca31cd2e 100644 --- a/lib/configs/essential.js +++ b/lib/configs/essential.js @@ -6,7 +6,6 @@ module.exports = { extends: require.resolve('./base'), rules: { - 'vue/custom-event-name-casing': 'error', 'vue/no-arrow-functions-in-watch': 'error', 'vue/no-async-in-computed-properties': 'error', 'vue/no-custom-modifiers-on-v-model': 'error', diff --git a/lib/configs/vue3-essential.js b/lib/configs/vue3-essential.js index d1e2d1768..802cb62ed 100644 --- a/lib/configs/vue3-essential.js +++ b/lib/configs/vue3-essential.js @@ -6,7 +6,6 @@ module.exports = { extends: require.resolve('./base'), rules: { - 'vue/custom-event-name-casing': 'error', 'vue/no-arrow-functions-in-watch': 'error', 'vue/no-async-in-computed-properties': 'error', 'vue/no-deprecated-data-object-declaration': 'error', diff --git a/lib/rules/custom-event-name-casing.js b/lib/rules/custom-event-name-casing.js index 7ed9335a2..39d9436e7 100644 --- a/lib/rules/custom-event-name-casing.js +++ b/lib/rules/custom-event-name-casing.js @@ -10,21 +10,15 @@ const { findVariable } = require('eslint-utils') const utils = require('../utils') -const { isKebabCase } = require('../utils/casing') +const casing = require('../utils/casing') const { toRegExp } = require('../utils/regexp') // ------------------------------------------------------------------------------ // Helpers // ------------------------------------------------------------------------------ -/** - * Check whether the given event name is valid. - * @param {string} name The name to check. - * @returns {boolean} `true` if the given event name is valid. - */ -function isValidEventName(name) { - return isKebabCase(name) || name.startsWith('update:') -} +const ALLOWED_CASE_OPTIONS = ['kebab-case', 'camelCase'] +const DEFAULT_CASE = 'kebab-case' /** * Get the name param node from the given CallExpression @@ -64,53 +58,87 @@ function getCalleeMemberNode(node) { // Rule Definition // ------------------------------------------------------------------------------ +const OBJECT_OPTION_SCHEMA = { + type: 'object', + properties: { + ignores: { + type: 'array', + items: { type: 'string' }, + uniqueItems: true, + additionalItems: false + } + }, + additionalProperties: false +} module.exports = { meta: { type: 'suggestion', docs: { - description: 'enforce custom event names always use "kebab-case"', - categories: ['vue3-essential', 'essential'], + description: 'enforce specific casing for custom event name', + categories: undefined, url: 'https://eslint.vuejs.org/rules/custom-event-name-casing.html' }, fixable: null, - schema: [ - { - type: 'object', - properties: { - ignores: { - type: 'array', - items: { type: 'string' }, - uniqueItems: true, - additionalItems: false - } + schema: { + anyOf: [ + { + type: 'array', + items: [ + { + enum: ALLOWED_CASE_OPTIONS + }, + OBJECT_OPTION_SCHEMA + ] }, - additionalProperties: false - } - ], + // For backward compatibility + { + type: 'array', + items: [OBJECT_OPTION_SCHEMA] + } + ] + }, messages: { - unexpected: "Custom event name '{{name}}' must be kebab-case." + unexpected: "Custom event name '{{name}}' must be {{caseType}}." } }, /** @param {RuleContext} context */ create(context) { + /** @type {Map,emitReferenceIds:Set}>} */ const setupContexts = new Map() - const options = context.options[0] || {} + const options = + context.options.length === 1 && typeof context.options[0] !== 'string' + ? // For backward compatibility + [undefined, context.options[0]] + : context.options + const caseType = options[0] || DEFAULT_CASE + const objectOption = options[1] || {} + const caseChecker = casing.getChecker(caseType) /** @type {RegExp[]} */ - const ignores = (options.ignores || []).map(toRegExp) + const ignores = (objectOption.ignores || []).map(toRegExp) + + /** + * Check whether the given event name is valid. + * @param {string} name The name to check. + * @returns {boolean} `true` if the given event name is valid. + */ + function isValidEventName(name) { + return caseChecker(name) || name.startsWith('update:') + } /** * @param { Literal & { value: string } } nameLiteralNode */ function verify(nameLiteralNode) { const name = nameLiteralNode.value - if (ignores.some((re) => re.test(name)) || isValidEventName(name)) { + if (isValidEventName(name) || ignores.some((re) => re.test(name))) { return } context.report({ node: nameLiteralNode, messageId: 'unexpected', data: { - name + name, + caseType } }) } @@ -190,7 +218,10 @@ module.exports = { const setupContext = setupContexts.get(vueNode) if (setupContext) { const { contextReferenceIds, emitReferenceIds } = setupContext - if (emitReferenceIds.has(node.callee)) { + if ( + node.callee.type === 'Identifier' && + emitReferenceIds.has(node.callee) + ) { // verify setup(props,{emit}) {emit()} verify(nameLiteralNode) } else { @@ -198,6 +229,7 @@ module.exports = { if ( emit && emit.name === 'emit' && + emit.member.object.type === 'Identifier' && contextReferenceIds.has(emit.member.object) ) { // verify setup(props,context) {context.emit()} diff --git a/tests/lib/rules/custom-event-name-casing.js b/tests/lib/rules/custom-event-name-casing.js index ed0ba0eb5..cd857b4ce 100644 --- a/tests/lib/rules/custom-event-name-casing.js +++ b/tests/lib/rules/custom-event-name-casing.js @@ -191,7 +191,7 @@ tester.run('custom-event-name-casing', rule, { } `, - options: [{ ignores: ['fooBar', 'barBaz', 'bazQux'] }] + options: ['kebab-case', { ignores: ['fooBar', 'barBaz', 'bazQux'] }] }, { filename: 'test.vue', @@ -225,7 +225,50 @@ tester.run('custom-event-name-casing', rule, { } `, - options: [{ ignores: ['/^[a-z]+(?:-[a-z]+)*:[a-z]+(?:-[a-z]+)*$/u'] }] + options: [ + 'kebab-case', + { ignores: ['/^[a-z]+(?:-[a-z]+)*:[a-z]+(?:-[a-z]+)*$/u'] } + ] + }, + + // For backward compatibility + { + filename: 'test.vue', + code: ` + + `, + options: [{ ignores: ['fooBar'] }] + }, + + // camelCase + { + filename: 'test.vue', + code: ` + + + `, + options: ['camelCase'] } ], invalid: [ @@ -361,7 +404,10 @@ tester.run('custom-event-name-casing', rule, { } `, - options: [{ ignores: ['/^[a-z]+(?:-[a-z]+)*:[a-z]+(?:-[a-z]+)*$/u'] }], + options: [ + 'kebab-case', + { ignores: ['/^[a-z]+(?:-[a-z]+)*:[a-z]+(?:-[a-z]+)*$/u'] } + ], errors: [ "Custom event name 'input/update' must be kebab-case.", "Custom event name 'search/update' must be kebab-case.", @@ -392,12 +438,46 @@ tester.run('custom-event-name-casing', rule, { } `, - options: [{ ignores: ['input:update', 'search:update', 'click:row'] }], + options: [ + 'kebab-case', + { ignores: ['input:update', 'search:update', 'click:row'] } + ], errors: [ "Custom event name 'input/update' must be kebab-case.", "Custom event name 'search/update' must be kebab-case.", "Custom event name 'click/row' must be kebab-case." ] + }, + { + filename: 'test.vue', + code: ` + + + `, + options: ['camelCase'], + errors: [ + "Custom event name 'foo-bar' must be camelCase.", + "Custom event name 'bar-baz' must be camelCase.", + "Custom event name 'baz-qux' must be camelCase." + ] } ] })