Skip to content

Commit

Permalink
feat: add slot-name-casing rule (#2620)
Browse files Browse the repository at this point in the history
Co-authored-by: Flo Edelmann <[email protected]>
  • Loading branch information
waynzh and FloEdelmann authored Nov 27, 2024
1 parent fdfffd6 commit a270df8
Show file tree
Hide file tree
Showing 6 changed files with 323 additions and 3 deletions.
3 changes: 2 additions & 1 deletion docs/rules/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -281,8 +281,9 @@ For example:
| [vue/require-prop-comment](./require-prop-comment.md) | require props to have a comment | | :hammer: |
| [vue/require-typed-object-prop](./require-typed-object-prop.md) | enforce adding type declarations to object props | :bulb: | :hammer: |
| [vue/require-typed-ref](./require-typed-ref.md) | require `ref` and `shallowRef` functions to be strongly typed | | :hammer: |
| [vue/restricted-component-names](./restricted-component-names.md) | enforce using only specific in component names | | :warning: |
| [vue/restricted-component-names](./restricted-component-names.md) | enforce using only specific component names | | :warning: |
| [vue/script-indent](./script-indent.md) | enforce consistent indentation in `<script>` | :wrench: | :lipstick: |
| [vue/slot-name-casing](./slot-name-casing.md) | enforce specific casing for slot names | | :hammer: |
| [vue/sort-keys](./sort-keys.md) | enforce sort-keys in a manner that is compatible with order-in-components | | :hammer: |
| [vue/static-class-names-order](./static-class-names-order.md) | enforce static class names order | :wrench: | :hammer: |
| [vue/v-for-delimiter-style](./v-for-delimiter-style.md) | enforce `v-for` directive's delimiter style | :wrench: | :lipstick: |
Expand Down
4 changes: 2 additions & 2 deletions docs/rules/restricted-component-names.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@
pageClass: rule-details
sidebarDepth: 0
title: vue/restricted-component-names
description: enforce using only specific in component names
description: enforce using only specific component names
---

# vue/restricted-component-names

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

Expand Down
88 changes: 88 additions & 0 deletions docs/rules/slot-name-casing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
---
pageClass: rule-details
sidebarDepth: 0
title: vue/slot-name-casing
description: enforce specific casing for slot names
---

# vue/slot-name-casing

> enforce specific casing for slot names
- :exclamation: <badge text="This rule has not been released yet." vertical="middle" type="error"> _**This rule has not been released yet.**_ </badge>

## :book: Rule Details

This rule enforces proper casing of slot names in Vue components.

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

```vue
<template>
<!-- ✓ GOOD -->
<slot name="foo" />
<slot name="fooBar" />
<!-- ✗ BAD -->
<slot name="foo-bar" />
<slot name="foo_bar" />
<slot name="foo:bar" />
</template>
```

</eslint-code-block>

## :wrench: Options

```json
{
"vue/slot-name-casing": ["error", "camelCase" | "kebab-case" | "singleword"]
}
```

- `"camelCase"` (default) ... Enforce slot name to be in camel case.
- `"kebab-case"` ... Enforce slot name to be in kebab case.
- `"singleword"` ... Enforce slot name to be a single word.

### `"kebab-case"`

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

```vue
<template>
<!-- ✓ GOOD -->
<slot name="foo" />
<slot name="foo-bar" />
<!-- ✗ BAD -->
<slot name="fooBar" />
<slot name="foo_bar" />
<slot name="foo:bar" />
</template>
```

</eslint-code-block>

### `"singleword"`

<eslint-code-block :rules="{'vue/prop-name-casing': ['error', 'singleword']}">

```vue
<template>
<!-- ✓ GOOD -->
<slot name="foo" />
<!-- ✗ BAD -->
<slot name="foo-bar" />
<slot name="fooBar" />
<slot name="foo_bar" />
<slot name="foo:bar" />
</template>
```

</eslint-code-block>

## :mag: Implementation

- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/slot-name-casing.js)
- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/slot-name-casing.js)
1 change: 1 addition & 0 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,7 @@ const plugin = {
'script-indent': require('./rules/script-indent'),
'script-setup-uses-vars': require('./rules/script-setup-uses-vars'),
'singleline-html-element-content-newline': require('./rules/singleline-html-element-content-newline'),
'slot-name-casing': require('./rules/slot-name-casing'),
'sort-keys': require('./rules/sort-keys'),
'space-in-parens': require('./rules/space-in-parens'),
'space-infix-ops': require('./rules/space-infix-ops'),
Expand Down
82 changes: 82 additions & 0 deletions lib/rules/slot-name-casing.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/**
* @author Wayne Zhang
* See LICENSE file in root directory for full license.
*/
'use strict'

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

/**
* @typedef { 'camelCase' | 'kebab-case' | 'singleword' } OptionType
* @typedef { (str: string) => boolean } CheckerType
*/

/**
* Checks whether the given string is a single word.
* @param {string} str
* @return {boolean}
*/
function isSingleWord(str) {
return /^[a-z]+$/u.test(str)
}

/** @type {OptionType[]} */
const allowedCaseOptions = ['camelCase', 'kebab-case', 'singleword']

module.exports = {
meta: {
type: 'suggestion',
docs: {
description: 'enforce specific casing for slot names',
categories: undefined,
url: 'https://eslint.vuejs.org/rules/slot-name-casing.html'
},
fixable: null,
schema: [
{
enum: allowedCaseOptions
}
],
messages: {
invalidCase: 'Slot name "{{name}}" is not {{caseType}}.'
}
},
/** @param {RuleContext} context */
create(context) {
const option = context.options[0]

/** @type {OptionType} */
const caseType = allowedCaseOptions.includes(option) ? option : 'camelCase'

/** @type {CheckerType} */
const checker =
caseType === 'singleword' ? isSingleWord : casing.getChecker(caseType)

/** @param {VAttribute} node */
function processSlotNode(node) {
const name = node.value?.value
if (name && !checker(name)) {
context.report({
node,
loc: node.loc,
messageId: 'invalidCase',
data: {
name,
caseType
}
})
}
}

return utils.defineTemplateBodyVisitor(context, {
/** @param {VElement} node */
"VElement[name='slot']"(node) {
const slotName = utils.getAttribute(node, 'name')
if (slotName) {
processSlotNode(slotName)
}
}
})
}
}
148 changes: 148 additions & 0 deletions tests/lib/rules/slot-name-casing.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
/**
* @author WayneZhang
* See LICENSE file in root directory for full license.
*/
'use strict'

const RuleTester = require('../../eslint-compat').RuleTester
const rule = require('../../../lib/rules/slot-name-casing')

const tester = new RuleTester({
languageOptions: {
parser: require('vue-eslint-parser'),
ecmaVersion: 2020,
sourceType: 'module'
}
})

tester.run('slot-name-casing', rule, {
valid: [
`<template><slot key="foo" /></template>`,
`<template><slot name /></template>`,
`<template><slot name="foo" /></template>`,
`<template><slot name="fooBar" /></template>`,
`<template><slot :name="fooBar" /></template>`,
{
filename: 'test.vue',
code: `
<template>
<slot name="foo" />
<slot name="foo-bar" />
<slot :name="fooBar" />
</template>
`,
options: ['kebab-case']
},
{
filename: 'test.vue',
code: `
<template>
<slot name="foo" />
<slot :name="fooBar" />
</template>
`,
options: ['singleword']
}
],
invalid: [
{
filename: 'test.vue',
code: `
<template>
<slot name="foo-bar" />
<slot name="foo-Bar_baz" />
</template>
`,
errors: [
{
messageId: 'invalidCase',
data: {
name: 'foo-bar',
caseType: 'camelCase'
},
line: 3,
column: 17
},
{
messageId: 'invalidCase',
data: {
name: 'foo-Bar_baz',
caseType: 'camelCase'
},
line: 4,
column: 17
}
]
},
{
filename: 'test.vue',
code: `
<template>
<slot name="fooBar" />
<slot name="foo-Bar_baz" />
</template>
`,
options: ['kebab-case'],
errors: [
{
messageId: 'invalidCase',
data: {
name: 'fooBar',
caseType: 'kebab-case'
},
line: 3,
column: 17
},
{
messageId: 'invalidCase',
data: {
name: 'foo-Bar_baz',
caseType: 'kebab-case'
},
line: 4,
column: 17
}
]
},
{
filename: 'test.vue',
code: `
<template>
<slot name="foo-bar" />
<slot name="fooBar" />
<slot name="foo-Bar_baz" />
</template>
`,
options: ['singleword'],
errors: [
{
messageId: 'invalidCase',
data: {
name: 'foo-bar',
caseType: 'singleword'
},
line: 3,
column: 17
},
{
messageId: 'invalidCase',
data: {
name: 'fooBar',
caseType: 'singleword'
},
line: 4,
column: 17
},
{
messageId: 'invalidCase',
data: {
name: 'foo-Bar_baz',
caseType: 'singleword'
},
line: 5,
column: 17
}
]
}
]
})

0 comments on commit a270df8

Please sign in to comment.