@blumintinc/eslint-plugin-blumint 1.20.87 → 1.20.88

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.87',
226
+ version: '1.20.88',
227
227
  },
228
228
  parseOptions: {
229
229
  ecmaVersion: 2020,
@@ -263,6 +263,55 @@ const brokenOpenCallText = (sourceCode, call, callback, head, indentUnit) => {
263
263
  const comma = wantsTrailingComma(sourceCode, call) ? ',' : '';
264
264
  return `${head}(\n${calleeIndent}${moved}${comma}\n${callIndent})`;
265
265
  };
266
+ /** Whether a range sits wholly inside one of the ranges a fix deletes. */
267
+ const fallsInside = (ranges, range) => ranges.some(([start, end]) => start <= range[0] && range[1] <= end);
268
+ /**
269
+ * Whether the binding belongs to a function rather than to the module. A
270
+ * module-scope binding is left alone because this file cannot settle whether it
271
+ * is dead: it may be exported, re-exported, or declared for a side effect,
272
+ * whereas a binding declared inside a component or hook is reachable only from
273
+ * the references this file spells out.
274
+ */
275
+ const isFunctionLocal = (variable) => {
276
+ for (let scope = variable.scope; scope; scope = scope.upper) {
277
+ if (scope.type === 'function') {
278
+ return true;
279
+ }
280
+ }
281
+ return false;
282
+ };
283
+ /** The bindings a reference inside one of the deleted ranges resolves to. */
284
+ const variablesReferencedIn = (root, deletedRanges) => {
285
+ const referenced = new Set();
286
+ const walk = (scope) => {
287
+ for (const reference of scope.references) {
288
+ if (reference.resolved &&
289
+ fallsInside(deletedRanges, reference.identifier.range)) {
290
+ referenced.add(reference.resolved);
291
+ }
292
+ }
293
+ scope.childScopes.forEach(walk);
294
+ };
295
+ walk(root);
296
+ return referenced;
297
+ };
298
+ /**
299
+ * Whether deleting the given ranges would strip the last read of a binding
300
+ * declared inside a function, leaving a declaration — and whatever imports feed
301
+ * it — that nothing uses.
302
+ *
303
+ * A dependency array can hold the sole reference to a value computed for it
304
+ * alone, which is precisely what `no-array-length-in-deps` produces when it
305
+ * hoists `const listHash = useMemo(() => stableHash(list), [list])` and points
306
+ * the dependency at it. Dropping the array then strands that statement, turning
307
+ * a clean file into one `no-unused-vars` rejects with a violation this plugin
308
+ * cannot itself fix (issue #1652). Only a write left behind counts as no use at
309
+ * all, matching how an unused-variable check reads the result.
310
+ */
311
+ const orphansLocalBinding = (root, deletedRanges) => [...variablesReferencedIn(root, deletedRanges)]
312
+ .filter((variable) => variable.defs.length > 0 && isFunctionLocal(variable))
313
+ .some((variable) => variable.references.every((reference) => !reference.isRead() ||
314
+ fallsInside(deletedRanges, reference.identifier.range)));
266
315
  exports.useLatestCallback = (0, createRule_1.createRule)({
267
316
  name: 'use-latest-callback',
268
317
  meta: {
@@ -676,6 +725,14 @@ exports.useLatestCallback = (0, createRule_1.createRule)({
676
725
  }
677
726
  return texts;
678
727
  };
728
+ // Everything a rewritten call carries past its callback argument — the
729
+ // dependency array, and any further argument — is absent from the
730
+ // replacement text, so those ranges are what the fix deletes.
731
+ const droppedRanges = batchedConversions.map((conversion) => [
732
+ conversion.node.arguments[0].range[1],
733
+ conversion.node.range[1],
734
+ ]);
735
+ const programScope = ASTHelpers_1.ASTHelpers.getScope(context, program);
679
736
  // Every call-site conversion and the import rewrite ride on ONE fix
680
737
  // from ONE report. ESLint discards a multi-part fix wholesale when any
681
738
  // part conflicts with another rule's fix and retries it on the next
@@ -700,6 +757,14 @@ exports.useLatestCallback = (0, createRule_1.createRule)({
700
757
  if (!batchedConversions.every(reachesHook)) {
701
758
  return null;
702
759
  }
760
+ // Deleting the dependency arrays must not strand a binding that
761
+ // exists only to be listed in one. The batch is atomic, so a single
762
+ // orphaned binding withholds all of it, and the violation still
763
+ // stands as a report: the author drops the dead declaration
764
+ // together with the array.
765
+ if (orphansLocalBinding(programScope, droppedRanges)) {
766
+ return null;
767
+ }
703
768
  const texts = conversionTexts();
704
769
  return [
705
770
  ...batchedConversions.map((conversion) => fixer.replaceText(conversion.node, texts.get(conversion.node))),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blumintinc/eslint-plugin-blumint",
3
- "version": "1.20.87",
3
+ "version": "1.20.88",
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.88",
4
+ "date": "2026-08-03T09:05:50.265Z",
5
+ "rules": [
6
+ {
7
+ "name": "use-latest-callback",
8
+ "changeType": "fix",
9
+ "issues": [
10
+ 1652
11
+ ],
12
+ "summary": "decline the fix when dropping the deps array orphans a local binding (closes #1652)"
13
+ }
14
+ ]
15
+ },
2
16
  {
3
17
  "version": "1.20.87",
4
18
  "date": "2026-08-03T08:16:33.082Z",