Skip to content

Commit 0c5f2a1

Browse files
authored
Fix false positives when using v-for variable for v-slot in vue/valid-v-slot rule (#1366)
1 parent 805b3f5 commit 0c5f2a1

File tree

2 files changed

+187
-12
lines changed

2 files changed

+187
-12
lines changed

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

+126-12
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@
66

77
const utils = require('../utils')
88

9+
/**
10+
* @typedef { { expr: VForExpression, variables: VVariable[] } } VSlotVForVariables
11+
*/
12+
913
/**
1014
* Get all `v-slot` directives on a given element.
1115
* @param {VElement} node The VElement node to check.
@@ -93,27 +97,128 @@ function getNormalizedName(node, sourceCode) {
9397
* Get all `v-slot` directives which are distributed to the same slot as a given `v-slot` directive node.
9498
* @param {VDirective[][]} vSlotGroups The result of `getAllNamedSlotElements()`.
9599
* @param {VDirective} currentVSlot The current `v-slot` directive node.
100+
* @param {VSlotVForVariables | null} currentVSlotVForVars The current `v-for` variables.
96101
* @param {SourceCode} sourceCode The source code.
102+
* @param {ParserServices.TokenStore} tokenStore The token store.
97103
* @returns {VDirective[][]} The array of the group of `v-slot` directives.
98104
*/
99-
function filterSameSlot(vSlotGroups, currentVSlot, sourceCode) {
105+
function filterSameSlot(
106+
vSlotGroups,
107+
currentVSlot,
108+
currentVSlotVForVars,
109+
sourceCode,
110+
tokenStore
111+
) {
100112
const currentName = getNormalizedName(currentVSlot, sourceCode)
101113
return vSlotGroups
102114
.map((vSlots) =>
103-
vSlots.filter(
104-
(vSlot) => getNormalizedName(vSlot, sourceCode) === currentName
105-
)
115+
vSlots.filter((vSlot) => {
116+
if (getNormalizedName(vSlot, sourceCode) !== currentName) {
117+
return false
118+
}
119+
const vForExpr = getVSlotVForVariableIfUsingIterationVars(
120+
vSlot,
121+
utils.getDirective(vSlot.parent.parent, 'for')
122+
)
123+
if (!currentVSlotVForVars || !vForExpr) {
124+
return !currentVSlotVForVars && !vForExpr
125+
}
126+
if (
127+
!equalVSlotVForVariables(currentVSlotVForVars, vForExpr, tokenStore)
128+
) {
129+
return false
130+
}
131+
//
132+
return true
133+
})
106134
)
107135
.filter((slots) => slots.length >= 1)
108136
}
109137

110138
/**
111-
* Check whether a given argument node is using an iteration variable that the element defined.
139+
* Determines whether the two given `v-slot` variables are considered to be equal.
140+
* @param {VSlotVForVariables} a First element.
141+
* @param {VSlotVForVariables} b Second element.
142+
* @param {ParserServices.TokenStore} tokenStore The token store.
143+
* @returns {boolean} `true` if the elements are considered to be equal.
144+
*/
145+
function equalVSlotVForVariables(a, b, tokenStore) {
146+
if (a.variables.length !== b.variables.length) {
147+
return false
148+
}
149+
if (!equal(a.expr.right, b.expr.right)) {
150+
return false
151+
}
152+
153+
const checkedVarNames = new Set()
154+
const len = Math.min(a.expr.left.length, b.expr.left.length)
155+
for (let index = 0; index < len; index++) {
156+
const aPtn = a.expr.left[index]
157+
const bPtn = b.expr.left[index]
158+
159+
const aVar = a.variables.find(
160+
(v) => aPtn.range[0] <= v.id.range[0] && v.id.range[1] <= aPtn.range[1]
161+
)
162+
const bVar = b.variables.find(
163+
(v) => bPtn.range[0] <= v.id.range[0] && v.id.range[1] <= bPtn.range[1]
164+
)
165+
if (aVar && bVar) {
166+
if (aVar.id.name !== bVar.id.name) {
167+
return false
168+
}
169+
if (!equal(aPtn, bPtn)) {
170+
return false
171+
}
172+
checkedVarNames.add(aVar.id.name)
173+
} else if (aVar || bVar) {
174+
return false
175+
}
176+
}
177+
for (const v of a.variables) {
178+
if (!checkedVarNames.has(v.id.name)) {
179+
if (b.variables.every((bv) => v.id.name !== bv.id.name)) {
180+
return false
181+
}
182+
}
183+
}
184+
return true
185+
186+
/**
187+
* Determines whether the two given nodes are considered to be equal.
188+
* @param {ASTNode} a First node.
189+
* @param {ASTNode} b Second node.
190+
* @returns {boolean} `true` if the nodes are considered to be equal.
191+
*/
192+
function equal(a, b) {
193+
if (a.type !== b.type) {
194+
return false
195+
}
196+
return utils.equalTokens(a, b, tokenStore)
197+
}
198+
}
199+
200+
/**
201+
* Gets the `v-for` directive and variable that provide the variables used by the given` v-slot` directive.
202+
* @param {VDirective} vSlot The current `v-slot` directive node.
203+
* @param {VDirective | null} [vFor] The current `v-for` directive node.
204+
* @returns { VSlotVForVariables | null } The VSlotVForVariable.
205+
*/
206+
function getVSlotVForVariableIfUsingIterationVars(vSlot, vFor) {
207+
const expr =
208+
vFor && vFor.value && /** @type {VForExpression} */ (vFor.value.expression)
209+
const variables =
210+
expr && getUsingIterationVars(vSlot.key.argument, vSlot.parent.parent)
211+
return expr && variables && variables.length ? { expr, variables } : null
212+
}
213+
214+
/**
215+
* Gets iterative variables if a given argument node is using iterative variables that the element defined.
112216
* @param {VExpressionContainer|VIdentifier|null} argument The argument node to check.
113217
* @param {VElement} element The element node which has the argument.
114-
* @returns {boolean} `true` if the argument node is using the iteration variable.
218+
* @returns {VVariable[]} The argument node is using iteration variables.
115219
*/
116-
function isUsingIterationVar(argument, element) {
220+
function getUsingIterationVars(argument, element) {
221+
const vars = []
117222
if (argument && argument.type === 'VExpressionContainer') {
118223
for (const { variable } of argument.references) {
119224
if (
@@ -122,11 +227,11 @@ function isUsingIterationVar(argument, element) {
122227
variable.id.range[0] > element.startTag.range[0] &&
123228
variable.id.range[1] < element.startTag.range[1]
124229
) {
125-
return true
230+
vars.push(variable)
126231
}
127232
}
128233
}
129-
return false
234+
return vars
130235
}
131236

132237
/**
@@ -206,6 +311,9 @@ module.exports = {
206311
/** @param {RuleContext} context */
207312
create(context) {
208313
const sourceCode = context.getSourceCode()
314+
const tokenStore =
315+
context.parserServices.getTemplateBodyTokenStore &&
316+
context.parserServices.getTemplateBodyTokenStore()
209317
const options = context.options[0] || {}
210318
const allowModifiers = options.allowModifiers === true
211319

@@ -256,12 +364,18 @@ module.exports = {
256364
})
257365
}
258366
if (ownerElement === parentElement) {
367+
const vFor = utils.getDirective(element, 'for')
368+
const vSlotVForVar = getVSlotVForVariableIfUsingIterationVars(
369+
node,
370+
vFor
371+
)
259372
const vSlotGroupsOfSameSlot = filterSameSlot(
260373
vSlotGroupsOnChildren,
261374
node,
262-
sourceCode
375+
vSlotVForVar,
376+
sourceCode,
377+
tokenStore
263378
)
264-
const vFor = utils.getDirective(element, 'for')
265379
if (
266380
vSlotGroupsOfSameSlot.length >= 2 &&
267381
!vSlotGroupsOfSameSlot[0].includes(node)
@@ -273,7 +387,7 @@ module.exports = {
273387
messageId: 'disallowDuplicateSlotsOnChildren'
274388
})
275389
}
276-
if (vFor && !isUsingIterationVar(node.key.argument, element)) {
390+
if (vFor && !vSlotVForVar) {
277391
// E.g., <template v-for="x of xs" #one></template>
278392
context.report({
279393
node,

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

+61
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,30 @@ tester.run('valid-v-slot', rule, {
8484
<template v-for="(key, value) in xxxx" #[key]>{{value}}</template>
8585
</MyComponent>
8686
</template>`,
87+
`<template>
88+
<MyComponent>
89+
<template v-for="(key, value) in xxxx" #[key]>{{value}}</template>
90+
<template v-for="(key, value) in yyyy" #[key]>{{value}}</template>
91+
</MyComponent>
92+
</template>`,
93+
`<template>
94+
<MyComponent>
95+
<template #[key]>{{value}}</template>
96+
<template v-for="(key, value) in yyyy" #[key]>{{value}}</template>
97+
</MyComponent>
98+
</template>`,
99+
`<template>
100+
<MyComponent>
101+
<template v-for="(value, key) in xxxx" #[key]>{{value}}</template>
102+
<template v-for="(key, value) in xxxx" #[key]>{{value}}</template>
103+
</MyComponent>
104+
</template>`,
105+
`<template>
106+
<MyComponent>
107+
<template v-for="(key) in xxxx" #[key+value]>{{value}}</template>
108+
<template v-for="(key, value) in xxxx" #[key+value]>{{value}}</template>
109+
</MyComponent>
110+
</template>`,
87111
{
88112
code: `
89113
<template>
@@ -282,6 +306,43 @@ tester.run('valid-v-slot', rule, {
282306
`,
283307
errors: [{ messageId: 'disallowDuplicateSlotsOnChildren' }]
284308
},
309+
{
310+
code: `
311+
<template>
312+
<MyComponent>
313+
<template v-for="(key, value) in xxxx" v-slot:key>{{value}}</template>
314+
<template v-for="(key, value) in yyyy" v-slot:key>{{value}}</template>
315+
</MyComponent>
316+
</template>
317+
`,
318+
errors: [
319+
{ messageId: 'disallowDuplicateSlotsOnChildren' },
320+
{ messageId: 'disallowDuplicateSlotsOnChildren' },
321+
{ messageId: 'disallowDuplicateSlotsOnChildren' }
322+
]
323+
},
324+
{
325+
code: `
326+
<template>
327+
<MyComponent>
328+
<template v-for="(key, value) in xxxx" v-slot:[key]>{{value}}</template>
329+
<template v-for="(key, value) in xxxx" v-slot:[key]>{{value}}</template>
330+
</MyComponent>
331+
</template>
332+
`,
333+
errors: [{ messageId: 'disallowDuplicateSlotsOnChildren' }]
334+
},
335+
{
336+
code: `
337+
<template>
338+
<MyComponent>
339+
<template v-for="(key) in xxxx" v-slot:[key]>{{value}}</template>
340+
<template v-for="(key, value) in xxxx" v-slot:[key]>{{value}}</template>
341+
</MyComponent>
342+
</template>
343+
`,
344+
errors: [{ messageId: 'disallowDuplicateSlotsOnChildren' }]
345+
},
285346
{
286347
code: `
287348
<template>

0 commit comments

Comments
 (0)