Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit a270df8

Browse filesBrowse files
waynzhFloEdelmann
andauthored
feat: add slot-name-casing rule (#2620)
Co-authored-by: Flo Edelmann <git@flo-edelmann.de>
1 parent fdfffd6 commit a270df8
Copy full SHA for a270df8

File tree

Expand file treeCollapse file tree

6 files changed

+323
-3
lines changed
Filter options
Expand file treeCollapse file tree

6 files changed

+323
-3
lines changed

‎docs/rules/index.md

Copy file name to clipboardExpand all lines: docs/rules/index.md
+2-1Lines changed: 2 additions & 1 deletion
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: |

‎docs/rules/restricted-component-names.md

Copy file name to clipboardExpand all lines: docs/rules/restricted-component-names.md
+2-2Lines changed: 2 additions & 2 deletions
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

‎docs/rules/slot-name-casing.md

Copy file name to clipboard
+88Lines changed: 88 additions & 0 deletions
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)

‎lib/index.js

Copy file name to clipboardExpand all lines: lib/index.js
+1Lines changed: 1 addition & 0 deletions
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'),

‎lib/rules/slot-name-casing.js

Copy file name to clipboard
+82Lines changed: 82 additions & 0 deletions
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+
}

‎tests/lib/rules/slot-name-casing.js

Copy file name to clipboard
+148Lines changed: 148 additions & 0 deletions
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 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.