UNPKG

eslint-plugin-vue

Version:

Official ESLint plugin for Vue.js

280 lines (277 loc) 8.55 kB
'use strict'; const require_runtime = require('../_virtual/_rolldown/runtime.js'); const require_index = require('../utils/index.js'); //#region lib/rules/order-in-components.js /** * @fileoverview Keep order of properties in components * @author Michał Sajnóg */ var require_order_in_components = /* @__PURE__ */ require_runtime.__commonJSMin(((exports, module) => { const utils = require_index.default; const traverseNodes = require("vue-eslint-parser").AST.traverseNodes; /** * @typedef {import('eslint-visitor-keys').VisitorKeys} VisitorKeys */ const defaultOrder = [ "el", "name", "key", "parent", "functional", ["delimiters", "comments"], [ "components", "directives", "filters" ], "extends", "mixins", ["provide", "inject"], "ROUTER_GUARDS", "layout", "middleware", "validate", "scrollToTop", "transition", "loading", "inheritAttrs", "model", ["props", "propsData"], "emits", "slots", "expose", "setup", "asyncData", "data", "fetch", "head", "computed", "watch", "watchQuery", "LIFECYCLE_HOOKS", "methods", ["template", "render"], "renderError" ]; /** @type { { [key: string]: string[] } } */ const groups = { LIFECYCLE_HOOKS: [ "beforeCreate", "created", "beforeMount", "mounted", "beforeUpdate", "updated", "activated", "deactivated", "beforeUnmount", "unmounted", "beforeDestroy", "destroyed", "renderTracked", "renderTriggered", "errorCaptured" ], ROUTER_GUARDS: [ "beforeRouteEnter", "beforeRouteUpdate", "beforeRouteLeave" ] }; /** * @param {(string | string[])[]} order */ function getOrderMap(order) { /** @type {Map<string, number>} */ const orderMap = /* @__PURE__ */ new Map(); for (const [i, property] of order.entries()) if (Array.isArray(property)) for (const p of property) orderMap.set(p, i); else orderMap.set(property, i); return orderMap; } /** * @param {Token} node */ function isComma(node) { return node.type === "Punctuator" && node.value === ","; } const ARITHMETIC_OPERATORS = [ "+", "-", "*", "/", "%", "**" ]; const BITWISE_OPERATORS = [ "&", "|", "^", "~", "<<", ">>", ">>>" ]; const COMPARISON_OPERATORS = [ "==", "!=", "===", "!==", ">", ">=", "<", "<=" ]; const RELATIONAL_OPERATORS = ["in", "instanceof"]; const ALL_BINARY_OPERATORS = new Set([ ...ARITHMETIC_OPERATORS, ...BITWISE_OPERATORS, ...COMPARISON_OPERATORS, ...RELATIONAL_OPERATORS ]); const LOGICAL_OPERATORS = new Set([ "&&", "||", "??" ]); /** * Result `true` if the node is sure that there are no side effects * * Currently known side effects types * * node.type === 'CallExpression' * node.type === 'NewExpression' * node.type === 'UpdateExpression' * node.type === 'AssignmentExpression' * node.type === 'TaggedTemplateExpression' * node.type === 'UnaryExpression' && node.operator === 'delete' * * @param {ASTNode} node target node * @param {VisitorKeys} visitorKeys sourceCode.visitorKey * @returns {boolean} no side effects */ function isNotSideEffectsNode(node, visitorKeys) { let result = true; /** @type {ASTNode | null} */ let skipNode = null; traverseNodes(node, { visitorKeys, enterNode(node) { if (!result || skipNode) return; if (node.type === "FunctionExpression" || node.type === "Identifier" || node.type === "Literal" || node.type === "ArrowFunctionExpression" || node.type === "TemplateElement" || node.type === "TSAsExpression") skipNode = node; else if (node.type !== "Property" && node.type !== "ObjectExpression" && node.type !== "ArrayExpression" && (node.type !== "UnaryExpression" || ![ "!", "~", "+", "-", "typeof" ].includes(node.operator)) && (node.type !== "BinaryExpression" || !ALL_BINARY_OPERATORS.has(node.operator)) && (node.type !== "LogicalExpression" || !LOGICAL_OPERATORS.has(node.operator)) && node.type !== "MemberExpression" && node.type !== "ConditionalExpression" && node.type !== "SpreadElement" && node.type !== "TemplateLiteral" && node.type !== "ChainExpression") result = false; }, leaveNode(node) { if (skipNode === node) skipNode = null; } }); return result; } module.exports = { meta: { type: "suggestion", docs: { description: "enforce order of properties in components", categories: ["vue3-recommended", "vue2-recommended"], url: "https://eslint.vuejs.org/rules/order-in-components.html" }, fixable: "code", hasSuggestions: true, schema: [{ type: "object", properties: { order: { type: "array" } }, additionalProperties: false }], messages: { order: "The \"{{name}}\" property should be above the \"{{firstUnorderedPropertyName}}\" property on line {{line}}.", reorderWithSideEffects: "Manually move \"{{name}}\" property above \"{{firstUnorderedPropertyName}}\" property on line {{line}} (might break side effects)." } }, create(context) { const orderMap = getOrderMap(((context.options[0] || {}).order || defaultOrder).map((property) => typeof property === "string" && groups[property] || property)); const sourceCode = context.sourceCode; /** * @param {string} name */ function getOrderPosition(name) { const num = orderMap.get(name); return num == null ? -1 : num; } /** * @param {RuleFixer} fixer * @param {Property} propertyNode * @param {Property} unorderedPropertyNode */ function* handleFix(fixer, propertyNode, unorderedPropertyNode) { const afterComma = sourceCode.getTokenAfter(propertyNode); const hasAfterComma = isComma(afterComma); const beforeComma = sourceCode.getTokenBefore(propertyNode); const codeStart = beforeComma.range[1]; const codeEnd = hasAfterComma ? afterComma.range[1] : propertyNode.range[1]; const removeStart = hasAfterComma ? codeStart : beforeComma.range[0]; yield fixer.removeRange([removeStart, codeEnd]); const propertyCode = sourceCode.text.slice(codeStart, codeEnd) + (hasAfterComma ? "" : ","); const insertTarget = sourceCode.getTokenBefore(unorderedPropertyNode); yield fixer.insertTextAfter(insertTarget, propertyCode); } /** * @param {(Property | SpreadElement)[]} propertiesNodes */ function checkOrder(propertiesNodes) { const properties = propertiesNodes.filter(utils.isProperty).map((property) => ({ node: property, name: utils.getStaticPropertyName(property) || property.key.type === "Identifier" && property.key.name || "" })); for (const [i, property] of properties.entries()) { if (getOrderPosition(property.name) < 0) continue; const firstUnorderedProperty = properties.slice(0, i).filter((p) => getOrderPosition(p.name) > getOrderPosition(property.name)).sort((p1, p2) => getOrderPosition(p1.name) > getOrderPosition(p2.name) ? 1 : -1)[0]; if (firstUnorderedProperty) { const line = firstUnorderedProperty.node.loc.start.line; const propertyNode = property.node; const firstUnorderedPropertyNode = firstUnorderedProperty.node; const hasSideEffectsPossibility = propertiesNodes.slice(propertiesNodes.indexOf(firstUnorderedPropertyNode), propertiesNodes.indexOf(propertyNode) + 1).some((property) => !isNotSideEffectsNode(property, sourceCode.visitorKeys)); context.report({ node: property.node, messageId: "order", data: { name: property.name, firstUnorderedPropertyName: firstUnorderedProperty.name, line }, fix: hasSideEffectsPossibility ? void 0 : (fixer) => handleFix(fixer, propertyNode, firstUnorderedPropertyNode), suggest: hasSideEffectsPossibility ? [{ messageId: "reorderWithSideEffects", data: { name: property.name, firstUnorderedPropertyName: firstUnorderedProperty.name, line }, fix: (fixer) => handleFix(fixer, propertyNode, firstUnorderedPropertyNode) }] : void 0 }); } } } return utils.compositingVisitors(utils.executeOnVue(context, (obj) => { checkOrder(obj.properties); }), utils.defineScriptSetupVisitor(context, { onDefineOptionsEnter(node) { if (node.arguments.length === 0) return; const define = node.arguments[0]; if (define.type !== "ObjectExpression") return; checkOrder(define.properties); } })); } }; })); //#endregion Object.defineProperty(exports, 'default', { enumerable: true, get: function () { return require_order_in_components(); } });