sort-imports.js 11 KB

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