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: `
+
+
+
+ This works 1
+
+
+
+
+ This works 2
+
+
+
+ `,
+ output: `
+
+
+
+ This works 1
+
+
+
+
+ This works 2
+
+
+
+ `,
+ errors: [
+ {
+ message: '`slot` attributes are deprecated.',
+ line: 4
+ },
+ {
+ message: '`slot` attributes are deprecated.',
+ line: 9
+ }
+ ]
+ },
+ {
+ code: `
+
+
+
+ A
+
+
+ B
+
+
+ `,
+ output: `
+
+
+
+ A
+
+
+ B
+
+
+ `,
+ errors: [
+ '`slot` attributes are deprecated.',
+ '`slot` attributes are deprecated.'
+ ]
+ },
+ {
+ code: `
+
+
+
+ A
+
+
+ B
+
+
+ `,
+ output: `
+
+
+
+ A
+
+
+ B
+
+
+ `,
+ errors: [
+ '`slot` attributes are deprecated.',
+ '`slot` attributes are deprecated.'
+ ]
+ },
+ {
+ code: `
+
+
+
+ A
+
+
+ B
+
+
+ `,
+ output: `
+
+
+
+ A
+
+
+ B
+
+
+ `,
+ errors: [
+ '`slot` attributes are deprecated.',
+ '`slot` attributes are deprecated.'
+ ]
+ },
+ {
+ code: `
+
+
+
+ A
+
+
+ B
+
+
+ `,
+ output: null,
+ errors: [
+ '`slot` attributes are deprecated.',
+ '`slot` attributes are deprecated.'
+ ]
+ },
+ {
+ code: `
+
+
+
+ A
+
+
+ B
+
+
+ `,
+ output: `
+
+
+
+ A
+
+
+ B
+
+
+ `,
+ errors: [
+ '`slot` attributes are deprecated.',
+ '`slot` attributes are deprecated.'
+ ]
+ },
+ {
+ code: `
+
+
+
+ A
+
+
+ B
+
+
+ `,
+ output: `
+
+
+
+ A
+
+
+ B
+
+
+ `,
+ 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: `
+
+
+
+ {{a}}
+
+
+ `,
+ output: null,
+ errors: ['`slot-scope` are deprecated.']
+ },
+ {
+ code: `
+
+
+
+ {{a}}
+
+
+
+ `,
+ output: null,
+ errors: ['`slot-scope` are deprecated.']
}
]
})