eslint-plugin-imports-regulation 0.2.6 → 0.2.7

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.
@@ -151,17 +151,75 @@ const rule = {
151
151
  return undefined;
152
152
  return [open.range[1], close.range[0]];
153
153
  };
154
- /** Re-join specifiers in the shape the braces already had, so a multi-line import stays one. */
155
- const rejoin = (range, texts) => {
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
- return `${pad}${texts.join(', ')}${pad}`;
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
- const comma = /,\s*$/.test(interior) ? ',' : '';
164
- return `${eol}${indent}${texts.join(`,${eol}${indent}`)}${comma}${eol}${closeIndent}`;
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.map(asValueSpecifier))));
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.map(asValueSpecifier))),
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].map(specifier => source.getText(specifier)))),
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
- // An own-line comment belongs to the import beneath it. One above the *first* import of
286
- // a chunk is left where it is, being as likely a file header as a note about the import.
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 trailing = source.getCommentsAfter(node)[0];
297
- if (trailing && trailing.loc.start.line === node.loc.end.line)
298
- end = trailing.range[1];
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-imports-regulation",
3
- "version": "0.2.6",
3
+ "version": "0.2.7",
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",