Skip to content

Commit

Permalink
Fix false positives when using v-for variable for v-slot in `vue/vali…
Browse files Browse the repository at this point in the history
…d-v-slot` rule (#1366)
  • Loading branch information
ota-meshi committed Dec 4, 2020
1 parent 805b3f5 commit 0c5f2a1
Show file tree
Hide file tree
Showing 2 changed files with 187 additions and 12 deletions.
138 changes: 126 additions & 12 deletions lib/rules/valid-v-slot.js
Expand Up @@ -6,6 +6,10 @@

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

/**
* @typedef { { expr: VForExpression, variables: VVariable[] } } VSlotVForVariables
*/

/**
* Get all `v-slot` directives on a given element.
* @param {VElement} node The VElement node to check.
Expand Down Expand Up @@ -93,27 +97,128 @@ function getNormalizedName(node, sourceCode) {
* Get all `v-slot` directives which are distributed to the same slot as a given `v-slot` directive node.
* @param {VDirective[][]} vSlotGroups The result of `getAllNamedSlotElements()`.
* @param {VDirective} currentVSlot The current `v-slot` directive node.
* @param {VSlotVForVariables | null} currentVSlotVForVars The current `v-for` variables.
* @param {SourceCode} sourceCode The source code.
* @param {ParserServices.TokenStore} tokenStore The token store.
* @returns {VDirective[][]} The array of the group of `v-slot` directives.
*/
function filterSameSlot(vSlotGroups, currentVSlot, sourceCode) {
function filterSameSlot(
vSlotGroups,
currentVSlot,
currentVSlotVForVars,
sourceCode,
tokenStore
) {
const currentName = getNormalizedName(currentVSlot, sourceCode)
return vSlotGroups
.map((vSlots) =>
vSlots.filter(
(vSlot) => getNormalizedName(vSlot, sourceCode) === currentName
)
vSlots.filter((vSlot) => {
if (getNormalizedName(vSlot, sourceCode) !== currentName) {
return false
}
const vForExpr = getVSlotVForVariableIfUsingIterationVars(
vSlot,
utils.getDirective(vSlot.parent.parent, 'for')
)
if (!currentVSlotVForVars || !vForExpr) {
return !currentVSlotVForVars && !vForExpr
}
if (
!equalVSlotVForVariables(currentVSlotVForVars, vForExpr, tokenStore)
) {
return false
}
//
return true
})
)
.filter((slots) => slots.length >= 1)
}

/**
* Check whether a given argument node is using an iteration variable that the element defined.
* Determines whether the two given `v-slot` variables are considered to be equal.
* @param {VSlotVForVariables} a First element.
* @param {VSlotVForVariables} b Second element.
* @param {ParserServices.TokenStore} tokenStore The token store.
* @returns {boolean} `true` if the elements are considered to be equal.
*/
function equalVSlotVForVariables(a, b, tokenStore) {
if (a.variables.length !== b.variables.length) {
return false
}
if (!equal(a.expr.right, b.expr.right)) {
return false
}

const checkedVarNames = new Set()
const len = Math.min(a.expr.left.length, b.expr.left.length)
for (let index = 0; index < len; index++) {
const aPtn = a.expr.left[index]
const bPtn = b.expr.left[index]

const aVar = a.variables.find(
(v) => aPtn.range[0] <= v.id.range[0] && v.id.range[1] <= aPtn.range[1]
)
const bVar = b.variables.find(
(v) => bPtn.range[0] <= v.id.range[0] && v.id.range[1] <= bPtn.range[1]
)
if (aVar && bVar) {
if (aVar.id.name !== bVar.id.name) {
return false
}
if (!equal(aPtn, bPtn)) {
return false
}
checkedVarNames.add(aVar.id.name)
} else if (aVar || bVar) {
return false
}
}
for (const v of a.variables) {
if (!checkedVarNames.has(v.id.name)) {
if (b.variables.every((bv) => v.id.name !== bv.id.name)) {
return false
}
}
}
return true

/**
* Determines whether the two given nodes are considered to be equal.
* @param {ASTNode} a First node.
* @param {ASTNode} b Second node.
* @returns {boolean} `true` if the nodes are considered to be equal.
*/
function equal(a, b) {
if (a.type !== b.type) {
return false
}
return utils.equalTokens(a, b, tokenStore)
}
}

/**
* Gets the `v-for` directive and variable that provide the variables used by the given` v-slot` directive.
* @param {VDirective} vSlot The current `v-slot` directive node.
* @param {VDirective | null} [vFor] The current `v-for` directive node.
* @returns { VSlotVForVariables | null } The VSlotVForVariable.
*/
function getVSlotVForVariableIfUsingIterationVars(vSlot, vFor) {
const expr =
vFor && vFor.value && /** @type {VForExpression} */ (vFor.value.expression)
const variables =
expr && getUsingIterationVars(vSlot.key.argument, vSlot.parent.parent)
return expr && variables && variables.length ? { expr, variables } : null
}

/**
* Gets iterative variables if a given argument node is using iterative variables that the element defined.
* @param {VExpressionContainer|VIdentifier|null} argument The argument node to check.
* @param {VElement} element The element node which has the argument.
* @returns {boolean} `true` if the argument node is using the iteration variable.
* @returns {VVariable[]} The argument node is using iteration variables.
*/
function isUsingIterationVar(argument, element) {
function getUsingIterationVars(argument, element) {
const vars = []
if (argument && argument.type === 'VExpressionContainer') {
for (const { variable } of argument.references) {
if (
Expand All @@ -122,11 +227,11 @@ function isUsingIterationVar(argument, element) {
variable.id.range[0] > element.startTag.range[0] &&
variable.id.range[1] < element.startTag.range[1]
) {
return true
vars.push(variable)
}
}
}
return false
return vars
}

/**
Expand Down Expand Up @@ -206,6 +311,9 @@ module.exports = {
/** @param {RuleContext} context */
create(context) {
const sourceCode = context.getSourceCode()
const tokenStore =
context.parserServices.getTemplateBodyTokenStore &&
context.parserServices.getTemplateBodyTokenStore()
const options = context.options[0] || {}
const allowModifiers = options.allowModifiers === true

Expand Down Expand Up @@ -256,12 +364,18 @@ module.exports = {
})
}
if (ownerElement === parentElement) {
const vFor = utils.getDirective(element, 'for')
const vSlotVForVar = getVSlotVForVariableIfUsingIterationVars(
node,
vFor
)
const vSlotGroupsOfSameSlot = filterSameSlot(
vSlotGroupsOnChildren,
node,
sourceCode
vSlotVForVar,
sourceCode,
tokenStore
)
const vFor = utils.getDirective(element, 'for')
if (
vSlotGroupsOfSameSlot.length >= 2 &&
!vSlotGroupsOfSameSlot[0].includes(node)
Expand All @@ -273,7 +387,7 @@ module.exports = {
messageId: 'disallowDuplicateSlotsOnChildren'
})
}
if (vFor && !isUsingIterationVar(node.key.argument, element)) {
if (vFor && !vSlotVForVar) {
// E.g., <template v-for="x of xs" #one></template>
context.report({
node,
Expand Down
61 changes: 61 additions & 0 deletions tests/lib/rules/valid-v-slot.js
Expand Up @@ -84,6 +84,30 @@ tester.run('valid-v-slot', rule, {
<template v-for="(key, value) in xxxx" #[key]>{{value}}</template>
</MyComponent>
</template>`,
`<template>
<MyComponent>
<template v-for="(key, value) in xxxx" #[key]>{{value}}</template>
<template v-for="(key, value) in yyyy" #[key]>{{value}}</template>
</MyComponent>
</template>`,
`<template>
<MyComponent>
<template #[key]>{{value}}</template>
<template v-for="(key, value) in yyyy" #[key]>{{value}}</template>
</MyComponent>
</template>`,
`<template>
<MyComponent>
<template v-for="(value, key) in xxxx" #[key]>{{value}}</template>
<template v-for="(key, value) in xxxx" #[key]>{{value}}</template>
</MyComponent>
</template>`,
`<template>
<MyComponent>
<template v-for="(key) in xxxx" #[key+value]>{{value}}</template>
<template v-for="(key, value) in xxxx" #[key+value]>{{value}}</template>
</MyComponent>
</template>`,
{
code: `
<template>
Expand Down Expand Up @@ -282,6 +306,43 @@ tester.run('valid-v-slot', rule, {
`,
errors: [{ messageId: 'disallowDuplicateSlotsOnChildren' }]
},
{
code: `
<template>
<MyComponent>
<template v-for="(key, value) in xxxx" v-slot:key>{{value}}</template>
<template v-for="(key, value) in yyyy" v-slot:key>{{value}}</template>
</MyComponent>
</template>
`,
errors: [
{ messageId: 'disallowDuplicateSlotsOnChildren' },
{ messageId: 'disallowDuplicateSlotsOnChildren' },
{ messageId: 'disallowDuplicateSlotsOnChildren' }
]
},
{
code: `
<template>
<MyComponent>
<template v-for="(key, value) in xxxx" v-slot:[key]>{{value}}</template>
<template v-for="(key, value) in xxxx" v-slot:[key]>{{value}}</template>
</MyComponent>
</template>
`,
errors: [{ messageId: 'disallowDuplicateSlotsOnChildren' }]
},
{
code: `
<template>
<MyComponent>
<template v-for="(key) in xxxx" v-slot:[key]>{{value}}</template>
<template v-for="(key, value) in xxxx" v-slot:[key]>{{value}}</template>
</MyComponent>
</template>
`,
errors: [{ messageId: 'disallowDuplicateSlotsOnChildren' }]
},
{
code: `
<template>
Expand Down

0 comments on commit 0c5f2a1

Please sign in to comment.