@mrpalmer/eslint-plugin 1.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/lib/constants.d.ts +2 -0
- package/lib/constants.js +1 -0
- package/lib/index.d.ts +36 -0
- package/lib/index.js +31 -0
- package/lib/meta.d.ts +4 -0
- package/lib/meta.js +17 -0
- package/lib/rules/shorten-paths.d.ts +8 -0
- package/lib/rules/shorten-paths.js +70 -0
- package/lib/rules/sort-imports.d.ts +19 -0
- package/lib/rules/sort-imports.js +636 -0
- package/lib/rules/sort-named.d.ts +8 -0
- package/lib/rules/sort-named.js +436 -0
- package/lib/types.d.ts +12 -0
- package/lib/types.js +1 -0
- package/lib/utils/array.d.ts +6 -0
- package/lib/utils/array.js +23 -0
- package/lib/utils/ast.d.ts +4 -0
- package/lib/utils/ast.js +102 -0
- package/lib/utils/create-rule.d.ts +2 -0
- package/lib/utils/create-rule.js +58 -0
- package/lib/utils/import-path-options.d.ts +6 -0
- package/lib/utils/import-path-options.js +98 -0
- package/lib/utils/import-type.d.ts +3 -0
- package/lib/utils/import-type.js +88 -0
- package/lib/utils/settings.d.ts +3 -0
- package/lib/utils/settings.js +20 -0
- package/lib/utils/shortest-path.d.ts +2 -0
- package/lib/utils/shortest-path.js +45 -0
- package/lib/utils/ts-config.d.ts +16 -0
- package/lib/utils/ts-config.js +54 -0
- package/package.json +31 -0
|
@@ -0,0 +1,636 @@
|
|
|
1
|
+
import { TSESTree } from '@typescript-eslint/utils';
|
|
2
|
+
import { minimatch } from 'minimatch';
|
|
3
|
+
import { reverse, findOutOfOrder, groupBy } from '../utils/array.js';
|
|
4
|
+
import { findRootNode, findEndOfLineWithComments, findStartOfLineWithComments, } from '../utils/ast.js';
|
|
5
|
+
import { createRule } from '../utils/create-rule.js';
|
|
6
|
+
import { getSettings } from '../utils/settings.js';
|
|
7
|
+
import { loadConfig } from '../utils/ts-config.js';
|
|
8
|
+
import { importType, } from '../utils/import-type.js';
|
|
9
|
+
const categories = {
|
|
10
|
+
import: 'import',
|
|
11
|
+
exports: 'exports',
|
|
12
|
+
};
|
|
13
|
+
const defaultGroups = [
|
|
14
|
+
'core',
|
|
15
|
+
'builtin',
|
|
16
|
+
'external',
|
|
17
|
+
'relative',
|
|
18
|
+
'sibling',
|
|
19
|
+
'index',
|
|
20
|
+
];
|
|
21
|
+
// REPORTING AND FIXING
|
|
22
|
+
function isRequireExpression(expr) {
|
|
23
|
+
return (expr != null &&
|
|
24
|
+
expr.type === TSESTree.AST_NODE_TYPES.CallExpression &&
|
|
25
|
+
// eslint-disable-next-line eslint-plugin/no-property-in-node
|
|
26
|
+
'name' in expr.callee &&
|
|
27
|
+
expr.callee.name === 'require' &&
|
|
28
|
+
expr.arguments.length === 1 &&
|
|
29
|
+
expr.arguments[0].type === TSESTree.AST_NODE_TYPES.Literal);
|
|
30
|
+
}
|
|
31
|
+
function isSupportedRequireModule(node) {
|
|
32
|
+
if (node.type !== TSESTree.AST_NODE_TYPES.VariableDeclaration) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
if (node.declarations.length !== 1) {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
const decl = node.declarations[0];
|
|
39
|
+
const isPlainRequire = (decl.id.type === TSESTree.AST_NODE_TYPES.Identifier ||
|
|
40
|
+
decl.id.type === TSESTree.AST_NODE_TYPES.ObjectPattern) &&
|
|
41
|
+
isRequireExpression(decl.init);
|
|
42
|
+
const isRequireWithMemberExpression = (decl.id.type === TSESTree.AST_NODE_TYPES.Identifier ||
|
|
43
|
+
decl.id.type === TSESTree.AST_NODE_TYPES.ObjectPattern) &&
|
|
44
|
+
decl.init != null &&
|
|
45
|
+
decl.init.type === TSESTree.AST_NODE_TYPES.CallExpression &&
|
|
46
|
+
decl.init.callee.type === TSESTree.AST_NODE_TYPES.MemberExpression &&
|
|
47
|
+
isRequireExpression(decl.init.callee.object);
|
|
48
|
+
return isPlainRequire || isRequireWithMemberExpression;
|
|
49
|
+
}
|
|
50
|
+
function isPlainImportModule(node) {
|
|
51
|
+
return node.type === TSESTree.AST_NODE_TYPES.ImportDeclaration;
|
|
52
|
+
}
|
|
53
|
+
function canCrossNodeWhileReorder(node) {
|
|
54
|
+
return isSupportedRequireModule(node) || isPlainImportModule(node);
|
|
55
|
+
}
|
|
56
|
+
function canReorderItems(firstNode, secondNode) {
|
|
57
|
+
const parent = firstNode.parent;
|
|
58
|
+
// eslint-disable-next-line eslint-plugin/no-property-in-node
|
|
59
|
+
if (!parent || !('body' in parent) || !Array.isArray(parent.body)) {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
const body = parent.body;
|
|
63
|
+
const [firstIndex, secondIndex] = [
|
|
64
|
+
body.indexOf(firstNode),
|
|
65
|
+
body.indexOf(secondNode),
|
|
66
|
+
].sort();
|
|
67
|
+
const nodesBetween = parent.body.slice(firstIndex, secondIndex + 1);
|
|
68
|
+
for (const nodeBetween of nodesBetween) {
|
|
69
|
+
if (!canCrossNodeWhileReorder(nodeBetween)) {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
function makeImportDescription(node) {
|
|
76
|
+
if (node.type === 'export') {
|
|
77
|
+
if (node.node.exportKind === 'type') {
|
|
78
|
+
return 'type export';
|
|
79
|
+
}
|
|
80
|
+
return 'export';
|
|
81
|
+
}
|
|
82
|
+
if (node.node.importKind === 'type') {
|
|
83
|
+
return 'type import';
|
|
84
|
+
}
|
|
85
|
+
// @ts-expect-error - flow type
|
|
86
|
+
if (node.node.importKind === 'typeof') {
|
|
87
|
+
return 'typeof import';
|
|
88
|
+
}
|
|
89
|
+
return 'import';
|
|
90
|
+
}
|
|
91
|
+
function fixOutOfOrder(context, firstNode, secondNode, order, category) {
|
|
92
|
+
const isExports = category === categories.exports;
|
|
93
|
+
const { sourceCode } = context;
|
|
94
|
+
const firstRoot = findRootNode(firstNode.node);
|
|
95
|
+
const secondRoot = findRootNode(secondNode.node);
|
|
96
|
+
const firstRootStart = findStartOfLineWithComments(sourceCode, firstRoot);
|
|
97
|
+
const firstRootEnd = findEndOfLineWithComments(sourceCode, firstRoot);
|
|
98
|
+
const secondRootStart = findStartOfLineWithComments(sourceCode, secondRoot);
|
|
99
|
+
const secondRootEnd = findEndOfLineWithComments(sourceCode, secondRoot);
|
|
100
|
+
const firstDesc = makeImportDescription(firstNode);
|
|
101
|
+
const secondDesc = makeImportDescription(secondNode);
|
|
102
|
+
if (firstNode.displayName === secondNode.displayName &&
|
|
103
|
+
firstDesc === secondDesc) {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
const firstImport = `${firstDesc} of \`${firstNode.displayName}\``;
|
|
107
|
+
const secondImport = `\`${secondNode.displayName}\` ${secondDesc}`;
|
|
108
|
+
const messageOptions = {
|
|
109
|
+
messageId: 'order',
|
|
110
|
+
data: { firstImport, secondImport, order },
|
|
111
|
+
};
|
|
112
|
+
const canFix = isExports || canReorderItems(firstRoot, secondRoot);
|
|
113
|
+
let newCode = sourceCode.text.slice(secondRootStart, secondRootEnd);
|
|
114
|
+
if (!newCode.endsWith('\n')) {
|
|
115
|
+
newCode = `${newCode}\n`;
|
|
116
|
+
}
|
|
117
|
+
if (order === 'before') {
|
|
118
|
+
context.report({
|
|
119
|
+
node: secondNode.node,
|
|
120
|
+
...messageOptions,
|
|
121
|
+
fix: canFix
|
|
122
|
+
? (fixer) => fixer.replaceTextRange([firstRootStart, secondRootEnd], newCode + sourceCode.text.slice(firstRootStart, secondRootStart))
|
|
123
|
+
: null,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
context.report({
|
|
128
|
+
node: secondNode.node,
|
|
129
|
+
...messageOptions,
|
|
130
|
+
fix: canFix
|
|
131
|
+
? (fixer) => fixer.replaceTextRange([secondRootStart, firstRootEnd], sourceCode.text.slice(secondRootEnd, firstRootEnd) + newCode)
|
|
132
|
+
: null,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
function reportOutOfOrder(context, imported, outOfOrder, order, category) {
|
|
137
|
+
for (const imp of outOfOrder) {
|
|
138
|
+
fixOutOfOrder(context,
|
|
139
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
140
|
+
imported.find((importedItem) => importedItem.rank > imp.rank), imp, order, category);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
const compareString = (a, b) => {
|
|
144
|
+
if (a < b) {
|
|
145
|
+
return -1;
|
|
146
|
+
}
|
|
147
|
+
if (a > b) {
|
|
148
|
+
return 1;
|
|
149
|
+
}
|
|
150
|
+
return 0;
|
|
151
|
+
};
|
|
152
|
+
/** Some parsers (languages without types) don't provide ImportKind */
|
|
153
|
+
const DEFAULT_IMPORT_KIND = 'value';
|
|
154
|
+
const RELATIVE_DOTS = new Set(['.', '..']);
|
|
155
|
+
function getSorter() {
|
|
156
|
+
return function importsSorter(nodeA, nodeB) {
|
|
157
|
+
let result = 0;
|
|
158
|
+
if (!nodeA.sortableSource.includes('/') &&
|
|
159
|
+
!nodeB.sortableSource.includes('/')) {
|
|
160
|
+
result =
|
|
161
|
+
compareString(nodeA.sortableSource, nodeB.sortableSource) ||
|
|
162
|
+
compareString(nodeA.originalSource, nodeB.originalSource);
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
const A = nodeA.sortableSource.split('/');
|
|
166
|
+
const OA = nodeA.originalSource.split('/');
|
|
167
|
+
const B = nodeB.sortableSource.split('/');
|
|
168
|
+
const OB = nodeB.originalSource.split('/');
|
|
169
|
+
const a = A.length;
|
|
170
|
+
const b = B.length;
|
|
171
|
+
for (let i = 0; i < Math.min(a, b); i++) {
|
|
172
|
+
// Skip comparing the first path segment, if they are relative segments for both imports
|
|
173
|
+
const x = A[i];
|
|
174
|
+
const y = B[i];
|
|
175
|
+
if (i === 0 && RELATIVE_DOTS.has(x) && RELATIVE_DOTS.has(y)) {
|
|
176
|
+
// If one is sibling and the other parent import, no need to compare at all, since the paths belong in different groups
|
|
177
|
+
if (x !== y) {
|
|
178
|
+
break;
|
|
179
|
+
}
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
result = compareString(x, y) || compareString(OA[i], OB[i]);
|
|
183
|
+
if (result) {
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
if (!result && a !== b) {
|
|
188
|
+
result = a < b ? -1 : 1;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
// In case the paths are equal (result === 0), sort them by importKind
|
|
192
|
+
result ||= compareString(nodeA.node.importKind ?? DEFAULT_IMPORT_KIND, nodeB.node.importKind ?? DEFAULT_IMPORT_KIND);
|
|
193
|
+
return result;
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
function mutateRanksToAlphabetize(imported) {
|
|
197
|
+
const groupedByRanks = groupBy(imported, (item) => item.rank);
|
|
198
|
+
const sorterFn = getSorter();
|
|
199
|
+
// sort group keys so that they can be iterated on in order
|
|
200
|
+
const groupRanks = Object.keys(groupedByRanks).sort((a, b) => +a - +b);
|
|
201
|
+
// sort imports locally within their group
|
|
202
|
+
for (const groupRank of groupRanks) {
|
|
203
|
+
if (groupRank === '0') {
|
|
204
|
+
// don't sort side-effect imports
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
groupedByRanks[groupRank].sort(sorterFn);
|
|
208
|
+
}
|
|
209
|
+
// assign globally unique rank to each import
|
|
210
|
+
let newRank = 0;
|
|
211
|
+
const alphabetizedRanks = groupRanks.reduce((acc, groupRank) => {
|
|
212
|
+
for (const importedItem of groupedByRanks[groupRank]) {
|
|
213
|
+
acc[`${importedItem.value}|${importedItem.node.importKind}`] =
|
|
214
|
+
Number.parseInt(groupRank, 10) + newRank;
|
|
215
|
+
newRank += 1;
|
|
216
|
+
}
|
|
217
|
+
return acc;
|
|
218
|
+
}, {});
|
|
219
|
+
// mutate the original group-rank with alphabetized-rank
|
|
220
|
+
for (const importedItem of imported) {
|
|
221
|
+
importedItem.rank =
|
|
222
|
+
alphabetizedRanks[`${importedItem.value}|${importedItem.node.importKind}`];
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
function makeOutOfOrderReport(context, imported, category) {
|
|
226
|
+
const outOfOrder = findOutOfOrder(imported);
|
|
227
|
+
if (outOfOrder.length === 0) {
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
// There are things to report. Try to minimize the number of reported errors.
|
|
231
|
+
const reversedImported = reverse(imported);
|
|
232
|
+
const reversedOrder = findOutOfOrder(reversedImported);
|
|
233
|
+
if (reversedOrder.length < outOfOrder.length) {
|
|
234
|
+
reportOutOfOrder(context, reversedImported, reversedOrder, 'after', category);
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
reportOutOfOrder(context, imported, outOfOrder, 'before', category);
|
|
238
|
+
}
|
|
239
|
+
// DETECTING
|
|
240
|
+
function computePathRank(ranks, pathGroups, path, maxPosition, isSideEffectOnly) {
|
|
241
|
+
for (const { pattern, patternOptions, group, position = 1, isSideEffect, } of pathGroups) {
|
|
242
|
+
if (minimatch(path, pattern, patternOptions ?? { nocomment: true }) &&
|
|
243
|
+
(isSideEffect == null || isSideEffect === isSideEffectOnly)) {
|
|
244
|
+
return ranks[group] + position / maxPosition;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return undefined;
|
|
248
|
+
}
|
|
249
|
+
function computeRank(settings, ranks, importEntry, isSideEffectOnly = false) {
|
|
250
|
+
let rank = typeof importEntry.value === 'string'
|
|
251
|
+
? computePathRank(ranks.groups, ranks.pathGroups, importEntry.value, ranks.maxPosition, isSideEffectOnly)
|
|
252
|
+
: undefined;
|
|
253
|
+
if (rank === undefined) {
|
|
254
|
+
if (isSideEffectOnly) {
|
|
255
|
+
return ranks.groups.sideEffect;
|
|
256
|
+
}
|
|
257
|
+
const impType = importType(importEntry.value, settings);
|
|
258
|
+
rank = ranks.groups[impType];
|
|
259
|
+
if (rank === undefined &&
|
|
260
|
+
ranks.groups.core &&
|
|
261
|
+
impType.startsWith('core:')) {
|
|
262
|
+
rank = ranks.groups.core;
|
|
263
|
+
}
|
|
264
|
+
if (rank === undefined) {
|
|
265
|
+
return -1;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
if (importEntry.type !== 'import') {
|
|
269
|
+
rank += 100;
|
|
270
|
+
}
|
|
271
|
+
return rank;
|
|
272
|
+
}
|
|
273
|
+
function registerNode(context, settings, importEntry, ranks, imported) {
|
|
274
|
+
const isSideEffectOnly = isSideEffectImport(importEntry.node, context.sourceCode);
|
|
275
|
+
const rank = computeRank(settings, ranks, importEntry, isSideEffectOnly);
|
|
276
|
+
if (rank !== -1) {
|
|
277
|
+
let importNode = importEntry.node;
|
|
278
|
+
if (importEntry.type === 'require' &&
|
|
279
|
+
importNode.parent?.parent?.type ===
|
|
280
|
+
TSESTree.AST_NODE_TYPES.VariableDeclaration) {
|
|
281
|
+
importNode = importNode.parent.parent;
|
|
282
|
+
}
|
|
283
|
+
imported.push({
|
|
284
|
+
...importEntry,
|
|
285
|
+
rank,
|
|
286
|
+
index: imported.length,
|
|
287
|
+
sortableSource: transformSource(importEntry.value),
|
|
288
|
+
originalSource: String(importEntry.value),
|
|
289
|
+
isSideEffectOnly,
|
|
290
|
+
isMultiline: importNode.loc.end.line !== importNode.loc.start.line,
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
function isSideEffectImport(node, sourceCode) {
|
|
295
|
+
if (node.type !== TSESTree.AST_NODE_TYPES.ImportDeclaration) {
|
|
296
|
+
return false;
|
|
297
|
+
}
|
|
298
|
+
return (node.specifiers.length === 0 &&
|
|
299
|
+
node.importKind !== 'type' &&
|
|
300
|
+
!isPunctuator(sourceCode.getFirstToken(node, { skip: 1 }), '{'));
|
|
301
|
+
}
|
|
302
|
+
function isPunctuator(token, value) {
|
|
303
|
+
return (token &&
|
|
304
|
+
token.type === TSESTree.AST_TOKEN_TYPES.Punctuator &&
|
|
305
|
+
token.value === value);
|
|
306
|
+
}
|
|
307
|
+
function getRequireBlock(node) {
|
|
308
|
+
let n = node;
|
|
309
|
+
// Handle cases like `const baz = require('foo').bar.baz`
|
|
310
|
+
// and `const foo = require('foo')()`
|
|
311
|
+
while ((n.parent?.type === TSESTree.AST_NODE_TYPES.MemberExpression &&
|
|
312
|
+
n.parent.object === n) ||
|
|
313
|
+
(n.parent?.type === TSESTree.AST_NODE_TYPES.CallExpression &&
|
|
314
|
+
n.parent.callee === n)) {
|
|
315
|
+
n = n.parent;
|
|
316
|
+
}
|
|
317
|
+
if (n.parent?.type === TSESTree.AST_NODE_TYPES.VariableDeclarator &&
|
|
318
|
+
n.parent.parent.parent.type === TSESTree.AST_NODE_TYPES.Program) {
|
|
319
|
+
return n.parent.parent.parent;
|
|
320
|
+
}
|
|
321
|
+
return undefined;
|
|
322
|
+
}
|
|
323
|
+
const baseTypes = [
|
|
324
|
+
'sideEffect',
|
|
325
|
+
'core',
|
|
326
|
+
'builtin',
|
|
327
|
+
'external',
|
|
328
|
+
'internal',
|
|
329
|
+
'unknown',
|
|
330
|
+
'relative',
|
|
331
|
+
'sibling',
|
|
332
|
+
'index',
|
|
333
|
+
];
|
|
334
|
+
function transformSource(source) {
|
|
335
|
+
return (String(source)
|
|
336
|
+
.toLowerCase()
|
|
337
|
+
// Treat `.` as `./`, `..` as `../`, `../..` as `../../` etc.
|
|
338
|
+
.replace(/^[./]*\.$/, '$&/')
|
|
339
|
+
// Make `../` sort after `../../` but before `../a` etc.
|
|
340
|
+
// Why a comma? See the next comment.
|
|
341
|
+
.replace(/^[./]*\/$/, '$&,')
|
|
342
|
+
// Make `.` and `/` sort before any other punctuation.
|
|
343
|
+
// The default order is: _ - , x x x . x x x / x x x
|
|
344
|
+
// We’re changing it to: . / , x x x _ x x x - x x x
|
|
345
|
+
.replace(/[._-]/g, (char) => {
|
|
346
|
+
switch (char) {
|
|
347
|
+
case '.':
|
|
348
|
+
return '_';
|
|
349
|
+
case '_':
|
|
350
|
+
return '.';
|
|
351
|
+
case '-':
|
|
352
|
+
return '/';
|
|
353
|
+
default:
|
|
354
|
+
return char;
|
|
355
|
+
}
|
|
356
|
+
}));
|
|
357
|
+
}
|
|
358
|
+
// Creates an object with type-rank pairs.
|
|
359
|
+
// Example: { index: 0, sibling: 1, parent: 1, external: 1, builtin: 2, internal: 2 }
|
|
360
|
+
// Will throw an error if it contains a type that does not exist, or has a duplicate
|
|
361
|
+
function convertGroupsToRanks(groups, types, coreModules) {
|
|
362
|
+
const coreIndex = groups.indexOf('core');
|
|
363
|
+
if (coreIndex !== -1 && coreModules) {
|
|
364
|
+
groups = groups.toSpliced(coreIndex + 1, 0, ...coreModules.map((module) => `core:${module}`));
|
|
365
|
+
}
|
|
366
|
+
const rankObject = groups.reduce((res, group, index) => {
|
|
367
|
+
for (const groupItem of [group].flat()) {
|
|
368
|
+
if (!types.has(groupItem) && !groupItem.startsWith('core:')) {
|
|
369
|
+
throw new Error(`Incorrect configuration of the rule: Unknown type \`${JSON.stringify(groupItem)}\``);
|
|
370
|
+
}
|
|
371
|
+
if (res[groupItem] !== undefined) {
|
|
372
|
+
throw new Error(`Incorrect configuration of the rule: \`${groupItem}\` is duplicated`);
|
|
373
|
+
}
|
|
374
|
+
res[groupItem] = index * 2;
|
|
375
|
+
}
|
|
376
|
+
return res;
|
|
377
|
+
}, {});
|
|
378
|
+
const omittedTypes = new Set(types);
|
|
379
|
+
Object.keys(rankObject)
|
|
380
|
+
.filter((key) => types.has(key))
|
|
381
|
+
.forEach((key) => {
|
|
382
|
+
omittedTypes.delete(key);
|
|
383
|
+
});
|
|
384
|
+
if (coreIndex === -1) {
|
|
385
|
+
omittedTypes.add('core');
|
|
386
|
+
}
|
|
387
|
+
const ranks = [...omittedTypes].reduce((res, type) => {
|
|
388
|
+
res[type] = groups.length * 2;
|
|
389
|
+
return res;
|
|
390
|
+
}, rankObject);
|
|
391
|
+
assertAllTypesRanked(ranks);
|
|
392
|
+
return { groups: ranks, omittedTypes: [...omittedTypes] };
|
|
393
|
+
}
|
|
394
|
+
function assertAllTypesRanked(ranks) {
|
|
395
|
+
const expectedTypes = new Set(baseTypes);
|
|
396
|
+
expectedTypes.delete('core');
|
|
397
|
+
for (const type of expectedTypes) {
|
|
398
|
+
if (ranks[type] === undefined) {
|
|
399
|
+
throw new Error(`Incorrect configuration of the rule: \`${type}\` is missing`);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
function convertPathGroupsForRanks(pathGroups, tsconfigPathAliases) {
|
|
404
|
+
const after = {};
|
|
405
|
+
const before = {};
|
|
406
|
+
const tsconfigPathGroups = tsconfigPathAliases.map((alias) => ({
|
|
407
|
+
pattern: alias.replace(/\/\*$/, '/**/*'),
|
|
408
|
+
group: 'internal',
|
|
409
|
+
}));
|
|
410
|
+
const transformed = [...pathGroups, ...tsconfigPathGroups].map((pathGroup, index) => {
|
|
411
|
+
const { group, position: positionString } = pathGroup;
|
|
412
|
+
let position = 0;
|
|
413
|
+
if (positionString === 'after') {
|
|
414
|
+
after[group] ||= 1;
|
|
415
|
+
position = after[group]++;
|
|
416
|
+
}
|
|
417
|
+
else if (positionString === 'before') {
|
|
418
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
419
|
+
before[group] ||= [];
|
|
420
|
+
before[group].push(index);
|
|
421
|
+
}
|
|
422
|
+
return { ...pathGroup, position };
|
|
423
|
+
});
|
|
424
|
+
let maxPosition = 1;
|
|
425
|
+
for (const group of Object.keys(before)) {
|
|
426
|
+
const groupLength = before[group].length;
|
|
427
|
+
for (const [index, groupIndex] of before[group].entries()) {
|
|
428
|
+
transformed[groupIndex].position = -1 * (groupLength - index);
|
|
429
|
+
}
|
|
430
|
+
maxPosition = Math.max(maxPosition, groupLength);
|
|
431
|
+
}
|
|
432
|
+
for (const key of Object.keys(after)) {
|
|
433
|
+
const groupNextPosition = after[key];
|
|
434
|
+
maxPosition = Math.max(maxPosition, groupNextPosition - 1);
|
|
435
|
+
}
|
|
436
|
+
return {
|
|
437
|
+
pathGroups: transformed,
|
|
438
|
+
maxPosition: maxPosition > 10 ? 10 ** Math.ceil(Math.log10(maxPosition)) : 10,
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
function removeNewLineAfterImport(context, currentImport, previousImport) {
|
|
442
|
+
const { sourceCode } = context;
|
|
443
|
+
const prevRoot = findRootNode(previousImport.node);
|
|
444
|
+
const currRoot = findRootNode(currentImport.node);
|
|
445
|
+
const rangeToRemove = [
|
|
446
|
+
findEndOfLineWithComments(sourceCode, prevRoot),
|
|
447
|
+
findStartOfLineWithComments(sourceCode, currRoot),
|
|
448
|
+
];
|
|
449
|
+
if (/^\s*$/.test(sourceCode.text.slice(rangeToRemove[0], rangeToRemove[1]))) {
|
|
450
|
+
return (fixer) => fixer.removeRange(rangeToRemove);
|
|
451
|
+
}
|
|
452
|
+
return undefined;
|
|
453
|
+
}
|
|
454
|
+
function makeNewlinesBetweenReport(context, imported) {
|
|
455
|
+
const getNumberOfEmptyLinesBetween = (currentImport, previousImport) => {
|
|
456
|
+
return context.sourceCode.lines
|
|
457
|
+
.slice(previousImport.node.loc.end.line, currentImport.node.loc.start.line - 1)
|
|
458
|
+
.filter((line) => line.trim().length === 0).length;
|
|
459
|
+
};
|
|
460
|
+
let previousImport = imported[0];
|
|
461
|
+
for (const currentImport of imported.slice(1)) {
|
|
462
|
+
const emptyLinesBetween = getNumberOfEmptyLinesBetween(currentImport, previousImport);
|
|
463
|
+
if (emptyLinesBetween > 0) {
|
|
464
|
+
context.report({
|
|
465
|
+
node: previousImport.node,
|
|
466
|
+
messageId: 'noLineBetweenImports',
|
|
467
|
+
fix: removeNewLineAfterImport(context, currentImport, previousImport),
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
previousImport = currentImport;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
export default createRule({
|
|
474
|
+
meta: {
|
|
475
|
+
type: 'suggestion',
|
|
476
|
+
docs: {
|
|
477
|
+
description: 'Ensure imports are sorted consistently.',
|
|
478
|
+
},
|
|
479
|
+
fixable: 'code',
|
|
480
|
+
schema: [
|
|
481
|
+
{
|
|
482
|
+
type: 'object',
|
|
483
|
+
properties: {
|
|
484
|
+
groups: {
|
|
485
|
+
type: 'array',
|
|
486
|
+
},
|
|
487
|
+
pathGroups: {
|
|
488
|
+
type: 'array',
|
|
489
|
+
items: {
|
|
490
|
+
type: 'object',
|
|
491
|
+
properties: {
|
|
492
|
+
pattern: {
|
|
493
|
+
type: 'string',
|
|
494
|
+
},
|
|
495
|
+
patternOptions: {
|
|
496
|
+
type: 'object',
|
|
497
|
+
},
|
|
498
|
+
group: {
|
|
499
|
+
type: 'string',
|
|
500
|
+
// enum: baseTypes,
|
|
501
|
+
},
|
|
502
|
+
position: {
|
|
503
|
+
type: 'string',
|
|
504
|
+
enum: ['after', 'before'],
|
|
505
|
+
},
|
|
506
|
+
isSideEffect: {
|
|
507
|
+
type: 'boolean',
|
|
508
|
+
},
|
|
509
|
+
},
|
|
510
|
+
additionalProperties: false,
|
|
511
|
+
required: ['pattern', 'group'],
|
|
512
|
+
},
|
|
513
|
+
},
|
|
514
|
+
},
|
|
515
|
+
additionalProperties: false,
|
|
516
|
+
},
|
|
517
|
+
],
|
|
518
|
+
messages: {
|
|
519
|
+
error: '{{error}}',
|
|
520
|
+
noLineBetweenImports: 'There should be no empty line between import statements',
|
|
521
|
+
order: '{{secondImport}} should occur {{order}} {{firstImport}}',
|
|
522
|
+
},
|
|
523
|
+
},
|
|
524
|
+
defaultOptions: [
|
|
525
|
+
{
|
|
526
|
+
groups: defaultGroups,
|
|
527
|
+
pathGroups: [],
|
|
528
|
+
},
|
|
529
|
+
],
|
|
530
|
+
create(context, [options]) {
|
|
531
|
+
const settings = getSettings(context);
|
|
532
|
+
const tsConfigPaths = loadConfig(settings)?.config.compilerOptions?.paths ?? {};
|
|
533
|
+
let ranks;
|
|
534
|
+
try {
|
|
535
|
+
const { pathGroups, maxPosition } = convertPathGroupsForRanks(options.pathGroups, Object.keys(tsConfigPaths));
|
|
536
|
+
const types = new Set(baseTypes);
|
|
537
|
+
pathGroups.forEach((group) => {
|
|
538
|
+
types.add(group.group);
|
|
539
|
+
});
|
|
540
|
+
const { groups, omittedTypes } = convertGroupsToRanks(['sideEffect', ...options.groups], types, settings['mrpalmer/coreModules']);
|
|
541
|
+
ranks = {
|
|
542
|
+
groups,
|
|
543
|
+
omittedTypes,
|
|
544
|
+
pathGroups,
|
|
545
|
+
maxPosition,
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
catch (error) {
|
|
549
|
+
// Malformed configuration
|
|
550
|
+
return {
|
|
551
|
+
Program(node) {
|
|
552
|
+
context.report({
|
|
553
|
+
node,
|
|
554
|
+
messageId: 'error',
|
|
555
|
+
data: {
|
|
556
|
+
error: getErrorMessage(error) ?? 'Invalid configuration',
|
|
557
|
+
},
|
|
558
|
+
});
|
|
559
|
+
},
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
const importMap = new Map();
|
|
563
|
+
const exportMap = new Map();
|
|
564
|
+
function getBlockImports(node) {
|
|
565
|
+
let blockImports = importMap.get(node);
|
|
566
|
+
if (!blockImports) {
|
|
567
|
+
importMap.set(node, (blockImports = []));
|
|
568
|
+
}
|
|
569
|
+
return blockImports;
|
|
570
|
+
}
|
|
571
|
+
return {
|
|
572
|
+
ImportDeclaration(node) {
|
|
573
|
+
const name = node.source.value;
|
|
574
|
+
registerNode(context, settings, {
|
|
575
|
+
node,
|
|
576
|
+
value: name,
|
|
577
|
+
displayName: name,
|
|
578
|
+
type: 'import',
|
|
579
|
+
}, ranks, getBlockImports(node.parent));
|
|
580
|
+
},
|
|
581
|
+
CallExpression(node) {
|
|
582
|
+
if (!isStaticRequire(node)) {
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
const block = getRequireBlock(node);
|
|
586
|
+
const firstArg = node.arguments[0];
|
|
587
|
+
// eslint-disable-next-line eslint-plugin/no-property-in-node
|
|
588
|
+
if (!block || !('value' in firstArg)) {
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
const { value } = firstArg;
|
|
592
|
+
registerNode(context, settings, {
|
|
593
|
+
node,
|
|
594
|
+
value,
|
|
595
|
+
displayName: value,
|
|
596
|
+
type: 'require',
|
|
597
|
+
}, ranks, getBlockImports(block));
|
|
598
|
+
},
|
|
599
|
+
'Program:exit'() {
|
|
600
|
+
for (const imported of importMap.values()) {
|
|
601
|
+
makeNewlinesBetweenReport(context, imported);
|
|
602
|
+
mutateRanksToAlphabetize(imported);
|
|
603
|
+
makeOutOfOrderReport(context, imported, categories.import);
|
|
604
|
+
}
|
|
605
|
+
for (const exported of exportMap.values()) {
|
|
606
|
+
mutateRanksToAlphabetize(exported);
|
|
607
|
+
makeOutOfOrderReport(context, exported, categories.exports);
|
|
608
|
+
}
|
|
609
|
+
importMap.clear();
|
|
610
|
+
exportMap.clear();
|
|
611
|
+
},
|
|
612
|
+
};
|
|
613
|
+
},
|
|
614
|
+
});
|
|
615
|
+
function getErrorMessage(error) {
|
|
616
|
+
if (typeof error === 'string') {
|
|
617
|
+
return error;
|
|
618
|
+
}
|
|
619
|
+
if (error instanceof Error) {
|
|
620
|
+
return error.message;
|
|
621
|
+
}
|
|
622
|
+
if (typeof error === 'object' &&
|
|
623
|
+
error &&
|
|
624
|
+
'message' in error &&
|
|
625
|
+
typeof error.message === 'string') {
|
|
626
|
+
return error.message;
|
|
627
|
+
}
|
|
628
|
+
return undefined;
|
|
629
|
+
}
|
|
630
|
+
function isStaticRequire(node) {
|
|
631
|
+
return (node.callee.type === TSESTree.AST_NODE_TYPES.Identifier &&
|
|
632
|
+
node.callee.name === 'require' &&
|
|
633
|
+
node.arguments.length === 1 &&
|
|
634
|
+
node.arguments[0].type === TSESTree.AST_NODE_TYPES.Literal &&
|
|
635
|
+
typeof node.arguments[0].value === 'string');
|
|
636
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type TSESLint } from '@typescript-eslint/utils';
|
|
2
|
+
export interface Options {
|
|
3
|
+
ignoreCase?: boolean;
|
|
4
|
+
types?: NamedTypes;
|
|
5
|
+
}
|
|
6
|
+
declare const _default: TSESLint.RuleModule<"order", [Options], unknown, TSESLint.RuleListener>;
|
|
7
|
+
export default _default;
|
|
8
|
+
type NamedTypes = 'mixed' | 'types-first' | 'types-last';
|