@blumintinc/eslint-plugin-blumint 1.20.90 → 1.20.91

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/lib/index.js CHANGED
@@ -223,7 +223,7 @@ function noFrontendImportsFromFunctionsPatterns(pattern) {
223
223
  module.exports = {
224
224
  meta: {
225
225
  name: '@blumintinc/eslint-plugin-blumint',
226
- version: '1.20.90',
226
+ version: '1.20.91',
227
227
  },
228
228
  parseOptions: {
229
229
  ecmaVersion: 2020,
@@ -5,6 +5,7 @@ const utils_1 = require("@typescript-eslint/utils");
5
5
  const createRule_1 = require("../utils/createRule");
6
6
  const ASTHelpers_1 = require("../utils/ASTHelpers");
7
7
  const importInsertion_1 = require("../utils/importInsertion");
8
+ const importRemoval_1 = require("../utils/importRemoval");
8
9
  const DEEP_COMPARE_MODULE = '@blumintinc/use-deep-compare';
9
10
  const DEEP_COMPARE_HOOK = 'useDeepCompareMemo';
10
11
  // Consider these as memoizing hooks producing stable references
@@ -218,49 +219,6 @@ function ensureDeepCompareImportFixes(context, fixer) {
218
219
  (0, importInsertion_1.insertAtImportAnchor)(sourceCode, fixer, (0, importInsertion_1.importAnchorLineStart)(sourceCode, anchor), importText),
219
220
  ];
220
221
  }
221
- function findVariableInScope(scope, name) {
222
- if (!scope)
223
- return null;
224
- const found = scope.variables?.find((v) => v.name === name);
225
- if (found)
226
- return found;
227
- if (Array.isArray(scope.childScopes)) {
228
- for (const child of scope.childScopes) {
229
- const v = findVariableInScope(child, name);
230
- if (v)
231
- return v;
232
- }
233
- }
234
- return null;
235
- }
236
- function isIdentifierReferenced(sourceCode, name) {
237
- const scope = sourceCode.scopeManager?.globalScope;
238
- if (!scope)
239
- return true;
240
- const variable = findVariableInScope(scope, name);
241
- if (!variable)
242
- return false;
243
- return variable.references.some((ref) => ref.identifier && ref.identifier.name === name);
244
- }
245
- function removeImportSpecifierFixes(sourceCode, fixer, importDecl, specifier) {
246
- const fixes = [];
247
- if (importDecl.specifiers.length === 1) {
248
- fixes.push(fixer.remove(importDecl));
249
- return fixes;
250
- }
251
- const tokenAfter = sourceCode.getTokenAfter(specifier);
252
- const tokenBefore = sourceCode.getTokenBefore(specifier);
253
- if (tokenAfter && tokenAfter.value === ',') {
254
- fixes.push(fixer.removeRange([specifier.range[0], tokenAfter.range[1]]));
255
- }
256
- else if (tokenBefore && tokenBefore.value === ',') {
257
- fixes.push(fixer.removeRange([tokenBefore.range[0], specifier.range[1]]));
258
- }
259
- else {
260
- fixes.push(fixer.remove(specifier));
261
- }
262
- return fixes;
263
- }
264
222
  function isImportedIdentifier(context, name) {
265
223
  const sourceCode = context.sourceCode;
266
224
  const program = sourceCode.ast;
@@ -436,6 +394,28 @@ exports.preferUseDeepCompareMemo = (0, createRule_1.createRule)({
436
394
  !bindsHookImport(existing, findDeepCompareMemoImport(context.sourceCode.ast))) {
437
395
  return null;
438
396
  }
397
+ // The callee is the text this fix deletes, so it is also the text
398
+ // whatever carried the hook stops being read from: `useMemo` for a
399
+ // bare call, `React` for a member call. A binding left with no
400
+ // reference at all is unbound here, in this same fix — stripping
401
+ // its last use and keeping the declaration trades this rule's
402
+ // report for an unused-import one, and nothing re-reports that debt
403
+ // once the rewrite has resolved the original violation.
404
+ //
405
+ // Orphanhood is judged against this one callee's removal and the
406
+ // file as it stands. A second `useMemo` call site keeps the
407
+ // specifier, even one this rule also reports: that sibling may be
408
+ // `eslint-disable`d (suppression is applied after a rule emits its
409
+ // reports, so its fix never runs) or lose its fix to a conflict, and
410
+ // either way the surviving call would spell a name nothing binds. A
411
+ // later pass, reading a source where the sibling is already
412
+ // converted, finds the specifier orphaned and removes it then.
413
+ const importRemoval = (0, importRemoval_1.planOrphanedImportRemoval)(context.sourceCode, [node.callee.range]);
414
+ // No plan means a binding is orphaned yet cannot be unbound safely,
415
+ // so the rewrite stays too: the report without a fixer is the lesser
416
+ // damage.
417
+ if (!importRemoval)
418
+ return null;
439
419
  const fixes = [];
440
420
  // Replace callee
441
421
  if (node.callee.type === utils_1.AST_NODE_TYPES.Identifier) {
@@ -446,26 +426,7 @@ exports.preferUseDeepCompareMemo = (0, createRule_1.createRule)({
446
426
  }
447
427
  // Ensure import exists
448
428
  fixes.push(...ensureDeepCompareImportFixes(context, fixer));
449
- // Clean up now-unused React/useMemo imports if safe
450
- const sourceCode = context.sourceCode;
451
- const program = sourceCode.ast;
452
- const reactImport = program.body.find((n) => n.type === utils_1.AST_NODE_TYPES.ImportDeclaration &&
453
- n.source.value === 'react');
454
- if (reactImport) {
455
- // remove named useMemo if unused
456
- const useMemoSpec = reactImport.specifiers.find((s) => s.type === utils_1.AST_NODE_TYPES.ImportSpecifier &&
457
- s.imported.type === utils_1.AST_NODE_TYPES.Identifier &&
458
- s.imported.name === 'useMemo');
459
- if (useMemoSpec &&
460
- !isIdentifierReferenced(sourceCode, useMemoSpec.local.name)) {
461
- fixes.push(...removeImportSpecifierFixes(sourceCode, fixer, reactImport, useMemoSpec));
462
- }
463
- const defaultSpec = reactImport.specifiers.find((s) => s.type === utils_1.AST_NODE_TYPES.ImportDefaultSpecifier);
464
- if (defaultSpec &&
465
- !isIdentifierReferenced(sourceCode, defaultSpec.local.name)) {
466
- fixes.push(...removeImportSpecifierFixes(sourceCode, fixer, reactImport, defaultSpec));
467
- }
468
- }
429
+ fixes.push(...importRemoval.map((range) => fixer.removeRange([range[0], range[1]])));
469
430
  return fixes;
470
431
  },
471
432
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blumintinc/eslint-plugin-blumint",
3
- "version": "1.20.90",
3
+ "version": "1.20.91",
4
4
  "description": "Custom eslint rules for use within BluMint",
5
5
  "author": {
6
6
  "name": "Brodie McGuire",
@@ -1,4 +1,18 @@
1
1
  [
2
+ {
3
+ "version": "1.20.91",
4
+ "date": "2026-08-03T15:15:42.831Z",
5
+ "rules": [
6
+ {
7
+ "name": "prefer-use-deep-compare-memo",
8
+ "changeType": "fix",
9
+ "issues": [
10
+ 1661
11
+ ],
12
+ "summary": "unbind the orphaned useMemo import (closes #1661)"
13
+ }
14
+ ]
15
+ },
2
16
  {
3
17
  "version": "1.20.90",
4
18
  "date": "2026-08-03T13:37:18.271Z",