| /** | |
|  * @fileoverview Rule to require object keys to be sorted | |
|  * @author Toru Nagashima | |
|  */ | |
| 
 | |
| "use strict"; | |
| 
 | |
| //------------------------------------------------------------------------------ | |
| // Requirements | |
| //------------------------------------------------------------------------------ | |
|  | |
| const astUtils = require("./utils/ast-utils"), | |
|     naturalCompare = require("natural-compare"); | |
| 
 | |
| //------------------------------------------------------------------------------ | |
| // Helpers | |
| //------------------------------------------------------------------------------ | |
|  | |
| /** | |
|  * Gets the property name of the given `Property` node. | |
|  * | |
|  * - If the property's key is an `Identifier` node, this returns the key's name | |
|  *   whether it's a computed property or not. | |
|  * - If the property has a static name, this returns the static name. | |
|  * - Otherwise, this returns null. | |
|  * @param {ASTNode} node The `Property` node to get. | |
|  * @returns {string|null} The property name or null. | |
|  * @private | |
|  */ | |
| function getPropertyName(node) { | |
|     const staticName = astUtils.getStaticPropertyName(node); | |
| 
 | |
|     if (staticName !== null) { | |
|         return staticName; | |
|     } | |
| 
 | |
|     return node.key.name || null; | |
| } | |
| 
 | |
| /** | |
|  * Functions which check that the given 2 names are in specific order. | |
|  * | |
|  * Postfix `I` is meant insensitive. | |
|  * Postfix `N` is meant natural. | |
|  * @private | |
|  */ | |
| const isValidOrders = { | |
|     asc(a, b) { | |
|         return a <= b; | |
|     }, | |
|     ascI(a, b) { | |
|         return a.toLowerCase() <= b.toLowerCase(); | |
|     }, | |
|     ascN(a, b) { | |
|         return naturalCompare(a, b) <= 0; | |
|     }, | |
|     ascIN(a, b) { | |
|         return naturalCompare(a.toLowerCase(), b.toLowerCase()) <= 0; | |
|     }, | |
|     desc(a, b) { | |
|         return isValidOrders.asc(b, a); | |
|     }, | |
|     descI(a, b) { | |
|         return isValidOrders.ascI(b, a); | |
|     }, | |
|     descN(a, b) { | |
|         return isValidOrders.ascN(b, a); | |
|     }, | |
|     descIN(a, b) { | |
|         return isValidOrders.ascIN(b, a); | |
|     } | |
| }; | |
| 
 | |
| //------------------------------------------------------------------------------ | |
| // Rule Definition | |
| //------------------------------------------------------------------------------ | |
|  | |
| /** @type {import('../shared/types').Rule} */ | |
| module.exports = { | |
|     meta: { | |
|         type: "suggestion", | |
| 
 | |
|         docs: { | |
|             description: "Require object keys to be sorted", | |
|             recommended: false, | |
|             url: "https://eslint.org/docs/latest/rules/sort-keys" | |
|         }, | |
| 
 | |
|         schema: [ | |
|             { | |
|                 enum: ["asc", "desc"] | |
|             }, | |
|             { | |
|                 type: "object", | |
|                 properties: { | |
|                     caseSensitive: { | |
|                         type: "boolean", | |
|                         default: true | |
|                     }, | |
|                     natural: { | |
|                         type: "boolean", | |
|                         default: false | |
|                     }, | |
|                     minKeys: { | |
|                         type: "integer", | |
|                         minimum: 2, | |
|                         default: 2 | |
|                     }, | |
|                     allowLineSeparatedGroups: { | |
|                         type: "boolean", | |
|                         default: false | |
|                     } | |
|                 }, | |
|                 additionalProperties: false | |
|             } | |
|         ], | |
| 
 | |
|         messages: { | |
|             sortKeys: "Expected object keys to be in {{natural}}{{insensitive}}{{order}}ending order. '{{thisName}}' should be before '{{prevName}}'." | |
|         } | |
|     }, | |
| 
 | |
|     create(context) { | |
| 
 | |
|         // Parse options. | |
|         const order = context.options[0] || "asc"; | |
|         const options = context.options[1]; | |
|         const insensitive = options && options.caseSensitive === false; | |
|         const natural = options && options.natural; | |
|         const minKeys = options && options.minKeys; | |
|         const allowLineSeparatedGroups = options && options.allowLineSeparatedGroups || false; | |
|         const isValidOrder = isValidOrders[ | |
|             order + (insensitive ? "I" : "") + (natural ? "N" : "") | |
|         ]; | |
| 
 | |
|         // The stack to save the previous property's name for each object literals. | |
|         let stack = null; | |
|         const sourceCode = context.sourceCode; | |
| 
 | |
|         return { | |
|             ObjectExpression(node) { | |
|                 stack = { | |
|                     upper: stack, | |
|                     prevNode: null, | |
|                     prevBlankLine: false, | |
|                     prevName: null, | |
|                     numKeys: node.properties.length | |
|                 }; | |
|             }, | |
| 
 | |
|             "ObjectExpression:exit"() { | |
|                 stack = stack.upper; | |
|             }, | |
| 
 | |
|             SpreadElement(node) { | |
|                 if (node.parent.type === "ObjectExpression") { | |
|                     stack.prevName = null; | |
|                 } | |
|             }, | |
| 
 | |
|             Property(node) { | |
|                 if (node.parent.type === "ObjectPattern") { | |
|                     return; | |
|                 } | |
| 
 | |
|                 const prevName = stack.prevName; | |
|                 const numKeys = stack.numKeys; | |
|                 const thisName = getPropertyName(node); | |
| 
 | |
|                 // Get tokens between current node and previous node | |
|                 const tokens = stack.prevNode && sourceCode | |
|                     .getTokensBetween(stack.prevNode, node, { includeComments: true }); | |
| 
 | |
|                 let isBlankLineBetweenNodes = stack.prevBlankLine; | |
| 
 | |
|                 if (tokens) { | |
| 
 | |
|                     // check blank line between tokens | |
|                     tokens.forEach((token, index) => { | |
|                         const previousToken = tokens[index - 1]; | |
| 
 | |
|                         if (previousToken && (token.loc.start.line - previousToken.loc.end.line > 1)) { | |
|                             isBlankLineBetweenNodes = true; | |
|                         } | |
|                     }); | |
| 
 | |
|                     // check blank line between the current node and the last token | |
|                     if (!isBlankLineBetweenNodes && (node.loc.start.line - tokens[tokens.length - 1].loc.end.line > 1)) { | |
|                         isBlankLineBetweenNodes = true; | |
|                     } | |
| 
 | |
|                     // check blank line between the first token and the previous node | |
|                     if (!isBlankLineBetweenNodes && (tokens[0].loc.start.line - stack.prevNode.loc.end.line > 1)) { | |
|                         isBlankLineBetweenNodes = true; | |
|                     } | |
|                 } | |
| 
 | |
|                 stack.prevNode = node; | |
| 
 | |
|                 if (thisName !== null) { | |
|                     stack.prevName = thisName; | |
|                 } | |
| 
 | |
|                 if (allowLineSeparatedGroups && isBlankLineBetweenNodes) { | |
|                     stack.prevBlankLine = thisName === null; | |
|                     return; | |
|                 } | |
| 
 | |
|                 if (prevName === null || thisName === null || numKeys < minKeys) { | |
|                     return; | |
|                 } | |
| 
 | |
|                 if (!isValidOrder(prevName, thisName)) { | |
|                     context.report({ | |
|                         node, | |
|                         loc: node.key.loc, | |
|                         messageId: "sortKeys", | |
|                         data: { | |
|                             thisName, | |
|                             prevName, | |
|                             order, | |
|                             insensitive: insensitive ? "insensitive " : "", | |
|                             natural: natural ? "natural " : "" | |
|                         } | |
|                     }); | |
|                 } | |
|             } | |
|         }; | |
|     } | |
| };
 |