eslint-plugin-imports-regulation 0.2.13 → 0.2.15
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';
|
|
2
|
+
type MessageIds = 'groupOrder' | 'exportBlock' | 'blankLinesBetween' | 'unexpectedBlankLine' | 'blankLineBelowComment' | 'typeSpecifiersLast' | 'typeSpecifiersFirst' | 'markTypeOnly' | 'typeOnlyImport' | 'typeOnlySpecifier' | 'blankLinesAfter' | 'multilineBraces' | 'codeAmongImports';
|
|
3
3
|
type Origin = 'external' | 'internal';
|
|
4
4
|
type Options = [
|
|
5
5
|
{
|
|
@@ -13,6 +13,7 @@ type Options = [
|
|
|
13
13
|
importsAddenda?: string[];
|
|
14
14
|
preferTypeDeclarations?: boolean;
|
|
15
15
|
inferTypeOnlyImports?: boolean;
|
|
16
|
+
maxLen?: number;
|
|
16
17
|
}?
|
|
17
18
|
];
|
|
18
19
|
declare const rule: TSESLint.RuleModule<MessageIds, Options>;
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
const DEFAULT_BLANK_LINES_BETWEEN = 1;
|
|
2
2
|
const DEFAULT_BLANK_LINES_AFTER = 2;
|
|
3
|
+
// A rule cannot read `max-len`'s options, so this is its default.
|
|
4
|
+
const DEFAULT_MAX_LEN = 80;
|
|
3
5
|
// The order, as written. The blank line falls between 1 and 2 — nowhere else.
|
|
4
6
|
const EXTERNAL_TYPE = 0;
|
|
5
7
|
const INTERNAL_TYPE = 2;
|
|
@@ -58,6 +60,8 @@ const rule = {
|
|
|
58
60
|
preferTypeDeclarations: { type: 'boolean' },
|
|
59
61
|
// Mark bindings that scope analysis shows are only ever used in type positions.
|
|
60
62
|
inferTypeOnlyImports: { type: 'boolean' },
|
|
63
|
+
// The longest line a fix may write when it gathers names; lines as written are left alone.
|
|
64
|
+
maxLen: { type: 'integer', minimum: 1 },
|
|
61
65
|
},
|
|
62
66
|
additionalProperties: false,
|
|
63
67
|
},
|
|
@@ -74,6 +78,7 @@ const rule = {
|
|
|
74
78
|
typeOnlyImport: 'Everything this imports is only used as a type — write `import type`.',
|
|
75
79
|
typeOnlySpecifier: '`{{name}}` is only used as a type — mark it `type`.',
|
|
76
80
|
blankLinesAfter: 'Expected {{expected}} blank {{lines}} after the imports, found {{actual}}.',
|
|
81
|
+
codeAmongImports: 'Expected no code among the imports.',
|
|
77
82
|
multilineBraces: 'Expected a multiline {{kind}} to end a line with `{` and start one with `}{{tail}}`.',
|
|
78
83
|
},
|
|
79
84
|
},
|
|
@@ -90,6 +95,7 @@ const rule = {
|
|
|
90
95
|
const addenda = (options?.importsAddenda ?? []).map(lineGlobToRegExp);
|
|
91
96
|
const preferTypeDeclarations = options?.preferTypeDeclarations ?? true;
|
|
92
97
|
const inferTypeOnlyImports = options?.inferTypeOnlyImports ?? true;
|
|
98
|
+
const maxLen = options?.maxLen ?? DEFAULT_MAX_LEN;
|
|
93
99
|
const eol = BREAK.exec(source.text)?.[0] ?? '\n';
|
|
94
100
|
const originOf = (path) => {
|
|
95
101
|
for (const { test, group } of patterns)
|
|
@@ -352,8 +358,7 @@ const rule = {
|
|
|
352
358
|
return fixer.replaceTextRange(range, rejoin(range, named, [...types, ...values], specifier => source.getText(specifier)));
|
|
353
359
|
}
|
|
354
360
|
const { indent, closeIndent, trailingComma } = shapeOf(range, named);
|
|
355
|
-
const
|
|
356
|
-
const text = `${line(types)},${eol}${line(values)}${trailingComma ? ',' : ''}`;
|
|
361
|
+
const text = stacked(named, indent, trailingComma).join(eol);
|
|
357
362
|
return fixer.replaceTextRange(range, `${eol}${text}${eol}${closeIndent}`);
|
|
358
363
|
},
|
|
359
364
|
});
|
|
@@ -369,6 +374,31 @@ const rule = {
|
|
|
369
374
|
fix: fixer => fixer.replaceTextRange(range, rejoin(range, named, [...values, ...types], specifier => source.getText(specifier))),
|
|
370
375
|
});
|
|
371
376
|
};
|
|
377
|
+
const isTypeSpecifier = (specifier) => (specifier.type === 'ImportSpecifier' ? specifier.importKind : specifier.exportKind) === 'type';
|
|
378
|
+
/** Names packed greedily into lines within `maxLen`, every line but the last ending in a comma. */
|
|
379
|
+
const pack = (specifiers, indent, comma) => {
|
|
380
|
+
const lines = [];
|
|
381
|
+
let line = '';
|
|
382
|
+
for (const specifier of specifiers) {
|
|
383
|
+
const text = source.getText(specifier);
|
|
384
|
+
const joined = line ? `${line}, ${text}` : text;
|
|
385
|
+
if (line && indent.length + joined.length + 1 > maxLen) {
|
|
386
|
+
lines.push(line);
|
|
387
|
+
line = text;
|
|
388
|
+
}
|
|
389
|
+
else
|
|
390
|
+
line = joined;
|
|
391
|
+
}
|
|
392
|
+
if (line)
|
|
393
|
+
lines.push(line);
|
|
394
|
+
return lines.map((text, i) => `${indent}${text}${i < lines.length - 1 || comma ? ',' : ''}`);
|
|
395
|
+
};
|
|
396
|
+
/** Types' lines above the values', since a line holding both would read as one list. */
|
|
397
|
+
const stacked = (specifiers, indent, trailingComma) => {
|
|
398
|
+
const types = specifiers.filter(isTypeSpecifier);
|
|
399
|
+
const values = specifiers.filter(specifier => !isTypeSpecifier(specifier));
|
|
400
|
+
return [...pack(types, indent, values.length > 0 || trailingComma), ...pack(values, indent, trailingComma)];
|
|
401
|
+
};
|
|
372
402
|
const checkBraces = (node, specifiers) => {
|
|
373
403
|
const first = specifiers[0];
|
|
374
404
|
const last = specifiers[specifiers.length - 1];
|
|
@@ -418,6 +448,20 @@ const rule = {
|
|
|
418
448
|
},
|
|
419
449
|
});
|
|
420
450
|
};
|
|
451
|
+
// Matched by its name after the plugin prefix, which differs by config.
|
|
452
|
+
const ruleName = context.id.split('/').pop() ?? context.id;
|
|
453
|
+
/** This rule is disabled on the statement's last line. */
|
|
454
|
+
const passedOver = (statement) => source.getCommentsAfter(statement).some(comment => {
|
|
455
|
+
if (comment.loc.start.line !== statement.loc.end.line)
|
|
456
|
+
return false;
|
|
457
|
+
const directive = /^eslint-disable-line(?:\s+([^]*?))?(?:\s--[^]*)?$/.exec(comment.value.trim());
|
|
458
|
+
if (!directive)
|
|
459
|
+
return false;
|
|
460
|
+
const rules = directive[1]?.split(',').map(rule => rule.trim()).filter(Boolean) ?? [];
|
|
461
|
+
return rules.length === 0 || rules.some(rule => rule === ruleName || rule.endsWith(`/${ruleName}`));
|
|
462
|
+
});
|
|
463
|
+
// Passed-over statements, by the header statement they follow and travel with.
|
|
464
|
+
const attached = new Map();
|
|
421
465
|
/** `export … from` and `export { … }`, which sit among the imports; a declaration is code. */
|
|
422
466
|
const isHeaderExport = (node) => node.type === 'ExportAllDeclaration' || (node.type === 'ExportNamedDeclaration' && node.declaration === null);
|
|
423
467
|
/** Comments that mean something only where they are, so they never move with an import. */
|
|
@@ -465,9 +509,10 @@ const rule = {
|
|
|
465
509
|
}
|
|
466
510
|
// Every comment on the import's last line, not just the first: a second one there is
|
|
467
511
|
// not a note on the next import.
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
512
|
+
const tail = attached.get(node) ?? node;
|
|
513
|
+
let end = tail.range[1];
|
|
514
|
+
for (const comment of source.getCommentsAfter(tail)) {
|
|
515
|
+
if (comment.loc.start.line !== tail.loc.end.line)
|
|
471
516
|
break;
|
|
472
517
|
end = comment.range[1];
|
|
473
518
|
}
|
|
@@ -750,14 +795,32 @@ const rule = {
|
|
|
750
795
|
checkChunk(chunk, chunk[0] === program.body[0]);
|
|
751
796
|
chunk = [];
|
|
752
797
|
};
|
|
753
|
-
|
|
798
|
+
const isHeader = (statement) => statement !== undefined && (statement.type === 'ImportDeclaration' || isHeaderExport(statement));
|
|
799
|
+
const body = program.body;
|
|
800
|
+
for (let i = 0; i < body.length; i++) {
|
|
801
|
+
const statement = body[i];
|
|
754
802
|
// A bare `import 'x'` sorts like any other — where its side effect has to run in a
|
|
755
803
|
// particular place, that is what a disable comment is for. Anything that is not an
|
|
756
804
|
// import or re-export does end the run, since moving code across it could change what happens.
|
|
757
|
-
if (
|
|
805
|
+
if (isHeader(statement)) {
|
|
758
806
|
chunk.push(statement);
|
|
759
|
-
|
|
760
|
-
|
|
807
|
+
continue;
|
|
808
|
+
}
|
|
809
|
+
// Unless this rule is disabled on its line and imports carry on after it: then it goes with the import above.
|
|
810
|
+
let j = i;
|
|
811
|
+
for (let next = body[j]; next && !isHeader(next) && passedOver(next); next = body[j])
|
|
812
|
+
j++;
|
|
813
|
+
const above = chunk[chunk.length - 1];
|
|
814
|
+
const last = body[j - 1];
|
|
815
|
+
if (above && last && j > i && isHeader(body[j])) {
|
|
816
|
+
attached.set(above, last);
|
|
817
|
+
// Suppressed by the directive, and what keeps it in use: unused, `--fix` would delete it.
|
|
818
|
+
for (const passed of body.slice(i, j))
|
|
819
|
+
context.report({ node: passed, messageId: 'codeAmongImports' });
|
|
820
|
+
i = j - 1;
|
|
821
|
+
continue;
|
|
822
|
+
}
|
|
823
|
+
flush();
|
|
761
824
|
}
|
|
762
825
|
flush();
|
|
763
826
|
},
|
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.15",
|
|
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",
|