Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 2 commits into from Dec 4, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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