@blumintinc/eslint-plugin-blumint 1.21.12 → 1.21.13

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.12',
227
+ version: '1.21.13',
228
228
  },
229
229
  parseOptions: {
230
230
  ecmaVersion: 2020,
@@ -588,8 +588,10 @@ const isInferenceSite = (identifier) => {
588
588
  * second: a walk keyed on the first parameter would enrol a binding typed from
589
589
  * the seed value and miss the one typed from the constant (Issue #2338).
590
590
  *
591
- * Only the element parameter is enrolled. The index beside it is a `number`
592
- * whatever the receiver holds, so nothing the assertion changes reaches it.
591
+ * The INDEX parameter is absent from every map here, and from nothing else:
592
+ * it is a `number` whatever the receiver holds, so nothing the assertion
593
+ * changes reaches it. The parameter AFTER the index is a different matter —
594
+ * see `ARRAY_PARAMETER_INDEX_BY_METHOD`.
593
595
  */
594
596
  const ELEMENT_PARAMETER_INDEX_BY_METHOD = new Map([
595
597
  ['forEach', 0],
@@ -605,6 +607,45 @@ const ELEMENT_PARAMETER_INDEX_BY_METHOD = new Map([
605
607
  ['reduce', 1],
606
608
  ['reduceRight', 1],
607
609
  ]);
610
+ /**
611
+ * The same methods, mapped to the position the RECEIVER ARRAY arrives in.
612
+ *
613
+ * That parameter is a second name for the iterated value itself, so a mutating
614
+ * call through it writes to the constant:
615
+ * `ITEMS.forEach((item, index, arr) => { arr.push(2); })` is TS2339 once
616
+ * `ITEMS` is frozen, for an input that compiled — the identical call written
617
+ * directly as `ITEMS.push(2)` is one the rule already declines for, so only the
618
+ * handed-node spelling escapes it (Issue #2339).
619
+ *
620
+ * The position is carried apart from the element's because the two are enrolled
621
+ * on different terms rather than because they differ by one: an element keeps
622
+ * the constant's type through every derivation the iteration walk follows,
623
+ * while the receiver is the constant only when the iteration reads the
624
+ * constant's own value or member path — see `iterationBindingsOf`.
625
+ * `reduce`/`reduceRight` push it to fourth, having spent the first position on
626
+ * the accumulator.
627
+ *
628
+ * `flatMap` is listed even though its lib signature declares the parameter
629
+ * `T[]` where every sibling declares it `readonly T[]` — measured against
630
+ * `lib.es2020`, so a mutating METHOD through it survives the assertion. Its
631
+ * ELEMENTS are frozen regardless, so `arr[0].n = 2` inside a `flatMap` callback
632
+ * is TS2540 for an input that compiled, and the walk's write check reaches it
633
+ * only once the parameter is enrolled.
634
+ */
635
+ const ARRAY_PARAMETER_INDEX_BY_METHOD = new Map([
636
+ ['forEach', 2],
637
+ ['map', 2],
638
+ ['filter', 2],
639
+ ['find', 2],
640
+ ['findIndex', 2],
641
+ ['findLast', 2],
642
+ ['findLastIndex', 2],
643
+ ['some', 2],
644
+ ['every', 2],
645
+ ['flatMap', 2],
646
+ ['reduce', 3],
647
+ ['reduceRight', 3],
648
+ ]);
608
649
  /**
609
650
  * The `Object.values(X)` / `Object.entries(X)` call this value feeds — a fresh
610
651
  * array whose ELEMENTS are the constant's own property values, so freezing the
@@ -630,9 +671,10 @@ const elementProjectionCallOf = (node) => {
630
671
  : null;
631
672
  };
632
673
  /**
633
- * The bindings a construct that ITERATES `iterable` introduces for its
634
- * elements: the head of a `for…of` over it, or the parameter an array method
635
- * hands each element to.
674
+ * The bindings a construct that ITERATES `iterable` introduces: the head of a
675
+ * `for…of` over it, the parameter an array method hands each element to, and —
676
+ * when the iterated expression is the constant itself — the parameter that
677
+ * method hands the RECEIVER ARRAY to.
636
678
  *
637
679
  * A `for…of` head is accepted in all three binding spellings, on the same terms
638
680
  * as `aliasDeclaratorOf` accepts all three declarator spellings — every name a
@@ -645,8 +687,14 @@ const elementProjectionCallOf = (node) => {
645
687
  * A callback parameter is reached only through a function LITERAL: a callback
646
688
  * passed by name is declared elsewhere, where its parameter carries whatever
647
689
  * type that declaration gives it rather than one read off the constant.
690
+ *
691
+ * `iteratesConstantValue` says whether `iterable` is the constant's own value
692
+ * or member path rather than something derived from it. It gates the receiver
693
+ * ARRAY parameter alone: the element parameter is typed from the constant
694
+ * either way, while the array parameter names the constant only in the first
695
+ * case — see `iterationBindingsOf`.
648
696
  */
649
- const elementBindingsOfIteration = (iterable, declaredVariablesOf) => {
697
+ const bindingsOfIterationOver = (iterable, declaredVariablesOf, iteratesConstantValue) => {
650
698
  // The member path is resolved first because the iterated expression is
651
699
  // routinely a PROPERTY of the constant (`for (const x of CONFIG.list)`),
652
700
  // which the alias walk refuses precisely because it arrives through a member
@@ -666,7 +714,10 @@ const elementBindingsOfIteration = (iterable, declaredVariablesOf) => {
666
714
  return [];
667
715
  }
668
716
  const method = accessedPropertyName(path);
669
- const elementIndex = method === null ? undefined : ELEMENT_PARAMETER_INDEX_BY_METHOD.get(method);
717
+ if (method === null) {
718
+ return [];
719
+ }
720
+ const elementIndex = ELEMENT_PARAMETER_INDEX_BY_METHOD.get(method);
670
721
  if (elementIndex === undefined ||
671
722
  parent.type !== utils_1.AST_NODE_TYPES.CallExpression ||
672
723
  parent.callee !== value) {
@@ -676,25 +727,35 @@ const elementBindingsOfIteration = (iterable, declaredVariablesOf) => {
676
727
  if (!callback || !isFunctionValue(callback)) {
677
728
  return [];
678
729
  }
679
- const element = callback.params[elementIndex];
680
- if (!element) {
730
+ const arrayIndex = iteratesConstantValue
731
+ ? ARRAY_PARAMETER_INDEX_BY_METHOD.get(method)
732
+ : undefined;
733
+ // A callback routinely declares fewer parameters than the method passes, so
734
+ // each position is taken only where the signature actually spells it.
735
+ const enrolled = [
736
+ callback.params[elementIndex],
737
+ arrayIndex === undefined ? undefined : callback.params[arrayIndex],
738
+ ].filter((param) => param !== undefined);
739
+ if (enrolled.length === 0) {
681
740
  return [];
682
741
  }
683
742
  // The scope manager answers for the WHOLE function — every parameter, and a
684
- // function expression's own name — so the element parameter's bindings are
685
- // picked out by the span they are declared in. Taking the function's list
686
- // whole would enrol the accumulator of a `reduce`, which is typed from the
687
- // seed value rather than from the constant.
688
- return declaredVariablesOf(callback).filter((variable) => variable.defs.some((def) => def.name.range[0] >= element.range[0] &&
689
- def.name.range[1] <= element.range[1]));
743
+ // function expression's own name — so the enrolled parameters' bindings are
744
+ // picked out by the spans they are declared in. Taking the function's list
745
+ // whole would enrol the accumulator of a `reduce`, typed from the seed value
746
+ // rather than from the constant, and the index, which the assertion cannot
747
+ // reach.
748
+ return declaredVariablesOf(callback).filter((variable) => variable.defs.some((def) => enrolled.some((param) => def.name.range[0] >= param.range[0] &&
749
+ def.name.range[1] <= param.range[1])));
690
750
  };
691
751
  /**
692
752
  * The bindings ITERATING this reference introduces, directly or through a value
693
753
  * derived from it that keeps its element types.
694
754
  *
695
755
  * Such a binding is typed from the constant exactly as a destructured copy is —
696
- * it carries the ELEMENT type rather than the whole value — so a write through
697
- * it breaks on the assertion the same way a write through an alias does:
756
+ * it carries the ELEMENT type, or for the receiver parameter the whole value —
757
+ * so a write through it breaks on the assertion as a write through an alias
758
+ * does:
698
759
  * `for (const item of ITEMS) { item.label = 'b'; }` is TS2540 once `ITEMS` is
699
760
  * frozen, for an input that compiled (Issue #2338). Enrolling the binding is
700
761
  * therefore the whole remedy; the walk's existing write, mutating-method and
@@ -707,15 +768,26 @@ const elementBindingsOfIteration = (iterable, declaredVariablesOf) => {
707
768
  * builds a fresh OUTER value whose elements are still the frozen ones, so the
708
769
  * element binding breaks identically. One derivation step is followed, matching
709
770
  * the depth the alias walk already follows a copy to.
771
+ *
772
+ * The receiver ARRAY parameter is enrolled for the constant's own value or
773
+ * member path ALONE, which is the one iterable of the three that hands the
774
+ * callback the constant itself. A derivation hands it the fresh outer value it
775
+ * built, and mutating that is no readonly violation:
776
+ * `[...ITEMS].forEach((item, index, arr) => { arr.push(3); })` does break after
777
+ * the fix, but as TS2345 — the spread narrows the element type, so `3` is not
778
+ * assignable — which belongs to the literal-narrowing family filed as #2330 and
779
+ * needs the type checker. Enrolling it here would withhold the assertion for a
780
+ * reason this arm cannot justify, so it is an over-decline (Issue #2339).
710
781
  */
711
782
  const iterationBindingsOf = (identifier, declaredVariablesOf) => {
712
783
  const value = outermostValueOf(identifier);
713
- const iterables = [
714
- value,
715
- copyExpressionOf(value),
716
- elementProjectionCallOf(value),
784
+ const derivations = [copyExpressionOf(value), elementProjectionCallOf(value)];
785
+ return [
786
+ ...bindingsOfIterationOver(value, declaredVariablesOf, true),
787
+ ...derivations.flatMap((iterable) => iterable
788
+ ? bindingsOfIterationOver(iterable, declaredVariablesOf, false)
789
+ : []),
717
790
  ];
718
- return iterables.flatMap((iterable) => iterable ? elementBindingsOfIteration(iterable, declaredVariablesOf) : []);
719
791
  };
720
792
  /**
721
793
  * Whether anything in the file stops this binding taking `as const`, under its
@@ -748,13 +820,16 @@ const iterationBindingsOf = (identifier, declaredVariablesOf) => {
748
820
  * the one value — and `visited` keeps a chain that leads back on itself, which
749
821
  * a redeclared `var` can build, from looping forever.
750
822
  *
751
- * Iteration is followed on the same reasoning, keyed on the ELEMENT rather than
752
- * the whole value: a `for…of` head and an iteration callback's parameter are
753
- * second names for the constant's contents, so `for (const item of ITEMS) {
823
+ * Iteration is followed on the same reasoning, keyed on what the construct
824
+ * HANDS its body: a `for…of` head and an iteration callback's element parameter
825
+ * are second names for the constant's contents, so `for (const item of ITEMS) {
754
826
  * item.label = 'b'; }` is TS2540 once `ITEMS` is frozen while `ITEMS`'s own
755
- * references show nothing but a read (Issue #2338). Enrolling the binding is
756
- * all it takes — the checks above then decide, so a loop that only reads its
757
- * element keeps the assertion.
827
+ * references show nothing but a read (Issue #2338); the parameter after the
828
+ * index is a second name for the constant ITSELF, so `arr.push(2)` inside the
829
+ * callback is the TS2339 the rule already declines for when the same call is
830
+ * written directly (Issue #2339). Enrolling the binding is all it takes — the
831
+ * checks above then decide, so a callback that only reads what it is handed
832
+ * keeps the assertion.
758
833
  *
759
834
  * The declaring KEYWORD is deliberately not screened. `as const` types the
760
835
  * value `readonly`, and a binding takes its declared type from its initializer,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blumintinc/eslint-plugin-blumint",
3
- "version": "1.21.12",
3
+ "version": "1.21.13",
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.13",
4
+ "date": "2026-09-05T22:39:56.997Z",
5
+ "rules": [
6
+ {
7
+ "name": "global-const-style",
8
+ "changeType": "fix",
9
+ "issues": [
10
+ 2339
11
+ ],
12
+ "summary": "enrol the receiver array parameter of an iteration over the constant (closes #2339)"
13
+ }
14
+ ]
15
+ },
2
16
  {
3
17
  "version": "1.21.12",
4
18
  "date": "2026-09-05T18:30:20.453Z",