@blumintinc/eslint-plugin-blumint 1.20.85 → 1.20.87

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.
@@ -0,0 +1,397 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.planOrphanedImportRemoval = exports.importBindingSpecifierOf = exports.removeImportBindingFixes = exports.planImportBindingRemoval = void 0;
4
+ const utils_1 = require("@typescript-eslint/utils");
5
+ const disableDirectives_1 = require("./disableDirectives");
6
+ function contains(outer, inner) {
7
+ return inner[0] >= outer[0] && inner[1] <= outer[1];
8
+ }
9
+ function isWithinAny(range, ranges) {
10
+ return ranges.some((candidate) => contains(candidate, range));
11
+ }
12
+ function isNamedSpecifier(specifier) {
13
+ return specifier.type === utils_1.AST_NODE_TYPES.ImportSpecifier;
14
+ }
15
+ /**
16
+ * A comment that governs the line directly below it. Deleting the declaration
17
+ * underneath such a comment strands the directive on whatever moves up into
18
+ * its place — an `@ts-expect-error` then becomes a compile error of its own and
19
+ * a disable directive silences an unrelated line.
20
+ */
21
+ function bindsNextLine(comment) {
22
+ const [directive] = (0, disableDirectives_1.parseDisableDirectives)([comment]);
23
+ if (directive?.kind === 'disable-next-line') {
24
+ return true;
25
+ }
26
+ const value = comment.value.trim();
27
+ return value.startsWith('@ts-expect-error') || value.startsWith('@ts-ignore');
28
+ }
29
+ function isComment(token) {
30
+ return (token.type === utils_1.AST_TOKEN_TYPES.Line || token.type === utils_1.AST_TOKEN_TYPES.Block);
31
+ }
32
+ /**
33
+ * The slice to delete for a declaration that loses its last binding. The whole
34
+ * line goes when the declaration owns it, trailing same-line comments included:
35
+ * those describe the import that is going away, and a `// eslint-disable-line`
36
+ * among them would otherwise outlive its subject. A declaration sharing its
37
+ * line with other code gives up only its own characters.
38
+ */
39
+ function declarationRemovalRange(source, declaration) {
40
+ const commentsBefore = source.getCommentsBefore(declaration);
41
+ const boundComment = commentsBefore[commentsBefore.length - 1];
42
+ if (boundComment &&
43
+ bindsNextLine(boundComment) &&
44
+ boundComment.loc.end.line + 1 === declaration.loc.start.line) {
45
+ return null;
46
+ }
47
+ const tokenBefore = source.getTokenBefore(declaration, {
48
+ includeComments: true,
49
+ });
50
+ const ownsLineStart = !tokenBefore || tokenBefore.loc.end.line < declaration.loc.start.line;
51
+ const start = ownsLineStart
52
+ ? source.text.lastIndexOf('\n', declaration.range[0] - 1) + 1
53
+ : declaration.range[0];
54
+ let cursor = declaration;
55
+ for (;;) {
56
+ const next = source.getTokenAfter(cursor, { includeComments: true });
57
+ if (!next || next.loc.start.line !== cursor.loc.end.line) {
58
+ break;
59
+ }
60
+ if (!isComment(next)) {
61
+ // Code shares the line, so the line terminator belongs to that code.
62
+ return [start, declaration.range[1]];
63
+ }
64
+ cursor = next;
65
+ }
66
+ const lineEnd = source.text.indexOf('\n', cursor.range[1]);
67
+ return [start, lineEnd === -1 ? source.text.length : lineEnd + 1];
68
+ }
69
+ /**
70
+ * Ranges covering every removed item together with the separators that would
71
+ * be left dangling. A removed run reaches forward to the next survivor, or
72
+ * backward from the previous one when it ends the list, so exactly one
73
+ * separator per removed item disappears. Returns `null` when every item is
74
+ * removed — that collapse is the caller's to make, since what encloses the
75
+ * list (braces, the declaration itself) must go too.
76
+ */
77
+ function removalRuns(items) {
78
+ const ranges = [];
79
+ let index = 0;
80
+ while (index < items.length) {
81
+ if (!items[index].removed) {
82
+ index++;
83
+ continue;
84
+ }
85
+ let last = index;
86
+ while (last + 1 < items.length && items[last + 1].removed) {
87
+ last++;
88
+ }
89
+ const following = items[last + 1];
90
+ if (following) {
91
+ ranges.push([items[index].range[0], following.range[0]]);
92
+ }
93
+ else {
94
+ const preceding = items[index - 1];
95
+ if (!preceding)
96
+ return null;
97
+ ranges.push([preceding.range[1], items[last].range[1]]);
98
+ }
99
+ index = last + 1;
100
+ }
101
+ return ranges;
102
+ }
103
+ /**
104
+ * The declaration a specifier belongs to, found by scanning the program rather
105
+ * than by following `parent`: parent links exist only once ESLint's traversal
106
+ * has installed them, and a helper that reads a `SourceCode` should not depend
107
+ * on having been called from inside a rule visitor.
108
+ */
109
+ function declarationOf(source, specifier) {
110
+ return source.ast.body.find((statement) => statement.type === utils_1.AST_NODE_TYPES.ImportDeclaration &&
111
+ statement.specifiers.includes(specifier));
112
+ }
113
+ function bracesRange(source, named) {
114
+ const open = source.getTokenBefore(named[0], {
115
+ filter: (token) => token.value === '{',
116
+ });
117
+ const close = source.getTokenAfter(named[named.length - 1], {
118
+ filter: (token) => token.value === '}',
119
+ });
120
+ return open && close ? [open.range[0], close.range[1]] : null;
121
+ }
122
+ /**
123
+ * The text ranges that unbind `specifiers` from their shared declaration, or
124
+ * `null` when no removal is provably safe. All specifiers of the declaration
125
+ * being listed collapses the declaration itself rather than leaving
126
+ * `import {} from './User';` behind, and losing every *named* specifier next to
127
+ * a surviving default takes the braces with it.
128
+ *
129
+ * Any comment inside the declaration declines the removal outright: the ranges
130
+ * computed here span separators, so a comment nested among the specifiers
131
+ * would be swallowed or stranded depending on where it sits.
132
+ */
133
+ function planImportBindingRemoval(source, specifiers) {
134
+ const [first] = specifiers;
135
+ if (!first)
136
+ return [];
137
+ const declaration = declarationOf(source, first);
138
+ if (!declaration)
139
+ return null;
140
+ if (specifiers.some((specifier) => !declaration.specifiers.includes(specifier))) {
141
+ return null;
142
+ }
143
+ if (source.getCommentsInside(declaration).length > 0)
144
+ return null;
145
+ const removed = new Set(specifiers);
146
+ if (declaration.specifiers.every((specifier) => removed.has(specifier))) {
147
+ const range = declarationRemovalRange(source, declaration);
148
+ return range ? [range] : null;
149
+ }
150
+ const named = declaration.specifiers.filter(isNamedSpecifier);
151
+ const bracesRemoved = named.length > 0 && named.every((specifier) => removed.has(specifier));
152
+ const clauses = declaration.specifiers
153
+ .filter((specifier) => !isNamedSpecifier(specifier))
154
+ .map((specifier) => ({
155
+ range: specifier.range,
156
+ removed: removed.has(specifier),
157
+ }));
158
+ if (named.length > 0) {
159
+ const braces = bracesRange(source, named);
160
+ if (!braces)
161
+ return null;
162
+ clauses.push({ range: braces, removed: bracesRemoved });
163
+ }
164
+ const ranges = removalRuns(clauses);
165
+ if (!ranges)
166
+ return null;
167
+ if (named.length > 0 && !bracesRemoved) {
168
+ const inner = removalRuns(named.map((specifier) => ({
169
+ range: specifier.range,
170
+ removed: removed.has(specifier),
171
+ })));
172
+ if (!inner)
173
+ return null;
174
+ ranges.push(...inner);
175
+ }
176
+ return [...ranges].sort((left, right) => left[0] - right[0]);
177
+ }
178
+ exports.planImportBindingRemoval = planImportBindingRemoval;
179
+ /**
180
+ * Fixes unbinding `specifiers`, or `null` when the removal is not provably
181
+ * safe. A caller that gets `null` must drop its whole fix: a rewrite that
182
+ * strips a binding's last use while leaving the binding behind trades its own
183
+ * report for an unused-variable one.
184
+ */
185
+ function removeImportBindingFixes(source, fixer, specifiers) {
186
+ const ranges = planImportBindingRemoval(source, specifiers);
187
+ return (ranges?.map((range) => fixer.removeRange([range[0], range[1]])) ?? null);
188
+ }
189
+ exports.removeImportBindingFixes = removeImportBindingFixes;
190
+ /**
191
+ * The specifier that binds `variable`, when the variable is a named/default/
192
+ * namespace import. `import X = require('y')` binds through a declaration of
193
+ * its own rather than a specifier and is left alone.
194
+ */
195
+ function importBindingSpecifierOf(variable) {
196
+ if (variable.defs.length !== 1)
197
+ return undefined;
198
+ const [definition] = variable.defs;
199
+ if (definition.type !== utils_1.TSESLint.Scope.DefinitionType.ImportBinding) {
200
+ return undefined;
201
+ }
202
+ const { node } = definition;
203
+ return node.type === utils_1.AST_NODE_TYPES.TSImportEqualsDeclaration
204
+ ? undefined
205
+ : node;
206
+ }
207
+ exports.importBindingSpecifierOf = importBindingSpecifierOf;
208
+ function isImportBindingVariable(variable) {
209
+ return variable.defs.some((definition) => definition.type === utils_1.TSESLint.Scope.DefinitionType.ImportBinding);
210
+ }
211
+ function allVariables(scopeManager) {
212
+ const globalScope = scopeManager?.globalScope;
213
+ if (!globalScope)
214
+ return [];
215
+ const variables = [];
216
+ const stack = [globalScope];
217
+ while (stack.length > 0) {
218
+ const scope = stack.pop();
219
+ variables.push(...scope.variables);
220
+ stack.push(...scope.childScopes);
221
+ }
222
+ return variables;
223
+ }
224
+ function isAstNode(value) {
225
+ return (typeof value === 'object' &&
226
+ value !== null &&
227
+ typeof value.type === 'string');
228
+ }
229
+ function forEachIdentifier(node, parent, key, visit) {
230
+ if (node.type === utils_1.AST_NODE_TYPES.Identifier ||
231
+ node.type === utils_1.AST_NODE_TYPES.JSXIdentifier) {
232
+ visit(node, parent, key);
233
+ }
234
+ for (const [childKey, value] of Object.entries(node)) {
235
+ if (childKey === 'parent')
236
+ continue;
237
+ const children = Array.isArray(value) ? value : [value];
238
+ for (const child of children) {
239
+ if (isAstNode(child)) {
240
+ forEachIdentifier(child, node, childKey, visit);
241
+ }
242
+ }
243
+ }
244
+ }
245
+ /**
246
+ * True when the name still occurs in the source outside `ranges`, ignoring the
247
+ * positions where a name is a module's export name rather than a reference to
248
+ * a local binding.
249
+ *
250
+ * Scope analysis already answers "is this binding referenced", and it is what
251
+ * `no-unused-vars` answers with. This coarser second opinion exists to catch
252
+ * the cases where the two could disagree — a shadowing binding, a construct the
253
+ * scope manager does not link — because a removal that turns out to be wrong
254
+ * deletes working code, while a removal declined in error only leaves the
255
+ * original report standing.
256
+ */
257
+ function nameOccursOutside(source, name, ranges, binding) {
258
+ let found = false;
259
+ forEachIdentifier(source.ast, undefined, undefined, (node, parent, key) => {
260
+ if (found || node.name !== name)
261
+ return;
262
+ if (isWithinAny(node.range, ranges))
263
+ return;
264
+ if (contains(binding.range, node.range))
265
+ return;
266
+ if (parent?.type === utils_1.AST_NODE_TYPES.ImportSpecifier && key === 'imported') {
267
+ return;
268
+ }
269
+ if (parent?.type === utils_1.AST_NODE_TYPES.ExportSpecifier && key === 'exported') {
270
+ return;
271
+ }
272
+ found = true;
273
+ });
274
+ return found;
275
+ }
276
+ function patternIdentifiers(pattern) {
277
+ switch (pattern?.type) {
278
+ case utils_1.AST_NODE_TYPES.Identifier:
279
+ return [pattern];
280
+ case utils_1.AST_NODE_TYPES.ObjectPattern:
281
+ return pattern.properties.flatMap((property) => patternIdentifiers(property.type === utils_1.AST_NODE_TYPES.RestElement
282
+ ? property.argument
283
+ : property.value));
284
+ case utils_1.AST_NODE_TYPES.ArrayPattern:
285
+ return pattern.elements.flatMap((element) => patternIdentifiers(element ?? null));
286
+ case utils_1.AST_NODE_TYPES.AssignmentPattern:
287
+ return patternIdentifiers(pattern.left);
288
+ case utils_1.AST_NODE_TYPES.RestElement:
289
+ return patternIdentifiers(pattern.argument);
290
+ default:
291
+ return [];
292
+ }
293
+ }
294
+ /**
295
+ * The names the program exports by declaration. A binding among them has a
296
+ * consumer no edit to this file can reach, so no deletion here orphans it.
297
+ *
298
+ * Only the exported name itself counts — a type parameter or a local declared
299
+ * *inside* an exported function is not exported, and treating it as such would
300
+ * wave through the very deletion this guard exists to catch.
301
+ */
302
+ function exportedBindingKeys(ast) {
303
+ const keys = new Set();
304
+ for (const statement of ast.body) {
305
+ if (statement.type !== utils_1.AST_NODE_TYPES.ExportNamedDeclaration &&
306
+ statement.type !== utils_1.AST_NODE_TYPES.ExportDefaultDeclaration) {
307
+ continue;
308
+ }
309
+ const declaration = statement.declaration;
310
+ const identifiers = declaration?.type === utils_1.AST_NODE_TYPES.VariableDeclaration
311
+ ? declaration.declarations.flatMap((declarator) => patternIdentifiers(declarator.id))
312
+ : declaration && 'id' in declaration
313
+ ? patternIdentifiers(declaration.id ?? null)
314
+ : [];
315
+ for (const identifier of identifiers) {
316
+ keys.add(`${identifier.range[0]},${identifier.range[1]}`);
317
+ }
318
+ }
319
+ return keys;
320
+ }
321
+ /**
322
+ * The extra ranges a fix must delete so that removing `removed` leaves nothing
323
+ * bound to nothing, or `null` when some binding would be orphaned yet cannot be
324
+ * unbound safely — an import behind a directive comment, a type alias or a type
325
+ * parameter, all of which this helper refuses to guess at.
326
+ *
327
+ * `null` is a demand, not a suggestion: the caller declines its whole fix. The
328
+ * alternative — deleting the last use and keeping the declaration — converts a
329
+ * file that lints clean into one that fails `no-unused-vars`, and since the
330
+ * original report is resolved by the fix, nothing re-reports the debt.
331
+ *
332
+ * `removed` is one fix's own deletion, judged against the file as it stands.
333
+ * Widening it to "everything this `--fix` run will delete" looks like it would
334
+ * catch a binding two edits share, but it is unsound: a rule cannot see
335
+ * `eslint-disable` comments, because suppression is applied to reports after the
336
+ * rule emits them. A suppressed sibling edit never happens, so the widened view
337
+ * deletes an import that a surviving reference still needs — trading an unused
338
+ * import for a dangling type reference, a lint warning for a compile error.
339
+ * There is deliberately no way to ask this helper that question.
340
+ */
341
+ function planOrphanedImportRemoval(source, removed) {
342
+ const orphaned = [];
343
+ const names = [];
344
+ const exported = exportedBindingKeys(source.ast);
345
+ for (const variable of allVariables(source.scopeManager)) {
346
+ // A global has no declaration in this file, so nothing here can orphan it.
347
+ if (variable.defs.length === 0)
348
+ continue;
349
+ const references = variable.references.map((reference) => reference.identifier.range);
350
+ // Only bindings this deletion touches are its business, and only those it
351
+ // leaves with nothing at all are unbound: a reference that survives this
352
+ // edit keeps the binding alive, whatever another edit might later do to it.
353
+ if (!references.some((range) => isWithinAny(range, removed)))
354
+ continue;
355
+ if (!references.every((range) => isWithinAny(range, removed)))
356
+ continue;
357
+ if (variable.identifiers.some((identifier) => exported.has(`${identifier.range[0]},${identifier.range[1]}`))) {
358
+ continue;
359
+ }
360
+ const specifier = isImportBindingVariable(variable)
361
+ ? importBindingSpecifierOf(variable)
362
+ : undefined;
363
+ // Orphaned, yet bound by something this helper cannot rewrite.
364
+ if (!specifier)
365
+ return null;
366
+ orphaned.push(specifier);
367
+ names.push(variable.name);
368
+ }
369
+ if (orphaned.length === 0)
370
+ return [];
371
+ if (names.some((name, index) => nameOccursOutside(source, name, removed, orphaned[index]))) {
372
+ return null;
373
+ }
374
+ const byDeclaration = new Map();
375
+ for (const specifier of orphaned) {
376
+ const declaration = declarationOf(source, specifier);
377
+ if (!declaration)
378
+ return null;
379
+ const group = byDeclaration.get(declaration);
380
+ if (group) {
381
+ group.push(specifier);
382
+ }
383
+ else {
384
+ byDeclaration.set(declaration, [specifier]);
385
+ }
386
+ }
387
+ const ranges = [];
388
+ for (const group of byDeclaration.values()) {
389
+ const plan = planImportBindingRemoval(source, group);
390
+ if (!plan)
391
+ return null;
392
+ ranges.push(...plan);
393
+ }
394
+ return ranges.sort((left, right) => left[0] - right[0]);
395
+ }
396
+ exports.planOrphanedImportRemoval = planOrphanedImportRemoval;
397
+ //# sourceMappingURL=importRemoval.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blumintinc/eslint-plugin-blumint",
3
- "version": "1.20.85",
3
+ "version": "1.20.87",
4
4
  "description": "Custom eslint rules for use within BluMint",
5
5
  "author": {
6
6
  "name": "Brodie McGuire",
@@ -1,4 +1,168 @@
1
1
  [
2
+ {
3
+ "version": "1.20.87",
4
+ "date": "2026-08-03T08:16:33.082Z",
5
+ "rules": [
6
+ {
7
+ "name": "enforce-assert-safe-object-key",
8
+ "changeType": "fix",
9
+ "issues": [
10
+ 1648
11
+ ],
12
+ "summary": "keep the autofix's added import below the file prologue (#1648)"
13
+ },
14
+ {
15
+ "name": "enforce-memoize-async",
16
+ "changeType": "fix",
17
+ "issues": [
18
+ 1648
19
+ ],
20
+ "summary": "keep the autofix's added import below the file prologue (#1648)"
21
+ },
22
+ {
23
+ "name": "enforce-memoize-getters",
24
+ "changeType": "fix",
25
+ "issues": [
26
+ 1648
27
+ ],
28
+ "summary": "keep the autofix's added import below the file prologue (#1648)"
29
+ },
30
+ {
31
+ "name": "enforce-microdiff",
32
+ "changeType": "fix",
33
+ "issues": [
34
+ 1648
35
+ ],
36
+ "summary": "keep the autofix's added import below the file prologue (#1648)"
37
+ },
38
+ {
39
+ "name": "enforce-querykey-ts",
40
+ "changeType": "fix",
41
+ "issues": [
42
+ 1648
43
+ ],
44
+ "summary": "keep the autofix's added import below the file prologue (#1648)"
45
+ },
46
+ {
47
+ "name": "enforce-safe-stringify",
48
+ "changeType": "fix",
49
+ "issues": [
50
+ 1648
51
+ ],
52
+ "summary": "keep the suggestion's added import below the file prologue (#1648)"
53
+ },
54
+ {
55
+ "name": "enforce-snapshot-state-narrowing",
56
+ "changeType": "fix",
57
+ "issues": [
58
+ 1648
59
+ ],
60
+ "summary": "keep the suggestion's added import below the file prologue (#1648)"
61
+ },
62
+ {
63
+ "name": "enforce-stable-hash-spread-props",
64
+ "changeType": "fix",
65
+ "issues": [
66
+ 1648
67
+ ],
68
+ "summary": "keep the autofix's added import below the file prologue (#1648)"
69
+ },
70
+ {
71
+ "name": "logical-top-to-bottom-grouping",
72
+ "changeType": "fix",
73
+ "issues": [
74
+ 1651
75
+ ],
76
+ "summary": "never reorder across a sequential-await run parallelize-async-operations owns (closes #1651)"
77
+ },
78
+ {
79
+ "name": "no-array-length-in-deps",
80
+ "changeType": "fix",
81
+ "issues": [
82
+ 1648
83
+ ],
84
+ "summary": "keep the autofix's added import below the file prologue (#1648)"
85
+ },
86
+ {
87
+ "name": "no-empty-dependency-use-callbacks",
88
+ "changeType": "fix",
89
+ "issues": [
90
+ 1650
91
+ ],
92
+ "summary": "unbind the hook import the hoist empties (closes #1650)"
93
+ },
94
+ {
95
+ "name": "no-explicit-return-type",
96
+ "changeType": "fix",
97
+ "issues": [
98
+ 1649
99
+ ],
100
+ "summary": "drop the import the stripped annotation solely consumed (closes #1649)"
101
+ },
102
+ {
103
+ "name": "prefer-fragment-component",
104
+ "changeType": "fix",
105
+ "issues": [
106
+ 1648
107
+ ],
108
+ "summary": "keep the autofix's added import below the file prologue (#1648)"
109
+ },
110
+ {
111
+ "name": "prefer-global-router-state-key",
112
+ "changeType": "fix",
113
+ "issues": [
114
+ 1648
115
+ ],
116
+ "summary": "keep the autofix's added import below the file prologue (#1648)"
117
+ },
118
+ {
119
+ "name": "prefer-use-deep-compare-memo",
120
+ "changeType": "fix",
121
+ "issues": [
122
+ 1648
123
+ ],
124
+ "summary": "keep the autofix's added import below the file prologue (#1648)"
125
+ },
126
+ {
127
+ "name": "react-memoize-literals",
128
+ "changeType": "fix",
129
+ "issues": [
130
+ 1648
131
+ ],
132
+ "summary": "keep the suggestion's added import below the file prologue (#1648)"
133
+ },
134
+ {
135
+ "name": "require-memo",
136
+ "changeType": "fix",
137
+ "issues": [
138
+ 1648
139
+ ],
140
+ "summary": "keep the autofix's added import below the file prologue (#1648)"
141
+ },
142
+ {
143
+ "name": "require-memoize-jsx-returners",
144
+ "changeType": "fix",
145
+ "issues": [
146
+ 1648
147
+ ],
148
+ "summary": "keep the autofix's added import below the file prologue (closes #1648)"
149
+ }
150
+ ]
151
+ },
152
+ {
153
+ "version": "1.20.86",
154
+ "date": "2026-08-03T03:52:41.277Z",
155
+ "rules": [
156
+ {
157
+ "name": "prefer-spread-over-reassembly",
158
+ "changeType": "fix",
159
+ "issues": [
160
+ 1644
161
+ ],
162
+ "summary": "prove narrowing through a relative import (closes #1644)"
163
+ }
164
+ ]
165
+ },
2
166
  {
3
167
  "version": "1.20.85",
4
168
  "date": "2026-08-03T03:21:29.711Z",