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