sort-keys.js 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236
  1. /**
  2. * @fileoverview Rule to require object keys to be sorted
  3. * @author Toru Nagashima
  4. */
  5. "use strict";
  6. //------------------------------------------------------------------------------
  7. // Requirements
  8. //------------------------------------------------------------------------------
  9. const astUtils = require("./utils/ast-utils"),
  10. naturalCompare = require("natural-compare");
  11. //------------------------------------------------------------------------------
  12. // Helpers
  13. //------------------------------------------------------------------------------
  14. /**
  15. * Gets the property name of the given `Property` node.
  16. *
  17. * - If the property's key is an `Identifier` node, this returns the key's name
  18. * whether it's a computed property or not.
  19. * - If the property has a static name, this returns the static name.
  20. * - Otherwise, this returns null.
  21. * @param {ASTNode} node The `Property` node to get.
  22. * @returns {string|null} The property name or null.
  23. * @private
  24. */
  25. function getPropertyName(node) {
  26. const staticName = astUtils.getStaticPropertyName(node);
  27. if (staticName !== null) {
  28. return staticName;
  29. }
  30. return node.key.name || null;
  31. }
  32. /**
  33. * Functions which check that the given 2 names are in specific order.
  34. *
  35. * Postfix `I` is meant insensitive.
  36. * Postfix `N` is meant natural.
  37. * @private
  38. */
  39. const isValidOrders = {
  40. asc(a, b) {
  41. return a <= b;
  42. },
  43. ascI(a, b) {
  44. return a.toLowerCase() <= b.toLowerCase();
  45. },
  46. ascN(a, b) {
  47. return naturalCompare(a, b) <= 0;
  48. },
  49. ascIN(a, b) {
  50. return naturalCompare(a.toLowerCase(), b.toLowerCase()) <= 0;
  51. },
  52. desc(a, b) {
  53. return isValidOrders.asc(b, a);
  54. },
  55. descI(a, b) {
  56. return isValidOrders.ascI(b, a);
  57. },
  58. descN(a, b) {
  59. return isValidOrders.ascN(b, a);
  60. },
  61. descIN(a, b) {
  62. return isValidOrders.ascIN(b, a);
  63. }
  64. };
  65. //------------------------------------------------------------------------------
  66. // Rule Definition
  67. //------------------------------------------------------------------------------
  68. /** @type {import('../shared/types').Rule} */
  69. module.exports = {
  70. meta: {
  71. type: "suggestion",
  72. defaultOptions: ["asc", {
  73. allowLineSeparatedGroups: false,
  74. caseSensitive: true,
  75. ignoreComputedKeys: false,
  76. minKeys: 2,
  77. natural: false
  78. }],
  79. docs: {
  80. description: "Require object keys to be sorted",
  81. recommended: false,
  82. url: "https://eslint.org/docs/latest/rules/sort-keys"
  83. },
  84. schema: [
  85. {
  86. enum: ["asc", "desc"]
  87. },
  88. {
  89. type: "object",
  90. properties: {
  91. caseSensitive: {
  92. type: "boolean"
  93. },
  94. natural: {
  95. type: "boolean"
  96. },
  97. minKeys: {
  98. type: "integer",
  99. minimum: 2
  100. },
  101. allowLineSeparatedGroups: {
  102. type: "boolean"
  103. },
  104. ignoreComputedKeys: {
  105. type: "boolean"
  106. }
  107. },
  108. additionalProperties: false
  109. }
  110. ],
  111. messages: {
  112. sortKeys: "Expected object keys to be in {{natural}}{{insensitive}}{{order}}ending order. '{{thisName}}' should be before '{{prevName}}'."
  113. }
  114. },
  115. create(context) {
  116. const [order, { caseSensitive, natural, minKeys, allowLineSeparatedGroups, ignoreComputedKeys }] = context.options;
  117. const insensitive = !caseSensitive;
  118. const isValidOrder = isValidOrders[
  119. order + (insensitive ? "I" : "") + (natural ? "N" : "")
  120. ];
  121. // The stack to save the previous property's name for each object literals.
  122. let stack = null;
  123. const sourceCode = context.sourceCode;
  124. return {
  125. ObjectExpression(node) {
  126. stack = {
  127. upper: stack,
  128. prevNode: null,
  129. prevBlankLine: false,
  130. prevName: null,
  131. numKeys: node.properties.length
  132. };
  133. },
  134. "ObjectExpression:exit"() {
  135. stack = stack.upper;
  136. },
  137. SpreadElement(node) {
  138. if (node.parent.type === "ObjectExpression") {
  139. stack.prevName = null;
  140. }
  141. },
  142. Property(node) {
  143. if (node.parent.type === "ObjectPattern") {
  144. return;
  145. }
  146. if (ignoreComputedKeys && node.computed) {
  147. stack.prevName = null; // reset sort
  148. return;
  149. }
  150. const prevName = stack.prevName;
  151. const numKeys = stack.numKeys;
  152. const thisName = getPropertyName(node);
  153. // Get tokens between current node and previous node
  154. const tokens = stack.prevNode && sourceCode
  155. .getTokensBetween(stack.prevNode, node, { includeComments: true });
  156. let isBlankLineBetweenNodes = stack.prevBlankLine;
  157. if (tokens) {
  158. // check blank line between tokens
  159. tokens.forEach((token, index) => {
  160. const previousToken = tokens[index - 1];
  161. if (previousToken && (token.loc.start.line - previousToken.loc.end.line > 1)) {
  162. isBlankLineBetweenNodes = true;
  163. }
  164. });
  165. // check blank line between the current node and the last token
  166. if (!isBlankLineBetweenNodes && (node.loc.start.line - tokens.at(-1).loc.end.line > 1)) {
  167. isBlankLineBetweenNodes = true;
  168. }
  169. // check blank line between the first token and the previous node
  170. if (!isBlankLineBetweenNodes && (tokens[0].loc.start.line - stack.prevNode.loc.end.line > 1)) {
  171. isBlankLineBetweenNodes = true;
  172. }
  173. }
  174. stack.prevNode = node;
  175. if (thisName !== null) {
  176. stack.prevName = thisName;
  177. }
  178. if (allowLineSeparatedGroups && isBlankLineBetweenNodes) {
  179. stack.prevBlankLine = thisName === null;
  180. return;
  181. }
  182. if (prevName === null || thisName === null || numKeys < minKeys) {
  183. return;
  184. }
  185. if (!isValidOrder(prevName, thisName)) {
  186. context.report({
  187. node,
  188. loc: node.key.loc,
  189. messageId: "sortKeys",
  190. data: {
  191. thisName,
  192. prevName,
  193. order,
  194. insensitive: insensitive ? "insensitive " : "",
  195. natural: natural ? "natural " : ""
  196. }
  197. });
  198. }
  199. }
  200. };
  201. }
  202. };