sort-imports.js 9.1 KB

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