eslint-plugin-imports-regulation 0.2.0 → 0.2.2
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' | 'blankLinesBetween' | 'unexpectedBlankLine' | 'typeSpecifiersLast' | 'markTypeOnly' | 'blankLinesAfter';
|
|
2
|
+
type MessageIds = 'groupOrder' | 'blankLinesBetween' | 'unexpectedBlankLine' | 'typeSpecifiersLast' | 'markTypeOnly' | 'typeOnlyImport' | 'typeOnlySpecifier' | 'blankLinesAfter';
|
|
3
3
|
type Origin = 'external' | 'internal';
|
|
4
4
|
type Options = [
|
|
5
5
|
{
|
|
@@ -8,10 +8,11 @@ type Options = [
|
|
|
8
8
|
pattern: string;
|
|
9
9
|
group: Origin;
|
|
10
10
|
}[];
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
blankLinesBetween?: number;
|
|
12
|
+
blankLinesAfter?: number;
|
|
13
13
|
importsAddenda?: string[];
|
|
14
14
|
preferTypeDeclarations?: boolean;
|
|
15
|
+
inferTypeOnlyImports?: boolean;
|
|
15
16
|
}?
|
|
16
17
|
];
|
|
17
18
|
declare const rule: TSESLint.RuleModule<MessageIds, Options>;
|
|
@@ -47,15 +47,17 @@ const rule = {
|
|
|
47
47
|
},
|
|
48
48
|
},
|
|
49
49
|
// How many blank lines separate the external block from the internal block. 0 means none.
|
|
50
|
-
|
|
50
|
+
blankLinesBetween: { type: 'integer', minimum: 0 },
|
|
51
51
|
// How many blank lines separate the imports from whatever follows them.
|
|
52
|
-
|
|
52
|
+
blankLinesAfter: { type: 'integer', minimum: 0 },
|
|
53
53
|
// Line globs that still count as part of the imports block, so the gap is measured
|
|
54
54
|
// after them rather than after the last import.
|
|
55
55
|
importsAddenda: { type: 'array', items: { type: 'string' }, uniqueItems: true },
|
|
56
56
|
// An import of nothing but types is rewritten `import type { A }`. Off, and the
|
|
57
57
|
// spelling is left alone either way.
|
|
58
58
|
preferTypeDeclarations: { type: 'boolean' },
|
|
59
|
+
// Mark bindings that scope analysis shows are only ever used in type positions.
|
|
60
|
+
inferTypeOnlyImports: { type: 'boolean' },
|
|
59
61
|
},
|
|
60
62
|
additionalProperties: false,
|
|
61
63
|
},
|
|
@@ -66,6 +68,8 @@ const rule = {
|
|
|
66
68
|
unexpectedBlankLine: 'Unexpected blank line between imports in the same group.',
|
|
67
69
|
typeSpecifiersLast: 'Type specifiers must come after value specifiers.',
|
|
68
70
|
markTypeOnly: 'An import of nothing but types must be written `import type { A }`.',
|
|
71
|
+
typeOnlyImport: 'Everything this imports is only used as a type — write `import type`.',
|
|
72
|
+
typeOnlySpecifier: '`{{name}}` is only used as a type — mark it `type`.',
|
|
69
73
|
blankLinesAfter: 'Expected {{expected}} blank {{lines}} after the imports, found {{actual}}.',
|
|
70
74
|
},
|
|
71
75
|
},
|
|
@@ -77,10 +81,11 @@ const rule = {
|
|
|
77
81
|
test: globToRegExp(entry.pattern),
|
|
78
82
|
group: entry.group,
|
|
79
83
|
}));
|
|
80
|
-
const blankLinesBetween = options?.
|
|
81
|
-
const blankLinesAfter = options?.
|
|
84
|
+
const blankLinesBetween = options?.blankLinesBetween ?? DEFAULT_BLANK_LINES_BETWEEN;
|
|
85
|
+
const blankLinesAfter = options?.blankLinesAfter ?? DEFAULT_BLANK_LINES_AFTER;
|
|
82
86
|
const addenda = (options?.importsAddenda ?? []).map(lineGlobToRegExp);
|
|
83
87
|
const preferTypeDeclarations = options?.preferTypeDeclarations ?? true;
|
|
88
|
+
const inferTypeOnlyImports = options?.inferTypeOnlyImports ?? true;
|
|
84
89
|
const eol = BREAK.exec(source.text)?.[0] ?? '\n';
|
|
85
90
|
const originOf = (path) => {
|
|
86
91
|
for (const { test, group } of patterns)
|
|
@@ -141,13 +146,78 @@ const rule = {
|
|
|
141
146
|
}
|
|
142
147
|
return `${imported} as ${specifier.local.name}`;
|
|
143
148
|
};
|
|
149
|
+
/**
|
|
150
|
+
* Local names this import binds that are only ever referenced from a type position.
|
|
151
|
+
*
|
|
152
|
+
* Scope analysis, not type information: the parser's scope manager flags each reference as a
|
|
153
|
+
* value or a type one. A binding with *no* references says nothing either way, so it is left
|
|
154
|
+
* out — deciding it is a type would fight `no-unused-vars` over an import that is on its way
|
|
155
|
+
* out anyway. Under a non-TypeScript parser no reference is a type reference, so this yields
|
|
156
|
+
* nothing and the check quietly does not apply.
|
|
157
|
+
*/
|
|
158
|
+
const typeOnlyBindings = (node) => {
|
|
159
|
+
const names = new Set();
|
|
160
|
+
for (const variable of source.getDeclaredVariables(node)) {
|
|
161
|
+
if (variable.references.length === 0)
|
|
162
|
+
continue;
|
|
163
|
+
if (variable.references.every(reference => reference.isTypeReference))
|
|
164
|
+
names.add(variable.name);
|
|
165
|
+
}
|
|
166
|
+
return names;
|
|
167
|
+
};
|
|
168
|
+
/** Returns whether it reported, so the syntax-only checks can stand down. */
|
|
169
|
+
const checkInferredTypes = (node, named) => {
|
|
170
|
+
// Already spelled with inline markers throughout: `markTypeOnly` owns that case.
|
|
171
|
+
if (unmarkedTypeOnly(node))
|
|
172
|
+
return false;
|
|
173
|
+
const typeOnly = typeOnlyBindings(node);
|
|
174
|
+
if (typeOnly.size === 0)
|
|
175
|
+
return false;
|
|
176
|
+
const everyBinding = node.specifiers.length > 0
|
|
177
|
+
&& node.specifiers.every(specifier => typeOnly.has(specifier.local.name));
|
|
178
|
+
if (everyBinding && preferTypeDeclarations) {
|
|
179
|
+
const keyword = source.getFirstToken(node);
|
|
180
|
+
if (!keyword)
|
|
181
|
+
return false;
|
|
182
|
+
const range = braceInterior(named);
|
|
183
|
+
context.report({
|
|
184
|
+
node,
|
|
185
|
+
messageId: 'typeOnlyImport',
|
|
186
|
+
fix: fixer => {
|
|
187
|
+
const fixes = [fixer.insertTextAfter(keyword, ' type')];
|
|
188
|
+
// An inline marker is redundant under `import type`, and illegal besides.
|
|
189
|
+
if (range && named.some(specifier => specifier.importKind === 'type')) {
|
|
190
|
+
fixes.push(fixer.replaceTextRange(range, rejoin(range, named.map(asValueSpecifier))));
|
|
191
|
+
}
|
|
192
|
+
return fixes;
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
return true;
|
|
196
|
+
}
|
|
197
|
+
// A default or namespace binding has nowhere to put an inline marker, so only named
|
|
198
|
+
// specifiers can be marked one at a time.
|
|
199
|
+
const unmarked = named.filter(specifier => specifier.importKind !== 'type' && typeOnly.has(specifier.local.name));
|
|
200
|
+
if (unmarked.length === 0)
|
|
201
|
+
return false;
|
|
202
|
+
for (const specifier of unmarked) {
|
|
203
|
+
context.report({
|
|
204
|
+
node: specifier,
|
|
205
|
+
messageId: 'typeOnlySpecifier',
|
|
206
|
+
data: { name: specifier.local.name },
|
|
207
|
+
fix: fixer => fixer.insertTextBefore(specifier, 'type '),
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
return true;
|
|
211
|
+
};
|
|
144
212
|
const checkSpecifiers = (node) => {
|
|
213
|
+
if (node.importKind === 'type')
|
|
214
|
+
return;
|
|
145
215
|
const named = namedOf(node);
|
|
216
|
+
if (inferTypeOnlyImports && checkInferredTypes(node, named))
|
|
217
|
+
return;
|
|
146
218
|
const range = braceInterior(named);
|
|
147
219
|
if (!range)
|
|
148
220
|
return;
|
|
149
|
-
if (node.importKind === 'type')
|
|
150
|
-
return;
|
|
151
221
|
if (preferTypeDeclarations && unmarkedTypeOnly(node)) {
|
|
152
222
|
const keyword = source.getFirstToken(node);
|
|
153
223
|
if (!keyword)
|
|
@@ -239,6 +309,13 @@ const rule = {
|
|
|
239
309
|
const to = source.getIndexFromLoc({ line: next + 1, column: 0 });
|
|
240
310
|
context.report({
|
|
241
311
|
node: last.node,
|
|
312
|
+
// The line that follows the gap, not the import above it: the import is where it
|
|
313
|
+
// belongs, and squiggling it reads as though it were the thing at fault. This also
|
|
314
|
+
// matches the between-imports checks, which report the import *after* their gap.
|
|
315
|
+
loc: {
|
|
316
|
+
start: { line: next + 1, column: 0 },
|
|
317
|
+
end: { line: next + 1, column: lineAt(next).length },
|
|
318
|
+
},
|
|
242
319
|
messageId: 'blankLinesAfter',
|
|
243
320
|
data: {
|
|
244
321
|
expected: String(blankLinesAfter),
|
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.2",
|
|
4
4
|
"description": "Order imports: package type-only, package, blank line, local type-only, local — with type specifiers last inside each import.",
|
|
5
5
|
"author": "Robert Sandiford",
|
|
6
6
|
"type": "module",
|