eslint-plugin-vue
Version:
Official ESLint plugin for Vue.js
280 lines (277 loc) • 8.55 kB
JavaScript
;
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();
}
});