@blumintinc/eslint-plugin-blumint 1.21.10 → 1.21.12
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
|
@@ -350,9 +350,14 @@ const storageContainerOf = (node) => {
|
|
|
350
350
|
* the file compiling is not.
|
|
351
351
|
*
|
|
352
352
|
* A reference STORED INTO a composite literal is followed through that
|
|
353
|
-
* container, since storing does not copy — see `storageContainerOf`.
|
|
354
|
-
*
|
|
355
|
-
*
|
|
353
|
+
* container, since storing does not copy — see `storageContainerOf`.
|
|
354
|
+
*
|
|
355
|
+
* A destructuring id is accepted in BOTH spellings. It does not name the whole
|
|
356
|
+
* value, but every binding it introduces is typed from that value, and a rest
|
|
357
|
+
* element is itself a fresh container the assertion narrows: `const [, ...rest]
|
|
358
|
+
* = ITEMS` gives `rest` the frozen element type, so `rest.push(4)` is TS2345
|
|
359
|
+
* for an input that compiled. Admitting only the object spelling gave the same
|
|
360
|
+
* construct opposite verdicts (#2336).
|
|
356
361
|
*/
|
|
357
362
|
const aliasDeclaratorOf = (identifier) => {
|
|
358
363
|
// Ascends strictly, so reaching a node with no parent terminates the walk.
|
|
@@ -362,7 +367,8 @@ const aliasDeclaratorOf = (identifier) => {
|
|
|
362
367
|
if (declarator?.type === utils_1.AST_NODE_TYPES.VariableDeclarator &&
|
|
363
368
|
declarator.init === value &&
|
|
364
369
|
(declarator.id.type === utils_1.AST_NODE_TYPES.Identifier ||
|
|
365
|
-
declarator.id.type === utils_1.AST_NODE_TYPES.ObjectPattern
|
|
370
|
+
declarator.id.type === utils_1.AST_NODE_TYPES.ObjectPattern ||
|
|
371
|
+
declarator.id.type === utils_1.AST_NODE_TYPES.ArrayPattern)) {
|
|
366
372
|
return declarator;
|
|
367
373
|
}
|
|
368
374
|
const container = storageContainerOf(value);
|
|
@@ -573,6 +579,144 @@ const isInferenceSite = (identifier) => {
|
|
|
573
579
|
value = outermostValueOf(container);
|
|
574
580
|
}
|
|
575
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
|
+
* 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.
|
|
593
|
+
*/
|
|
594
|
+
const ELEMENT_PARAMETER_INDEX_BY_METHOD = new Map([
|
|
595
|
+
['forEach', 0],
|
|
596
|
+
['map', 0],
|
|
597
|
+
['filter', 0],
|
|
598
|
+
['find', 0],
|
|
599
|
+
['findIndex', 0],
|
|
600
|
+
['findLast', 0],
|
|
601
|
+
['findLastIndex', 0],
|
|
602
|
+
['some', 0],
|
|
603
|
+
['every', 0],
|
|
604
|
+
['flatMap', 0],
|
|
605
|
+
['reduce', 1],
|
|
606
|
+
['reduceRight', 1],
|
|
607
|
+
]);
|
|
608
|
+
/**
|
|
609
|
+
* The `Object.values(X)` / `Object.entries(X)` call this value feeds — a fresh
|
|
610
|
+
* array whose ELEMENTS are the constant's own property values, so freezing the
|
|
611
|
+
* constant retypes them exactly as it retypes an array's elements.
|
|
612
|
+
*
|
|
613
|
+
* It is not a copy in `copyExpressionOf`'s sense: the result has a different
|
|
614
|
+
* shape from the argument, so a write to the array itself says nothing about
|
|
615
|
+
* the constant. It is resolved here instead, where only the ITERATION question
|
|
616
|
+
* is asked and a decline still requires a write through the element binding.
|
|
617
|
+
*
|
|
618
|
+
* `Object.keys` is absent because its result is `string[]` whatever the
|
|
619
|
+
* argument's type, so the assertion cannot reach a binding taken from it.
|
|
620
|
+
*/
|
|
621
|
+
const elementProjectionCallOf = (node) => {
|
|
622
|
+
const parent = node.parent;
|
|
623
|
+
if (parent?.type !== utils_1.AST_NODE_TYPES.CallExpression ||
|
|
624
|
+
parent.arguments[0] !== node) {
|
|
625
|
+
return null;
|
|
626
|
+
}
|
|
627
|
+
return isNamespacedCallee(parent.callee, 'Object', 'values') ||
|
|
628
|
+
isNamespacedCallee(parent.callee, 'Object', 'entries')
|
|
629
|
+
? parent
|
|
630
|
+
: null;
|
|
631
|
+
};
|
|
632
|
+
/**
|
|
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.
|
|
636
|
+
*
|
|
637
|
+
* A `for…of` head is accepted in all three binding spellings, on the same terms
|
|
638
|
+
* as `aliasDeclaratorOf` accepts all three declarator spellings — every name a
|
|
639
|
+
* pattern introduces is typed from the value it destructures. A head that is
|
|
640
|
+
* not a declaration assigns into a binding declared elsewhere, whose type the
|
|
641
|
+
* constant never gave it, so it introduces nothing to enrol. `for await` is the
|
|
642
|
+
* same node with `await` set and binds its element the same way, so the flag is
|
|
643
|
+
* not screened.
|
|
644
|
+
*
|
|
645
|
+
* A callback parameter is reached only through a function LITERAL: a callback
|
|
646
|
+
* passed by name is declared elsewhere, where its parameter carries whatever
|
|
647
|
+
* type that declaration gives it rather than one read off the constant.
|
|
648
|
+
*/
|
|
649
|
+
const elementBindingsOfIteration = (iterable, declaredVariablesOf) => {
|
|
650
|
+
// The member path is resolved first because the iterated expression is
|
|
651
|
+
// routinely a PROPERTY of the constant (`for (const x of CONFIG.list)`),
|
|
652
|
+
// which the alias walk refuses precisely because it arrives through a member
|
|
653
|
+
// access — the property is frozen with the object that holds it.
|
|
654
|
+
const path = accessPathOf(iterable);
|
|
655
|
+
const value = outermostValueOf(path ?? iterable);
|
|
656
|
+
const parent = value.parent;
|
|
657
|
+
if (!parent) {
|
|
658
|
+
return [];
|
|
659
|
+
}
|
|
660
|
+
if (parent.type === utils_1.AST_NODE_TYPES.ForOfStatement &&
|
|
661
|
+
parent.right === value &&
|
|
662
|
+
parent.left.type === utils_1.AST_NODE_TYPES.VariableDeclaration) {
|
|
663
|
+
return declaredVariablesOf(parent.left);
|
|
664
|
+
}
|
|
665
|
+
if (path === null) {
|
|
666
|
+
return [];
|
|
667
|
+
}
|
|
668
|
+
const method = accessedPropertyName(path);
|
|
669
|
+
const elementIndex = method === null ? undefined : ELEMENT_PARAMETER_INDEX_BY_METHOD.get(method);
|
|
670
|
+
if (elementIndex === undefined ||
|
|
671
|
+
parent.type !== utils_1.AST_NODE_TYPES.CallExpression ||
|
|
672
|
+
parent.callee !== value) {
|
|
673
|
+
return [];
|
|
674
|
+
}
|
|
675
|
+
const callback = parent.arguments[0];
|
|
676
|
+
if (!callback || !isFunctionValue(callback)) {
|
|
677
|
+
return [];
|
|
678
|
+
}
|
|
679
|
+
const element = callback.params[elementIndex];
|
|
680
|
+
if (!element) {
|
|
681
|
+
return [];
|
|
682
|
+
}
|
|
683
|
+
// 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]));
|
|
690
|
+
};
|
|
691
|
+
/**
|
|
692
|
+
* The bindings ITERATING this reference introduces, directly or through a value
|
|
693
|
+
* derived from it that keeps its element types.
|
|
694
|
+
*
|
|
695
|
+
* 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:
|
|
698
|
+
* `for (const item of ITEMS) { item.label = 'b'; }` is TS2540 once `ITEMS` is
|
|
699
|
+
* frozen, for an input that compiled (Issue #2338). Enrolling the binding is
|
|
700
|
+
* therefore the whole remedy; the walk's existing write, mutating-method and
|
|
701
|
+
* inference checks answer the question on it, which is what keeps a loop that
|
|
702
|
+
* only READS its element fixable.
|
|
703
|
+
*
|
|
704
|
+
* The derivations are followed because the receiver of the iteration is
|
|
705
|
+
* routinely one step removed from the constant (`[...ITEMS].forEach(…)`,
|
|
706
|
+
* `ITEMS.filter(Boolean).forEach(…)`, `Object.values(CONFIG).forEach(…)`): each
|
|
707
|
+
* builds a fresh OUTER value whose elements are still the frozen ones, so the
|
|
708
|
+
* element binding breaks identically. One derivation step is followed, matching
|
|
709
|
+
* the depth the alias walk already follows a copy to.
|
|
710
|
+
*/
|
|
711
|
+
const iterationBindingsOf = (identifier, declaredVariablesOf) => {
|
|
712
|
+
const value = outermostValueOf(identifier);
|
|
713
|
+
const iterables = [
|
|
714
|
+
value,
|
|
715
|
+
copyExpressionOf(value),
|
|
716
|
+
elementProjectionCallOf(value),
|
|
717
|
+
];
|
|
718
|
+
return iterables.flatMap((iterable) => iterable ? elementBindingsOfIteration(iterable, declaredVariablesOf) : []);
|
|
719
|
+
};
|
|
576
720
|
/**
|
|
577
721
|
* Whether anything in the file stops this binding taking `as const`, under its
|
|
578
722
|
* own name or through an alias of it.
|
|
@@ -604,6 +748,14 @@ const isInferenceSite = (identifier) => {
|
|
|
604
748
|
* the one value — and `visited` keeps a chain that leads back on itself, which
|
|
605
749
|
* a redeclared `var` can build, from looping forever.
|
|
606
750
|
*
|
|
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) {
|
|
754
|
+
* 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.
|
|
758
|
+
*
|
|
607
759
|
* The declaring KEYWORD is deliberately not screened. `as const` types the
|
|
608
760
|
* value `readonly`, and a binding takes its declared type from its initializer,
|
|
609
761
|
* so `let other = ITEMS; other.push(3);` is the same TS2339 as the `const`
|
|
@@ -645,10 +797,14 @@ const blocksAsConstAssertion = (variable, declaredVariablesOf) => {
|
|
|
645
797
|
: path === null
|
|
646
798
|
? aliasDeclaratorOf(reference.identifier)
|
|
647
799
|
: null;
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
800
|
+
// A binding introduced by ITERATING the constant is enrolled beside the
|
|
801
|
+
// aliases: it names the constant's CONTENTS, which the assertion freezes
|
|
802
|
+
// with the constant itself — see `iterationBindingsOf`.
|
|
803
|
+
const derived = [
|
|
804
|
+
...(declarator ? declaredVariablesOf(declarator) : []),
|
|
805
|
+
...iterationBindingsOf(reference.identifier, declaredVariablesOf),
|
|
806
|
+
];
|
|
807
|
+
for (const alias of derived) {
|
|
652
808
|
if (!visited.has(alias)) {
|
|
653
809
|
visited.add(alias);
|
|
654
810
|
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);
|
|
@@ -410,13 +411,16 @@ const consumptionOfReference = (identifier, reactImports) => {
|
|
|
410
411
|
* fragment. Falling back to the name there keeps the rule's reach while letting
|
|
411
412
|
* evidence override the guess wherever evidence exists.
|
|
412
413
|
*/
|
|
413
|
-
const
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
414
|
+
const consumptionOfDeclaration = (declaration, id, context, reactImports) => {
|
|
415
|
+
// `getDeclaredVariables` on a FunctionDeclaration yields its PARAMETERS
|
|
416
|
+
// alongside the function name, and a parameter handed to a non-component prop
|
|
417
|
+
// votes `callback` for a binding it says nothing about. Keep only the
|
|
418
|
+
// variable this declaration's own id introduces.
|
|
419
|
+
const declared = context
|
|
420
|
+
.getDeclaredVariables(declaration)
|
|
421
|
+
.filter((variable) => variable.defs.some((def) => def.name === id));
|
|
418
422
|
let sawCallback = false;
|
|
419
|
-
for (const variable of
|
|
423
|
+
for (const variable of declared) {
|
|
420
424
|
for (const reference of variable.references) {
|
|
421
425
|
const consumption = consumptionOfReference(reference.identifier, reactImports);
|
|
422
426
|
// A single mounting use settles it: the identity churn happens there
|
|
@@ -431,6 +435,14 @@ const consumptionOfBinding = (node, context, reactImports) => {
|
|
|
431
435
|
}
|
|
432
436
|
return sawCallback ? 'callback' : 'unknown';
|
|
433
437
|
};
|
|
438
|
+
const consumptionOfBinding = (node, context, reactImports) => {
|
|
439
|
+
const declarator = parentBeyondChain(node);
|
|
440
|
+
if (declarator?.type !== utils_1.AST_NODE_TYPES.VariableDeclarator ||
|
|
441
|
+
declarator.id.type !== utils_1.AST_NODE_TYPES.Identifier) {
|
|
442
|
+
return 'unknown';
|
|
443
|
+
}
|
|
444
|
+
return consumptionOfDeclaration(declarator, declarator.id, context, reactImports);
|
|
445
|
+
};
|
|
434
446
|
/**
|
|
435
447
|
* The values a container hands to its caller: object property values and array
|
|
436
448
|
* elements. Mirrors `containedValues` in the paired `require-memo` rule (#1919),
|
|
@@ -841,8 +853,10 @@ See: https://react.dev/learn/your-first-component#nesting-and-organizing-compone
|
|
|
841
853
|
return;
|
|
842
854
|
if (node.id.type !== utils_1.AST_NODE_TYPES.Identifier)
|
|
843
855
|
return;
|
|
844
|
-
|
|
845
|
-
if (
|
|
856
|
+
const vdConsumption = consumptionOfDeclaration(node, node.id, context, reactImports);
|
|
857
|
+
if (vdConsumption === 'callback')
|
|
858
|
+
return;
|
|
859
|
+
if (vdConsumption === 'unknown' && !isPascalCaseName(node.id.name))
|
|
846
860
|
return;
|
|
847
861
|
if (!isInsideFunction(node))
|
|
848
862
|
return;
|
|
@@ -866,7 +880,12 @@ See: https://react.dev/learn/your-first-component#nesting-and-organizing-compone
|
|
|
866
880
|
reportNestedComponentViolation(node, node.id.name, 'a render body');
|
|
867
881
|
},
|
|
868
882
|
FunctionDeclaration(node) {
|
|
869
|
-
if (!node.id
|
|
883
|
+
if (!node.id)
|
|
884
|
+
return;
|
|
885
|
+
const fdConsumption = consumptionOfDeclaration(node, node.id, context, reactImports);
|
|
886
|
+
if (fdConsumption === 'callback')
|
|
887
|
+
return;
|
|
888
|
+
if (fdConsumption === 'unknown' && !isPascalCaseName(node.id.name))
|
|
870
889
|
return;
|
|
871
890
|
if (!isInsideFunction(node))
|
|
872
891
|
return;
|
|
@@ -883,12 +902,15 @@ See: https://react.dev/learn/your-first-component#nesting-and-organizing-compone
|
|
|
883
902
|
if (node.name.type !== utils_1.AST_NODE_TYPES.JSXIdentifier)
|
|
884
903
|
return;
|
|
885
904
|
const attrName = node.name.name;
|
|
886
|
-
//
|
|
887
|
-
//
|
|
888
|
-
//
|
|
889
|
-
//
|
|
890
|
-
|
|
891
|
-
|
|
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)) {
|
|
892
914
|
return;
|
|
893
915
|
}
|
|
894
916
|
if (!node.value ||
|
package/package.json
CHANGED
package/release-manifest.json
CHANGED
|
@@ -1,4 +1,40 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "1.21.12",
|
|
4
|
+
"date": "2026-09-05T18:30:20.453Z",
|
|
5
|
+
"rules": [
|
|
6
|
+
{
|
|
7
|
+
"name": "global-const-style",
|
|
8
|
+
"changeType": "fix",
|
|
9
|
+
"issues": [
|
|
10
|
+
2338
|
|
11
|
+
],
|
|
12
|
+
"summary": "withhold `as const` when a binding introduced by iterating the constant is written (closes #2338)"
|
|
13
|
+
}
|
|
14
|
+
]
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"version": "1.21.11",
|
|
18
|
+
"date": "2026-09-05T11:06:01.167Z",
|
|
19
|
+
"rules": [
|
|
20
|
+
{
|
|
21
|
+
"name": "global-const-style",
|
|
22
|
+
"changeType": "fix",
|
|
23
|
+
"issues": [
|
|
24
|
+
2336
|
|
25
|
+
],
|
|
26
|
+
"summary": "follow the array spelling of a destructured copy, and the rest element that needs no copy (closes #2336)"
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"name": "memo-nested-react-components",
|
|
30
|
+
"changeType": "fix",
|
|
31
|
+
"issues": [
|
|
32
|
+
2335
|
|
33
|
+
],
|
|
34
|
+
"summary": "decide by the use site in the two non-hook visitors too (closes #2335)"
|
|
35
|
+
}
|
|
36
|
+
]
|
|
37
|
+
},
|
|
2
38
|
{
|
|
3
39
|
"version": "1.21.10",
|
|
4
40
|
"date": "2026-09-05T09:28:38.523Z",
|