Skip to content

Commit 263076a

Browse files
authored
New: valid-v-slot rule (fixes #802) (#837)
1 parent 9f2cec1 commit 263076a

File tree

3 files changed

+661
-0
lines changed

3 files changed

+661
-0
lines changed

Diff for: docs/rules/valid-v-slot.md

+112
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
---
2+
pageClass: rule-details
3+
sidebarDepth: 0
4+
title: vue/valid-v-slot
5+
description: enforce valid `v-slot` directives
6+
---
7+
# vue/valid-v-slot
8+
> enforce valid `v-slot` directives
9+
10+
This rule checks whether every `v-slot` directive is valid.
11+
12+
## :book: Rule Details
13+
14+
This rule reports `v-slot` directives in the following cases:
15+
16+
- The directive is not owned by a custom element. E.g. `<div v-slot=""></div>`
17+
- The directive is a named slot and is on a custom element directly. E.g. `<my-component v-slot:foo></my-component>`
18+
- The directive is the default slot, is on a custom element directly, and there are other named slots. E.g. `<my-component v-slot=""><template v-slot:foo></template></my-component>`
19+
- The element which has the directive has another `v-slot` directive. E.g. `<my-component v-slot:one v-slot:two></my-component>`
20+
- The element which has the directive has another `v-slot` directive that is distributed to the same slot. E.g. `<my-component><template v-slot:foo></template><template v-slot:foo></template></my-component>`
21+
- The directive has a dynamic argument which uses the scope properties that the directive defined. E.g. `<my-component><template v-slot:[data]="data"></template></my-component>`
22+
- The directive has any modifier. E.g. `<my-component v-slot.foo></my-component>`
23+
- The directive is the default slot, is on a custom element directly, and has no value. E.g. `<my-component v-slot></my-component>`
24+
25+
<eslint-code-block :rules="{'vue/valid-v-slot': ['error']}">
26+
27+
```vue
28+
<template>
29+
<!-- ✓ GOOD -->
30+
<my-component v-slot="data">
31+
{{data}}
32+
</my-component>
33+
<my-component>
34+
<template v-slot:default>
35+
default
36+
</template>
37+
<template v-slot:one>
38+
one
39+
</template>
40+
<template v-slot:two>
41+
two
42+
</template>
43+
</my-component>
44+
45+
<!-- ✗ BAD -->
46+
<div v-slot="data">
47+
{{data}}
48+
</div>
49+
<div>
50+
<template v-slot:one>
51+
one
52+
</template>
53+
</div>
54+
55+
<my-component v-slot:one="data">
56+
{{data}}
57+
</my-component>
58+
<my-component v-slot="data">
59+
{{data}}
60+
<template v-slot:one>
61+
one
62+
</template>
63+
</my-component>
64+
65+
<my-component v-slot:one v-slot:two>
66+
one and two
67+
</my-component>
68+
<my-component>
69+
<template v-slot:one>
70+
one 1
71+
</template>
72+
<template v-slot:one>
73+
one 2
74+
</template>
75+
</my-component>
76+
77+
<my-component>
78+
<template v-slot:[data]="data">
79+
dynamic?
80+
</template>
81+
</my-component>
82+
83+
<my-component v-slot.mod="data">
84+
{{data}}
85+
</my-component>
86+
87+
<my-component v-slot>
88+
content
89+
</my-component>
90+
</template>
91+
```
92+
93+
</eslint-code-block>
94+
95+
::: warning Note
96+
This rule does not check syntax errors in directives because it's checked by [no-parsing-error] rule.
97+
:::
98+
99+
## :wrench: Options
100+
101+
Nothing.
102+
103+
## :couple: Related rules
104+
105+
- [no-parsing-error]
106+
107+
[no-parsing-error]: no-parsing-error.md
108+
109+
## :mag: Implementation
110+
111+
- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/valid-v-slot.js)
112+
- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/valid-v-slot.js)

Diff for: lib/rules/valid-v-slot.js

+252
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
/**
2+
* @author Toru Nagashima
3+
* See LICENSE file in root directory for full license.
4+
*/
5+
'use strict'
6+
7+
const utils = require('../utils')
8+
9+
/**
10+
* Get all `v-slot` directives on a given element.
11+
* @param {VElement} node The VElement node to check.
12+
* @returns {VAttribute[]} The array of `v-slot` directives.
13+
*/
14+
function getSlotDirectivesOnElement (node) {
15+
return node.startTag.attributes.filter(attribute =>
16+
attribute.directive &&
17+
attribute.key.name.name === 'slot'
18+
)
19+
}
20+
21+
/**
22+
* Get all `v-slot` directives on the children of a given element.
23+
* @param {VElement} node The VElement node to check.
24+
* @returns {VAttribute[][]}
25+
* The array of the group of `v-slot` directives.
26+
* The group bundles `v-slot` directives of element sequence which is connected
27+
* by `v-if`/`v-else-if`/`v-else`.
28+
*/
29+
function getSlotDirectivesOnChildren (node) {
30+
return node.children
31+
.reduce(({ groups, vIf }, childNode) => {
32+
if (childNode.type === 'VElement') {
33+
let connected
34+
if (utils.hasDirective(childNode, 'if')) {
35+
connected = false
36+
vIf = true
37+
} else if (utils.hasDirective(childNode, 'else-if')) {
38+
connected = vIf
39+
vIf = true
40+
} else if (utils.hasDirective(childNode, 'else')) {
41+
connected = vIf
42+
vIf = false
43+
} else {
44+
connected = false
45+
vIf = false
46+
}
47+
48+
if (connected) {
49+
groups[groups.length - 1].push(childNode)
50+
} else {
51+
groups.push([childNode])
52+
}
53+
} else if (childNode.type !== 'VText' || childNode.value.trim() !== '') {
54+
vIf = false
55+
}
56+
return { groups, vIf }
57+
}, { groups: [], vIf: false })
58+
.groups
59+
.map(group =>
60+
group
61+
.map(childElement =>
62+
childElement.name === 'template'
63+
? utils.getDirective(childElement, 'slot')
64+
: null
65+
)
66+
.filter(Boolean)
67+
)
68+
.filter(group => group.length >= 1)
69+
}
70+
71+
/**
72+
* Get the normalized name of a given `v-slot` directive node.
73+
* @param {VAttribute} node The `v-slot` directive node.
74+
* @returns {string} The normalized name.
75+
*/
76+
function getNormalizedName (node, sourceCode) {
77+
return node.key.argument == null ? 'default' : sourceCode.getText(node.key.argument)
78+
}
79+
80+
/**
81+
* Get all `v-slot` directives which are distributed to the same slot as a given `v-slot` directive node.
82+
* @param {VAttribute[][]} vSlotGroups The result of `getAllNamedSlotElements()`.
83+
* @param {VElement} currentVSlot The current `v-slot` directive node.
84+
* @returns {VAttribute[][]} The array of the group of `v-slot` directives.
85+
*/
86+
function filterSameSlot (vSlotGroups, currentVSlot, sourceCode) {
87+
const currentName = getNormalizedName(currentVSlot, sourceCode)
88+
return vSlotGroups
89+
.map(vSlots =>
90+
vSlots.filter(vSlot => getNormalizedName(vSlot, sourceCode) === currentName)
91+
)
92+
.filter(slots => slots.length >= 1)
93+
}
94+
95+
/**
96+
* Check whether a given argument node is using an iteration variable that the element defined.
97+
* @param {VExpressionContainer|VIdentifier|null} argument The argument node to check.
98+
* @param {VElement} element The element node which has the argument.
99+
* @returns {boolean} `true` if the argument node is using the iteration variable.
100+
*/
101+
function isUsingIterationVar (argument, element) {
102+
if (argument && argument.type === 'VExpressionContainer') {
103+
for (const { variable } of argument.references) {
104+
if (
105+
variable != null &&
106+
variable.kind === 'v-for' &&
107+
variable.id.range[0] > element.startTag.range[0] &&
108+
variable.id.range[1] < element.startTag.range[1]
109+
) {
110+
return true
111+
}
112+
}
113+
}
114+
return false
115+
}
116+
117+
/**
118+
* Check whether a given argument node is using an scope variable that the directive defined.
119+
* @param {VAttribute} vSlot The `v-slot` directive to check.
120+
* @returns {boolean} `true` if that argument node is using a scope variable the directive defined.
121+
*/
122+
function isUsingScopeVar (vSlot) {
123+
const argument = vSlot.key.argument
124+
const value = vSlot.value
125+
126+
if (argument && value && argument.type === 'VExpressionContainer') {
127+
for (const { variable } of argument.references) {
128+
if (
129+
variable != null &&
130+
variable.kind === 'scope' &&
131+
variable.id.range[0] > value.range[0] &&
132+
variable.id.range[1] < value.range[1]
133+
) {
134+
return true
135+
}
136+
}
137+
}
138+
}
139+
140+
module.exports = {
141+
meta: {
142+
type: 'problem',
143+
docs: {
144+
description: 'enforce valid `v-slot` directives',
145+
category: undefined, // essential
146+
url: 'https://eslint.vuejs.org/rules/valid-v-slot.html'
147+
},
148+
fixable: null,
149+
schema: [],
150+
messages: {
151+
ownerMustBeCustomElement: "'v-slot' directive must be owned by a custom element, but '{{name}}' is not.",
152+
namedSlotMustBeOnTemplate: "Named slots must use '<template>' on a custom element.",
153+
defaultSlotMustBeOnTemplate: "Default slot must use '<template>' on a custom element when there are other named slots.",
154+
disallowDuplicateSlotsOnElement: "An element cannot have multiple 'v-slot' directives.",
155+
disallowDuplicateSlotsOnChildren: "An element cannot have multiple '<template>' elements which are distributed to the same slot.",
156+
disallowArgumentUseSlotParams: "Dynamic argument of 'v-slot' directive cannot use that slot parameter.",
157+
disallowAnyModifier: "'v-slot' directive doesn't support any modifier.",
158+
requireAttributeValue: "'v-slot' directive on a custom element requires that attribute value."
159+
}
160+
},
161+
162+
create (context) {
163+
const sourceCode = context.getSourceCode()
164+
165+
return utils.defineTemplateBodyVisitor(context, {
166+
"VAttribute[directive=true][key.name.name='slot']" (node) {
167+
const isDefaultSlot = node.key.argument == null || node.key.argument.name === 'default'
168+
const element = node.parent.parent
169+
const parentElement = element.parent
170+
const ownerElement = element.name === 'template' ? parentElement : element
171+
const vSlotsOnElement = getSlotDirectivesOnElement(element)
172+
const vSlotGroupsOnChildren = getSlotDirectivesOnChildren(ownerElement)
173+
174+
// Verify location.
175+
if (!utils.isCustomComponent(ownerElement)) {
176+
context.report({
177+
node,
178+
messageId: 'ownerMustBeCustomElement',
179+
data: { name: ownerElement.rawName }
180+
})
181+
}
182+
if (!isDefaultSlot && element.name !== 'template') {
183+
context.report({
184+
node,
185+
messageId: 'namedSlotMustBeOnTemplate'
186+
})
187+
}
188+
if (ownerElement === element && vSlotGroupsOnChildren.length >= 1) {
189+
context.report({
190+
node,
191+
messageId: 'defaultSlotMustBeOnTemplate'
192+
})
193+
}
194+
195+
// Verify duplication.
196+
if (vSlotsOnElement.length >= 2 && vSlotsOnElement[0] !== node) {
197+
// E.g., <my-component #one #two>
198+
context.report({
199+
node,
200+
messageId: 'disallowDuplicateSlotsOnElement'
201+
})
202+
}
203+
if (ownerElement === parentElement) {
204+
const vSlotGroupsOfSameSlot = filterSameSlot(vSlotGroupsOnChildren, node, sourceCode)
205+
const vFor = utils.getDirective(element, 'for')
206+
if (
207+
vSlotGroupsOfSameSlot.length >= 2 &&
208+
!vSlotGroupsOfSameSlot[0].includes(node)
209+
) {
210+
// E.g., <template #one></template>
211+
// <template #one></template>
212+
context.report({
213+
node,
214+
messageId: 'disallowDuplicateSlotsOnChildren'
215+
})
216+
}
217+
if (vFor && !isUsingIterationVar(node.key.argument, element)) {
218+
// E.g., <template v-for="x of xs" #one></template>
219+
context.report({
220+
node,
221+
messageId: 'disallowDuplicateSlotsOnChildren'
222+
})
223+
}
224+
}
225+
226+
// Verify argument.
227+
if (isUsingScopeVar(node)) {
228+
context.report({
229+
node,
230+
messageId: 'disallowArgumentUseSlotParams'
231+
})
232+
}
233+
234+
// Verify modifiers.
235+
if (node.key.modifiers.length >= 1) {
236+
context.report({
237+
node,
238+
messageId: 'disallowAnyModifier'
239+
})
240+
}
241+
242+
// Verify value.
243+
if (ownerElement === element && isDefaultSlot && !utils.hasAttributeValue(node)) {
244+
context.report({
245+
node,
246+
messageId: 'requireAttributeValue'
247+
})
248+
}
249+
}
250+
})
251+
}
252+
}

0 commit comments

Comments
 (0)