sort-imports.js 8.5 KB

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