@blumintinc/eslint-plugin-blumint 1.21.11 → 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
|
@@ -579,6 +579,216 @@ const isInferenceSite = (identifier) => {
|
|
|
579
579
|
value = outermostValueOf(container);
|
|
580
580
|
}
|
|
581
581
|
};
|
|
582
|
+
/**
|
|
583
|
+
* Array methods that hand an ELEMENT of the receiver to a callback, mapped to
|
|
584
|
+
* the parameter position that element arrives in.
|
|
585
|
+
*
|
|
586
|
+
* The position is carried per method rather than assumed to be the first,
|
|
587
|
+
* because `reduce`/`reduceRight` pass the accumulator first and the element
|
|
588
|
+
* second: a walk keyed on the first parameter would enrol a binding typed from
|
|
589
|
+
* the seed value and miss the one typed from the constant (Issue #2338).
|
|
590
|
+
*
|
|
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`.
|
|
595
|
+
*/
|
|
596
|
+
const ELEMENT_PARAMETER_INDEX_BY_METHOD = new Map([
|
|
597
|
+
['forEach', 0],
|
|
598
|
+
['map', 0],
|
|
599
|
+
['filter', 0],
|
|
600
|
+
['find', 0],
|
|
601
|
+
['findIndex', 0],
|
|
602
|
+
['findLast', 0],
|
|
603
|
+
['findLastIndex', 0],
|
|
604
|
+
['some', 0],
|
|
605
|
+
['every', 0],
|
|
606
|
+
['flatMap', 0],
|
|
607
|
+
['reduce', 1],
|
|
608
|
+
['reduceRight', 1],
|
|
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
|
+
]);
|
|
649
|
+
/**
|
|
650
|
+
* The `Object.values(X)` / `Object.entries(X)` call this value feeds — a fresh
|
|
651
|
+
* array whose ELEMENTS are the constant's own property values, so freezing the
|
|
652
|
+
* constant retypes them exactly as it retypes an array's elements.
|
|
653
|
+
*
|
|
654
|
+
* It is not a copy in `copyExpressionOf`'s sense: the result has a different
|
|
655
|
+
* shape from the argument, so a write to the array itself says nothing about
|
|
656
|
+
* the constant. It is resolved here instead, where only the ITERATION question
|
|
657
|
+
* is asked and a decline still requires a write through the element binding.
|
|
658
|
+
*
|
|
659
|
+
* `Object.keys` is absent because its result is `string[]` whatever the
|
|
660
|
+
* argument's type, so the assertion cannot reach a binding taken from it.
|
|
661
|
+
*/
|
|
662
|
+
const elementProjectionCallOf = (node) => {
|
|
663
|
+
const parent = node.parent;
|
|
664
|
+
if (parent?.type !== utils_1.AST_NODE_TYPES.CallExpression ||
|
|
665
|
+
parent.arguments[0] !== node) {
|
|
666
|
+
return null;
|
|
667
|
+
}
|
|
668
|
+
return isNamespacedCallee(parent.callee, 'Object', 'values') ||
|
|
669
|
+
isNamespacedCallee(parent.callee, 'Object', 'entries')
|
|
670
|
+
? parent
|
|
671
|
+
: null;
|
|
672
|
+
};
|
|
673
|
+
/**
|
|
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.
|
|
678
|
+
*
|
|
679
|
+
* A `for…of` head is accepted in all three binding spellings, on the same terms
|
|
680
|
+
* as `aliasDeclaratorOf` accepts all three declarator spellings — every name a
|
|
681
|
+
* pattern introduces is typed from the value it destructures. A head that is
|
|
682
|
+
* not a declaration assigns into a binding declared elsewhere, whose type the
|
|
683
|
+
* constant never gave it, so it introduces nothing to enrol. `for await` is the
|
|
684
|
+
* same node with `await` set and binds its element the same way, so the flag is
|
|
685
|
+
* not screened.
|
|
686
|
+
*
|
|
687
|
+
* A callback parameter is reached only through a function LITERAL: a callback
|
|
688
|
+
* passed by name is declared elsewhere, where its parameter carries whatever
|
|
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`.
|
|
696
|
+
*/
|
|
697
|
+
const bindingsOfIterationOver = (iterable, declaredVariablesOf, iteratesConstantValue) => {
|
|
698
|
+
// The member path is resolved first because the iterated expression is
|
|
699
|
+
// routinely a PROPERTY of the constant (`for (const x of CONFIG.list)`),
|
|
700
|
+
// which the alias walk refuses precisely because it arrives through a member
|
|
701
|
+
// access — the property is frozen with the object that holds it.
|
|
702
|
+
const path = accessPathOf(iterable);
|
|
703
|
+
const value = outermostValueOf(path ?? iterable);
|
|
704
|
+
const parent = value.parent;
|
|
705
|
+
if (!parent) {
|
|
706
|
+
return [];
|
|
707
|
+
}
|
|
708
|
+
if (parent.type === utils_1.AST_NODE_TYPES.ForOfStatement &&
|
|
709
|
+
parent.right === value &&
|
|
710
|
+
parent.left.type === utils_1.AST_NODE_TYPES.VariableDeclaration) {
|
|
711
|
+
return declaredVariablesOf(parent.left);
|
|
712
|
+
}
|
|
713
|
+
if (path === null) {
|
|
714
|
+
return [];
|
|
715
|
+
}
|
|
716
|
+
const method = accessedPropertyName(path);
|
|
717
|
+
if (method === null) {
|
|
718
|
+
return [];
|
|
719
|
+
}
|
|
720
|
+
const elementIndex = ELEMENT_PARAMETER_INDEX_BY_METHOD.get(method);
|
|
721
|
+
if (elementIndex === undefined ||
|
|
722
|
+
parent.type !== utils_1.AST_NODE_TYPES.CallExpression ||
|
|
723
|
+
parent.callee !== value) {
|
|
724
|
+
return [];
|
|
725
|
+
}
|
|
726
|
+
const callback = parent.arguments[0];
|
|
727
|
+
if (!callback || !isFunctionValue(callback)) {
|
|
728
|
+
return [];
|
|
729
|
+
}
|
|
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) {
|
|
740
|
+
return [];
|
|
741
|
+
}
|
|
742
|
+
// The scope manager answers for the WHOLE function — every parameter, and a
|
|
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])));
|
|
750
|
+
};
|
|
751
|
+
/**
|
|
752
|
+
* The bindings ITERATING this reference introduces, directly or through a value
|
|
753
|
+
* derived from it that keeps its element types.
|
|
754
|
+
*
|
|
755
|
+
* Such a binding is typed from the constant exactly as a destructured copy is —
|
|
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:
|
|
759
|
+
* `for (const item of ITEMS) { item.label = 'b'; }` is TS2540 once `ITEMS` is
|
|
760
|
+
* frozen, for an input that compiled (Issue #2338). Enrolling the binding is
|
|
761
|
+
* therefore the whole remedy; the walk's existing write, mutating-method and
|
|
762
|
+
* inference checks answer the question on it, which is what keeps a loop that
|
|
763
|
+
* only READS its element fixable.
|
|
764
|
+
*
|
|
765
|
+
* The derivations are followed because the receiver of the iteration is
|
|
766
|
+
* routinely one step removed from the constant (`[...ITEMS].forEach(…)`,
|
|
767
|
+
* `ITEMS.filter(Boolean).forEach(…)`, `Object.values(CONFIG).forEach(…)`): each
|
|
768
|
+
* builds a fresh OUTER value whose elements are still the frozen ones, so the
|
|
769
|
+
* element binding breaks identically. One derivation step is followed, matching
|
|
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).
|
|
781
|
+
*/
|
|
782
|
+
const iterationBindingsOf = (identifier, declaredVariablesOf) => {
|
|
783
|
+
const value = outermostValueOf(identifier);
|
|
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
|
+
: []),
|
|
790
|
+
];
|
|
791
|
+
};
|
|
582
792
|
/**
|
|
583
793
|
* Whether anything in the file stops this binding taking `as const`, under its
|
|
584
794
|
* own name or through an alias of it.
|
|
@@ -610,6 +820,17 @@ const isInferenceSite = (identifier) => {
|
|
|
610
820
|
* the one value — and `visited` keeps a chain that leads back on itself, which
|
|
611
821
|
* a redeclared `var` can build, from looping forever.
|
|
612
822
|
*
|
|
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) {
|
|
826
|
+
* item.label = 'b'; }` is TS2540 once `ITEMS` is frozen while `ITEMS`'s own
|
|
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.
|
|
833
|
+
*
|
|
613
834
|
* The declaring KEYWORD is deliberately not screened. `as const` types the
|
|
614
835
|
* value `readonly`, and a binding takes its declared type from its initializer,
|
|
615
836
|
* so `let other = ITEMS; other.push(3);` is the same TS2339 as the `const`
|
|
@@ -651,10 +872,14 @@ const blocksAsConstAssertion = (variable, declaredVariablesOf) => {
|
|
|
651
872
|
: path === null
|
|
652
873
|
? aliasDeclaratorOf(reference.identifier)
|
|
653
874
|
: null;
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
875
|
+
// A binding introduced by ITERATING the constant is enrolled beside the
|
|
876
|
+
// aliases: it names the constant's CONTENTS, which the assertion freezes
|
|
877
|
+
// with the constant itself — see `iterationBindingsOf`.
|
|
878
|
+
const derived = [
|
|
879
|
+
...(declarator ? declaredVariablesOf(declarator) : []),
|
|
880
|
+
...iterationBindingsOf(reference.identifier, declaredVariablesOf),
|
|
881
|
+
];
|
|
882
|
+
for (const alias of derived) {
|
|
658
883
|
if (!visited.has(alias)) {
|
|
659
884
|
visited.add(alias);
|
|
660
885
|
pending.push(alias);
|
|
@@ -348,9 +348,10 @@ const isInsideFunction = (node) => {
|
|
|
348
348
|
};
|
|
349
349
|
const isPascalCaseName = (name) => /^[A-Z]/.test(name);
|
|
350
350
|
/**
|
|
351
|
-
* Prop names whose value a parent MOUNTS rather than calls.
|
|
352
|
-
* the
|
|
353
|
-
* what a component-type prop is — the disagreement between them
|
|
351
|
+
* Prop names whose value a parent MOUNTS rather than calls. The `JSXAttribute`
|
|
352
|
+
* visitor and the binding-side classifier both read it, so the two paths cannot
|
|
353
|
+
* disagree about what a component-type prop is — the disagreement between them
|
|
354
|
+
* is #2334, and the duplicated literal that would let it return is #2337.
|
|
354
355
|
*/
|
|
355
356
|
const COMPONENT_PROP_SUFFIX = /(Wrapper|Component|Template|Header|Footer)$/;
|
|
356
357
|
const isComponentPropName = (name) => isPascalCaseName(name) && COMPONENT_PROP_SUFFIX.test(name);
|
|
@@ -901,12 +902,15 @@ See: https://react.dev/learn/your-first-component#nesting-and-organizing-compone
|
|
|
901
902
|
if (node.name.type !== utils_1.AST_NODE_TYPES.JSXIdentifier)
|
|
902
903
|
return;
|
|
903
904
|
const attrName = node.name.name;
|
|
904
|
-
//
|
|
905
|
-
//
|
|
906
|
-
//
|
|
907
|
-
//
|
|
908
|
-
|
|
909
|
-
|
|
905
|
+
// A non-PascalCase prop (e.g. renderHeader) is a render callback used
|
|
906
|
+
// with a render={...} prop, not a component. The suffix alone is not
|
|
907
|
+
// enough: it matches the tail of renderHeader.
|
|
908
|
+
//
|
|
909
|
+
// Read through the shared helper rather than a second copy of the
|
|
910
|
+
// pattern, so the binding side and this one cannot answer differently
|
|
911
|
+
// about what a component-type prop is — the disagreement between the
|
|
912
|
+
// two paths is #2334, and a duplicated literal is what lets it return.
|
|
913
|
+
if (!isComponentPropName(attrName)) {
|
|
910
914
|
return;
|
|
911
915
|
}
|
|
912
916
|
if (!node.value ||
|
package/package.json
CHANGED
package/release-manifest.json
CHANGED
|
@@ -1,4 +1,32 @@
|
|
|
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
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"version": "1.21.12",
|
|
18
|
+
"date": "2026-09-05T18:30:20.453Z",
|
|
19
|
+
"rules": [
|
|
20
|
+
{
|
|
21
|
+
"name": "global-const-style",
|
|
22
|
+
"changeType": "fix",
|
|
23
|
+
"issues": [
|
|
24
|
+
2338
|
|
25
|
+
],
|
|
26
|
+
"summary": "withhold `as const` when a binding introduced by iterating the constant is written (closes #2338)"
|
|
27
|
+
}
|
|
28
|
+
]
|
|
29
|
+
},
|
|
2
30
|
{
|
|
3
31
|
"version": "1.21.11",
|
|
4
32
|
"date": "2026-09-05T11:06:01.167Z",
|