@blumintinc/eslint-plugin-blumint 1.21.13 → 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 +342 -44
- package/package.json +1 -1
- package/release-manifest.json +14 -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
|
|
@@ -625,12 +687,12 @@ const ELEMENT_PARAMETER_INDEX_BY_METHOD = new Map([
|
|
|
625
687
|
* `reduce`/`reduceRight` push it to fourth, having spent the first position on
|
|
626
688
|
* the accumulator.
|
|
627
689
|
*
|
|
628
|
-
* `flatMap` is listed
|
|
629
|
-
* `T[]` where every sibling declares it `readonly T[]` — measured
|
|
630
|
-
* `lib.es2020
|
|
631
|
-
*
|
|
632
|
-
*
|
|
633
|
-
*
|
|
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).
|
|
634
696
|
*/
|
|
635
697
|
const ARRAY_PARAMETER_INDEX_BY_METHOD = new Map([
|
|
636
698
|
['forEach', 2],
|
|
@@ -646,6 +708,26 @@ const ARRAY_PARAMETER_INDEX_BY_METHOD = new Map([
|
|
|
646
708
|
['reduce', 3],
|
|
647
709
|
['reduceRight', 3],
|
|
648
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']);
|
|
649
731
|
/**
|
|
650
732
|
* The `Object.values(X)` / `Object.entries(X)` call this value feeds — a fresh
|
|
651
733
|
* array whose ELEMENTS are the constant's own property values, so freezing the
|
|
@@ -670,6 +752,146 @@ const elementProjectionCallOf = (node) => {
|
|
|
670
752
|
? parent
|
|
671
753
|
: null;
|
|
672
754
|
};
|
|
755
|
+
/**
|
|
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
|
+
};
|
|
673
895
|
/**
|
|
674
896
|
* The bindings a construct that ITERATES `iterable` introduces: the head of a
|
|
675
897
|
* `for…of` over it, the parameter an array method hands each element to, and —
|
|
@@ -708,7 +930,23 @@ const bindingsOfIterationOver = (iterable, declaredVariablesOf, iteratesConstant
|
|
|
708
930
|
if (parent.type === utils_1.AST_NODE_TYPES.ForOfStatement &&
|
|
709
931
|
parent.right === value &&
|
|
710
932
|
parent.left.type === utils_1.AST_NODE_TYPES.VariableDeclaration) {
|
|
711
|
-
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
|
+
: [];
|
|
712
950
|
}
|
|
713
951
|
if (path === null) {
|
|
714
952
|
return [];
|
|
@@ -730,23 +968,17 @@ const bindingsOfIterationOver = (iterable, declaredVariablesOf, iteratesConstant
|
|
|
730
968
|
const arrayIndex = iteratesConstantValue
|
|
731
969
|
? ARRAY_PARAMETER_INDEX_BY_METHOD.get(method)
|
|
732
970
|
: undefined;
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
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])));
|
|
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);
|
|
750
982
|
};
|
|
751
983
|
/**
|
|
752
984
|
* The bindings ITERATING this reference introduces, directly or through a value
|
|
@@ -763,15 +995,25 @@ const bindingsOfIterationOver = (iterable, declaredVariablesOf, iteratesConstant
|
|
|
763
995
|
* only READS its element fixable.
|
|
764
996
|
*
|
|
765
997
|
* The derivations are followed because the receiver of the iteration is
|
|
766
|
-
* routinely
|
|
767
|
-
* `ITEMS.filter(Boolean).forEach(…)`, `Object.values(CONFIG).forEach(…)
|
|
768
|
-
* builds a fresh OUTER value whose
|
|
769
|
-
*
|
|
770
|
-
*
|
|
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`.
|
|
771
1013
|
*
|
|
772
1014
|
* The receiver ARRAY parameter is enrolled for the constant's own value or
|
|
773
|
-
* member path ALONE,
|
|
774
|
-
*
|
|
1015
|
+
* member path ALONE, the one iterable that hands the callback the constant
|
|
1016
|
+
* itself. A derivation hands it the fresh outer value it
|
|
775
1017
|
* built, and mutating that is no readonly violation:
|
|
776
1018
|
* `[...ITEMS].forEach((item, index, arr) => { arr.push(3); })` does break after
|
|
777
1019
|
* the fix, but as TS2345 — the spread narrows the element type, so `3` is not
|
|
@@ -781,13 +1023,28 @@ const bindingsOfIterationOver = (iterable, declaredVariablesOf, iteratesConstant
|
|
|
781
1023
|
*/
|
|
782
1024
|
const iterationBindingsOf = (identifier, declaredVariablesOf) => {
|
|
783
1025
|
const value = outermostValueOf(identifier);
|
|
784
|
-
const
|
|
785
|
-
return [
|
|
1026
|
+
const bindings = [
|
|
786
1027
|
...bindingsOfIterationOver(value, declaredVariablesOf, true),
|
|
787
|
-
...derivations.flatMap((iterable) => iterable
|
|
788
|
-
? bindingsOfIterationOver(iterable, declaredVariablesOf, false)
|
|
789
|
-
: []),
|
|
790
1028
|
];
|
|
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;
|
|
791
1048
|
};
|
|
792
1049
|
/**
|
|
793
1050
|
* Whether anything in the file stops this binding taking `as const`, under its
|
|
@@ -842,10 +1099,37 @@ const iterationBindingsOf = (identifier, declaredVariablesOf) => {
|
|
|
842
1099
|
const blocksAsConstAssertion = (variable, declaredVariablesOf) => {
|
|
843
1100
|
// Grown in place and walked by index: an alias found mid-walk is appended and
|
|
844
1101
|
// reached by the same loop, so the traversal needs no recursion of its own.
|
|
845
|
-
const pending = [
|
|
846
|
-
|
|
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
|
+
};
|
|
847
1130
|
for (let index = 0; index < pending.length; index += 1) {
|
|
848
|
-
|
|
1131
|
+
const { variable: enrolled, breaksOnAnyMutatingMethod } = pending[index];
|
|
1132
|
+
for (const reference of enrolled.references) {
|
|
849
1133
|
// Reassigning an alias is as disqualifying as writing through one. A
|
|
850
1134
|
// binding that takes its type from the constant narrows to the frozen
|
|
851
1135
|
// literal, so `let stage = DEFAULT; stage = 'live';` becomes TS2322 for
|
|
@@ -858,8 +1142,20 @@ const blocksAsConstAssertion = (variable, declaredVariablesOf) => {
|
|
|
858
1142
|
return true;
|
|
859
1143
|
}
|
|
860
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).
|
|
861
1155
|
if (path !== null &&
|
|
862
|
-
|
|
1156
|
+
isMutatingMethodCall(path) &&
|
|
1157
|
+
(breaksOnAnyMutatingMethod ||
|
|
1158
|
+
introducesForeignElement(path, isEnrolledReference))) {
|
|
863
1159
|
return true;
|
|
864
1160
|
}
|
|
865
1161
|
// A copy carries the constant's frozen type into a second binding, so it
|
|
@@ -874,14 +1170,16 @@ const blocksAsConstAssertion = (variable, declaredVariablesOf) => {
|
|
|
874
1170
|
: null;
|
|
875
1171
|
// A binding introduced by ITERATING the constant is enrolled beside the
|
|
876
1172
|
// aliases: it names the constant's CONTENTS, which the assertion freezes
|
|
877
|
-
// 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.
|
|
878
1176
|
const derived = [
|
|
879
|
-
...(declarator ? declaredVariablesOf(declarator) : []),
|
|
1177
|
+
...enrolFully(declarator ? declaredVariablesOf(declarator) : []),
|
|
880
1178
|
...iterationBindingsOf(reference.identifier, declaredVariablesOf),
|
|
881
1179
|
];
|
|
882
1180
|
for (const alias of derived) {
|
|
883
|
-
if (!visited.has(alias)) {
|
|
884
|
-
visited.add(alias);
|
|
1181
|
+
if (!visited.has(alias.variable)) {
|
|
1182
|
+
visited.add(alias.variable);
|
|
885
1183
|
pending.push(alias);
|
|
886
1184
|
}
|
|
887
1185
|
}
|
package/package.json
CHANGED
package/release-manifest.json
CHANGED
|
@@ -1,4 +1,18 @@
|
|
|
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
|
+
},
|
|
2
16
|
{
|
|
3
17
|
"version": "1.21.13",
|
|
4
18
|
"date": "2026-09-05T22:39:56.997Z",
|