@blumintinc/eslint-plugin-blumint 1.21.8 → 1.21.9

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.8',
227
+ version: '1.21.9',
228
228
  },
229
229
  parseOptions: {
230
230
  ecmaVersion: 2020,
@@ -313,8 +313,10 @@ const isWriteTarget = (node) => {
313
313
  * Storing a reference does not copy it: the same array stays reachable through
314
314
  * the container, so `holder.items.push(3)` writes through to the binding
315
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
316
+ * `SpreadElement` is excluded because it builds a fresh VALUE
317
+ * (`const COPY = [...ITEMS]`) — it is not excluded from the walk entirely,
318
+ * because the copy still carries the constant's frozen TYPE, which
319
+ * `copyExpressionOf` handles. A computed key is excluded because it coerces
318
320
  * the reference to a property name rather than retaining it.
319
321
  */
320
322
  const storageContainerOf = (node) => {
@@ -369,6 +371,70 @@ const aliasDeclaratorOf = (identifier) => {
369
371
  value = outermostValueOf(container);
370
372
  }
371
373
  };
374
+ /**
375
+ * Whether a callee spells `Object.assign`, in either the dotted or the
376
+ * bracketed form — read through `accessedPropertyName` so the two spellings
377
+ * cannot diverge from how the mutation walk already reads a method name.
378
+ */
379
+ const isObjectAssignCallee = (callee) => {
380
+ const value = outermostValueOf(callee);
381
+ return (value.type === utils_1.AST_NODE_TYPES.MemberExpression &&
382
+ value.object.type === utils_1.AST_NODE_TYPES.Identifier &&
383
+ value.object.name === 'Object' &&
384
+ accessedPropertyName(value) === 'assign');
385
+ };
386
+ /**
387
+ * Array methods whose result keeps the receiver's ELEMENT type. `map` is
388
+ * absent because its result is typed from the CALLBACK, so the constant's type
389
+ * reaches it only for a callback that returns its argument unchanged — a no-op
390
+ * `map`. Admitting it would withhold the assertion from every derived array
391
+ * anything is computed from, to cover a spelling nobody writes.
392
+ */
393
+ const TYPE_PRESERVING_COPY_METHODS = new Set(['concat', 'slice', 'filter']);
394
+ /**
395
+ * The expression that builds a COPY carrying this value's type — the literal
396
+ * around a spread of it, the call of a copying array method on it, or an
397
+ * `Object.assign` it feeds.
398
+ *
399
+ * A copy is a fresh, mutable value, which is why `storageContainerOf` refuses
400
+ * it: writing to the copy cannot write through to the constant. But `as const`
401
+ * changes the constant's TYPE as well as its mutability, and a copy inherits
402
+ * that type — `[...ITEMS]` of a frozen `readonly [1, 2]` is `(1 | 2)[]`, so
403
+ * `COPY.push(3)` is TS2345 for an input that compiled. The copy is therefore
404
+ * followed for exactly the same question the alias walk asks: is the derived
405
+ * binding written?
406
+ */
407
+ const copyExpressionOf = (node) => {
408
+ const parent = node.parent;
409
+ if (!parent) {
410
+ return null;
411
+ }
412
+ if (parent.type === utils_1.AST_NODE_TYPES.SpreadElement &&
413
+ parent.argument === node &&
414
+ (parent.parent?.type === utils_1.AST_NODE_TYPES.ObjectExpression ||
415
+ parent.parent?.type === utils_1.AST_NODE_TYPES.ArrayExpression)) {
416
+ return parent.parent;
417
+ }
418
+ if (parent.type === utils_1.AST_NODE_TYPES.CallExpression &&
419
+ parent.arguments.includes(node) &&
420
+ isObjectAssignCallee(parent.callee)) {
421
+ return parent;
422
+ }
423
+ if (parent.type === utils_1.AST_NODE_TYPES.MemberExpression &&
424
+ parent.object === node) {
425
+ const method = accessedPropertyName(parent);
426
+ const callee = outermostValueOf(parent);
427
+ // A method REFERENCE (`const take = ITEMS.concat;`) builds nothing, so the
428
+ // copy only exists once the method is actually called.
429
+ if (method !== null &&
430
+ TYPE_PRESERVING_COPY_METHODS.has(method) &&
431
+ callee.parent?.type === utils_1.AST_NODE_TYPES.CallExpression &&
432
+ callee.parent.callee === callee) {
433
+ return callee.parent;
434
+ }
435
+ }
436
+ return null;
437
+ };
372
438
  /** Pattern nodes a parameter's binding can be nested inside. */
373
439
  const PATTERN_CONTAINERS = new Set([
374
440
  utils_1.AST_NODE_TYPES.AssignmentPattern,
@@ -414,16 +480,27 @@ const isInferredParameterDefault = (pattern) => {
414
480
  }
415
481
  };
416
482
  /**
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.
483
+ * Whether a reference sits where TypeScript INFERS a type from it — a default
484
+ * parameter or a class property initializer — reached directly or through a
485
+ * composite literal it is stored into.
420
486
  *
421
487
  * `as const` does not only freeze: it makes the literal type NON-WIDENING, and
422
488
  * an inference site that widened `'ready'` to `string` then keeps the literal.
423
489
  * A parameter defaulted from the constant therefore narrows to that one value,
424
490
  * and every call passing a different one stops compiling (TS2345) for an input
425
491
  * that compiled. The mutation walk cannot see this: nothing is written, the
426
- * signature is simply inferred from a value the assertion changes.
492
+ * declaration is simply inferred from a value the assertion changes.
493
+ *
494
+ * Both sites are answered on the same terms, because an annotation is what
495
+ * settles the question in each: a type written by hand is DECLARED, so nothing
496
+ * infers from the value and freezing it cannot move the declaration. Only the
497
+ * unannotated spelling narrows.
498
+ *
499
+ * A RETURN position infers in exactly the same way and is deliberately absent.
500
+ * Declining there costs 59 of 778 consumer reports (7.6%) — the constant need
501
+ * only be held in a literal that is returned — to prevent breaks that the
502
+ * consumer does not contain, so it is documented as a limitation instead. The
503
+ * comparable trade in #2330 was rejected at 5%.
427
504
  */
428
505
  const isInferenceSite = (identifier) => {
429
506
  let value = outermostValueOf(identifier);
@@ -433,6 +510,14 @@ const isInferenceSite = (identifier) => {
433
510
  parent.right === value) {
434
511
  return isInferredParameterDefault(parent.left);
435
512
  }
513
+ // A class property's type is inferred from its initializer exactly as a
514
+ // parameter's is from its default, so `session.stage = 'live'` becomes
515
+ // TS2322 once the constant behind `stage = DEFAULT_STAGE` is frozen.
516
+ if ((parent?.type === utils_1.AST_NODE_TYPES.PropertyDefinition ||
517
+ parent?.type === utils_1.AST_NODE_TYPES.AccessorProperty) &&
518
+ parent.value === value) {
519
+ return !parent.typeAnnotation;
520
+ }
436
521
  const container = storageContainerOf(value);
437
522
  if (!container) {
438
523
  return false;
@@ -448,8 +533,13 @@ const isInferenceSite = (identifier) => {
448
533
  * value, so a WRITE — through the binding (`X.push(1)`), or to a binding that
449
534
  * aliases it (`other = X`) — becomes TS2339/TS2540. And it makes the literal
450
535
  * 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.
536
+ * literal instead, which rewrites a declaration the assertion was never asked
537
+ * to touch.
538
+ *
539
+ * The type half reaches further than the value half, so the walk follows one
540
+ * edge the mutation question does not need: a COPY (`[...X]`, `X.concat()`),
541
+ * which is a fresh value but not a fresh type, and breaks on a write to the
542
+ * copy rather than to `X`.
453
543
  *
454
544
  * Answered from the scope manager's reference list rather than a textual
455
545
  * search for the name, so a same-named binding in
@@ -493,13 +583,20 @@ const blocksAsConstAssertion = (variable, declaredVariablesOf) => {
493
583
  return true;
494
584
  }
495
585
  const path = accessPathOf(reference.identifier);
496
- if (path !== null) {
497
- if (isMutatingMethodCall(path) || isWriteTarget(path)) {
498
- return true;
499
- }
500
- continue;
586
+ if (path !== null &&
587
+ (isMutatingMethodCall(path) || isWriteTarget(path))) {
588
+ return true;
501
589
  }
502
- const declarator = aliasDeclaratorOf(reference.identifier);
590
+ // A copy carries the constant's frozen type into a second binding, so it
591
+ // is enrolled on the same terms as an alias — but it is reached through a
592
+ // member access (`ITEMS.concat()`), which the alias walk deliberately
593
+ // refuses, so it is resolved before that refusal applies.
594
+ const copy = copyExpressionOf(outermostValueOf(reference.identifier));
595
+ const declarator = copy
596
+ ? aliasDeclaratorOf(copy)
597
+ : path === null
598
+ ? aliasDeclaratorOf(reference.identifier)
599
+ : null;
503
600
  if (!declarator) {
504
601
  continue;
505
602
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blumintinc/eslint-plugin-blumint",
3
- "version": "1.21.8",
3
+ "version": "1.21.9",
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.21.9",
4
+ "date": "2026-09-05T05:55:33.427Z",
5
+ "rules": [
6
+ {
7
+ "name": "global-const-style",
8
+ "changeType": "fix",
9
+ "issues": [
10
+ 2331
11
+ ],
12
+ "summary": "withhold the assertion where a copy or a class property carries the frozen type (closes #2331)"
13
+ }
14
+ ]
15
+ },
2
16
  {
3
17
  "version": "1.21.8",
4
18
  "date": "2026-09-05T03:15:57.605Z",