UNPKG

9.3 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 },
49 additionalProperties: false
50 }
51 ],
52
53 fixable: "code"
54 },
55
56 create(context) {
57
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;
65
66 /**
67 * Gets the used member syntax style.
68 *
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 *
74 * @param {ASTNode} node - the ImportDeclaration node.
75 * @returns {string} used member parameter style, ["all", "multiple", "single"]
76 */
77 function usedMemberSyntax(node) {
78 if (node.specifiers.length === 0) {
79 return "none";
80 }
81 if (node.specifiers[0].type === "ImportNamespaceSpecifier") {
82 return "all";
83 }
84 if (node.specifiers.length === 1) {
85 return "single";
86 }
87 return "multiple";
88
89 }
90
91 /**
92 * Gets the group by member parameter index for given declaration.
93 * @param {ASTNode} node - the ImportDeclaration node.
94 * @returns {number} the declaration group by member index.
95 */
96 function getMemberParameterGroupIndex(node) {
97 return memberSyntaxSortOrder.indexOf(usedMemberSyntax(node));
98 }
99
100 /**
101 * Gets the local name of the first imported module.
102 * @param {ASTNode} node - the ImportDeclaration node.
103 * @returns {?string} the local name of the first imported module.
104 */
105 function getFirstLocalMemberName(node) {
106 if (node.specifiers[0]) {
107 return node.specifiers[0].local.name;
108 }
109 return null;
110
111 }
112
113 return {
114 ImportDeclaration(node) {
115 if (!ignoreDeclarationSort) {
116 if (previousDeclaration) {
117 const currentMemberSyntaxGroupIndex = getMemberParameterGroupIndex(node),
118 previousMemberSyntaxGroupIndex = getMemberParameterGroupIndex(previousDeclaration);
119 let currentLocalMemberName = getFirstLocalMemberName(node),
120 previousLocalMemberName = getFirstLocalMemberName(previousDeclaration);
121
122 if (ignoreCase) {
123 previousLocalMemberName = previousLocalMemberName && previousLocalMemberName.toLowerCase();
124 currentLocalMemberName = currentLocalMemberName && currentLocalMemberName.toLowerCase();
125 }
126
127 /*
128 * When the current declaration uses a different member syntax,
129 * then check if the ordering is correct.
130 * Otherwise, make a default string compare (like rule sort-vars to be consistent) of the first used local member name.
131 */
132 if (currentMemberSyntaxGroupIndex !== previousMemberSyntaxGroupIndex) {
133 if (currentMemberSyntaxGroupIndex < previousMemberSyntaxGroupIndex) {
134 context.report({
135 node,
136 message: "Expected '{{syntaxA}}' syntax before '{{syntaxB}}' syntax.",
137 data: {
138 syntaxA: memberSyntaxSortOrder[currentMemberSyntaxGroupIndex],
139 syntaxB: memberSyntaxSortOrder[previousMemberSyntaxGroupIndex]
140 }
141 });
142 }
143 } else {
144 if (previousLocalMemberName &&
145 currentLocalMemberName &&
146 currentLocalMemberName < previousLocalMemberName
147 ) {
148 context.report({
149 node,
150 message: "Imports should be sorted alphabetically."
151 });
152 }
153 }
154 }
155
156 previousDeclaration = node;
157 }
158
159 if (!ignoreMemberSort) {
160 const importSpecifiers = node.specifiers.filter(specifier => specifier.type === "ImportSpecifier");
161 const getSortableName = ignoreCase ? specifier => specifier.local.name.toLowerCase() : specifier => specifier.local.name;
162 const firstUnsortedIndex = importSpecifiers.map(getSortableName).findIndex((name, index, array) => array[index - 1] > name);
163
164 if (firstUnsortedIndex !== -1) {
165 context.report({
166 node: importSpecifiers[firstUnsortedIndex],
167 message: "Member '{{memberName}}' of the import declaration should be sorted alphabetically.",
168 data: { memberName: importSpecifiers[firstUnsortedIndex].local.name },
169 fix(fixer) {
170 if (importSpecifiers.some(specifier =>
171 sourceCode.getCommentsBefore(specifier).length || sourceCode.getCommentsAfter(specifier).length)) {
172
173 // If there are comments in the ImportSpecifier list, don't rearrange the specifiers.
174 return null;
175 }
176
177 return fixer.replaceTextRange(
178 [importSpecifiers[0].range[0], importSpecifiers[importSpecifiers.length - 1].range[1]],
179 importSpecifiers
180
181 // Clone the importSpecifiers array to avoid mutating it
182 .slice()
183
184 // Sort the array into the desired order
185 .sort((specifierA, specifierB) => {
186 const aName = getSortableName(specifierA);
187 const bName = getSortableName(specifierB);
188
189 return aName > bName ? 1 : -1;
190 })
191
192 // Build a string out of the sorted list of import specifiers and the text between the originals
193 .reduce((sourceText, specifier, index) => {
194 const textAfterSpecifier = index === importSpecifiers.length - 1
195 ? ""
196 : sourceCode.getText().slice(importSpecifiers[index].range[1], importSpecifiers[index + 1].range[0]);
197
198 return sourceText + sourceCode.getText(specifier) + textAfterSpecifier;
199 }, "")
200 );
201 }
202 });
203 }
204 }
205 }
206 };
207 }
208};