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: {
48 allowSeparatedGroups: {
53 additionalProperties: false
60 sortImportsAlphabetically: "Imports should be sorted alphabetically.",
61 sortMembersAlphabetically: "Member '{{memberName}}' of the import declaration should be sorted alphabetically.",
62 unexpectedSyntaxOrder: "Expected '{{syntaxA}}' syntax before '{{syntaxB}}' syntax."
68 const configuration = context.options[0] || {},
69 ignoreCase = configuration.ignoreCase || false,
70 ignoreDeclarationSort = configuration.ignoreDeclarationSort || false,
71 ignoreMemberSort = configuration.ignoreMemberSort || false,
72 memberSyntaxSortOrder = configuration.memberSyntaxSortOrder || ["none", "all", "multiple", "single"],
73 allowSeparatedGroups = configuration.allowSeparatedGroups || false,
74 sourceCode = context.getSourceCode();
75 let previousDeclaration = null;
78 * Gets the used member syntax style.
80 * import "my-module.js" --> none
81 * import * as myModule from "my-module.js" --> all
82 * import {myMember} from "my-module.js" --> single
83 * import {foo, bar} from "my-module.js" --> multiple
84 * @param {ASTNode} node the ImportDeclaration node.
85 * @returns {string} used member parameter style, ["all", "multiple", "single"]
87 function usedMemberSyntax(node) {
88 if (node.specifiers.length === 0) {
91 if (node.specifiers[0].type === "ImportNamespaceSpecifier") {
94 if (node.specifiers.length === 1) {
102 * Gets the group by member parameter index for given declaration.
103 * @param {ASTNode} node the ImportDeclaration node.
104 * @returns {number} the declaration group by member index.
106 function getMemberParameterGroupIndex(node) {
107 return memberSyntaxSortOrder.indexOf(usedMemberSyntax(node));
111 * Gets the local name of the first imported module.
112 * @param {ASTNode} node the ImportDeclaration node.
113 * @returns {?string} the local name of the first imported module.
115 function getFirstLocalMemberName(node) {
116 if (node.specifiers[0]) {
117 return node.specifiers[0].local.name;
124 * Calculates number of lines between two nodes. It is assumed that the given `left` node appears before
125 * the given `right` node in the source code. Lines are counted from the end of the `left` node till the
126 * start of the `right` node. If the given nodes are on the same line, it returns `0`, same as if they were
127 * on two consecutive lines.
128 * @param {ASTNode} left node that appears before the given `right` node.
129 * @param {ASTNode} right node that appears after the given `left` node.
130 * @returns {number} number of lines between nodes.
132 function getNumberOfLinesBetween(left, right) {
133 return Math.max(right.loc.start.line - left.loc.end.line - 1, 0);
137 ImportDeclaration(node) {
138 if (!ignoreDeclarationSort) {
140 previousDeclaration &&
141 allowSeparatedGroups &&
142 getNumberOfLinesBetween(previousDeclaration, node) > 0
145 // reset declaration sort
146 previousDeclaration = null;
149 if (previousDeclaration) {
150 const currentMemberSyntaxGroupIndex = getMemberParameterGroupIndex(node),
151 previousMemberSyntaxGroupIndex = getMemberParameterGroupIndex(previousDeclaration);
152 let currentLocalMemberName = getFirstLocalMemberName(node),
153 previousLocalMemberName = getFirstLocalMemberName(previousDeclaration);
156 previousLocalMemberName = previousLocalMemberName && previousLocalMemberName.toLowerCase();
157 currentLocalMemberName = currentLocalMemberName && currentLocalMemberName.toLowerCase();
161 * When the current declaration uses a different member syntax,
162 * then check if the ordering is correct.
163 * Otherwise, make a default string compare (like rule sort-vars to be consistent) of the first used local member name.
165 if (currentMemberSyntaxGroupIndex !== previousMemberSyntaxGroupIndex) {
166 if (currentMemberSyntaxGroupIndex < previousMemberSyntaxGroupIndex) {
169 messageId: "unexpectedSyntaxOrder",
171 syntaxA: memberSyntaxSortOrder[currentMemberSyntaxGroupIndex],
172 syntaxB: memberSyntaxSortOrder[previousMemberSyntaxGroupIndex]
177 if (previousLocalMemberName &&
178 currentLocalMemberName &&
179 currentLocalMemberName < previousLocalMemberName
183 messageId: "sortImportsAlphabetically"
189 previousDeclaration = node;
192 if (!ignoreMemberSort) {
193 const importSpecifiers = node.specifiers.filter(specifier => specifier.type === "ImportSpecifier");
194 const getSortableName = ignoreCase ? specifier => specifier.local.name.toLowerCase() : specifier => specifier.local.name;
195 const firstUnsortedIndex = importSpecifiers.map(getSortableName).findIndex((name, index, array) => array[index - 1] > name);
197 if (firstUnsortedIndex !== -1) {
199 node: importSpecifiers[firstUnsortedIndex],
200 messageId: "sortMembersAlphabetically",
201 data: { memberName: importSpecifiers[firstUnsortedIndex].local.name },
203 if (importSpecifiers.some(specifier =>
204 sourceCode.getCommentsBefore(specifier).length || sourceCode.getCommentsAfter(specifier).length)) {
206 // If there are comments in the ImportSpecifier list, don't rearrange the specifiers.
210 return fixer.replaceTextRange(
211 [importSpecifiers[0].range[0], importSpecifiers[importSpecifiers.length - 1].range[1]],
214 // Clone the importSpecifiers array to avoid mutating it
217 // Sort the array into the desired order
218 .sort((specifierA, specifierB) => {
219 const aName = getSortableName(specifierA);
220 const bName = getSortableName(specifierB);
222 return aName > bName ? 1 : -1;
225 // Build a string out of the sorted list of import specifiers and the text between the originals
226 .reduce((sourceText, specifier, index) => {
227 const textAfterSpecifier = index === importSpecifiers.length - 1
229 : sourceCode.getText().slice(importSpecifiers[index].range[1], importSpecifiers[index + 1].range[0]);
231 return sourceText + sourceCode.getText(specifier) + textAfterSpecifier;