@blumintinc/eslint-plugin-blumint 1.21.13 → 1.21.15

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.
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const utils_1 = require("@typescript-eslint/utils");
4
+ const ASTHelpers_1 = require("../utils/ASTHelpers");
4
5
  const createRule_1 = require("../utils/createRule");
5
6
  const isUpperSnakeCase = (str) => /^[A-Z][A-Z0-9_]*$/.test(str);
6
7
  /**
@@ -270,6 +271,68 @@ const isMutatingMethodCall = (path) => {
270
271
  return (callee.parent?.type === utils_1.AST_NODE_TYPES.CallExpression &&
271
272
  callee.parent.callee === callee);
272
273
  };
274
+ /**
275
+ * The mutating methods that INSERT a value into the receiver, mapped to the
276
+ * argument positions that value can occupy.
277
+ *
278
+ * The positions are carried per method rather than taken as "every argument",
279
+ * because two of these spend leading or trailing arguments on INDICES:
280
+ * `splice(start, deleteCount, ...items)` inserts from the third argument on,
281
+ * and `fill(value, start, end)` inserts at the first alone.
282
+ *
283
+ * `sort`, `reverse`, `pop`, `shift` and `copyWithin` are absent because they
284
+ * insert nothing — they reorder, remove or copy elements the receiver already
285
+ * holds, so no element type can reject what they write. (`sort`'s argument is a
286
+ * comparator function, `copyWithin`'s three are indices.)
287
+ */
288
+ const INSERTED_VALUE_POSITIONS_BY_METHOD = new Map([
289
+ ['push', { first: 0 }],
290
+ ['unshift', { first: 0 }],
291
+ ['splice', { first: 2 }],
292
+ ['fill', { first: 0, last: 0 }],
293
+ ]);
294
+ /**
295
+ * Whether a mutating call INTRODUCES a value the receiver's element type would
296
+ * have to accept from outside the constant.
297
+ *
298
+ * This is the question that decides a mutating call through a receiver-array
299
+ * parameter the lib declares MUTABLE, where no readonly violation is possible
300
+ * and the only way the assertion can break the call is by narrowing what the
301
+ * array accepts — see `MUTABLE_ARRAY_PARAMETER_METHODS`. Three answers, and the
302
+ * boundary sits between the second and the third:
303
+ *
304
+ * - a method that inserts nothing (`arr.sort()`) cannot narrow-break, because
305
+ * it writes back only elements the receiver already holds;
306
+ * - an inserted value that is a REFERENCE to a binding already enrolled for
307
+ * this constant (`arr.push(item)`, where `item` is the element the callback
308
+ * was handed) is typed from the constant itself, so the assertion narrows the
309
+ * argument and the parameter together and the call keeps compiling;
310
+ * - an inserted value from anywhere else (`arr.push({ n: 3 })`) is typed
311
+ * independently of the constant, so narrowing the element type can reject it:
312
+ * TS2322 for an input that compiled (Issue #2340).
313
+ *
314
+ * A SPREAD argument is treated as introducing a foreign value even when it
315
+ * spreads the constant. Its elements do satisfy the narrowed type, so this
316
+ * withholds the assertion from a call that would have compiled — the cheap
317
+ * error of the two, and the one this predicate exists to prefer.
318
+ */
319
+ const introducesForeignElement = (path, isEnrolledReference) => {
320
+ const method = accessedPropertyName(path);
321
+ const positions = method === null
322
+ ? undefined
323
+ : INSERTED_VALUE_POSITIONS_BY_METHOD.get(method);
324
+ if (!positions) {
325
+ return false;
326
+ }
327
+ const call = outermostValueOf(path).parent;
328
+ if (call?.type !== utils_1.AST_NODE_TYPES.CallExpression) {
329
+ return false;
330
+ }
331
+ const last = positions.last ?? call.arguments.length - 1;
332
+ return call.arguments
333
+ .slice(positions.first, last + 1)
334
+ .some((argument) => !isEnrolledReference(argument));
335
+ };
273
336
  /**
274
337
  * Whether `node` sits in a position that writes to it: the left of an
275
338
  * assignment (plain or compound), the operand of `++`/`--` or `delete`, the
@@ -396,14 +459,123 @@ const isStructuredCloneCallee = (callee) => {
396
459
  const value = outermostValueOf(callee);
397
460
  return (value.type === utils_1.AST_NODE_TYPES.Identifier && value.name === 'structuredClone');
398
461
  };
462
+ const FUNCTION_TYPES = new Set([
463
+ utils_1.AST_NODE_TYPES.FunctionDeclaration,
464
+ utils_1.AST_NODE_TYPES.FunctionExpression,
465
+ utils_1.AST_NODE_TYPES.ArrowFunctionExpression,
466
+ utils_1.AST_NODE_TYPES.TSDeclareFunction,
467
+ ]);
468
+ /**
469
+ * The identifier an ACCESS PATH is rooted at — `x` for `x`, `x.n`, `x.a[0]`,
470
+ * `x?.n` and `x!.n` alike — or the expression itself when it is not an access
471
+ * path at all. Descends through the wrappers `outermostValueOf` climbs out of,
472
+ * so the root cannot depend on which type syntax annotates a step of the path.
473
+ */
474
+ const accessPathRootOf = (node) => {
475
+ let current = unwrapValueWrappers(node);
476
+ for (;;) {
477
+ if (current.type === utils_1.AST_NODE_TYPES.ChainExpression) {
478
+ current = unwrapValueWrappers(current.expression);
479
+ continue;
480
+ }
481
+ if (current.type === utils_1.AST_NODE_TYPES.MemberExpression) {
482
+ current = unwrapValueWrappers(current.object);
483
+ continue;
484
+ }
485
+ return current;
486
+ }
487
+ };
488
+ /**
489
+ * Every value a function hands back: its expression body, or the argument of
490
+ * each `return` in its block.
491
+ *
492
+ * Descent stops at a nested function, whose `return` answers for THAT function
493
+ * rather than this one — the same boundary `ASTHelpers.hasReturnStatement`
494
+ * keeps, for the same reason.
495
+ */
496
+ const returnedValuesOf = (callback) => {
497
+ if (callback.body.type !== utils_1.AST_NODE_TYPES.BlockStatement) {
498
+ return [callback.body];
499
+ }
500
+ const returned = [];
501
+ const visit = (node) => {
502
+ if (FUNCTION_TYPES.has(node.type)) {
503
+ return;
504
+ }
505
+ if (node.type === utils_1.AST_NODE_TYPES.ReturnStatement) {
506
+ if (node.argument) {
507
+ returned.push(node.argument);
508
+ }
509
+ return;
510
+ }
511
+ for (const [key, value] of Object.entries(node)) {
512
+ if (key === 'parent') {
513
+ continue;
514
+ }
515
+ for (const child of Array.isArray(value) ? value : [value]) {
516
+ if (ASTHelpers_1.ASTHelpers.isNode(child)) {
517
+ visit(child);
518
+ }
519
+ }
520
+ }
521
+ };
522
+ visit(callback.body);
523
+ return returned;
524
+ };
525
+ /**
526
+ * Whether a callback hands back the value it is given at `elementIndex`, or an
527
+ * access path rooted at it.
528
+ *
529
+ * This is what decides whether a `map`-shaped call keeps the receiver's element
530
+ * type. `(x) => x.n` over a frozen `[{ n: 1 }]` yields `1[]` rather than
531
+ * `number[]`, so `ns.push(3)` is TS2345 for an input that compiled — and that
532
+ * mapper is the commonest one written, not the no-op spelling the exclusion was
533
+ * justified against (Issue #2342). A callback that COMPUTES (`(x) => x * 2`,
534
+ * `() => Math.random()`) widens, carries nothing of the constant into its
535
+ * result, and keeps its report.
536
+ *
537
+ * The parameter is matched by NAME within the callback's own body, the single
538
+ * span this question is asked over, because a derivation resolver is handed a
539
+ * node and no scope. A name a nested function rebinds is unreachable — descent
540
+ * stops at every function boundary — so the worst a shadow can do is withhold
541
+ * the assertion from a call that would have kept it, the cheap error of the two.
542
+ *
543
+ * ANY returned value rooted at the element is enough. Branches returning
544
+ * different things widen their union, so the assertion may then reach nothing
545
+ * and the withhold costs a report; demanding EVERY branch would instead ship a
546
+ * `--fix` that stops the file compiling.
547
+ */
548
+ const returnsHandedElement = (callback, elementIndex) => {
549
+ if (!callback || !isFunctionValue(callback)) {
550
+ return false;
551
+ }
552
+ const parameter = callback.params[elementIndex];
553
+ if (!parameter || parameter.type !== utils_1.AST_NODE_TYPES.Identifier) {
554
+ return false;
555
+ }
556
+ return returnedValuesOf(callback).some((value) => {
557
+ const root = accessPathRootOf(value);
558
+ return (root.type === utils_1.AST_NODE_TYPES.Identifier && root.name === parameter.name);
559
+ });
560
+ };
561
+ /**
562
+ * The position the receiver's element arrives in for `Array.from`'s mapper.
563
+ *
564
+ * Named because the mapper is the SECOND argument while its element is the
565
+ * first parameter, so two different zeroes and ones sit beside each other here.
566
+ */
567
+ const ARRAY_FROM_MAPPER_ELEMENT_INDEX = 0;
399
568
  /**
400
569
  * Whether a call COPIES the argument at `index` while keeping its type.
401
570
  *
402
571
  * `Array.from(X)` and `structuredClone(X)` both hand back a fresh, mutable
403
572
  * value whose element or property types are the argument's — so freezing the
404
- * argument narrows the copy exactly as a spread does. `Array.from(X, fn)` is
405
- * excluded for the same reason `map` is: a mapper retypes the result, so
406
- * nothing of the constant's type survives into it.
573
+ * argument narrows the copy exactly as a spread does.
574
+ *
575
+ * `Array.from(X, fn)` is decided per CALL for the reason `map` is: a mapper
576
+ * that hands back the element or a property of it keeps the frozen type, so
577
+ * `Array.from(ITEMS, (x) => x.n)` is TS2345 on a later `push` for an input that
578
+ * compiled, while a mapper that COMPUTES widens and carries nothing.
407
579
  */
408
580
  const isCopyingCall = (call, index) => {
409
581
  if (isObjectAssignCallee(call.callee)) {
@@ -416,16 +588,26 @@ const isCopyingCall = (call, index) => {
416
588
  return true;
417
589
  }
418
590
  if (isNamespacedCallee(call.callee, 'Array', 'from')) {
419
- return call.arguments.length === 1;
591
+ return (call.arguments.length === 1 ||
592
+ returnsHandedElement(call.arguments[1], ARRAY_FROM_MAPPER_ELEMENT_INDEX));
420
593
  }
421
594
  return false;
422
595
  };
423
596
  /**
424
- * Array methods whose result keeps the receiver's ELEMENT type. `map` is
425
- * absent because its result is typed from the CALLBACK, so the constant's type
426
- * reaches it only for a callback that returns its argument unchanged — a no-op
427
- * `map`. Admitting it would withhold the assertion from every derived array
428
- * anything is computed from, to cover a spelling nobody writes.
597
+ * The copying array methods whose result is typed from a CALLBACK, mapped to
598
+ * the position the receiver's element arrives in.
599
+ *
600
+ * They are carried apart from `TYPE_PRESERVING_COPY_METHODS` because the
601
+ * question they raise is answered per CALL rather than per method — see
602
+ * `returnsHandedElement`.
603
+ */
604
+ const CALLBACK_TYPED_COPY_METHODS = new Map([['map', 0]]);
605
+ /**
606
+ * Array methods whose result keeps the receiver's ELEMENT type WHATEVER the
607
+ * call spells: nothing they are passed can retype what they hand back.
608
+ *
609
+ * `map` is carried separately rather than absent — see
610
+ * `CALLBACK_TYPED_COPY_METHODS`.
429
611
  */
430
612
  const TYPE_PRESERVING_COPY_METHODS = new Set([
431
613
  'concat',
@@ -476,15 +658,66 @@ const copyExpressionOf = (node) => {
476
658
  const callee = outermostValueOf(parent);
477
659
  // A method REFERENCE (`const take = ITEMS.concat;`) builds nothing, so the
478
660
  // copy only exists once the method is actually called.
479
- if (method !== null &&
480
- TYPE_PRESERVING_COPY_METHODS.has(method) &&
481
- callee.parent?.type === utils_1.AST_NODE_TYPES.CallExpression &&
482
- callee.parent.callee === callee) {
661
+ if (method === null ||
662
+ callee.parent?.type !== utils_1.AST_NODE_TYPES.CallExpression ||
663
+ callee.parent.callee !== callee) {
664
+ return null;
665
+ }
666
+ if (TYPE_PRESERVING_COPY_METHODS.has(method)) {
483
667
  return callee.parent;
484
668
  }
669
+ const elementIndex = CALLBACK_TYPED_COPY_METHODS.get(method);
670
+ return elementIndex !== undefined &&
671
+ returnsHandedElement(callee.parent.arguments[0], elementIndex)
672
+ ? callee.parent
673
+ : null;
485
674
  }
486
675
  return null;
487
676
  };
677
+ /**
678
+ * Array methods whose result is an ELEMENT of the receiver rather than a fresh
679
+ * array over it.
680
+ *
681
+ * `as const` freezes in depth, so the element they hand back carries the
682
+ * assertion exactly as one reached by index does: `const first = ITEMS.at(0)!;
683
+ * first.n = 2;` is TS2540 once `ITEMS` is frozen, for an input that compiled
684
+ * (Issue #2341). They belong with the element family rather than with
685
+ * `TYPE_PRESERVING_COPY_METHODS`, whose members hand back a container.
686
+ */
687
+ const ELEMENT_RETURNING_METHODS = new Set(['at', 'find', 'findLast']);
688
+ /**
689
+ * The folds whose result is an element of the receiver, in the SEEDLESS
690
+ * spelling alone.
691
+ *
692
+ * `ITEMS.reduce((a, b) => b)` is typed `T` because the overload without an
693
+ * initial value takes the first element as the seed. Given a seed the result is
694
+ * typed from THAT value, which the constant need not have given —
695
+ * `NUMS.reduce((sum, n) => sum + n, 0)` is `number` however `NUMS` is frozen —
696
+ * so enrolling the seeded spelling would withhold the assertion for a break
697
+ * that cannot happen.
698
+ */
699
+ const ELEMENT_FOLD_METHODS = new Set(['reduce', 'reduceRight']);
700
+ /** The call that hands back an ELEMENT of this value — see the two sets above. */
701
+ const elementExpressionOf = (node) => {
702
+ const parent = node.parent;
703
+ if (parent?.type !== utils_1.AST_NODE_TYPES.MemberExpression ||
704
+ parent.object !== node) {
705
+ return null;
706
+ }
707
+ const method = accessedPropertyName(parent);
708
+ if (method === null) {
709
+ return null;
710
+ }
711
+ const callee = outermostValueOf(parent);
712
+ const call = callee.parent;
713
+ if (call?.type !== utils_1.AST_NODE_TYPES.CallExpression || call.callee !== callee) {
714
+ return null;
715
+ }
716
+ return ELEMENT_RETURNING_METHODS.has(method) ||
717
+ (ELEMENT_FOLD_METHODS.has(method) && call.arguments.length === 1)
718
+ ? call
719
+ : null;
720
+ };
488
721
  /** Pattern nodes a parameter's binding can be nested inside. */
489
722
  const PATTERN_CONTAINERS = new Set([
490
723
  utils_1.AST_NODE_TYPES.AssignmentPattern,
@@ -497,12 +730,6 @@ const PATTERN_CONTAINERS = new Set([
497
730
  // constructor's params and `constructor(public stage = DEFAULT)` narrows.
498
731
  utils_1.AST_NODE_TYPES.TSParameterProperty,
499
732
  ]);
500
- const FUNCTION_TYPES = new Set([
501
- utils_1.AST_NODE_TYPES.FunctionDeclaration,
502
- utils_1.AST_NODE_TYPES.FunctionExpression,
503
- utils_1.AST_NODE_TYPES.ArrowFunctionExpression,
504
- utils_1.AST_NODE_TYPES.TSDeclareFunction,
505
- ]);
506
733
  /**
507
734
  * Whether a default value is what a PARAMETER's type is inferred FROM.
508
735
  *
@@ -625,12 +852,12 @@ const ELEMENT_PARAMETER_INDEX_BY_METHOD = new Map([
625
852
  * `reduce`/`reduceRight` push it to fourth, having spent the first position on
626
853
  * the accumulator.
627
854
  *
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.
855
+ * `flatMap` is listed for its ELEMENTS alone. Its lib signature declares the
856
+ * parameter `T[]` where every sibling declares it `readonly T[]` — measured
857
+ * against `lib.es2020` — so `arr[0].n = 2` inside a `flatMap` callback is
858
+ * TS2540 for an input that compiled, while a mutating method called through the
859
+ * parameter compiles unchanged. `MUTABLE_ARRAY_PARAMETER_METHODS` carries that
860
+ * second half, which enrolment alone cannot express (Issue #2340).
634
861
  */
635
862
  const ARRAY_PARAMETER_INDEX_BY_METHOD = new Map([
636
863
  ['forEach', 2],
@@ -646,6 +873,26 @@ const ARRAY_PARAMETER_INDEX_BY_METHOD = new Map([
646
873
  ['reduce', 3],
647
874
  ['reduceRight', 3],
648
875
  ]);
876
+ /**
877
+ * The methods above whose receiver-array parameter is declared MUTABLE `T[]`.
878
+ *
879
+ * Enrolling that parameter answers two questions at once, and each needs its own
880
+ * answer. Its ELEMENTS are frozen with the constant, so an element write through
881
+ * it is TS2540 and the assertion is withheld. A mutating METHOD through it is no
882
+ * readonly violation at all — the declared type is mutable, so there is no
883
+ * TS2339 to have — and withholding the assertion for one costs a report for a
884
+ * break that does not happen: `ITEMS.flatMap((x, i, arr) => { arr.sort(); return
885
+ * [x]; })` and the `arr.push(x)` spelling both compile under the assertion,
886
+ * measured by appending it by hand and reading the checker.
887
+ *
888
+ * What such a call CAN break is assignability, and only by introducing a value
889
+ * from outside the constant: `arr.push({ n: 3 })` is TS2322 once the assertion
890
+ * narrows the element type. That is decided per CALL by
891
+ * `introducesForeignElement`, not per method — an exemption keyed on the method
892
+ * alone would trade two over-declines for a `--fix` that stops the file
893
+ * compiling, which is the defect this walk exists to prevent (Issue #2340).
894
+ */
895
+ const MUTABLE_ARRAY_PARAMETER_METHODS = new Set(['flatMap']);
649
896
  /**
650
897
  * The `Object.values(X)` / `Object.entries(X)` call this value feeds — a fresh
651
898
  * array whose ELEMENTS are the constant's own property values, so freezing the
@@ -670,6 +917,399 @@ const elementProjectionCallOf = (node) => {
670
917
  ? parent
671
918
  : null;
672
919
  };
920
+ /**
921
+ * Constructors that build a collection out of the argument's ELEMENTS.
922
+ *
923
+ * `new Set(ITEMS)` holds the constant's own contents, so iterating it hands out
924
+ * the frozen elements and `for (const item of new Set(ITEMS)) { item.n = 2; }`
925
+ * is TS2540 for an input that compiled (Issue #2340).
926
+ *
927
+ * The construction is not a copy in `copyExpressionOf`'s sense — the result has
928
+ * a different shape from the argument, so a write to the collection says
929
+ * nothing about the constant — which is why it is resolved here, where only the
930
+ * ITERATION question is asked. `WeakSet`/`WeakMap` are absent because they are
931
+ * not iterable, so no binding can be taken from one.
932
+ */
933
+ const ELEMENT_PRESERVING_COLLECTION_NAMES = new Set(['Set', 'Map']);
934
+ /** Whether an expression CONSTRUCTS one of those collections. */
935
+ const isElementPreservingCollection = (node) => {
936
+ const value = unwrapValueWrappers(node);
937
+ if (value.type !== utils_1.AST_NODE_TYPES.NewExpression) {
938
+ return false;
939
+ }
940
+ const callee = unwrapValueWrappers(value.callee);
941
+ return (callee.type === utils_1.AST_NODE_TYPES.Identifier &&
942
+ ELEMENT_PRESERVING_COLLECTION_NAMES.has(callee.name));
943
+ };
944
+ const elementCollectionOf = (node) => {
945
+ const parent = node.parent;
946
+ return parent?.type === utils_1.AST_NODE_TYPES.NewExpression &&
947
+ parent.arguments[0] === node &&
948
+ isElementPreservingCollection(parent)
949
+ ? parent
950
+ : null;
951
+ };
952
+ /**
953
+ * Array methods whose result ITERATES the receiver's own elements.
954
+ *
955
+ * The result is an iterator rather than an array, which is why it belongs to
956
+ * neither of the maps the walk already reads: nothing is copied, so
957
+ * `TYPE_PRESERVING_COPY_METHODS` refuses it, and nothing is handed to a
958
+ * callback, so `ELEMENT_PARAMETER_INDEX_BY_METHOD` refuses it too. Every
959
+ * binding taken from it still carries the constant's element type, so
960
+ * `for (const item of ITEMS.values()) { item.n = 2; }` is TS2540 once `ITEMS`
961
+ * is frozen, for an input that compiled (Issue #2340). `entries` yields
962
+ * `[index, element]` pairs, which carry the element exactly as `values` does.
963
+ *
964
+ * `keys` is absent for an ARRAY receiver, for the reason `Object.keys` is: it
965
+ * yields INDICES, numbers whatever the receiver holds, which the assertion
966
+ * cannot reach.
967
+ */
968
+ const ELEMENT_ITERATOR_METHODS = new Set(['values', 'entries']);
969
+ /**
970
+ * The same methods for a Set/Map receiver, where `keys` joins them.
971
+ *
972
+ * `Set.prototype.keys` is an alias for `values`, and a `Map`'s hands back the
973
+ * frozen key of each entry, so `for (const item of new Set(ITEMS).keys()) {
974
+ * item.n = 2; }` is TS2540 once `ITEMS` is frozen, for an input that compiled
975
+ * — while the `for…of` and `forEach` spellings over the same `new Set(ITEMS)`
976
+ * already decline (Issue #2341). One set per receiver, because a single set
977
+ * would decide the two receivers by the same name and be wrong for one of them.
978
+ *
979
+ * The receiver is read syntactically: the collection this iterator is reached
980
+ * through is the `new Set(ITEMS)` expression the derivation walk just resolved.
981
+ */
982
+ const COLLECTION_ELEMENT_ITERATOR_METHODS = new Set([
983
+ 'values',
984
+ 'entries',
985
+ 'keys',
986
+ ]);
987
+ const iteratorProjectionCallOf = (node) => {
988
+ const parent = node.parent;
989
+ if (parent?.type !== utils_1.AST_NODE_TYPES.MemberExpression ||
990
+ parent.object !== node) {
991
+ return null;
992
+ }
993
+ const method = accessedPropertyName(parent);
994
+ const iteratorMethods = isElementPreservingCollection(node)
995
+ ? COLLECTION_ELEMENT_ITERATOR_METHODS
996
+ : ELEMENT_ITERATOR_METHODS;
997
+ if (method === null || !iteratorMethods.has(method)) {
998
+ return null;
999
+ }
1000
+ // A method REFERENCE (`const walk = ITEMS.values;`) iterates nothing, so the
1001
+ // iterator exists only once the method is called — the same terms
1002
+ // `copyExpressionOf` reads a copy on.
1003
+ const callee = outermostValueOf(parent);
1004
+ return callee.parent?.type === utils_1.AST_NODE_TYPES.CallExpression &&
1005
+ callee.parent.callee === callee
1006
+ ? callee.parent
1007
+ : null;
1008
+ };
1009
+ /**
1010
+ * The expressions a reference denotes a FROZEN value through: the reference
1011
+ * itself and every property access rooted at it — `CONFIG`, `CONFIG.list`,
1012
+ * `CONFIG.list.rows` for `CONFIG.list.rows`.
1013
+ *
1014
+ * `as const` freezes the value in depth, so a property of the constant carries
1015
+ * the assertion exactly as the constant does, and an iteration is routinely
1016
+ * reached through one (`CONFIG.list.values()`, `Array.from(CONFIG.list, fn)`).
1017
+ * A derivation resolver reads a node's immediate parent, so it sees only the
1018
+ * innermost access unless each step of the path is offered to it in turn
1019
+ * (Issue #2340).
1020
+ */
1021
+ const accessPathsRootedAt = (identifier) => {
1022
+ const paths = [];
1023
+ let current = outermostValueOf(identifier);
1024
+ for (;;) {
1025
+ paths.push(current);
1026
+ const parent = current.parent;
1027
+ if (!parent ||
1028
+ parent.type !== utils_1.AST_NODE_TYPES.MemberExpression ||
1029
+ parent.object !== current) {
1030
+ return paths;
1031
+ }
1032
+ current = outermostValueOf(parent);
1033
+ }
1034
+ };
1035
+ /**
1036
+ * Whether `as const` reaches INTO this value, or stops at the property that
1037
+ * holds it.
1038
+ *
1039
+ * The assertion retypes the literal it is written on: nested array and object
1040
+ * literals become `readonly`, and primitive literals narrow to their literal
1041
+ * type. A value the literal merely refers to keeps whatever type it already
1042
+ * had, and an explicit `as T` cast is exactly such a value — so
1043
+ * `{ items: [] as string[] } as const` freezes the `items` PROPERTY while
1044
+ * leaving the array it holds a mutable `string[]`.
1045
+ *
1046
+ * Only the cast is screened, because it is the one spelling that PROVES the
1047
+ * assertion cannot deepen. Anything else answers true, so an unrecognized value
1048
+ * is still enrolled and the walk keeps declining — the direction that withholds
1049
+ * an assertion rather than breaking a build (Issue #2341).
1050
+ */
1051
+ const assertionDeepensInto = (node) => {
1052
+ if (node.type !== utils_1.AST_NODE_TYPES.TSAsExpression &&
1053
+ node.type !== utils_1.AST_NODE_TYPES.TSTypeAssertion) {
1054
+ return true;
1055
+ }
1056
+ return (node.typeAnnotation.type === utils_1.AST_NODE_TYPES.TSTypeReference &&
1057
+ node.typeAnnotation.typeName.type === utils_1.AST_NODE_TYPES.Identifier &&
1058
+ node.typeAnnotation.typeName.name === 'const');
1059
+ };
1060
+ /**
1061
+ * The value a single access step reads out of a literal, or `undefined` when
1062
+ * the step cannot be resolved statically.
1063
+ *
1064
+ * A spread makes an object literal's own properties an incomplete account of
1065
+ * what it holds, so a miss under one is unresolved rather than absent.
1066
+ */
1067
+ const literalValueAtKey = (value, key) => {
1068
+ if (value.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
1069
+ if (typeof key !== 'string') {
1070
+ return undefined;
1071
+ }
1072
+ let resolved;
1073
+ for (const property of value.properties) {
1074
+ if (property.type === utils_1.AST_NODE_TYPES.SpreadElement) {
1075
+ return undefined;
1076
+ }
1077
+ const propertyKey = property.key;
1078
+ const keyName = !property.computed && propertyKey.type === utils_1.AST_NODE_TYPES.Identifier
1079
+ ? propertyKey.name
1080
+ : propertyKey.type === utils_1.AST_NODE_TYPES.Literal &&
1081
+ typeof propertyKey.value === 'string'
1082
+ ? propertyKey.value
1083
+ : null;
1084
+ // The LAST matching key wins, as it does at runtime.
1085
+ if (keyName === key) {
1086
+ resolved = property.value;
1087
+ }
1088
+ }
1089
+ return resolved;
1090
+ }
1091
+ if (value.type === utils_1.AST_NODE_TYPES.ArrayExpression) {
1092
+ if (typeof key !== 'number') {
1093
+ return undefined;
1094
+ }
1095
+ const element = value.elements[key];
1096
+ return element === null ||
1097
+ element === undefined ||
1098
+ element.type === utils_1.AST_NODE_TYPES.SpreadElement
1099
+ ? undefined
1100
+ : element;
1101
+ }
1102
+ return undefined;
1103
+ };
1104
+ /**
1105
+ * The value a single MEMBER ACCESS step reads out of a literal, or `undefined`
1106
+ * when the step cannot be resolved statically.
1107
+ *
1108
+ * Delegates to `literalValueAtKey` so a property reached by a member access and
1109
+ * the same property reached by a destructuring pattern cannot be read on
1110
+ * different terms — the divergence between those two spellings is what this
1111
+ * screen exists to close (Issue #2341).
1112
+ */
1113
+ const literalValueAtStep = (value, step) => {
1114
+ const name = accessedPropertyName(step);
1115
+ if (name !== null) {
1116
+ return literalValueAtKey(value, name);
1117
+ }
1118
+ return step.computed &&
1119
+ step.property.type === utils_1.AST_NODE_TYPES.Literal &&
1120
+ typeof step.property.value === 'number'
1121
+ ? literalValueAtKey(value, step.property.value)
1122
+ : undefined;
1123
+ };
1124
+ /**
1125
+ * The names a destructuring pattern binds to values `as const` does NOT reach.
1126
+ *
1127
+ * A pattern binds a property WITHOUT writing a member access, so the step
1128
+ * screen in `frozenAccessPathsRootedAt` never sees `const { items } = CONFIG`
1129
+ * — it only sees `CONFIG.items`. Reading the pattern against the same literal
1130
+ * keeps the two spellings of one extraction on identical terms. They must
1131
+ * agree: `prefer-destructuring-no-class` rewrites the first into the second
1132
+ * under `--fix`, so a screen applied to one spelling alone lets a sibling fixer
1133
+ * flip this rule's verdict on unchanged semantics (Issue #2341).
1134
+ *
1135
+ * A `RestElement` is left enrolled because it gathers whatever the pattern did
1136
+ * not name, which no single literal value answers for.
1137
+ */
1138
+ const collectUnfrozenPatternNames = (id, value, unfrozen) => {
1139
+ if (value === undefined) {
1140
+ return;
1141
+ }
1142
+ if (id.type === utils_1.AST_NODE_TYPES.AssignmentPattern) {
1143
+ collectUnfrozenPatternNames(id.left, value, unfrozen);
1144
+ return;
1145
+ }
1146
+ if (id.type === utils_1.AST_NODE_TYPES.Identifier) {
1147
+ if (!assertionDeepensInto(value)) {
1148
+ unfrozen.add(id.name);
1149
+ }
1150
+ return;
1151
+ }
1152
+ const literal = unwrapValueWrappers(value);
1153
+ if (id.type === utils_1.AST_NODE_TYPES.ObjectPattern) {
1154
+ for (const property of id.properties) {
1155
+ if (property.type !== utils_1.AST_NODE_TYPES.Property) {
1156
+ continue;
1157
+ }
1158
+ const key = property.key;
1159
+ const name = !property.computed && key.type === utils_1.AST_NODE_TYPES.Identifier
1160
+ ? key.name
1161
+ : key.type === utils_1.AST_NODE_TYPES.Literal && typeof key.value === 'string'
1162
+ ? key.value
1163
+ : null;
1164
+ if (name === null) {
1165
+ continue;
1166
+ }
1167
+ collectUnfrozenPatternNames(property.value, literalValueAtKey(literal, name), unfrozen);
1168
+ }
1169
+ return;
1170
+ }
1171
+ if (id.type === utils_1.AST_NODE_TYPES.ArrayPattern) {
1172
+ id.elements.forEach((element, index) => {
1173
+ if (!element || element.type === utils_1.AST_NODE_TYPES.RestElement) {
1174
+ return;
1175
+ }
1176
+ collectUnfrozenPatternNames(element, literalValueAtKey(literal, index), unfrozen);
1177
+ });
1178
+ }
1179
+ };
1180
+ /**
1181
+ * The access paths rooted at a reference that the assertion actually FREEZES,
1182
+ * read against the literal the constant is declared from.
1183
+ *
1184
+ * The climb stops before the first step whose value `as const` cannot deepen
1185
+ * into, because neither that value nor anything reached through it carries the
1186
+ * assertion — a binding taken from it is no second name for frozen contents and
1187
+ * enrolling it would withhold the assertion for a break that cannot happen.
1188
+ *
1189
+ * Without a literal to read, every step answers unresolved and the result is
1190
+ * the whole path, which is what `accessPathsRootedAt` returns on its own.
1191
+ */
1192
+ const frozenAccessPathsRootedAt = (identifier, frozenValue) => {
1193
+ const paths = [];
1194
+ let current = outermostValueOf(identifier);
1195
+ let value = frozenValue
1196
+ ? unwrapValueWrappers(frozenValue)
1197
+ : undefined;
1198
+ for (;;) {
1199
+ paths.push({ node: current, literal: value });
1200
+ const parent = current.parent;
1201
+ if (!parent ||
1202
+ parent.type !== utils_1.AST_NODE_TYPES.MemberExpression ||
1203
+ parent.object !== current) {
1204
+ return paths;
1205
+ }
1206
+ if (value !== undefined) {
1207
+ const stepValue = literalValueAtStep(value, parent);
1208
+ if (stepValue === undefined) {
1209
+ value = undefined;
1210
+ }
1211
+ else if (!assertionDeepensInto(stepValue)) {
1212
+ return paths;
1213
+ }
1214
+ else {
1215
+ value = unwrapValueWrappers(stepValue);
1216
+ }
1217
+ }
1218
+ current = outermostValueOf(parent);
1219
+ }
1220
+ };
1221
+ /**
1222
+ * Every way a value derived from this one keeps the constant's ELEMENT types:
1223
+ * a copy of it, the `Object.values`/`Object.entries` array over it, the
1224
+ * iterator its own `values`/`entries` hands back, and the collection built out
1225
+ * of it. Each resolver returns an ANCESTOR of the node it is given, which is
1226
+ * what lets the iteration walk follow them transitively without looping.
1227
+ */
1228
+ const DERIVATION_RESOLVERS = [
1229
+ copyExpressionOf,
1230
+ elementProjectionCallOf,
1231
+ iteratorProjectionCallOf,
1232
+ elementCollectionOf,
1233
+ ];
1234
+ /**
1235
+ * The derivations that carry the constant's frozen type into a value a BINDING
1236
+ * can be initialized from: a copy of it and an element taken out of it.
1237
+ *
1238
+ * The three iteration resolvers are absent because each hands back a value of a
1239
+ * DIFFERENT SHAPE whose container is fresh and mutable — `Object.values(CONFIG)`
1240
+ * and `new Set(ITEMS)` are arrays and sets nothing frozen was written to, so a
1241
+ * write to the container itself is no readonly violation. Only their ELEMENTS
1242
+ * carry the assertion, which is the question the iteration walk asks.
1243
+ */
1244
+ const ALIAS_DERIVATION_RESOLVERS = [copyExpressionOf, elementExpressionOf];
1245
+ /**
1246
+ * Every expression that denotes a value typed from this reference: the
1247
+ * reference and each step of the access path rooted at it, then — transitively
1248
+ * — whatever `resolvers` derive from any of them.
1249
+ *
1250
+ * Grown in place and walked by index, so a derivation OF a derivation is
1251
+ * reached by the same loop without recursion of its own. Every resolver returns
1252
+ * an ANCESTOR of the node it is given, so the walk strictly ascends and
1253
+ * terminates; `visited` keeps a node two resolvers agree on from being expanded
1254
+ * twice.
1255
+ */
1256
+ const derivedValueExpressionsOf = (identifier, resolvers, frozenValue) => {
1257
+ const pending = frozenAccessPathsRootedAt(identifier, frozenValue);
1258
+ const visited = new Set(pending.map(({ node }) => node));
1259
+ for (let index = 0; index < pending.length; index += 1) {
1260
+ for (const resolveDerivation of resolvers) {
1261
+ const derived = resolveDerivation(pending[index].node);
1262
+ if (!derived || visited.has(derived)) {
1263
+ continue;
1264
+ }
1265
+ visited.add(derived);
1266
+ // A copy or an element call hands back a value with no literal of its
1267
+ // own, so nothing downstream of one is screened — the direction that
1268
+ // keeps enrolling rather than withholding the assertion.
1269
+ pending.push({ node: derived, literal: undefined });
1270
+ }
1271
+ }
1272
+ return pending;
1273
+ };
1274
+ const enrolFully = (variables) => variables.map((variable) => ({
1275
+ variable,
1276
+ breaksOnAnyMutatingMethod: true,
1277
+ }));
1278
+ /**
1279
+ * The bindings a callback's parameters at `positions` introduce, each carrying
1280
+ * the question its position can break on.
1281
+ *
1282
+ * A callback routinely declares fewer parameters than the caller passes, so a
1283
+ * position is taken only where the signature actually spells it.
1284
+ *
1285
+ * The scope manager answers for the WHOLE function — every parameter, and a
1286
+ * function expression's own name — so the enrolled parameters' bindings are
1287
+ * picked out by the spans they are declared in. Taking the function's list
1288
+ * whole would enrol the accumulator of a `reduce`, typed from the seed value
1289
+ * rather than from the constant, and the index, which the assertion cannot
1290
+ * reach.
1291
+ */
1292
+ const parameterBindingsOf = (callback, positions, declaredVariablesOf) => {
1293
+ const enrolled = positions.flatMap(({ index, breaksOnAnyMutatingMethod }) => {
1294
+ const param = callback.params[index];
1295
+ return param ? [{ param, breaksOnAnyMutatingMethod }] : [];
1296
+ });
1297
+ if (enrolled.length === 0) {
1298
+ return [];
1299
+ }
1300
+ return declaredVariablesOf(callback).flatMap((variable) => {
1301
+ const position = enrolled.find(({ param }) => variable.defs.some((def) => def.name.range[0] >= param.range[0] &&
1302
+ def.name.range[1] <= param.range[1]));
1303
+ return position
1304
+ ? [
1305
+ {
1306
+ variable,
1307
+ breaksOnAnyMutatingMethod: position.breaksOnAnyMutatingMethod,
1308
+ },
1309
+ ]
1310
+ : [];
1311
+ });
1312
+ };
673
1313
  /**
674
1314
  * The bindings a construct that ITERATES `iterable` introduces: the head of a
675
1315
  * `for…of` over it, the parameter an array method hands each element to, and —
@@ -696,9 +1336,9 @@ const elementProjectionCallOf = (node) => {
696
1336
  */
697
1337
  const bindingsOfIterationOver = (iterable, declaredVariablesOf, iteratesConstantValue) => {
698
1338
  // 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.
1339
+ // routinely a PROPERTY of the constant (`for (const x of CONFIG.list)`): the
1340
+ // property is frozen with the object that holds it, so iterating it hands the
1341
+ // body the constant's own frozen contents.
702
1342
  const path = accessPathOf(iterable);
703
1343
  const value = outermostValueOf(path ?? iterable);
704
1344
  const parent = value.parent;
@@ -708,7 +1348,23 @@ const bindingsOfIterationOver = (iterable, declaredVariablesOf, iteratesConstant
708
1348
  if (parent.type === utils_1.AST_NODE_TYPES.ForOfStatement &&
709
1349
  parent.right === value &&
710
1350
  parent.left.type === utils_1.AST_NODE_TYPES.VariableDeclaration) {
711
- return declaredVariablesOf(parent.left);
1351
+ return enrolFully(declaredVariablesOf(parent.left));
1352
+ }
1353
+ // `Array.from(X, mapfn)` hands each element of `X` to `mapfn` exactly as
1354
+ // `X.map` hands it to a callback, so the mapper's first parameter is typed
1355
+ // from the constant and `Array.from(ITEMS, (item) => { item.n = 2; … })` is
1356
+ // TS2540 once `ITEMS` is frozen. The two-argument form reaches the walk
1357
+ // nowhere else: `isCopyingCall` admits `Array.from` at one argument alone,
1358
+ // because a mapper retypes the RESULT — which says nothing about the element
1359
+ // it is handed (Issue #2340). The mapper takes the element first and the
1360
+ // index second, and is handed no receiver array at all.
1361
+ if (parent.type === utils_1.AST_NODE_TYPES.CallExpression &&
1362
+ parent.arguments[0] === value &&
1363
+ isNamespacedCallee(parent.callee, 'Array', 'from')) {
1364
+ const mapper = parent.arguments[1];
1365
+ return mapper && isFunctionValue(mapper)
1366
+ ? parameterBindingsOf(mapper, [{ index: 0, breaksOnAnyMutatingMethod: true }], declaredVariablesOf)
1367
+ : [];
712
1368
  }
713
1369
  if (path === null) {
714
1370
  return [];
@@ -730,23 +1386,17 @@ const bindingsOfIterationOver = (iterable, declaredVariablesOf, iteratesConstant
730
1386
  const arrayIndex = iteratesConstantValue
731
1387
  ? ARRAY_PARAMETER_INDEX_BY_METHOD.get(method)
732
1388
  : 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])));
1389
+ return parameterBindingsOf(callback, [
1390
+ { index: elementIndex, breaksOnAnyMutatingMethod: true },
1391
+ ...(arrayIndex === undefined
1392
+ ? []
1393
+ : [
1394
+ {
1395
+ index: arrayIndex,
1396
+ breaksOnAnyMutatingMethod: !MUTABLE_ARRAY_PARAMETER_METHODS.has(method),
1397
+ },
1398
+ ]),
1399
+ ], declaredVariablesOf);
750
1400
  };
751
1401
  /**
752
1402
  * The bindings ITERATING this reference introduces, directly or through a value
@@ -763,15 +1413,25 @@ const bindingsOfIterationOver = (iterable, declaredVariablesOf, iteratesConstant
763
1413
  * only READS its element fixable.
764
1414
  *
765
1415
  * 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.
1416
+ * routinely removed from the constant (`[...ITEMS].forEach(…)`,
1417
+ * `ITEMS.filter(Boolean).forEach(…)`, `Object.values(CONFIG).forEach(…)`,
1418
+ * `ITEMS.values()`, `new Set(ITEMS)`): each builds a fresh OUTER value whose
1419
+ * elements are still the frozen ones, so the element binding breaks
1420
+ * identically.
1421
+ *
1422
+ * Following them is TRANSITIVE, on the same reasoning as the alias walk — every
1423
+ * hop keeps the element type, so a chain of them keeps it too, and
1424
+ * `ITEMS.filter(Boolean).slice().forEach((item) => { item.n = 2; })` is the
1425
+ * same TS2540 as the one-hop spelling that already declines. A single step
1426
+ * gave two spellings of one construct opposite verdicts (Issue #2340).
1427
+ *
1428
+ * The derivations are resolved from each step of the reference's own access
1429
+ * path as well as from the reference, since the value a derivation is taken
1430
+ * from is routinely a PROPERTY of the constant — see `accessPathsRootedAt`.
771
1431
  *
772
1432
  * 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
1433
+ * member path ALONE, the one iterable that hands the callback the constant
1434
+ * itself. A derivation hands it the fresh outer value it
775
1435
  * built, and mutating that is no readonly violation:
776
1436
  * `[...ITEMS].forEach((item, index, arr) => { arr.push(3); })` does break after
777
1437
  * the fix, but as TS2345 — the spread narrows the element type, so `3` is not
@@ -781,13 +1441,21 @@ const bindingsOfIterationOver = (iterable, declaredVariablesOf, iteratesConstant
781
1441
  */
782
1442
  const iterationBindingsOf = (identifier, declaredVariablesOf) => {
783
1443
  const value = outermostValueOf(identifier);
784
- const derivations = [copyExpressionOf(value), elementProjectionCallOf(value)];
785
- return [
1444
+ const bindings = [
786
1445
  ...bindingsOfIterationOver(value, declaredVariablesOf, true),
787
- ...derivations.flatMap((iterable) => iterable
788
- ? bindingsOfIterationOver(iterable, declaredVariablesOf, false)
789
- : []),
790
1446
  ];
1447
+ // The constant's own value and access path are iterated by the call above,
1448
+ // which resolves the path itself; every DERIVED value is iterated on the
1449
+ // narrower terms — it hands a callback a fresh outer value rather than the
1450
+ // constant.
1451
+ const ownValues = new Set(accessPathsRootedAt(value));
1452
+ for (const { node: derived } of derivedValueExpressionsOf(value, DERIVATION_RESOLVERS)) {
1453
+ if (ownValues.has(derived)) {
1454
+ continue;
1455
+ }
1456
+ bindings.push(...bindingsOfIterationOver(derived, declaredVariablesOf, false));
1457
+ }
1458
+ return bindings;
791
1459
  };
792
1460
  /**
793
1461
  * Whether anything in the file stops this binding taking `as const`, under its
@@ -840,12 +1508,49 @@ const iterationBindingsOf = (identifier, declaredVariablesOf) => {
840
1508
  * `--fix`.
841
1509
  */
842
1510
  const blocksAsConstAssertion = (variable, declaredVariablesOf) => {
1511
+ // The literal the constant is declared from, so an access path through it can
1512
+ // be screened against what `as const` actually freezes. Only the constant's
1513
+ // own declarator carries one: an ALIAS is initialized from a path into that
1514
+ // same literal, and resolving through it would need the path carried too, so
1515
+ // an alias is left unresolved and every step of it stays enrolled.
1516
+ const declaredValue = variable.defs.find((def) => def.node.type === utils_1.AST_NODE_TYPES.VariableDeclarator)?.node;
1517
+ const frozenValue = declaredValue?.type === utils_1.AST_NODE_TYPES.VariableDeclarator &&
1518
+ declaredValue.init
1519
+ ? declaredValue.init
1520
+ : undefined;
843
1521
  // Grown in place and walked by index: an alias found mid-walk is appended and
844
1522
  // reached by the same loop, so the traversal needs no recursion of its own.
845
- const pending = [variable];
846
- const visited = new Set(pending);
1523
+ const pending = [
1524
+ { variable, breaksOnAnyMutatingMethod: true, frozenValue },
1525
+ ];
1526
+ const visited = new Set([variable]);
1527
+ /**
1528
+ * Whether a node is a REFERENCE to a binding already enrolled for this
1529
+ * constant, and so holds a value typed from the constant itself.
1530
+ *
1531
+ * Answered from the scope manager's reference lists rather than by name, on
1532
+ * the same terms as the rest of the walk: a same-named binding from another
1533
+ * scope names another value and must not exempt anything.
1534
+ *
1535
+ * A binding enrolled LATER in the walk than the one being examined answers
1536
+ * false here. That can only withhold an exemption, never grant one wrongly,
1537
+ * so the walk order costs a report at worst.
1538
+ */
1539
+ const isEnrolledReference = (node) => {
1540
+ const value = unwrapValueWrappers(node);
1541
+ if (value.type !== utils_1.AST_NODE_TYPES.Identifier) {
1542
+ return false;
1543
+ }
1544
+ for (const enrolledVariable of visited) {
1545
+ if (enrolledVariable.references.some((enrolledReference) => enrolledReference.identifier === value)) {
1546
+ return true;
1547
+ }
1548
+ }
1549
+ return false;
1550
+ };
847
1551
  for (let index = 0; index < pending.length; index += 1) {
848
- for (const reference of pending[index].references) {
1552
+ const { variable: enrolled, breaksOnAnyMutatingMethod, frozenValue: enrolledValue, } = pending[index];
1553
+ for (const reference of enrolled.references) {
849
1554
  // Reassigning an alias is as disqualifying as writing through one. A
850
1555
  // binding that takes its type from the constant narrows to the frozen
851
1556
  // literal, so `let stage = DEFAULT; stage = 'live';` becomes TS2322 for
@@ -858,30 +1563,64 @@ const blocksAsConstAssertion = (variable, declaredVariablesOf) => {
858
1563
  return true;
859
1564
  }
860
1565
  const path = accessPathOf(reference.identifier);
1566
+ // The ELEMENT question is asked of every binding alike: the elements are
1567
+ // frozen whatever the container's own declaration says.
1568
+ if (path !== null && isWriteTarget(path)) {
1569
+ return true;
1570
+ }
1571
+ // The mutating-method question is asked in full of every binding the
1572
+ // assertion types `readonly`, where any such call is a TS2339. A
1573
+ // parameter the lib declares MUTABLE has no such break to have, so it is
1574
+ // asked the narrower question that remains: does this call introduce a
1575
+ // value the narrowed element type would have to accept (Issue #2340).
861
1576
  if (path !== null &&
862
- (isMutatingMethodCall(path) || isWriteTarget(path))) {
1577
+ isMutatingMethodCall(path) &&
1578
+ (breaksOnAnyMutatingMethod ||
1579
+ introducesForeignElement(path, isEnrolledReference))) {
863
1580
  return true;
864
1581
  }
865
- // A copy carries the constant's frozen type into a second binding, so it
866
- // is enrolled on the same terms as an alias — but it is reached through a
867
- // member access (`ITEMS.concat()`), which the alias walk deliberately
868
- // refuses, so it is resolved before that refusal applies.
869
- const copy = copyExpressionOf(outermostValueOf(reference.identifier));
870
- const declarator = copy
871
- ? aliasDeclaratorOf(copy)
872
- : path === null
873
- ? aliasDeclaratorOf(reference.identifier)
874
- : null;
1582
+ // A binding is initialized from any expression that denotes the
1583
+ // constant's frozen contents, not from the reference alone. A PROPERTY or
1584
+ // ELEMENT of the constant carries the assertion exactly as the constant
1585
+ // does, so `const list = CONFIG.list; list.push(3);` is the same TS2339
1586
+ // the rule already declines for when the identical call is written
1587
+ // directly — only the extracted spelling escaped it, while the
1588
+ // DESTRUCTURED spelling of the same extraction was enrolled all along
1589
+ // (Issue #2341). A copy taken of the constant or of any step of that path
1590
+ // (`ITEMS.concat()`, `[...CONFIG.a.b]`) carries the frozen TYPE into a
1591
+ // fresh value on the same terms.
1592
+ const aliases = derivedValueExpressionsOf(reference.identifier, ALIAS_DERIVATION_RESOLVERS, enrolledValue).flatMap(({ node, literal }) => {
1593
+ const declarator = aliasDeclaratorOf(node);
1594
+ if (!declarator) {
1595
+ return [];
1596
+ }
1597
+ const variables = declaredVariablesOf(declarator);
1598
+ // The pattern screen applies only where the declarator destructures
1599
+ // THIS value directly. Reached through a storage container
1600
+ // (`const HOLDER = { ITEMS }`), the pattern names the container's own
1601
+ // properties, which this literal does not answer for.
1602
+ if (literal === undefined ||
1603
+ declarator.init !== outermostValueOf(node)) {
1604
+ return variables;
1605
+ }
1606
+ const unfrozen = new Set();
1607
+ collectUnfrozenPatternNames(declarator.id, literal, unfrozen);
1608
+ return unfrozen.size === 0
1609
+ ? variables
1610
+ : variables.filter((variable) => !unfrozen.has(variable.name));
1611
+ });
875
1612
  // A binding introduced by ITERATING the constant is enrolled beside the
876
1613
  // aliases: it names the constant's CONTENTS, which the assertion freezes
877
- // with the constant itself — see `iterationBindingsOf`.
1614
+ // with the constant itself — see `iterationBindingsOf`. An alias is
1615
+ // enrolled on the constant's own terms, because it denotes the constant's
1616
+ // value and so carries its readonly-ness whole.
878
1617
  const derived = [
879
- ...(declarator ? declaredVariablesOf(declarator) : []),
1618
+ ...enrolFully(aliases),
880
1619
  ...iterationBindingsOf(reference.identifier, declaredVariablesOf),
881
1620
  ];
882
1621
  for (const alias of derived) {
883
- if (!visited.has(alias)) {
884
- visited.add(alias);
1622
+ if (!visited.has(alias.variable)) {
1623
+ visited.add(alias.variable);
885
1624
  pending.push(alias);
886
1625
  }
887
1626
  }