You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

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