Skip to content

Commit

Permalink
Add vue/component-options-name-casing rule (#1725)
Browse files Browse the repository at this point in the history
* Add `vue/component-options-name-casing` rule

* fix docs

* fix demo

* refactor

* fix auto-fix

* fix checking kebab-case

* provide suggestions

* accept suggestions
  • Loading branch information
g-plane committed Nov 30, 2021
1 parent b08fe0b commit 10dd1a9
Show file tree
Hide file tree
Showing 5 changed files with 840 additions and 0 deletions.
1 change: 1 addition & 0 deletions docs/rules/README.md
Expand Up @@ -311,6 +311,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-api-style](./component-api-style.md) | enforce component API style | |
| [vue/component-name-in-template-casing](./component-name-in-template-casing.md) | enforce specific casing for the component naming style in template | :wrench: |
| [vue/component-options-name-casing](./component-options-name-casing.md) | enforce the casing of component name in `components` options | :wrench::bulb: |
| [vue/custom-event-name-casing](./custom-event-name-casing.md) | enforce specific casing for custom event name | |
| [vue/html-button-has-type](./html-button-has-type.md) | disallow usage of button without an explicit type attribute | |
| [vue/html-comment-content-newline](./html-comment-content-newline.md) | enforce unified line brake in HTML comments | :wrench: |
Expand Down
165 changes: 165 additions & 0 deletions docs/rules/component-options-name-casing.md
@@ -0,0 +1,165 @@
---
pageClass: rule-details
sidebarDepth: 0
title: vue/component-options-name-casing
description: enforce the casing of component name in `components` options
---
# vue/component-options-name-casing

> enforce the casing of component name in `components` options
- :exclamation: <badge text="This rule has not been released yet." vertical="middle" type="error"> ***This rule has not been released yet.*** </badge>
- :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.
- :bulb: Some problems reported by this rule are manually fixable by editor [suggestions](https://eslint.org/docs/developer-guide/working-with-rules#providing-suggestions).

## :book: Rule Details

This rule aims to enforce casing of the component names in `components` options.

## :wrench: Options

```json
{
"vue/component-options-name-casing": ["error", "PascalCase" | "kebab-case" | "camelCase"]
}
```

This rule has an option which can be one of these values:

- `"PascalCase"` (default) ... enforce component names to pascal case.
- `"kebab-case"` ... enforce component names to kebab case.
- `"camelCase"` ... enforce component names to camel case.

Please note that if you use kebab case in `components` options,
you can **only** use kebab case in template;
and if you use camel case in `components` options,
you **can't** use pascal case in template.

For demonstration, the code example is invalid:

```vue
<template>
<div>
<!-- All are invalid. DO NOT use like these. -->
<KebabCase />
<kebabCase />
<CamelCase />
</div>
</template>
<script>
export default {
components: {
camelCase: MyComponent,
'kebab-case': MyComponent
}
}
</script>
```

### `"PascalCase"` (default)

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

```vue
<script>
export default {
/* ✓ GOOD */
components: {
AppHeader,
AppSidebar
}
}
</script>
```

</eslint-code-block>

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

```vue
<script>
export default {
/* ✗ BAD */
components: {
appHeader,
'app-sidebar': AppSidebar
}
}
</script>
```

</eslint-code-block>

### `"kebab-case"`

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

```vue
<script>
export default {
/* ✓ GOOD */
components: {
'app-header': AppHeader,
'app-sidebar': appSidebar
}
}
</script>
```

</eslint-code-block>

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

```vue
<script>
export default {
/* ✗ BAD */
components: {
AppHeader,
appSidebar
}
}
</script>
```

</eslint-code-block>

### `"camelCase"`

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

```vue
<script>
export default {
/* ✓ GOOD */
components: {
appHeader,
appSidebar
}
}
</script>
```

</eslint-code-block>

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

```vue
<script>
export default {
/* ✗ BAD */
components: {
AppHeader,
'app-sidebar': appSidebar
}
}
</script>
```

</eslint-code-block>

## :mag: Implementation

- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/component-options-name-casing.js)
- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/component-options-name-casing.js)
1 change: 1 addition & 0 deletions lib/index.js
Expand Up @@ -24,6 +24,7 @@ module.exports = {
'component-api-style': require('./rules/component-api-style'),
'component-definition-name-casing': require('./rules/component-definition-name-casing'),
'component-name-in-template-casing': require('./rules/component-name-in-template-casing'),
'component-options-name-casing': require('./rules/component-options-name-casing'),
'component-tags-order': require('./rules/component-tags-order'),
'custom-event-name-casing': require('./rules/custom-event-name-casing'),
'dot-location': require('./rules/dot-location'),
Expand Down
115 changes: 115 additions & 0 deletions lib/rules/component-options-name-casing.js
@@ -0,0 +1,115 @@
/**
* @author Pig Fang
* See LICENSE file in root directory for full license.
*/
'use strict'

// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------

const utils = require('../utils')
const casing = require('../utils/casing')

// ------------------------------------------------------------------------------
// Helpers
// ------------------------------------------------------------------------------

/**
* @param {import('../../typings/eslint-plugin-vue/util-types/ast').Expression} node
* @returns {string | null}
*/
function getOptionsComponentName(node) {
if (node.type === 'Identifier') {
return node.name
}
if (node.type === 'Literal') {
return typeof node.value === 'string' ? node.value : null
}
return null
}

// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------

module.exports = {
meta: {
type: 'suggestion',
docs: {
description:
'enforce the casing of component name in `components` options',
categories: undefined,
url: 'https://eslint.vuejs.org/rules/component-options-name-casing.html'
},
fixable: 'code',
hasSuggestions: true,
schema: [{ enum: casing.allowedCaseOptions }],
messages: {
caseNotMatched: 'Component name "{{component}}" is not {{caseType}}.',
possibleRenaming: 'Rename component name to be in {{caseType}}.'
}
},
/** @param {RuleContext} context */
create(context) {
const caseType = context.options[0] || 'PascalCase'

const canAutoFix = caseType === 'PascalCase'
const checkCase = casing.getChecker(caseType)
const convert = casing.getConverter(caseType)

return utils.executeOnVue(context, (obj) => {
const node = utils.findProperty(obj, 'components')
if (!node || node.value.type !== 'ObjectExpression') {
return
}

node.value.properties.forEach((property) => {
if (property.type !== 'Property') {
return
}

const name = getOptionsComponentName(property.key)
if (!name || checkCase(name)) {
return
}

context.report({
node: property.key,
messageId: 'caseNotMatched',
data: {
component: name,
caseType
},
fix: canAutoFix
? (fixer) => {
const converted = convert(name)
return property.shorthand
? fixer.replaceText(property, `${converted}: ${name}`)
: fixer.replaceText(property.key, converted)
}
: undefined,
suggest: canAutoFix
? undefined
: [
{
messageId: 'possibleRenaming',
data: { caseType },
fix: (fixer) => {
const converted = convert(name)
if (caseType === 'kebab-case') {
return property.shorthand
? fixer.replaceText(property, `'${converted}': ${name}`)
: fixer.replaceText(property.key, `'${converted}'`)
}
return property.shorthand
? fixer.replaceText(property, `${converted}: ${name}`)
: fixer.replaceText(property.key, converted)
}
}
]
})
})
})
}
}

0 comments on commit 10dd1a9

Please sign in to comment.