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 line = (group) => `${indent}${group.map(specifier => source.getText(specifier)).join(', ')}`;
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
- let end = node.range[1];
469
- for (const comment of source.getCommentsAfter(node)) {
470
- if (comment.loc.start.line !== node.loc.end.line)
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
- for (const statement of program.body) {
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 (statement.type === 'ImportDeclaration' || isHeaderExport(statement))
805
+ if (isHeader(statement)) {
758
806
  chunk.push(statement);
759
- else
760
- flush();
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.13",
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",