UNPKG

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