sort-imports.js 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207
  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. },
  44. additionalProperties: false
  45. }
  46. ],
  47. fixable: "code"
  48. },
  49. create(context) {
  50. const configuration = context.options[0] || {},
  51. ignoreCase = configuration.ignoreCase || false,
  52. ignoreDeclarationSort = configuration.ignoreDeclarationSort || false,
  53. ignoreMemberSort = configuration.ignoreMemberSort || false,
  54. memberSyntaxSortOrder = configuration.memberSyntaxSortOrder || ["none", "all", "multiple", "single"],
  55. sourceCode = context.getSourceCode();
  56. let previousDeclaration = null;
  57. /**
  58. * Gets the used member syntax style.
  59. *
  60. * import "my-module.js" --> none
  61. * import * as myModule from "my-module.js" --> all
  62. * import {myMember} from "my-module.js" --> single
  63. * import {foo, bar} from "my-module.js" --> multiple
  64. * @param {ASTNode} node the ImportDeclaration node.
  65. * @returns {string} used member parameter style, ["all", "multiple", "single"]
  66. */
  67. function usedMemberSyntax(node) {
  68. if (node.specifiers.length === 0) {
  69. return "none";
  70. }
  71. if (node.specifiers[0].type === "ImportNamespaceSpecifier") {
  72. return "all";
  73. }
  74. if (node.specifiers.length === 1) {
  75. return "single";
  76. }
  77. return "multiple";
  78. }
  79. /**
  80. * Gets the group by member parameter index for given declaration.
  81. * @param {ASTNode} node the ImportDeclaration node.
  82. * @returns {number} the declaration group by member index.
  83. */
  84. function getMemberParameterGroupIndex(node) {
  85. return memberSyntaxSortOrder.indexOf(usedMemberSyntax(node));
  86. }
  87. /**
  88. * Gets the local name of the first imported module.
  89. * @param {ASTNode} node the ImportDeclaration node.
  90. * @returns {?string} the local name of the first imported module.
  91. */
  92. function getFirstLocalMemberName(node) {
  93. if (node.specifiers[0]) {
  94. return node.specifiers[0].local.name;
  95. }
  96. return null;
  97. }
  98. return {
  99. ImportDeclaration(node) {
  100. if (!ignoreDeclarationSort) {
  101. if (previousDeclaration) {
  102. const currentMemberSyntaxGroupIndex = getMemberParameterGroupIndex(node),
  103. previousMemberSyntaxGroupIndex = getMemberParameterGroupIndex(previousDeclaration);
  104. let currentLocalMemberName = getFirstLocalMemberName(node),
  105. previousLocalMemberName = getFirstLocalMemberName(previousDeclaration);
  106. if (ignoreCase) {
  107. previousLocalMemberName = previousLocalMemberName && previousLocalMemberName.toLowerCase();
  108. currentLocalMemberName = currentLocalMemberName && currentLocalMemberName.toLowerCase();
  109. }
  110. /*
  111. * When the current declaration uses a different member syntax,
  112. * then check if the ordering is correct.
  113. * Otherwise, make a default string compare (like rule sort-vars to be consistent) of the first used local member name.
  114. */
  115. if (currentMemberSyntaxGroupIndex !== previousMemberSyntaxGroupIndex) {
  116. if (currentMemberSyntaxGroupIndex < previousMemberSyntaxGroupIndex) {
  117. context.report({
  118. node,
  119. message: "Expected '{{syntaxA}}' syntax before '{{syntaxB}}' syntax.",
  120. data: {
  121. syntaxA: memberSyntaxSortOrder[currentMemberSyntaxGroupIndex],
  122. syntaxB: memberSyntaxSortOrder[previousMemberSyntaxGroupIndex]
  123. }
  124. });
  125. }
  126. } else {
  127. if (previousLocalMemberName &&
  128. currentLocalMemberName &&
  129. currentLocalMemberName < previousLocalMemberName
  130. ) {
  131. context.report({
  132. node,
  133. message: "Imports should be sorted alphabetically."
  134. });
  135. }
  136. }
  137. }
  138. previousDeclaration = node;
  139. }
  140. if (!ignoreMemberSort) {
  141. const importSpecifiers = node.specifiers.filter(specifier => specifier.type === "ImportSpecifier");
  142. const getSortableName = ignoreCase ? specifier => specifier.local.name.toLowerCase() : specifier => specifier.local.name;
  143. const firstUnsortedIndex = importSpecifiers.map(getSortableName).findIndex((name, index, array) => array[index - 1] > name);
  144. if (firstUnsortedIndex !== -1) {
  145. context.report({
  146. node: importSpecifiers[firstUnsortedIndex],
  147. message: "Member '{{memberName}}' of the import declaration should be sorted alphabetically.",
  148. data: { memberName: importSpecifiers[firstUnsortedIndex].local.name },
  149. fix(fixer) {
  150. if (importSpecifiers.some(specifier =>
  151. sourceCode.getCommentsBefore(specifier).length || sourceCode.getCommentsAfter(specifier).length)) {
  152. // If there are comments in the ImportSpecifier list, don't rearrange the specifiers.
  153. return null;
  154. }
  155. return fixer.replaceTextRange(
  156. [importSpecifiers[0].range[0], importSpecifiers[importSpecifiers.length - 1].range[1]],
  157. importSpecifiers
  158. // Clone the importSpecifiers array to avoid mutating it
  159. .slice()
  160. // Sort the array into the desired order
  161. .sort((specifierA, specifierB) => {
  162. const aName = getSortableName(specifierA);
  163. const bName = getSortableName(specifierB);
  164. return aName > bName ? 1 : -1;
  165. })
  166. // Build a string out of the sorted list of import specifiers and the text between the originals
  167. .reduce((sourceText, specifier, index) => {
  168. const textAfterSpecifier = index === importSpecifiers.length - 1
  169. ? ""
  170. : sourceCode.getText().slice(importSpecifiers[index].range[1], importSpecifiers[index + 1].range[0]);
  171. return sourceText + sourceCode.getText(specifier) + textAfterSpecifier;
  172. }, "")
  173. );
  174. }
  175. });
  176. }
  177. }
  178. }
  179. };
  180. }
  181. };