Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix false positives when using v-for variable for v-slot in vue/valid-v-slot rule #1366

Merged
merged 1 commit into from Dec 4, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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