123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254 |
- /**
- * @author Toru Nagashima
- * See LICENSE file in root directory for full license.
- */
- 'use strict'
- const utils = require('../utils')
- /**
- * Get all `v-slot` directives on a given element.
- * @param {VElement} node The VElement node to check.
- * @returns {VAttribute[]} The array of `v-slot` directives.
- */
- function getSlotDirectivesOnElement (node) {
- return node.startTag.attributes.filter(attribute =>
- attribute.directive &&
- attribute.key.name.name === 'slot'
- )
- }
- /**
- * Get all `v-slot` directives on the children of a given element.
- * @param {VElement} node The VElement node to check.
- * @returns {VAttribute[][]}
- * The array of the group of `v-slot` directives.
- * The group bundles `v-slot` directives of element sequence which is connected
- * 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
- }
- 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 }
- }, { groups: [], vIf: false })
- .groups
- .map(group =>
- group
- .map(childElement =>
- childElement.name === 'template'
- ? utils.getDirective(childElement, 'slot')
- : null
- )
- .filter(Boolean)
- )
- .filter(group => group.length >= 1)
- }
- /**
- * Get the normalized name of a given `v-slot` directive node.
- * @param {VAttribute} node The `v-slot` directive node.
- * @returns {string} The normalized name.
- */
- function getNormalizedName (node, sourceCode) {
- return node.key.argument == null ? 'default' : sourceCode.getText(node.key.argument)
- }
- /**
- * Get all `v-slot` directives which are distributed to the same slot as a given `v-slot` directive node.
- * @param {VAttribute[][]} vSlotGroups The result of `getAllNamedSlotElements()`.
- * @param {VElement} currentVSlot The current `v-slot` directive node.
- * @returns {VAttribute[][]} The array of the group of `v-slot` directives.
- */
- function filterSameSlot (vSlotGroups, currentVSlot, sourceCode) {
- const currentName = getNormalizedName(currentVSlot, sourceCode)
- return vSlotGroups
- .map(vSlots =>
- vSlots.filter(vSlot => getNormalizedName(vSlot, sourceCode) === currentName)
- )
- .filter(slots => slots.length >= 1)
- }
- /**
- * Check whether a given argument node is using an iteration variable 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.
- */
- function isUsingIterationVar (argument, element) {
- if (argument && argument.type === 'VExpressionContainer') {
- for (const { variable } of argument.references) {
- if (
- variable != null &&
- variable.kind === 'v-for' &&
- variable.id.range[0] > element.startTag.range[0] &&
- variable.id.range[1] < element.startTag.range[1]
- ) {
- return true
- }
- }
- }
- return false
- }
- /**
- * Check whether a given argument node is using an scope variable that the directive defined.
- * @param {VAttribute} vSlot The `v-slot` directive to check.
- * @returns {boolean} `true` if that argument node is using a scope variable the directive defined.
- */
- function isUsingScopeVar (vSlot) {
- const argument = vSlot.key.argument
- const value = vSlot.value
- if (argument && value && argument.type === 'VExpressionContainer') {
- for (const { variable } of argument.references) {
- if (
- variable != null &&
- variable.kind === 'scope' &&
- variable.id.range[0] > value.range[0] &&
- variable.id.range[1] < value.range[1]
- ) {
- return true
- }
- }
- }
- }
- module.exports = {
- meta: {
- type: 'problem',
- docs: {
- description: 'enforce valid `v-slot` directives',
- category: undefined, // essential
- // TODO Change with major version.
- // category: 'essential',
- url: 'https://eslint.vuejs.org/rules/valid-v-slot.html'
- },
- fixable: null,
- schema: [],
- messages: {
- ownerMustBeCustomElement: "'v-slot' directive must be owned by a custom element, but '{{name}}' is not.",
- namedSlotMustBeOnTemplate: "Named slots must use '<template>' on a custom element.",
- defaultSlotMustBeOnTemplate: "Default slot must use '<template>' on a custom element when there are other named slots.",
- disallowDuplicateSlotsOnElement: "An element cannot have multiple 'v-slot' directives.",
- disallowDuplicateSlotsOnChildren: "An element cannot have multiple '<template>' elements which are distributed to the same slot.",
- disallowArgumentUseSlotParams: "Dynamic argument of 'v-slot' directive cannot use that slot parameter.",
- disallowAnyModifier: "'v-slot' directive doesn't support any modifier.",
- requireAttributeValue: "'v-slot' directive on a custom element requires that attribute value."
- }
- },
- create (context) {
- const sourceCode = context.getSourceCode()
- return utils.defineTemplateBodyVisitor(context, {
- "VAttribute[directive=true][key.name.name='slot']" (node) {
- const isDefaultSlot = node.key.argument == null || node.key.argument.name === 'default'
- const element = node.parent.parent
- const parentElement = element.parent
- const ownerElement = element.name === 'template' ? parentElement : element
- const vSlotsOnElement = getSlotDirectivesOnElement(element)
- const vSlotGroupsOnChildren = getSlotDirectivesOnChildren(ownerElement)
- // Verify location.
- if (!utils.isCustomComponent(ownerElement)) {
- context.report({
- node,
- messageId: 'ownerMustBeCustomElement',
- data: { name: ownerElement.rawName }
- })
- }
- if (!isDefaultSlot && element.name !== 'template') {
- context.report({
- node,
- messageId: 'namedSlotMustBeOnTemplate'
- })
- }
- if (ownerElement === element && vSlotGroupsOnChildren.length >= 1) {
- context.report({
- node,
- messageId: 'defaultSlotMustBeOnTemplate'
- })
- }
- // Verify duplication.
- if (vSlotsOnElement.length >= 2 && vSlotsOnElement[0] !== node) {
- // E.g., <my-component #one #two>
- context.report({
- node,
- messageId: 'disallowDuplicateSlotsOnElement'
- })
- }
- if (ownerElement === parentElement) {
- const vSlotGroupsOfSameSlot = filterSameSlot(vSlotGroupsOnChildren, node, sourceCode)
- const vFor = utils.getDirective(element, 'for')
- if (
- vSlotGroupsOfSameSlot.length >= 2 &&
- !vSlotGroupsOfSameSlot[0].includes(node)
- ) {
- // E.g., <template #one></template>
- // <template #one></template>
- context.report({
- node,
- messageId: 'disallowDuplicateSlotsOnChildren'
- })
- }
- if (vFor && !isUsingIterationVar(node.key.argument, element)) {
- // E.g., <template v-for="x of xs" #one></template>
- context.report({
- node,
- messageId: 'disallowDuplicateSlotsOnChildren'
- })
- }
- }
- // Verify argument.
- if (isUsingScopeVar(node)) {
- context.report({
- node,
- messageId: 'disallowArgumentUseSlotParams'
- })
- }
- // Verify modifiers.
- if (node.key.modifiers.length >= 1) {
- context.report({
- node,
- messageId: 'disallowAnyModifier'
- })
- }
- // Verify value.
- if (ownerElement === element && isDefaultSlot && !utils.hasAttributeValue(node)) {
- context.report({
- node,
- messageId: 'requireAttributeValue'
- })
- }
- }
- })
- }
- }
|