2 * @fileoverview Rule to require sorting of import declarations
3 * @author Christian Schuller
8 //------------------------------------------------------------------------------
10 //------------------------------------------------------------------------------
17 description: "enforce sorted import declarations within modules",
18 category: "ECMAScript 6",
20 url: "https://eslint.org/docs/rules/sort-imports"
31 memberSyntaxSortOrder: {
34 enum: ["none", "all", "multiple", "single"]
40 ignoreDeclarationSort: {
49 additionalProperties: false
58 const configuration = context.options[0] || {},
59 ignoreCase = configuration.ignoreCase || false,
60 ignoreDeclarationSort = configuration.ignoreDeclarationSort || false,
61 ignoreMemberSort = configuration.ignoreMemberSort || false,
62 memberSyntaxSortOrder = configuration.memberSyntaxSortOrder || ["none", "all", "multiple", "single"],
63 sourceCode = context.getSourceCode();
64 let previousDeclaration = null;
67 * Gets the used member syntax style.
69 * import "my-module.js" --> none
70 * import * as myModule from "my-module.js" --> all
71 * import {myMember} from "my-module.js" --> single
72 * import {foo, bar} from "my-module.js" --> multiple
73 * @param {ASTNode} node the ImportDeclaration node.
74 * @returns {string} used member parameter style, ["all", "multiple", "single"]
76 function usedMemberSyntax(node) {
77 if (node.specifiers.length === 0) {
80 if (node.specifiers[0].type === "ImportNamespaceSpecifier") {
83 if (node.specifiers.length === 1) {
91 * Gets the group by member parameter index for given declaration.
92 * @param {ASTNode} node the ImportDeclaration node.
93 * @returns {number} the declaration group by member index.
95 function getMemberParameterGroupIndex(node) {
96 return memberSyntaxSortOrder.indexOf(usedMemberSyntax(node));
100 * Gets the local name of the first imported module.
101 * @param {ASTNode} node the ImportDeclaration node.
102 * @returns {?string} the local name of the first imported module.
104 function getFirstLocalMemberName(node) {
105 if (node.specifiers[0]) {
106 return node.specifiers[0].local.name;
113 ImportDeclaration(node) {
114 if (!ignoreDeclarationSort) {
115 if (previousDeclaration) {
116 const currentMemberSyntaxGroupIndex = getMemberParameterGroupIndex(node),
117 previousMemberSyntaxGroupIndex = getMemberParameterGroupIndex(previousDeclaration);
118 let currentLocalMemberName = getFirstLocalMemberName(node),
119 previousLocalMemberName = getFirstLocalMemberName(previousDeclaration);
122 previousLocalMemberName = previousLocalMemberName && previousLocalMemberName.toLowerCase();
123 currentLocalMemberName = currentLocalMemberName && currentLocalMemberName.toLowerCase();
127 * When the current declaration uses a different member syntax,
128 * then check if the ordering is correct.
129 * Otherwise, make a default string compare (like rule sort-vars to be consistent) of the first used local member name.
131 if (currentMemberSyntaxGroupIndex !== previousMemberSyntaxGroupIndex) {
132 if (currentMemberSyntaxGroupIndex < previousMemberSyntaxGroupIndex) {
135 message: "Expected '{{syntaxA}}' syntax before '{{syntaxB}}' syntax.",
137 syntaxA: memberSyntaxSortOrder[currentMemberSyntaxGroupIndex],
138 syntaxB: memberSyntaxSortOrder[previousMemberSyntaxGroupIndex]
143 if (previousLocalMemberName &&
144 currentLocalMemberName &&
145 currentLocalMemberName < previousLocalMemberName
149 message: "Imports should be sorted alphabetically."
155 previousDeclaration = node;
158 if (!ignoreMemberSort) {
159 const importSpecifiers = node.specifiers.filter(specifier => specifier.type === "ImportSpecifier");
160 const getSortableName = ignoreCase ? specifier => specifier.local.name.toLowerCase() : specifier => specifier.local.name;
161 const firstUnsortedIndex = importSpecifiers.map(getSortableName).findIndex((name, index, array) => array[index - 1] > name);
163 if (firstUnsortedIndex !== -1) {
165 node: importSpecifiers[firstUnsortedIndex],
166 message: "Member '{{memberName}}' of the import declaration should be sorted alphabetically.",
167 data: { memberName: importSpecifiers[firstUnsortedIndex].local.name },
169 if (importSpecifiers.some(specifier =>
170 sourceCode.getCommentsBefore(specifier).length || sourceCode.getCommentsAfter(specifier).length)) {
172 // If there are comments in the ImportSpecifier list, don't rearrange the specifiers.
176 return fixer.replaceTextRange(
177 [importSpecifiers[0].range[0], importSpecifiers[importSpecifiers.length - 1].range[1]],
180 // Clone the importSpecifiers array to avoid mutating it
183 // Sort the array into the desired order
184 .sort((specifierA, specifierB) => {
185 const aName = getSortableName(specifierA);
186 const bName = getSortableName(specifierB);
188 return aName > bName ? 1 : -1;
191 // Build a string out of the sorted list of import specifiers and the text between the originals
192 .reduce((sourceText, specifier, index) => {
193 const textAfterSpecifier = index === importSpecifiers.length - 1
195 : sourceCode.getText().slice(importSpecifiers[index].range[1], importSpecifiers[index + 1].range[0]);
197 return sourceText + sourceCode.getText(specifier) + textAfterSpecifier;