Skip to content

Commit a270df8

Browse files
waynzhFloEdelmann
andauthored
feat: add slot-name-casing rule (#2620)
Co-authored-by: Flo Edelmann <[email protected]>
1 parent fdfffd6 commit a270df8

File tree

6 files changed

+323
-3
lines changed

6 files changed

+323
-3
lines changed

Diff for: docs/rules/index.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -281,8 +281,9 @@ For example:
281281
| [vue/require-prop-comment](./require-prop-comment.md) | require props to have a comment | | :hammer: |
282282
| [vue/require-typed-object-prop](./require-typed-object-prop.md) | enforce adding type declarations to object props | :bulb: | :hammer: |
283283
| [vue/require-typed-ref](./require-typed-ref.md) | require `ref` and `shallowRef` functions to be strongly typed | | :hammer: |
284-
| [vue/restricted-component-names](./restricted-component-names.md) | enforce using only specific in component names | | :warning: |
284+
| [vue/restricted-component-names](./restricted-component-names.md) | enforce using only specific component names | | :warning: |
285285
| [vue/script-indent](./script-indent.md) | enforce consistent indentation in `<script>` | :wrench: | :lipstick: |
286+
| [vue/slot-name-casing](./slot-name-casing.md) | enforce specific casing for slot names | | :hammer: |
286287
| [vue/sort-keys](./sort-keys.md) | enforce sort-keys in a manner that is compatible with order-in-components | | :hammer: |
287288
| [vue/static-class-names-order](./static-class-names-order.md) | enforce static class names order | :wrench: | :hammer: |
288289
| [vue/v-for-delimiter-style](./v-for-delimiter-style.md) | enforce `v-for` directive's delimiter style | :wrench: | :lipstick: |

Diff for: docs/rules/restricted-component-names.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@
22
pageClass: rule-details
33
sidebarDepth: 0
44
title: vue/restricted-component-names
5-
description: enforce using only specific in component names
5+
description: enforce using only specific component names
66
---
77

88
# vue/restricted-component-names
99

10-
> enforce using only specific in component names
10+
> enforce using only specific component names
1111
1212
- :exclamation: <badge text="This rule has not been released yet." vertical="middle" type="error"> _**This rule has not been released yet.**_ </badge>
1313

Diff for: docs/rules/slot-name-casing.md

+88
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
---
2+
pageClass: rule-details
3+
sidebarDepth: 0
4+
title: vue/slot-name-casing
5+
description: enforce specific casing for slot names
6+
---
7+
8+
# vue/slot-name-casing
9+
10+
> enforce specific casing for slot names
11+
12+
- :exclamation: <badge text="This rule has not been released yet." vertical="middle" type="error"> _**This rule has not been released yet.**_ </badge>
13+
14+
## :book: Rule Details
15+
16+
This rule enforces proper casing of slot names in Vue components.
17+
18+
<eslint-code-block :rules="{'vue/slot-name-casing': ['error']}">
19+
20+
```vue
21+
<template>
22+
<!-- ✓ GOOD -->
23+
<slot name="foo" />
24+
<slot name="fooBar" />
25+
26+
<!-- ✗ BAD -->
27+
<slot name="foo-bar" />
28+
<slot name="foo_bar" />
29+
<slot name="foo:bar" />
30+
</template>
31+
```
32+
33+
</eslint-code-block>
34+
35+
## :wrench: Options
36+
37+
```json
38+
{
39+
"vue/slot-name-casing": ["error", "camelCase" | "kebab-case" | "singleword"]
40+
}
41+
```
42+
43+
- `"camelCase"` (default) ... Enforce slot name to be in camel case.
44+
- `"kebab-case"` ... Enforce slot name to be in kebab case.
45+
- `"singleword"` ... Enforce slot name to be a single word.
46+
47+
### `"kebab-case"`
48+
49+
<eslint-code-block :rules="{'vue/prop-name-casing': ['error', 'kebab-case']}">
50+
51+
```vue
52+
<template>
53+
<!-- ✓ GOOD -->
54+
<slot name="foo" />
55+
<slot name="foo-bar" />
56+
57+
<!-- ✗ BAD -->
58+
<slot name="fooBar" />
59+
<slot name="foo_bar" />
60+
<slot name="foo:bar" />
61+
</template>
62+
```
63+
64+
</eslint-code-block>
65+
66+
### `"singleword"`
67+
68+
<eslint-code-block :rules="{'vue/prop-name-casing': ['error', 'singleword']}">
69+
70+
```vue
71+
<template>
72+
<!-- ✓ GOOD -->
73+
<slot name="foo" />
74+
75+
<!-- ✗ BAD -->
76+
<slot name="foo-bar" />
77+
<slot name="fooBar" />
78+
<slot name="foo_bar" />
79+
<slot name="foo:bar" />
80+
</template>
81+
```
82+
83+
</eslint-code-block>
84+
85+
## :mag: Implementation
86+
87+
- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/slot-name-casing.js)
88+
- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/slot-name-casing.js)

Diff for: lib/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,7 @@ const plugin = {
237237
'script-indent': require('./rules/script-indent'),
238238
'script-setup-uses-vars': require('./rules/script-setup-uses-vars'),
239239
'singleline-html-element-content-newline': require('./rules/singleline-html-element-content-newline'),
240+
'slot-name-casing': require('./rules/slot-name-casing'),
240241
'sort-keys': require('./rules/sort-keys'),
241242
'space-in-parens': require('./rules/space-in-parens'),
242243
'space-infix-ops': require('./rules/space-infix-ops'),

Diff for: lib/rules/slot-name-casing.js

+82
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/**
2+
* @author Wayne Zhang
3+
* See LICENSE file in root directory for full license.
4+
*/
5+
'use strict'
6+
7+
const utils = require('../utils')
8+
const casing = require('../utils/casing')
9+
10+
/**
11+
* @typedef { 'camelCase' | 'kebab-case' | 'singleword' } OptionType
12+
* @typedef { (str: string) => boolean } CheckerType
13+
*/
14+
15+
/**
16+
* Checks whether the given string is a single word.
17+
* @param {string} str
18+
* @return {boolean}
19+
*/
20+
function isSingleWord(str) {
21+
return /^[a-z]+$/u.test(str)
22+
}
23+
24+
/** @type {OptionType[]} */
25+
const allowedCaseOptions = ['camelCase', 'kebab-case', 'singleword']
26+
27+
module.exports = {
28+
meta: {
29+
type: 'suggestion',
30+
docs: {
31+
description: 'enforce specific casing for slot names',
32+
categories: undefined,
33+
url: 'https://eslint.vuejs.org/rules/slot-name-casing.html'
34+
},
35+
fixable: null,
36+
schema: [
37+
{
38+
enum: allowedCaseOptions
39+
}
40+
],
41+
messages: {
42+
invalidCase: 'Slot name "{{name}}" is not {{caseType}}.'
43+
}
44+
},
45+
/** @param {RuleContext} context */
46+
create(context) {
47+
const option = context.options[0]
48+
49+
/** @type {OptionType} */
50+
const caseType = allowedCaseOptions.includes(option) ? option : 'camelCase'
51+
52+
/** @type {CheckerType} */
53+
const checker =
54+
caseType === 'singleword' ? isSingleWord : casing.getChecker(caseType)
55+
56+
/** @param {VAttribute} node */
57+
function processSlotNode(node) {
58+
const name = node.value?.value
59+
if (name && !checker(name)) {
60+
context.report({
61+
node,
62+
loc: node.loc,
63+
messageId: 'invalidCase',
64+
data: {
65+
name,
66+
caseType
67+
}
68+
})
69+
}
70+
}
71+
72+
return utils.defineTemplateBodyVisitor(context, {
73+
/** @param {VElement} node */
74+
"VElement[name='slot']"(node) {
75+
const slotName = utils.getAttribute(node, 'name')
76+
if (slotName) {
77+
processSlotNode(slotName)
78+
}
79+
}
80+
})
81+
}
82+
}

Diff for: tests/lib/rules/slot-name-casing.js

+148
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
/**
2+
* @author WayneZhang
3+
* See LICENSE file in root directory for full license.
4+
*/
5+
'use strict'
6+
7+
const RuleTester = require('../../eslint-compat').RuleTester
8+
const rule = require('../../../lib/rules/slot-name-casing')
9+
10+
const tester = new RuleTester({
11+
languageOptions: {
12+
parser: require('vue-eslint-parser'),
13+
ecmaVersion: 2020,
14+
sourceType: 'module'
15+
}
16+
})
17+
18+
tester.run('slot-name-casing', rule, {
19+
valid: [
20+
`<template><slot key="foo" /></template>`,
21+
`<template><slot name /></template>`,
22+
`<template><slot name="foo" /></template>`,
23+
`<template><slot name="fooBar" /></template>`,
24+
`<template><slot :name="fooBar" /></template>`,
25+
{
26+
filename: 'test.vue',
27+
code: `
28+
<template>
29+
<slot name="foo" />
30+
<slot name="foo-bar" />
31+
<slot :name="fooBar" />
32+
</template>
33+
`,
34+
options: ['kebab-case']
35+
},
36+
{
37+
filename: 'test.vue',
38+
code: `
39+
<template>
40+
<slot name="foo" />
41+
<slot :name="fooBar" />
42+
</template>
43+
`,
44+
options: ['singleword']
45+
}
46+
],
47+
invalid: [
48+
{
49+
filename: 'test.vue',
50+
code: `
51+
<template>
52+
<slot name="foo-bar" />
53+
<slot name="foo-Bar_baz" />
54+
</template>
55+
`,
56+
errors: [
57+
{
58+
messageId: 'invalidCase',
59+
data: {
60+
name: 'foo-bar',
61+
caseType: 'camelCase'
62+
},
63+
line: 3,
64+
column: 17
65+
},
66+
{
67+
messageId: 'invalidCase',
68+
data: {
69+
name: 'foo-Bar_baz',
70+
caseType: 'camelCase'
71+
},
72+
line: 4,
73+
column: 17
74+
}
75+
]
76+
},
77+
{
78+
filename: 'test.vue',
79+
code: `
80+
<template>
81+
<slot name="fooBar" />
82+
<slot name="foo-Bar_baz" />
83+
</template>
84+
`,
85+
options: ['kebab-case'],
86+
errors: [
87+
{
88+
messageId: 'invalidCase',
89+
data: {
90+
name: 'fooBar',
91+
caseType: 'kebab-case'
92+
},
93+
line: 3,
94+
column: 17
95+
},
96+
{
97+
messageId: 'invalidCase',
98+
data: {
99+
name: 'foo-Bar_baz',
100+
caseType: 'kebab-case'
101+
},
102+
line: 4,
103+
column: 17
104+
}
105+
]
106+
},
107+
{
108+
filename: 'test.vue',
109+
code: `
110+
<template>
111+
<slot name="foo-bar" />
112+
<slot name="fooBar" />
113+
<slot name="foo-Bar_baz" />
114+
</template>
115+
`,
116+
options: ['singleword'],
117+
errors: [
118+
{
119+
messageId: 'invalidCase',
120+
data: {
121+
name: 'foo-bar',
122+
caseType: 'singleword'
123+
},
124+
line: 3,
125+
column: 17
126+
},
127+
{
128+
messageId: 'invalidCase',
129+
data: {
130+
name: 'fooBar',
131+
caseType: 'singleword'
132+
},
133+
line: 4,
134+
column: 17
135+
},
136+
{
137+
messageId: 'invalidCase',
138+
data: {
139+
name: 'foo-Bar_baz',
140+
caseType: 'singleword'
141+
},
142+
line: 5,
143+
column: 17
144+
}
145+
]
146+
}
147+
]
148+
})

0 commit comments

Comments
 (0)