UNPKG

eslint-plugin-sort-destructure-keys

Version:
268 lines (237 loc) 6.99 kB
const naturalCompare = require("natural-compare-lite"); /** * Get's the "name" of the node, which could be an Identifier, * StringLiteral, or NumberLiteral, etc. * * This returned string is used to sort nodes of the same type. */ function getNodeName(node) { switch (node.type) { case "Property": return getNodeName(node.key); case "Identifier": return node.name; case "Literal": return node.value.toString(); case "RestElement": case "RestProperty": case "ExperimentalRestProperty": return node.argument.name; default: return node.type; } } /** * Sort priority for node types. */ const NODE_TYPE_SORT_ORDER = { /* ObjectPattern children */ Property: 1, RestElement: 99, RestProperty: 99, ExperimentalRestProperty: 99, /* Property keys */ Identifier: 1, Literal: 1, TemplateLiteral: 2, ComputedProperty: 2, }; const SORT_ORDER_DEFAULT = 98; function getSortOrder(node) { return isComputedProperty(node) ? NODE_TYPE_SORT_ORDER.ComputedProperty : NODE_TYPE_SORT_ORDER[node.type] || SORT_ORDER_DEFAULT; } /** * Returns true if the node is a "real", computed property. We don't consider * computed properties with a literal value to be a "real" computed property. This * is useful since that means they can be sorted. */ function isComputedProperty(node) { return node.computed && node.key.type !== "Literal"; } /** * Returns true if any references to this ID are within the objectPatternNode */ function isReferencedByOtherProperties(scope, objectPatternNode, id) { for (const variable of scope.variables) { if (variable.name !== id.name) { continue; } for (const reference of variable.references) { if (reference.identifier === id) { continue; } let current = reference.identifier; while (current) { if (current === objectPatternNode) { return true; } current = current.parent; } } return false; } } /** * Returns whether or not a node is safe to be sorted. */ function shouldCheck(scope, objectPatternNode, node) { if (node.type !== "Property") { return true; } switch (node.value.type) { case "ObjectPattern": return node.value.properties.every((propertyNode) => shouldCheck(scope, objectPatternNode, propertyNode), ); case "ArrayPattern": // Fake the element as a property for simplicity return node.value.elements.every( (element) => !element || shouldCheck(scope, objectPatternNode, { type: "Property", value: element, }), ); case "AssignmentPattern": if (node.value.left.type === "Identifier") { return !isReferencedByOtherProperties( scope, objectPatternNode, node.value.left, ); } return true; case "Identifier": return !isReferencedByOtherProperties( scope, objectPatternNode, node.value, ); default: return true; } } /** * Returns a function that will sort two nodes found in an `ObjectPattern`. * * TODO: Maybe it makes sense to do a topological sort here based on identifiers? * Ideally we wouldn't need to arbitrarily skip sorting nodes because we are worried * about breaking the code. */ function createSorter(caseSensitive) { const sortName = (a) => (caseSensitive ? a : a.toLowerCase()); return (a, b) => { // When we have different node "types" const nodeResult = getSortOrder(a) - getSortOrder(b); if (nodeResult !== 0) return nodeResult; // When the keys have different "types" const keyResult = getSortOrder(a.key) - getSortOrder(b.key); if (keyResult !== 0) return keyResult; return naturalCompare(sortName(getNodeName(a)), sortName(getNodeName(b))); }; } /** * Creates a "fixer" function to be used by `--fix`. */ function createFix({ context, fixer, node, sorter }) { const sourceCode = context.getSourceCode(); const sourceText = sourceCode.getText(); const sorted = node.properties.concat().sort(sorter); const newText = sorted .map((child, i) => { const textAfter = i === sorted.length - 1 ? // If it's the last item, there's no text after to append. "" : // Otherwise, we need to grab the text after the original node. sourceText.slice( node.properties[i].range[1], // End index of the current node . node.properties[i + 1].range[0], // Start index of the next node. ); return sourceCode.getText(child) + textAfter; }) .join(""); return fixer.replaceTextRange( [ node.properties[0].range[0], // Start index of the first node. node.properties[node.properties.length - 1].range[1], // End index of the last node. ], newText, ); } module.exports = { meta: { docs: { description: "require object destructure keys to be sorted", category: "Stylistic Issues", recommended: false, }, fixable: "code", messages: { sort: `Expected object keys to be in sorted order. Expected {{first}} to be before {{second}}.`, }, schema: [ { type: "object", properties: { caseSensitive: { type: "boolean", }, }, additionalProperties: false, }, ], }, create(context) { const { sourceCode } = context; const options = context.options[0] || {}; const { caseSensitive = true } = options; const sorter = createSorter(caseSensitive); return { ObjectPattern(objectPatternNode) { const scope = sourceCode && sourceCode.getScope ? sourceCode.getScope(objectPatternNode) : context.getScope(); /* * If the node is more complex than just basic destructuring * with literal defaults, we just skip it. If some values use * previous values as defaults, then we cannot simply sort them. */ if ( !objectPatternNode.properties.every((node) => shouldCheck(scope, objectPatternNode, node), ) ) { return; } let prevNode = null; for (const nextNode of objectPatternNode.properties) { if (prevNode && sorter(prevNode, nextNode) > 0) { context.report({ node: nextNode, messageId: "sort", data: { first: getNodeName(nextNode), second: getNodeName(prevNode), }, fix: (fixer) => createFix({ context, caseSensitive, fixer, node: objectPatternNode, sorter, }), }); break; } prevNode = nextNode; } }, }; }, };