UNPKG

eslint-plugin-vue

Version:

Official ESLint plugin for Vue.js

202 lines (199 loc) 8.7 kB
'use strict'; const require_rolldown_runtime = require('../_virtual/rolldown_runtime.js'); const require_index = require('../utils/index.js'); const require_casing$1 = require('../utils/casing.js'); //#region lib/rules/v-if-else-key.js /** * @author Felipe Melendez * See LICENSE file in root directory for full license. */ var require_v_if_else_key = /* @__PURE__ */ require_rolldown_runtime.__commonJSMin(((exports, module) => { const utils = require_index.default; const casing = require_casing$1.default; /** * A conditional family is made up of a group of repeated components that are conditionally rendered * using v-if, v-else-if, and v-else. * * @typedef {Object} ConditionalFamily * @property {VElement} if - The node associated with the 'v-if' directive. * @property {VElement[]} elseIf - An array of nodes associated with 'v-else-if' directives. * @property {VElement | null} else - The node associated with the 'v-else' directive, or null if there isn't one. */ /** * Checks if a given node has sibling nodes of the same type that are also conditionally rendered. * This is used to determine if multiple instances of the same component are being conditionally * rendered within the same parent scope. * * @param {VElement} node - The Vue component node to check for conditional rendering siblings. * @param {string} componentName - The name of the component to check for sibling instances. * @returns {boolean} True if there are sibling nodes of the same type and conditionally rendered, false otherwise. */ const hasConditionalRenderedSiblings = (node, componentName) => { if (!node.parent || node.parent.type !== "VElement") return false; return node.parent.children.some((sibling) => sibling !== node && sibling.type === "VElement" && sibling.rawName === componentName && hasConditionalDirective(sibling)); }; /** * Checks for the presence of a 'key' attribute in the given node. If the 'key' attribute is missing * and the node is part of a conditional family a report is generated. * The fix proposed adds a unique key based on the component's name and count, * following the format '${kebabCase(componentName)}-${componentCount}', e.g., 'some-component-2'. * * @param {VElement} node - The Vue component node to check for a 'key' attribute. * @param {RuleContext} context - The rule's context object, used for reporting. * @param {string} componentName - Name of the component. * @param {string} uniqueKey - A unique key for the repeated component, used for the fix. * @param {Map<VElement, ConditionalFamily>} conditionalFamilies - Map of conditionally rendered components and their respective conditional directives. */ const checkForKey = (node, context, componentName, uniqueKey, conditionalFamilies) => { if (!node.parent || node.parent.type !== "VElement" || !hasConditionalRenderedSiblings(node, componentName)) return; const conditionalFamily = conditionalFamilies.get(node.parent); if (!conditionalFamily || utils.hasAttribute(node, "key")) return; if (conditionalFamily.if === node || conditionalFamily.else === node || conditionalFamily.elseIf.includes(node)) context.report({ node: node.startTag, loc: node.startTag.loc, messageId: "requireKey", data: { componentName }, fix(fixer) { const afterComponentNamePosition = node.startTag.range[0] + componentName.length + 1; return fixer.insertTextBeforeRange([afterComponentNamePosition, afterComponentNamePosition], ` key="${uniqueKey}"`); } }); }; /** * Checks for the presence of conditional directives in the given node. * * @param {VElement} node - The node to check for conditional directives. * @returns {boolean} Returns true if a conditional directive is found in the node or its parents, * false otherwise. */ const hasConditionalDirective = (node) => utils.hasDirective(node, "if") || utils.hasDirective(node, "else-if") || utils.hasDirective(node, "else"); /** @type {import('eslint').Rule.RuleModule} */ module.exports = { meta: { type: "problem", docs: { description: "require key attribute for conditionally rendered repeated components", categories: null, url: "https://eslint.vuejs.org/rules/v-if-else-key.html" }, fixable: "code", schema: [], messages: { requireKey: "Conditionally rendered repeated component '{{componentName}}' expected to have a 'key' attribute." } }, create(context) { /** * Map to store conditionally rendered components and their respective conditional directives. * * @type {Map<VElement, ConditionalFamily>} */ const conditionalFamilies = /* @__PURE__ */ new Map(); /** * Array of Maps to keep track of components and their usage counts along with the first * node instance. Each Map represents a different scope level, and maps a component name to * an object containing the count and a reference to the first node. */ /** @type {Map<string, { count: number; firstNode: any }>[]} */ const componentUsageStack = [/* @__PURE__ */ new Map()]; /** * Checks if a given node represents a custom component without any conditional directives. * * @param {VElement} node - The AST node to check. * @returns {boolean} True if the node represents a custom component without any conditional directives, false otherwise. */ const isCustomComponentWithoutCondition = (node) => node.type === "VElement" && utils.isCustomComponent(node) && !hasConditionalDirective(node); /** Set of built-in Vue components that are exempt from the rule. */ /** @type {Set<string>} */ const exemptTags = new Set([ "component", "slot", "template" ]); /** Set to keep track of nodes we've pushed to the stack. */ /** @type {Set<any>} */ const pushedNodes = /* @__PURE__ */ new Set(); /** * Creates and returns an object representing a conditional family. * * @param {VElement} ifNode - The VElement associated with the 'v-if' directive. * @returns {ConditionalFamily} */ const createConditionalFamily = (ifNode) => ({ if: ifNode, elseIf: [], else: null }); return utils.defineTemplateBodyVisitor(context, { VElement(node) { if (exemptTags.has(node.rawName)) return; const condition = utils.getDirective(node, "if") || utils.getDirective(node, "else-if") || utils.getDirective(node, "else"); if (condition) { const conditionType = condition.key.name.name; if (node.parent && node.parent.type === "VElement") { let conditionalFamily = conditionalFamilies.get(node.parent); if (!conditionalFamily) { conditionalFamily = createConditionalFamily(node); conditionalFamilies.set(node.parent, conditionalFamily); } if (conditionalFamily) switch (conditionType) { case "if": conditionalFamily = createConditionalFamily(node); conditionalFamilies.set(node.parent, conditionalFamily); break; case "else-if": conditionalFamily.elseIf.push(node); break; case "else": conditionalFamily.else = node; break; } } } if (isCustomComponentWithoutCondition(node)) { componentUsageStack.push(/* @__PURE__ */ new Map()); return; } if (!utils.isCustomComponent(node)) return; const componentName = node.rawName; const currentScope = componentUsageStack.at(-1); const usageInfo = currentScope?.get(componentName) ?? { count: 0, firstNode: null }; if (hasConditionalDirective(node)) { if (usageInfo.count === 0) usageInfo.firstNode = node; if (usageInfo.count > 0) { checkForKey(node, context, componentName, `${casing.kebabCase(componentName)}-${usageInfo.count + 1}`, conditionalFamilies); if (usageInfo.count === 1) { const uniqueKeyForFirstInstance = `${casing.kebabCase(componentName)}-1`; checkForKey(usageInfo.firstNode, context, componentName, uniqueKeyForFirstInstance, conditionalFamilies); } } usageInfo.count += 1; currentScope?.set(componentName, usageInfo); } componentUsageStack.push(/* @__PURE__ */ new Map()); pushedNodes.add(node); }, "VElement:exit"(node) { if (exemptTags.has(node.rawName)) return; if (isCustomComponentWithoutCondition(node)) { componentUsageStack.pop(); return; } if (!utils.isCustomComponent(node)) return; if (pushedNodes.has(node)) { componentUsageStack.pop(); pushedNodes.delete(node); } } }); } }; })); //#endregion Object.defineProperty(exports, 'default', { enumerable: true, get: function () { return require_v_if_else_key(); } });