sort-imports.js 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319
  1. /**
  2. * @fileoverview Rule to enforce sorted `import` declarations within modules
  3. * @author Christian Schuller
  4. */
  5. "use strict";
  6. //------------------------------------------------------------------------------
  7. // Rule Definition
  8. //------------------------------------------------------------------------------
  9. /** @type {import('../types').Rule.RuleModule} */
  10. module.exports = {
  11. meta: {
  12. type: "suggestion",
  13. defaultOptions: [
  14. {
  15. allowSeparatedGroups: false,
  16. ignoreCase: false,
  17. ignoreDeclarationSort: false,
  18. ignoreMemberSort: false,
  19. memberSyntaxSortOrder: ["none", "all", "multiple", "single"],
  20. },
  21. ],
  22. docs: {
  23. description: "Enforce sorted `import` declarations within modules",
  24. recommended: false,
  25. frozen: true,
  26. url: "https://eslint.org/docs/latest/rules/sort-imports",
  27. },
  28. schema: [
  29. {
  30. type: "object",
  31. properties: {
  32. ignoreCase: {
  33. type: "boolean",
  34. },
  35. memberSyntaxSortOrder: {
  36. type: "array",
  37. items: {
  38. enum: ["none", "all", "multiple", "single"],
  39. },
  40. uniqueItems: true,
  41. minItems: 4,
  42. maxItems: 4,
  43. },
  44. ignoreDeclarationSort: {
  45. type: "boolean",
  46. },
  47. ignoreMemberSort: {
  48. type: "boolean",
  49. },
  50. allowSeparatedGroups: {
  51. type: "boolean",
  52. },
  53. },
  54. additionalProperties: false,
  55. },
  56. ],
  57. fixable: "code",
  58. messages: {
  59. sortImportsAlphabetically:
  60. "Imports should be sorted alphabetically.",
  61. sortMembersAlphabetically:
  62. "Member '{{memberName}}' of the import declaration should be sorted alphabetically.",
  63. unexpectedSyntaxOrder:
  64. "Expected '{{syntaxA}}' syntax before '{{syntaxB}}' syntax.",
  65. },
  66. },
  67. create(context) {
  68. const [
  69. {
  70. ignoreCase,
  71. ignoreDeclarationSort,
  72. ignoreMemberSort,
  73. memberSyntaxSortOrder,
  74. allowSeparatedGroups,
  75. },
  76. ] = context.options;
  77. const sourceCode = context.sourceCode;
  78. let previousDeclaration = null;
  79. /**
  80. * Gets the used member syntax style.
  81. *
  82. * import "my-module.js" --> none
  83. * import * as myModule from "my-module.js" --> all
  84. * import {myMember} from "my-module.js" --> single
  85. * import {foo, bar} from "my-module.js" --> multiple
  86. * @param {ASTNode} node the ImportDeclaration node.
  87. * @returns {string} used member parameter style, ["all", "multiple", "single"]
  88. */
  89. function usedMemberSyntax(node) {
  90. if (node.specifiers.length === 0) {
  91. return "none";
  92. }
  93. if (node.specifiers[0].type === "ImportNamespaceSpecifier") {
  94. return "all";
  95. }
  96. if (node.specifiers.length === 1) {
  97. return "single";
  98. }
  99. return "multiple";
  100. }
  101. /**
  102. * Gets the group by member parameter index for given declaration.
  103. * @param {ASTNode} node the ImportDeclaration node.
  104. * @returns {number} the declaration group by member index.
  105. */
  106. function getMemberParameterGroupIndex(node) {
  107. return memberSyntaxSortOrder.indexOf(usedMemberSyntax(node));
  108. }
  109. /**
  110. * Gets the local name of the first imported module.
  111. * @param {ASTNode} node the ImportDeclaration node.
  112. * @returns {?string} the local name of the first imported module.
  113. */
  114. function getFirstLocalMemberName(node) {
  115. if (node.specifiers[0]) {
  116. return node.specifiers[0].local.name;
  117. }
  118. return null;
  119. }
  120. /**
  121. * Calculates number of lines between two nodes. It is assumed that the given `left` node appears before
  122. * the given `right` node in the source code. Lines are counted from the end of the `left` node till the
  123. * start of the `right` node. If the given nodes are on the same line, it returns `0`, same as if they were
  124. * on two consecutive lines.
  125. * @param {ASTNode} left node that appears before the given `right` node.
  126. * @param {ASTNode} right node that appears after the given `left` node.
  127. * @returns {number} number of lines between nodes.
  128. */
  129. function getNumberOfLinesBetween(left, right) {
  130. return Math.max(right.loc.start.line - left.loc.end.line - 1, 0);
  131. }
  132. return {
  133. ImportDeclaration(node) {
  134. if (!ignoreDeclarationSort) {
  135. if (
  136. previousDeclaration &&
  137. allowSeparatedGroups &&
  138. getNumberOfLinesBetween(previousDeclaration, node) > 0
  139. ) {
  140. // reset declaration sort
  141. previousDeclaration = null;
  142. }
  143. if (previousDeclaration) {
  144. const currentMemberSyntaxGroupIndex =
  145. getMemberParameterGroupIndex(node),
  146. previousMemberSyntaxGroupIndex =
  147. getMemberParameterGroupIndex(
  148. previousDeclaration,
  149. );
  150. let currentLocalMemberName =
  151. getFirstLocalMemberName(node),
  152. previousLocalMemberName =
  153. getFirstLocalMemberName(previousDeclaration);
  154. if (ignoreCase) {
  155. previousLocalMemberName =
  156. previousLocalMemberName &&
  157. previousLocalMemberName.toLowerCase();
  158. currentLocalMemberName =
  159. currentLocalMemberName &&
  160. currentLocalMemberName.toLowerCase();
  161. }
  162. /*
  163. * When the current declaration uses a different member syntax,
  164. * then check if the ordering is correct.
  165. * Otherwise, make a default string compare (like rule sort-vars to be consistent) of the first used local member name.
  166. */
  167. if (
  168. currentMemberSyntaxGroupIndex !==
  169. previousMemberSyntaxGroupIndex
  170. ) {
  171. if (
  172. currentMemberSyntaxGroupIndex <
  173. previousMemberSyntaxGroupIndex
  174. ) {
  175. context.report({
  176. node,
  177. messageId: "unexpectedSyntaxOrder",
  178. data: {
  179. syntaxA:
  180. memberSyntaxSortOrder[
  181. currentMemberSyntaxGroupIndex
  182. ],
  183. syntaxB:
  184. memberSyntaxSortOrder[
  185. previousMemberSyntaxGroupIndex
  186. ],
  187. },
  188. });
  189. }
  190. } else {
  191. if (
  192. previousLocalMemberName &&
  193. currentLocalMemberName &&
  194. currentLocalMemberName < previousLocalMemberName
  195. ) {
  196. context.report({
  197. node,
  198. messageId: "sortImportsAlphabetically",
  199. });
  200. }
  201. }
  202. }
  203. previousDeclaration = node;
  204. }
  205. if (!ignoreMemberSort) {
  206. const importSpecifiers = node.specifiers.filter(
  207. specifier => specifier.type === "ImportSpecifier",
  208. );
  209. const getSortableName = ignoreCase
  210. ? specifier => specifier.local.name.toLowerCase()
  211. : specifier => specifier.local.name;
  212. const firstUnsortedIndex = importSpecifiers
  213. .map(getSortableName)
  214. .findIndex(
  215. (name, index, array) => array[index - 1] > name,
  216. );
  217. if (firstUnsortedIndex !== -1) {
  218. context.report({
  219. node: importSpecifiers[firstUnsortedIndex],
  220. messageId: "sortMembersAlphabetically",
  221. data: {
  222. memberName:
  223. importSpecifiers[firstUnsortedIndex].local
  224. .name,
  225. },
  226. fix(fixer) {
  227. if (
  228. importSpecifiers.some(
  229. specifier =>
  230. sourceCode.getCommentsBefore(
  231. specifier,
  232. ).length ||
  233. sourceCode.getCommentsAfter(
  234. specifier,
  235. ).length,
  236. )
  237. ) {
  238. // If there are comments in the ImportSpecifier list, don't rearrange the specifiers.
  239. return null;
  240. }
  241. return fixer.replaceTextRange(
  242. [
  243. importSpecifiers[0].range[0],
  244. importSpecifiers.at(-1).range[1],
  245. ],
  246. importSpecifiers
  247. // Clone the importSpecifiers array to avoid mutating it
  248. .slice()
  249. // Sort the array into the desired order
  250. .sort((specifierA, specifierB) => {
  251. const aName =
  252. getSortableName(specifierA);
  253. const bName =
  254. getSortableName(specifierB);
  255. return aName > bName ? 1 : -1;
  256. })
  257. // Build a string out of the sorted list of import specifiers and the text between the originals
  258. .reduce(
  259. (sourceText, specifier, index) => {
  260. const textAfterSpecifier =
  261. index ===
  262. importSpecifiers.length - 1
  263. ? ""
  264. : sourceCode
  265. .getText()
  266. .slice(
  267. importSpecifiers[
  268. index
  269. ].range[1],
  270. importSpecifiers[
  271. index +
  272. 1
  273. ].range[0],
  274. );
  275. return (
  276. sourceText +
  277. sourceCode.getText(
  278. specifier,
  279. ) +
  280. textAfterSpecifier
  281. );
  282. },
  283. "",
  284. ),
  285. );
  286. },
  287. });
  288. }
  289. }
  290. },
  291. };
  292. },
  293. };