Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
epidemian
GitHub Repository: epidemian/eslint-plugin-import
Path: blob/main/src/rules/order.js
829 views
1
'use strict';
2
3
import minimatch from 'minimatch';
4
import importType from '../core/importType';
5
import isStaticRequire from '../core/staticRequire';
6
import docsUrl from '../docsUrl';
7
8
const defaultGroups = ['builtin', 'external', 'parent', 'sibling', 'index'];
9
10
// REPORTING AND FIXING
11
12
function reverse(array) {
13
return array.map(function (v) {
14
return Object.assign({}, v, { rank: -v.rank });
15
}).reverse();
16
}
17
18
function getTokensOrCommentsAfter(sourceCode, node, count) {
19
let currentNodeOrToken = node;
20
const result = [];
21
for (let i = 0; i < count; i++) {
22
currentNodeOrToken = sourceCode.getTokenOrCommentAfter(currentNodeOrToken);
23
if (currentNodeOrToken == null) {
24
break;
25
}
26
result.push(currentNodeOrToken);
27
}
28
return result;
29
}
30
31
function getTokensOrCommentsBefore(sourceCode, node, count) {
32
let currentNodeOrToken = node;
33
const result = [];
34
for (let i = 0; i < count; i++) {
35
currentNodeOrToken = sourceCode.getTokenOrCommentBefore(currentNodeOrToken);
36
if (currentNodeOrToken == null) {
37
break;
38
}
39
result.push(currentNodeOrToken);
40
}
41
return result.reverse();
42
}
43
44
function takeTokensAfterWhile(sourceCode, node, condition) {
45
const tokens = getTokensOrCommentsAfter(sourceCode, node, 100);
46
const result = [];
47
for (let i = 0; i < tokens.length; i++) {
48
if (condition(tokens[i])) {
49
result.push(tokens[i]);
50
} else {
51
break;
52
}
53
}
54
return result;
55
}
56
57
function takeTokensBeforeWhile(sourceCode, node, condition) {
58
const tokens = getTokensOrCommentsBefore(sourceCode, node, 100);
59
const result = [];
60
for (let i = tokens.length - 1; i >= 0; i--) {
61
if (condition(tokens[i])) {
62
result.push(tokens[i]);
63
} else {
64
break;
65
}
66
}
67
return result.reverse();
68
}
69
70
function findOutOfOrder(imported) {
71
if (imported.length === 0) {
72
return [];
73
}
74
let maxSeenRankNode = imported[0];
75
return imported.filter(function (importedModule) {
76
const res = importedModule.rank < maxSeenRankNode.rank;
77
if (maxSeenRankNode.rank < importedModule.rank) {
78
maxSeenRankNode = importedModule;
79
}
80
return res;
81
});
82
}
83
84
function findRootNode(node) {
85
let parent = node;
86
while (parent.parent != null && parent.parent.body == null) {
87
parent = parent.parent;
88
}
89
return parent;
90
}
91
92
function findEndOfLineWithComments(sourceCode, node) {
93
const tokensToEndOfLine = takeTokensAfterWhile(sourceCode, node, commentOnSameLineAs(node));
94
const endOfTokens = tokensToEndOfLine.length > 0
95
? tokensToEndOfLine[tokensToEndOfLine.length - 1].range[1]
96
: node.range[1];
97
let result = endOfTokens;
98
for (let i = endOfTokens; i < sourceCode.text.length; i++) {
99
if (sourceCode.text[i] === '\n') {
100
result = i + 1;
101
break;
102
}
103
if (sourceCode.text[i] !== ' ' && sourceCode.text[i] !== '\t' && sourceCode.text[i] !== '\r') {
104
break;
105
}
106
result = i + 1;
107
}
108
return result;
109
}
110
111
function commentOnSameLineAs(node) {
112
return token => (token.type === 'Block' || token.type === 'Line') &&
113
token.loc.start.line === token.loc.end.line &&
114
token.loc.end.line === node.loc.end.line;
115
}
116
117
function findStartOfLineWithComments(sourceCode, node) {
118
const tokensToEndOfLine = takeTokensBeforeWhile(sourceCode, node, commentOnSameLineAs(node));
119
const startOfTokens = tokensToEndOfLine.length > 0 ? tokensToEndOfLine[0].range[0] : node.range[0];
120
let result = startOfTokens;
121
for (let i = startOfTokens - 1; i > 0; i--) {
122
if (sourceCode.text[i] !== ' ' && sourceCode.text[i] !== '\t') {
123
break;
124
}
125
result = i;
126
}
127
return result;
128
}
129
130
function isPlainRequireModule(node) {
131
if (node.type !== 'VariableDeclaration') {
132
return false;
133
}
134
if (node.declarations.length !== 1) {
135
return false;
136
}
137
const decl = node.declarations[0];
138
const result = decl.id &&
139
(decl.id.type === 'Identifier' || decl.id.type === 'ObjectPattern') &&
140
decl.init != null &&
141
decl.init.type === 'CallExpression' &&
142
decl.init.callee != null &&
143
decl.init.callee.name === 'require' &&
144
decl.init.arguments != null &&
145
decl.init.arguments.length === 1 &&
146
decl.init.arguments[0].type === 'Literal';
147
return result;
148
}
149
150
function isPlainImportModule(node) {
151
return node.type === 'ImportDeclaration' && node.specifiers != null && node.specifiers.length > 0;
152
}
153
154
function isPlainImportEquals(node) {
155
return node.type === 'TSImportEqualsDeclaration' && node.moduleReference.expression;
156
}
157
158
function canCrossNodeWhileReorder(node) {
159
return isPlainRequireModule(node) || isPlainImportModule(node) || isPlainImportEquals(node);
160
}
161
162
function canReorderItems(firstNode, secondNode) {
163
const parent = firstNode.parent;
164
const [firstIndex, secondIndex] = [
165
parent.body.indexOf(firstNode),
166
parent.body.indexOf(secondNode),
167
].sort();
168
const nodesBetween = parent.body.slice(firstIndex, secondIndex + 1);
169
for (const nodeBetween of nodesBetween) {
170
if (!canCrossNodeWhileReorder(nodeBetween)) {
171
return false;
172
}
173
}
174
return true;
175
}
176
177
function fixOutOfOrder(context, firstNode, secondNode, order) {
178
const sourceCode = context.getSourceCode();
179
180
const firstRoot = findRootNode(firstNode.node);
181
const firstRootStart = findStartOfLineWithComments(sourceCode, firstRoot);
182
const firstRootEnd = findEndOfLineWithComments(sourceCode, firstRoot);
183
184
const secondRoot = findRootNode(secondNode.node);
185
const secondRootStart = findStartOfLineWithComments(sourceCode, secondRoot);
186
const secondRootEnd = findEndOfLineWithComments(sourceCode, secondRoot);
187
const canFix = canReorderItems(firstRoot, secondRoot);
188
189
let newCode = sourceCode.text.substring(secondRootStart, secondRootEnd);
190
if (newCode[newCode.length - 1] !== '\n') {
191
newCode = newCode + '\n';
192
}
193
194
const message = `\`${secondNode.displayName}\` import should occur ${order} import of \`${firstNode.displayName}\``;
195
196
if (order === 'before') {
197
context.report({
198
node: secondNode.node,
199
message,
200
fix: canFix && (fixer =>
201
fixer.replaceTextRange(
202
[firstRootStart, secondRootEnd],
203
newCode + sourceCode.text.substring(firstRootStart, secondRootStart),
204
)),
205
});
206
} else if (order === 'after') {
207
context.report({
208
node: secondNode.node,
209
message,
210
fix: canFix && (fixer =>
211
fixer.replaceTextRange(
212
[secondRootStart, firstRootEnd],
213
sourceCode.text.substring(secondRootEnd, firstRootEnd) + newCode,
214
)),
215
});
216
}
217
}
218
219
function reportOutOfOrder(context, imported, outOfOrder, order) {
220
outOfOrder.forEach(function (imp) {
221
const found = imported.find(function hasHigherRank(importedItem) {
222
return importedItem.rank > imp.rank;
223
});
224
fixOutOfOrder(context, found, imp, order);
225
});
226
}
227
228
function makeOutOfOrderReport(context, imported) {
229
const outOfOrder = findOutOfOrder(imported);
230
if (!outOfOrder.length) {
231
return;
232
}
233
// There are things to report. Try to minimize the number of reported errors.
234
const reversedImported = reverse(imported);
235
const reversedOrder = findOutOfOrder(reversedImported);
236
if (reversedOrder.length < outOfOrder.length) {
237
reportOutOfOrder(context, reversedImported, reversedOrder, 'after');
238
return;
239
}
240
reportOutOfOrder(context, imported, outOfOrder, 'before');
241
}
242
243
function getSorter(ascending) {
244
const multiplier = ascending ? 1 : -1;
245
246
return function importsSorter(importA, importB) {
247
let result;
248
249
if (importA < importB) {
250
result = -1;
251
} else if (importA > importB) {
252
result = 1;
253
} else {
254
result = 0;
255
}
256
257
return result * multiplier;
258
};
259
}
260
261
function mutateRanksToAlphabetize(imported, alphabetizeOptions) {
262
const groupedByRanks = imported.reduce(function (acc, importedItem) {
263
if (!Array.isArray(acc[importedItem.rank])) {
264
acc[importedItem.rank] = [];
265
}
266
acc[importedItem.rank].push(importedItem);
267
return acc;
268
}, {});
269
270
const groupRanks = Object.keys(groupedByRanks);
271
272
const sorterFn = getSorter(alphabetizeOptions.order === 'asc');
273
const comparator = alphabetizeOptions.caseInsensitive
274
? (a, b) => sorterFn(String(a.value).toLowerCase(), String(b.value).toLowerCase())
275
: (a, b) => sorterFn(a.value, b.value);
276
277
// sort imports locally within their group
278
groupRanks.forEach(function (groupRank) {
279
groupedByRanks[groupRank].sort(comparator);
280
});
281
282
// assign globally unique rank to each import
283
let newRank = 0;
284
const alphabetizedRanks = groupRanks.sort().reduce(function (acc, groupRank) {
285
groupedByRanks[groupRank].forEach(function (importedItem) {
286
acc[`${importedItem.value}|${importedItem.node.importKind}`] = parseInt(groupRank, 10) + newRank;
287
newRank += 1;
288
});
289
return acc;
290
}, {});
291
292
// mutate the original group-rank with alphabetized-rank
293
imported.forEach(function (importedItem) {
294
importedItem.rank = alphabetizedRanks[`${importedItem.value}|${importedItem.node.importKind}`];
295
});
296
}
297
298
// DETECTING
299
300
function computePathRank(ranks, pathGroups, path, maxPosition) {
301
for (let i = 0, l = pathGroups.length; i < l; i++) {
302
const { pattern, patternOptions, group, position = 1 } = pathGroups[i];
303
if (minimatch(path, pattern, patternOptions || { nocomment: true })) {
304
return ranks[group] + (position / maxPosition);
305
}
306
}
307
}
308
309
function computeRank(context, ranks, importEntry, excludedImportTypes) {
310
let impType;
311
let rank;
312
if (importEntry.type === 'import:object') {
313
impType = 'object';
314
} else if (importEntry.node.importKind === 'type' && ranks.omittedTypes.indexOf('type') === -1) {
315
impType = 'type';
316
} else {
317
impType = importType(importEntry.value, context);
318
}
319
if (!excludedImportTypes.has(impType)) {
320
rank = computePathRank(ranks.groups, ranks.pathGroups, importEntry.value, ranks.maxPosition);
321
}
322
if (typeof rank === 'undefined') {
323
rank = ranks.groups[impType];
324
}
325
if (importEntry.type !== 'import' && !importEntry.type.startsWith('import:')) {
326
rank += 100;
327
}
328
329
return rank;
330
}
331
332
function registerNode(context, importEntry, ranks, imported, excludedImportTypes) {
333
const rank = computeRank(context, ranks, importEntry, excludedImportTypes);
334
if (rank !== -1) {
335
imported.push(Object.assign({}, importEntry, { rank }));
336
}
337
}
338
339
function getRequireBlock(node) {
340
let n = node;
341
// Handle cases like `const baz = require('foo').bar.baz`
342
// and `const foo = require('foo')()`
343
while (
344
(n.parent.type === 'MemberExpression' && n.parent.object === n) ||
345
(n.parent.type === 'CallExpression' && n.parent.callee === n)
346
) {
347
n = n.parent;
348
}
349
if (
350
n.parent.type === 'VariableDeclarator' &&
351
n.parent.parent.type === 'VariableDeclaration' &&
352
n.parent.parent.parent.type === 'Program'
353
) {
354
return n.parent.parent.parent;
355
}
356
}
357
358
const types = ['builtin', 'external', 'internal', 'unknown', 'parent', 'sibling', 'index', 'object', 'type'];
359
360
// Creates an object with type-rank pairs.
361
// Example: { index: 0, sibling: 1, parent: 1, external: 1, builtin: 2, internal: 2 }
362
// Will throw an error if it contains a type that does not exist, or has a duplicate
363
function convertGroupsToRanks(groups) {
364
const rankObject = groups.reduce(function (res, group, index) {
365
if (typeof group === 'string') {
366
group = [group];
367
}
368
group.forEach(function (groupItem) {
369
if (types.indexOf(groupItem) === -1) {
370
throw new Error('Incorrect configuration of the rule: Unknown type `' +
371
JSON.stringify(groupItem) + '`');
372
}
373
if (res[groupItem] !== undefined) {
374
throw new Error('Incorrect configuration of the rule: `' + groupItem + '` is duplicated');
375
}
376
res[groupItem] = index;
377
});
378
return res;
379
}, {});
380
381
const omittedTypes = types.filter(function (type) {
382
return rankObject[type] === undefined;
383
});
384
385
const ranks = omittedTypes.reduce(function (res, type) {
386
res[type] = groups.length;
387
return res;
388
}, rankObject);
389
390
return { groups: ranks, omittedTypes };
391
}
392
393
function convertPathGroupsForRanks(pathGroups) {
394
const after = {};
395
const before = {};
396
397
const transformed = pathGroups.map((pathGroup, index) => {
398
const { group, position: positionString } = pathGroup;
399
let position = 0;
400
if (positionString === 'after') {
401
if (!after[group]) {
402
after[group] = 1;
403
}
404
position = after[group]++;
405
} else if (positionString === 'before') {
406
if (!before[group]) {
407
before[group] = [];
408
}
409
before[group].push(index);
410
}
411
412
return Object.assign({}, pathGroup, { position });
413
});
414
415
let maxPosition = 1;
416
417
Object.keys(before).forEach((group) => {
418
const groupLength = before[group].length;
419
before[group].forEach((groupIndex, index) => {
420
transformed[groupIndex].position = -1 * (groupLength - index);
421
});
422
maxPosition = Math.max(maxPosition, groupLength);
423
});
424
425
Object.keys(after).forEach((key) => {
426
const groupNextPosition = after[key];
427
maxPosition = Math.max(maxPosition, groupNextPosition - 1);
428
});
429
430
return {
431
pathGroups: transformed,
432
maxPosition: maxPosition > 10 ? Math.pow(10, Math.ceil(Math.log10(maxPosition))) : 10,
433
};
434
}
435
436
function fixNewLineAfterImport(context, previousImport) {
437
const prevRoot = findRootNode(previousImport.node);
438
const tokensToEndOfLine = takeTokensAfterWhile(
439
context.getSourceCode(), prevRoot, commentOnSameLineAs(prevRoot));
440
441
let endOfLine = prevRoot.range[1];
442
if (tokensToEndOfLine.length > 0) {
443
endOfLine = tokensToEndOfLine[tokensToEndOfLine.length - 1].range[1];
444
}
445
return (fixer) => fixer.insertTextAfterRange([prevRoot.range[0], endOfLine], '\n');
446
}
447
448
function removeNewLineAfterImport(context, currentImport, previousImport) {
449
const sourceCode = context.getSourceCode();
450
const prevRoot = findRootNode(previousImport.node);
451
const currRoot = findRootNode(currentImport.node);
452
const rangeToRemove = [
453
findEndOfLineWithComments(sourceCode, prevRoot),
454
findStartOfLineWithComments(sourceCode, currRoot),
455
];
456
if (/^\s*$/.test(sourceCode.text.substring(rangeToRemove[0], rangeToRemove[1]))) {
457
return (fixer) => fixer.removeRange(rangeToRemove);
458
}
459
return undefined;
460
}
461
462
function makeNewlinesBetweenReport(context, imported, newlinesBetweenImports) {
463
const getNumberOfEmptyLinesBetween = (currentImport, previousImport) => {
464
const linesBetweenImports = context.getSourceCode().lines.slice(
465
previousImport.node.loc.end.line,
466
currentImport.node.loc.start.line - 1,
467
);
468
469
return linesBetweenImports.filter((line) => !line.trim().length).length;
470
};
471
let previousImport = imported[0];
472
473
imported.slice(1).forEach(function (currentImport) {
474
const emptyLinesBetween = getNumberOfEmptyLinesBetween(currentImport, previousImport);
475
476
if (newlinesBetweenImports === 'always'
477
|| newlinesBetweenImports === 'always-and-inside-groups') {
478
if (currentImport.rank !== previousImport.rank && emptyLinesBetween === 0) {
479
context.report({
480
node: previousImport.node,
481
message: 'There should be at least one empty line between import groups',
482
fix: fixNewLineAfterImport(context, previousImport),
483
});
484
} else if (currentImport.rank === previousImport.rank
485
&& emptyLinesBetween > 0
486
&& newlinesBetweenImports !== 'always-and-inside-groups') {
487
context.report({
488
node: previousImport.node,
489
message: 'There should be no empty line within import group',
490
fix: removeNewLineAfterImport(context, currentImport, previousImport),
491
});
492
}
493
} else if (emptyLinesBetween > 0) {
494
context.report({
495
node: previousImport.node,
496
message: 'There should be no empty line between import groups',
497
fix: removeNewLineAfterImport(context, currentImport, previousImport),
498
});
499
}
500
501
previousImport = currentImport;
502
});
503
}
504
505
function getAlphabetizeConfig(options) {
506
const alphabetize = options.alphabetize || {};
507
const order = alphabetize.order || 'ignore';
508
const caseInsensitive = alphabetize.caseInsensitive || false;
509
510
return { order, caseInsensitive };
511
}
512
513
module.exports = {
514
meta: {
515
type: 'suggestion',
516
docs: {
517
url: docsUrl('order'),
518
},
519
520
fixable: 'code',
521
schema: [
522
{
523
type: 'object',
524
properties: {
525
groups: {
526
type: 'array',
527
},
528
pathGroupsExcludedImportTypes: {
529
type: 'array',
530
},
531
pathGroups: {
532
type: 'array',
533
items: {
534
type: 'object',
535
properties: {
536
pattern: {
537
type: 'string',
538
},
539
patternOptions: {
540
type: 'object',
541
},
542
group: {
543
type: 'string',
544
enum: types,
545
},
546
position: {
547
type: 'string',
548
enum: ['after', 'before'],
549
},
550
},
551
required: ['pattern', 'group'],
552
},
553
},
554
'newlines-between': {
555
enum: [
556
'ignore',
557
'always',
558
'always-and-inside-groups',
559
'never',
560
],
561
},
562
alphabetize: {
563
type: 'object',
564
properties: {
565
caseInsensitive: {
566
type: 'boolean',
567
default: false,
568
},
569
order: {
570
enum: ['ignore', 'asc', 'desc'],
571
default: 'ignore',
572
},
573
},
574
additionalProperties: false,
575
},
576
warnOnUnassignedImports: {
577
type: 'boolean',
578
default: false,
579
},
580
},
581
additionalProperties: false,
582
},
583
],
584
},
585
586
create: function importOrderRule(context) {
587
const options = context.options[0] || {};
588
const newlinesBetweenImports = options['newlines-between'] || 'ignore';
589
const pathGroupsExcludedImportTypes = new Set(options['pathGroupsExcludedImportTypes'] || ['builtin', 'external', 'object']);
590
const alphabetize = getAlphabetizeConfig(options);
591
let ranks;
592
593
try {
594
const { pathGroups, maxPosition } = convertPathGroupsForRanks(options.pathGroups || []);
595
const { groups, omittedTypes } = convertGroupsToRanks(options.groups || defaultGroups);
596
ranks = {
597
groups,
598
omittedTypes,
599
pathGroups,
600
maxPosition,
601
};
602
} catch (error) {
603
// Malformed configuration
604
return {
605
Program(node) {
606
context.report(node, error.message);
607
},
608
};
609
}
610
const importMap = new Map();
611
612
function getBlockImports(node) {
613
if (!importMap.has(node)) {
614
importMap.set(node, []);
615
}
616
return importMap.get(node);
617
}
618
619
return {
620
ImportDeclaration: function handleImports(node) {
621
// Ignoring unassigned imports unless warnOnUnassignedImports is set
622
if (node.specifiers.length || options.warnOnUnassignedImports) {
623
const name = node.source.value;
624
registerNode(
625
context,
626
{
627
node,
628
value: name,
629
displayName: name,
630
type: 'import',
631
},
632
ranks,
633
getBlockImports(node.parent),
634
pathGroupsExcludedImportTypes,
635
);
636
}
637
},
638
TSImportEqualsDeclaration: function handleImports(node) {
639
let displayName;
640
let value;
641
let type;
642
// skip "export import"s
643
if (node.isExport) {
644
return;
645
}
646
if (node.moduleReference.type === 'TSExternalModuleReference') {
647
value = node.moduleReference.expression.value;
648
displayName = value;
649
type = 'import';
650
} else {
651
value = '';
652
displayName = context.getSourceCode().getText(node.moduleReference);
653
type = 'import:object';
654
}
655
registerNode(
656
context,
657
{
658
node,
659
value,
660
displayName,
661
type,
662
},
663
ranks,
664
getBlockImports(node.parent),
665
pathGroupsExcludedImportTypes,
666
);
667
},
668
CallExpression: function handleRequires(node) {
669
if (!isStaticRequire(node)) {
670
return;
671
}
672
const block = getRequireBlock(node);
673
if (!block) {
674
return;
675
}
676
const name = node.arguments[0].value;
677
registerNode(
678
context,
679
{
680
node,
681
value: name,
682
displayName: name,
683
type: 'require',
684
},
685
ranks,
686
getBlockImports(block),
687
pathGroupsExcludedImportTypes,
688
);
689
},
690
'Program:exit': function reportAndReset() {
691
importMap.forEach((imported) => {
692
if (newlinesBetweenImports !== 'ignore') {
693
makeNewlinesBetweenReport(context, imported, newlinesBetweenImports);
694
}
695
696
if (alphabetize.order !== 'ignore') {
697
mutateRanksToAlphabetize(imported, alphabetize);
698
}
699
700
makeOutOfOrderReport(context, imported);
701
});
702
703
importMap.clear();
704
},
705
};
706
},
707
};
708
709