2 * @fileoverview Rule to require sorting of import declarations
3 * @author Christian Schuller
8 //------------------------------------------------------------------------------
10 //------------------------------------------------------------------------------
17 description
: "enforce sorted import declarations within modules",
19 url
: "https://eslint.org/docs/rules/sort-imports"
30 memberSyntaxSortOrder
: {
33 enum: ["none", "all", "multiple", "single"]
39 ignoreDeclarationSort
: {
47 allowSeparatedGroups
: {
52 additionalProperties
: false
59 sortImportsAlphabetically
: "Imports should be sorted alphabetically.",
60 sortMembersAlphabetically
: "Member '{{memberName}}' of the import declaration should be sorted alphabetically.",
61 unexpectedSyntaxOrder
: "Expected '{{syntaxA}}' syntax before '{{syntaxB}}' syntax."
67 const configuration
= context
.options
[0] || {},
68 ignoreCase
= configuration
.ignoreCase
|| false,
69 ignoreDeclarationSort
= configuration
.ignoreDeclarationSort
|| false,
70 ignoreMemberSort
= configuration
.ignoreMemberSort
|| false,
71 memberSyntaxSortOrder
= configuration
.memberSyntaxSortOrder
|| ["none", "all", "multiple", "single"],
72 allowSeparatedGroups
= configuration
.allowSeparatedGroups
|| false,
73 sourceCode
= context
.getSourceCode();
74 let previousDeclaration
= null;
77 * Gets the used member syntax style.
79 * import "my-module.js" --> none
80 * import * as myModule from "my-module.js" --> all
81 * import {myMember} from "my-module.js" --> single
82 * import {foo, bar} from "my-module.js" --> multiple
83 * @param {ASTNode} node the ImportDeclaration node.
84 * @returns {string} used member parameter style, ["all", "multiple", "single"]
86 function usedMemberSyntax(node
) {
87 if (node
.specifiers
.length
=== 0) {
90 if (node
.specifiers
[0].type
=== "ImportNamespaceSpecifier") {
93 if (node
.specifiers
.length
=== 1) {
101 * Gets the group by member parameter index for given declaration.
102 * @param {ASTNode} node the ImportDeclaration node.
103 * @returns {number} the declaration group by member index.
105 function getMemberParameterGroupIndex(node
) {
106 return memberSyntaxSortOrder
.indexOf(usedMemberSyntax(node
));
110 * Gets the local name of the first imported module.
111 * @param {ASTNode} node the ImportDeclaration node.
112 * @returns {?string} the local name of the first imported module.
114 function getFirstLocalMemberName(node
) {
115 if (node
.specifiers
[0]) {
116 return node
.specifiers
[0].local
.name
;
123 * Calculates number of lines between two nodes. It is assumed that the given `left` node appears before
124 * the given `right` node in the source code. Lines are counted from the end of the `left` node till the
125 * start of the `right` node. If the given nodes are on the same line, it returns `0`, same as if they were
126 * on two consecutive lines.
127 * @param {ASTNode} left node that appears before the given `right` node.
128 * @param {ASTNode} right node that appears after the given `left` node.
129 * @returns {number} number of lines between nodes.
131 function getNumberOfLinesBetween(left
, right
) {
132 return Math
.max(right
.loc
.start
.line
- left
.loc
.end
.line
- 1, 0);
136 ImportDeclaration(node
) {
137 if (!ignoreDeclarationSort
) {
139 previousDeclaration
&&
140 allowSeparatedGroups
&&
141 getNumberOfLinesBetween(previousDeclaration
, node
) > 0
144 // reset declaration sort
145 previousDeclaration
= null;
148 if (previousDeclaration
) {
149 const currentMemberSyntaxGroupIndex
= getMemberParameterGroupIndex(node
),
150 previousMemberSyntaxGroupIndex
= getMemberParameterGroupIndex(previousDeclaration
);
151 let currentLocalMemberName
= getFirstLocalMemberName(node
),
152 previousLocalMemberName
= getFirstLocalMemberName(previousDeclaration
);
155 previousLocalMemberName
= previousLocalMemberName
&& previousLocalMemberName
.toLowerCase();
156 currentLocalMemberName
= currentLocalMemberName
&& currentLocalMemberName
.toLowerCase();
160 * When the current declaration uses a different member syntax,
161 * then check if the ordering is correct.
162 * Otherwise, make a default string compare (like rule sort-vars to be consistent) of the first used local member name.
164 if (currentMemberSyntaxGroupIndex
!== previousMemberSyntaxGroupIndex
) {
165 if (currentMemberSyntaxGroupIndex
< previousMemberSyntaxGroupIndex
) {
168 messageId
: "unexpectedSyntaxOrder",
170 syntaxA
: memberSyntaxSortOrder
[currentMemberSyntaxGroupIndex
],
171 syntaxB
: memberSyntaxSortOrder
[previousMemberSyntaxGroupIndex
]
176 if (previousLocalMemberName
&&
177 currentLocalMemberName
&&
178 currentLocalMemberName
< previousLocalMemberName
182 messageId
: "sortImportsAlphabetically"
188 previousDeclaration
= node
;
191 if (!ignoreMemberSort
) {
192 const importSpecifiers
= node
.specifiers
.filter(specifier
=> specifier
.type
=== "ImportSpecifier");
193 const getSortableName
= ignoreCase
? specifier
=> specifier
.local
.name
.toLowerCase() : specifier
=> specifier
.local
.name
;
194 const firstUnsortedIndex
= importSpecifiers
.map(getSortableName
).findIndex((name
, index
, array
) => array
[index
- 1] > name
);
196 if (firstUnsortedIndex
!== -1) {
198 node
: importSpecifiers
[firstUnsortedIndex
],
199 messageId
: "sortMembersAlphabetically",
200 data
: { memberName
: importSpecifiers
[firstUnsortedIndex
].local
.name
},
202 if (importSpecifiers
.some(specifier
=>
203 sourceCode
.getCommentsBefore(specifier
).length
|| sourceCode
.getCommentsAfter(specifier
).length
)) {
205 // If there are comments in the ImportSpecifier list, don't rearrange the specifiers.
209 return fixer
.replaceTextRange(
210 [importSpecifiers
[0].range
[0], importSpecifiers
[importSpecifiers
.length
- 1].range
[1]],
213 // Clone the importSpecifiers array to avoid mutating it
216 // Sort the array into the desired order
217 .sort((specifierA
, specifierB
) => {
218 const aName
= getSortableName(specifierA
);
219 const bName
= getSortableName(specifierB
);
221 return aName
> bName
? 1 : -1;
224 // Build a string out of the sorted list of import specifiers and the text between the originals
225 .reduce((sourceText
, specifier
, index
) => {
226 const textAfterSpecifier
= index
=== importSpecifiers
.length
- 1
228 : sourceCode
.getText().slice(importSpecifiers
[index
].range
[1], importSpecifiers
[index
+ 1].range
[0]);
230 return sourceText
+ sourceCode
.getText(specifier
) + textAfterSpecifier
;