@blumintinc/eslint-plugin-blumint 1.21.6 → 1.21.8

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
@@ -224,7 +224,7 @@ function noFrontendImportsFromFunctionsPatterns(pattern) {
224
224
  module.exports = {
225
225
  meta: {
226
226
  name: '@blumintinc/eslint-plugin-blumint',
227
- version: '1.21.6',
227
+ version: '1.21.8',
228
228
  },
229
229
  parseOptions: {
230
230
  ecmaVersion: 2020,
@@ -305,6 +305,34 @@ const isWriteTarget = (node) => {
305
305
  return false;
306
306
  }
307
307
  };
308
+ /**
309
+ * The composite literal a reference is STORED INTO — the object in
310
+ * `{ items: ITEMS }`, the array in `[ITEMS]` — or null for every other
311
+ * position.
312
+ *
313
+ * Storing a reference does not copy it: the same array stays reachable through
314
+ * the container, so `holder.items.push(3)` writes through to the binding
315
+ * exactly as a direct alias does, and freezing it raises the same TS2339. A
316
+ * `SpreadElement` is excluded because it genuinely builds a fresh value
317
+ * (`const COPY = [...ITEMS]`), and a computed key is excluded because it coerces
318
+ * the reference to a property name rather than retaining it.
319
+ */
320
+ const storageContainerOf = (node) => {
321
+ const parent = node.parent;
322
+ if (!parent) {
323
+ return null;
324
+ }
325
+ if (parent.type === utils_1.AST_NODE_TYPES.Property &&
326
+ parent.value === node &&
327
+ parent.parent?.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
328
+ return parent.parent;
329
+ }
330
+ if (parent.type === utils_1.AST_NODE_TYPES.ArrayExpression &&
331
+ parent.elements.some((element) => element === node)) {
332
+ return parent;
333
+ }
334
+ return null;
335
+ };
308
336
  /**
309
337
  * The declarator a reference initializes IN WHOLE — `OTHER` in
310
338
  * `const OTHER = ITEMS` — or null for every other position. Such a declaration
@@ -319,24 +347,112 @@ const isWriteTarget = (node) => {
319
347
  * tolerated — staying silent is the cheap error here, emitting a fix that stops
320
348
  * the file compiling is not.
321
349
  *
322
- * A reference that is only PART of an initializer builds a fresh value rather
323
- * than aliasing this one (`const COPY = [...ITEMS]`), and a destructuring id
324
- * extracts a member rather than the whole, so neither is an alias here.
350
+ * A reference STORED INTO a composite literal is followed through that
351
+ * container, since storing does not copy — see `storageContainerOf`. A
352
+ * destructuring id extracts a member rather than the whole, so it is not an
353
+ * alias here.
325
354
  */
326
355
  const aliasDeclaratorOf = (identifier) => {
327
- const value = outermostValueOf(identifier);
328
- const declarator = value.parent;
329
- if (declarator?.type !== utils_1.AST_NODE_TYPES.VariableDeclarator ||
330
- declarator.init !== value ||
331
- declarator.id.type !== utils_1.AST_NODE_TYPES.Identifier) {
332
- return null;
356
+ // Ascends strictly, so reaching a node with no parent terminates the walk.
357
+ let value = outermostValueOf(identifier);
358
+ for (;;) {
359
+ const declarator = value.parent;
360
+ if (declarator?.type === utils_1.AST_NODE_TYPES.VariableDeclarator &&
361
+ declarator.init === value &&
362
+ declarator.id.type === utils_1.AST_NODE_TYPES.Identifier) {
363
+ return declarator;
364
+ }
365
+ const container = storageContainerOf(value);
366
+ if (!container) {
367
+ return null;
368
+ }
369
+ value = outermostValueOf(container);
333
370
  }
334
- return declarator;
335
371
  };
372
+ /** Pattern nodes a parameter's binding can be nested inside. */
373
+ const PATTERN_CONTAINERS = new Set([
374
+ utils_1.AST_NODE_TYPES.AssignmentPattern,
375
+ utils_1.AST_NODE_TYPES.Property,
376
+ utils_1.AST_NODE_TYPES.ObjectPattern,
377
+ utils_1.AST_NODE_TYPES.ArrayPattern,
378
+ utils_1.AST_NODE_TYPES.RestElement,
379
+ ]);
380
+ const FUNCTION_TYPES = new Set([
381
+ utils_1.AST_NODE_TYPES.FunctionDeclaration,
382
+ utils_1.AST_NODE_TYPES.FunctionExpression,
383
+ utils_1.AST_NODE_TYPES.ArrowFunctionExpression,
384
+ utils_1.AST_NODE_TYPES.TSDeclareFunction,
385
+ ]);
336
386
  /**
337
- * Whether the binding is written through anywhere in the file, under its own
338
- * name or through an alias of it. Answered from the scope manager's reference
339
- * list rather than a textual search for the name, so a same-named binding in
387
+ * Whether a default value is what a PARAMETER's type is inferred FROM.
388
+ *
389
+ * Answered false in the two cases where freezing the default cannot change a
390
+ * signature: the parameter carries a type annotation, so its type is declared
391
+ * rather than inferred — looked for up the whole pattern, since a destructured
392
+ * parameter carries it on the pattern (`({ distance = DEFAULT }: Props)`) and
393
+ * a plain one on its binding (`(model: ModelName = DEFAULT)`) — or the default
394
+ * belongs to a destructuring declaration rather than a parameter list, which
395
+ * declares no signature at all.
396
+ */
397
+ const isInferredParameterDefault = (pattern) => {
398
+ let current = pattern;
399
+ for (;;) {
400
+ if (current.typeAnnotation) {
401
+ return false;
402
+ }
403
+ const parent = current.parent;
404
+ if (!parent) {
405
+ return false;
406
+ }
407
+ if (FUNCTION_TYPES.has(parent.type)) {
408
+ return parent.params.includes(current);
409
+ }
410
+ if (!PATTERN_CONTAINERS.has(parent.type)) {
411
+ return false;
412
+ }
413
+ current = parent;
414
+ }
415
+ };
416
+ /**
417
+ * Whether a reference sits where TypeScript INFERS a type from it — the value
418
+ * of a default parameter, reached directly or through a composite literal it
419
+ * is stored into.
420
+ *
421
+ * `as const` does not only freeze: it makes the literal type NON-WIDENING, and
422
+ * an inference site that widened `'ready'` to `string` then keeps the literal.
423
+ * A parameter defaulted from the constant therefore narrows to that one value,
424
+ * and every call passing a different one stops compiling (TS2345) for an input
425
+ * that compiled. The mutation walk cannot see this: nothing is written, the
426
+ * signature is simply inferred from a value the assertion changes.
427
+ */
428
+ const isInferenceSite = (identifier) => {
429
+ let value = outermostValueOf(identifier);
430
+ for (;;) {
431
+ const parent = value.parent;
432
+ if (parent?.type === utils_1.AST_NODE_TYPES.AssignmentPattern &&
433
+ parent.right === value) {
434
+ return isInferredParameterDefault(parent.left);
435
+ }
436
+ const container = storageContainerOf(value);
437
+ if (!container) {
438
+ return false;
439
+ }
440
+ value = outermostValueOf(container);
441
+ }
442
+ };
443
+ /**
444
+ * Whether anything in the file stops this binding taking `as const`, under its
445
+ * own name or through an alias of it.
446
+ *
447
+ * Two things disqualify it, because `as const` does two things. It freezes the
448
+ * value, so a WRITE — through the binding (`X.push(1)`), or to a binding that
449
+ * aliases it (`other = X`) — becomes TS2339/TS2540. And it makes the literal
450
+ * type NON-WIDENING, so an INFERENCE site that read the widened type keeps the
451
+ * literal instead, which rewrites a signature the assertion was never asked to
452
+ * touch.
453
+ *
454
+ * Answered from the scope manager's reference list rather than a textual
455
+ * search for the name, so a same-named binding in
340
456
  * another scope (`const arr` shadowed inside a callback) contributes nothing,
341
457
  * and a same-named method on an unrelated receiver (`other.push(1)`) is never
342
458
  * even visited.
@@ -358,13 +474,24 @@ const aliasDeclaratorOf = (identifier) => {
358
474
  * check keyed on `const` would leave the `let` spelling breaking builds under
359
475
  * `--fix`.
360
476
  */
361
- const isBindingMutated = (variable, declaredVariablesOf) => {
477
+ const blocksAsConstAssertion = (variable, declaredVariablesOf) => {
362
478
  // Grown in place and walked by index: an alias found mid-walk is appended and
363
479
  // reached by the same loop, so the traversal needs no recursion of its own.
364
480
  const pending = [variable];
365
481
  const visited = new Set(pending);
366
482
  for (let index = 0; index < pending.length; index += 1) {
367
483
  for (const reference of pending[index].references) {
484
+ // Reassigning an alias is as disqualifying as writing through one. A
485
+ // binding that takes its type from the constant narrows to the frozen
486
+ // literal, so `let stage = DEFAULT; stage = 'live';` becomes TS2322 for
487
+ // an input that compiled. `init` excludes the declaration's own write,
488
+ // which is how the alias was established rather than a change to it.
489
+ if (reference.isWrite() && !reference.init) {
490
+ return true;
491
+ }
492
+ if (isInferenceSite(reference.identifier)) {
493
+ return true;
494
+ }
368
495
  const path = accessPathOf(reference.identifier);
369
496
  if (path !== null) {
370
497
  if (isMutatingMethodCall(path) || isWriteTarget(path)) {
@@ -801,7 +928,7 @@ exports.default = (0, createRule_1.createRule)({
801
928
  .getDeclaredVariables(declaration)
802
929
  .find((variable) => variable.name === name);
803
930
  return (!declaredVariable ||
804
- !isBindingMutated(declaredVariable, declaredVariablesOf));
931
+ !blocksAsConstAssertion(declaredVariable, declaredVariablesOf));
805
932
  };
806
933
  if (shouldHaveAsConst(init)) {
807
934
  context.report({
@@ -68,6 +68,28 @@ function isFirestoreModuleSource(source) {
68
68
  return (productSegment === product || !!productSegment?.startsWith(`${product}-`));
69
69
  });
70
70
  }
71
+ /**
72
+ * First segments that mark a path alias into the project's own tree rather than
73
+ * a published package. A scoped package's first segment is `@scope`, so a bare
74
+ * `@` can only be an alias.
75
+ */
76
+ const FIRST_PARTY_SOURCE_ROOTS = new Set(['@', '~', 'src', 'app', 'lib']);
77
+ /**
78
+ * Whether a specifier names a published package, which is the only thing that
79
+ * can REFUTE Firestore provenance.
80
+ *
81
+ * A relative or absolute path, or a path alias, resolves into first-party code
82
+ * this rule cannot follow, so such a source says nothing about which product
83
+ * the binding came from. Reading its failure to look like `firebase/firestore`
84
+ * as proof of a different product is what silenced the rule on every
85
+ * `import { db } from '../../config/firebaseAdmin'` re-export.
86
+ */
87
+ function isBarePackageSource(source) {
88
+ if (source.startsWith('.') || source.startsWith('/') || source === '') {
89
+ return false;
90
+ }
91
+ return !FIRST_PARTY_SOURCE_ROOTS.has(moduleSegments(source)[0]);
92
+ }
71
93
  /**
72
94
  * The module `name` is imported from, or null when the file declares the name
73
95
  * itself (a local helper, a parameter) or nothing declares it at all.
@@ -117,11 +139,13 @@ function provenanceIdentifier(callee) {
117
139
  /**
118
140
  * Whether a `runTransaction` call is the Firestore one this rule speaks about.
119
141
  *
120
- * The gate speaks only when it knows: a binding that resolves to an import is
121
- * judged by its module source, and anything else — a bare call, a parameter, a
122
- * local helper, a member call on an unresolvable receiver — keeps the rule's
123
- * posture of reporting, since a name with no traceable origin is far more often
124
- * Firestore (`db.runTransaction(...)`) than not.
142
+ * The gate speaks only when it knows: a binding that resolves to an import of a
143
+ * published package is judged by its module source, and anything else — a bare
144
+ * call, a parameter, a local helper, a member call on an unresolvable receiver,
145
+ * an import from the project's own tree — keeps the rule's posture of
146
+ * reporting, since a name with no traceable origin is far more often Firestore
147
+ * (`db.runTransaction(...)`) than not. Only a package specifier can refute
148
+ * Firestore; a first-party path merely fails to confirm it.
125
149
  */
126
150
  function isFirestoreTransactionCall(node, context) {
127
151
  if (!isRunTransactionCall(node)) {
@@ -132,7 +156,7 @@ function isFirestoreTransactionCall(node, context) {
132
156
  return true;
133
157
  }
134
158
  const source = importedSourceOf(ASTHelpers_1.ASTHelpers.getScope(context, node), carrier.name);
135
- if (source === null) {
159
+ if (source === null || !isBarePackageSource(source)) {
136
160
  return true;
137
161
  }
138
162
  return isFirestoreModuleSource(source);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blumintinc/eslint-plugin-blumint",
3
- "version": "1.21.6",
3
+ "version": "1.21.8",
4
4
  "description": "Custom eslint rules for use within BluMint",
5
5
  "author": {
6
6
  "name": "Brodie McGuire",
@@ -1,4 +1,40 @@
1
1
  [
2
+ {
3
+ "version": "1.21.8",
4
+ "date": "2026-09-05T03:15:57.605Z",
5
+ "rules": [
6
+ {
7
+ "name": "global-const-style",
8
+ "changeType": "fix",
9
+ "issues": [
10
+ 2329
11
+ ],
12
+ "summary": "withhold the assertion where a type is inferred from the constant (closes #2329)"
13
+ }
14
+ ]
15
+ },
16
+ {
17
+ "version": "1.21.7",
18
+ "date": "2026-09-04T21:52:00.765Z",
19
+ "rules": [
20
+ {
21
+ "name": "global-const-style",
22
+ "changeType": "fix",
23
+ "issues": [
24
+ 2327
25
+ ],
26
+ "summary": "follow the binding into a container (closes #2327)"
27
+ },
28
+ {
29
+ "name": "no-try-catch-already-exists-in-transaction",
30
+ "changeType": "fix",
31
+ "issues": [
32
+ 2326
33
+ ],
34
+ "summary": "report on first-party imports (closes #2326)"
35
+ }
36
+ ]
37
+ },
2
38
  {
3
39
  "version": "1.21.6",
4
40
  "date": "2026-09-04T20:52:21.747Z",