UNPKG

eslint-plugin-vue

Version:

Official ESLint plugin for Vue.js

338 lines (335 loc) 11.7 kB
'use strict'; const require_runtime = require('../_virtual/_rolldown/runtime.js'); const require_index = require('../utils/index.js'); //#region lib/rules/attributes-order.js /** * @fileoverview enforce ordering of attributes * @author Erin Depew */ var require_attributes_order = /* @__PURE__ */ require_runtime.__commonJSMin(((exports, module) => { const utils = require_index.default; /** * @typedef { VDirective & { key: VDirectiveKey & { name: VIdentifier & { name: 'bind' } } } } VBindDirective */ const ATTRS = { DEFINITION: "DEFINITION", LIST_RENDERING: "LIST_RENDERING", CONDITIONALS: "CONDITIONALS", RENDER_MODIFIERS: "RENDER_MODIFIERS", GLOBAL: "GLOBAL", UNIQUE: "UNIQUE", SLOT: "SLOT", TWO_WAY_BINDING: "TWO_WAY_BINDING", OTHER_DIRECTIVES: "OTHER_DIRECTIVES", OTHER_ATTR: "OTHER_ATTR", ATTR_STATIC: "ATTR_STATIC", ATTR_DYNAMIC: "ATTR_DYNAMIC", ATTR_SHORTHAND_BOOL: "ATTR_SHORTHAND_BOOL", EVENTS: "EVENTS", CONTENT: "CONTENT" }; /** * Check whether the given attribute is `v-bind` directive. * @param {VAttribute | VDirective | undefined | null} node * @returns { node is VBindDirective } */ function isVBind(node) { return Boolean(node && node.directive && node.key.name.name === "bind"); } /** * Check whether the given attribute is `v-model` directive. * @param {VAttribute | VDirective | undefined | null} node * @returns { node is VDirective } */ function isVModel(node) { return Boolean(node && node.directive && node.key.name.name === "model"); } /** * Check whether the given attribute is plain attribute. * @param {VAttribute | VDirective | undefined | null} node * @returns { node is VAttribute } */ function isVAttribute(node) { return Boolean(node && !node.directive); } /** * Check whether the given attribute is plain attribute, `v-bind` directive or `v-model` directive. * @param {VAttribute | VDirective | undefined | null} node * @returns { node is VAttribute } */ function isVAttributeOrVBindOrVModel(node) { return isVAttribute(node) || isVBind(node) || isVModel(node); } /** * Check whether the given attribute is `v-bind="..."` directive. * @param {VAttribute | VDirective | undefined | null} node * @returns { node is VBindDirective } */ function isVBindObject(node) { return isVBind(node) && node.key.argument == null; } /** * Check whether the given attribute is a shorthand boolean like `selected`. * @param {VAttribute | VDirective | undefined | null} node * @returns { node is VAttribute } */ function isVShorthandBoolean(node) { return isVAttribute(node) && !node.value; } /** * @param {VAttribute | VDirective} attribute * @param {SourceCode} sourceCode */ function getAttributeName(attribute, sourceCode) { if (attribute.directive) if (isVBind(attribute)) return attribute.key.argument ? sourceCode.getText(attribute.key.argument) : ""; else return getDirectiveKeyName(attribute.key, sourceCode); else return attribute.key.name; } /** * @param {VDirectiveKey} directiveKey * @param {SourceCode} sourceCode */ function getDirectiveKeyName(directiveKey, sourceCode) { let text = `v-${directiveKey.name.name}`; if (directiveKey.argument) text += `:${sourceCode.getText(directiveKey.argument)}`; for (const modifier of directiveKey.modifiers) text += `.${modifier.name}`; return text; } /** * @param {VAttribute | VDirective} attribute */ function getAttributeType(attribute) { let propName; if (attribute.directive) { if (!isVBind(attribute)) switch (attribute.key.name.name) { case "for": return ATTRS.LIST_RENDERING; case "if": case "else-if": case "else": case "show": case "cloak": return ATTRS.CONDITIONALS; case "pre": case "once": return ATTRS.RENDER_MODIFIERS; case "model": return ATTRS.TWO_WAY_BINDING; case "on": return ATTRS.EVENTS; case "html": case "text": return ATTRS.CONTENT; case "slot": return ATTRS.SLOT; case "is": return ATTRS.DEFINITION; default: return ATTRS.OTHER_DIRECTIVES; } propName = attribute.key.argument && attribute.key.argument.type === "VIdentifier" ? attribute.key.argument.rawName : ""; } else propName = attribute.key.name; switch (propName) { case "is": return ATTRS.DEFINITION; case "id": return ATTRS.GLOBAL; case "ref": case "key": return ATTRS.UNIQUE; case "slot": case "slot-scope": return ATTRS.SLOT; default: if (isVBind(attribute)) return ATTRS.ATTR_DYNAMIC; if (isVShorthandBoolean(attribute)) return ATTRS.ATTR_SHORTHAND_BOOL; return ATTRS.ATTR_STATIC; } } /** * @param {VAttribute | VDirective} attribute * @param { { [key: string]: number } } attributePosition * @returns {number | null} If the value is null, the order is omitted. Do not force the order. */ function getPosition(attribute, attributePosition) { const attributeType = getAttributeType(attribute); return attributePosition[attributeType] == null ? null : attributePosition[attributeType]; } /** * @param {VAttribute | VDirective} prevNode * @param {VAttribute | VDirective} currNode * @param {SourceCode} sourceCode */ function isAlphabetical(prevNode, currNode, sourceCode) { const prevName = getAttributeName(prevNode, sourceCode); const currName = getAttributeName(currNode, sourceCode); if (prevName === currName) return isVBind(prevNode) <= isVBind(currNode); return prevName < currName; } /** * @param {RuleContext} context - The rule context. * @returns {RuleListener} AST event handlers. */ function create(context) { const sourceCode = context.sourceCode; const otherAttrs = [ ATTRS.ATTR_DYNAMIC, ATTRS.ATTR_STATIC, ATTRS.ATTR_SHORTHAND_BOOL ]; let attributeOrder = [ ATTRS.DEFINITION, ATTRS.LIST_RENDERING, ATTRS.CONDITIONALS, ATTRS.RENDER_MODIFIERS, ATTRS.GLOBAL, [ATTRS.UNIQUE, ATTRS.SLOT], ATTRS.TWO_WAY_BINDING, ATTRS.OTHER_DIRECTIVES, otherAttrs, ATTRS.EVENTS, ATTRS.CONTENT ]; if (context.options[0] && context.options[0].order) { attributeOrder = [...context.options[0].order]; for (const item of attributeOrder.flat()) if (item === ATTRS.OTHER_ATTR) { for (const attribute of attributeOrder.flat()) if (otherAttrs.includes(attribute)) throw new Error(`Value "${ATTRS.OTHER_ATTR}" is not allowed with "${attribute}".`); } for (const [index, item] of attributeOrder.entries()) if (item === ATTRS.OTHER_ATTR) attributeOrder[index] = otherAttrs; else if (Array.isArray(item) && item.includes(ATTRS.OTHER_ATTR)) { const attributes = item.filter((i) => i !== ATTRS.OTHER_ATTR); attributes.push(...otherAttrs); attributeOrder[index] = attributes; } } const alphabetical = Boolean(context.options[0] && context.options[0].alphabetical); const sortLineLength = Boolean(context.options[0] && context.options[0].sortLineLength); const ignoreVBindObject = Boolean(context.options[0] && context.options[0].ignoreVBindObject); /** @type { { [key: string]: number } } */ const attributePosition = {}; for (const [i, item] of attributeOrder.entries()) if (Array.isArray(item)) for (const attr of item) attributePosition[attr] = i; else attributePosition[item] = i; /** * @param {VAttribute | VDirective} node * @param {VAttribute | VDirective} previousNode */ function reportIssue(node, previousNode) { const currentNode = sourceCode.getText(node.key); const prevNode = sourceCode.getText(previousNode.key); context.report({ node, messageId: "expectedOrder", data: { currentNode, prevNode }, fix(fixer) { const attributes = node.parent.attributes; /** @type { (node: VAttribute | VDirective | undefined) => boolean } */ let isMoveUp; if (isVBindObject(node)) isMoveUp = isVAttributeOrVBindOrVModel; else if (isVAttributeOrVBindOrVModel(node)) isMoveUp = isVBindObject; else isMoveUp = () => false; const previousNodes = attributes.slice(attributes.indexOf(previousNode), attributes.indexOf(node)); const moveNodes = [node]; for (const node of previousNodes) if (isMoveUp(node)) moveNodes.unshift(node); else moveNodes.push(node); return moveNodes.map((moveNode, index) => { const text = sourceCode.getText(moveNode); return fixer.replaceText(previousNodes[index] || node, text); }); } }); } return utils.defineTemplateBodyVisitor(context, { VStartTag(node) { const attributeAndPositions = getAttributeAndPositionList(node); if (attributeAndPositions.length <= 1) return; let { attr: previousNode, position: previousPosition } = attributeAndPositions[0]; for (let index = 1; index < attributeAndPositions.length; index++) { const { attr, position } = attributeAndPositions[index]; let valid = previousPosition <= position; if (valid && previousPosition === position) { let sortedByLength = false; if (sortLineLength) { const prevText = sourceCode.getText(previousNode); const currText = sourceCode.getText(attr); if (prevText.length !== currText.length) { valid = prevText.length < currText.length; sortedByLength = true; } } if (alphabetical && !sortedByLength) valid = isAlphabetical(previousNode, attr, sourceCode); } if (valid) { previousNode = attr; previousPosition = position; } else reportIssue(attr, previousNode); } } }); /** * @param {VStartTag} node * @returns { { attr: ( VAttribute | VDirective ), position: number }[] } */ function getAttributeAndPositionList(node) { const attributes = node.attributes.filter((node, index, attributes) => { if (ignoreVBindObject && isVBindObject(node)) return false; if (isVBindObject(node) && (isVAttributeOrVBindOrVModel(attributes[index - 1]) || isVAttributeOrVBindOrVModel(attributes[index + 1]))) return false; return true; }); const results = []; for (const [index, attr] of attributes.entries()) { const position = getPositionFromAttrIndex(index); if (position == null) continue; results.push({ attr, position }); } return results; /** * @param {number} index * @returns {number | null} */ function getPositionFromAttrIndex(index) { const node = attributes[index]; if (isVBindObject(node)) { const len = attributes.length; for (let nextIndex = index + 1; nextIndex < len; nextIndex++) { const next = attributes[nextIndex]; if (isVAttributeOrVBindOrVModel(next) && !isVBindObject(next)) return getPositionFromAttrIndex(nextIndex); } } return getPosition(node, attributePosition); } } } module.exports = { meta: { type: "suggestion", docs: { description: "enforce order of attributes", categories: ["vue3-recommended", "vue2-recommended"], url: "https://eslint.vuejs.org/rules/attributes-order.html" }, fixable: "code", schema: [{ type: "object", properties: { order: { type: "array", items: { oneOf: [{ enum: Object.values(ATTRS) }, { type: "array", items: { enum: Object.values(ATTRS), uniqueItems: true, additionalItems: false } }] }, uniqueItems: true, additionalItems: false }, alphabetical: { type: "boolean" }, sortLineLength: { type: "boolean" }, ignoreVBindObject: { type: "boolean" } }, additionalProperties: false }], messages: { expectedOrder: `Attribute "{{currentNode}}" should go before "{{prevNode}}".` } }, create }; })); //#endregion Object.defineProperty(exports, 'default', { enumerable: true, get: function () { return require_attributes_order(); } });