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 " : ""
 | 
						|
                        }
 | 
						|
                    });
 | 
						|
                }
 | 
						|
            }
 | 
						|
        };
 | 
						|
    }
 | 
						|
};
 |