231 lines
		
	
	
		
			7.2 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			231 lines
		
	
	
		
			7.2 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /**
 | |
|  * @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 " : ""
 | |
|                         }
 | |
|                     });
 | |
|                 }
 | |
|             }
 | |
|         };
 | |
|     }
 | |
| };
 |