Skip to content

Commit 10dd1a9

Browse files
authored
Add vue/component-options-name-casing rule (#1725)
* Add `vue/component-options-name-casing` rule * fix docs * fix demo * refactor * fix auto-fix * fix checking kebab-case * provide suggestions * accept suggestions
1 parent b08fe0b commit 10dd1a9

File tree

5 files changed

+840
-0
lines changed

5 files changed

+840
-0
lines changed

docs/rules/README.md

+1
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,7 @@ For example:
311311
| [vue/block-tag-newline](./block-tag-newline.md) | enforce line breaks after opening and before closing block-level tags | :wrench: |
312312
| [vue/component-api-style](./component-api-style.md) | enforce component API style | |
313313
| [vue/component-name-in-template-casing](./component-name-in-template-casing.md) | enforce specific casing for the component naming style in template | :wrench: |
314+
| [vue/component-options-name-casing](./component-options-name-casing.md) | enforce the casing of component name in `components` options | :wrench::bulb: |
314315
| [vue/custom-event-name-casing](./custom-event-name-casing.md) | enforce specific casing for custom event name | |
315316
| [vue/html-button-has-type](./html-button-has-type.md) | disallow usage of button without an explicit type attribute | |
316317
| [vue/html-comment-content-newline](./html-comment-content-newline.md) | enforce unified line brake in HTML comments | :wrench: |
+165
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
---
2+
pageClass: rule-details
3+
sidebarDepth: 0
4+
title: vue/component-options-name-casing
5+
description: enforce the casing of component name in `components` options
6+
---
7+
# vue/component-options-name-casing
8+
9+
> enforce the casing of component name in `components` options
10+
11+
- :exclamation: <badge text="This rule has not been released yet." vertical="middle" type="error"> ***This rule has not been released yet.*** </badge>
12+
- :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.
13+
- :bulb: Some problems reported by this rule are manually fixable by editor [suggestions](https://eslint.org/docs/developer-guide/working-with-rules#providing-suggestions).
14+
15+
## :book: Rule Details
16+
17+
This rule aims to enforce casing of the component names in `components` options.
18+
19+
## :wrench: Options
20+
21+
```json
22+
{
23+
"vue/component-options-name-casing": ["error", "PascalCase" | "kebab-case" | "camelCase"]
24+
}
25+
```
26+
27+
This rule has an option which can be one of these values:
28+
29+
- `"PascalCase"` (default) ... enforce component names to pascal case.
30+
- `"kebab-case"` ... enforce component names to kebab case.
31+
- `"camelCase"` ... enforce component names to camel case.
32+
33+
Please note that if you use kebab case in `components` options,
34+
you can **only** use kebab case in template;
35+
and if you use camel case in `components` options,
36+
you **can't** use pascal case in template.
37+
38+
For demonstration, the code example is invalid:
39+
40+
```vue
41+
<template>
42+
<div>
43+
<!-- All are invalid. DO NOT use like these. -->
44+
<KebabCase />
45+
<kebabCase />
46+
<CamelCase />
47+
</div>
48+
</template>
49+
50+
<script>
51+
export default {
52+
components: {
53+
camelCase: MyComponent,
54+
'kebab-case': MyComponent
55+
}
56+
}
57+
</script>
58+
```
59+
60+
### `"PascalCase"` (default)
61+
62+
<eslint-code-block fix :rules="{'vue/component-options-name-casing': ['error']}">
63+
64+
```vue
65+
<script>
66+
export default {
67+
/* ✓ GOOD */
68+
components: {
69+
AppHeader,
70+
AppSidebar
71+
}
72+
}
73+
</script>
74+
```
75+
76+
</eslint-code-block>
77+
78+
<eslint-code-block fix :rules="{'vue/component-options-name-casing': ['error']}">
79+
80+
```vue
81+
<script>
82+
export default {
83+
/* ✗ BAD */
84+
components: {
85+
appHeader,
86+
'app-sidebar': AppSidebar
87+
}
88+
}
89+
</script>
90+
```
91+
92+
</eslint-code-block>
93+
94+
### `"kebab-case"`
95+
96+
<eslint-code-block fix :rules="{'vue/component-options-name-casing': ['error', 'kebab-case']}">
97+
98+
```vue
99+
<script>
100+
export default {
101+
/* ✓ GOOD */
102+
components: {
103+
'app-header': AppHeader,
104+
'app-sidebar': appSidebar
105+
}
106+
}
107+
</script>
108+
```
109+
110+
</eslint-code-block>
111+
112+
<eslint-code-block fix :rules="{'vue/component-options-name-casing': ['error', 'kebab-case']}">
113+
114+
```vue
115+
<script>
116+
export default {
117+
/* ✗ BAD */
118+
components: {
119+
AppHeader,
120+
appSidebar
121+
}
122+
}
123+
</script>
124+
```
125+
126+
</eslint-code-block>
127+
128+
### `"camelCase"`
129+
130+
<eslint-code-block fix :rules="{'vue/component-options-name-casing': ['error', 'camelCase']}">
131+
132+
```vue
133+
<script>
134+
export default {
135+
/* ✓ GOOD */
136+
components: {
137+
appHeader,
138+
appSidebar
139+
}
140+
}
141+
</script>
142+
```
143+
144+
</eslint-code-block>
145+
146+
<eslint-code-block fix :rules="{'vue/component-options-name-casing': ['error', 'camelCase']}">
147+
148+
```vue
149+
<script>
150+
export default {
151+
/* ✗ BAD */
152+
components: {
153+
AppHeader,
154+
'app-sidebar': appSidebar
155+
}
156+
}
157+
</script>
158+
```
159+
160+
</eslint-code-block>
161+
162+
## :mag: Implementation
163+
164+
- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/component-options-name-casing.js)
165+
- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/component-options-name-casing.js)

lib/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ module.exports = {
2424
'component-api-style': require('./rules/component-api-style'),
2525
'component-definition-name-casing': require('./rules/component-definition-name-casing'),
2626
'component-name-in-template-casing': require('./rules/component-name-in-template-casing'),
27+
'component-options-name-casing': require('./rules/component-options-name-casing'),
2728
'component-tags-order': require('./rules/component-tags-order'),
2829
'custom-event-name-casing': require('./rules/custom-event-name-casing'),
2930
'dot-location': require('./rules/dot-location'),
+115
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
/**
2+
* @author Pig Fang
3+
* See LICENSE file in root directory for full license.
4+
*/
5+
'use strict'
6+
7+
// ------------------------------------------------------------------------------
8+
// Requirements
9+
// ------------------------------------------------------------------------------
10+
11+
const utils = require('../utils')
12+
const casing = require('../utils/casing')
13+
14+
// ------------------------------------------------------------------------------
15+
// Helpers
16+
// ------------------------------------------------------------------------------
17+
18+
/**
19+
* @param {import('../../typings/eslint-plugin-vue/util-types/ast').Expression} node
20+
* @returns {string | null}
21+
*/
22+
function getOptionsComponentName(node) {
23+
if (node.type === 'Identifier') {
24+
return node.name
25+
}
26+
if (node.type === 'Literal') {
27+
return typeof node.value === 'string' ? node.value : null
28+
}
29+
return null
30+
}
31+
32+
// ------------------------------------------------------------------------------
33+
// Rule Definition
34+
// ------------------------------------------------------------------------------
35+
36+
module.exports = {
37+
meta: {
38+
type: 'suggestion',
39+
docs: {
40+
description:
41+
'enforce the casing of component name in `components` options',
42+
categories: undefined,
43+
url: 'https://eslint.vuejs.org/rules/component-options-name-casing.html'
44+
},
45+
fixable: 'code',
46+
hasSuggestions: true,
47+
schema: [{ enum: casing.allowedCaseOptions }],
48+
messages: {
49+
caseNotMatched: 'Component name "{{component}}" is not {{caseType}}.',
50+
possibleRenaming: 'Rename component name to be in {{caseType}}.'
51+
}
52+
},
53+
/** @param {RuleContext} context */
54+
create(context) {
55+
const caseType = context.options[0] || 'PascalCase'
56+
57+
const canAutoFix = caseType === 'PascalCase'
58+
const checkCase = casing.getChecker(caseType)
59+
const convert = casing.getConverter(caseType)
60+
61+
return utils.executeOnVue(context, (obj) => {
62+
const node = utils.findProperty(obj, 'components')
63+
if (!node || node.value.type !== 'ObjectExpression') {
64+
return
65+
}
66+
67+
node.value.properties.forEach((property) => {
68+
if (property.type !== 'Property') {
69+
return
70+
}
71+
72+
const name = getOptionsComponentName(property.key)
73+
if (!name || checkCase(name)) {
74+
return
75+
}
76+
77+
context.report({
78+
node: property.key,
79+
messageId: 'caseNotMatched',
80+
data: {
81+
component: name,
82+
caseType
83+
},
84+
fix: canAutoFix
85+
? (fixer) => {
86+
const converted = convert(name)
87+
return property.shorthand
88+
? fixer.replaceText(property, `${converted}: ${name}`)
89+
: fixer.replaceText(property.key, converted)
90+
}
91+
: undefined,
92+
suggest: canAutoFix
93+
? undefined
94+
: [
95+
{
96+
messageId: 'possibleRenaming',
97+
data: { caseType },
98+
fix: (fixer) => {
99+
const converted = convert(name)
100+
if (caseType === 'kebab-case') {
101+
return property.shorthand
102+
? fixer.replaceText(property, `'${converted}': ${name}`)
103+
: fixer.replaceText(property.key, `'${converted}'`)
104+
}
105+
return property.shorthand
106+
? fixer.replaceText(property, `${converted}: ${name}`)
107+
: fixer.replaceText(property.key, converted)
108+
}
109+
}
110+
]
111+
})
112+
})
113+
})
114+
}
115+
}

0 commit comments

Comments
 (0)