UNPKG

10.9 kBJavaScriptView Raw
1/**
2 * @fileoverview Rule to require sorting of import declarations
3 * @author Christian Schuller
4 */
5
6"use strict";
7
8//------------------------------------------------------------------------------
9// Rule Definition
10//------------------------------------------------------------------------------
11
12module.exports = {
13 meta: {
14 type: "suggestion",
15
16 docs: {
17 description: "enforce sorted import declarations within modules",
18 category: "ECMAScript 6",
19 recommended: false,
20 url: "https://eslint.org/docs/rules/sort-imports"
21 },
22
23 schema: [
24 {
25 type: "object",
26 properties: {
27 ignoreCase: {
28 type: "boolean",
29 default: false
30 },
31 memberSyntaxSortOrder: {
32 type: "array",
33 items: {
34 enum: ["none", "all", "multiple", "single"]
35 },
36 uniqueItems: true,
37 minItems: 4,
38 maxItems: 4
39 },
40 ignoreDeclarationSort: {
41 type: "boolean",
42 default: false
43 },
44 ignoreMemberSort: {
45 type: "boolean",
46 default: false
47 },
48 allowSeparatedGroups: {
49 type: "boolean",
50 default: false
51 }
52 },
53 additionalProperties: false
54 }
55 ],
56
57 fixable: "code",
58
59 messages: {
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."
63 }
64 },
65
66 create(context) {
67
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;
76
77 /**
78 * Gets the used member syntax style.
79 *
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"]
86 */
87 function usedMemberSyntax(node) {
88 if (node.specifiers.length === 0) {
89 return "none";
90 }
91 if (node.specifiers[0].type === "ImportNamespaceSpecifier") {
92 return "all";
93 }
94 if (node.specifiers.length === 1) {
95 return "single";
96 }
97 return "multiple";
98
99 }
100
101 /**
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.
105 */
106 function getMemberParameterGroupIndex(node) {
107 return memberSyntaxSortOrder.indexOf(usedMemberSyntax(node));
108 }
109
110 /**
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.
114 */
115 function getFirstLocalMemberName(node) {
116 if (node.specifiers[0]) {
117 return node.specifiers[0].local.name;
118 }
119 return null;
120
121 }
122
123 /**
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.
131 */
132 function getNumberOfLinesBetween(left, right) {
133 return Math.max(right.loc.start.line - left.loc.end.line - 1, 0);
134 }
135
136 return {
137 ImportDeclaration(node) {
138 if (!ignoreDeclarationSort) {
139 if (
140 previousDeclaration &&
141 allowSeparatedGroups &&
142 getNumberOfLinesBetween(previousDeclaration, node) > 0
143 ) {
144
145 // reset declaration sort
146 previousDeclaration = null;
147 }
148
149 if (previousDeclaration) {
150 const currentMemberSyntaxGroupIndex = getMemberParameterGroupIndex(node),
151 previousMemberSyntaxGroupIndex = getMemberParameterGroupIndex(previousDeclaration);
152 let currentLocalMemberName = getFirstLocalMemberName(node),
153 previousLocalMemberName = getFirstLocalMemberName(previousDeclaration);
154
155 if (ignoreCase) {
156 previousLocalMemberName = previousLocalMemberName && previousLocalMemberName.toLowerCase();
157 currentLocalMemberName = currentLocalMemberName && currentLocalMemberName.toLowerCase();
158 }
159
160 /*
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.
164 */
165 if (currentMemberSyntaxGroupIndex !== previousMemberSyntaxGroupIndex) {
166 if (currentMemberSyntaxGroupIndex < previousMemberSyntaxGroupIndex) {
167 context.report({
168 node,
169 messageId: "unexpectedSyntaxOrder",
170 data: {
171 syntaxA: memberSyntaxSortOrder[currentMemberSyntaxGroupIndex],
172 syntaxB: memberSyntaxSortOrder[previousMemberSyntaxGroupIndex]
173 }
174 });
175 }
176 } else {
177 if (previousLocalMemberName &&
178 currentLocalMemberName &&
179 currentLocalMemberName < previousLocalMemberName
180 ) {
181 context.report({
182 node,
183 messageId: "sortImportsAlphabetically"
184 });
185 }
186 }
187 }
188
189 previousDeclaration = node;
190 }
191
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);
196
197 if (firstUnsortedIndex !== -1) {
198 context.report({
199 node: importSpecifiers[firstUnsortedIndex],
200 messageId: "sortMembersAlphabetically",
201 data: { memberName: importSpecifiers[firstUnsortedIndex].local.name },
202 fix(fixer) {
203 if (importSpecifiers.some(specifier =>
204 sourceCode.getCommentsBefore(specifier).length || sourceCode.getCommentsAfter(specifier).length)) {
205
206 // If there are comments in the ImportSpecifier list, don't rearrange the specifiers.
207 return null;
208 }
209
210 return fixer.replaceTextRange(
211 [importSpecifiers[0].range[0], importSpecifiers[importSpecifiers.length - 1].range[1]],
212 importSpecifiers
213
214 // Clone the importSpecifiers array to avoid mutating it
215 .slice()
216
217 // Sort the array into the desired order
218 .sort((specifierA, specifierB) => {
219 const aName = getSortableName(specifierA);
220 const bName = getSortableName(specifierB);
221
222 return aName > bName ? 1 : -1;
223 })
224
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
228 ? ""
229 : sourceCode.getText().slice(importSpecifiers[index].range[1], importSpecifiers[index + 1].range[0]);
230
231 return sourceText + sourceCode.getText(specifier) + textAfterSpecifier;
232 }, "")
233 );
234 }
235 });
236 }
237 }
238 }
239 };
240 }
241};