diff --git a/lib/rules/syntaxes/slot-attribute.js b/lib/rules/syntaxes/slot-attribute.js index d5fa61898..4cfe2099c 100644 --- a/lib/rules/syntaxes/slot-attribute.js +++ b/lib/rules/syntaxes/slot-attribute.js @@ -3,12 +3,18 @@ * See LICENSE file in root directory for full license. */ 'use strict' + +const canConvertToVSlot = require('./utils/can-convert-to-v-slot') + module.exports = { deprecated: '2.6.0', supported: '<3.0.0', /** @param {RuleContext} context @returns {TemplateListener} */ createTemplateBodyVisitor(context) { const sourceCode = context.getSourceCode() + const tokenStore = + context.parserServices.getTemplateBodyTokenStore && + context.parserServices.getTemplateBodyTokenStore() /** * Checks whether the given node can convert to the `v-slot`. @@ -16,7 +22,7 @@ module.exports = { * @returns {boolean} `true` if the given node can convert to the `v-slot` */ function canConvertFromSlotToVSlot(slotAttr) { - if (slotAttr.parent.parent.name !== 'template') { + if (!canConvertToVSlot(slotAttr.parent.parent, sourceCode, tokenStore)) { return false } if (!slotAttr.value) { @@ -33,7 +39,7 @@ module.exports = { * @returns {boolean} `true` if the given node can convert to the `v-slot` */ function canConvertFromVBindSlotToVSlot(slotAttr) { - if (slotAttr.parent.parent.name !== 'template') { + if (!canConvertToVSlot(slotAttr.parent.parent, sourceCode, tokenStore)) { return false } diff --git a/lib/rules/syntaxes/slot-scope-attribute.js b/lib/rules/syntaxes/slot-scope-attribute.js index 902b5ded8..53a54d6de 100644 --- a/lib/rules/syntaxes/slot-scope-attribute.js +++ b/lib/rules/syntaxes/slot-scope-attribute.js @@ -3,6 +3,9 @@ * See LICENSE file in root directory for full license. */ 'use strict' + +const canConvertToVSlotForElement = require('./utils/can-convert-to-v-slot') + module.exports = { deprecated: '2.6.0', supported: '>=2.5.0 <3.0.0', @@ -14,6 +17,9 @@ module.exports = { */ createTemplateBodyVisitor(context, { fixToUpgrade } = {}) { const sourceCode = context.getSourceCode() + const tokenStore = + context.parserServices.getTemplateBodyTokenStore && + context.parserServices.getTemplateBodyTokenStore() /** * Checks whether the given node can convert to the `v-slot`. @@ -21,7 +27,9 @@ module.exports = { * @returns {boolean} `true` if the given node can convert to the `v-slot` */ function canConvertToVSlot(startTag) { - if (startTag.parent.name !== 'template') { + if ( + !canConvertToVSlotForElement(startTag.parent, sourceCode, tokenStore) + ) { return false } diff --git a/lib/rules/syntaxes/utils/can-convert-to-v-slot.js b/lib/rules/syntaxes/utils/can-convert-to-v-slot.js new file mode 100644 index 000000000..3bdb7e1bc --- /dev/null +++ b/lib/rules/syntaxes/utils/can-convert-to-v-slot.js @@ -0,0 +1,228 @@ +/** + * @author Yosuke Ota + * See LICENSE file in root directory for full license. + */ +'use strict' + +const utils = require('../../../utils') +/** + * @typedef {object} SlotVForVariables + * @property {VForExpression} expr + * @property {VVariable[]} variables + */ +/** + * @typedef {object} SlotContext + * @property {VElement} element + * @property {VAttribute | VDirective | null} slot + * @property {VDirective | null} vFor + * @property {SlotVForVariables | null} slotVForVars + * @property {string} normalizedName + */ +/** + * Checks whether the given element can use v-slot. + * @param {VElement} element + * @param {SourceCode} sourceCode + * @param {ParserServices.TokenStore} tokenStore + */ +module.exports = function canConvertToVSlot(element, sourceCode, tokenStore) { + if (element.name !== 'template') { + return false + } + const ownerElement = element.parent + if ( + ownerElement.type === 'VDocumentFragment' || + !utils.isCustomComponent(ownerElement) + ) { + return false + } + const slot = getSlotContext(element, sourceCode) + if (slot.vFor && !slot.slotVForVars) { + // E.g., + return false + } + if (hasSameSlotDirective(ownerElement, slot, sourceCode, tokenStore)) { + return false + } + return true +} +/** + * @param {VElement} element + * @param {SourceCode} sourceCode + * @returns {SlotContext} + */ +function getSlotContext(element, sourceCode) { + const slot = + utils.getAttribute(element, 'slot') || + utils.getDirective(element, 'bind', 'slot') + const vFor = utils.getDirective(element, 'for') + const slotVForVars = getSlotVForVariableIfUsingIterationVars(slot, vFor) + + return { + element, + slot, + vFor, + slotVForVars, + normalizedName: getNormalizedName(slot, sourceCode) + } +} + +/** + * Gets the `v-for` directive and variable that provide the variables used by the given `slot` attribute. + * @param {VAttribute | VDirective | null} slot The current `slot` attribute node. + * @param {VDirective | null} [vFor] The current `v-for` directive node. + * @returns { SlotVForVariables | null } The SlotVForVariables. + */ +function getSlotVForVariableIfUsingIterationVars(slot, vFor) { + if (!slot || !slot.directive) { + return null + } + const expr = + vFor && vFor.value && /** @type {VForExpression} */ (vFor.value.expression) + const variables = + expr && getUsingIterationVars(slot.value, slot.parent.parent) + return expr && variables && variables.length ? { expr, variables } : null +} + +/** + * Gets iterative variables if a given expression node is using iterative variables that the element defined. + * @param {VExpressionContainer|null} expression The expression node to check. + * @param {VElement} element The element node which has the expression. + * @returns {VVariable[]} The expression node is using iteration variables. + */ +function getUsingIterationVars(expression, element) { + const vars = [] + if (expression && expression.type === 'VExpressionContainer') { + for (const { variable } of expression.references) { + if ( + variable != null && + variable.kind === 'v-for' && + variable.id.range[0] > element.startTag.range[0] && + variable.id.range[1] < element.startTag.range[1] + ) { + vars.push(variable) + } + } + } + return vars +} + +/** + * Get the normalized name of a given `slot` attribute node. + * @param {VAttribute | VDirective | null} slotAttr node of `slot` + * @param {SourceCode} sourceCode The source code. + * @returns {string} The normalized name. + */ +function getNormalizedName(slotAttr, sourceCode) { + if (!slotAttr) { + return 'default' + } + if (!slotAttr.directive) { + return slotAttr.value ? slotAttr.value.value : 'default' + } + return slotAttr.value ? `[${sourceCode.getText(slotAttr.value)}]` : '[null]' +} + +/** + * Checks whether parent element has the same slot as the given slot. + * @param {VElement} ownerElement The parent element. + * @param {SlotContext} targetSlot The SlotContext with a slot to check if they are the same. + * @param {SourceCode} sourceCode + * @param {ParserServices.TokenStore} tokenStore + */ +function hasSameSlotDirective( + ownerElement, + targetSlot, + sourceCode, + tokenStore +) { + for (const group of utils.iterateChildElementsChains(ownerElement)) { + if (group.includes(targetSlot.element)) { + continue + } + for (const childElement of group) { + const slot = getSlotContext(childElement, sourceCode) + if (!targetSlot.slotVForVars || !slot.slotVForVars) { + if ( + !targetSlot.slotVForVars && + !slot.slotVForVars && + targetSlot.normalizedName === slot.normalizedName + ) { + return true + } + continue + } + if ( + equalSlotVForVariables( + targetSlot.slotVForVars, + slot.slotVForVars, + tokenStore + ) + ) { + return true + } + } + } + return false +} + +/** + * Determines whether the two given `v-slot` variables are considered to be equal. + * @param {SlotVForVariables} a First element. + * @param {SlotVForVariables} b Second element. + * @param {ParserServices.TokenStore} tokenStore The token store. + * @returns {boolean} `true` if the elements are considered to be equal. + */ +function equalSlotVForVariables(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) + } +} diff --git a/lib/rules/valid-v-slot.js b/lib/rules/valid-v-slot.js index 4914edef9..8d484d253 100644 --- a/lib/rules/valid-v-slot.js +++ b/lib/rules/valid-v-slot.js @@ -28,54 +28,22 @@ function getSlotDirectivesOnElement(node) { * by `v-if`/`v-else-if`/`v-else`. */ function getSlotDirectivesOnChildren(node) { - return node.children - .reduce( - ({ groups, vIf }, childNode) => { - if (childNode.type === 'VElement') { - let connected - if (utils.hasDirective(childNode, 'if')) { - connected = false - vIf = true - } else if (utils.hasDirective(childNode, 'else-if')) { - connected = vIf - vIf = true - } else if (utils.hasDirective(childNode, 'else')) { - connected = vIf - vIf = false - } else { - connected = false - vIf = false - } + /** @type {VDirective[][]} */ + const groups = [] + for (const group of utils.iterateChildElementsChains(node)) { + const slotDirs = group + .map((childElement) => + childElement.name === 'template' + ? utils.getDirective(childElement, 'slot') + : null + ) + .filter(utils.isDef) + if (slotDirs.length > 0) { + groups.push(slotDirs) + } + } - if (connected) { - groups[groups.length - 1].push(childNode) - } else { - groups.push([childNode]) - } - } else if ( - childNode.type !== 'VText' || - childNode.value.trim() !== '' - ) { - vIf = false - } - return { groups, vIf } - }, - { - /** @type {VElement[][]} */ - groups: [], - vIf: false - } - ) - .groups.map((group) => - group - .map((childElement) => - childElement.name === 'template' - ? utils.getDirective(childElement, 'slot') - : null - ) - .filter(utils.isDef) - ) - .filter((group) => group.length >= 1) + return groups } /** diff --git a/lib/utils/index.js b/lib/utils/index.js index 05e45d99c..06ef51420 100644 --- a/lib/utils/index.js +++ b/lib/utils/index.js @@ -624,6 +624,49 @@ module.exports = { ) }, + /** + * Returns a generator with all child element v-if chains of the given element. + * @param {VElement} node The element node to check. + * @returns {IterableIterator} + */ + *iterateChildElementsChains(node) { + let vIf = false + /** @type {VElement[]} */ + let elementChain = [] + for (const childNode of node.children) { + if (childNode.type === 'VElement') { + let connected + if (this.hasDirective(childNode, 'if')) { + connected = false + vIf = true + } else if (this.hasDirective(childNode, 'else-if')) { + connected = vIf + vIf = true + } else if (this.hasDirective(childNode, 'else')) { + connected = vIf + vIf = false + } else { + connected = false + vIf = false + } + + if (connected) { + elementChain.push(childNode) + } else { + if (elementChain.length) { + yield elementChain + } + elementChain = [childNode] + } + } else if (childNode.type !== 'VText' || childNode.value.trim() !== '') { + vIf = false + } + } + if (elementChain.length) { + yield elementChain + } + }, + /** * Check whether the given node is a custom component or not. * @param {VElement} node The start tag node to check. diff --git a/tests/lib/rules/no-deprecated-slot-attribute.js b/tests/lib/rules/no-deprecated-slot-attribute.js index ab08c4aff..0db5e5898 100644 --- a/tests/lib/rules/no-deprecated-slot-attribute.js +++ b/tests/lib/rules/no-deprecated-slot-attribute.js @@ -405,6 +405,205 @@ tester.run('no-deprecated-slot-attribute', rule, { `, errors: ['`slot` attributes are deprecated.'] + }, + { + // https://github.com/vuejs/eslint-plugin-vue/issues/1499 + code: ` + `, + output: ` + `, + errors: [ + { + message: '`slot` attributes are deprecated.', + line: 4 + }, + { + message: '`slot` attributes are deprecated.', + line: 9 + } + ] + }, + { + code: ` + `, + output: ` + `, + errors: [ + '`slot` attributes are deprecated.', + '`slot` attributes are deprecated.' + ] + }, + { + code: ` + `, + output: ` + `, + errors: [ + '`slot` attributes are deprecated.', + '`slot` attributes are deprecated.' + ] + }, + { + code: ` + `, + output: ` + `, + errors: [ + '`slot` attributes are deprecated.', + '`slot` attributes are deprecated.' + ] + }, + { + code: ` + `, + output: null, + errors: [ + '`slot` attributes are deprecated.', + '`slot` attributes are deprecated.' + ] + }, + { + code: ` + `, + output: ` + `, + errors: [ + '`slot` attributes are deprecated.', + '`slot` attributes are deprecated.' + ] + }, + { + code: ` + `, + output: ` + `, + errors: [ + '`slot` attributes are deprecated.', + '`slot` attributes are deprecated.' + ] } ] }) diff --git a/tests/lib/rules/no-deprecated-slot-scope-attribute.js b/tests/lib/rules/no-deprecated-slot-scope-attribute.js index 995909fc1..728aaccd3 100644 --- a/tests/lib/rules/no-deprecated-slot-scope-attribute.js +++ b/tests/lib/rules/no-deprecated-slot-scope-attribute.js @@ -138,6 +138,31 @@ tester.run('no-deprecated-slot-scope-attribute', rule, { line: 4 } ] + }, + { + code: ` + `, + output: null, + errors: ['`slot-scope` are deprecated.'] + }, + { + code: ` + `, + output: null, + errors: ['`slot-scope` are deprecated.'] } ] })