Skip to content

Commit b7ee76e

Browse files
authored
New: v-slot-style rule (fixes #801) (#836)
1 parent 263076a commit b7ee76e

File tree

3 files changed

+699
-0
lines changed

3 files changed

+699
-0
lines changed

docs/rules/v-slot-style.md

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
---
2+
pageClass: rule-details
3+
sidebarDepth: 0
4+
title: vue/v-slot-style
5+
description: enforce `v-slot` directive style
6+
---
7+
# vue/v-slot-style
8+
> enforce `v-slot` directive style
9+
10+
- :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.
11+
12+
## :book: Rule Details
13+
14+
This rule enforces `v-slot` directive style which you should use shorthand or long form.
15+
16+
<eslint-code-block fix :rules="{'vue/v-slot-style': ['error']}">
17+
18+
```vue
19+
<template>
20+
<!-- ✓ GOOD -->
21+
<my-component v-slot="data">
22+
{{data}}
23+
</my-component>
24+
<my-component>
25+
<template #default>content</template>
26+
<template #one>content</template>
27+
<template #two>content</template>
28+
</my-component>
29+
30+
<!-- ✗ BAD -->
31+
<my-component #default="data">
32+
{{data}}
33+
</my-component>
34+
<my-component>
35+
<template v-slot>content</template>
36+
<template v-slot:one>content</template>
37+
<template v-slot:two>content</template>
38+
</my-component>
39+
</template>
40+
```
41+
42+
</eslint-code-block>
43+
44+
## :wrench: Options
45+
46+
```json
47+
{
48+
"vue/v-slot-style": ["error", {
49+
"atComponent": "shorthand" | "longform" | "v-slot",
50+
"default": "shorthand" | "longform" | "v-slot",
51+
"named": "shorthand" | "longform",
52+
}]
53+
}
54+
```
55+
56+
| Name | Type | Default Value | Description
57+
|:-----|:-----|:--------------|:------------
58+
| `atComponent` | `"shorthand"` \| `"longform"` \| `"v-slot"` | `"v-slot"` | The style for the default slot at custom components directly (E.g. `<my-component v-slot="">`).
59+
| `default` | `"shorthand"` \| `"longform"` \| `"v-slot"` | `"shorthand"` | The style for the default slot at template wrappers (E.g. `<template #default="">`).
60+
| `named` | `"shorthand"` \| `"longform"` | `"shorthand"` | The style for named slots (E.g. `<template #named="">`).
61+
62+
Each value means:
63+
64+
- `"shorthand"` ... use `#` shorthand. E.g. `#default`, `#named`, ...
65+
- `"longform"` ... use `v-slot:` directive notation. E.g. `v-slot:default`, `v-slot:named`, ...
66+
- `"v-slot"` ... use `v-slot` without that argument. This is shorter than `#default` shorthand.
67+
68+
And a string option is supported to be consistent to similar `vue/v-bind-style` and `vue/v-on-style`.
69+
70+
- `["error", "longform"]` is same as `["error", { atComponent: "longform", default: "longform", named: "longform" }]`.
71+
- `["error", "shorthand"]` is same as `["error", { atComponent: "shorthand", default: "shorthand", named: "shorthand" }]`.
72+
73+
### `"longform"`
74+
75+
<eslint-code-block fix :rules="{'vue/v-slot-style': ['error', 'longform']}">
76+
77+
```vue
78+
<template>
79+
<!-- ✓ GOOD -->
80+
<my-component v-slot:default="data">
81+
{{data}}
82+
</my-component>
83+
<my-component>
84+
<template v-slot:default>content</template>
85+
<template v-slot:one>content</template>
86+
<template v-slot:two>content</template>
87+
</my-component>
88+
89+
<!-- ✗ BAD -->
90+
<my-component v-slot="data">
91+
{{data}}
92+
</my-component>
93+
<my-component>
94+
<template #default>content</template>
95+
<template #one>content</template>
96+
<template #two>content</template>
97+
</my-component>
98+
</template>
99+
```
100+
101+
</eslint-code-block>
102+
103+
## :books: Further reading
104+
105+
- [Style guide - Directive shorthands](https://vuejs.org/v2/style-guide/#Directive-shorthands-strongly-recommended)
106+
107+
## :mag: Implementation
108+
109+
- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/v-slot-style.js)
110+
- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/v-slot-style.js)

lib/rules/v-slot-style.js

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
/**
2+
* @author Toru Nagashima
3+
* See LICENSE file in root directory for full license.
4+
*/
5+
'use strict'
6+
7+
const { pascalCase } = require('../utils/casing')
8+
const utils = require('../utils')
9+
10+
/**
11+
* @typedef {Object} Options
12+
* @property {"shorthand" | "longform" | "v-slot"} atComponent The style for the default slot at a custom component directly.
13+
* @property {"shorthand" | "longform" | "v-slot"} default The style for the default slot at a template wrapper.
14+
* @property {"shorthand" | "longform"} named The style for named slots at a template wrapper.
15+
*/
16+
17+
/**
18+
* Normalize options.
19+
* @param {any} options The raw options to normalize.
20+
* @returns {Options} The normalized options.
21+
*/
22+
function normalizeOptions (options) {
23+
const normalized = {
24+
atComponent: 'v-slot',
25+
default: 'shorthand',
26+
named: 'shorthand'
27+
}
28+
29+
if (typeof options === 'string') {
30+
normalized.atComponent = normalized.default = normalized.named = options
31+
} else if (options != null) {
32+
for (const key of ['atComponent', 'default', 'named']) {
33+
if (options[key] != null) {
34+
normalized[key] = options[key]
35+
}
36+
}
37+
}
38+
39+
return normalized
40+
}
41+
42+
/**
43+
* Get the expected style.
44+
* @param {Options} options The options that defined expected types.
45+
* @param {VAttribute} node The `v-slot` node to check.
46+
* @returns {"shorthand" | "longform" | "v-slot"} The expected style.
47+
*/
48+
function getExpectedStyle (options, node) {
49+
const { argument } = node.key
50+
51+
if (argument == null || (argument.type === 'VIdentifier' && argument.name === 'default')) {
52+
const element = node.parent.parent
53+
return element.name === 'template' ? options.default : options.atComponent
54+
}
55+
return options.named
56+
}
57+
58+
/**
59+
* Get the expected style.
60+
* @param {VAttribute} node The `v-slot` node to check.
61+
* @returns {"shorthand" | "longform" | "v-slot"} The expected style.
62+
*/
63+
function getActualStyle (node) {
64+
const { name, argument } = node.key
65+
66+
if (name.rawName === '#') {
67+
return 'shorthand'
68+
}
69+
if (argument != null) {
70+
return 'longform'
71+
}
72+
return 'v-slot'
73+
}
74+
75+
module.exports = {
76+
meta: {
77+
type: 'suggestion',
78+
docs: {
79+
description: 'enforce `v-slot` directive style',
80+
category: undefined, // strongly-recommended
81+
url: 'https://eslint.vuejs.org/rules/v-slot-style.html'
82+
},
83+
fixable: 'code',
84+
schema: [
85+
{
86+
anyOf: [
87+
{ enum: ['shorthand', 'longform'] },
88+
{
89+
type: 'object',
90+
properties: {
91+
atComponent: { enum: ['shorthand', 'longform', 'v-slot'] },
92+
default: { enum: ['shorthand', 'longform', 'v-slot'] },
93+
named: { enum: ['shorthand', 'longform'] }
94+
},
95+
additionalProperties: false
96+
}
97+
]
98+
}
99+
],
100+
messages: {
101+
expectedShorthand: "Expected '#{{argument}}' instead of '{{actual}}'.",
102+
expectedLongform: "Expected 'v-slot:{{argument}}' instead of '{{actual}}'.",
103+
expectedVSlot: "Expected 'v-slot' instead of '{{actual}}'."
104+
}
105+
},
106+
107+
create (context) {
108+
const sourceCode = context.getSourceCode()
109+
const options = normalizeOptions(context.options[0])
110+
111+
return utils.defineTemplateBodyVisitor(context, {
112+
"VAttribute[directive=true][key.name.name='slot']" (node) {
113+
const expected = getExpectedStyle(options, node)
114+
const actual = getActualStyle(node)
115+
if (actual === expected) {
116+
return
117+
}
118+
119+
const { name, argument } = node.key
120+
const range = [name.range[0], (argument || name).range[1]]
121+
const argumentText = argument ? sourceCode.getText(argument) : 'default'
122+
context.report({
123+
node,
124+
messageId: `expected${pascalCase(expected)}`,
125+
data: {
126+
actual: sourceCode.text.slice(range[0], range[1]),
127+
argument: argumentText
128+
},
129+
130+
fix (fixer) {
131+
switch (expected) {
132+
case 'shorthand':
133+
return fixer.replaceTextRange(range, `#${argumentText}`)
134+
case 'longform':
135+
return fixer.replaceTextRange(range, `v-slot:${argumentText}`)
136+
case 'v-slot':
137+
return fixer.replaceTextRange(range, 'v-slot')
138+
default:
139+
return null
140+
}
141+
}
142+
})
143+
}
144+
})
145+
}
146+
}

0 commit comments

Comments
 (0)