@blumintinc/eslint-plugin-blumint 1.21.16 → 1.21.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/index.js +1 -1
- package/lib/rules/enforce-memoize-async.js +120 -3
- package/lib/rules/global-const-style.js +438 -43
- package/package.json +1 -1
- package/release-manifest.json +24 -0
package/lib/index.js
CHANGED
|
@@ -636,6 +636,108 @@ function namesRelease(call) {
|
|
|
636
636
|
const name = calleeName(call);
|
|
637
637
|
return (name !== undefined && wordsOf(name).some((word) => RELEASE_WORDS.has(word)));
|
|
638
638
|
}
|
|
639
|
+
/**
|
|
640
|
+
* Words that name REPORTING on the work rather than acting on what the work
|
|
641
|
+
* held. A call spelled from one of them observes the attempt: it reads the
|
|
642
|
+
* handle to say something about it, which leaves the handle exactly as
|
|
643
|
+
* outstanding as it was.
|
|
644
|
+
*
|
|
645
|
+
* Words for a lifetime ending stay out of this set for the reason they stay
|
|
646
|
+
* out of `RELEASE_WORDS`: `end` and `close` name the moment, and reading them
|
|
647
|
+
* here would strip the carve-out from a genuine hand-back spelled that way.
|
|
648
|
+
*/
|
|
649
|
+
const REPORT_WORDS = new Set([
|
|
650
|
+
'count',
|
|
651
|
+
'debug',
|
|
652
|
+
'emit',
|
|
653
|
+
'error',
|
|
654
|
+
'increment',
|
|
655
|
+
'info',
|
|
656
|
+
'log',
|
|
657
|
+
'measure',
|
|
658
|
+
'observe',
|
|
659
|
+
'record',
|
|
660
|
+
'report',
|
|
661
|
+
'span',
|
|
662
|
+
'time',
|
|
663
|
+
'track',
|
|
664
|
+
'trace',
|
|
665
|
+
'warn',
|
|
666
|
+
]);
|
|
667
|
+
/**
|
|
668
|
+
* Receivers that are somewhere to SEND an observation, not something a method
|
|
669
|
+
* holds. A call routed through one of them reports whatever its own verb is
|
|
670
|
+
* called, which is what reaches `span.setAttribute(…)` and `this.stats.bump(…)`
|
|
671
|
+
* — spellings whose verb carries no report word at all.
|
|
672
|
+
*/
|
|
673
|
+
const SINK_NAMES = new Set([
|
|
674
|
+
'analytics',
|
|
675
|
+
'console',
|
|
676
|
+
'log',
|
|
677
|
+
'logger',
|
|
678
|
+
'metrics',
|
|
679
|
+
'span',
|
|
680
|
+
'stats',
|
|
681
|
+
'telemetry',
|
|
682
|
+
'tracer',
|
|
683
|
+
]);
|
|
684
|
+
/**
|
|
685
|
+
* The nearest static name of a call's receiver: `logger` for
|
|
686
|
+
* `this.logger.info(…)` and for `this.deps.logger.info(…)` alike, `console`
|
|
687
|
+
* for `console.log(…)`. The nearest name rather than the root is what a sink
|
|
688
|
+
* is spelled as — the root of an injected logger is `this`, which says nothing.
|
|
689
|
+
*/
|
|
690
|
+
function receiverName(call) {
|
|
691
|
+
const callee = withoutChain(call.callee);
|
|
692
|
+
if (callee.type !== utils_1.AST_NODE_TYPES.MemberExpression) {
|
|
693
|
+
return undefined;
|
|
694
|
+
}
|
|
695
|
+
const receiver = withoutValueWrappers(callee.object);
|
|
696
|
+
if (receiver.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
697
|
+
return receiver.name;
|
|
698
|
+
}
|
|
699
|
+
return receiver.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
700
|
+
!receiver.computed &&
|
|
701
|
+
receiver.property.type === utils_1.AST_NODE_TYPES.Identifier
|
|
702
|
+
? receiver.property.name
|
|
703
|
+
: undefined;
|
|
704
|
+
}
|
|
705
|
+
/**
|
|
706
|
+
* The binding a call's receiver is rooted at: `log` for `log.flush()`,
|
|
707
|
+
* undefined for `this.logger.info(…)`, whose root is `this`.
|
|
708
|
+
*/
|
|
709
|
+
function receiverRootBinding(call) {
|
|
710
|
+
const callee = withoutChain(call.callee);
|
|
711
|
+
if (callee.type !== utils_1.AST_NODE_TYPES.MemberExpression) {
|
|
712
|
+
return undefined;
|
|
713
|
+
}
|
|
714
|
+
let receiver = withoutValueWrappers(callee.object);
|
|
715
|
+
while (receiver.type === utils_1.AST_NODE_TYPES.MemberExpression) {
|
|
716
|
+
receiver = withoutValueWrappers(receiver.object);
|
|
717
|
+
}
|
|
718
|
+
return receiver.type === utils_1.AST_NODE_TYPES.Identifier
|
|
719
|
+
? receiver.name
|
|
720
|
+
: undefined;
|
|
721
|
+
}
|
|
722
|
+
/**
|
|
723
|
+
* Whether the call reads as an observation of the attempt: its verb names a
|
|
724
|
+
* report, or it is routed through a sink. Either way what it does with a handle
|
|
725
|
+
* it names is describe it.
|
|
726
|
+
*
|
|
727
|
+
* A hand-back word in the verb is decided ahead of this, so
|
|
728
|
+
* `this.logger.release(lease)` stays a release: a sink is inferred from a name
|
|
729
|
+
* a caller chose freely, while a release verb states the intent outright.
|
|
730
|
+
*/
|
|
731
|
+
function readsAsReport(call) {
|
|
732
|
+
const name = calleeName(call);
|
|
733
|
+
if (name !== undefined &&
|
|
734
|
+
wordsOf(name).some((word) => REPORT_WORDS.has(word))) {
|
|
735
|
+
return true;
|
|
736
|
+
}
|
|
737
|
+
const receiver = receiverName(call);
|
|
738
|
+
return (receiver !== undefined &&
|
|
739
|
+
wordsOf(receiver).some((word) => SINK_NAMES.has(word)));
|
|
740
|
+
}
|
|
639
741
|
/**
|
|
640
742
|
* Whether the `finally` gives back something the method took, rather than
|
|
641
743
|
* reporting on the work that took it.
|
|
@@ -650,6 +752,14 @@ function namesRelease(call) {
|
|
|
650
752
|
* bookkeeping the work never touched — `clearTimeout(t)` beside a `fetch` that
|
|
651
753
|
* ignores `t`, or a span that observes the attempt from outside it.
|
|
652
754
|
*
|
|
755
|
+
* Naming the handle is evidence the finalizer USED it, though, and a log line
|
|
756
|
+
* uses it exactly as loudly as a hand-back does: `this.logger.info('used
|
|
757
|
+
* lease', lease.id)` closes the triangle while the lease leaks. So a call that
|
|
758
|
+
* reads as an observation — a reporting verb, or a route through a sink — does
|
|
759
|
+
* not close it. The exception is a call ON the handle whose own acquisition is
|
|
760
|
+
* being paired: `log.flush()` acts on the thing it is spelled after, and only a
|
|
761
|
+
* receiver NAME suggested otherwise.
|
|
762
|
+
*
|
|
653
763
|
* The second shape is the name, and it covers the release a method does not
|
|
654
764
|
* acquire: the caller takes the lock and passes it in, leaving `finally {
|
|
655
765
|
* lock.release() }` with nothing in the body to pair against. Structurally that
|
|
@@ -671,10 +781,16 @@ function releasesResource(guarded, finalizer, acquisitions) {
|
|
|
671
781
|
if (namesRelease(node)) {
|
|
672
782
|
return true;
|
|
673
783
|
}
|
|
784
|
+
const reporting = readsAsReport(node);
|
|
785
|
+
const receiver = reporting ? receiverRootBinding(node) : undefined;
|
|
674
786
|
for (const bindings of acquisitions) {
|
|
675
|
-
if (mentionsAny(node, bindings)
|
|
676
|
-
|
|
787
|
+
if (!mentionsAny(node, bindings) || !mentionsAny(guarded, bindings)) {
|
|
788
|
+
continue;
|
|
677
789
|
}
|
|
790
|
+
if (reporting && (receiver === undefined || !bindings.has(receiver))) {
|
|
791
|
+
continue;
|
|
792
|
+
}
|
|
793
|
+
return true;
|
|
678
794
|
}
|
|
679
795
|
}
|
|
680
796
|
return false;
|
|
@@ -691,7 +807,8 @@ function releasesResource(guarded, finalizer, acquisitions) {
|
|
|
691
807
|
* (`finally { done = true; }`) records that the attempt finished instead of
|
|
692
808
|
* undoing it, and one that logs, emits a metric, ends a span or clears a timer
|
|
693
809
|
* the work never touched reports on the attempt instead of giving anything
|
|
694
|
-
* back, so none of them costs the method its report.
|
|
810
|
+
* back, so none of them costs the method its report. A log line naming the
|
|
811
|
+
* handle is such a report however much of the handle it names.
|
|
695
812
|
*/
|
|
696
813
|
function releasesInOwnFinalizer(fn) {
|
|
697
814
|
const acquisitions = acquisitionsOf(fn);
|
|
@@ -486,7 +486,7 @@ const accessPathRootOf = (node) => {
|
|
|
486
486
|
}
|
|
487
487
|
};
|
|
488
488
|
/**
|
|
489
|
-
* Every
|
|
489
|
+
* Every identifier a binding PATTERN introduces — `x`, `{ n }`, `{ n: value }`,
|
|
490
490
|
* `[head]`, `{ n, ...rest }` and every nesting of them.
|
|
491
491
|
*
|
|
492
492
|
* Destructuring and member access are two spellings of ONE extraction, so a
|
|
@@ -505,16 +505,16 @@ const accessPathRootOf = (node) => {
|
|
|
505
505
|
* Reading the default would trade an over-decline, which costs one report, for
|
|
506
506
|
* a `--fix` that stops the file compiling.
|
|
507
507
|
*/
|
|
508
|
-
const
|
|
508
|
+
const patternBoundIdentifiers = (pattern, bindings = new Set()) => {
|
|
509
509
|
switch (pattern.type) {
|
|
510
510
|
case utils_1.AST_NODE_TYPES.Identifier:
|
|
511
|
-
|
|
511
|
+
bindings.add(pattern);
|
|
512
512
|
break;
|
|
513
513
|
case utils_1.AST_NODE_TYPES.ObjectPattern:
|
|
514
514
|
for (const property of pattern.properties) {
|
|
515
|
-
|
|
515
|
+
patternBoundIdentifiers(property.type === utils_1.AST_NODE_TYPES.Property
|
|
516
516
|
? property.value
|
|
517
|
-
: property.argument,
|
|
517
|
+
: property.argument, bindings);
|
|
518
518
|
}
|
|
519
519
|
break;
|
|
520
520
|
case utils_1.AST_NODE_TYPES.ArrayPattern:
|
|
@@ -522,21 +522,28 @@ const patternBoundNames = (pattern, names = new Set()) => {
|
|
|
522
522
|
// An array pattern holds a HOLE for each skipped position
|
|
523
523
|
// (`([, second]) => second`), which binds no name.
|
|
524
524
|
if (element) {
|
|
525
|
-
|
|
525
|
+
patternBoundIdentifiers(element, bindings);
|
|
526
526
|
}
|
|
527
527
|
}
|
|
528
528
|
break;
|
|
529
529
|
case utils_1.AST_NODE_TYPES.AssignmentPattern:
|
|
530
|
-
|
|
530
|
+
patternBoundIdentifiers(pattern.left, bindings);
|
|
531
531
|
break;
|
|
532
532
|
case utils_1.AST_NODE_TYPES.RestElement:
|
|
533
|
-
|
|
533
|
+
patternBoundIdentifiers(pattern.argument, bindings);
|
|
534
534
|
break;
|
|
535
535
|
default:
|
|
536
536
|
break;
|
|
537
537
|
}
|
|
538
|
-
return
|
|
538
|
+
return bindings;
|
|
539
539
|
};
|
|
540
|
+
/**
|
|
541
|
+
* The NAMES those bindings introduce, which is what the value descent matches
|
|
542
|
+
* the element by — see `returnsHandedElement` for why that descent works in
|
|
543
|
+
* spellings rather than in bindings. Derived from the identifiers rather than
|
|
544
|
+
* collected alongside them so the two answers cannot drift apart.
|
|
545
|
+
*/
|
|
546
|
+
const patternBoundNames = (pattern) => new Set([...patternBoundIdentifiers(pattern)].map((binding) => binding.name));
|
|
540
547
|
/**
|
|
541
548
|
* Every value a function hands back: its expression body, the argument of each
|
|
542
549
|
* `return` in its block, and the argument of each `yield` — a generator hands
|
|
@@ -706,6 +713,205 @@ const functionCarriesElementType = (fn, elementNames) => {
|
|
|
706
713
|
return (carriesDefault ||
|
|
707
714
|
returnedValuesOf(fn).some((handed) => carriesElementType(handed, reachable)));
|
|
708
715
|
};
|
|
716
|
+
/**
|
|
717
|
+
* Every VALUE binding a single statement introduces into the scope holding it.
|
|
718
|
+
*
|
|
719
|
+
* Type declarations are skipped because `typeof x` reads the value namespace,
|
|
720
|
+
* so an alias or an interface spelled like the element shadows it nowhere.
|
|
721
|
+
*
|
|
722
|
+
* A `var` is collected against the block it is written in rather than the
|
|
723
|
+
* function it hoists to, so it is seen from inside that block alone. That
|
|
724
|
+
* under-reads shadowing, and a missed shadow reads a `typeof` as the element,
|
|
725
|
+
* which withholds a report — the cheap error of the two, since claiming a shadow
|
|
726
|
+
* that is not there licenses a freeze.
|
|
727
|
+
*/
|
|
728
|
+
const statementBoundIdentifiers = (statement, bindings) => {
|
|
729
|
+
switch (statement.type) {
|
|
730
|
+
case utils_1.AST_NODE_TYPES.VariableDeclaration:
|
|
731
|
+
for (const declarator of statement.declarations) {
|
|
732
|
+
patternBoundIdentifiers(declarator.id, bindings);
|
|
733
|
+
}
|
|
734
|
+
break;
|
|
735
|
+
case utils_1.AST_NODE_TYPES.FunctionDeclaration:
|
|
736
|
+
case utils_1.AST_NODE_TYPES.ClassDeclaration:
|
|
737
|
+
case utils_1.AST_NODE_TYPES.TSEnumDeclaration:
|
|
738
|
+
if (statement.id) {
|
|
739
|
+
bindings.add(statement.id);
|
|
740
|
+
}
|
|
741
|
+
break;
|
|
742
|
+
case utils_1.AST_NODE_TYPES.ImportDeclaration:
|
|
743
|
+
for (const specifier of statement.specifiers) {
|
|
744
|
+
bindings.add(specifier.local);
|
|
745
|
+
}
|
|
746
|
+
break;
|
|
747
|
+
case utils_1.AST_NODE_TYPES.ExportNamedDeclaration:
|
|
748
|
+
case utils_1.AST_NODE_TYPES.ExportDefaultDeclaration:
|
|
749
|
+
if (statement.declaration) {
|
|
750
|
+
statementBoundIdentifiers(statement.declaration, bindings);
|
|
751
|
+
}
|
|
752
|
+
break;
|
|
753
|
+
default:
|
|
754
|
+
break;
|
|
755
|
+
}
|
|
756
|
+
};
|
|
757
|
+
/**
|
|
758
|
+
* The binding a node introduces for `name` in its OWN scope, or null when it
|
|
759
|
+
* introduces none.
|
|
760
|
+
*
|
|
761
|
+
* Carries just enough of the scope grammar to answer whether anything between a
|
|
762
|
+
* `typeof` and the element parameter rebinds the name, which is the only
|
|
763
|
+
* question `resolvesToElementBinding` asks of it. A scope form left out can fail
|
|
764
|
+
* to find a shadow and never invent one, so the error it admits is a withheld
|
|
765
|
+
* report rather than a freeze.
|
|
766
|
+
*/
|
|
767
|
+
const scopeBindingOf = (node, name) => {
|
|
768
|
+
const bindings = new Set();
|
|
769
|
+
switch (node.type) {
|
|
770
|
+
case utils_1.AST_NODE_TYPES.ArrowFunctionExpression:
|
|
771
|
+
case utils_1.AST_NODE_TYPES.FunctionExpression:
|
|
772
|
+
case utils_1.AST_NODE_TYPES.FunctionDeclaration:
|
|
773
|
+
case utils_1.AST_NODE_TYPES.TSDeclareFunction:
|
|
774
|
+
for (const parameter of node.params) {
|
|
775
|
+
patternBoundIdentifiers(parameter, bindings);
|
|
776
|
+
}
|
|
777
|
+
// A function EXPRESSION binds its own name inside itself; an arrow has no
|
|
778
|
+
// name to bind.
|
|
779
|
+
if (node.type !== utils_1.AST_NODE_TYPES.ArrowFunctionExpression && node.id) {
|
|
780
|
+
bindings.add(node.id);
|
|
781
|
+
}
|
|
782
|
+
break;
|
|
783
|
+
case utils_1.AST_NODE_TYPES.CatchClause:
|
|
784
|
+
if (node.param) {
|
|
785
|
+
patternBoundIdentifiers(node.param, bindings);
|
|
786
|
+
}
|
|
787
|
+
break;
|
|
788
|
+
case utils_1.AST_NODE_TYPES.ClassDeclaration:
|
|
789
|
+
case utils_1.AST_NODE_TYPES.ClassExpression:
|
|
790
|
+
if (node.id) {
|
|
791
|
+
bindings.add(node.id);
|
|
792
|
+
}
|
|
793
|
+
break;
|
|
794
|
+
case utils_1.AST_NODE_TYPES.ForStatement:
|
|
795
|
+
if (node.init) {
|
|
796
|
+
statementBoundIdentifiers(node.init, bindings);
|
|
797
|
+
}
|
|
798
|
+
break;
|
|
799
|
+
case utils_1.AST_NODE_TYPES.ForInStatement:
|
|
800
|
+
case utils_1.AST_NODE_TYPES.ForOfStatement:
|
|
801
|
+
statementBoundIdentifiers(node.left, bindings);
|
|
802
|
+
break;
|
|
803
|
+
case utils_1.AST_NODE_TYPES.Program:
|
|
804
|
+
case utils_1.AST_NODE_TYPES.BlockStatement:
|
|
805
|
+
case utils_1.AST_NODE_TYPES.TSModuleBlock:
|
|
806
|
+
case utils_1.AST_NODE_TYPES.StaticBlock:
|
|
807
|
+
for (const statement of node.body) {
|
|
808
|
+
statementBoundIdentifiers(statement, bindings);
|
|
809
|
+
}
|
|
810
|
+
break;
|
|
811
|
+
case utils_1.AST_NODE_TYPES.SwitchCase:
|
|
812
|
+
for (const statement of node.consequent) {
|
|
813
|
+
statementBoundIdentifiers(statement, bindings);
|
|
814
|
+
}
|
|
815
|
+
break;
|
|
816
|
+
default:
|
|
817
|
+
break;
|
|
818
|
+
}
|
|
819
|
+
for (const binding of bindings) {
|
|
820
|
+
if (binding.name === name) {
|
|
821
|
+
return binding;
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
return null;
|
|
825
|
+
};
|
|
826
|
+
/**
|
|
827
|
+
* Whether an identifier written in a TYPE position resolves to one of the
|
|
828
|
+
* bindings the element parameter introduces.
|
|
829
|
+
*
|
|
830
|
+
* Resolution is lexical — the nearest enclosing binder wins — rather than a
|
|
831
|
+
* match on the name, because a `typeof` deep inside a callback can spell the
|
|
832
|
+
* element's name while naming something else entirely: `(item) => () => { const
|
|
833
|
+
* item = { n: 9 }; return (x: typeof item) => x; }` is typed from that local
|
|
834
|
+
* binding, which the constant reaches never, and freezing under it is measured
|
|
835
|
+
* safe.
|
|
836
|
+
*/
|
|
837
|
+
const resolvesToElementBinding = (identifier, elementBindings) => {
|
|
838
|
+
for (let node = identifier.parent; node; node = node.parent) {
|
|
839
|
+
const binding = scopeBindingOf(node, identifier.name);
|
|
840
|
+
if (binding) {
|
|
841
|
+
return elementBindings.has(binding);
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
return false;
|
|
845
|
+
};
|
|
846
|
+
/**
|
|
847
|
+
* The identifier a `typeof` query is rooted at — `item` for `typeof item` and
|
|
848
|
+
* for `typeof item.n` alike — or null when the query names an import instead.
|
|
849
|
+
*/
|
|
850
|
+
const typeQueryRootOf = (query) => {
|
|
851
|
+
let entity = query.exprName;
|
|
852
|
+
while (entity.type === utils_1.AST_NODE_TYPES.TSQualifiedName) {
|
|
853
|
+
entity = entity.left;
|
|
854
|
+
}
|
|
855
|
+
return entity.type === utils_1.AST_NODE_TYPES.Identifier ? entity : null;
|
|
856
|
+
};
|
|
857
|
+
/**
|
|
858
|
+
* Whether a `typeof` anywhere inside `node` names the element.
|
|
859
|
+
*
|
|
860
|
+
* The element's type reaches a mapper's result through TYPE positions as
|
|
861
|
+
* readily as through value ones: a handed-back closure annotated `(x: typeof
|
|
862
|
+
* item) => x` is typed from the element without ever returning it, so freezing
|
|
863
|
+
* the receiver makes `makers.push((v: { n: number }) => v)` TS2345 for an input
|
|
864
|
+
* that compiled (Issue #2357).
|
|
865
|
+
*
|
|
866
|
+
* The query is looked for by STRUCTURE rather than by enumerating the type
|
|
867
|
+
* syntax it can sit under, because every wrapper measured breaks the build the
|
|
868
|
+
* same way — `typeof item.n`, `Wrapper<typeof item>`, `(typeof item)[]`,
|
|
869
|
+
* `[typeof item]`, `(typeof item)['n']`, a union, a conditional type, a
|
|
870
|
+
* function type, and a RETURN annotation rather than a parameter — and
|
|
871
|
+
* enumerating forms is what leaves the next spelling out.
|
|
872
|
+
*
|
|
873
|
+
* The sweep covers the whole CALLBACK rather than the values it returns,
|
|
874
|
+
* because the type an annotation names can be introduced beside them: `type
|
|
875
|
+
* Held = typeof item;` or an `interface` declared in the callback's body, a
|
|
876
|
+
* `satisfies` written in an expression, a class expression's field. Each of
|
|
877
|
+
* those is measured to break under `--fix` too, and each is invisible to a
|
|
878
|
+
* sweep of the returned value alone. A `typeof` of the element that composes
|
|
879
|
+
* the result nowhere then withholds one report, which is the cheap error of
|
|
880
|
+
* the two.
|
|
881
|
+
*/
|
|
882
|
+
const typeQueryNamesElement = (node, elementBindings) => {
|
|
883
|
+
// A query spelling some other name resolves to some other binding whatever
|
|
884
|
+
// the scopes hold, so screening on the spelling first keeps the lexical walk
|
|
885
|
+
// for the queries that can possibly be the element.
|
|
886
|
+
const spellings = new Set([...elementBindings].map((binding) => binding.name));
|
|
887
|
+
let found = false;
|
|
888
|
+
const visit = (current) => {
|
|
889
|
+
if (found) {
|
|
890
|
+
return;
|
|
891
|
+
}
|
|
892
|
+
if (current.type === utils_1.AST_NODE_TYPES.TSTypeQuery) {
|
|
893
|
+
const root = typeQueryRootOf(current);
|
|
894
|
+
if (root &&
|
|
895
|
+
spellings.has(root.name) &&
|
|
896
|
+
resolvesToElementBinding(root, elementBindings)) {
|
|
897
|
+
found = true;
|
|
898
|
+
return;
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
for (const [key, value] of Object.entries(current)) {
|
|
902
|
+
if (key === 'parent') {
|
|
903
|
+
continue;
|
|
904
|
+
}
|
|
905
|
+
for (const child of Array.isArray(value) ? value : [value]) {
|
|
906
|
+
if (ASTHelpers_1.ASTHelpers.isNode(child)) {
|
|
907
|
+
visit(child);
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
};
|
|
912
|
+
visit(node);
|
|
913
|
+
return found;
|
|
914
|
+
};
|
|
709
915
|
/**
|
|
710
916
|
* Whether a callback hands back the value it is given at `elementIndex`, or a
|
|
711
917
|
* value the element's type reaches — see `carriesElementType`.
|
|
@@ -732,6 +938,9 @@ const functionCarriesElementType = (fn, elementNames) => {
|
|
|
732
938
|
* different things widen their union, so the assertion may then reach nothing
|
|
733
939
|
* and the withhold costs a report; demanding EVERY branch would instead ship a
|
|
734
940
|
* `--fix` that stops the file compiling.
|
|
941
|
+
*
|
|
942
|
+
* A TYPE position carries the element too, and is asked as a separate question
|
|
943
|
+
* because it is resolved rather than name-matched — see `typeQueryNamesElement`.
|
|
735
944
|
*/
|
|
736
945
|
const returnsHandedElement = (callback, elementIndex) => {
|
|
737
946
|
if (!callback || !isFunctionValue(callback)) {
|
|
@@ -741,10 +950,14 @@ const returnsHandedElement = (callback, elementIndex) => {
|
|
|
741
950
|
if (!parameter) {
|
|
742
951
|
return false;
|
|
743
952
|
}
|
|
744
|
-
const
|
|
745
|
-
if (
|
|
953
|
+
const elementBindings = patternBoundIdentifiers(parameter);
|
|
954
|
+
if (elementBindings.size === 0) {
|
|
746
955
|
return false;
|
|
747
956
|
}
|
|
957
|
+
if (typeQueryNamesElement(callback, elementBindings)) {
|
|
958
|
+
return true;
|
|
959
|
+
}
|
|
960
|
+
const elementNames = patternBoundNames(parameter);
|
|
748
961
|
return returnedValuesOf(callback).some((value) => carriesElementType(value, elementNames));
|
|
749
962
|
};
|
|
750
963
|
/**
|
|
@@ -874,6 +1087,165 @@ const copyExpressionOf = (node) => {
|
|
|
874
1087
|
}
|
|
875
1088
|
return null;
|
|
876
1089
|
};
|
|
1090
|
+
/**
|
|
1091
|
+
* The argument position `Array.from`'s MAPPER arrives in.
|
|
1092
|
+
*
|
|
1093
|
+
* Named beside `ARRAY_FROM_MAPPER_ELEMENT_INDEX` because the mapper is the
|
|
1094
|
+
* SECOND argument while the element it is handed is its FIRST parameter, so
|
|
1095
|
+
* two different indices for one call sit beside each other here.
|
|
1096
|
+
*/
|
|
1097
|
+
const ARRAY_FROM_MAPPER_ARGUMENT_INDEX = 1;
|
|
1098
|
+
/**
|
|
1099
|
+
* The call whose RESULT is typed from what this function literal hands back:
|
|
1100
|
+
* the `map`/`flatMap` call it is the callback of, or the `Array.from` call it
|
|
1101
|
+
* is the mapper of.
|
|
1102
|
+
*
|
|
1103
|
+
* The RECEIVER is not consulted, because the question is what composes the
|
|
1104
|
+
* result rather than where the elements came from — a mapper handing back a
|
|
1105
|
+
* value of its own retypes the result whatever it is mapping over.
|
|
1106
|
+
*
|
|
1107
|
+
* A callback passed by NAME is a LIMITATION rather than a decision: the climb
|
|
1108
|
+
* ends at the function's own declaration, and reaching the call sites from
|
|
1109
|
+
* there is a hop through the binding that this resolver does not take, so
|
|
1110
|
+
* `const pick = () => OTHER; items.map(pick)` keeps its report and its fix.
|
|
1111
|
+
*/
|
|
1112
|
+
const mapperCallOf = (fn) => {
|
|
1113
|
+
const value = outermostValueOf(fn);
|
|
1114
|
+
const call = value.parent;
|
|
1115
|
+
if (!call || call.type !== utils_1.AST_NODE_TYPES.CallExpression) {
|
|
1116
|
+
return null;
|
|
1117
|
+
}
|
|
1118
|
+
if (call.arguments[ARRAY_FROM_MAPPER_ARGUMENT_INDEX] === value &&
|
|
1119
|
+
isNamespacedCallee(call.callee, 'Array', 'from')) {
|
|
1120
|
+
return call;
|
|
1121
|
+
}
|
|
1122
|
+
if (call.arguments[0] !== value) {
|
|
1123
|
+
return null;
|
|
1124
|
+
}
|
|
1125
|
+
const callee = unwrapValueWrappers(call.callee);
|
|
1126
|
+
if (callee.type !== utils_1.AST_NODE_TYPES.MemberExpression) {
|
|
1127
|
+
return null;
|
|
1128
|
+
}
|
|
1129
|
+
const method = accessedPropertyName(callee);
|
|
1130
|
+
return method !== null && CALLBACK_TYPED_COPY_METHODS.has(method)
|
|
1131
|
+
? call
|
|
1132
|
+
: null;
|
|
1133
|
+
};
|
|
1134
|
+
/** The function whose body encloses this node, or null at the top level. */
|
|
1135
|
+
const enclosingFunctionOf = (node) => {
|
|
1136
|
+
let current = node.parent;
|
|
1137
|
+
while (current) {
|
|
1138
|
+
if (FUNCTION_TYPES.has(current.type)) {
|
|
1139
|
+
return current;
|
|
1140
|
+
}
|
|
1141
|
+
current = current.parent;
|
|
1142
|
+
}
|
|
1143
|
+
return null;
|
|
1144
|
+
};
|
|
1145
|
+
/**
|
|
1146
|
+
* The expression whose type this value COMPOSES, or null where it composes
|
|
1147
|
+
* nothing that leaves the position it sits in.
|
|
1148
|
+
*
|
|
1149
|
+
* These are the positions `carriesElementType` descends through, read in the
|
|
1150
|
+
* opposite direction: that resolver starts from a returned value and asks
|
|
1151
|
+
* whether the element reaches it, while this one starts from a reference and
|
|
1152
|
+
* climbs to what is returned. The two must agree, since one file's
|
|
1153
|
+
* `ITEMS.map((x) => [x])` and `ITEMS.map(() => [OTHER])` are the same
|
|
1154
|
+
* composition of a frozen value into a fresh array.
|
|
1155
|
+
*
|
|
1156
|
+
* A `return` and a `yield` climb to the function they leave, whose result type
|
|
1157
|
+
* they compose — the pair `returnedValuesOf` collects going the other way.
|
|
1158
|
+
*
|
|
1159
|
+
* Everything else stops the climb, which withholds nothing that matters: an
|
|
1160
|
+
* arithmetic operand, a template literal and a call argument all WIDEN, so the
|
|
1161
|
+
* assertion no longer reaches the value the position produces.
|
|
1162
|
+
*/
|
|
1163
|
+
const composedValueOf = (value) => {
|
|
1164
|
+
const parent = value.parent;
|
|
1165
|
+
if (!parent) {
|
|
1166
|
+
return null;
|
|
1167
|
+
}
|
|
1168
|
+
switch (parent.type) {
|
|
1169
|
+
case utils_1.AST_NODE_TYPES.ConditionalExpression:
|
|
1170
|
+
// The TEST decides which branch runs rather than what the expression is
|
|
1171
|
+
// typed as, so only the branches compose the result's union.
|
|
1172
|
+
return parent.consequent === value || parent.alternate === value
|
|
1173
|
+
? parent
|
|
1174
|
+
: null;
|
|
1175
|
+
case utils_1.AST_NODE_TYPES.LogicalExpression:
|
|
1176
|
+
return parent;
|
|
1177
|
+
case utils_1.AST_NODE_TYPES.SequenceExpression:
|
|
1178
|
+
return parent.expressions[parent.expressions.length - 1] === value
|
|
1179
|
+
? parent
|
|
1180
|
+
: null;
|
|
1181
|
+
case utils_1.AST_NODE_TYPES.ArrayExpression:
|
|
1182
|
+
return parent;
|
|
1183
|
+
case utils_1.AST_NODE_TYPES.Property:
|
|
1184
|
+
// A COMPUTED key types the property NAME rather than the value, and the
|
|
1185
|
+
// pattern screen reads names, not keys.
|
|
1186
|
+
return parent.value === value &&
|
|
1187
|
+
parent.parent?.type === utils_1.AST_NODE_TYPES.ObjectExpression
|
|
1188
|
+
? parent.parent
|
|
1189
|
+
: null;
|
|
1190
|
+
case utils_1.AST_NODE_TYPES.SpreadElement:
|
|
1191
|
+
return parent.parent &&
|
|
1192
|
+
(parent.parent.type === utils_1.AST_NODE_TYPES.ArrayExpression ||
|
|
1193
|
+
parent.parent.type === utils_1.AST_NODE_TYPES.ObjectExpression)
|
|
1194
|
+
? parent.parent
|
|
1195
|
+
: null;
|
|
1196
|
+
case utils_1.AST_NODE_TYPES.AwaitExpression:
|
|
1197
|
+
return parent;
|
|
1198
|
+
case utils_1.AST_NODE_TYPES.ArrowFunctionExpression:
|
|
1199
|
+
return parent.body === value ? parent : null;
|
|
1200
|
+
case utils_1.AST_NODE_TYPES.CallExpression:
|
|
1201
|
+
// A function literal INVOKED on the spot hands its returns to the call,
|
|
1202
|
+
// which is the one call whose result the assertion still reaches — the
|
|
1203
|
+
// exception `carriesElementType` makes for the same shape.
|
|
1204
|
+
return isFunctionValue(value) && parent.callee === value ? parent : null;
|
|
1205
|
+
case utils_1.AST_NODE_TYPES.ReturnStatement:
|
|
1206
|
+
case utils_1.AST_NODE_TYPES.YieldExpression:
|
|
1207
|
+
return parent.argument === value ? enclosingFunctionOf(parent) : null;
|
|
1208
|
+
default:
|
|
1209
|
+
return null;
|
|
1210
|
+
}
|
|
1211
|
+
};
|
|
1212
|
+
/**
|
|
1213
|
+
* The `map`/`flatMap`/`Array.from` call whose result is typed from THIS value,
|
|
1214
|
+
* or null when no mapper hands it back.
|
|
1215
|
+
*
|
|
1216
|
+
* A mapper's result is typed from what it returns, so a constant reached
|
|
1217
|
+
* through that return types the copy exactly as the receiver's own element type
|
|
1218
|
+
* does — and freezing the constant narrows the copy's elements, making
|
|
1219
|
+
* `const xs = ITEMS.map(() => OTHER); xs.push({ n: 9 })` TS2322 for an input
|
|
1220
|
+
* that compiled (Issue #2356). `carriesElementType` cannot answer this: it is
|
|
1221
|
+
* keyed on the names the ELEMENT parameter binds, and the value here comes from
|
|
1222
|
+
* a different binding entirely.
|
|
1223
|
+
*
|
|
1224
|
+
* The constant is identified by the reference walk that calls this, which reads
|
|
1225
|
+
* the scope manager's reference lists rather than matching names — so a
|
|
1226
|
+
* callback-local binding that shadows the constant's name is a different
|
|
1227
|
+
* variable and reaches this resolver never, and a LOCAL constant, which this
|
|
1228
|
+
* rule does not freeze, is walked never.
|
|
1229
|
+
*
|
|
1230
|
+
* Returns an ANCESTOR of the node it is given, as every derivation resolver
|
|
1231
|
+
* must, so the walk that follows them strictly ascends.
|
|
1232
|
+
*/
|
|
1233
|
+
const mapperResultOf = (node) => {
|
|
1234
|
+
let value = outermostValueOf(node);
|
|
1235
|
+
for (;;) {
|
|
1236
|
+
if (isFunctionValue(value)) {
|
|
1237
|
+
const call = mapperCallOf(value);
|
|
1238
|
+
if (call) {
|
|
1239
|
+
return call;
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
const composed = composedValueOf(value);
|
|
1243
|
+
if (!composed) {
|
|
1244
|
+
return null;
|
|
1245
|
+
}
|
|
1246
|
+
value = outermostValueOf(composed);
|
|
1247
|
+
}
|
|
1248
|
+
};
|
|
877
1249
|
/**
|
|
878
1250
|
* Array methods whose result is an ELEMENT of the receiver rather than a fresh
|
|
879
1251
|
* array over it.
|
|
@@ -1045,19 +1417,23 @@ const ELEMENT_PARAMETER_INDEX_BY_METHOD = new Map([
|
|
|
1045
1417
|
* handed-node spelling escapes it (Issue #2339).
|
|
1046
1418
|
*
|
|
1047
1419
|
* The position is carried apart from the element's because the two are enrolled
|
|
1048
|
-
* on different
|
|
1049
|
-
* the constant's
|
|
1050
|
-
* while the receiver
|
|
1051
|
-
* constant's own value or member path
|
|
1420
|
+
* on different TERMS rather than because they differ by one: an element keeps
|
|
1421
|
+
* the constant's readonly-ness through every derivation the iteration walk
|
|
1422
|
+
* follows, while the receiver parameter carries it only when the iteration
|
|
1423
|
+
* reads the constant's own value or member path. Over a DERIVED receiver
|
|
1424
|
+
* (`[...ITEMS].forEach(…)`) the parameter names the fresh array the derivation
|
|
1425
|
+
* built, which is mutable, and only the narrower assignability question remains
|
|
1426
|
+
* — see `MUTABLE_ARRAY_PARAMETER_METHODS` and `iterationBindingsOf`.
|
|
1052
1427
|
* `reduce`/`reduceRight` push it to fourth, having spent the first position on
|
|
1053
1428
|
* the accumulator.
|
|
1054
1429
|
*
|
|
1055
1430
|
* `flatMap` is listed for its ELEMENTS alone. Its lib signature declares the
|
|
1056
|
-
* parameter `T[]` where every sibling
|
|
1057
|
-
* against `lib.es2020` — so `arr[0].n = 2` inside a
|
|
1058
|
-
* TS2540 for an input that compiled, while a mutating
|
|
1059
|
-
* parameter compiles unchanged.
|
|
1060
|
-
* second half, which enrolment
|
|
1431
|
+
* parameter `T[]` where every sibling on `ReadonlyArray` declares it
|
|
1432
|
+
* `readonly T[]` — measured against `lib.es2020` — so `arr[0].n = 2` inside a
|
|
1433
|
+
* `flatMap` callback is TS2540 for an input that compiled, while a mutating
|
|
1434
|
+
* method called through the parameter compiles unchanged.
|
|
1435
|
+
* `MUTABLE_ARRAY_PARAMETER_METHODS` carries that second half, which enrolment
|
|
1436
|
+
* alone cannot express (Issue #2340).
|
|
1061
1437
|
*/
|
|
1062
1438
|
const ARRAY_PARAMETER_INDEX_BY_METHOD = new Map([
|
|
1063
1439
|
['forEach', 2],
|
|
@@ -1074,7 +1450,14 @@ const ARRAY_PARAMETER_INDEX_BY_METHOD = new Map([
|
|
|
1074
1450
|
['reduceRight', 3],
|
|
1075
1451
|
]);
|
|
1076
1452
|
/**
|
|
1077
|
-
* The methods above whose receiver-array parameter is declared MUTABLE `T[]
|
|
1453
|
+
* The methods above whose receiver-array parameter is declared MUTABLE `T[]`
|
|
1454
|
+
* even when the receiver itself is frozen.
|
|
1455
|
+
*
|
|
1456
|
+
* The set is keyed on the METHOD because that is the only thing that varies
|
|
1457
|
+
* once the receiver is the constant's own value. A DERIVED receiver settles the
|
|
1458
|
+
* same question by a shorter route and does not consult this set at all: the
|
|
1459
|
+
* derivation builds a fresh `Array<T>`, so every one of its methods declares
|
|
1460
|
+
* the parameter `T[]` and the whole family is mutable (Issue #2355).
|
|
1078
1461
|
*
|
|
1079
1462
|
* Enrolling that parameter answers two questions at once, and each needs its own
|
|
1080
1463
|
* answer. Its ELEMENTS are frozen with the constant, so an element write through
|
|
@@ -1420,13 +1803,15 @@ const frozenAccessPathsRootedAt = (identifier, frozenValue) => {
|
|
|
1420
1803
|
};
|
|
1421
1804
|
/**
|
|
1422
1805
|
* Every way a value derived from this one keeps the constant's ELEMENT types:
|
|
1423
|
-
* a copy of it, the
|
|
1424
|
-
*
|
|
1425
|
-
*
|
|
1426
|
-
*
|
|
1806
|
+
* a copy of it, the mapper result it is handed back into, the
|
|
1807
|
+
* `Object.values`/`Object.entries` array over it, the iterator its own
|
|
1808
|
+
* `values`/`entries` hands back, and the collection built out of it. Each
|
|
1809
|
+
* resolver returns an ANCESTOR of the node it is given, which is what lets the
|
|
1810
|
+
* iteration walk follow them transitively without looping.
|
|
1427
1811
|
*/
|
|
1428
1812
|
const DERIVATION_RESOLVERS = [
|
|
1429
1813
|
copyExpressionOf,
|
|
1814
|
+
mapperResultOf,
|
|
1430
1815
|
elementProjectionCallOf,
|
|
1431
1816
|
iteratorProjectionCallOf,
|
|
1432
1817
|
elementCollectionOf,
|
|
@@ -1446,12 +1831,18 @@ const DERIVATION_RESOLVERS = [
|
|
|
1446
1831
|
* absent from the resolver itself, its result being `string[]` whatever the
|
|
1447
1832
|
* argument holds.
|
|
1448
1833
|
*
|
|
1834
|
+
* A mapper RESULT is followed for that same TYPE half: the copy it builds is
|
|
1835
|
+
* typed from what the callback hands back, so a constant reached through that
|
|
1836
|
+
* return narrows the copy's elements and `const xs = ITEMS.map(() => OTHER);
|
|
1837
|
+
* xs.push({ n: 9 });` is TS2322 for an input that compiled (Issue #2356).
|
|
1838
|
+
*
|
|
1449
1839
|
* The remaining iteration resolvers stay absent: an ITERATOR is not a value a
|
|
1450
1840
|
* binding is written through, and `new Set(ITEMS)` reshapes the constant into a
|
|
1451
1841
|
* collection whose own question the iteration walk asks.
|
|
1452
1842
|
*/
|
|
1453
1843
|
const ALIAS_DERIVATION_RESOLVERS = [
|
|
1454
1844
|
copyExpressionOf,
|
|
1845
|
+
mapperResultOf,
|
|
1455
1846
|
elementExpressionOf,
|
|
1456
1847
|
elementProjectionCallOf,
|
|
1457
1848
|
];
|
|
@@ -1541,11 +1932,12 @@ const parameterBindingsOf = (callback, positions, declaredVariablesOf) => {
|
|
|
1541
1932
|
* passed by name is declared elsewhere, where its parameter carries whatever
|
|
1542
1933
|
* type that declaration gives it rather than one read off the constant.
|
|
1543
1934
|
*
|
|
1544
|
-
* `iteratesConstantValue` says whether `iterable` is the constant's own value
|
|
1545
|
-
*
|
|
1546
|
-
* ARRAY parameter
|
|
1547
|
-
*
|
|
1548
|
-
* case
|
|
1935
|
+
* `iteratesConstantValue` says whether `iterable` is the constant's own value or
|
|
1936
|
+
* member path rather than something derived from it. It decides the TERMS the
|
|
1937
|
+
* receiver ARRAY parameter is enrolled on rather than whether to enrol it: the
|
|
1938
|
+
* element parameter is typed from the constant either way, while the array
|
|
1939
|
+
* parameter is the constant itself only in the first case and a fresh mutable
|
|
1940
|
+
* array in the second — see `iterationBindingsOf`.
|
|
1549
1941
|
*/
|
|
1550
1942
|
const bindingsOfIterationOver = (iterable, declaredVariablesOf, iteratesConstantValue) => {
|
|
1551
1943
|
// The member path is resolved first because the iterated expression is
|
|
@@ -1596,9 +1988,7 @@ const bindingsOfIterationOver = (iterable, declaredVariablesOf, iteratesConstant
|
|
|
1596
1988
|
if (!callback || !isFunctionValue(callback)) {
|
|
1597
1989
|
return [];
|
|
1598
1990
|
}
|
|
1599
|
-
const arrayIndex =
|
|
1600
|
-
? ARRAY_PARAMETER_INDEX_BY_METHOD.get(method)
|
|
1601
|
-
: undefined;
|
|
1991
|
+
const arrayIndex = ARRAY_PARAMETER_INDEX_BY_METHOD.get(method);
|
|
1602
1992
|
return parameterBindingsOf(callback, [
|
|
1603
1993
|
{ index: elementIndex, breaksOnAnyMutatingMethod: true },
|
|
1604
1994
|
...(arrayIndex === undefined
|
|
@@ -1606,7 +1996,8 @@ const bindingsOfIterationOver = (iterable, declaredVariablesOf, iteratesConstant
|
|
|
1606
1996
|
: [
|
|
1607
1997
|
{
|
|
1608
1998
|
index: arrayIndex,
|
|
1609
|
-
breaksOnAnyMutatingMethod:
|
|
1999
|
+
breaksOnAnyMutatingMethod: iteratesConstantValue &&
|
|
2000
|
+
!MUTABLE_ARRAY_PARAMETER_METHODS.has(method),
|
|
1610
2001
|
},
|
|
1611
2002
|
]),
|
|
1612
2003
|
], declaredVariablesOf);
|
|
@@ -1642,15 +2033,19 @@ const bindingsOfIterationOver = (iterable, declaredVariablesOf, iteratesConstant
|
|
|
1642
2033
|
* path as well as from the reference, since the value a derivation is taken
|
|
1643
2034
|
* from is routinely a PROPERTY of the constant — see `accessPathsRootedAt`.
|
|
1644
2035
|
*
|
|
1645
|
-
* The receiver ARRAY parameter is enrolled for
|
|
1646
|
-
*
|
|
1647
|
-
*
|
|
1648
|
-
*
|
|
1649
|
-
*
|
|
1650
|
-
*
|
|
1651
|
-
*
|
|
1652
|
-
*
|
|
1653
|
-
*
|
|
2036
|
+
* The receiver ARRAY parameter is enrolled for a derived iterable as well as for
|
|
2037
|
+
* the constant's own value, but on different terms, because the two break on
|
|
2038
|
+
* different things. The constant's own value hands the callback the frozen
|
|
2039
|
+
* array, so any mutating call through the parameter is a TS2339. A derivation
|
|
2040
|
+
* hands it the fresh MUTABLE array the derivation built, where there is no
|
|
2041
|
+
* TS2339 to have — and the fresh array's ELEMENTS are still the constant's, so
|
|
2042
|
+
* the assertion narrows what it accepts and a call that INSERTS a value from
|
|
2043
|
+
* outside the constant is rejected:
|
|
2044
|
+
* `[...ITEMS].forEach((item, index, arr) => { arr.push({ n: 3 }); })` compiles
|
|
2045
|
+
* and is TS2322 under the assertion (Issue #2355). That is the same narrow
|
|
2046
|
+
* question `MUTABLE_ARRAY_PARAMETER_METHODS` poses for `flatMap`, so the derived
|
|
2047
|
+
* enrolment reuses it, keeping `arr.sort()` and `arr.push(item)` fixable while
|
|
2048
|
+
* declining the insertion.
|
|
1654
2049
|
*/
|
|
1655
2050
|
const iterationBindingsOf = (identifier, declaredVariablesOf) => {
|
|
1656
2051
|
const value = outermostValueOf(identifier);
|
package/package.json
CHANGED
package/release-manifest.json
CHANGED
|
@@ -1,4 +1,28 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "1.21.17",
|
|
4
|
+
"date": "2026-09-07T12:35:50.668Z",
|
|
5
|
+
"rules": [
|
|
6
|
+
{
|
|
7
|
+
"name": "enforce-memoize-async",
|
|
8
|
+
"changeType": "fix",
|
|
9
|
+
"issues": [
|
|
10
|
+
2359
|
|
11
|
+
],
|
|
12
|
+
"summary": "tell a finalizer's log from a hand-back (closes #2359)"
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"name": "global-const-style",
|
|
16
|
+
"changeType": "fix",
|
|
17
|
+
"issues": [
|
|
18
|
+
2355,
|
|
19
|
+
2356,
|
|
20
|
+
2357
|
|
21
|
+
],
|
|
22
|
+
"summary": "read the element's type through type positions, not only value ones (closes #2357); track a copy against every constant whose type reaches it (closes #2356); withhold when a callback inserts through the receiver parameter (closes #2355)"
|
|
23
|
+
}
|
|
24
|
+
]
|
|
25
|
+
},
|
|
2
26
|
{
|
|
3
27
|
"version": "1.21.16",
|
|
4
28
|
"date": "2026-09-07T07:42:18.078Z",
|