diff --git a/lib/rules/valid-v-slot.js b/lib/rules/valid-v-slot.js index 07025f5e7..f5ff3a0e7 100644 --- a/lib/rules/valid-v-slot.js +++ b/lib/rules/valid-v-slot.js @@ -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. @@ -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 ( @@ -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 } /** @@ -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 @@ -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) @@ -273,7 +387,7 @@ module.exports = { messageId: 'disallowDuplicateSlotsOnChildren' }) } - if (vFor && !isUsingIterationVar(node.key.argument, element)) { + if (vFor && !vSlotVForVar) { // E.g., context.report({ node, diff --git a/tests/lib/rules/valid-v-slot.js b/tests/lib/rules/valid-v-slot.js index 08b55224d..dfcb0eb75 100644 --- a/tests/lib/rules/valid-v-slot.js +++ b/tests/lib/rules/valid-v-slot.js @@ -84,6 +84,30 @@ tester.run('valid-v-slot', rule, { `, + ``, + ``, + ``, + ``, { code: `