eslint-plugin-imports-regulation 0.2.6 → 0.2.8
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/dist/rules/imports-regulation.js +111 -16
- package/package.json +1 -1
|
@@ -65,7 +65,7 @@ const rule = {
|
|
|
65
65
|
messages: {
|
|
66
66
|
groupOrder: 'Expected this {{group}} import before the {{other}} imports.',
|
|
67
67
|
blankLinesBetween: 'Expected {{expected}} blank {{lines}} between external imports and internal imports, found {{actual}}.',
|
|
68
|
-
unexpectedBlankLine: '
|
|
68
|
+
unexpectedBlankLine: 'Expected no blank lines between imports in the same group, found {{actual}}.',
|
|
69
69
|
typeSpecifiersLast: 'Type specifiers must come after value specifiers.',
|
|
70
70
|
markTypeOnly: 'An import of nothing but types must be written `import type { A }`.',
|
|
71
71
|
typeOnlyImport: 'Everything this imports is only used as a type — write `import type`.',
|
|
@@ -151,17 +151,75 @@ const rule = {
|
|
|
151
151
|
return undefined;
|
|
152
152
|
return [open.range[1], close.range[0]];
|
|
153
153
|
};
|
|
154
|
-
/**
|
|
155
|
-
|
|
154
|
+
/**
|
|
155
|
+
* The comments inside the braces, shared out between the specifiers. An own-line one precedes
|
|
156
|
+
* the specifier it describes, a same-line one trails it, and any left before the closing brace
|
|
157
|
+
* stay last.
|
|
158
|
+
*/
|
|
159
|
+
const commentsInBraces = (named, range) => {
|
|
160
|
+
const carried = new Map();
|
|
161
|
+
for (const specifier of named)
|
|
162
|
+
carried.set(specifier, { leading: [], beforeComma: [], afterComma: [] });
|
|
163
|
+
const tail = [];
|
|
164
|
+
for (const comment of source.getAllComments()) {
|
|
165
|
+
if (comment.range[0] < range[0] || comment.range[1] > range[1])
|
|
166
|
+
continue;
|
|
167
|
+
const text = source.text.slice(comment.range[0], comment.range[1]);
|
|
168
|
+
const nextIndex = named.findIndex(specifier => specifier.range[0] >= comment.range[1]);
|
|
169
|
+
const next = nextIndex === -1 ? undefined : named[nextIndex];
|
|
170
|
+
const previous = named[(nextIndex === -1 ? named.length : nextIndex) - 1];
|
|
171
|
+
if (!previous) {
|
|
172
|
+
if (next)
|
|
173
|
+
carried.get(next)?.leading.push(text);
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
const after = source.getTokenAfter(previous);
|
|
177
|
+
const comma = after?.value === ',' ? after : undefined;
|
|
178
|
+
const own = carried.get(previous);
|
|
179
|
+
if (comma && comment.range[1] <= comma.range[0])
|
|
180
|
+
own?.beforeComma.push(text);
|
|
181
|
+
else if (comment.loc.start.line === (comma ?? previous).loc.end.line
|
|
182
|
+
&& (!next || next.loc.start.line > comment.loc.end.line))
|
|
183
|
+
own?.afterComma.push(text);
|
|
184
|
+
else if (next)
|
|
185
|
+
carried.get(next)?.leading.push(text);
|
|
186
|
+
else
|
|
187
|
+
tail.push(text);
|
|
188
|
+
}
|
|
189
|
+
return { carried, tail };
|
|
190
|
+
};
|
|
191
|
+
/** Re-join specifiers in the shape the braces already had, carrying their comments with them. */
|
|
192
|
+
const rejoin = (range, named, ordered, textOf) => {
|
|
156
193
|
const interior = source.text.slice(range[0], range[1]);
|
|
194
|
+
const { carried, tail } = commentsInBraces(named, range);
|
|
195
|
+
const pieceOf = (specifier) => carried.get(specifier) ?? { leading: [], beforeComma: [], afterComma: [] };
|
|
157
196
|
if (!BREAK.test(interior)) {
|
|
197
|
+
// Only block comments fit on one line, so a trailing one can always sit before the comma.
|
|
158
198
|
const pad = /^\s/.test(interior) ? ' ' : '';
|
|
159
|
-
|
|
199
|
+
const parts = ordered.map(specifier => {
|
|
200
|
+
const { leading, beforeComma, afterComma } = pieceOf(specifier);
|
|
201
|
+
return [...leading, textOf(specifier), ...beforeComma, ...afterComma].join(' ');
|
|
202
|
+
});
|
|
203
|
+
return `${pad}${parts.join(', ')}${tail.map(comment => ` ${comment}`).join('')}${pad}`;
|
|
160
204
|
}
|
|
161
205
|
const indent = /(?:\r\n|\n|\r)([^\S\r\n]*)\S/.exec(interior)?.[1] ?? ' ';
|
|
162
206
|
const closeIndent = /(?:\r\n|\n|\r)([^\S\r\n]*)$/.exec(interior)?.[1] ?? '';
|
|
163
|
-
|
|
164
|
-
|
|
207
|
+
// From the tokens, not the text: a comment after the last comma hides it from a regex.
|
|
208
|
+
const last = named[named.length - 1];
|
|
209
|
+
const trailingComma = last !== undefined && source.getTokenAfter(last)?.value === ',';
|
|
210
|
+
const lines = [];
|
|
211
|
+
for (const [index, specifier] of ordered.entries()) {
|
|
212
|
+
const { leading, beforeComma, afterComma } = pieceOf(specifier);
|
|
213
|
+
for (const comment of leading)
|
|
214
|
+
lines.push(`${indent}${comment}`);
|
|
215
|
+
const comma = index < ordered.length - 1 || trailingComma ? ',' : '';
|
|
216
|
+
// After the comma, since a line comment before it would swallow it.
|
|
217
|
+
const trailing = afterComma.map(comment => ` ${comment}`).join('');
|
|
218
|
+
lines.push(`${indent}${[textOf(specifier), ...beforeComma].join(' ')}${comma}${trailing}`);
|
|
219
|
+
}
|
|
220
|
+
for (const comment of tail)
|
|
221
|
+
lines.push(`${indent}${comment}`);
|
|
222
|
+
return `${eol}${lines.join(eol)}${eol}${closeIndent}`;
|
|
165
223
|
};
|
|
166
224
|
const asValueSpecifier = (specifier) => {
|
|
167
225
|
const imported = source.getText(specifier.imported);
|
|
@@ -211,7 +269,7 @@ const rule = {
|
|
|
211
269
|
const fixes = [fixer.insertTextAfter(keyword, ' type')];
|
|
212
270
|
// An inline marker is redundant under `import type`, and illegal besides.
|
|
213
271
|
if (range && named.some(specifier => specifier.importKind === 'type')) {
|
|
214
|
-
fixes.push(fixer.replaceTextRange(range, rejoin(range, named
|
|
272
|
+
fixes.push(fixer.replaceTextRange(range, rejoin(range, named, named, asValueSpecifier)));
|
|
215
273
|
}
|
|
216
274
|
return fixes;
|
|
217
275
|
},
|
|
@@ -251,7 +309,7 @@ const rule = {
|
|
|
251
309
|
messageId: 'markTypeOnly',
|
|
252
310
|
fix: fixer => [
|
|
253
311
|
fixer.insertTextAfter(keyword, ' type'),
|
|
254
|
-
fixer.replaceTextRange(range, rejoin(range, named
|
|
312
|
+
fixer.replaceTextRange(range, rejoin(range, named, named, asValueSpecifier)),
|
|
255
313
|
],
|
|
256
314
|
});
|
|
257
315
|
return;
|
|
@@ -273,17 +331,31 @@ const rule = {
|
|
|
273
331
|
context.report({
|
|
274
332
|
node: misplaced,
|
|
275
333
|
messageId: 'typeSpecifiersLast',
|
|
276
|
-
fix: fixer => fixer.replaceTextRange(range, rejoin(range, [...values, ...types]
|
|
334
|
+
fix: fixer => fixer.replaceTextRange(range, rejoin(range, named, [...values, ...types], specifier => source.getText(specifier))),
|
|
277
335
|
});
|
|
278
336
|
};
|
|
337
|
+
/** Comments that mean something only where they are, so they never move with an import. */
|
|
338
|
+
const staysPut = (comment) => {
|
|
339
|
+
if (source.text.startsWith('#!', comment.range[0]))
|
|
340
|
+
return true;
|
|
341
|
+
if (comment.type === 'Line' && comment.value.startsWith('/'))
|
|
342
|
+
return true; // `/// <reference>`
|
|
343
|
+
if (comment.type === 'Block' && comment.value.startsWith('!'))
|
|
344
|
+
return true; // `/*! licence */`
|
|
345
|
+
const text = comment.value.replace(/^\*+/, '').trim();
|
|
346
|
+
// Region and file directives. `eslint-disable-next-line` and `@ts-expect-error` are about
|
|
347
|
+
// the next line, so those do move with it.
|
|
348
|
+
return /^(?:eslint-(?:disable|enable)(?![-\w])|eslint(?:-env)?\s|globals?\s|exported\s|@ts-(?:no)?check\b|@jsx|@license|@preserve)/
|
|
349
|
+
.test(text);
|
|
350
|
+
};
|
|
279
351
|
/** An import plus the comments that travel with it. */
|
|
280
352
|
const entriesOf = (chunk) => {
|
|
281
353
|
const entries = [];
|
|
282
354
|
for (const node of chunk) {
|
|
283
355
|
const previous = entries[entries.length - 1];
|
|
284
356
|
let start = node.range[0];
|
|
285
|
-
//
|
|
286
|
-
//
|
|
357
|
+
// Comments precede what they describe, so an own-line comment belongs to the import
|
|
358
|
+
// beneath it.
|
|
287
359
|
if (previous) {
|
|
288
360
|
for (const comment of source.getCommentsBefore(node)) {
|
|
289
361
|
if (comment.range[0] >= previous.end) {
|
|
@@ -292,10 +364,27 @@ const rule = {
|
|
|
292
364
|
}
|
|
293
365
|
}
|
|
294
366
|
}
|
|
367
|
+
else {
|
|
368
|
+
// Above the first import only the comments directly on it: a blank line makes the
|
|
369
|
+
// rest a file header.
|
|
370
|
+
let below = node.loc.start.line;
|
|
371
|
+
const before = source.getCommentsBefore(node);
|
|
372
|
+
for (let i = before.length - 1; i >= 0; i--) {
|
|
373
|
+
const comment = before[i];
|
|
374
|
+
if (!comment || comment.loc.end.line < below - 1 || staysPut(comment))
|
|
375
|
+
break;
|
|
376
|
+
start = comment.range[0];
|
|
377
|
+
below = comment.loc.start.line;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
// Every comment on the import's last line, not just the first: a second one there is
|
|
381
|
+
// not a note on the next import.
|
|
295
382
|
let end = node.range[1];
|
|
296
|
-
const
|
|
297
|
-
|
|
298
|
-
|
|
383
|
+
for (const comment of source.getCommentsAfter(node)) {
|
|
384
|
+
if (comment.loc.start.line !== node.loc.end.line)
|
|
385
|
+
break;
|
|
386
|
+
end = comment.range[1];
|
|
387
|
+
}
|
|
299
388
|
entries.push({ node, group: groupOf(node), start, end });
|
|
300
389
|
}
|
|
301
390
|
return entries;
|
|
@@ -397,6 +486,8 @@ const rule = {
|
|
|
397
486
|
const wanted = boundary ? blankLinesBetween : 0;
|
|
398
487
|
if (blank === wanted)
|
|
399
488
|
continue;
|
|
489
|
+
// Every gap, each with the whole-run fix: ESLint applies one, and the rest are gone on
|
|
490
|
+
// the next pass.
|
|
400
491
|
if (boundary) {
|
|
401
492
|
context.report({
|
|
402
493
|
node: entry.node,
|
|
@@ -406,9 +497,13 @@ const rule = {
|
|
|
406
497
|
});
|
|
407
498
|
}
|
|
408
499
|
else {
|
|
409
|
-
context.report({
|
|
500
|
+
context.report({
|
|
501
|
+
node: entry.node,
|
|
502
|
+
messageId: 'unexpectedBlankLine',
|
|
503
|
+
data: { actual: String(blank) },
|
|
504
|
+
fix: rewrite,
|
|
505
|
+
});
|
|
410
506
|
}
|
|
411
|
-
return;
|
|
412
507
|
}
|
|
413
508
|
};
|
|
414
509
|
return {
|
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.8",
|
|
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",
|