@borela-tech/eslint-config 1.3.3 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/lint +2 -1
- package/bin/test +12 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +321 -18
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/index.ts +21 -16
- package/src/rules/__tests__/dedent/countLeadingSpaces.ts +4 -0
- package/src/rules/__tests__/dedent/findMinIndent.ts +7 -0
- package/src/rules/__tests__/dedent/index.ts +17 -0
- package/src/rules/__tests__/dedent/interpolate.ts +11 -0
- package/src/rules/__tests__/dedent/removeEmptyPrefix.ts +6 -0
- package/src/rules/__tests__/dedent/removeEmptySuffix.ts +6 -0
- package/src/rules/__tests__/dedent/removeIndent.ts +3 -0
- package/src/rules/__tests__/individualImports.test.ts +42 -0
- package/src/rules/__tests__/sortedImports.test.ts +114 -0
- package/src/rules/individualImports.ts +3 -3
- package/src/rules/sortedImports/CategorizedImport.ts +8 -0
- package/src/rules/sortedImports/ImportError.ts +6 -0
- package/src/rules/sortedImports/ImportGroup.ts +1 -0
- package/src/rules/sortedImports/areSpecifiersSorted.ts +10 -0
- package/src/rules/sortedImports/categorizeImport.ts +12 -0
- package/src/rules/sortedImports/categorizeImports.ts +12 -0
- package/src/rules/sortedImports/checkAlphabeticalSorting.ts +22 -0
- package/src/rules/sortedImports/checkGroupOrdering.ts +22 -0
- package/src/rules/sortedImports/checkSpecifiersSorting.ts +21 -0
- package/src/rules/sortedImports/createFix/ReplacementRange.ts +4 -0
- package/src/rules/sortedImports/createFix/buildSortedCode.ts +22 -0
- package/src/rules/sortedImports/createFix/findLastImportIndex.ts +12 -0
- package/src/rules/sortedImports/createFix/formatNamedImport.ts +23 -0
- package/src/rules/sortedImports/createFix/getReplacementRange.ts +26 -0
- package/src/rules/sortedImports/createFix/groupImportsByType.ts +17 -0
- package/src/rules/sortedImports/createFix/index.ts +29 -0
- package/src/rules/sortedImports/createFix/sortImportGroups.ts +9 -0
- package/src/rules/sortedImports/getImportDeclarations.ts +9 -0
- package/src/rules/sortedImports/getNamedSpecifiers.ts +8 -0
- package/src/rules/sortedImports/getSortKey.ts +20 -0
- package/src/rules/sortedImports/getSpecifierName.ts +7 -0
- package/src/rules/sortedImports/index.ts +52 -0
- package/src/rules/sortedImports/sortSpecifiersText.ts +14 -0
- package/src/rules/sortedImports.ts +0 -83
package/bin/lint
CHANGED
package/bin/test
ADDED
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import * as
|
|
1
|
+
import * as eslint_config from 'eslint/config';
|
|
2
2
|
|
|
3
|
-
declare const CONFIG:
|
|
3
|
+
declare const CONFIG: eslint_config.Config[];
|
|
4
4
|
|
|
5
5
|
export { CONFIG };
|
package/dist/index.js
CHANGED
|
@@ -4,13 +4,316 @@ import react from "eslint-plugin-react";
|
|
|
4
4
|
import reactHooks from "eslint-plugin-react-hooks";
|
|
5
5
|
import stylistic from "@stylistic/eslint-plugin";
|
|
6
6
|
import typescript from "typescript-eslint";
|
|
7
|
-
|
|
7
|
+
import { defineConfig } from "eslint/config";
|
|
8
|
+
|
|
9
|
+
// src/rules/individualImports.ts
|
|
10
|
+
var individualImports = {
|
|
11
|
+
meta: {
|
|
12
|
+
docs: {
|
|
13
|
+
description: "Enforce individual imports instead of grouped imports",
|
|
14
|
+
recommended: true
|
|
15
|
+
},
|
|
16
|
+
fixable: "code",
|
|
17
|
+
messages: {
|
|
18
|
+
individualImports: "Use individual imports instead of grouped imports."
|
|
19
|
+
},
|
|
20
|
+
schema: [],
|
|
21
|
+
type: "suggestion"
|
|
22
|
+
},
|
|
23
|
+
create(context) {
|
|
24
|
+
return {
|
|
25
|
+
ImportDeclaration(node) {
|
|
26
|
+
if (node.specifiers.length <= 1)
|
|
27
|
+
return;
|
|
28
|
+
context.report({
|
|
29
|
+
node,
|
|
30
|
+
messageId: "individualImports",
|
|
31
|
+
fix(fixer) {
|
|
32
|
+
const source = node.source.raw;
|
|
33
|
+
const specifiers = node.specifiers.map((importSpecifier) => {
|
|
34
|
+
if (importSpecifier.type === "ImportSpecifier")
|
|
35
|
+
return `import {${importSpecifier.local.name}} from ${source}`;
|
|
36
|
+
return null;
|
|
37
|
+
}).filter(Boolean);
|
|
38
|
+
if (specifiers.length !== node.specifiers.length)
|
|
39
|
+
return null;
|
|
40
|
+
return fixer.replaceText(
|
|
41
|
+
node,
|
|
42
|
+
specifiers.join("\n")
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// src/rules/sortedImports/categorizeImport.ts
|
|
52
|
+
function categorizeImport(declaration) {
|
|
53
|
+
if (declaration.specifiers.length === 0)
|
|
54
|
+
return "side-effect";
|
|
55
|
+
if (declaration.specifiers.some((s) => s.type === "ImportDefaultSpecifier"))
|
|
56
|
+
return "default";
|
|
57
|
+
return "named";
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// src/rules/sortedImports/getSortKey.ts
|
|
61
|
+
function getSortKey(declaration) {
|
|
62
|
+
const group = categorizeImport(declaration);
|
|
63
|
+
if (group === "side-effect")
|
|
64
|
+
return declaration.source.value.toLowerCase();
|
|
65
|
+
if (group === "default") {
|
|
66
|
+
const defaultSpecifier = declaration.specifiers.find(
|
|
67
|
+
(s) => s.type === "ImportDefaultSpecifier"
|
|
68
|
+
);
|
|
69
|
+
return defaultSpecifier?.local.name.toLowerCase() ?? "";
|
|
70
|
+
}
|
|
71
|
+
return "";
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// src/rules/sortedImports/categorizeImports.ts
|
|
75
|
+
function categorizeImports(declarations) {
|
|
76
|
+
return declarations.map((declaration) => ({
|
|
77
|
+
declaration,
|
|
78
|
+
group: categorizeImport(declaration),
|
|
79
|
+
sortKey: getSortKey(declaration)
|
|
80
|
+
}));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// src/rules/sortedImports/checkAlphabeticalSorting.ts
|
|
84
|
+
function checkAlphabeticalSorting(categorized) {
|
|
85
|
+
const errors = [];
|
|
86
|
+
for (const group of ["side-effect", "default"]) {
|
|
87
|
+
const groupImports = categorized.filter((c) => c.group === group);
|
|
88
|
+
const sorted = [...groupImports].sort((a, b) => a.sortKey.localeCompare(b.sortKey));
|
|
89
|
+
for (let i = 0; i < groupImports.length; i++) {
|
|
90
|
+
if (groupImports[i] !== sorted[i]) {
|
|
91
|
+
errors.push({
|
|
92
|
+
node: groupImports[i].declaration,
|
|
93
|
+
messageId: "sortedImports"
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return errors;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// src/rules/sortedImports/checkGroupOrdering.ts
|
|
102
|
+
function checkGroupOrdering(categorized) {
|
|
103
|
+
const groupOrder = ["side-effect", "default", "named"];
|
|
104
|
+
const errors = [];
|
|
105
|
+
let currentGroupIndex = -1;
|
|
106
|
+
for (const { declaration, group } of categorized) {
|
|
107
|
+
const groupIndex = groupOrder.indexOf(group);
|
|
108
|
+
if (groupIndex < currentGroupIndex) {
|
|
109
|
+
errors.push({
|
|
110
|
+
node: declaration,
|
|
111
|
+
messageId: "wrongGroup"
|
|
112
|
+
});
|
|
113
|
+
} else
|
|
114
|
+
currentGroupIndex = groupIndex;
|
|
115
|
+
}
|
|
116
|
+
return errors;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// src/rules/sortedImports/getSpecifierName.ts
|
|
120
|
+
function getSpecifierName(specifier) {
|
|
121
|
+
return specifier.imported.type === "Identifier" ? specifier.imported.name : String(specifier.imported.value);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// src/rules/sortedImports/areSpecifiersSorted.ts
|
|
125
|
+
function areSpecifiersSorted(specifiers) {
|
|
126
|
+
const names = specifiers.map((s) => getSpecifierName(s));
|
|
127
|
+
const sorted = [...names].sort(
|
|
128
|
+
(a, b) => a.toLowerCase().localeCompare(b.toLowerCase())
|
|
129
|
+
);
|
|
130
|
+
return names.every((name, i) => name === sorted[i]);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// src/rules/sortedImports/getNamedSpecifiers.ts
|
|
134
|
+
function getNamedSpecifiers(declaration) {
|
|
135
|
+
return declaration.specifiers.filter(
|
|
136
|
+
(s) => s.type === "ImportSpecifier"
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// src/rules/sortedImports/checkSpecifiersSorting.ts
|
|
141
|
+
function checkSpecifiersSorting(categorized) {
|
|
142
|
+
const errors = [];
|
|
143
|
+
const namedImports = categorized.filter((c) => c.group === "named");
|
|
144
|
+
for (const { declaration } of namedImports) {
|
|
145
|
+
const specifiers = getNamedSpecifiers(declaration);
|
|
146
|
+
if (specifiers.length > 1 && !areSpecifiersSorted(specifiers)) {
|
|
147
|
+
errors.push({
|
|
148
|
+
node: declaration,
|
|
149
|
+
messageId: "sortedNames"
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return errors;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// src/rules/sortedImports/sortSpecifiersText.ts
|
|
157
|
+
function sortSpecifiersText(specifiers, sourceCode) {
|
|
158
|
+
const sorted = [...specifiers].sort((a, b) => {
|
|
159
|
+
const lowerA = getSpecifierName(a).toLowerCase();
|
|
160
|
+
const lowerB = getSpecifierName(b).toLowerCase();
|
|
161
|
+
return lowerA.localeCompare(lowerB);
|
|
162
|
+
});
|
|
163
|
+
return sorted.map((s) => sourceCode.getText(s)).join(", ");
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// src/rules/sortedImports/createFix/formatNamedImport.ts
|
|
167
|
+
function formatNamedImport(declaration, sourceCode) {
|
|
168
|
+
const specifiers = getNamedSpecifiers(declaration);
|
|
169
|
+
if (specifiers.length > 1 && !areSpecifiersSorted(specifiers)) {
|
|
170
|
+
const importText = sourceCode.getText(declaration);
|
|
171
|
+
const specifiersStart = importText.indexOf("{");
|
|
172
|
+
const specifiersEnd = importText.lastIndexOf("}");
|
|
173
|
+
const before = importText.substring(0, specifiersStart + 1);
|
|
174
|
+
const after = importText.substring(specifiersEnd);
|
|
175
|
+
const sortedSpecifiers = sortSpecifiersText(specifiers, sourceCode);
|
|
176
|
+
return before + " " + sortedSpecifiers + " " + after;
|
|
177
|
+
}
|
|
178
|
+
return sourceCode.getText(declaration);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// src/rules/sortedImports/createFix/buildSortedCode.ts
|
|
182
|
+
function buildSortedCode(grouped, sourceCode) {
|
|
183
|
+
const groupOrder = ["side-effect", "default", "named"];
|
|
184
|
+
const sortedCode = [];
|
|
185
|
+
for (const group of groupOrder) {
|
|
186
|
+
for (const { declaration } of grouped[group]) {
|
|
187
|
+
if (group === "named")
|
|
188
|
+
sortedCode.push(formatNamedImport(declaration, sourceCode));
|
|
189
|
+
else
|
|
190
|
+
sortedCode.push(sourceCode.getText(declaration));
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return sortedCode;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// src/rules/sortedImports/createFix/findLastImportIndex.ts
|
|
197
|
+
function findLastImportIndex(programBody) {
|
|
198
|
+
let lastIndex = 0;
|
|
199
|
+
for (let i = 0; i < programBody.length; i++) {
|
|
200
|
+
if (programBody[i].type === "ImportDeclaration")
|
|
201
|
+
lastIndex = i;
|
|
202
|
+
else
|
|
203
|
+
break;
|
|
204
|
+
}
|
|
205
|
+
return lastIndex;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// src/rules/sortedImports/createFix/getReplacementRange.ts
|
|
209
|
+
function getReplacementRange(programBody, sourceCode) {
|
|
210
|
+
const fullText = sourceCode.getText();
|
|
211
|
+
const lastIndex = findLastImportIndex(programBody);
|
|
212
|
+
const firstImport = programBody[0];
|
|
213
|
+
const lastImport = programBody[lastIndex];
|
|
214
|
+
const start = firstImport.range[0];
|
|
215
|
+
let end = lastImport.range[1];
|
|
216
|
+
for (let i = end; i < fullText.length; i++) {
|
|
217
|
+
const char = fullText[i];
|
|
218
|
+
if (char === "\n" || char === " " || char === " ")
|
|
219
|
+
end++;
|
|
220
|
+
else
|
|
221
|
+
break;
|
|
222
|
+
}
|
|
223
|
+
return { start, end };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// src/rules/sortedImports/createFix/groupImportsByType.ts
|
|
227
|
+
function groupImportsByType(categorized) {
|
|
228
|
+
const grouped = {
|
|
229
|
+
"side-effect": [],
|
|
230
|
+
default: [],
|
|
231
|
+
named: []
|
|
232
|
+
};
|
|
233
|
+
for (const item of categorized)
|
|
234
|
+
grouped[item.group].push(item);
|
|
235
|
+
return grouped;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// src/rules/sortedImports/createFix/sortImportGroups.ts
|
|
239
|
+
function sortImportGroups(grouped) {
|
|
240
|
+
grouped["side-effect"].sort((a, b) => a.sortKey.localeCompare(b.sortKey));
|
|
241
|
+
grouped["default"].sort((a, b) => a.sortKey.localeCompare(b.sortKey));
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// src/rules/sortedImports/createFix/index.ts
|
|
245
|
+
function createFix(fixer, importDeclarations, sourceCode, programBody) {
|
|
246
|
+
const range = getReplacementRange(programBody, sourceCode);
|
|
247
|
+
const categorized = categorizeImports(importDeclarations);
|
|
248
|
+
const grouped = groupImportsByType(categorized);
|
|
249
|
+
sortImportGroups(grouped);
|
|
250
|
+
const sortedCode = buildSortedCode(grouped, sourceCode).join("\n");
|
|
251
|
+
return fixer.replaceTextRange(
|
|
252
|
+
[range.start, range.end],
|
|
253
|
+
sortedCode
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// src/rules/sortedImports/getImportDeclarations.ts
|
|
258
|
+
function getImportDeclarations(programBody) {
|
|
259
|
+
return programBody.filter(
|
|
260
|
+
(statement) => statement.type === "ImportDeclaration"
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// src/rules/sortedImports/index.ts
|
|
265
|
+
var sortedImports = {
|
|
266
|
+
meta: {
|
|
267
|
+
docs: {
|
|
268
|
+
description: "Enforce sorted imports alphabetically",
|
|
269
|
+
recommended: true
|
|
270
|
+
},
|
|
271
|
+
fixable: "code",
|
|
272
|
+
messages: {
|
|
273
|
+
sortedImports: "Imports should be sorted alphabetically",
|
|
274
|
+
sortedNames: "Named imports should be sorted alphabetically",
|
|
275
|
+
wrongGroup: "Import is in wrong group"
|
|
276
|
+
},
|
|
277
|
+
schema: [],
|
|
278
|
+
type: "suggestion"
|
|
279
|
+
},
|
|
280
|
+
create(context) {
|
|
281
|
+
return {
|
|
282
|
+
Program(node) {
|
|
283
|
+
const declarations = getImportDeclarations(node.body);
|
|
284
|
+
if (declarations.length === 0)
|
|
285
|
+
return;
|
|
286
|
+
const categorized = categorizeImports(declarations);
|
|
287
|
+
const errors = [
|
|
288
|
+
...checkGroupOrdering(categorized),
|
|
289
|
+
...checkAlphabeticalSorting(categorized),
|
|
290
|
+
...checkSpecifiersSorting(categorized)
|
|
291
|
+
];
|
|
292
|
+
for (const error of errors) {
|
|
293
|
+
context.report({
|
|
294
|
+
node: error.node,
|
|
295
|
+
messageId: error.messageId,
|
|
296
|
+
fix(fixer) {
|
|
297
|
+
const sourceCode = context.sourceCode;
|
|
298
|
+
return createFix(fixer, declarations, sourceCode, node.body);
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
// src/index.ts
|
|
308
|
+
var CONFIG = defineConfig([
|
|
8
309
|
{
|
|
9
310
|
ignores: [
|
|
10
311
|
"src/graphql/sdk.ts",
|
|
11
312
|
"**/node_modules/**",
|
|
12
313
|
"**/dist/**"
|
|
13
|
-
]
|
|
314
|
+
]
|
|
315
|
+
},
|
|
316
|
+
{
|
|
14
317
|
settings: {
|
|
15
318
|
react: {
|
|
16
319
|
version: "19"
|
|
@@ -28,6 +331,20 @@ var CONFIG = typescript.config(
|
|
|
28
331
|
},
|
|
29
332
|
rules: reactHooks.configs.recommended.rules
|
|
30
333
|
},
|
|
334
|
+
{
|
|
335
|
+
plugins: {
|
|
336
|
+
"@borela-tech": {
|
|
337
|
+
rules: {
|
|
338
|
+
"individual-imports": individualImports,
|
|
339
|
+
"sorted-imports": sortedImports
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
},
|
|
343
|
+
rules: {
|
|
344
|
+
"@borela-tech/individual-imports": "error",
|
|
345
|
+
"@borela-tech/sorted-imports": "error"
|
|
346
|
+
}
|
|
347
|
+
},
|
|
31
348
|
{
|
|
32
349
|
rules: {
|
|
33
350
|
"capitalized-comments": [
|
|
@@ -102,24 +419,10 @@ var CONFIG = typescript.config(
|
|
|
102
419
|
{ beforeStatementContinuationChars: "always" }
|
|
103
420
|
],
|
|
104
421
|
"@typescript-eslint/no-empty-function": "off",
|
|
105
|
-
"@typescript-eslint/consistent-indexed-object-style": "off"
|
|
106
|
-
"sort-imports": [
|
|
107
|
-
"error",
|
|
108
|
-
{
|
|
109
|
-
allowSeparatedGroups: true,
|
|
110
|
-
ignoreCase: true,
|
|
111
|
-
ignoreMemberSort: false,
|
|
112
|
-
memberSyntaxSortOrder: [
|
|
113
|
-
"none",
|
|
114
|
-
"all",
|
|
115
|
-
"single",
|
|
116
|
-
"multiple"
|
|
117
|
-
]
|
|
118
|
-
}
|
|
119
|
-
]
|
|
422
|
+
"@typescript-eslint/consistent-indexed-object-style": "off"
|
|
120
423
|
}
|
|
121
424
|
}
|
|
122
|
-
);
|
|
425
|
+
]);
|
|
123
426
|
export {
|
|
124
427
|
CONFIG
|
|
125
428
|
};
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["import eslint from '@eslint/js'\nimport react from 'eslint-plugin-react'\nimport reactHooks from 'eslint-plugin-react-hooks'\nimport stylistic from '@stylistic/eslint-plugin'\nimport typescript from 'typescript-eslint'\n\nexport const CONFIG = typescript.config(\n {\n ignores: [\n 'src/graphql/sdk.ts',\n '**/node_modules/**',\n '**/dist/**',\n ],\n settings: {\n react: {\n version: '19',\n },\n },\n },\n eslint.configs.recommended,\n react.configs.flat.recommended,\n stylistic.configs.recommended,\n typescript.configs.recommended,\n typescript.configs.stylistic,\n {\n plugins: {\n 'react-hooks': reactHooks,\n },\n rules: reactHooks.configs.recommended.rules,\n },\n {\n rules: {\n 'capitalized-comments': [\n 'error',\n 'always',\n {ignoreConsecutiveComments: true},\n ],\n 'react/react-in-jsx-scope': 'off',\n '@stylistic/arrow-parens': [\n 'error',\n 'as-needed',\n ],\n '@stylistic/array-bracket-newline': [\n 'error',\n 'consistent',\n ],\n '@stylistic/array-bracket-spacing': [\n 'error',\n 'never',\n ],\n '@stylistic/array-element-newline': [\n 'error',\n 'consistent',\n ],\n '@stylistic/block-spacing': 'off',\n '@stylistic/brace-style': [\n 'error',\n '1tbs',\n {allowSingleLine: true},\n ],\n '@stylistic/indent': [\n 'error',\n 2,\n {ignoredNodes: ['TSMappedType > *']},\n ],\n '@stylistic/jsx-tag-spacing': [\n 'error',\n {\n afterOpening: 'never',\n beforeClosing: 'never',\n beforeSelfClosing: 'never',\n closingSlash: 'never',\n },\n ],\n '@stylistic/jsx-wrap-multilines': 'off',\n '@stylistic/lines-between-class-members': 'off',\n '@stylistic/object-curly-newline': [\n 'error',\n {consistent: true},\n ],\n '@stylistic/object-curly-spacing': [\n 'error',\n 'never',\n ],\n '@stylistic/operator-linebreak': [\n 'error',\n 'before',\n {overrides: {'=': 'after'}},\n ],\n '@stylistic/quotes': [\n 'error',\n 'single',\n {avoidEscape: true},\n ],\n '@stylistic/quote-props': [\n 'error',\n 'as-needed',\n ],\n '@stylistic/semi': [\n 'error',\n 'never',\n {beforeStatementContinuationChars: 'always'},\n ],\n '@typescript-eslint/no-empty-function': 'off',\n '@typescript-eslint/consistent-indexed-object-style': 'off',\n 'sort-imports': [\n 'error',\n {\n allowSeparatedGroups: true,\n ignoreCase: true,\n ignoreMemberSort: false,\n memberSyntaxSortOrder: [\n 'none',\n 'all',\n 'single',\n 'multiple',\n ],\n },\n ],\n },\n },\n)\n"],"mappings":";AAAA,OAAO,YAAY;AACnB,OAAO,WAAW;AAClB,OAAO,gBAAgB;AACvB,OAAO,eAAe;AACtB,OAAO,gBAAgB;AAEhB,IAAM,SAAS,WAAW;AAAA,EAC/B;AAAA,IACE,SAAS;AAAA,MACP;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,IACA,UAAU;AAAA,MACR,OAAO;AAAA,QACL,SAAS;AAAA,MACX;AAAA,IACF;AAAA,EACF;AAAA,EACA,OAAO,QAAQ;AAAA,EACf,MAAM,QAAQ,KAAK;AAAA,EACnB,UAAU,QAAQ;AAAA,EAClB,WAAW,QAAQ;AAAA,EACnB,WAAW,QAAQ;AAAA,EACnB;AAAA,IACE,SAAS;AAAA,MACP,eAAe;AAAA,IACjB;AAAA,IACA,OAAO,WAAW,QAAQ,YAAY;AAAA,EACxC;AAAA,EACA;AAAA,IACE,OAAO;AAAA,MACL,wBAAwB;AAAA,QACtB;AAAA,QACA;AAAA,QACA,EAAC,2BAA2B,KAAI;AAAA,MAClC;AAAA,MACA,4BAA4B;AAAA,MAC5B,2BAA2B;AAAA,QACzB;AAAA,QACA;AAAA,MACF;AAAA,MACA,oCAAoC;AAAA,QAClC;AAAA,QACA;AAAA,MACF;AAAA,MACA,oCAAoC;AAAA,QAClC;AAAA,QACA;AAAA,MACF;AAAA,MACA,oCAAoC;AAAA,QAClC;AAAA,QACA;AAAA,MACF;AAAA,MACA,4BAA4B;AAAA,MAC5B,0BAA0B;AAAA,QACxB;AAAA,QACA;AAAA,QACA,EAAC,iBAAiB,KAAI;AAAA,MACxB;AAAA,MACA,qBAAqB;AAAA,QACnB;AAAA,QACA;AAAA,QACA,EAAC,cAAc,CAAC,kBAAkB,EAAC;AAAA,MACrC;AAAA,MACA,8BAA8B;AAAA,QAC5B;AAAA,QACA;AAAA,UACE,cAAc;AAAA,UACd,eAAe;AAAA,UACf,mBAAmB;AAAA,UACnB,cAAc;AAAA,QAChB;AAAA,MACF;AAAA,MACA,kCAAkC;AAAA,MAClC,0CAA0C;AAAA,MAC1C,mCAAmC;AAAA,QACjC;AAAA,QACA,EAAC,YAAY,KAAI;AAAA,MACnB;AAAA,MACA,mCAAmC;AAAA,QACjC;AAAA,QACA;AAAA,MACF;AAAA,MACA,iCAAiC;AAAA,QAC/B;AAAA,QACA;AAAA,QACA,EAAC,WAAW,EAAC,KAAK,QAAO,EAAC;AAAA,MAC5B;AAAA,MACA,qBAAqB;AAAA,QACnB;AAAA,QACA;AAAA,QACA,EAAC,aAAa,KAAI;AAAA,MACpB;AAAA,MACA,0BAA0B;AAAA,QACxB;AAAA,QACA;AAAA,MACF;AAAA,MACA,mBAAmB;AAAA,QACjB;AAAA,QACA;AAAA,QACA,EAAC,kCAAkC,SAAQ;AAAA,MAC7C;AAAA,MACA,wCAAwC;AAAA,MACxC,sDAAsD;AAAA,MACtD,gBAAgB;AAAA,QACd;AAAA,QACA;AAAA,UACE,sBAAsB;AAAA,UACtB,YAAY;AAAA,UACZ,kBAAkB;AAAA,UAClB,uBAAuB;AAAA,YACrB;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA,UACF;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/rules/individualImports.ts","../src/rules/sortedImports/categorizeImport.ts","../src/rules/sortedImports/getSortKey.ts","../src/rules/sortedImports/categorizeImports.ts","../src/rules/sortedImports/checkAlphabeticalSorting.ts","../src/rules/sortedImports/checkGroupOrdering.ts","../src/rules/sortedImports/getSpecifierName.ts","../src/rules/sortedImports/areSpecifiersSorted.ts","../src/rules/sortedImports/getNamedSpecifiers.ts","../src/rules/sortedImports/checkSpecifiersSorting.ts","../src/rules/sortedImports/sortSpecifiersText.ts","../src/rules/sortedImports/createFix/formatNamedImport.ts","../src/rules/sortedImports/createFix/buildSortedCode.ts","../src/rules/sortedImports/createFix/findLastImportIndex.ts","../src/rules/sortedImports/createFix/getReplacementRange.ts","../src/rules/sortedImports/createFix/groupImportsByType.ts","../src/rules/sortedImports/createFix/sortImportGroups.ts","../src/rules/sortedImports/createFix/index.ts","../src/rules/sortedImports/getImportDeclarations.ts","../src/rules/sortedImports/index.ts"],"sourcesContent":["import eslint from '@eslint/js'\nimport react from 'eslint-plugin-react'\nimport reactHooks from 'eslint-plugin-react-hooks'\nimport stylistic from '@stylistic/eslint-plugin'\nimport typescript from 'typescript-eslint'\nimport {defineConfig} from 'eslint/config'\nimport {individualImports} from './rules/individualImports'\nimport {sortedImports} from './rules/sortedImports'\n\nexport const CONFIG = defineConfig([\n {\n ignores: [\n 'src/graphql/sdk.ts',\n '**/node_modules/**',\n '**/dist/**',\n ],\n },\n {\n settings: {\n react: {\n version: '19',\n },\n },\n },\n eslint.configs.recommended,\n react.configs.flat.recommended,\n stylistic.configs.recommended,\n typescript.configs.recommended,\n typescript.configs.stylistic,\n {\n plugins: {\n 'react-hooks': reactHooks,\n },\n rules: reactHooks.configs.recommended.rules,\n },\n {\n plugins: {\n '@borela-tech': {\n rules: {\n 'individual-imports': individualImports,\n 'sorted-imports': sortedImports,\n },\n },\n },\n rules: {\n '@borela-tech/individual-imports': 'error',\n '@borela-tech/sorted-imports': 'error',\n },\n },\n {\n rules: {\n 'capitalized-comments': [\n 'error',\n 'always',\n {ignoreConsecutiveComments: true},\n ],\n 'react/react-in-jsx-scope': 'off',\n '@stylistic/arrow-parens': [\n 'error',\n 'as-needed',\n ],\n '@stylistic/array-bracket-newline': [\n 'error',\n 'consistent',\n ],\n '@stylistic/array-bracket-spacing': [\n 'error',\n 'never',\n ],\n '@stylistic/array-element-newline': [\n 'error',\n 'consistent',\n ],\n '@stylistic/block-spacing': 'off',\n '@stylistic/brace-style': [\n 'error',\n '1tbs',\n {allowSingleLine: true},\n ],\n '@stylistic/indent': [\n 'error',\n 2,\n {ignoredNodes: ['TSMappedType > *']},\n ],\n '@stylistic/jsx-tag-spacing': [\n 'error',\n {\n afterOpening: 'never',\n beforeClosing: 'never',\n beforeSelfClosing: 'never',\n closingSlash: 'never',\n },\n ],\n '@stylistic/jsx-wrap-multilines': 'off',\n '@stylistic/lines-between-class-members': 'off',\n '@stylistic/object-curly-newline': [\n 'error',\n {consistent: true},\n ],\n '@stylistic/object-curly-spacing': [\n 'error',\n 'never',\n ],\n '@stylistic/operator-linebreak': [\n 'error',\n 'before',\n {overrides: {'=': 'after'}},\n ],\n '@stylistic/quotes': [\n 'error',\n 'single',\n {avoidEscape: true},\n ],\n '@stylistic/quote-props': [\n 'error',\n 'as-needed',\n ],\n '@stylistic/semi': [\n 'error',\n 'never',\n {beforeStatementContinuationChars: 'always'},\n ],\n '@typescript-eslint/no-empty-function': 'off',\n '@typescript-eslint/consistent-indexed-object-style': 'off',\n },\n },\n])\n","import type {Rule} from 'eslint'\n\nexport const individualImports: Rule.RuleModule = {\n meta: {\n docs: {\n description: 'Enforce individual imports instead of grouped imports',\n recommended: true,\n },\n fixable: 'code',\n messages: {\n individualImports: 'Use individual imports instead of grouped imports.',\n },\n schema: [],\n type: 'suggestion',\n },\n create(context) {\n return {\n ImportDeclaration(node) {\n if (node.specifiers.length <= 1)\n return\n context.report({\n node,\n messageId: 'individualImports',\n fix(fixer) {\n const source = node.source.raw\n const specifiers = node.specifiers\n .map(importSpecifier => {\n if (importSpecifier.type === 'ImportSpecifier')\n return `import {${importSpecifier.local.name}} from ${source}`\n return null\n })\n .filter(Boolean)\n\n if (specifiers.length !== node.specifiers.length)\n return null\n\n return fixer.replaceText(\n node,\n specifiers.join('\\n'),\n )\n },\n })\n },\n }\n },\n}\n","import type {ImportDeclaration} from 'estree'\nimport type {ImportGroup} from './ImportGroup'\n\nexport function categorizeImport(declaration: ImportDeclaration): ImportGroup {\n if (declaration.specifiers.length === 0)\n return 'side-effect'\n\n if (declaration.specifiers.some(s => s.type === 'ImportDefaultSpecifier'))\n return 'default'\n\n return 'named'\n}\n","import type {ImportDeclaration} from 'estree'\nimport type {ImportDefaultSpecifier} from 'estree'\nimport {categorizeImport} from './categorizeImport'\n\nexport function getSortKey(declaration: ImportDeclaration): string {\n const group = categorizeImport(declaration)\n\n if (group === 'side-effect')\n return (declaration.source.value as string).toLowerCase()\n\n if (group === 'default') {\n const defaultSpecifier = declaration.specifiers.find(\n s => s.type === 'ImportDefaultSpecifier',\n ) as ImportDefaultSpecifier | undefined\n\n return defaultSpecifier?.local.name.toLowerCase() ?? ''\n }\n\n return ''\n}\n","import type {ImportDeclaration} from 'estree'\nimport type {CategorizedImport} from './CategorizedImport'\nimport {categorizeImport} from './categorizeImport'\nimport {getSortKey} from './getSortKey'\n\nexport function categorizeImports(declarations: ImportDeclaration[]): CategorizedImport[] {\n return declarations.map(declaration => ({\n declaration,\n group: categorizeImport(declaration),\n sortKey: getSortKey(declaration),\n }))\n}\n","import type {CategorizedImport} from './CategorizedImport'\nimport type {ImportError} from './ImportError'\nimport type {ImportGroup} from './ImportGroup'\n\nexport function checkAlphabeticalSorting(categorized: CategorizedImport[]): ImportError[] {\n const errors: ImportError[] = []\n\n for (const group of ['side-effect', 'default'] as ImportGroup[]) {\n const groupImports = categorized.filter(c => c.group === group)\n const sorted = [...groupImports].sort((a, b) => a.sortKey.localeCompare(b.sortKey))\n for (let i = 0; i < groupImports.length; i++) {\n if (groupImports[i] !== sorted[i]) {\n errors.push({\n node: groupImports[i].declaration,\n messageId: 'sortedImports',\n })\n }\n }\n }\n\n return errors\n}\n","import type {CategorizedImport} from './CategorizedImport'\nimport type {ImportError} from './ImportError'\nimport type {ImportGroup} from './ImportGroup'\n\nexport function checkGroupOrdering(categorized: CategorizedImport[]): ImportError[] {\n const groupOrder: ImportGroup[] = ['side-effect', 'default', 'named']\n const errors: ImportError[] = []\n\n let currentGroupIndex = -1\n for (const {declaration, group} of categorized) {\n const groupIndex = groupOrder.indexOf(group)\n if (groupIndex < currentGroupIndex) {\n errors.push({\n node: declaration,\n messageId: 'wrongGroup',\n })\n } else\n currentGroupIndex = groupIndex\n }\n\n return errors\n}\n","import type {ImportSpecifier} from 'estree'\n\nexport function getSpecifierName(specifier: ImportSpecifier): string {\n return specifier.imported.type === 'Identifier'\n ? specifier.imported.name\n : String(specifier.imported.value)\n}\n","import type {ImportSpecifier} from 'estree'\nimport {getSpecifierName} from './getSpecifierName'\n\nexport function areSpecifiersSorted(specifiers: ImportSpecifier[]): boolean {\n const names = specifiers.map(s => getSpecifierName(s))\n const sorted = [...names].sort((a, b) =>\n a.toLowerCase().localeCompare(b.toLowerCase()),\n )\n return names.every((name, i) => name === sorted[i])\n}\n","import type {ImportDeclaration} from 'estree'\nimport type {ImportSpecifier} from 'estree'\n\nexport function getNamedSpecifiers(declaration: ImportDeclaration): ImportSpecifier[] {\n return declaration.specifiers.filter(\n (s): s is ImportSpecifier => s.type === 'ImportSpecifier',\n )\n}\n","import type {CategorizedImport} from './CategorizedImport'\nimport type {ImportError} from './ImportError'\nimport {areSpecifiersSorted} from './areSpecifiersSorted'\nimport {getNamedSpecifiers} from './getNamedSpecifiers'\n\nexport function checkSpecifiersSorting(categorized: CategorizedImport[]): ImportError[] {\n const errors: ImportError[] = []\n const namedImports = categorized.filter(c => c.group === 'named')\n\n for (const {declaration} of namedImports) {\n const specifiers = getNamedSpecifiers(declaration)\n if (specifiers.length > 1 && !areSpecifiersSorted(specifiers)) {\n errors.push({\n node: declaration,\n messageId: 'sortedNames',\n })\n }\n }\n\n return errors\n}\n","import type {ImportSpecifier} from 'estree'\nimport {getSpecifierName} from './getSpecifierName'\n\nexport function sortSpecifiersText(\n specifiers: ImportSpecifier[],\n sourceCode: {getText: (node: ImportSpecifier) => string},\n): string {\n const sorted = [...specifiers].sort((a, b) => {\n const lowerA = getSpecifierName(a).toLowerCase()\n const lowerB = getSpecifierName(b).toLowerCase()\n return lowerA.localeCompare(lowerB)\n })\n return sorted.map(s => sourceCode.getText(s)).join(', ')\n}\n","import type {ImportDeclaration} from 'estree'\nimport {areSpecifiersSorted} from '../areSpecifiersSorted'\nimport {getNamedSpecifiers} from '../getNamedSpecifiers'\nimport {sortSpecifiersText} from '../sortSpecifiersText'\n\nexport function formatNamedImport(\n declaration: ImportDeclaration,\n sourceCode: {getText: (node?: unknown) => string},\n): string {\n const specifiers = getNamedSpecifiers(declaration)\n\n if (specifiers.length > 1 && !areSpecifiersSorted(specifiers)) {\n const importText = sourceCode.getText(declaration)\n const specifiersStart = importText.indexOf('{')\n const specifiersEnd = importText.lastIndexOf('}')\n const before = importText.substring(0, specifiersStart + 1)\n const after = importText.substring(specifiersEnd)\n const sortedSpecifiers = sortSpecifiersText(specifiers, sourceCode)\n return before + ' ' + sortedSpecifiers + ' ' + after\n }\n\n return sourceCode.getText(declaration)\n}\n","import type {CategorizedImport} from '../CategorizedImport'\nimport type {ImportGroup} from '../ImportGroup'\nimport {formatNamedImport} from './formatNamedImport'\n\nexport function buildSortedCode(\n grouped: Record<ImportGroup, CategorizedImport[]>,\n sourceCode: {getText: (node?: unknown) => string},\n): string[] {\n const groupOrder: ImportGroup[] = ['side-effect', 'default', 'named']\n const sortedCode: string[] = []\n\n for (const group of groupOrder) {\n for (const {declaration} of grouped[group]) {\n if (group === 'named')\n sortedCode.push(formatNamedImport(declaration, sourceCode))\n else\n sortedCode.push(sourceCode.getText(declaration))\n }\n }\n\n return sortedCode\n}\n","import type {Program} from 'estree'\n\nexport function findLastImportIndex(programBody: Program['body']): number {\n let lastIndex = 0\n for (let i = 0; i < programBody.length; i++) {\n if (programBody[i].type === 'ImportDeclaration')\n lastIndex = i\n else\n break\n }\n return lastIndex\n}\n","import type {ImportDeclaration} from 'estree'\nimport type {Program} from 'estree'\nimport type {ReplacementRange} from './ReplacementRange'\nimport {findLastImportIndex} from './findLastImportIndex'\n\nexport function getReplacementRange(\n programBody: Program['body'],\n sourceCode: {getText: () => string},\n): ReplacementRange {\n const fullText = sourceCode.getText()\n const lastIndex = findLastImportIndex(programBody)\n const firstImport = programBody[0] as ImportDeclaration\n const lastImport = programBody[lastIndex] as ImportDeclaration\n const start = firstImport.range![0]\n let end = lastImport.range![1]\n\n for (let i = end; i < fullText.length; i++) {\n const char = fullText[i]\n if (char === '\\n' || char === ' ' || char === '\\t')\n end++\n else\n break\n }\n\n return {start, end}\n}\n","import type {CategorizedImport} from '../CategorizedImport'\nimport type {ImportGroup} from '../ImportGroup'\n\nexport function groupImportsByType(\n categorized: CategorizedImport[],\n): Record<ImportGroup, CategorizedImport[]> {\n const grouped: Record<ImportGroup, CategorizedImport[]> = {\n 'side-effect': [],\n default: [],\n named: [],\n }\n\n for (const item of categorized)\n grouped[item.group].push(item)\n\n return grouped\n}\n","import type {CategorizedImport} from '../CategorizedImport'\nimport type {ImportGroup} from '../ImportGroup'\n\nexport function sortImportGroups(\n grouped: Record<ImportGroup, CategorizedImport[]>,\n): void {\n grouped['side-effect'].sort((a, b) => a.sortKey.localeCompare(b.sortKey))\n grouped['default'].sort((a, b) => a.sortKey.localeCompare(b.sortKey))\n}\n","import type {Rule} from 'eslint'\nimport type {ImportDeclaration} from 'estree'\nimport type {Program} from 'estree'\nimport {buildSortedCode} from './buildSortedCode'\nimport {categorizeImports} from '../categorizeImports'\nimport {getReplacementRange} from './getReplacementRange'\nimport {groupImportsByType} from './groupImportsByType'\nimport {sortImportGroups} from './sortImportGroups'\n\nexport function createFix(\n fixer: Rule.RuleFixer,\n importDeclarations: ImportDeclaration[],\n sourceCode: {getText: (node?: unknown) => string},\n programBody: Program['body'],\n) {\n const range = getReplacementRange(programBody, sourceCode)\n const categorized = categorizeImports(importDeclarations)\n const grouped = groupImportsByType(categorized)\n\n sortImportGroups(grouped)\n\n const sortedCode = buildSortedCode(grouped, sourceCode)\n .join('\\n')\n\n return fixer.replaceTextRange(\n [range.start, range.end],\n sortedCode,\n )\n}\n","import type {ImportDeclaration} from 'estree'\nimport type {Program} from 'estree'\n\nexport function getImportDeclarations(programBody: Program['body']): ImportDeclaration[] {\n return programBody.filter(\n (statement): statement is ImportDeclaration =>\n statement.type === 'ImportDeclaration',\n )\n}\n","import type {Rule} from 'eslint'\nimport type {ImportError} from './ImportError'\nimport {categorizeImports} from './categorizeImports'\nimport {checkAlphabeticalSorting} from './checkAlphabeticalSorting'\nimport {checkGroupOrdering} from './checkGroupOrdering'\nimport {checkSpecifiersSorting} from './checkSpecifiersSorting'\nimport {createFix} from './createFix'\nimport {getImportDeclarations} from './getImportDeclarations'\n\nexport const sortedImports: Rule.RuleModule = {\n meta: {\n docs: {\n description: 'Enforce sorted imports alphabetically',\n recommended: true,\n },\n fixable: 'code',\n messages: {\n sortedImports: 'Imports should be sorted alphabetically',\n sortedNames: 'Named imports should be sorted alphabetically',\n wrongGroup: 'Import is in wrong group',\n },\n schema: [],\n type: 'suggestion',\n },\n create(context) {\n return {\n Program(node) {\n const declarations = getImportDeclarations(node.body)\n if (declarations.length === 0)\n return\n\n const categorized = categorizeImports(declarations)\n const errors: ImportError[] = [\n ...checkGroupOrdering(categorized),\n ...checkAlphabeticalSorting(categorized),\n ...checkSpecifiersSorting(categorized),\n ]\n\n for (const error of errors) {\n context.report({\n node: error.node,\n messageId: error.messageId,\n fix(fixer) {\n const sourceCode = context.sourceCode\n return createFix(fixer, declarations, sourceCode, node.body)\n },\n })\n }\n },\n }\n },\n}\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@borela-tech/eslint-config",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "ESLint config used in Borela Tech projects.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"@stylistic/eslint-plugin": "^4.2.0",
|
|
32
32
|
"eslint-plugin-react": "^7.37.5",
|
|
33
33
|
"eslint-plugin-react-hooks": "^5.2.0",
|
|
34
|
-
"typescript-eslint": "
|
|
34
|
+
"typescript-eslint": "8.56.0"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"eslint": "^9.24.0",
|
package/src/index.ts
CHANGED
|
@@ -3,14 +3,19 @@ import react from 'eslint-plugin-react'
|
|
|
3
3
|
import reactHooks from 'eslint-plugin-react-hooks'
|
|
4
4
|
import stylistic from '@stylistic/eslint-plugin'
|
|
5
5
|
import typescript from 'typescript-eslint'
|
|
6
|
+
import {defineConfig} from 'eslint/config'
|
|
7
|
+
import {individualImports} from './rules/individualImports'
|
|
8
|
+
import {sortedImports} from './rules/sortedImports'
|
|
6
9
|
|
|
7
|
-
export const CONFIG =
|
|
10
|
+
export const CONFIG = defineConfig([
|
|
8
11
|
{
|
|
9
12
|
ignores: [
|
|
10
13
|
'src/graphql/sdk.ts',
|
|
11
14
|
'**/node_modules/**',
|
|
12
15
|
'**/dist/**',
|
|
13
16
|
],
|
|
17
|
+
},
|
|
18
|
+
{
|
|
14
19
|
settings: {
|
|
15
20
|
react: {
|
|
16
21
|
version: '19',
|
|
@@ -28,6 +33,20 @@ export const CONFIG = typescript.config(
|
|
|
28
33
|
},
|
|
29
34
|
rules: reactHooks.configs.recommended.rules,
|
|
30
35
|
},
|
|
36
|
+
{
|
|
37
|
+
plugins: {
|
|
38
|
+
'@borela-tech': {
|
|
39
|
+
rules: {
|
|
40
|
+
'individual-imports': individualImports,
|
|
41
|
+
'sorted-imports': sortedImports,
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
rules: {
|
|
46
|
+
'@borela-tech/individual-imports': 'error',
|
|
47
|
+
'@borela-tech/sorted-imports': 'error',
|
|
48
|
+
},
|
|
49
|
+
},
|
|
31
50
|
{
|
|
32
51
|
rules: {
|
|
33
52
|
'capitalized-comments': [
|
|
@@ -103,20 +122,6 @@ export const CONFIG = typescript.config(
|
|
|
103
122
|
],
|
|
104
123
|
'@typescript-eslint/no-empty-function': 'off',
|
|
105
124
|
'@typescript-eslint/consistent-indexed-object-style': 'off',
|
|
106
|
-
'sort-imports': [
|
|
107
|
-
'error',
|
|
108
|
-
{
|
|
109
|
-
allowSeparatedGroups: true,
|
|
110
|
-
ignoreCase: true,
|
|
111
|
-
ignoreMemberSort: false,
|
|
112
|
-
memberSyntaxSortOrder: [
|
|
113
|
-
'none',
|
|
114
|
-
'all',
|
|
115
|
-
'single',
|
|
116
|
-
'multiple',
|
|
117
|
-
],
|
|
118
|
-
},
|
|
119
|
-
],
|
|
120
125
|
},
|
|
121
126
|
},
|
|
122
|
-
)
|
|
127
|
+
])
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import {countLeadingSpaces} from './countLeadingSpaces'
|
|
2
|
+
|
|
3
|
+
export function findMinIndent(lines: string[]): number {
|
|
4
|
+
const nonEmptyLines = lines.filter(line => line.trim().length > 0)
|
|
5
|
+
const indents = nonEmptyLines.map(line => countLeadingSpaces(line))
|
|
6
|
+
return Math.min(...indents)
|
|
7
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import {findMinIndent} from './findMinIndent'
|
|
2
|
+
import {interpolate} from './interpolate'
|
|
3
|
+
import {removeEmptyPrefix} from './removeEmptyPrefix'
|
|
4
|
+
import {removeEmptySuffix} from './removeEmptySuffix'
|
|
5
|
+
import {removeIndent} from './removeIndent'
|
|
6
|
+
|
|
7
|
+
export function dedent(
|
|
8
|
+
strings: TemplateStringsArray,
|
|
9
|
+
...values: unknown[]
|
|
10
|
+
): string {
|
|
11
|
+
const raw = interpolate(strings, values)
|
|
12
|
+
const lines = raw.split('\n')
|
|
13
|
+
const withoutPrefix = removeEmptyPrefix(lines)
|
|
14
|
+
const trimmed = removeEmptySuffix(withoutPrefix)
|
|
15
|
+
const indentSize = findMinIndent(trimmed)
|
|
16
|
+
return removeIndent(trimmed, indentSize)
|
|
17
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export function interpolate(
|
|
2
|
+
strings: TemplateStringsArray,
|
|
3
|
+
values: unknown[],
|
|
4
|
+
): string {
|
|
5
|
+
let result = ''
|
|
6
|
+
for (let i = 0; i < strings.length; i++) {
|
|
7
|
+
result += strings[i]
|
|
8
|
+
if (i < values.length) result += String(values[i])
|
|
9
|
+
}
|
|
10
|
+
return result
|
|
11
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import typescript from 'typescript-eslint'
|
|
2
|
+
import {RuleTester} from 'eslint'
|
|
3
|
+
import {individualImports} from '../individualImports'
|
|
4
|
+
import {dedent} from './dedent'
|
|
5
|
+
|
|
6
|
+
const ruleTester = new RuleTester({
|
|
7
|
+
languageOptions: {
|
|
8
|
+
parser: typescript.parser,
|
|
9
|
+
parserOptions: {
|
|
10
|
+
ecmaVersion: 2020,
|
|
11
|
+
sourceType: 'module',
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
ruleTester.run('individual-imports', individualImports, {
|
|
17
|
+
valid: [{
|
|
18
|
+
code: "import {foo} from 'bar'",
|
|
19
|
+
}, {
|
|
20
|
+
code: "import foo from 'bar'",
|
|
21
|
+
}, {
|
|
22
|
+
code: "import * as foo from 'bar'",
|
|
23
|
+
}, {
|
|
24
|
+
code: "import 'bar'",
|
|
25
|
+
}],
|
|
26
|
+
invalid: [{
|
|
27
|
+
code: "import {foo, bar} from 'baz'",
|
|
28
|
+
errors: [{messageId: 'individualImports'}],
|
|
29
|
+
output: dedent`
|
|
30
|
+
import {foo} from 'baz'
|
|
31
|
+
import {bar} from 'baz'
|
|
32
|
+
`,
|
|
33
|
+
}, {
|
|
34
|
+
code: "import {foo, bar, baz} from 'qux'",
|
|
35
|
+
errors: [{messageId: 'individualImports'}],
|
|
36
|
+
output: dedent`
|
|
37
|
+
import {foo} from 'qux'
|
|
38
|
+
import {bar} from 'qux'
|
|
39
|
+
import {baz} from 'qux'
|
|
40
|
+
`,
|
|
41
|
+
}],
|
|
42
|
+
})
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import typescript from 'typescript-eslint'
|
|
2
|
+
import {RuleTester} from 'eslint'
|
|
3
|
+
import {sortedImports} from '../sortedImports'
|
|
4
|
+
import {dedent} from './dedent'
|
|
5
|
+
|
|
6
|
+
const ruleTester = new RuleTester({
|
|
7
|
+
languageOptions: {
|
|
8
|
+
parser: typescript.parser,
|
|
9
|
+
parserOptions: {
|
|
10
|
+
ecmaVersion: 2020,
|
|
11
|
+
sourceType: 'module',
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
ruleTester.run('sorted-imports', sortedImports, {
|
|
17
|
+
valid: [{
|
|
18
|
+
code: "import {foo} from 'bar'",
|
|
19
|
+
}, {
|
|
20
|
+
code: "import foo from 'bar'",
|
|
21
|
+
}, {
|
|
22
|
+
code: "import 'bar'",
|
|
23
|
+
}, {
|
|
24
|
+
code: dedent`
|
|
25
|
+
import 'aaa'
|
|
26
|
+
import 'bbb'
|
|
27
|
+
import bar from 'bbb'
|
|
28
|
+
import foo from 'aaa'
|
|
29
|
+
import {a} from 'aaa'
|
|
30
|
+
import {b} from 'bbb'
|
|
31
|
+
`,
|
|
32
|
+
}, {
|
|
33
|
+
code: dedent`
|
|
34
|
+
import {a} from 'ccc'
|
|
35
|
+
import {b} from 'aaa'
|
|
36
|
+
import {c} from 'bbb'
|
|
37
|
+
`,
|
|
38
|
+
}, {
|
|
39
|
+
code: dedent`
|
|
40
|
+
import {a, b, c} from 'bar'
|
|
41
|
+
`,
|
|
42
|
+
}, {
|
|
43
|
+
code: '',
|
|
44
|
+
}, {
|
|
45
|
+
code: 'const x = 1',
|
|
46
|
+
}],
|
|
47
|
+
invalid: [{
|
|
48
|
+
code: dedent`
|
|
49
|
+
import {c, a, b} from 'bar'
|
|
50
|
+
`,
|
|
51
|
+
errors: [{messageId: 'sortedNames'}],
|
|
52
|
+
output: dedent`
|
|
53
|
+
import { a, b, c } from 'bar'
|
|
54
|
+
`,
|
|
55
|
+
}, {
|
|
56
|
+
code: dedent`
|
|
57
|
+
import {z, a} from 'bar'
|
|
58
|
+
`,
|
|
59
|
+
errors: [{messageId: 'sortedNames'}],
|
|
60
|
+
output: dedent`
|
|
61
|
+
import { a, z } from 'bar'
|
|
62
|
+
`,
|
|
63
|
+
}, {
|
|
64
|
+
code: dedent`
|
|
65
|
+
import foo from 'aaa'
|
|
66
|
+
import bar from 'bbb'
|
|
67
|
+
`,
|
|
68
|
+
errors: [{messageId: 'sortedImports'}, {messageId: 'sortedImports'}],
|
|
69
|
+
output: dedent`
|
|
70
|
+
import bar from 'bbb'
|
|
71
|
+
import foo from 'aaa'
|
|
72
|
+
`,
|
|
73
|
+
}, {
|
|
74
|
+
code: dedent`
|
|
75
|
+
import 'bbb'
|
|
76
|
+
import 'aaa'
|
|
77
|
+
`,
|
|
78
|
+
errors: [{messageId: 'sortedImports'}, {messageId: 'sortedImports'}],
|
|
79
|
+
output: dedent`
|
|
80
|
+
import 'aaa'
|
|
81
|
+
import 'bbb'
|
|
82
|
+
`,
|
|
83
|
+
}, {
|
|
84
|
+
code: dedent`
|
|
85
|
+
import foo from 'bar'
|
|
86
|
+
import 'baz'
|
|
87
|
+
`,
|
|
88
|
+
errors: [{messageId: 'wrongGroup'}],
|
|
89
|
+
output: dedent`
|
|
90
|
+
import 'baz'
|
|
91
|
+
import foo from 'bar'
|
|
92
|
+
`,
|
|
93
|
+
}, {
|
|
94
|
+
code: dedent`
|
|
95
|
+
import {a} from 'bar'
|
|
96
|
+
import foo from 'baz'
|
|
97
|
+
`,
|
|
98
|
+
errors: [{messageId: 'wrongGroup'}],
|
|
99
|
+
output: dedent`
|
|
100
|
+
import foo from 'baz'
|
|
101
|
+
import {a} from 'bar'
|
|
102
|
+
`,
|
|
103
|
+
}, {
|
|
104
|
+
code: dedent`
|
|
105
|
+
import {b, a} from 'bar'
|
|
106
|
+
import foo from 'baz'
|
|
107
|
+
`,
|
|
108
|
+
errors: [{messageId: 'sortedNames'}, {messageId: 'wrongGroup'}],
|
|
109
|
+
output: dedent`
|
|
110
|
+
import foo from 'baz'
|
|
111
|
+
import { a, b } from 'bar'
|
|
112
|
+
`,
|
|
113
|
+
}],
|
|
114
|
+
})
|
|
@@ -24,9 +24,9 @@ export const individualImports: Rule.RuleModule = {
|
|
|
24
24
|
fix(fixer) {
|
|
25
25
|
const source = node.source.raw
|
|
26
26
|
const specifiers = node.specifiers
|
|
27
|
-
.map(
|
|
28
|
-
if (
|
|
29
|
-
return `import {${
|
|
27
|
+
.map(importSpecifier => {
|
|
28
|
+
if (importSpecifier.type === 'ImportSpecifier')
|
|
29
|
+
return `import {${importSpecifier.local.name}} from ${source}`
|
|
30
30
|
return null
|
|
31
31
|
})
|
|
32
32
|
.filter(Boolean)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type ImportGroup = 'side-effect' | 'default' | 'named'
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type {ImportSpecifier} from 'estree'
|
|
2
|
+
import {getSpecifierName} from './getSpecifierName'
|
|
3
|
+
|
|
4
|
+
export function areSpecifiersSorted(specifiers: ImportSpecifier[]): boolean {
|
|
5
|
+
const names = specifiers.map(s => getSpecifierName(s))
|
|
6
|
+
const sorted = [...names].sort((a, b) =>
|
|
7
|
+
a.toLowerCase().localeCompare(b.toLowerCase()),
|
|
8
|
+
)
|
|
9
|
+
return names.every((name, i) => name === sorted[i])
|
|
10
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type {ImportDeclaration} from 'estree'
|
|
2
|
+
import type {ImportGroup} from './ImportGroup'
|
|
3
|
+
|
|
4
|
+
export function categorizeImport(declaration: ImportDeclaration): ImportGroup {
|
|
5
|
+
if (declaration.specifiers.length === 0)
|
|
6
|
+
return 'side-effect'
|
|
7
|
+
|
|
8
|
+
if (declaration.specifiers.some(s => s.type === 'ImportDefaultSpecifier'))
|
|
9
|
+
return 'default'
|
|
10
|
+
|
|
11
|
+
return 'named'
|
|
12
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type {ImportDeclaration} from 'estree'
|
|
2
|
+
import type {CategorizedImport} from './CategorizedImport'
|
|
3
|
+
import {categorizeImport} from './categorizeImport'
|
|
4
|
+
import {getSortKey} from './getSortKey'
|
|
5
|
+
|
|
6
|
+
export function categorizeImports(declarations: ImportDeclaration[]): CategorizedImport[] {
|
|
7
|
+
return declarations.map(declaration => ({
|
|
8
|
+
declaration,
|
|
9
|
+
group: categorizeImport(declaration),
|
|
10
|
+
sortKey: getSortKey(declaration),
|
|
11
|
+
}))
|
|
12
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type {CategorizedImport} from './CategorizedImport'
|
|
2
|
+
import type {ImportError} from './ImportError'
|
|
3
|
+
import type {ImportGroup} from './ImportGroup'
|
|
4
|
+
|
|
5
|
+
export function checkAlphabeticalSorting(categorized: CategorizedImport[]): ImportError[] {
|
|
6
|
+
const errors: ImportError[] = []
|
|
7
|
+
|
|
8
|
+
for (const group of ['side-effect', 'default'] as ImportGroup[]) {
|
|
9
|
+
const groupImports = categorized.filter(c => c.group === group)
|
|
10
|
+
const sorted = [...groupImports].sort((a, b) => a.sortKey.localeCompare(b.sortKey))
|
|
11
|
+
for (let i = 0; i < groupImports.length; i++) {
|
|
12
|
+
if (groupImports[i] !== sorted[i]) {
|
|
13
|
+
errors.push({
|
|
14
|
+
node: groupImports[i].declaration,
|
|
15
|
+
messageId: 'sortedImports',
|
|
16
|
+
})
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return errors
|
|
22
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type {CategorizedImport} from './CategorizedImport'
|
|
2
|
+
import type {ImportError} from './ImportError'
|
|
3
|
+
import type {ImportGroup} from './ImportGroup'
|
|
4
|
+
|
|
5
|
+
export function checkGroupOrdering(categorized: CategorizedImport[]): ImportError[] {
|
|
6
|
+
const groupOrder: ImportGroup[] = ['side-effect', 'default', 'named']
|
|
7
|
+
const errors: ImportError[] = []
|
|
8
|
+
|
|
9
|
+
let currentGroupIndex = -1
|
|
10
|
+
for (const {declaration, group} of categorized) {
|
|
11
|
+
const groupIndex = groupOrder.indexOf(group)
|
|
12
|
+
if (groupIndex < currentGroupIndex) {
|
|
13
|
+
errors.push({
|
|
14
|
+
node: declaration,
|
|
15
|
+
messageId: 'wrongGroup',
|
|
16
|
+
})
|
|
17
|
+
} else
|
|
18
|
+
currentGroupIndex = groupIndex
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return errors
|
|
22
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type {CategorizedImport} from './CategorizedImport'
|
|
2
|
+
import type {ImportError} from './ImportError'
|
|
3
|
+
import {areSpecifiersSorted} from './areSpecifiersSorted'
|
|
4
|
+
import {getNamedSpecifiers} from './getNamedSpecifiers'
|
|
5
|
+
|
|
6
|
+
export function checkSpecifiersSorting(categorized: CategorizedImport[]): ImportError[] {
|
|
7
|
+
const errors: ImportError[] = []
|
|
8
|
+
const namedImports = categorized.filter(c => c.group === 'named')
|
|
9
|
+
|
|
10
|
+
for (const {declaration} of namedImports) {
|
|
11
|
+
const specifiers = getNamedSpecifiers(declaration)
|
|
12
|
+
if (specifiers.length > 1 && !areSpecifiersSorted(specifiers)) {
|
|
13
|
+
errors.push({
|
|
14
|
+
node: declaration,
|
|
15
|
+
messageId: 'sortedNames',
|
|
16
|
+
})
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return errors
|
|
21
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type {CategorizedImport} from '../CategorizedImport'
|
|
2
|
+
import type {ImportGroup} from '../ImportGroup'
|
|
3
|
+
import {formatNamedImport} from './formatNamedImport'
|
|
4
|
+
|
|
5
|
+
export function buildSortedCode(
|
|
6
|
+
grouped: Record<ImportGroup, CategorizedImport[]>,
|
|
7
|
+
sourceCode: {getText: (node?: unknown) => string},
|
|
8
|
+
): string[] {
|
|
9
|
+
const groupOrder: ImportGroup[] = ['side-effect', 'default', 'named']
|
|
10
|
+
const sortedCode: string[] = []
|
|
11
|
+
|
|
12
|
+
for (const group of groupOrder) {
|
|
13
|
+
for (const {declaration} of grouped[group]) {
|
|
14
|
+
if (group === 'named')
|
|
15
|
+
sortedCode.push(formatNamedImport(declaration, sourceCode))
|
|
16
|
+
else
|
|
17
|
+
sortedCode.push(sourceCode.getText(declaration))
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return sortedCode
|
|
22
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type {Program} from 'estree'
|
|
2
|
+
|
|
3
|
+
export function findLastImportIndex(programBody: Program['body']): number {
|
|
4
|
+
let lastIndex = 0
|
|
5
|
+
for (let i = 0; i < programBody.length; i++) {
|
|
6
|
+
if (programBody[i].type === 'ImportDeclaration')
|
|
7
|
+
lastIndex = i
|
|
8
|
+
else
|
|
9
|
+
break
|
|
10
|
+
}
|
|
11
|
+
return lastIndex
|
|
12
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type {ImportDeclaration} from 'estree'
|
|
2
|
+
import {areSpecifiersSorted} from '../areSpecifiersSorted'
|
|
3
|
+
import {getNamedSpecifiers} from '../getNamedSpecifiers'
|
|
4
|
+
import {sortSpecifiersText} from '../sortSpecifiersText'
|
|
5
|
+
|
|
6
|
+
export function formatNamedImport(
|
|
7
|
+
declaration: ImportDeclaration,
|
|
8
|
+
sourceCode: {getText: (node?: unknown) => string},
|
|
9
|
+
): string {
|
|
10
|
+
const specifiers = getNamedSpecifiers(declaration)
|
|
11
|
+
|
|
12
|
+
if (specifiers.length > 1 && !areSpecifiersSorted(specifiers)) {
|
|
13
|
+
const importText = sourceCode.getText(declaration)
|
|
14
|
+
const specifiersStart = importText.indexOf('{')
|
|
15
|
+
const specifiersEnd = importText.lastIndexOf('}')
|
|
16
|
+
const before = importText.substring(0, specifiersStart + 1)
|
|
17
|
+
const after = importText.substring(specifiersEnd)
|
|
18
|
+
const sortedSpecifiers = sortSpecifiersText(specifiers, sourceCode)
|
|
19
|
+
return before + ' ' + sortedSpecifiers + ' ' + after
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return sourceCode.getText(declaration)
|
|
23
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type {ImportDeclaration} from 'estree'
|
|
2
|
+
import type {Program} from 'estree'
|
|
3
|
+
import type {ReplacementRange} from './ReplacementRange'
|
|
4
|
+
import {findLastImportIndex} from './findLastImportIndex'
|
|
5
|
+
|
|
6
|
+
export function getReplacementRange(
|
|
7
|
+
programBody: Program['body'],
|
|
8
|
+
sourceCode: {getText: () => string},
|
|
9
|
+
): ReplacementRange {
|
|
10
|
+
const fullText = sourceCode.getText()
|
|
11
|
+
const lastIndex = findLastImportIndex(programBody)
|
|
12
|
+
const firstImport = programBody[0] as ImportDeclaration
|
|
13
|
+
const lastImport = programBody[lastIndex] as ImportDeclaration
|
|
14
|
+
const start = firstImport.range![0]
|
|
15
|
+
let end = lastImport.range![1]
|
|
16
|
+
|
|
17
|
+
for (let i = end; i < fullText.length; i++) {
|
|
18
|
+
const char = fullText[i]
|
|
19
|
+
if (char === '\n' || char === ' ' || char === '\t')
|
|
20
|
+
end++
|
|
21
|
+
else
|
|
22
|
+
break
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return {start, end}
|
|
26
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type {CategorizedImport} from '../CategorizedImport'
|
|
2
|
+
import type {ImportGroup} from '../ImportGroup'
|
|
3
|
+
|
|
4
|
+
export function groupImportsByType(
|
|
5
|
+
categorized: CategorizedImport[],
|
|
6
|
+
): Record<ImportGroup, CategorizedImport[]> {
|
|
7
|
+
const grouped: Record<ImportGroup, CategorizedImport[]> = {
|
|
8
|
+
'side-effect': [],
|
|
9
|
+
default: [],
|
|
10
|
+
named: [],
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
for (const item of categorized)
|
|
14
|
+
grouped[item.group].push(item)
|
|
15
|
+
|
|
16
|
+
return grouped
|
|
17
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type {Rule} from 'eslint'
|
|
2
|
+
import type {ImportDeclaration} from 'estree'
|
|
3
|
+
import type {Program} from 'estree'
|
|
4
|
+
import {buildSortedCode} from './buildSortedCode'
|
|
5
|
+
import {categorizeImports} from '../categorizeImports'
|
|
6
|
+
import {getReplacementRange} from './getReplacementRange'
|
|
7
|
+
import {groupImportsByType} from './groupImportsByType'
|
|
8
|
+
import {sortImportGroups} from './sortImportGroups'
|
|
9
|
+
|
|
10
|
+
export function createFix(
|
|
11
|
+
fixer: Rule.RuleFixer,
|
|
12
|
+
importDeclarations: ImportDeclaration[],
|
|
13
|
+
sourceCode: {getText: (node?: unknown) => string},
|
|
14
|
+
programBody: Program['body'],
|
|
15
|
+
) {
|
|
16
|
+
const range = getReplacementRange(programBody, sourceCode)
|
|
17
|
+
const categorized = categorizeImports(importDeclarations)
|
|
18
|
+
const grouped = groupImportsByType(categorized)
|
|
19
|
+
|
|
20
|
+
sortImportGroups(grouped)
|
|
21
|
+
|
|
22
|
+
const sortedCode = buildSortedCode(grouped, sourceCode)
|
|
23
|
+
.join('\n')
|
|
24
|
+
|
|
25
|
+
return fixer.replaceTextRange(
|
|
26
|
+
[range.start, range.end],
|
|
27
|
+
sortedCode,
|
|
28
|
+
)
|
|
29
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type {CategorizedImport} from '../CategorizedImport'
|
|
2
|
+
import type {ImportGroup} from '../ImportGroup'
|
|
3
|
+
|
|
4
|
+
export function sortImportGroups(
|
|
5
|
+
grouped: Record<ImportGroup, CategorizedImport[]>,
|
|
6
|
+
): void {
|
|
7
|
+
grouped['side-effect'].sort((a, b) => a.sortKey.localeCompare(b.sortKey))
|
|
8
|
+
grouped['default'].sort((a, b) => a.sortKey.localeCompare(b.sortKey))
|
|
9
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type {ImportDeclaration} from 'estree'
|
|
2
|
+
import type {Program} from 'estree'
|
|
3
|
+
|
|
4
|
+
export function getImportDeclarations(programBody: Program['body']): ImportDeclaration[] {
|
|
5
|
+
return programBody.filter(
|
|
6
|
+
(statement): statement is ImportDeclaration =>
|
|
7
|
+
statement.type === 'ImportDeclaration',
|
|
8
|
+
)
|
|
9
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type {ImportDeclaration} from 'estree'
|
|
2
|
+
import type {ImportSpecifier} from 'estree'
|
|
3
|
+
|
|
4
|
+
export function getNamedSpecifiers(declaration: ImportDeclaration): ImportSpecifier[] {
|
|
5
|
+
return declaration.specifiers.filter(
|
|
6
|
+
(s): s is ImportSpecifier => s.type === 'ImportSpecifier',
|
|
7
|
+
)
|
|
8
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type {ImportDeclaration} from 'estree'
|
|
2
|
+
import type {ImportDefaultSpecifier} from 'estree'
|
|
3
|
+
import {categorizeImport} from './categorizeImport'
|
|
4
|
+
|
|
5
|
+
export function getSortKey(declaration: ImportDeclaration): string {
|
|
6
|
+
const group = categorizeImport(declaration)
|
|
7
|
+
|
|
8
|
+
if (group === 'side-effect')
|
|
9
|
+
return (declaration.source.value as string).toLowerCase()
|
|
10
|
+
|
|
11
|
+
if (group === 'default') {
|
|
12
|
+
const defaultSpecifier = declaration.specifiers.find(
|
|
13
|
+
s => s.type === 'ImportDefaultSpecifier',
|
|
14
|
+
) as ImportDefaultSpecifier | undefined
|
|
15
|
+
|
|
16
|
+
return defaultSpecifier?.local.name.toLowerCase() ?? ''
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return ''
|
|
20
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type {Rule} from 'eslint'
|
|
2
|
+
import type {ImportError} from './ImportError'
|
|
3
|
+
import {categorizeImports} from './categorizeImports'
|
|
4
|
+
import {checkAlphabeticalSorting} from './checkAlphabeticalSorting'
|
|
5
|
+
import {checkGroupOrdering} from './checkGroupOrdering'
|
|
6
|
+
import {checkSpecifiersSorting} from './checkSpecifiersSorting'
|
|
7
|
+
import {createFix} from './createFix'
|
|
8
|
+
import {getImportDeclarations} from './getImportDeclarations'
|
|
9
|
+
|
|
10
|
+
export const sortedImports: Rule.RuleModule = {
|
|
11
|
+
meta: {
|
|
12
|
+
docs: {
|
|
13
|
+
description: 'Enforce sorted imports alphabetically',
|
|
14
|
+
recommended: true,
|
|
15
|
+
},
|
|
16
|
+
fixable: 'code',
|
|
17
|
+
messages: {
|
|
18
|
+
sortedImports: 'Imports should be sorted alphabetically',
|
|
19
|
+
sortedNames: 'Named imports should be sorted alphabetically',
|
|
20
|
+
wrongGroup: 'Import is in wrong group',
|
|
21
|
+
},
|
|
22
|
+
schema: [],
|
|
23
|
+
type: 'suggestion',
|
|
24
|
+
},
|
|
25
|
+
create(context) {
|
|
26
|
+
return {
|
|
27
|
+
Program(node) {
|
|
28
|
+
const declarations = getImportDeclarations(node.body)
|
|
29
|
+
if (declarations.length === 0)
|
|
30
|
+
return
|
|
31
|
+
|
|
32
|
+
const categorized = categorizeImports(declarations)
|
|
33
|
+
const errors: ImportError[] = [
|
|
34
|
+
...checkGroupOrdering(categorized),
|
|
35
|
+
...checkAlphabeticalSorting(categorized),
|
|
36
|
+
...checkSpecifiersSorting(categorized),
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
for (const error of errors) {
|
|
40
|
+
context.report({
|
|
41
|
+
node: error.node,
|
|
42
|
+
messageId: error.messageId,
|
|
43
|
+
fix(fixer) {
|
|
44
|
+
const sourceCode = context.sourceCode
|
|
45
|
+
return createFix(fixer, declarations, sourceCode, node.body)
|
|
46
|
+
},
|
|
47
|
+
})
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type {ImportSpecifier} from 'estree'
|
|
2
|
+
import {getSpecifierName} from './getSpecifierName'
|
|
3
|
+
|
|
4
|
+
export function sortSpecifiersText(
|
|
5
|
+
specifiers: ImportSpecifier[],
|
|
6
|
+
sourceCode: {getText: (node: ImportSpecifier) => string},
|
|
7
|
+
): string {
|
|
8
|
+
const sorted = [...specifiers].sort((a, b) => {
|
|
9
|
+
const lowerA = getSpecifierName(a).toLowerCase()
|
|
10
|
+
const lowerB = getSpecifierName(b).toLowerCase()
|
|
11
|
+
return lowerA.localeCompare(lowerB)
|
|
12
|
+
})
|
|
13
|
+
return sorted.map(s => sourceCode.getText(s)).join(', ')
|
|
14
|
+
}
|
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
import type {ImportDeclaration} from 'estree'
|
|
2
|
-
import type {Rule} from 'eslint'
|
|
3
|
-
|
|
4
|
-
export const sortedImports: Rule.RuleModule = {
|
|
5
|
-
meta: {
|
|
6
|
-
docs: {
|
|
7
|
-
description: 'Enforce sorted imports alphabetically',
|
|
8
|
-
recommended: true,
|
|
9
|
-
},
|
|
10
|
-
fixable: 'code',
|
|
11
|
-
messages: {
|
|
12
|
-
sortedImports: 'Imports should be sorted alphabetically',
|
|
13
|
-
},
|
|
14
|
-
schema: [],
|
|
15
|
-
type: 'suggestion',
|
|
16
|
-
},
|
|
17
|
-
create(context) {
|
|
18
|
-
return {
|
|
19
|
-
Program(node) {
|
|
20
|
-
const importDeclarations = node.body.filter(
|
|
21
|
-
(statement): statement is ImportDeclaration =>
|
|
22
|
-
statement.type === 'ImportDeclaration',
|
|
23
|
-
)
|
|
24
|
-
|
|
25
|
-
if (importDeclarations.length <= 1) return
|
|
26
|
-
|
|
27
|
-
const sortedImports = importDeclarations.sort((a, b) => {
|
|
28
|
-
const sourceA = (a.source.value as string).toLowerCase()
|
|
29
|
-
const sourceB = (b.source.value as string).toLowerCase()
|
|
30
|
-
return sourceA.localeCompare(sourceB)
|
|
31
|
-
})
|
|
32
|
-
|
|
33
|
-
const isSorted = importDeclarations.every((decl, index) => decl === sortedImports[index])
|
|
34
|
-
|
|
35
|
-
if (!isSorted) {
|
|
36
|
-
context.report({
|
|
37
|
-
node: importDeclarations[0],
|
|
38
|
-
messageId: 'sortedImports',
|
|
39
|
-
fix(fixer) {
|
|
40
|
-
const sourceCode = context.getSourceCode()
|
|
41
|
-
|
|
42
|
-
// Find all consecutive import statements at the beginning
|
|
43
|
-
const programBody = node.body
|
|
44
|
-
let lastImportIndex = 0
|
|
45
|
-
for (let i = 0; i < programBody.length; i++) {
|
|
46
|
-
if (programBody[i].type === 'ImportDeclaration') {
|
|
47
|
-
lastImportIndex = i
|
|
48
|
-
} else {
|
|
49
|
-
break
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const firstImport = programBody[0] as import('estree').ImportDeclaration
|
|
54
|
-
const lastImport = programBody[lastImportIndex] as import('estree').ImportDeclaration
|
|
55
|
-
|
|
56
|
-
// Get the range from start of first import to end of last import
|
|
57
|
-
const startRange = firstImport.range![0]
|
|
58
|
-
let endRange = lastImport.range![1]
|
|
59
|
-
|
|
60
|
-
// Include any trailing whitespace/newlines after the last import
|
|
61
|
-
const fullText = sourceCode.getText()
|
|
62
|
-
for (let i = endRange; i < fullText.length; i++) {
|
|
63
|
-
const char = fullText[i]
|
|
64
|
-
if (char === '\n' || char === ' ' || char === '\t') {
|
|
65
|
-
endRange++
|
|
66
|
-
} else {
|
|
67
|
-
break
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// Generate the sorted import code
|
|
72
|
-
const sortedCode = sortedImports.map(decl => {
|
|
73
|
-
return sourceCode.getText(decl)
|
|
74
|
-
}).join('\n')
|
|
75
|
-
|
|
76
|
-
return fixer.replaceTextRange([startRange, endRange], sortedCode)
|
|
77
|
-
},
|
|
78
|
-
})
|
|
79
|
-
}
|
|
80
|
-
},
|
|
81
|
-
}
|
|
82
|
-
},
|
|
83
|
-
}
|