@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.
|
|
405
|
-
*
|
|
406
|
-
*
|
|
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
|
-
*
|
|
425
|
-
*
|
|
426
|
-
*
|
|
427
|
-
*
|
|
428
|
-
*
|
|
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
|
|
480
|
-
|
|
481
|
-
callee.parent
|
|
482
|
-
|
|
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
|
|
629
|
-
* `T[]` where every sibling declares it `readonly T[]` — measured
|
|
630
|
-
* `lib.es2020
|
|
631
|
-
*
|
|
632
|
-
*
|
|
633
|
-
*
|
|
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
|
-
//
|
|
701
|
-
//
|
|
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
|
-
|
|
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])));
|
|
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
|
|
767
|
-
* `ITEMS.filter(Boolean).forEach(…)`, `Object.values(CONFIG).forEach(…)
|
|
768
|
-
* builds a fresh OUTER value whose
|
|
769
|
-
*
|
|
770
|
-
*
|
|
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,
|
|
774
|
-
*
|
|
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
|
|
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 = [
|
|
846
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1577
|
+
isMutatingMethodCall(path) &&
|
|
1578
|
+
(breaksOnAnyMutatingMethod ||
|
|
1579
|
+
introducesForeignElement(path, isEnrolledReference))) {
|
|
863
1580
|
return true;
|
|
864
1581
|
}
|
|
865
|
-
// A
|
|
866
|
-
//
|
|
867
|
-
//
|
|
868
|
-
//
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
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
|
-
...(
|
|
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
|
}
|