eslint-plugin-imports-regulation 0.2.16 → 0.2.17
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.
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { TSESLint } from '@typescript-eslint/utils';
|
|
2
|
-
type MessageIds = 'groupOrder' | 'exportBlock' | 'blankLinesBetween' | 'unexpectedBlankLine' | 'blankLineBelowComment' | 'typeSpecifiersLast' | 'typeSpecifiersFirst' | 'markTypeOnly' | 'typeOnlyImport' | 'typeOnlySpecifier' | 'blankLinesAfter' | 'multilineBraces' | 'codeAmongImports' | '
|
|
2
|
+
type MessageIds = 'groupOrder' | 'exportBlock' | 'blankLinesBetween' | 'unexpectedBlankLine' | 'blankLineBelowComment' | 'typeSpecifiersLast' | 'typeSpecifiersFirst' | 'markTypeOnly' | 'typeOnlyImport' | 'typeOnlySpecifier' | 'blankLinesAfter' | 'multilineBraces' | 'codeAmongImports' | 'namesFitOneLine' | 'nameLineTooLong' | 'nameFitsAbove' | 'exportsBlockGap' | 'exportsBlockSpacing';
|
|
3
3
|
type Origin = 'external' | 'internal';
|
|
4
4
|
type Options = [
|
|
5
5
|
{
|
|
@@ -82,7 +82,9 @@ const rule = {
|
|
|
82
82
|
typeOnlySpecifier: '`{{name}}` is only used as a type — mark it `type`.',
|
|
83
83
|
blankLinesAfter: 'Expected {{expected}} blank {{lines}} after the imports, found {{actual}}.',
|
|
84
84
|
codeAmongImports: 'Expected no code among the imports.',
|
|
85
|
-
|
|
85
|
+
namesFitOneLine: 'This import fits on one line.',
|
|
86
|
+
nameLineTooLong: 'This name runs the line past {{maxLen}} characters.',
|
|
87
|
+
nameFitsAbove: 'This name fits on the line above.',
|
|
86
88
|
exportsBlockGap: 'Expected at most {{expected}} blank {{lines}} before the exports block, found {{actual}}.',
|
|
87
89
|
exportsBlockSpacing: 'Expected at most {{expected}} blank {{lines}} between the exports, found {{actual}}.',
|
|
88
90
|
multilineBraces: 'Expected a multiline {{kind}} to end a line with `{` and start one with `}{{tail}}`.',
|
|
@@ -219,14 +221,23 @@ const rule = {
|
|
|
219
221
|
const interior = source.text.slice(range[0], range[1]);
|
|
220
222
|
const { carried, tail } = commentsInBraces(named, range);
|
|
221
223
|
const pieceOf = (specifier) => carried.get(specifier) ?? { leading: [], beforeComma: [], afterComma: [] };
|
|
222
|
-
|
|
224
|
+
// The names' own lines, not the braces': names on one line stay on one line, whether or
|
|
225
|
+
// not the braces sit on lines of their own.
|
|
226
|
+
const first = named[0];
|
|
227
|
+
const last = named[named.length - 1];
|
|
228
|
+
const oneLine = first !== undefined && last !== undefined && first.loc.start.line === last.loc.end.line;
|
|
229
|
+
if (oneLine) {
|
|
223
230
|
// Only block comments fit on one line, so a trailing one can always sit before the comma.
|
|
224
|
-
const pad = /^\s/.test(interior) ? ' ' : '';
|
|
231
|
+
const pad = BREAK.test(interior) ? '' : /^\s/.test(interior) ? ' ' : '';
|
|
225
232
|
const parts = ordered.map(specifier => {
|
|
226
233
|
const { leading, beforeComma, afterComma } = pieceOf(specifier);
|
|
227
234
|
return [...leading, textOf(specifier), ...beforeComma, ...afterComma].join(' ');
|
|
228
235
|
});
|
|
229
|
-
|
|
236
|
+
const names = `${parts.join(', ')}${tail.map(comment => ` ${comment}`).join('')}`;
|
|
237
|
+
if (!BREAK.test(interior))
|
|
238
|
+
return `${pad}${names}${pad}`;
|
|
239
|
+
const { indent, closeIndent, trailingComma } = shapeOf(range, named);
|
|
240
|
+
return `${eol}${indent}${names}${trailingComma ? ',' : ''}${eol}${closeIndent}`;
|
|
230
241
|
}
|
|
231
242
|
const { indent, closeIndent, trailingComma } = shapeOf(range, named);
|
|
232
243
|
const lines = [];
|
|
@@ -409,56 +420,138 @@ const rule = {
|
|
|
409
420
|
/** Whitespace runs closed up, so a statement spread over lines can be measured as one. */
|
|
410
421
|
const flattened = (text) => text.replace(/\s*(?:\r\n|\n|\r)\s*/g, ' ');
|
|
411
422
|
/**
|
|
412
|
-
* The
|
|
423
|
+
* The line length, as three faults with a fix each: a statement spread over lines that would
|
|
424
|
+
* fit on one, a line of names past the limit, and a name that would fit on the line above.
|
|
413
425
|
*
|
|
414
|
-
*
|
|
415
|
-
* and
|
|
426
|
+
* Each fix moves one break, leaving everything else as written — the order, the trailing
|
|
427
|
+
* comma and the spacing are other checks' business.
|
|
416
428
|
*/
|
|
417
|
-
const
|
|
429
|
+
const checkLineLength = (node, specifiers, open, close) => {
|
|
418
430
|
if (specifiers.length === 0)
|
|
419
431
|
return false;
|
|
420
432
|
const interior = [open.range[1], close.range[0]];
|
|
421
|
-
//
|
|
422
|
-
//
|
|
433
|
+
// A comment among the names has a line of its own to be on, and moving names across it
|
|
434
|
+
// would change what it describes; attributes bring braces that flattening would mangle.
|
|
423
435
|
if (source.getAllComments().some(c => c.range[0] >= interior[0] && c.range[1] <= interior[1]))
|
|
424
436
|
return false;
|
|
425
437
|
if (node.type === 'ImportDeclaration' && node.attributes.length > 0)
|
|
426
438
|
return false;
|
|
439
|
+
const written = source.getText(node);
|
|
427
440
|
const base = /^[^\S\r\n]*/.exec(lineAt(node.loc.start.line - 1))?.[0] ?? '';
|
|
428
|
-
|
|
429
|
-
const
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
441
|
+
// On one line a trailing comma is a dangling one, so it goes with the break it belonged to.
|
|
442
|
+
const flat = flattened(written).replace(/,(\s*})/, '$1');
|
|
443
|
+
if (BREAK.test(written) && base.length + flat.length <= maxLen) {
|
|
444
|
+
context.report({
|
|
445
|
+
node,
|
|
446
|
+
messageId: 'namesFitOneLine',
|
|
447
|
+
fix: fixer => fixer.replaceTextRange(node.range, flat),
|
|
448
|
+
});
|
|
449
|
+
return true;
|
|
450
|
+
}
|
|
451
|
+
// The names as they are laid out, a run to each line.
|
|
452
|
+
const rows = [];
|
|
453
|
+
for (const specifier of specifiers) {
|
|
454
|
+
const row = rows[rows.length - 1];
|
|
455
|
+
if (row?.[row.length - 1]?.loc.end.line === specifier.loc.start.line)
|
|
456
|
+
row.push(specifier);
|
|
457
|
+
else
|
|
458
|
+
rows.push([specifier]);
|
|
459
|
+
}
|
|
460
|
+
/**
|
|
461
|
+
* The names rewritten to fill lines from `column`, breaking to `indent` where they must.
|
|
462
|
+
*
|
|
463
|
+
* `tail` is what follows the last name on its line — a comma, the closing brace, the
|
|
464
|
+
* module — which is part of that line's length even though it is not a name.
|
|
465
|
+
*/
|
|
466
|
+
const rewrapped = (names, column, indent, tail) => {
|
|
467
|
+
let text = '';
|
|
468
|
+
let width = column;
|
|
469
|
+
for (const [i, specifier] of names.entries()) {
|
|
470
|
+
const name = source.getText(specifier);
|
|
471
|
+
const last = i === names.length - 1;
|
|
472
|
+
// A comma follows every name but the last, which carries the tail instead.
|
|
473
|
+
const owed = last ? tail : 1;
|
|
474
|
+
if (i === 0) {
|
|
475
|
+
text = name;
|
|
476
|
+
width += name.length;
|
|
477
|
+
}
|
|
478
|
+
else if (width + 2 + name.length + owed <= maxLen) {
|
|
479
|
+
text += `, ${name}`;
|
|
480
|
+
width += 2 + name.length;
|
|
481
|
+
}
|
|
482
|
+
else {
|
|
483
|
+
text += `,${eol}${indent}${name}`;
|
|
484
|
+
width = indent.length + name.length;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
return text;
|
|
488
|
+
};
|
|
489
|
+
const indentOf = (line) => line === node.loc.start.line ? `${base} ` : /^[^\S\r\n]*/.exec(lineAt(line - 1))?.[0] ?? `${base} `;
|
|
490
|
+
const spanOf = (names) => [names[0].range[0], names[names.length - 1].range[1]];
|
|
491
|
+
// To the end of the statement, not the end of the line: a comment beside an import is not
|
|
492
|
+
// the import's length, and breaking the names would not shorten it.
|
|
493
|
+
const widthAt = (line) => line === node.loc.end.line ? node.loc.end.column : lineAt(line - 1).trimEnd().length;
|
|
494
|
+
/** What sits after these names on their last line, and so shares it. */
|
|
495
|
+
const tailOf = (names) => {
|
|
496
|
+
const last = names[names.length - 1];
|
|
497
|
+
return Math.max(widthAt(last.loc.end.line) - last.loc.end.column, 0);
|
|
498
|
+
};
|
|
499
|
+
const report = (names, on, messageId) => {
|
|
500
|
+
const span = spanOf(names);
|
|
501
|
+
const text = rewrapped(names, names[0].loc.start.column, indentOf(names[0].loc.start.line), tailOf(names));
|
|
502
|
+
if (text === source.text.slice(...span))
|
|
450
503
|
return false;
|
|
504
|
+
context.report({
|
|
505
|
+
node: on,
|
|
506
|
+
messageId,
|
|
507
|
+
data: { maxLen: String(maxLen) },
|
|
508
|
+
fix: fixer => fixer.replaceTextRange(span, text),
|
|
509
|
+
});
|
|
510
|
+
return true;
|
|
511
|
+
};
|
|
512
|
+
// A line over the limit with one name on it is over because of what surrounds the name, so
|
|
513
|
+
// the braces are what has to give: the names take lines of their own.
|
|
514
|
+
const overLong = rows.find(row => row[0] && widthAt(row[0].loc.start.line) > maxLen);
|
|
515
|
+
if (overLong && !BREAK.test(written)) {
|
|
516
|
+
const indent = `${base} `;
|
|
517
|
+
const prefix = flattened(source.text.slice(node.range[0], open.range[1]));
|
|
518
|
+
const suffix = flattened(source.text.slice(close.range[0], node.range[1]));
|
|
519
|
+
const names = rewrapped(specifiers, indent.length, indent, 0);
|
|
520
|
+
context.report({
|
|
521
|
+
node,
|
|
522
|
+
messageId: 'nameLineTooLong',
|
|
523
|
+
data: { maxLen: String(maxLen) },
|
|
524
|
+
fix: fixer => fixer.replaceTextRange(node.range, `${prefix}${eol}${indent}${names}${eol}${base}${suffix}`),
|
|
525
|
+
});
|
|
526
|
+
return true;
|
|
451
527
|
}
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
528
|
+
// Names of one kind to a line, so a value never joins a line of types, or the other way.
|
|
529
|
+
// A row holding both is the order check's to sort out, and stands alone here.
|
|
530
|
+
const kindOf = (row) => {
|
|
531
|
+
const types = row.filter(isTypeSpecifier).length;
|
|
532
|
+
return types === 0 ? 'value' : types === row.length ? 'type' : 'mixed';
|
|
533
|
+
};
|
|
534
|
+
const runs = [];
|
|
535
|
+
for (const row of rows) {
|
|
536
|
+
const run = runs[runs.length - 1];
|
|
537
|
+
const previous = run?.[run.length - 1];
|
|
538
|
+
if (run && previous && kindOf(previous) === kindOf(row) && kindOf(row) !== 'mixed')
|
|
539
|
+
run.push(row);
|
|
540
|
+
else
|
|
541
|
+
runs.push([row]);
|
|
542
|
+
}
|
|
543
|
+
// A run at a time, since one line's names spilling down changes what the next line can hold;
|
|
544
|
+
// fixing a pair at a time would need a pass for every line.
|
|
545
|
+
for (const run of runs) {
|
|
546
|
+
const names = run.flat();
|
|
547
|
+
const long = run.some(row => widthAt(row[0].loc.start.line) > maxLen);
|
|
548
|
+
const on = long
|
|
549
|
+
? names.find(specifier => specifier.loc.end.column > maxLen) ?? names[1] ?? names[0]
|
|
550
|
+
: (run[1]?.[0] ?? names[0]);
|
|
551
|
+
if (report(names, on, long ? 'nameLineTooLong' : 'nameFitsAbove'))
|
|
552
|
+
return true;
|
|
553
|
+
}
|
|
554
|
+
return false;
|
|
462
555
|
};
|
|
463
556
|
/** Whether it reported the layout, which leaves the order and shape checks nothing to say. */
|
|
464
557
|
const checkBraces = (node, specifiers) => {
|
|
@@ -470,7 +563,7 @@ const rule = {
|
|
|
470
563
|
const close = source.getTokenAfter(last, { filter: token => token.value === '}' });
|
|
471
564
|
if (!open || !close)
|
|
472
565
|
return false;
|
|
473
|
-
if (manageLineLength &&
|
|
566
|
+
if (manageLineLength && checkLineLength(node, specifiers, open, close))
|
|
474
567
|
return true;
|
|
475
568
|
// To the module, not the statement's end, so a multiline `with { … }` is not counted.
|
|
476
569
|
const end = node.source ?? close;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-plugin-imports-regulation",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.17",
|
|
4
4
|
"description": "Order imports: external type-only, external, blank line, internal type-only, internal — with type specifiers last inside each import.",
|
|
5
5
|
"author": "Robert Sandiford",
|
|
6
6
|
"type": "module",
|