@blumintinc/eslint-plugin-blumint 1.21.12 → 1.21.14
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 +1 -1
- package/lib/rules/global-const-style.js +418 -45
- package/package.json +1 -1
- package/release-manifest.json +28 -0
package/lib/index.js
CHANGED
|
@@ -270,6 +270,68 @@ const isMutatingMethodCall = (path) => {
|
|
|
270
270
|
return (callee.parent?.type === utils_1.AST_NODE_TYPES.CallExpression &&
|
|
271
271
|
callee.parent.callee === callee);
|
|
272
272
|
};
|
|
273
|
+
/**
|
|
274
|
+
* The mutating methods that INSERT a value into the receiver, mapped to the
|
|
275
|
+
* argument positions that value can occupy.
|
|
276
|
+
*
|
|
277
|
+
* The positions are carried per method rather than taken as "every argument",
|
|
278
|
+
* because two of these spend leading or trailing arguments on INDICES:
|
|
279
|
+
* `splice(start, deleteCount, ...items)` inserts from the third argument on,
|
|
280
|
+
* and `fill(value, start, end)` inserts at the first alone.
|
|
281
|
+
*
|
|
282
|
+
* `sort`, `reverse`, `pop`, `shift` and `copyWithin` are absent because they
|
|
283
|
+
* insert nothing — they reorder, remove or copy elements the receiver already
|
|
284
|
+
* holds, so no element type can reject what they write. (`sort`'s argument is a
|
|
285
|
+
* comparator function, `copyWithin`'s three are indices.)
|
|
286
|
+
*/
|
|
287
|
+
const INSERTED_VALUE_POSITIONS_BY_METHOD = new Map([
|
|
288
|
+
['push', { first: 0 }],
|
|
289
|
+
['unshift', { first: 0 }],
|
|
290
|
+
['splice', { first: 2 }],
|
|
291
|
+
['fill', { first: 0, last: 0 }],
|
|
292
|
+
]);
|
|
293
|
+
/**
|
|
294
|
+
* Whether a mutating call INTRODUCES a value the receiver's element type would
|
|
295
|
+
* have to accept from outside the constant.
|
|
296
|
+
*
|
|
297
|
+
* This is the question that decides a mutating call through a receiver-array
|
|
298
|
+
* parameter the lib declares MUTABLE, where no readonly violation is possible
|
|
299
|
+
* and the only way the assertion can break the call is by narrowing what the
|
|
300
|
+
* array accepts — see `MUTABLE_ARRAY_PARAMETER_METHODS`. Three answers, and the
|
|
301
|
+
* boundary sits between the second and the third:
|
|
302
|
+
*
|
|
303
|
+
* - a method that inserts nothing (`arr.sort()`) cannot narrow-break, because
|
|
304
|
+
* it writes back only elements the receiver already holds;
|
|
305
|
+
* - an inserted value that is a REFERENCE to a binding already enrolled for
|
|
306
|
+
* this constant (`arr.push(item)`, where `item` is the element the callback
|
|
307
|
+
* was handed) is typed from the constant itself, so the assertion narrows the
|
|
308
|
+
* argument and the parameter together and the call keeps compiling;
|
|
309
|
+
* - an inserted value from anywhere else (`arr.push({ n: 3 })`) is typed
|
|
310
|
+
* independently of the constant, so narrowing the element type can reject it:
|
|
311
|
+
* TS2322 for an input that compiled (Issue #2340).
|
|
312
|
+
*
|
|
313
|
+
* A SPREAD argument is treated as introducing a foreign value even when it
|
|
314
|
+
* spreads the constant. Its elements do satisfy the narrowed type, so this
|
|
315
|
+
* withholds the assertion from a call that would have compiled — the cheap
|
|
316
|
+
* error of the two, and the one this predicate exists to prefer.
|
|
317
|
+
*/
|
|
318
|
+
const introducesForeignElement = (path, isEnrolledReference) => {
|
|
319
|
+
const method = accessedPropertyName(path);
|
|
320
|
+
const positions = method === null
|
|
321
|
+
? undefined
|
|
322
|
+
: INSERTED_VALUE_POSITIONS_BY_METHOD.get(method);
|
|
323
|
+
if (!positions) {
|
|
324
|
+
return false;
|
|
325
|
+
}
|
|
326
|
+
const call = outermostValueOf(path).parent;
|
|
327
|
+
if (call?.type !== utils_1.AST_NODE_TYPES.CallExpression) {
|
|
328
|
+
return false;
|
|
329
|
+
}
|
|
330
|
+
const last = positions.last ?? call.arguments.length - 1;
|
|
331
|
+
return call.arguments
|
|
332
|
+
.slice(positions.first, last + 1)
|
|
333
|
+
.some((argument) => !isEnrolledReference(argument));
|
|
334
|
+
};
|
|
273
335
|
/**
|
|
274
336
|
* Whether `node` sits in a position that writes to it: the left of an
|
|
275
337
|
* assignment (plain or compound), the operand of `++`/`--` or `delete`, the
|
|
@@ -588,8 +650,10 @@ const isInferenceSite = (identifier) => {
|
|
|
588
650
|
* second: a walk keyed on the first parameter would enrol a binding typed from
|
|
589
651
|
* the seed value and miss the one typed from the constant (Issue #2338).
|
|
590
652
|
*
|
|
591
|
-
*
|
|
592
|
-
* whatever the receiver holds, so nothing the assertion
|
|
653
|
+
* The INDEX parameter is absent from every map here, and from nothing else:
|
|
654
|
+
* it is a `number` whatever the receiver holds, so nothing the assertion
|
|
655
|
+
* changes reaches it. The parameter AFTER the index is a different matter —
|
|
656
|
+
* see `ARRAY_PARAMETER_INDEX_BY_METHOD`.
|
|
593
657
|
*/
|
|
594
658
|
const ELEMENT_PARAMETER_INDEX_BY_METHOD = new Map([
|
|
595
659
|
['forEach', 0],
|
|
@@ -605,6 +669,65 @@ const ELEMENT_PARAMETER_INDEX_BY_METHOD = new Map([
|
|
|
605
669
|
['reduce', 1],
|
|
606
670
|
['reduceRight', 1],
|
|
607
671
|
]);
|
|
672
|
+
/**
|
|
673
|
+
* The same methods, mapped to the position the RECEIVER ARRAY arrives in.
|
|
674
|
+
*
|
|
675
|
+
* That parameter is a second name for the iterated value itself, so a mutating
|
|
676
|
+
* call through it writes to the constant:
|
|
677
|
+
* `ITEMS.forEach((item, index, arr) => { arr.push(2); })` is TS2339 once
|
|
678
|
+
* `ITEMS` is frozen, for an input that compiled — the identical call written
|
|
679
|
+
* directly as `ITEMS.push(2)` is one the rule already declines for, so only the
|
|
680
|
+
* handed-node spelling escapes it (Issue #2339).
|
|
681
|
+
*
|
|
682
|
+
* The position is carried apart from the element's because the two are enrolled
|
|
683
|
+
* on different terms rather than because they differ by one: an element keeps
|
|
684
|
+
* the constant's type through every derivation the iteration walk follows,
|
|
685
|
+
* while the receiver is the constant only when the iteration reads the
|
|
686
|
+
* constant's own value or member path — see `iterationBindingsOf`.
|
|
687
|
+
* `reduce`/`reduceRight` push it to fourth, having spent the first position on
|
|
688
|
+
* the accumulator.
|
|
689
|
+
*
|
|
690
|
+
* `flatMap` is listed for its ELEMENTS alone. Its lib signature declares the
|
|
691
|
+
* parameter `T[]` where every sibling declares it `readonly T[]` — measured
|
|
692
|
+
* against `lib.es2020` — so `arr[0].n = 2` inside a `flatMap` callback is
|
|
693
|
+
* TS2540 for an input that compiled, while a mutating method called through the
|
|
694
|
+
* parameter compiles unchanged. `MUTABLE_ARRAY_PARAMETER_METHODS` carries that
|
|
695
|
+
* second half, which enrolment alone cannot express (Issue #2340).
|
|
696
|
+
*/
|
|
697
|
+
const ARRAY_PARAMETER_INDEX_BY_METHOD = new Map([
|
|
698
|
+
['forEach', 2],
|
|
699
|
+
['map', 2],
|
|
700
|
+
['filter', 2],
|
|
701
|
+
['find', 2],
|
|
702
|
+
['findIndex', 2],
|
|
703
|
+
['findLast', 2],
|
|
704
|
+
['findLastIndex', 2],
|
|
705
|
+
['some', 2],
|
|
706
|
+
['every', 2],
|
|
707
|
+
['flatMap', 2],
|
|
708
|
+
['reduce', 3],
|
|
709
|
+
['reduceRight', 3],
|
|
710
|
+
]);
|
|
711
|
+
/**
|
|
712
|
+
* The methods above whose receiver-array parameter is declared MUTABLE `T[]`.
|
|
713
|
+
*
|
|
714
|
+
* Enrolling that parameter answers two questions at once, and each needs its own
|
|
715
|
+
* answer. Its ELEMENTS are frozen with the constant, so an element write through
|
|
716
|
+
* it is TS2540 and the assertion is withheld. A mutating METHOD through it is no
|
|
717
|
+
* readonly violation at all — the declared type is mutable, so there is no
|
|
718
|
+
* TS2339 to have — and withholding the assertion for one costs a report for a
|
|
719
|
+
* break that does not happen: `ITEMS.flatMap((x, i, arr) => { arr.sort(); return
|
|
720
|
+
* [x]; })` and the `arr.push(x)` spelling both compile under the assertion,
|
|
721
|
+
* measured by appending it by hand and reading the checker.
|
|
722
|
+
*
|
|
723
|
+
* What such a call CAN break is assignability, and only by introducing a value
|
|
724
|
+
* from outside the constant: `arr.push({ n: 3 })` is TS2322 once the assertion
|
|
725
|
+
* narrows the element type. That is decided per CALL by
|
|
726
|
+
* `introducesForeignElement`, not per method — an exemption keyed on the method
|
|
727
|
+
* alone would trade two over-declines for a `--fix` that stops the file
|
|
728
|
+
* compiling, which is the defect this walk exists to prevent (Issue #2340).
|
|
729
|
+
*/
|
|
730
|
+
const MUTABLE_ARRAY_PARAMETER_METHODS = new Set(['flatMap']);
|
|
608
731
|
/**
|
|
609
732
|
* The `Object.values(X)` / `Object.entries(X)` call this value feeds — a fresh
|
|
610
733
|
* array whose ELEMENTS are the constant's own property values, so freezing the
|
|
@@ -630,9 +753,150 @@ const elementProjectionCallOf = (node) => {
|
|
|
630
753
|
: null;
|
|
631
754
|
};
|
|
632
755
|
/**
|
|
633
|
-
*
|
|
634
|
-
*
|
|
635
|
-
*
|
|
756
|
+
* Array methods whose result ITERATES the receiver's own elements.
|
|
757
|
+
*
|
|
758
|
+
* The result is an iterator rather than an array, which is why it belongs to
|
|
759
|
+
* neither of the maps the walk already reads: nothing is copied, so
|
|
760
|
+
* `TYPE_PRESERVING_COPY_METHODS` refuses it, and nothing is handed to a
|
|
761
|
+
* callback, so `ELEMENT_PARAMETER_INDEX_BY_METHOD` refuses it too. Every
|
|
762
|
+
* binding taken from it still carries the constant's element type, so
|
|
763
|
+
* `for (const item of ITEMS.values()) { item.n = 2; }` is TS2540 once `ITEMS`
|
|
764
|
+
* is frozen, for an input that compiled (Issue #2340). `entries` yields
|
|
765
|
+
* `[index, element]` pairs, which carry the element exactly as `values` does.
|
|
766
|
+
*
|
|
767
|
+
* `keys` is absent for the reason `Object.keys` is: its result is a number
|
|
768
|
+
* whatever the receiver holds, so the assertion cannot reach a binding taken
|
|
769
|
+
* from it.
|
|
770
|
+
*/
|
|
771
|
+
const ELEMENT_ITERATOR_METHODS = new Set(['values', 'entries']);
|
|
772
|
+
const iteratorProjectionCallOf = (node) => {
|
|
773
|
+
const parent = node.parent;
|
|
774
|
+
if (parent?.type !== utils_1.AST_NODE_TYPES.MemberExpression ||
|
|
775
|
+
parent.object !== node) {
|
|
776
|
+
return null;
|
|
777
|
+
}
|
|
778
|
+
const method = accessedPropertyName(parent);
|
|
779
|
+
if (method === null || !ELEMENT_ITERATOR_METHODS.has(method)) {
|
|
780
|
+
return null;
|
|
781
|
+
}
|
|
782
|
+
// A method REFERENCE (`const walk = ITEMS.values;`) iterates nothing, so the
|
|
783
|
+
// iterator exists only once the method is called — the same terms
|
|
784
|
+
// `copyExpressionOf` reads a copy on.
|
|
785
|
+
const callee = outermostValueOf(parent);
|
|
786
|
+
return callee.parent?.type === utils_1.AST_NODE_TYPES.CallExpression &&
|
|
787
|
+
callee.parent.callee === callee
|
|
788
|
+
? callee.parent
|
|
789
|
+
: null;
|
|
790
|
+
};
|
|
791
|
+
/**
|
|
792
|
+
* Constructors that build a collection out of the argument's ELEMENTS.
|
|
793
|
+
*
|
|
794
|
+
* `new Set(ITEMS)` holds the constant's own contents, so iterating it hands out
|
|
795
|
+
* the frozen elements and `for (const item of new Set(ITEMS)) { item.n = 2; }`
|
|
796
|
+
* is TS2540 for an input that compiled (Issue #2340).
|
|
797
|
+
*
|
|
798
|
+
* The construction is not a copy in `copyExpressionOf`'s sense — the result has
|
|
799
|
+
* a different shape from the argument, so a write to the collection says
|
|
800
|
+
* nothing about the constant — which is why it is resolved here, where only the
|
|
801
|
+
* ITERATION question is asked. `WeakSet`/`WeakMap` are absent because they are
|
|
802
|
+
* not iterable, so no binding can be taken from one.
|
|
803
|
+
*/
|
|
804
|
+
const ELEMENT_PRESERVING_COLLECTION_NAMES = new Set(['Set', 'Map']);
|
|
805
|
+
const elementCollectionOf = (node) => {
|
|
806
|
+
const parent = node.parent;
|
|
807
|
+
if (parent?.type !== utils_1.AST_NODE_TYPES.NewExpression ||
|
|
808
|
+
parent.arguments[0] !== node) {
|
|
809
|
+
return null;
|
|
810
|
+
}
|
|
811
|
+
const callee = unwrapValueWrappers(parent.callee);
|
|
812
|
+
return callee.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
813
|
+
ELEMENT_PRESERVING_COLLECTION_NAMES.has(callee.name)
|
|
814
|
+
? parent
|
|
815
|
+
: null;
|
|
816
|
+
};
|
|
817
|
+
/**
|
|
818
|
+
* The expressions a reference denotes a FROZEN value through: the reference
|
|
819
|
+
* itself and every property access rooted at it — `CONFIG`, `CONFIG.list`,
|
|
820
|
+
* `CONFIG.list.rows` for `CONFIG.list.rows`.
|
|
821
|
+
*
|
|
822
|
+
* `as const` freezes the value in depth, so a property of the constant carries
|
|
823
|
+
* the assertion exactly as the constant does, and an iteration is routinely
|
|
824
|
+
* reached through one (`CONFIG.list.values()`, `Array.from(CONFIG.list, fn)`).
|
|
825
|
+
* A derivation resolver reads a node's immediate parent, so it sees only the
|
|
826
|
+
* innermost access unless each step of the path is offered to it in turn
|
|
827
|
+
* (Issue #2340).
|
|
828
|
+
*/
|
|
829
|
+
const accessPathsRootedAt = (identifier) => {
|
|
830
|
+
const paths = [];
|
|
831
|
+
let current = outermostValueOf(identifier);
|
|
832
|
+
for (;;) {
|
|
833
|
+
paths.push(current);
|
|
834
|
+
const parent = current.parent;
|
|
835
|
+
if (!parent ||
|
|
836
|
+
parent.type !== utils_1.AST_NODE_TYPES.MemberExpression ||
|
|
837
|
+
parent.object !== current) {
|
|
838
|
+
return paths;
|
|
839
|
+
}
|
|
840
|
+
current = outermostValueOf(parent);
|
|
841
|
+
}
|
|
842
|
+
};
|
|
843
|
+
/**
|
|
844
|
+
* Every way a value derived from this one keeps the constant's ELEMENT types:
|
|
845
|
+
* a copy of it, the `Object.values`/`Object.entries` array over it, the
|
|
846
|
+
* iterator its own `values`/`entries` hands back, and the collection built out
|
|
847
|
+
* of it. Each resolver returns an ANCESTOR of the node it is given, which is
|
|
848
|
+
* what lets the iteration walk follow them transitively without looping.
|
|
849
|
+
*/
|
|
850
|
+
const DERIVATION_RESOLVERS = [
|
|
851
|
+
copyExpressionOf,
|
|
852
|
+
elementProjectionCallOf,
|
|
853
|
+
iteratorProjectionCallOf,
|
|
854
|
+
elementCollectionOf,
|
|
855
|
+
];
|
|
856
|
+
const enrolFully = (variables) => variables.map((variable) => ({
|
|
857
|
+
variable,
|
|
858
|
+
breaksOnAnyMutatingMethod: true,
|
|
859
|
+
}));
|
|
860
|
+
/**
|
|
861
|
+
* The bindings a callback's parameters at `positions` introduce, each carrying
|
|
862
|
+
* the question its position can break on.
|
|
863
|
+
*
|
|
864
|
+
* A callback routinely declares fewer parameters than the caller passes, so a
|
|
865
|
+
* position is taken only where the signature actually spells it.
|
|
866
|
+
*
|
|
867
|
+
* The scope manager answers for the WHOLE function — every parameter, and a
|
|
868
|
+
* function expression's own name — so the enrolled parameters' bindings are
|
|
869
|
+
* picked out by the spans they are declared in. Taking the function's list
|
|
870
|
+
* whole would enrol the accumulator of a `reduce`, typed from the seed value
|
|
871
|
+
* rather than from the constant, and the index, which the assertion cannot
|
|
872
|
+
* reach.
|
|
873
|
+
*/
|
|
874
|
+
const parameterBindingsOf = (callback, positions, declaredVariablesOf) => {
|
|
875
|
+
const enrolled = positions.flatMap(({ index, breaksOnAnyMutatingMethod }) => {
|
|
876
|
+
const param = callback.params[index];
|
|
877
|
+
return param ? [{ param, breaksOnAnyMutatingMethod }] : [];
|
|
878
|
+
});
|
|
879
|
+
if (enrolled.length === 0) {
|
|
880
|
+
return [];
|
|
881
|
+
}
|
|
882
|
+
return declaredVariablesOf(callback).flatMap((variable) => {
|
|
883
|
+
const position = enrolled.find(({ param }) => variable.defs.some((def) => def.name.range[0] >= param.range[0] &&
|
|
884
|
+
def.name.range[1] <= param.range[1]));
|
|
885
|
+
return position
|
|
886
|
+
? [
|
|
887
|
+
{
|
|
888
|
+
variable,
|
|
889
|
+
breaksOnAnyMutatingMethod: position.breaksOnAnyMutatingMethod,
|
|
890
|
+
},
|
|
891
|
+
]
|
|
892
|
+
: [];
|
|
893
|
+
});
|
|
894
|
+
};
|
|
895
|
+
/**
|
|
896
|
+
* The bindings a construct that ITERATES `iterable` introduces: the head of a
|
|
897
|
+
* `for…of` over it, the parameter an array method hands each element to, and —
|
|
898
|
+
* when the iterated expression is the constant itself — the parameter that
|
|
899
|
+
* method hands the RECEIVER ARRAY to.
|
|
636
900
|
*
|
|
637
901
|
* A `for…of` head is accepted in all three binding spellings, on the same terms
|
|
638
902
|
* as `aliasDeclaratorOf` accepts all three declarator spellings — every name a
|
|
@@ -645,8 +909,14 @@ const elementProjectionCallOf = (node) => {
|
|
|
645
909
|
* A callback parameter is reached only through a function LITERAL: a callback
|
|
646
910
|
* passed by name is declared elsewhere, where its parameter carries whatever
|
|
647
911
|
* type that declaration gives it rather than one read off the constant.
|
|
912
|
+
*
|
|
913
|
+
* `iteratesConstantValue` says whether `iterable` is the constant's own value
|
|
914
|
+
* or member path rather than something derived from it. It gates the receiver
|
|
915
|
+
* ARRAY parameter alone: the element parameter is typed from the constant
|
|
916
|
+
* either way, while the array parameter names the constant only in the first
|
|
917
|
+
* case — see `iterationBindingsOf`.
|
|
648
918
|
*/
|
|
649
|
-
const
|
|
919
|
+
const bindingsOfIterationOver = (iterable, declaredVariablesOf, iteratesConstantValue) => {
|
|
650
920
|
// The member path is resolved first because the iterated expression is
|
|
651
921
|
// routinely a PROPERTY of the constant (`for (const x of CONFIG.list)`),
|
|
652
922
|
// which the alias walk refuses precisely because it arrives through a member
|
|
@@ -660,13 +930,32 @@ const elementBindingsOfIteration = (iterable, declaredVariablesOf) => {
|
|
|
660
930
|
if (parent.type === utils_1.AST_NODE_TYPES.ForOfStatement &&
|
|
661
931
|
parent.right === value &&
|
|
662
932
|
parent.left.type === utils_1.AST_NODE_TYPES.VariableDeclaration) {
|
|
663
|
-
return declaredVariablesOf(parent.left);
|
|
933
|
+
return enrolFully(declaredVariablesOf(parent.left));
|
|
934
|
+
}
|
|
935
|
+
// `Array.from(X, mapfn)` hands each element of `X` to `mapfn` exactly as
|
|
936
|
+
// `X.map` hands it to a callback, so the mapper's first parameter is typed
|
|
937
|
+
// from the constant and `Array.from(ITEMS, (item) => { item.n = 2; … })` is
|
|
938
|
+
// TS2540 once `ITEMS` is frozen. The two-argument form reaches the walk
|
|
939
|
+
// nowhere else: `isCopyingCall` admits `Array.from` at one argument alone,
|
|
940
|
+
// because a mapper retypes the RESULT — which says nothing about the element
|
|
941
|
+
// it is handed (Issue #2340). The mapper takes the element first and the
|
|
942
|
+
// index second, and is handed no receiver array at all.
|
|
943
|
+
if (parent.type === utils_1.AST_NODE_TYPES.CallExpression &&
|
|
944
|
+
parent.arguments[0] === value &&
|
|
945
|
+
isNamespacedCallee(parent.callee, 'Array', 'from')) {
|
|
946
|
+
const mapper = parent.arguments[1];
|
|
947
|
+
return mapper && isFunctionValue(mapper)
|
|
948
|
+
? parameterBindingsOf(mapper, [{ index: 0, breaksOnAnyMutatingMethod: true }], declaredVariablesOf)
|
|
949
|
+
: [];
|
|
664
950
|
}
|
|
665
951
|
if (path === null) {
|
|
666
952
|
return [];
|
|
667
953
|
}
|
|
668
954
|
const method = accessedPropertyName(path);
|
|
669
|
-
|
|
955
|
+
if (method === null) {
|
|
956
|
+
return [];
|
|
957
|
+
}
|
|
958
|
+
const elementIndex = ELEMENT_PARAMETER_INDEX_BY_METHOD.get(method);
|
|
670
959
|
if (elementIndex === undefined ||
|
|
671
960
|
parent.type !== utils_1.AST_NODE_TYPES.CallExpression ||
|
|
672
961
|
parent.callee !== value) {
|
|
@@ -676,25 +965,29 @@ const elementBindingsOfIteration = (iterable, declaredVariablesOf) => {
|
|
|
676
965
|
if (!callback || !isFunctionValue(callback)) {
|
|
677
966
|
return [];
|
|
678
967
|
}
|
|
679
|
-
const
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
968
|
+
const arrayIndex = iteratesConstantValue
|
|
969
|
+
? ARRAY_PARAMETER_INDEX_BY_METHOD.get(method)
|
|
970
|
+
: undefined;
|
|
971
|
+
return parameterBindingsOf(callback, [
|
|
972
|
+
{ index: elementIndex, breaksOnAnyMutatingMethod: true },
|
|
973
|
+
...(arrayIndex === undefined
|
|
974
|
+
? []
|
|
975
|
+
: [
|
|
976
|
+
{
|
|
977
|
+
index: arrayIndex,
|
|
978
|
+
breaksOnAnyMutatingMethod: !MUTABLE_ARRAY_PARAMETER_METHODS.has(method),
|
|
979
|
+
},
|
|
980
|
+
]),
|
|
981
|
+
], declaredVariablesOf);
|
|
690
982
|
};
|
|
691
983
|
/**
|
|
692
984
|
* The bindings ITERATING this reference introduces, directly or through a value
|
|
693
985
|
* derived from it that keeps its element types.
|
|
694
986
|
*
|
|
695
987
|
* Such a binding is typed from the constant exactly as a destructured copy is —
|
|
696
|
-
* it carries the ELEMENT type
|
|
697
|
-
* it breaks on the assertion
|
|
988
|
+
* it carries the ELEMENT type, or for the receiver parameter the whole value —
|
|
989
|
+
* so a write through it breaks on the assertion as a write through an alias
|
|
990
|
+
* does:
|
|
698
991
|
* `for (const item of ITEMS) { item.label = 'b'; }` is TS2540 once `ITEMS` is
|
|
699
992
|
* frozen, for an input that compiled (Issue #2338). Enrolling the binding is
|
|
700
993
|
* therefore the whole remedy; the walk's existing write, mutating-method and
|
|
@@ -702,20 +995,56 @@ const elementBindingsOfIteration = (iterable, declaredVariablesOf) => {
|
|
|
702
995
|
* only READS its element fixable.
|
|
703
996
|
*
|
|
704
997
|
* The derivations are followed because the receiver of the iteration is
|
|
705
|
-
* routinely
|
|
706
|
-
* `ITEMS.filter(Boolean).forEach(…)`, `Object.values(CONFIG).forEach(…)
|
|
707
|
-
* builds a fresh OUTER value whose
|
|
708
|
-
*
|
|
709
|
-
*
|
|
998
|
+
* routinely removed from the constant (`[...ITEMS].forEach(…)`,
|
|
999
|
+
* `ITEMS.filter(Boolean).forEach(…)`, `Object.values(CONFIG).forEach(…)`,
|
|
1000
|
+
* `ITEMS.values()`, `new Set(ITEMS)`): each builds a fresh OUTER value whose
|
|
1001
|
+
* elements are still the frozen ones, so the element binding breaks
|
|
1002
|
+
* identically.
|
|
1003
|
+
*
|
|
1004
|
+
* Following them is TRANSITIVE, on the same reasoning as the alias walk — every
|
|
1005
|
+
* hop keeps the element type, so a chain of them keeps it too, and
|
|
1006
|
+
* `ITEMS.filter(Boolean).slice().forEach((item) => { item.n = 2; })` is the
|
|
1007
|
+
* same TS2540 as the one-hop spelling that already declines. A single step
|
|
1008
|
+
* gave two spellings of one construct opposite verdicts (Issue #2340).
|
|
1009
|
+
*
|
|
1010
|
+
* The derivations are resolved from each step of the reference's own access
|
|
1011
|
+
* path as well as from the reference, since the value a derivation is taken
|
|
1012
|
+
* from is routinely a PROPERTY of the constant — see `accessPathsRootedAt`.
|
|
1013
|
+
*
|
|
1014
|
+
* The receiver ARRAY parameter is enrolled for the constant's own value or
|
|
1015
|
+
* member path ALONE, the one iterable that hands the callback the constant
|
|
1016
|
+
* itself. A derivation hands it the fresh outer value it
|
|
1017
|
+
* built, and mutating that is no readonly violation:
|
|
1018
|
+
* `[...ITEMS].forEach((item, index, arr) => { arr.push(3); })` does break after
|
|
1019
|
+
* the fix, but as TS2345 — the spread narrows the element type, so `3` is not
|
|
1020
|
+
* assignable — which belongs to the literal-narrowing family filed as #2330 and
|
|
1021
|
+
* needs the type checker. Enrolling it here would withhold the assertion for a
|
|
1022
|
+
* reason this arm cannot justify, so it is an over-decline (Issue #2339).
|
|
710
1023
|
*/
|
|
711
1024
|
const iterationBindingsOf = (identifier, declaredVariablesOf) => {
|
|
712
1025
|
const value = outermostValueOf(identifier);
|
|
713
|
-
const
|
|
714
|
-
value,
|
|
715
|
-
copyExpressionOf(value),
|
|
716
|
-
elementProjectionCallOf(value),
|
|
1026
|
+
const bindings = [
|
|
1027
|
+
...bindingsOfIterationOver(value, declaredVariablesOf, true),
|
|
717
1028
|
];
|
|
718
|
-
|
|
1029
|
+
// Grown in place and walked by index, so a derivation OF a derivation is
|
|
1030
|
+
// reached by the same loop without recursion of its own. Every resolver
|
|
1031
|
+
// returns an ancestor of the node it is given, so the walk strictly ascends
|
|
1032
|
+
// and terminates; `visited` keeps a node two resolvers agree on from being
|
|
1033
|
+
// expanded twice.
|
|
1034
|
+
const pending = accessPathsRootedAt(value);
|
|
1035
|
+
const visited = new Set(pending);
|
|
1036
|
+
for (let index = 0; index < pending.length; index += 1) {
|
|
1037
|
+
for (const resolveDerivation of DERIVATION_RESOLVERS) {
|
|
1038
|
+
const derived = resolveDerivation(pending[index]);
|
|
1039
|
+
if (!derived || visited.has(derived)) {
|
|
1040
|
+
continue;
|
|
1041
|
+
}
|
|
1042
|
+
visited.add(derived);
|
|
1043
|
+
pending.push(derived);
|
|
1044
|
+
bindings.push(...bindingsOfIterationOver(derived, declaredVariablesOf, false));
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
return bindings;
|
|
719
1048
|
};
|
|
720
1049
|
/**
|
|
721
1050
|
* Whether anything in the file stops this binding taking `as const`, under its
|
|
@@ -748,13 +1077,16 @@ const iterationBindingsOf = (identifier, declaredVariablesOf) => {
|
|
|
748
1077
|
* the one value — and `visited` keeps a chain that leads back on itself, which
|
|
749
1078
|
* a redeclared `var` can build, from looping forever.
|
|
750
1079
|
*
|
|
751
|
-
* Iteration is followed on the same reasoning, keyed on the
|
|
752
|
-
*
|
|
753
|
-
* second names for the constant's contents, so `for (const item of ITEMS) {
|
|
1080
|
+
* Iteration is followed on the same reasoning, keyed on what the construct
|
|
1081
|
+
* HANDS its body: a `for…of` head and an iteration callback's element parameter
|
|
1082
|
+
* are second names for the constant's contents, so `for (const item of ITEMS) {
|
|
754
1083
|
* item.label = 'b'; }` is TS2540 once `ITEMS` is frozen while `ITEMS`'s own
|
|
755
|
-
* references show nothing but a read (Issue #2338)
|
|
756
|
-
*
|
|
757
|
-
*
|
|
1084
|
+
* references show nothing but a read (Issue #2338); the parameter after the
|
|
1085
|
+
* index is a second name for the constant ITSELF, so `arr.push(2)` inside the
|
|
1086
|
+
* callback is the TS2339 the rule already declines for when the same call is
|
|
1087
|
+
* written directly (Issue #2339). Enrolling the binding is all it takes — the
|
|
1088
|
+
* checks above then decide, so a callback that only reads what it is handed
|
|
1089
|
+
* keeps the assertion.
|
|
758
1090
|
*
|
|
759
1091
|
* The declaring KEYWORD is deliberately not screened. `as const` types the
|
|
760
1092
|
* value `readonly`, and a binding takes its declared type from its initializer,
|
|
@@ -767,10 +1099,37 @@ const iterationBindingsOf = (identifier, declaredVariablesOf) => {
|
|
|
767
1099
|
const blocksAsConstAssertion = (variable, declaredVariablesOf) => {
|
|
768
1100
|
// Grown in place and walked by index: an alias found mid-walk is appended and
|
|
769
1101
|
// reached by the same loop, so the traversal needs no recursion of its own.
|
|
770
|
-
const pending = [
|
|
771
|
-
|
|
1102
|
+
const pending = [
|
|
1103
|
+
{ variable, breaksOnAnyMutatingMethod: true },
|
|
1104
|
+
];
|
|
1105
|
+
const visited = new Set([variable]);
|
|
1106
|
+
/**
|
|
1107
|
+
* Whether a node is a REFERENCE to a binding already enrolled for this
|
|
1108
|
+
* constant, and so holds a value typed from the constant itself.
|
|
1109
|
+
*
|
|
1110
|
+
* Answered from the scope manager's reference lists rather than by name, on
|
|
1111
|
+
* the same terms as the rest of the walk: a same-named binding from another
|
|
1112
|
+
* scope names another value and must not exempt anything.
|
|
1113
|
+
*
|
|
1114
|
+
* A binding enrolled LATER in the walk than the one being examined answers
|
|
1115
|
+
* false here. That can only withhold an exemption, never grant one wrongly,
|
|
1116
|
+
* so the walk order costs a report at worst.
|
|
1117
|
+
*/
|
|
1118
|
+
const isEnrolledReference = (node) => {
|
|
1119
|
+
const value = unwrapValueWrappers(node);
|
|
1120
|
+
if (value.type !== utils_1.AST_NODE_TYPES.Identifier) {
|
|
1121
|
+
return false;
|
|
1122
|
+
}
|
|
1123
|
+
for (const enrolledVariable of visited) {
|
|
1124
|
+
if (enrolledVariable.references.some((enrolledReference) => enrolledReference.identifier === value)) {
|
|
1125
|
+
return true;
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
return false;
|
|
1129
|
+
};
|
|
772
1130
|
for (let index = 0; index < pending.length; index += 1) {
|
|
773
|
-
|
|
1131
|
+
const { variable: enrolled, breaksOnAnyMutatingMethod } = pending[index];
|
|
1132
|
+
for (const reference of enrolled.references) {
|
|
774
1133
|
// Reassigning an alias is as disqualifying as writing through one. A
|
|
775
1134
|
// binding that takes its type from the constant narrows to the frozen
|
|
776
1135
|
// literal, so `let stage = DEFAULT; stage = 'live';` becomes TS2322 for
|
|
@@ -783,8 +1142,20 @@ const blocksAsConstAssertion = (variable, declaredVariablesOf) => {
|
|
|
783
1142
|
return true;
|
|
784
1143
|
}
|
|
785
1144
|
const path = accessPathOf(reference.identifier);
|
|
1145
|
+
// The ELEMENT question is asked of every binding alike: the elements are
|
|
1146
|
+
// frozen whatever the container's own declaration says.
|
|
1147
|
+
if (path !== null && isWriteTarget(path)) {
|
|
1148
|
+
return true;
|
|
1149
|
+
}
|
|
1150
|
+
// The mutating-method question is asked in full of every binding the
|
|
1151
|
+
// assertion types `readonly`, where any such call is a TS2339. A
|
|
1152
|
+
// parameter the lib declares MUTABLE has no such break to have, so it is
|
|
1153
|
+
// asked the narrower question that remains: does this call introduce a
|
|
1154
|
+
// value the narrowed element type would have to accept (Issue #2340).
|
|
786
1155
|
if (path !== null &&
|
|
787
|
-
|
|
1156
|
+
isMutatingMethodCall(path) &&
|
|
1157
|
+
(breaksOnAnyMutatingMethod ||
|
|
1158
|
+
introducesForeignElement(path, isEnrolledReference))) {
|
|
788
1159
|
return true;
|
|
789
1160
|
}
|
|
790
1161
|
// A copy carries the constant's frozen type into a second binding, so it
|
|
@@ -799,14 +1170,16 @@ const blocksAsConstAssertion = (variable, declaredVariablesOf) => {
|
|
|
799
1170
|
: null;
|
|
800
1171
|
// A binding introduced by ITERATING the constant is enrolled beside the
|
|
801
1172
|
// aliases: it names the constant's CONTENTS, which the assertion freezes
|
|
802
|
-
// with the constant itself — see `iterationBindingsOf`.
|
|
1173
|
+
// with the constant itself — see `iterationBindingsOf`. An alias is
|
|
1174
|
+
// enrolled on the constant's own terms, because it denotes the constant's
|
|
1175
|
+
// value and so carries its readonly-ness whole.
|
|
803
1176
|
const derived = [
|
|
804
|
-
...(declarator ? declaredVariablesOf(declarator) : []),
|
|
1177
|
+
...enrolFully(declarator ? declaredVariablesOf(declarator) : []),
|
|
805
1178
|
...iterationBindingsOf(reference.identifier, declaredVariablesOf),
|
|
806
1179
|
];
|
|
807
1180
|
for (const alias of derived) {
|
|
808
|
-
if (!visited.has(alias)) {
|
|
809
|
-
visited.add(alias);
|
|
1181
|
+
if (!visited.has(alias.variable)) {
|
|
1182
|
+
visited.add(alias.variable);
|
|
810
1183
|
pending.push(alias);
|
|
811
1184
|
}
|
|
812
1185
|
}
|
package/package.json
CHANGED
package/release-manifest.json
CHANGED
|
@@ -1,4 +1,32 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "1.21.14",
|
|
4
|
+
"date": "2026-09-06T01:53:58.858Z",
|
|
5
|
+
"rules": [
|
|
6
|
+
{
|
|
7
|
+
"name": "global-const-style",
|
|
8
|
+
"changeType": "fix",
|
|
9
|
+
"issues": [
|
|
10
|
+
2340
|
|
11
|
+
],
|
|
12
|
+
"summary": "enrol the element bindings of derived iterables (closes #2340)"
|
|
13
|
+
}
|
|
14
|
+
]
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"version": "1.21.13",
|
|
18
|
+
"date": "2026-09-05T22:39:56.997Z",
|
|
19
|
+
"rules": [
|
|
20
|
+
{
|
|
21
|
+
"name": "global-const-style",
|
|
22
|
+
"changeType": "fix",
|
|
23
|
+
"issues": [
|
|
24
|
+
2339
|
|
25
|
+
],
|
|
26
|
+
"summary": "enrol the receiver array parameter of an iteration over the constant (closes #2339)"
|
|
27
|
+
}
|
|
28
|
+
]
|
|
29
|
+
},
|
|
2
30
|
{
|
|
3
31
|
"version": "1.21.12",
|
|
4
32
|
"date": "2026-09-05T18:30:20.453Z",
|