sort-keys.js 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268
  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('../types').Rule.RuleModule} */
  69. module.exports = {
  70. meta: {
  71. type: "suggestion",
  72. defaultOptions: [
  73. "asc",
  74. {
  75. allowLineSeparatedGroups: false,
  76. caseSensitive: true,
  77. ignoreComputedKeys: false,
  78. minKeys: 2,
  79. natural: false,
  80. },
  81. ],
  82. docs: {
  83. description: "Require object keys to be sorted",
  84. recommended: false,
  85. frozen: true,
  86. url: "https://eslint.org/docs/latest/rules/sort-keys",
  87. },
  88. schema: [
  89. {
  90. enum: ["asc", "desc"],
  91. },
  92. {
  93. type: "object",
  94. properties: {
  95. caseSensitive: {
  96. type: "boolean",
  97. },
  98. natural: {
  99. type: "boolean",
  100. },
  101. minKeys: {
  102. type: "integer",
  103. minimum: 2,
  104. },
  105. allowLineSeparatedGroups: {
  106. type: "boolean",
  107. },
  108. ignoreComputedKeys: {
  109. type: "boolean",
  110. },
  111. },
  112. additionalProperties: false,
  113. },
  114. ],
  115. messages: {
  116. sortKeys:
  117. "Expected object keys to be in {{natural}}{{insensitive}}{{order}}ending order. '{{thisName}}' should be before '{{prevName}}'.",
  118. },
  119. },
  120. create(context) {
  121. const [
  122. order,
  123. {
  124. caseSensitive,
  125. natural,
  126. minKeys,
  127. allowLineSeparatedGroups,
  128. ignoreComputedKeys,
  129. },
  130. ] = context.options;
  131. const insensitive = !caseSensitive;
  132. const isValidOrder =
  133. isValidOrders[
  134. order + (insensitive ? "I" : "") + (natural ? "N" : "")
  135. ];
  136. // The stack to save the previous property's name for each object literals.
  137. let stack = null;
  138. const sourceCode = context.sourceCode;
  139. return {
  140. ObjectExpression(node) {
  141. stack = {
  142. upper: stack,
  143. prevNode: null,
  144. prevBlankLine: false,
  145. prevName: null,
  146. numKeys: node.properties.length,
  147. };
  148. },
  149. "ObjectExpression:exit"() {
  150. stack = stack.upper;
  151. },
  152. SpreadElement(node) {
  153. if (node.parent.type === "ObjectExpression") {
  154. stack.prevName = null;
  155. }
  156. },
  157. Property(node) {
  158. if (node.parent.type === "ObjectPattern") {
  159. return;
  160. }
  161. if (ignoreComputedKeys && node.computed) {
  162. stack.prevName = null; // reset sort
  163. return;
  164. }
  165. const prevName = stack.prevName;
  166. const numKeys = stack.numKeys;
  167. const thisName = getPropertyName(node);
  168. // Get tokens between current node and previous node
  169. const tokens =
  170. stack.prevNode &&
  171. sourceCode.getTokensBetween(stack.prevNode, node, {
  172. includeComments: true,
  173. });
  174. let isBlankLineBetweenNodes = stack.prevBlankLine;
  175. if (tokens) {
  176. // check blank line between tokens
  177. tokens.forEach((token, index) => {
  178. const previousToken = tokens[index - 1];
  179. if (
  180. previousToken &&
  181. token.loc.start.line - previousToken.loc.end.line >
  182. 1
  183. ) {
  184. isBlankLineBetweenNodes = true;
  185. }
  186. });
  187. // check blank line between the current node and the last token
  188. if (
  189. !isBlankLineBetweenNodes &&
  190. node.loc.start.line - tokens.at(-1).loc.end.line > 1
  191. ) {
  192. isBlankLineBetweenNodes = true;
  193. }
  194. // check blank line between the first token and the previous node
  195. if (
  196. !isBlankLineBetweenNodes &&
  197. tokens[0].loc.start.line - stack.prevNode.loc.end.line >
  198. 1
  199. ) {
  200. isBlankLineBetweenNodes = true;
  201. }
  202. }
  203. stack.prevNode = node;
  204. if (thisName !== null) {
  205. stack.prevName = thisName;
  206. }
  207. if (allowLineSeparatedGroups && isBlankLineBetweenNodes) {
  208. stack.prevBlankLine = thisName === null;
  209. return;
  210. }
  211. if (
  212. prevName === null ||
  213. thisName === null ||
  214. numKeys < minKeys
  215. ) {
  216. return;
  217. }
  218. if (!isValidOrder(prevName, thisName)) {
  219. context.report({
  220. node,
  221. loc: node.key.loc,
  222. messageId: "sortKeys",
  223. data: {
  224. thisName,
  225. prevName,
  226. order,
  227. insensitive: insensitive ? "insensitive " : "",
  228. natural: natural ? "natural " : "",
  229. },
  230. });
  231. }
  232. },
  233. };
  234. },
  235. };