@blumintinc/eslint-plugin-blumint 1.20.189 → 1.20.191

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.189',
226
+ version: '1.20.191',
227
227
  },
228
228
  parseOptions: {
229
229
  ecmaVersion: 2020,
@@ -1255,6 +1255,110 @@ exports.noEntireObjectHookDeps = (0, createRule_1.createRule)({
1255
1255
  return typeInfo;
1256
1256
  }
1257
1257
  const sourceCode = context.getSourceCode();
1258
+ /**
1259
+ * The variable each reference resolves to, keyed by the referencing
1260
+ * identifier NODE rather than by name.
1261
+ *
1262
+ * why: identity keying is what makes the orphan check below immune to
1263
+ * shadowing — a name lookup would resolve an inner `hydrated` against an
1264
+ * outer binding of the same name and read its uses as survivors.
1265
+ */
1266
+ let variableByReference = null;
1267
+ function resolveBinding(identifier) {
1268
+ if (!variableByReference) {
1269
+ variableByReference = new Map();
1270
+ for (const scope of sourceCode.scopeManager?.scopes ?? []) {
1271
+ for (const variable of scope.variables) {
1272
+ for (const reference of variable.references) {
1273
+ variableByReference.set(reference.identifier, variable);
1274
+ }
1275
+ }
1276
+ }
1277
+ }
1278
+ return variableByReference.get(identifier) ?? null;
1279
+ }
1280
+ /**
1281
+ * Every identifier that a hook dependency array holds as an entry.
1282
+ *
1283
+ * why: an entry is a recompute trigger rather than a consumer of the value,
1284
+ * so it cannot keep a declaration alive once the entries are pruned. The
1285
+ * set is built from the rule's own notion of a dependency array — the last
1286
+ * argument of a hook call — so it can never disagree with what the fixer
1287
+ * edits. Keying on the identifier NODE, rather than on ancestry walked from
1288
+ * a reference, keeps the answer available for entries the traversal has not
1289
+ * reached yet, which is precisely the later-hook entry that #2210 turns on.
1290
+ */
1291
+ let dependencyEntryIdentifiers = null;
1292
+ function collectDependencyEntryIdentifiers() {
1293
+ if (dependencyEntryIdentifiers) {
1294
+ return dependencyEntryIdentifiers;
1295
+ }
1296
+ const entries = new Set();
1297
+ const visit = (node) => {
1298
+ if (node.type === utils_1.AST_NODE_TYPES.CallExpression && isHookCall(node)) {
1299
+ const deps = node.arguments[node.arguments.length - 1];
1300
+ if (deps && deps.type === utils_1.AST_NODE_TYPES.ArrayExpression) {
1301
+ for (const entry of deps.elements) {
1302
+ if (entry)
1303
+ entries.add(unwrapExpression(entry));
1304
+ }
1305
+ }
1306
+ }
1307
+ forEachChildNode(node, visit);
1308
+ };
1309
+ visit(sourceCode.ast);
1310
+ dependencyEntryIdentifiers = entries;
1311
+ return entries;
1312
+ }
1313
+ /**
1314
+ * Whether removing `element`'s binding from every dependency array that
1315
+ * lists it would leave the binding with no reader in the file.
1316
+ *
1317
+ * why: a value declared and then read ONLY inside dependency arrays is by
1318
+ * construction load-bearing — the declaration would be pointless otherwise
1319
+ * — so removing any one entry discards a deliberate recompute trigger, and
1320
+ * removing all of them strands the declaration. The consumer runs
1321
+ * `no-unused-vars` as an error and builds with `noUnusedLocals`, so the
1322
+ * rewrite turns a green file red on their machine while staying green here.
1323
+ * Every sibling instance of this class was fixed by deleting the stranded
1324
+ * declaration too, but that remedy is unavailable here: the declaration is
1325
+ * a hook CALL, and dropping it changes the component's hook order.
1326
+ * Declining the edit is the only safe remedy, so the report stands and the
1327
+ * autofix steps aside.
1328
+ *
1329
+ * The verdict deliberately ignores which entry is under repair. A test
1330
+ * scoped to one report's own range calls a sibling array's entry a
1331
+ * survivor, and when two hooks list the same unread dependency both reports
1332
+ * reach that conclusion, both fixes apply in one pass, and the binding is
1333
+ * stranded after all (#2210). Discounting every entry instead gates on a
1334
+ * property this fixer owns: no other fixer can manufacture a
1335
+ * non-dependency read, so no sibling edit can move the answer. The
1336
+ * inference is safe in the conservative direction — if a sibling report is
1337
+ * suppressed the cost is a withheld fix, never a dangling reference.
1338
+ *
1339
+ * Parameters are deliberately exempt. An unread parameter is not an unused
1340
+ * BINDING to either instrument — `no-unused-vars` runs with `args: 'none'`
1341
+ * and `noUnusedLocals` does not cover parameters — so declining there would
1342
+ * withhold a fix without preventing any breakage, and it would silently
1343
+ * settle the reporting question #1621 defers.
1344
+ */
1345
+ function wouldStrandBinding(element) {
1346
+ const identifier = unwrapExpression(element);
1347
+ if (identifier.type !== utils_1.AST_NODE_TYPES.Identifier)
1348
+ return false;
1349
+ const variable = resolveBinding(identifier);
1350
+ if (!variable || variable.defs.length === 0)
1351
+ return false;
1352
+ if (variable.defs.some((def) => def.type === 'Parameter')) {
1353
+ return false;
1354
+ }
1355
+ const entries = collectDependencyEntryIdentifiers();
1356
+ // why: a declarator's own initializer counts as a WRITE reference, so a
1357
+ // survivor test that accepts any reference never fires for `const x = …`
1358
+ // — the shape this check exists for (#1868 is the same trap).
1359
+ return !variable.references.some((reference) => reference.isRead() &&
1360
+ !entries.has(reference.identifier));
1361
+ }
1258
1362
  // why: scanning every comment once per file rather than once per hook call
1259
1363
  // keeps the check off the hot path of files with many hooks.
1260
1364
  let manuallyManagedLines = null;
@@ -1373,6 +1477,10 @@ exports.noEntireObjectHookDeps = (0, createRule_1.createRule)({
1373
1477
  const elementIndex = depsArg.elements.indexOf(element);
1374
1478
  if (elementIndex === -1)
1375
1479
  return null;
1480
+ // The report stands either way; only the rewrite is withheld.
1481
+ if (wouldStrandBinding(element)) {
1482
+ return null;
1483
+ }
1376
1484
  // If this is the only element, just remove it
1377
1485
  if (depsArg.elements.length === 1) {
1378
1486
  return fixer.remove(element);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blumintinc/eslint-plugin-blumint",
3
- "version": "1.20.189",
3
+ "version": "1.20.191",
4
4
  "description": "Custom eslint rules for use within BluMint",
5
5
  "author": {
6
6
  "name": "Brodie McGuire",
@@ -1,4 +1,32 @@
1
1
  [
2
+ {
3
+ "version": "1.20.191",
4
+ "date": "2026-08-29T19:38:08.123Z",
5
+ "rules": [
6
+ {
7
+ "name": "no-entire-object-hook-deps",
8
+ "changeType": "fix",
9
+ "issues": [
10
+ 2210
11
+ ],
12
+ "summary": "discount every dependency entry from the strand check (closes #2210)"
13
+ }
14
+ ]
15
+ },
16
+ {
17
+ "version": "1.20.190",
18
+ "date": "2026-08-29T13:41:20.482Z",
19
+ "rules": [
20
+ {
21
+ "name": "no-entire-object-hook-deps",
22
+ "changeType": "fix",
23
+ "issues": [
24
+ 2209
25
+ ],
26
+ "summary": "decline a removal that strands its binding (closes #2209)"
27
+ }
28
+ ]
29
+ },
2
30
  {
3
31
  "version": "1.20.189",
4
32
  "date": "2026-08-29T11:57:05.586Z",