diff --git a/docs/rules/valid-v-slot.md b/docs/rules/valid-v-slot.md new file mode 100644 index 000000000..edb17c8df --- /dev/null +++ b/docs/rules/valid-v-slot.md @@ -0,0 +1,112 @@ +--- +pageClass: rule-details +sidebarDepth: 0 +title: vue/valid-v-slot +description: enforce valid `v-slot` directives +--- +# vue/valid-v-slot +> enforce valid `v-slot` directives + +This rule checks whether every `v-slot` directive is valid. + +## :book: Rule Details + +This rule reports `v-slot` directives in the following cases: + +- The directive is not owned by a custom element. E.g. `
` +- The directive is a named slot and is on a custom element directly. E.g. `` +- The directive is the default slot, is on a custom element directly, and there are other named slots. E.g. `` +- The element which has the directive has another `v-slot` directive. E.g. `` +- The element which has the directive has another `v-slot` directive that is distributed to the same slot. E.g. `` +- The directive has a dynamic argument which uses the scope properties that the directive defined. E.g. `` +- The directive has any modifier. E.g. `` +- The directive is the default slot, is on a custom element directly, and has no value. E.g. `` + + + +```vue + +``` + + + +::: warning Note +This rule does not check syntax errors in directives because it's checked by [no-parsing-error] rule. +::: + +## :wrench: Options + +Nothing. + +## :couple: Related rules + +- [no-parsing-error] + +[no-parsing-error]: no-parsing-error.md + +## :mag: Implementation + +- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/valid-v-slot.js) +- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/valid-v-slot.js) diff --git a/lib/rules/valid-v-slot.js b/lib/rules/valid-v-slot.js new file mode 100644 index 000000000..567e3b251 --- /dev/null +++ b/lib/rules/valid-v-slot.js @@ -0,0 +1,252 @@ +/** + * @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 + 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 '