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