UNPKG

8.75 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 docs: {
15 description: "enforce sorted import declarations within modules",
16 category: "ECMAScript 6",
17 recommended: false,
18 url: "https://eslint.org/docs/rules/sort-imports"
19 },
20
21 schema: [
22 {
23 type: "object",
24 properties: {
25 ignoreCase: {
26 type: "boolean"
27 },
28 memberSyntaxSortOrder: {
29 type: "array",
30 items: {
31 enum: ["none", "all", "multiple", "single"]
32 },
33 uniqueItems: true,
34 minItems: 4,
35 maxItems: 4
36 },
37 ignoreMemberSort: {
38 type: "boolean"
39 }
40 },
41 additionalProperties: false
42 }
43 ],
44
45 fixable: "code"
46 },
47
48 create(context) {
49
50 const configuration = context.options[0] || {},
51 ignoreCase = configuration.ignoreCase || false,
52 ignoreMemberSort = configuration.ignoreMemberSort || false,
53 memberSyntaxSortOrder = configuration.memberSyntaxSortOrder || ["none", "all", "multiple", "single"],
54 sourceCode = context.getSourceCode();
55 let previousDeclaration = null;
56
57 /**
58 * Gets the used member syntax style.
59 *
60 * import "my-module.js" --> none
61 * import * as myModule from "my-module.js" --> all
62 * import {myMember} from "my-module.js" --> single
63 * import {foo, bar} from "my-module.js" --> multiple
64 *
65 * @param {ASTNode} node - the ImportDeclaration node.
66 * @returns {string} used member parameter style, ["all", "multiple", "single"]
67 */
68 function usedMemberSyntax(node) {
69 if (node.specifiers.length === 0) {
70 return "none";
71 }
72 if (node.specifiers[0].type === "ImportNamespaceSpecifier") {
73 return "all";
74 }
75 if (node.specifiers.length === 1) {
76 return "single";
77 }
78 return "multiple";
79
80 }
81
82 /**
83 * Gets the group by member parameter index for given declaration.
84 * @param {ASTNode} node - the ImportDeclaration node.
85 * @returns {number} the declaration group by member index.
86 */
87 function getMemberParameterGroupIndex(node) {
88 return memberSyntaxSortOrder.indexOf(usedMemberSyntax(node));
89 }
90
91 /**
92 * Gets the local name of the first imported module.
93 * @param {ASTNode} node - the ImportDeclaration node.
94 * @returns {?string} the local name of the first imported module.
95 */
96 function getFirstLocalMemberName(node) {
97 if (node.specifiers[0]) {
98 return node.specifiers[0].local.name;
99 }
100 return null;
101
102 }
103
104 return {
105 ImportDeclaration(node) {
106 if (previousDeclaration) {
107 const currentMemberSyntaxGroupIndex = getMemberParameterGroupIndex(node),
108 previousMemberSyntaxGroupIndex = getMemberParameterGroupIndex(previousDeclaration);
109 let currentLocalMemberName = getFirstLocalMemberName(node),
110 previousLocalMemberName = getFirstLocalMemberName(previousDeclaration);
111
112 if (ignoreCase) {
113 previousLocalMemberName = previousLocalMemberName && previousLocalMemberName.toLowerCase();
114 currentLocalMemberName = currentLocalMemberName && currentLocalMemberName.toLowerCase();
115 }
116
117 /*
118 * When the current declaration uses a different member syntax,
119 * then check if the ordering is correct.
120 * Otherwise, make a default string compare (like rule sort-vars to be consistent) of the first used local member name.
121 */
122 if (currentMemberSyntaxGroupIndex !== previousMemberSyntaxGroupIndex) {
123 if (currentMemberSyntaxGroupIndex < previousMemberSyntaxGroupIndex) {
124 context.report({
125 node,
126 message: "Expected '{{syntaxA}}' syntax before '{{syntaxB}}' syntax.",
127 data: {
128 syntaxA: memberSyntaxSortOrder[currentMemberSyntaxGroupIndex],
129 syntaxB: memberSyntaxSortOrder[previousMemberSyntaxGroupIndex]
130 }
131 });
132 }
133 } else {
134 if (previousLocalMemberName &&
135 currentLocalMemberName &&
136 currentLocalMemberName < previousLocalMemberName
137 ) {
138 context.report({
139 node,
140 message: "Imports should be sorted alphabetically."
141 });
142 }
143 }
144 }
145
146 if (!ignoreMemberSort) {
147 const importSpecifiers = node.specifiers.filter(specifier => specifier.type === "ImportSpecifier");
148 const getSortableName = ignoreCase ? specifier => specifier.local.name.toLowerCase() : specifier => specifier.local.name;
149 const firstUnsortedIndex = importSpecifiers.map(getSortableName).findIndex((name, index, array) => array[index - 1] > name);
150
151 if (firstUnsortedIndex !== -1) {
152 context.report({
153 node: importSpecifiers[firstUnsortedIndex],
154 message: "Member '{{memberName}}' of the import declaration should be sorted alphabetically.",
155 data: { memberName: importSpecifiers[firstUnsortedIndex].local.name },
156 fix(fixer) {
157 if (importSpecifiers.some(specifier =>
158 sourceCode.getCommentsBefore(specifier).length || sourceCode.getCommentsAfter(specifier).length)) {
159
160 // If there are comments in the ImportSpecifier list, don't rearrange the specifiers.
161 return null;
162 }
163
164 return fixer.replaceTextRange(
165 [importSpecifiers[0].range[0], importSpecifiers[importSpecifiers.length - 1].range[1]],
166 importSpecifiers
167
168 // Clone the importSpecifiers array to avoid mutating it
169 .slice()
170
171 // Sort the array into the desired order
172 .sort((specifierA, specifierB) => {
173 const aName = getSortableName(specifierA);
174 const bName = getSortableName(specifierB);
175
176 return aName > bName ? 1 : -1;
177 })
178
179 // Build a string out of the sorted list of import specifiers and the text between the originals
180 .reduce((sourceText, specifier, index) => {
181 const textAfterSpecifier = index === importSpecifiers.length - 1
182 ? ""
183 : sourceCode.getText().slice(importSpecifiers[index].range[1], importSpecifiers[index + 1].range[0]);
184
185 return sourceText + sourceCode.getText(specifier) + textAfterSpecifier;
186 }, "")
187 );
188 }
189 });
190 }
191 }
192
193 previousDeclaration = node;
194 }
195 };
196 }
197};