Skip to content

Commit 7d16008

Browse files
waynzhota-meshi
authored andcommitted
feat: add slot-name-casing rule
1 parent bed816b commit 7d16008

File tree

5 files changed

+315
-0
lines changed

5 files changed

+315
-0
lines changed

Diff for: docs/rules/index.md

+1
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,7 @@ For example:
283283
| [vue/require-typed-ref](./require-typed-ref.md) | require `ref` and `shallowRef` functions to be strongly typed | | :hammer: |
284284
| [vue/restricted-component-names](./restricted-component-names.md) | enforce using only specific in 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 the slot name | | :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/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 the slot name
6+
---
7+
8+
# vue/slot-name-casing
9+
10+
> enforce specific casing for the slot name
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 enforce 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 camel case.
44+
- `"kebab-case"` ... Enforce slot name to be kebab case.
45+
- `"singleword"` ... Enforce slot name to be 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 the slot name',
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 in {{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

+143
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
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+
options: ['kebab-case']
34+
},
35+
{
36+
filename: 'test.vue',
37+
code: `
38+
<template>
39+
<slot name="foo" />
40+
<slot :name="fooBar" />
41+
</template>`,
42+
options: ['singleword']
43+
}
44+
],
45+
invalid: [
46+
{
47+
filename: 'test.vue',
48+
code: `
49+
<template>
50+
<slot name="foo-bar" />
51+
<slot name="foo-Bar_baz" />
52+
</template>`,
53+
errors: [
54+
{
55+
messageId: 'invalidCase',
56+
data: {
57+
name: 'foo-bar',
58+
caseType: 'camelCase'
59+
},
60+
line: 3,
61+
column: 13
62+
},
63+
{
64+
messageId: 'invalidCase',
65+
data: {
66+
name: 'foo-Bar_baz',
67+
caseType: 'camelCase'
68+
},
69+
line: 4,
70+
column: 13
71+
}
72+
]
73+
},
74+
{
75+
filename: 'test.vue',
76+
code: `
77+
<template>
78+
<slot name="fooBar" />
79+
<slot name="foo-Bar_baz" />
80+
</template>`,
81+
options: ['kebab-case'],
82+
errors: [
83+
{
84+
messageId: 'invalidCase',
85+
data: {
86+
name: 'fooBar',
87+
caseType: 'kebab-case'
88+
},
89+
line: 3,
90+
column: 13
91+
},
92+
{
93+
messageId: 'invalidCase',
94+
data: {
95+
name: 'foo-Bar_baz',
96+
caseType: 'kebab-case'
97+
},
98+
line: 4,
99+
column: 13
100+
}
101+
]
102+
},
103+
{
104+
filename: 'test.vue',
105+
code: `
106+
<template>
107+
<slot name="foo-bar" />
108+
<slot name="fooBar" />
109+
<slot name="foo-Bar_baz" />
110+
</template>`,
111+
options: ['singleword'],
112+
errors: [
113+
{
114+
messageId: 'invalidCase',
115+
data: {
116+
name: 'foo-bar',
117+
caseType: 'singleword'
118+
},
119+
line: 3,
120+
column: 13
121+
},
122+
{
123+
messageId: 'invalidCase',
124+
data: {
125+
name: 'fooBar',
126+
caseType: 'singleword'
127+
},
128+
line: 4,
129+
column: 13
130+
},
131+
{
132+
messageId: 'invalidCase',
133+
data: {
134+
name: 'foo-Bar_baz',
135+
caseType: 'singleword'
136+
},
137+
line: 5,
138+
column: 13
139+
}
140+
]
141+
}
142+
]
143+
})

0 commit comments

Comments
 (0)