@blumintinc/eslint-plugin-blumint 1.21.14 → 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.
package/lib/index.js
CHANGED
|
@@ -1404,6 +1404,68 @@ exports.enforceEmptyObjectCheck = (0, createRule_1.createRule)({
|
|
|
1404
1404
|
}
|
|
1405
1405
|
return false;
|
|
1406
1406
|
}
|
|
1407
|
+
/**
|
|
1408
|
+
* The type the binding behind this value DECLARES, if it declares one.
|
|
1409
|
+
*
|
|
1410
|
+
* Parameters and variables are the two declarations that annotate the
|
|
1411
|
+
* value a guard tests; an import is answered ahead of this by
|
|
1412
|
+
* `tracesToImport`. The annotation has to sit on the BINDING: the one on
|
|
1413
|
+
* `const { config }: Props = load()` describes the container, and reading
|
|
1414
|
+
* it as a verdict on a single property would need exactly the resolution
|
|
1415
|
+
* that failed, so a destructured binding keeps the naming heuristic.
|
|
1416
|
+
*/
|
|
1417
|
+
function declaredTypeOf(identifier) {
|
|
1418
|
+
const variable = variableFor(identifier);
|
|
1419
|
+
if (!variable) {
|
|
1420
|
+
return null;
|
|
1421
|
+
}
|
|
1422
|
+
for (const def of variable.defs) {
|
|
1423
|
+
if (def.type !== 'Parameter' && def.type !== 'Variable') {
|
|
1424
|
+
continue;
|
|
1425
|
+
}
|
|
1426
|
+
if (def.name.typeAnnotation) {
|
|
1427
|
+
return def.name.typeAnnotation.typeAnnotation;
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
return null;
|
|
1431
|
+
}
|
|
1432
|
+
/**
|
|
1433
|
+
* Whether a declared type declares NOTHING.
|
|
1434
|
+
*
|
|
1435
|
+
* An explicit `any` is the checker reporting what the source told it, not
|
|
1436
|
+
* a resolution failure, so it leaves the value in the same position as an
|
|
1437
|
+
* unannotated one and the naming heuristic keeps answering. `unknown` is
|
|
1438
|
+
* excluded from this reading on purpose: `Object.keys` rejects an
|
|
1439
|
+
* `unknown` operand, so the fix the heuristic would attach there does not
|
|
1440
|
+
* typecheck.
|
|
1441
|
+
*/
|
|
1442
|
+
function declaresNothing(node) {
|
|
1443
|
+
if (node.type === utils_1.AST_NODE_TYPES.TSUnionType) {
|
|
1444
|
+
return node.types.some(declaresNothing);
|
|
1445
|
+
}
|
|
1446
|
+
return node.type === utils_1.AST_NODE_TYPES.TSAnyKeyword;
|
|
1447
|
+
}
|
|
1448
|
+
/**
|
|
1449
|
+
* Whether a declared type the checker could not resolve nonetheless
|
|
1450
|
+
* SPELLS an index-signature dictionary.
|
|
1451
|
+
*
|
|
1452
|
+
* `Record<K, V>` is an index signature whichever way `K` and `V` resolve,
|
|
1453
|
+
* so the value is a plain data map rather than a class instance and
|
|
1454
|
+
* `Object.keys` measures its emptiness correctly. Reading that from the
|
|
1455
|
+
* source keeps the verdict a complete program gives — `object` — reachable
|
|
1456
|
+
* without one, which is why a union answers yes as soon as one member
|
|
1457
|
+
* spells it, mirroring `isObjectLikeType`. A reference the source does not
|
|
1458
|
+
* pin this way, `Readonly<NextResponse>` among them, carries no such
|
|
1459
|
+
* guarantee.
|
|
1460
|
+
*/
|
|
1461
|
+
function spellsDictionary(node) {
|
|
1462
|
+
if (node.type === utils_1.AST_NODE_TYPES.TSUnionType) {
|
|
1463
|
+
return node.types.some(spellsDictionary);
|
|
1464
|
+
}
|
|
1465
|
+
return (node.type === utils_1.AST_NODE_TYPES.TSTypeReference &&
|
|
1466
|
+
node.typeName.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
1467
|
+
node.typeName.name === 'Record');
|
|
1468
|
+
}
|
|
1407
1469
|
function isLikelyObject(identifier) {
|
|
1408
1470
|
if (checker && parserServices?.esTreeNodeToTSNodeMap) {
|
|
1409
1471
|
try {
|
|
@@ -1424,13 +1486,39 @@ exports.enforceEmptyObjectCheck = (0, createRule_1.createRule)({
|
|
|
1424
1486
|
* absence of evidence became evidence the value can be `{}`.
|
|
1425
1487
|
*
|
|
1426
1488
|
* The heuristic is still the right answer for a value with no
|
|
1427
|
-
* declaration to resolve — removing it outright silences
|
|
1428
|
-
* rule's own
|
|
1429
|
-
*
|
|
1489
|
+
* declaration to resolve — removing it outright silences 56 of this
|
|
1490
|
+
* rule's own 108 fixtures — so the fall-through is carved out only
|
|
1491
|
+
* where evidence EXISTS and went unread: a value from an unresolved
|
|
1492
|
+
* IMPORT (#2252), and a binding that DECLARES a type the checker
|
|
1493
|
+
* could not resolve (#2344). An unresolvable annotation means the
|
|
1494
|
+
* checker could not look, not that the value is loosely typed:
|
|
1495
|
+
* `response: Readonly<NextResponse>` read off its name alone
|
|
1496
|
+
* reported a class instance, whose state lives behind prototype
|
|
1497
|
+
* accessors, so the prescribed `Object.keys` clause holds for every
|
|
1498
|
+
* valid value and inverts the guard it was meant to harden.
|
|
1499
|
+
*
|
|
1500
|
+
* The carve-out costs reach, and the cost is measured rather than
|
|
1501
|
+
* assumed: across the consumer's 14,059 tracked sources it drops 8
|
|
1502
|
+
* reports, and re-running each under a real `ts.Program` — the
|
|
1503
|
+
* verdict this arm exists to preserve — agrees with 6 of them. The
|
|
1504
|
+
* remaining 2 annotate an all-optional imported type
|
|
1505
|
+
* (`Record<string, string[]>` behind a local alias, and an
|
|
1506
|
+
* `algoliasearch-helper` parameter bag), which a resolved program
|
|
1507
|
+
* calls `object` and reports. Nothing at the annotation site
|
|
1508
|
+
* separates those from `Readonly<NextResponse>`: both are bare
|
|
1509
|
+
* references into a module this program did not load, so recovering
|
|
1510
|
+
* them needs the cross-file resolution whose absence defines this
|
|
1511
|
+
* branch. Two silent true positives is the accepted price of six
|
|
1512
|
+
* guard inversions, because a fix that rewrites correct code costs
|
|
1513
|
+
* more than one the rule declines to make.
|
|
1430
1514
|
*/
|
|
1431
1515
|
if (tracesToImport(identifier)) {
|
|
1432
1516
|
return false;
|
|
1433
1517
|
}
|
|
1518
|
+
const declared = declaredTypeOf(identifier);
|
|
1519
|
+
if (declared && !declaresNothing(declared)) {
|
|
1520
|
+
return spellsDictionary(declared);
|
|
1521
|
+
}
|
|
1434
1522
|
}
|
|
1435
1523
|
catch {
|
|
1436
1524
|
// TypeScript parser services can throw when AST-to-TS node mapping fails; fall back to naming heuristic so linting does not crash.
|
|
@@ -423,6 +423,168 @@ function transactionParticipantsOf(body) {
|
|
|
423
423
|
}
|
|
424
424
|
return participants;
|
|
425
425
|
}
|
|
426
|
+
/**
|
|
427
|
+
* Node types that own the statements written inside them. A walk bounded by
|
|
428
|
+
* them answers about what the method DOES rather than about what it merely
|
|
429
|
+
* defines: a `try`/`finally` written inside a callback is that callback's
|
|
430
|
+
* acquire/release pair, and a field written inside one is written on whatever
|
|
431
|
+
* schedule the callback runs on. The decorator caches the method's own promise,
|
|
432
|
+
* so only the method's own steps bear on whether caching is safe.
|
|
433
|
+
*/
|
|
434
|
+
const OWN_STEP_BOUNDARIES = new Set([
|
|
435
|
+
utils_1.AST_NODE_TYPES.ArrowFunctionExpression,
|
|
436
|
+
utils_1.AST_NODE_TYPES.FunctionDeclaration,
|
|
437
|
+
utils_1.AST_NODE_TYPES.FunctionExpression,
|
|
438
|
+
]);
|
|
439
|
+
/**
|
|
440
|
+
* Every node the enclosing function executes as its own step, `root` included.
|
|
441
|
+
* Descent stops at a nested function, which `subtreeOf` enters.
|
|
442
|
+
*/
|
|
443
|
+
function* ownSubtreeOf(root) {
|
|
444
|
+
yield root;
|
|
445
|
+
for (const [key, value] of Object.entries(root)) {
|
|
446
|
+
if (NON_TRAVERSABLE_KEYS.has(key)) {
|
|
447
|
+
continue;
|
|
448
|
+
}
|
|
449
|
+
const children = Array.isArray(value) ? value : [value];
|
|
450
|
+
for (const child of children) {
|
|
451
|
+
if (ASTHelpers_1.ASTHelpers.isNode(child) && !OWN_STEP_BOUNDARIES.has(child.type)) {
|
|
452
|
+
yield* ownSubtreeOf(child);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
/**
|
|
458
|
+
* Whether the block performs a call as a step of the function that owns it.
|
|
459
|
+
* A call written inside a function the block only DEFINES — `finally { const
|
|
460
|
+
* undo = () => release(t); }` — performs nothing, so the bounded walk is what
|
|
461
|
+
* separates a release from a closure that could perform one elsewhere.
|
|
462
|
+
*/
|
|
463
|
+
function performsCall(block) {
|
|
464
|
+
for (const node of ownSubtreeOf(block)) {
|
|
465
|
+
if (node.type === utils_1.AST_NODE_TYPES.CallExpression) {
|
|
466
|
+
return true;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
return false;
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* Whether the method owns an acquire/release pair: a `try` among its own steps
|
|
473
|
+
* whose `finalizer` calls something.
|
|
474
|
+
*
|
|
475
|
+
* A `finally` that calls something exists to undo an effect the `try`
|
|
476
|
+
* performed. What such a method hands back describes one attempt — a grant held
|
|
477
|
+
* while a queue ticket was outstanding, a read taken while a lock was held —
|
|
478
|
+
* rather than a fact that stays true once the release has run.
|
|
479
|
+
*
|
|
480
|
+
* An empty `finalizer` releases nothing, and one that only writes a local
|
|
481
|
+
* (`finally { done = true; }`) records that the attempt finished instead of
|
|
482
|
+
* undoing it, so neither costs the method its report.
|
|
483
|
+
*/
|
|
484
|
+
function releasesInOwnFinalizer(fn) {
|
|
485
|
+
for (const node of ownSubtreeOf(fn.body)) {
|
|
486
|
+
if (node.type === utils_1.AST_NODE_TYPES.TryStatement &&
|
|
487
|
+
node.finalizer &&
|
|
488
|
+
performsCall(node.finalizer)) {
|
|
489
|
+
return true;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
return false;
|
|
493
|
+
}
|
|
494
|
+
/**
|
|
495
|
+
* The expression under the wrappers that stand between a value and the access
|
|
496
|
+
* path it evaluates. `await this.pending`, `this.pending!`, `this.pending as
|
|
497
|
+
* Ready` and `this?.pending` all read the same field, and a discriminator blind
|
|
498
|
+
* to one of those spellings would answer "hands back something else" about a
|
|
499
|
+
* method that hands back exactly what it wrote.
|
|
500
|
+
*/
|
|
501
|
+
function withoutValueWrappers(node) {
|
|
502
|
+
switch (node.type) {
|
|
503
|
+
case utils_1.AST_NODE_TYPES.AwaitExpression:
|
|
504
|
+
return withoutValueWrappers(node.argument);
|
|
505
|
+
case utils_1.AST_NODE_TYPES.ChainExpression:
|
|
506
|
+
case utils_1.AST_NODE_TYPES.TSAsExpression:
|
|
507
|
+
case utils_1.AST_NODE_TYPES.TSNonNullExpression:
|
|
508
|
+
case utils_1.AST_NODE_TYPES.TSSatisfiesExpression:
|
|
509
|
+
case utils_1.AST_NODE_TYPES.TSTypeAssertion:
|
|
510
|
+
return withoutValueWrappers(node.expression);
|
|
511
|
+
default:
|
|
512
|
+
return node;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
/**
|
|
516
|
+
* The instance field an expression is rooted at: `cached` for `this.cached`,
|
|
517
|
+
* for `this.cached.value` and for `this.cache[id]` alike.
|
|
518
|
+
*
|
|
519
|
+
* Both sides of the discriminator below read the ROOT, so an element write
|
|
520
|
+
* (`this.cache[id] = …`) and the read that answers it (`return this.cache[id]`)
|
|
521
|
+
* meet on the same name. A computed root (`this[key]`) names no field
|
|
522
|
+
* statically and answers nothing, which leaves the method reporting.
|
|
523
|
+
*/
|
|
524
|
+
function rootThisField(node) {
|
|
525
|
+
const expression = withoutValueWrappers(node);
|
|
526
|
+
if (expression.type !== utils_1.AST_NODE_TYPES.MemberExpression) {
|
|
527
|
+
return undefined;
|
|
528
|
+
}
|
|
529
|
+
const object = withoutValueWrappers(expression.object);
|
|
530
|
+
if (object.type !== utils_1.AST_NODE_TYPES.ThisExpression) {
|
|
531
|
+
return rootThisField(object);
|
|
532
|
+
}
|
|
533
|
+
return !expression.computed &&
|
|
534
|
+
expression.property.type === utils_1.AST_NODE_TYPES.Identifier
|
|
535
|
+
? expression.property.name
|
|
536
|
+
: undefined;
|
|
537
|
+
}
|
|
538
|
+
/**
|
|
539
|
+
* Whether the expression reads a field the method wrote. Depth and nesting are
|
|
540
|
+
* immaterial — `this.cached ?? EMPTY` and `() => this.cached` hand the written
|
|
541
|
+
* state back as plainly as a bare `this.cached` does — because the question is
|
|
542
|
+
* whether the result carries that state at all, not how it is spelled.
|
|
543
|
+
*/
|
|
544
|
+
function readsWrittenField(node, fields) {
|
|
545
|
+
for (const descendant of subtreeOf(node)) {
|
|
546
|
+
const field = rootThisField(descendant);
|
|
547
|
+
if (field !== undefined && fields.has(field)) {
|
|
548
|
+
return true;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
return false;
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* Whether the method writes an instance field and hands back a result that
|
|
555
|
+
* reads none of the fields it wrote.
|
|
556
|
+
*
|
|
557
|
+
* Such a method reports on an effect: its result says what THIS call did, so
|
|
558
|
+
* the next call has to be free to do it again. Writing a field and returning it
|
|
559
|
+
* is the opposite shape — a hand-rolled cache, which is the very thing
|
|
560
|
+
* `@Memoize()` replaces — and the field read is what separates the two. A
|
|
561
|
+
* single returned read is enough, because a method with one cache-hit path
|
|
562
|
+
* hands back state on that path however many effects its other paths report.
|
|
563
|
+
*
|
|
564
|
+
* A method with no value-returning `return` is left alone. Its result is void
|
|
565
|
+
* by inference rather than by declaration, and #1548 fenced inferred void out
|
|
566
|
+
* of this rule deliberately: an unannotated body carries no declaration of
|
|
567
|
+
* intent to honour.
|
|
568
|
+
*/
|
|
569
|
+
function writesFieldItDoesNotReturn(fn) {
|
|
570
|
+
const written = new Set();
|
|
571
|
+
const results = [];
|
|
572
|
+
for (const node of ownSubtreeOf(fn.body)) {
|
|
573
|
+
if (node.type === utils_1.AST_NODE_TYPES.AssignmentExpression) {
|
|
574
|
+
const field = rootThisField(node.left);
|
|
575
|
+
if (field !== undefined) {
|
|
576
|
+
written.add(field);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
else if (node.type === utils_1.AST_NODE_TYPES.ReturnStatement && node.argument) {
|
|
580
|
+
results.push(node.argument);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
if (written.size === 0 || results.length === 0) {
|
|
584
|
+
return false;
|
|
585
|
+
}
|
|
586
|
+
return !results.some((result) => readsWrittenField(result, written));
|
|
587
|
+
}
|
|
426
588
|
/** The statically known name of a method, for matching call sites against it. */
|
|
427
589
|
function methodName(node) {
|
|
428
590
|
if (node.computed) {
|
|
@@ -664,6 +826,37 @@ exports.enforceMemoizeAsync = (0, createRule_1.createRule)({
|
|
|
664
826
|
if (participatesInTransaction(node, node.value)) {
|
|
665
827
|
return;
|
|
666
828
|
}
|
|
829
|
+
// A `finally` that calls something exists to undo an effect the `try`
|
|
830
|
+
// performed: the method takes a resource, works while it holds it, and
|
|
831
|
+
// gives it back on the way out. What it hands back describes that one
|
|
832
|
+
// attempt, not a fact that stays true afterwards. Cached, the
|
|
833
|
+
// acquire/release pair runs once per instance and every later caller
|
|
834
|
+
// receives a value computed while a resource the `finally` has since
|
|
835
|
+
// released was still held — the hazard the resource-handle exemption
|
|
836
|
+
// above names, seen from the side where the method releases the handle
|
|
837
|
+
// itself, so nothing of it reaches the return type for a signature-keyed
|
|
838
|
+
// gate to read. The failure is silent and arrives only under
|
|
839
|
+
// concurrency, so the fixer would apply it unattended under `--fix`,
|
|
840
|
+
// and both report and fix are withheld.
|
|
841
|
+
if (releasesInOwnFinalizer(node.value)) {
|
|
842
|
+
return;
|
|
843
|
+
}
|
|
844
|
+
// A method that writes an instance field and hands back a result
|
|
845
|
+
// reading none of the fields it wrote reports on an effect: the result
|
|
846
|
+
// says what THIS call did — "I reclaimed the slot" — and the next call
|
|
847
|
+
// has to be free to do it again. Cached, the write happens once per
|
|
848
|
+
// instance while every later caller reads the first call's verdict
|
|
849
|
+
// about work that call alone performed, and a caller passing a freshly
|
|
850
|
+
// built argument object instead accumulates one dead entry per call.
|
|
851
|
+
//
|
|
852
|
+
// The read is the discriminator, and it is deliberately the whole test:
|
|
853
|
+
// a method that writes a field and returns it is a hand-rolled cache,
|
|
854
|
+
// which is the shape `@Memoize()` exists to replace, so it keeps both
|
|
855
|
+
// report and fix. Carving out every write to `this` would silence that
|
|
856
|
+
// shape along with these.
|
|
857
|
+
if (writesFieldItDoesNotReturn(node.value)) {
|
|
858
|
+
return;
|
|
859
|
+
}
|
|
667
860
|
const { aliases: memoizeAliases, namespaces: memoizeNamespaces } = memoizeImports();
|
|
668
861
|
const hasMemoizeImport = memoizeAliases.size > 0 || memoizeNamespaces.size > 0;
|
|
669
862
|
// Check if method already has @Memoize or @Memoize() decorator
|
|
@@ -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
|
/**
|
|
@@ -458,14 +459,123 @@ const isStructuredCloneCallee = (callee) => {
|
|
|
458
459
|
const value = outermostValueOf(callee);
|
|
459
460
|
return (value.type === utils_1.AST_NODE_TYPES.Identifier && value.name === 'structuredClone');
|
|
460
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;
|
|
461
568
|
/**
|
|
462
569
|
* Whether a call COPIES the argument at `index` while keeping its type.
|
|
463
570
|
*
|
|
464
571
|
* `Array.from(X)` and `structuredClone(X)` both hand back a fresh, mutable
|
|
465
572
|
* value whose element or property types are the argument's — so freezing the
|
|
466
|
-
* argument narrows the copy exactly as a spread does.
|
|
467
|
-
*
|
|
468
|
-
*
|
|
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.
|
|
469
579
|
*/
|
|
470
580
|
const isCopyingCall = (call, index) => {
|
|
471
581
|
if (isObjectAssignCallee(call.callee)) {
|
|
@@ -478,16 +588,26 @@ const isCopyingCall = (call, index) => {
|
|
|
478
588
|
return true;
|
|
479
589
|
}
|
|
480
590
|
if (isNamespacedCallee(call.callee, 'Array', 'from')) {
|
|
481
|
-
return call.arguments.length === 1
|
|
591
|
+
return (call.arguments.length === 1 ||
|
|
592
|
+
returnsHandedElement(call.arguments[1], ARRAY_FROM_MAPPER_ELEMENT_INDEX));
|
|
482
593
|
}
|
|
483
594
|
return false;
|
|
484
595
|
};
|
|
485
596
|
/**
|
|
486
|
-
*
|
|
487
|
-
*
|
|
488
|
-
*
|
|
489
|
-
*
|
|
490
|
-
*
|
|
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`.
|
|
491
611
|
*/
|
|
492
612
|
const TYPE_PRESERVING_COPY_METHODS = new Set([
|
|
493
613
|
'concat',
|
|
@@ -538,15 +658,66 @@ const copyExpressionOf = (node) => {
|
|
|
538
658
|
const callee = outermostValueOf(parent);
|
|
539
659
|
// A method REFERENCE (`const take = ITEMS.concat;`) builds nothing, so the
|
|
540
660
|
// copy only exists once the method is actually called.
|
|
541
|
-
if (method
|
|
542
|
-
|
|
543
|
-
callee.parent
|
|
544
|
-
|
|
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)) {
|
|
545
667
|
return callee.parent;
|
|
546
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;
|
|
547
674
|
}
|
|
548
675
|
return null;
|
|
549
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
|
+
};
|
|
550
721
|
/** Pattern nodes a parameter's binding can be nested inside. */
|
|
551
722
|
const PATTERN_CONTAINERS = new Set([
|
|
552
723
|
utils_1.AST_NODE_TYPES.AssignmentPattern,
|
|
@@ -559,12 +730,6 @@ const PATTERN_CONTAINERS = new Set([
|
|
|
559
730
|
// constructor's params and `constructor(public stage = DEFAULT)` narrows.
|
|
560
731
|
utils_1.AST_NODE_TYPES.TSParameterProperty,
|
|
561
732
|
]);
|
|
562
|
-
const FUNCTION_TYPES = new Set([
|
|
563
|
-
utils_1.AST_NODE_TYPES.FunctionDeclaration,
|
|
564
|
-
utils_1.AST_NODE_TYPES.FunctionExpression,
|
|
565
|
-
utils_1.AST_NODE_TYPES.ArrowFunctionExpression,
|
|
566
|
-
utils_1.AST_NODE_TYPES.TSDeclareFunction,
|
|
567
|
-
]);
|
|
568
733
|
/**
|
|
569
734
|
* Whether a default value is what a PARAMETER's type is inferred FROM.
|
|
570
735
|
*
|
|
@@ -752,6 +917,38 @@ const elementProjectionCallOf = (node) => {
|
|
|
752
917
|
? parent
|
|
753
918
|
: null;
|
|
754
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
|
+
};
|
|
755
952
|
/**
|
|
756
953
|
* Array methods whose result ITERATES the receiver's own elements.
|
|
757
954
|
*
|
|
@@ -764,11 +961,29 @@ const elementProjectionCallOf = (node) => {
|
|
|
764
961
|
* is frozen, for an input that compiled (Issue #2340). `entries` yields
|
|
765
962
|
* `[index, element]` pairs, which carry the element exactly as `values` does.
|
|
766
963
|
*
|
|
767
|
-
* `keys` is absent for the reason `Object.keys` is:
|
|
768
|
-
* whatever the receiver holds,
|
|
769
|
-
*
|
|
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.
|
|
770
967
|
*/
|
|
771
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
|
+
]);
|
|
772
987
|
const iteratorProjectionCallOf = (node) => {
|
|
773
988
|
const parent = node.parent;
|
|
774
989
|
if (parent?.type !== utils_1.AST_NODE_TYPES.MemberExpression ||
|
|
@@ -776,7 +991,10 @@ const iteratorProjectionCallOf = (node) => {
|
|
|
776
991
|
return null;
|
|
777
992
|
}
|
|
778
993
|
const method = accessedPropertyName(parent);
|
|
779
|
-
|
|
994
|
+
const iteratorMethods = isElementPreservingCollection(node)
|
|
995
|
+
? COLLECTION_ELEMENT_ITERATOR_METHODS
|
|
996
|
+
: ELEMENT_ITERATOR_METHODS;
|
|
997
|
+
if (method === null || !iteratorMethods.has(method)) {
|
|
780
998
|
return null;
|
|
781
999
|
}
|
|
782
1000
|
// A method REFERENCE (`const walk = ITEMS.values;`) iterates nothing, so the
|
|
@@ -788,32 +1006,6 @@ const iteratorProjectionCallOf = (node) => {
|
|
|
788
1006
|
? callee.parent
|
|
789
1007
|
: null;
|
|
790
1008
|
};
|
|
791
|
-
/**
|
|
792
|
-
* Constructors that build a collection out of the argument's ELEMENTS.
|
|
793
|
-
*
|
|
794
|
-
* `new Set(ITEMS)` holds the constant's own contents, so iterating it hands out
|
|
795
|
-
* the frozen elements and `for (const item of new Set(ITEMS)) { item.n = 2; }`
|
|
796
|
-
* is TS2540 for an input that compiled (Issue #2340).
|
|
797
|
-
*
|
|
798
|
-
* The construction is not a copy in `copyExpressionOf`'s sense — the result has
|
|
799
|
-
* a different shape from the argument, so a write to the collection says
|
|
800
|
-
* nothing about the constant — which is why it is resolved here, where only the
|
|
801
|
-
* ITERATION question is asked. `WeakSet`/`WeakMap` are absent because they are
|
|
802
|
-
* not iterable, so no binding can be taken from one.
|
|
803
|
-
*/
|
|
804
|
-
const ELEMENT_PRESERVING_COLLECTION_NAMES = new Set(['Set', 'Map']);
|
|
805
|
-
const elementCollectionOf = (node) => {
|
|
806
|
-
const parent = node.parent;
|
|
807
|
-
if (parent?.type !== utils_1.AST_NODE_TYPES.NewExpression ||
|
|
808
|
-
parent.arguments[0] !== node) {
|
|
809
|
-
return null;
|
|
810
|
-
}
|
|
811
|
-
const callee = unwrapValueWrappers(parent.callee);
|
|
812
|
-
return callee.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
813
|
-
ELEMENT_PRESERVING_COLLECTION_NAMES.has(callee.name)
|
|
814
|
-
? parent
|
|
815
|
-
: null;
|
|
816
|
-
};
|
|
817
1009
|
/**
|
|
818
1010
|
* The expressions a reference denotes a FROZEN value through: the reference
|
|
819
1011
|
* itself and every property access rooted at it — `CONFIG`, `CONFIG.list`,
|
|
@@ -840,6 +1032,192 @@ const accessPathsRootedAt = (identifier) => {
|
|
|
840
1032
|
current = outermostValueOf(parent);
|
|
841
1033
|
}
|
|
842
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
|
+
};
|
|
843
1221
|
/**
|
|
844
1222
|
* Every way a value derived from this one keeps the constant's ELEMENT types:
|
|
845
1223
|
* a copy of it, the `Object.values`/`Object.entries` array over it, the
|
|
@@ -853,6 +1231,46 @@ const DERIVATION_RESOLVERS = [
|
|
|
853
1231
|
iteratorProjectionCallOf,
|
|
854
1232
|
elementCollectionOf,
|
|
855
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
|
+
};
|
|
856
1274
|
const enrolFully = (variables) => variables.map((variable) => ({
|
|
857
1275
|
variable,
|
|
858
1276
|
breaksOnAnyMutatingMethod: true,
|
|
@@ -918,9 +1336,9 @@ const parameterBindingsOf = (callback, positions, declaredVariablesOf) => {
|
|
|
918
1336
|
*/
|
|
919
1337
|
const bindingsOfIterationOver = (iterable, declaredVariablesOf, iteratesConstantValue) => {
|
|
920
1338
|
// The member path is resolved first because the iterated expression is
|
|
921
|
-
// routinely a PROPERTY of the constant (`for (const x of CONFIG.list)`)
|
|
922
|
-
//
|
|
923
|
-
//
|
|
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.
|
|
924
1342
|
const path = accessPathOf(iterable);
|
|
925
1343
|
const value = outermostValueOf(path ?? iterable);
|
|
926
1344
|
const parent = value.parent;
|
|
@@ -1026,23 +1444,16 @@ const iterationBindingsOf = (identifier, declaredVariablesOf) => {
|
|
|
1026
1444
|
const bindings = [
|
|
1027
1445
|
...bindingsOfIterationOver(value, declaredVariablesOf, true),
|
|
1028
1446
|
];
|
|
1029
|
-
//
|
|
1030
|
-
//
|
|
1031
|
-
//
|
|
1032
|
-
//
|
|
1033
|
-
|
|
1034
|
-
const
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
for (const resolveDerivation of DERIVATION_RESOLVERS) {
|
|
1038
|
-
const derived = resolveDerivation(pending[index]);
|
|
1039
|
-
if (!derived || visited.has(derived)) {
|
|
1040
|
-
continue;
|
|
1041
|
-
}
|
|
1042
|
-
visited.add(derived);
|
|
1043
|
-
pending.push(derived);
|
|
1044
|
-
bindings.push(...bindingsOfIterationOver(derived, declaredVariablesOf, false));
|
|
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;
|
|
1045
1455
|
}
|
|
1456
|
+
bindings.push(...bindingsOfIterationOver(derived, declaredVariablesOf, false));
|
|
1046
1457
|
}
|
|
1047
1458
|
return bindings;
|
|
1048
1459
|
};
|
|
@@ -1097,10 +1508,20 @@ const iterationBindingsOf = (identifier, declaredVariablesOf) => {
|
|
|
1097
1508
|
* `--fix`.
|
|
1098
1509
|
*/
|
|
1099
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;
|
|
1100
1521
|
// Grown in place and walked by index: an alias found mid-walk is appended and
|
|
1101
1522
|
// reached by the same loop, so the traversal needs no recursion of its own.
|
|
1102
1523
|
const pending = [
|
|
1103
|
-
{ variable, breaksOnAnyMutatingMethod: true },
|
|
1524
|
+
{ variable, breaksOnAnyMutatingMethod: true, frozenValue },
|
|
1104
1525
|
];
|
|
1105
1526
|
const visited = new Set([variable]);
|
|
1106
1527
|
/**
|
|
@@ -1128,7 +1549,7 @@ const blocksAsConstAssertion = (variable, declaredVariablesOf) => {
|
|
|
1128
1549
|
return false;
|
|
1129
1550
|
};
|
|
1130
1551
|
for (let index = 0; index < pending.length; index += 1) {
|
|
1131
|
-
const { variable: enrolled, breaksOnAnyMutatingMethod } = pending[index];
|
|
1552
|
+
const { variable: enrolled, breaksOnAnyMutatingMethod, frozenValue: enrolledValue, } = pending[index];
|
|
1132
1553
|
for (const reference of enrolled.references) {
|
|
1133
1554
|
// Reassigning an alias is as disqualifying as writing through one. A
|
|
1134
1555
|
// binding that takes its type from the constant narrows to the frozen
|
|
@@ -1158,23 +1579,43 @@ const blocksAsConstAssertion = (variable, declaredVariablesOf) => {
|
|
|
1158
1579
|
introducesForeignElement(path, isEnrolledReference))) {
|
|
1159
1580
|
return true;
|
|
1160
1581
|
}
|
|
1161
|
-
// A
|
|
1162
|
-
//
|
|
1163
|
-
//
|
|
1164
|
-
//
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
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
|
+
});
|
|
1171
1612
|
// A binding introduced by ITERATING the constant is enrolled beside the
|
|
1172
1613
|
// aliases: it names the constant's CONTENTS, which the assertion freezes
|
|
1173
1614
|
// with the constant itself — see `iterationBindingsOf`. An alias is
|
|
1174
1615
|
// enrolled on the constant's own terms, because it denotes the constant's
|
|
1175
1616
|
// value and so carries its readonly-ness whole.
|
|
1176
1617
|
const derived = [
|
|
1177
|
-
...enrolFully(
|
|
1618
|
+
...enrolFully(aliases),
|
|
1178
1619
|
...iterationBindingsOf(reference.identifier, declaredVariablesOf),
|
|
1179
1620
|
];
|
|
1180
1621
|
for (const alias of derived) {
|
package/package.json
CHANGED
package/release-manifest.json
CHANGED
|
@@ -1,4 +1,35 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "1.21.15",
|
|
4
|
+
"date": "2026-09-06T18:24:58.623Z",
|
|
5
|
+
"rules": [
|
|
6
|
+
{
|
|
7
|
+
"name": "enforce-empty-object-check",
|
|
8
|
+
"changeType": "fix",
|
|
9
|
+
"issues": [
|
|
10
|
+
2344
|
|
11
|
+
],
|
|
12
|
+
"summary": "keep a declared type's verdict when the checker cannot resolve it (closes #2344)"
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"name": "enforce-memoize-async",
|
|
16
|
+
"changeType": "fix",
|
|
17
|
+
"issues": [
|
|
18
|
+
2343
|
|
19
|
+
],
|
|
20
|
+
"summary": "withhold from methods that release what they acquired and from methods reporting an effect (closes #2343)"
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"name": "global-const-style",
|
|
24
|
+
"changeType": "fix",
|
|
25
|
+
"issues": [
|
|
26
|
+
2341,
|
|
27
|
+
2342
|
|
28
|
+
],
|
|
29
|
+
"summary": "enrol bindings extracted by member access and by a type-preserving map (closes #2341, closes #2342)"
|
|
30
|
+
}
|
|
31
|
+
]
|
|
32
|
+
},
|
|
2
33
|
{
|
|
3
34
|
"version": "1.21.14",
|
|
4
35
|
"date": "2026-09-06T01:53:58.858Z",
|