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
- blankLinesBetweenExternalAndInternalImports?: number;
12
- blankLinesAfterImports?: number;
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
- blankLinesBetweenExternalAndInternalImports: { type: 'integer', minimum: 0 },
50
+ blankLinesBetween: { type: 'integer', minimum: 0 },
51
51
  // How many blank lines separate the imports from whatever follows them.
52
- blankLinesAfterImports: { type: 'integer', minimum: 0 },
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?.blankLinesBetweenExternalAndInternalImports ?? DEFAULT_BLANK_LINES_BETWEEN;
81
- const blankLinesAfter = options?.blankLinesAfterImports ?? DEFAULT_BLANK_LINES_AFTER;
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.0",
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",