Skip to content

Commit

Permalink
Add casing option to vue/custom-event-name-casing rule & remove from …
Browse files Browse the repository at this point in the history
…configs. (#1364)

* Add casing option to vue/custom-event-name-casing rule & remove from configs.

* update
  • Loading branch information
ota-meshi committed Dec 4, 2020
1 parent af5c4c0 commit 6d6203f
Show file tree
Hide file tree
Showing 6 changed files with 227 additions and 46 deletions.
3 changes: 1 addition & 2 deletions docs/rules/README.md
Expand Up @@ -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: |
Expand Down Expand Up @@ -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 | |
Expand Down Expand Up @@ -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: |
Expand Down
88 changes: 80 additions & 8 deletions docs/rules/custom-event-name-casing.md
Expand Up @@ -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.

<eslint-code-block :rules="{'vue/custom-event-name-casing': ['error']}">

Expand Down Expand Up @@ -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"`

<eslint-code-block :rules="{'vue/custom-event-name-casing': ['error', 'kebab-case']}">

```vue
<template>
<!-- ✓ GOOD -->
<button @click="$emit('my-event')" />
<!-- ✗ BAD -->
<button @click="$emit('myEvent')" />
</template>
<script>
export default {
methods: {
onClick () {
/* ✓ GOOD */
this.$emit('my-event')
/* ✗ BAD */
this.$emit('myEvent')
}
}
}
</script>
```

</eslint-code-block>

### `"camelCase"`

<eslint-code-block :rules="{'vue/custom-event-name-casing': ['error', 'camelCase']}">

```vue
<template>
<!-- ✓ GOOD -->
<button @click="$emit('myEvent')" />
<!-- ✗ BAD -->
<button @click="$emit('my-event')" />
</template>
<script>
export default {
methods: {
onClick () {
/* ✓ GOOD */
this.$emit('myEvent')
/* ✗ BAD */
this.$emit('my-event')
}
}
}
</script>
```

</eslint-code-block>

### `"ignores": ["fooBar", "/^[a-z]+(?:-[a-z]+)*:[a-z]+(?:-[a-z]+)*$/u"]`

<eslint-code-block :rules="{'vue/custom-event-name-casing': ['error', { ignores: ['fooBar', '/^[a-z]+(?:-[a-z]+)*:[a-z]+(?:-[a-z]+)*$/u'] }]}">
Expand Down Expand Up @@ -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
Expand Down
1 change: 0 additions & 1 deletion lib/configs/essential.js
Expand Up @@ -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',
Expand Down
1 change: 0 additions & 1 deletion lib/configs/vue3-essential.js
Expand Up @@ -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',
Expand Down
92 changes: 62 additions & 30 deletions lib/rules/custom-event-name-casing.js
Expand Up @@ -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
Expand Down Expand Up @@ -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<ObjectExpression, {contextReferenceIds:Set<Identifier>,emitReferenceIds:Set<Identifier>}>} */
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
}
})
}
Expand Down Expand Up @@ -190,14 +218,18 @@ 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 {
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()}
Expand Down

0 comments on commit 6d6203f

Please sign in to comment.