tsc-ts 0.1.2 → 0.1.4

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/README.md CHANGED
@@ -33,7 +33,7 @@ npx tsc-ts --noEmit src/index.ts
33
33
 
34
34
  `tsc-ts --api` serves the TypeScript API over stdin and stdout, like `tsc --api`. On Windows, `--api --pipe \\.\pipe\<name>` serves it over a named pipe it creates, as `tsc --api --pipe` does, for clients that connect to the pipe (such as scriptc under Node.js); only the native executable supports `--pipe`. On macOS and Linux, `--api --listen <path>` serves it on a Unix socket it creates, with mode 0600, to one client of the same user at a time, each until the client closes its end. Each client gets the answers a new `tsc-ts --api` process would give, except that symbol ids continue from earlier clients, and the server reads every file through the client again, but it keeps parsed files between clients and reuses those whose text is unchanged, along with the encoding `getSourceFile` sends for each, so a client that opens the same project again loads it faster. The server exits when no client connects for 60 seconds (`--idle-exit <seconds>` sets the time), or after a client when its resident memory is above 2.5 times what it held after its first client (`TSC_TS_LISTEN_MEMORY` sets the limit in megabytes). To stop, it removes the socket file, serves the clients that had connected, and exits. It replaces a socket file that no server answers on, such as one left by a server that was killed. Only the native executable supports `--listen`. With `--default-libraries-from-disk`, the server reads the files in its default library directory (`TSC_TS_LIB_DIR`, or the executable's directory) from the disk itself, even when the client handles file system calls (`--callbacks`), which saves a round trip to the client for each library file.
35
35
 
36
- `--checkers N` checks the program on N threads. Unless `--checkers` or `--singleThreaded` is given, the native executable uses one checker per CPU it may run on (up to 16), and at least tsgo's four; it uses fewer than the CPUs, but never fewer than four, when the estimated peak memory of more checkers would not fit 85% of the machine's memory (`TSC_TS_CHECKERS=fixed` keeps tsgo's four), and checkers beyond the fourth stop taking files, one at a time, when the process's memory reaches that share; it shares one program between its threads and also parses and binds the program's files on them. Unless `--singleThreaded` is given, the program's files are parsed and bound on threads also with `--checkers 1` and in the first build of `--watch`, with or without build info, as tsgo does. Files that share costly declarations, such as a large generated schema and the few files importing it, are checked together on one checker. The checker threads share the results they compute where the run expects that to pay: at 12 or more checkers, when 40% of the program's files are declarations or come from `node_modules` and the checkers' ranges mostly resolve the same large libraries. `TSC_TS_SHARE=1` turns sharing on for any program and `TSC_TS_SHARE=0` keeps it off, and a run whose sharing stops paying stops sharing its results as it goes. A sharing run may also bound how many results it posts at a time, which costs a few percent on a project that does not gain by it and saves more on one that does: the learned choice among a project's runs, off until its record shows it faster, and `TSC_TS_SHARE_BOUNDED=1` turns it on for any program while `TSC_TS_SHARE_BOUNDED=0` keeps it off. `TSC_TS_AUTO=0` keeps the choices of a run before automatic selection (fewer checkers where the ranges resolve large libraries, no sharing), and `TSC_TS_AUTO=static` keeps the rules and the in-run switches but reads and writes no records, and `TSC_TS_AUTO=explore` adds the experimental exploration described below. Unless `--singleThreaded` is given, the files are parsed on the CPUs the process may run on, but one, whatever the checker count, and `TSC_TS_PARSE_THREADS` sets how many. `--extendedDiagnostics` prints the checker threads, the sharing and the parse threads after the statistics table, under `Suggestions:`, with where each value came from (`pinned: --checkers`, `default`, `automatic: ...`, `learned: check 14% faster over 3 runs`, `exploring`, `control`) and how to pin it; `--build` prints one section per project it builds. The section also names the run's record under `Records:`. Each run that checks a program writes what it measured to `~/.cache/tsc-ts/runs-v1/` (`TSC_TS_CACHE_DIR` sets the directory, `TSC_TS_CACHE=0` turns records off), for later runs of the same project on the same machine to read. A later run chooses from that record where the rules left a setting open: a configuration the samples show faster than the static choice — by 3% or twice the standard error of the gains each of its samples makes against the nearest run of the other arm, on three samples of each taken while the machine was not busy — is adopted, every sixteenth run (and the first after two weeks) re-measures the static choice as a control run, and an adopted configuration whose last three samples all fall behind the control runs reverts at once. A run never tries a configuration that could make itself slower: it adopts only what the samples already show faster, from `tsc-ts doctor`, from a pinned run or from a run that already used it. `TSC_TS_AUTO=explore` turns on an experimental alternative in which a run also tries a setting the signals leave uncertain: one run in eight tries three quarters of the count where the library text puts it near the cut, sharing at eight checkers near the rule's limits, or half the parse threads, and such a run is slower than the configuration it would have used. A program whose shape changed by more than 15% discards the record's samples. Watch mode, a `--build` of several projects at once, `--singleThreaded`, Node builds and `TSC_TS_AUTO=0` or `static` keep no record, and the section says why; a run with `CI` set reads and adopts but never explores. `tsc-ts doctor [-p <tsconfig>] [--runs N] [--budget SECONDS] [--emit] [--json] [--no-save]` measures these settings on this machine: it runs the project's check under the default configuration, with sharing on, with up to 16 checkers, with both, and with half the parse threads, compares the phase times by the same rule automatic selection uses, and prints what it recommends with the evidence for it. Each measured run is a child process, so it runs as a user's run would, and it writes what it measured to the file named by `TSC_TS_DOCTOR_REPORT`; runs without the variable are unaffected. Doctor stores its samples in the record with role `doctor`, unless `--no-save` is given. The command is experimental: the configurations it compares and what it recommends may change. Under Node.js, where every thread builds its own copy of the program, tsc-ts checks on one thread unless `--checkers` is given. `--incremental`, `--watch` and `--build` always check on one thread. The diagnostics are the same as with one checker.
36
+ `--checkers N` checks the program on N threads. Unless `--checkers` or `--singleThreaded` is given, the native executable uses one checker per CPU it may run on (up to 16), and at least tsgo's four; it uses fewer than the CPUs, but never fewer than four, when the estimated peak memory of more checkers would not fit 85% of the machine's memory (`TSC_TS_CHECKERS=fixed` keeps tsgo's four), and checkers beyond the fourth stop taking files, one at a time, when the process's memory reaches that share; it shares one program between its threads and also parses and binds the program's files on them. Unless `--singleThreaded` is given, the program's files are parsed and bound on threads also with `--checkers 1` and in the first build of `--watch`, with or without build info, as tsgo does. Files that share costly declarations, such as a large generated schema and the few files importing it, are checked together on one checker. The checker threads share the results they compute where the run expects that to pay: at 12 or more checkers, when 40% of the program's files are declarations or come from `node_modules` and the checkers' ranges mostly resolve the same large libraries. `TSC_TS_SHARE=1` turns sharing on for any program and `TSC_TS_SHARE=0` keeps it off, and a run whose sharing stops paying stops sharing its results as it goes. A sharing run may also bound how many results it posts at a time, which costs a few percent on a project that does not gain by it and saves more on one that does: the learned choice among a project's runs, off until its record shows it faster, and `TSC_TS_SHARE_BOUNDED=1` turns it on for any program while `TSC_TS_SHARE_BOUNDED=0` keeps it off. `TSC_TS_AUTO=0` keeps the choices of a run before automatic selection (fewer checkers where the ranges resolve large libraries, no sharing), and `TSC_TS_AUTO=static` keeps the rules and the in-run switches but reads and writes no records, and `TSC_TS_AUTO=explore` adds the experimental exploration described below. Unless `--singleThreaded` is given, the files are parsed on the CPUs the process may run on, but one, whatever the checker count, and `TSC_TS_PARSE_THREADS` sets how many. `--extendedDiagnostics` prints the checker threads, the sharing and the parse threads after the statistics table, under `Suggestions:`, with where each value came from (`pinned: --checkers`, `default`, `automatic: ...`, `learned: check 14% faster over 3 runs`, `exploring`, `control`) and how to pin it; `--build` prints one section per project it builds. The section also names the run's record under `Records:`. Each run that checks a program writes what it measured to `~/.cache/tsc-ts/runs-v1/` (`TSC_TS_CACHE_DIR` sets the directory, `TSC_TS_CACHE=0` turns records off), for later runs of the same project on the same machine to read. A later run chooses from that record where the rules left a setting open: a configuration the samples show faster than the static choice — by 3% or twice the standard error of the gains each of its samples makes against the nearest run of the other arm, on three samples of each taken while the machine was not busy — is adopted, every sixteenth run (and the first after two weeks) re-measures the static choice as a control run, and an adopted configuration whose last three samples all fall behind the control runs reverts at once. A run never tries a configuration that could make itself slower: it adopts only what the samples already show faster, from `tsc-ts doctor`, from a pinned run or from a run that already used it. `TSC_TS_AUTO=explore` turns on an experimental alternative in which a run also tries a setting the signals leave uncertain: one run in eight tries three quarters of the count where the library text puts it near the cut, sharing at eight checkers near the rule's limits, or half the parse threads, and such a run is slower than the configuration it would have used. A program whose shape changed by more than 15% discards the record's samples. Watch mode, a `--build` of several projects at once, `--singleThreaded`, Node builds and `TSC_TS_AUTO=0` or `static` keep no record, and the section says why; a run with `CI` set reads and adopts but never explores. `tsc-ts doctor [-p <tsconfig>] [--runs N] [--budget SECONDS] [--emit] [--json] [--no-save]` measures these settings on this machine: it runs the project's check under the default configuration, with sharing on, with up to 16 checkers, with both, and with half the parse threads, compares the phase times by the same rule automatic selection uses, and prints what it recommends with the evidence for it. Each measured run is a child process, so it runs as a user's run would, and it writes what it measured to the file named by `TSC_TS_DOCTOR_REPORT`; runs without the variable are unaffected. Doctor stores its samples in the record with role `doctor`, unless `--no-save` is given. The command is experimental: the configurations it compares and what it recommends may change. Under Node.js, where every thread builds its own copy of the program, tsc-ts checks on one thread unless `--checkers` is given. `--incremental`, `--watch` and `--build` always check on one thread. The diagnostics are the same as with one checker, except for the two a check reports when a type instantiation reaches the depth limit: TS5115 on the instantiations, and TS2344 on the constraint check that stops there. Whether a file reaches the limit depends on the declarations its checker has already resolved, and which files a checker has is decided while checking runs, so those two diagnostics are best effort with more than one checker: one run can report them in different files than another run, and a run with one checker can report them where a run with several does not.
37
37
 
38
38
  The native executable never frees syntax trees, types, signatures or symbols: it keeps them until it exits, which saves the cost of tracking them during a build. A one-shot build is unaffected, but a long-running process grows with each rebuild or project update. Native `--watch` (also with `--build`) runs in a child process, and when a change arrives while that process holds more than 2.5 times the memory it held after its first build, a new process replaces it and builds the change, reporting what the old process would have. `TSC_TS_WATCH_MEMORY` sets that limit in megabytes, and `TSC_TS_WATCH_MEMORY=0` turns the replacement off. A `--api` process grows with each project update; restart it to reclaim the memory.
39
39
 
@@ -33,7 +33,7 @@ import { couldContainTypeVariables, instantiateSignature } from './instantiation
33
33
  import { getContextualSignatureForFunctionLikeDeclaration, getInferenceContext, instantiateContextualType, } from './checker-contextual.js';
34
34
  import { checkJSDocFullSignature, checkJSDocFullSignatureType, checkUnmatchedJSDocParameters, getFullSignatureTypeNode, } from './checker-jsdoc.js';
35
35
  import { getEffectiveCheckNode, isFunctionLike, nodeIsMissing } from './ast-utilities.js';
36
- import { getFlowTypeOfReferenceAt, isConstantReference, isReachableFlowNode, isSymbolAssigned, } from './checker-narrowing.js';
36
+ import { endFlowAnalysisStop, getFlowTypeOfReferenceAt, isConstantReference, isReachableFlowNode, isSymbolAssigned, } from './checker-narrowing.js';
37
37
  import { FlowNode } from './flow.js';
38
38
  import { checkAwaitedType, checkGeneratorInstantiationAssignabilityToReturnType, checkIteratedTypeOrElementType, createGeneratorType, createPromiseReturnType, createPromiseType, getAwaitedTypeEx, getAwaitedTypeNoAlias, getGlobalPromiseTypeChecked, getIterationTypeOfGeneratorFunctionReturnType, getIterationTypeOfIterable, getIterationTypesOfGeneratorFunctionReturnType, getIterationTypesOfIterable, getPromisedTypeOfPromise, isReferenceToType, unwrapAwaitedType, unwrapReturnType, } from './checker-iteration.js';
39
39
  import { typeToString } from './type-printer.js';
@@ -274,6 +274,10 @@ export function checkFunctionLikeBody(c, node) {
274
274
  if (returnType !== undefined)
275
275
  checkReturnExpression(c, node, unwrapReturnType(c, returnType, functionFlags), body, body, exprType, false);
276
276
  }
277
+ // A body too large for control flow analysis stops narrowing in the rest of that body only
278
+ // (flowAnalysisDisabled, checker-narrowing.ts); what follows the body starts over. Inferring the
279
+ // return type above checks the body's return expressions, which can reach the limit.
280
+ endFlowAnalysisStop(c, body);
277
281
  }
278
282
  /** Checks a generator's or async function's return type annotation. */
279
283
  function checkSignatureReturnType(c, node, returnTypeNode, functionFlags) {
@@ -513,9 +513,13 @@ class InferenceState {
513
513
  const sourceAlias = source.alias;
514
514
  const targetAlias = target.alias;
515
515
  if (sourceAlias !== undefined && targetAlias !== undefined && sourceAlias.symbol === targetAlias.symbol) {
516
- // Instantiations of the same alias infer between their type arguments, once for each pair of types, as tsc does.
516
+ // Instantiations of the same alias infer between their type arguments, once for each pair of types, as tsc
517
+ // does. The pair is memoized, which keeps inference from type arguments from repeating exponentially (tsgo
518
+ // #64378), but it does not take part in invokeOnce's recursion-identity check: the identities of a recursive
519
+ // alias repeat through its properties, and below such a property that check reports a circularity and skips
520
+ // an inference that tsc 7.0.2 makes (#418).
517
521
  if (sourceAlias.typeArguments.length !== 0 || targetAlias.typeArguments.length !== 0)
518
- this.invokeOnce(source, target, 0 /* InferenceAction.AliasTypeArguments */);
522
+ this.invokeOnce(source, target, 0 /* InferenceAction.AliasTypeArguments */, false);
519
523
  return;
520
524
  }
521
525
  if (source === target && (source.flags & 402653184 /* TypeFlags.UnionOrIntersection */) !== 0) {
@@ -718,8 +722,13 @@ class InferenceState {
718
722
  else
719
723
  this.inferFromTypes(source, target);
720
724
  }
721
- /** Runs `action` once per source and target pair, stopping at circularities. */
722
- invokeOnce(source, target, action) {
725
+ /**
726
+ * Runs `action` once per source and target pair, stopping at circularities. With `checkRecursion` false, the pair
727
+ * is still memoized but its action neither joins the recursion stacks nor stops at the recursion identities the
728
+ * stacks hold; alias type arguments, which infer only between the type arguments of two instantiations of one
729
+ * declaration, are such a pair.
730
+ */
731
+ invokeOnce(source, target, action, checkRecursion = true) {
723
732
  // As in tsc, a pair is visited once for each priority and variance it is inferred with.
724
733
  const mode = visitMode(this.priority, this.contravariant, this.bivariant);
725
734
  const key = visitKey(source, target);
@@ -735,19 +744,21 @@ class InferenceState {
735
744
  const caches = this.c.inferenceCaches;
736
745
  const stackDepth = this.stackDepth;
737
746
  const start = this.stackBase;
738
- const end = start + stackDepth + 1;
739
- setStackEntry(caches.invokeSourceStack, end - 1, source);
740
- setStackEntry(caches.invokeTargetStack, end - 1, target);
741
- setNumberEntry(caches.invokeModeStack, end - 1, mode);
742
- setNumberEntry(caches.invokeSourceIdentities, end - 1, 0);
743
- setNumberEntry(caches.invokeTargetIdentities, end - 1, 0);
744
- this.stackDepth = stackDepth + 1;
745
- caches.invokeStackDepth = end;
746
- if (isDeeplyNestedStackEntry(this.c, caches.invokeSourceStack, caches.invokeSourceIdentities, start, end, 2))
747
- this.expandingFlags |= 1 /* ExpandingFlags.Source */;
748
- if (isDeeplyNestedStackEntry(this.c, caches.invokeTargetStack, caches.invokeTargetIdentities, start, end, 2))
749
- this.expandingFlags |= 2 /* ExpandingFlags.Target */;
750
- if (this.expandingFlags !== 3 /* ExpandingFlags.Both */) {
747
+ const end = start + stackDepth + (checkRecursion ? 1 : 0);
748
+ if (checkRecursion) {
749
+ setStackEntry(caches.invokeSourceStack, end - 1, source);
750
+ setStackEntry(caches.invokeTargetStack, end - 1, target);
751
+ setNumberEntry(caches.invokeModeStack, end - 1, mode);
752
+ setNumberEntry(caches.invokeSourceIdentities, end - 1, 0);
753
+ setNumberEntry(caches.invokeTargetIdentities, end - 1, 0);
754
+ this.stackDepth = stackDepth + 1;
755
+ caches.invokeStackDepth = end;
756
+ if (isDeeplyNestedStackEntry(this.c, caches.invokeSourceStack, caches.invokeSourceIdentities, start, end, 2))
757
+ this.expandingFlags |= 1 /* ExpandingFlags.Source */;
758
+ if (isDeeplyNestedStackEntry(this.c, caches.invokeTargetStack, caches.invokeTargetIdentities, start, end, 2))
759
+ this.expandingFlags |= 2 /* ExpandingFlags.Target */;
760
+ }
761
+ if (!checkRecursion || this.expandingFlags !== 3 /* ExpandingFlags.Both */) {
751
762
  switch (action) {
752
763
  case 0 /* InferenceAction.AliasTypeArguments */:
753
764
  this.inferFromAliasTypeArguments(source, target);
@@ -768,9 +779,11 @@ class InferenceState {
768
779
  }
769
780
  else
770
781
  this.inferencePriority = -1 /* InferencePriority.Circularity */;
771
- this.stackDepth = stackDepth;
772
- caches.invokeStackDepth = end - 1;
773
- this.expandingFlags = saveExpandingFlags;
782
+ if (checkRecursion) {
783
+ this.stackDepth = stackDepth;
784
+ caches.invokeStackDepth = end - 1;
785
+ this.expandingFlags = saveExpandingFlags;
786
+ }
774
787
  this.setVisited(source, target, key, mode, this.inferencePriority);
775
788
  this.inferencePriority = Math.min(this.inferencePriority, saveInferencePriority);
776
789
  }
@@ -901,9 +901,9 @@ function getTargetOfImportClause(c, node) {
901
901
  function getTargetOfModuleDefault(c, moduleSymbol, node, dontResolveAlias) {
902
902
  const file = getSourceFileDeclaration(moduleSymbol);
903
903
  const specifier = getModuleSpecifierForImportOrExport(node);
904
- const exportDefaultSymbol = isShorthandAmbientModuleSymbol(moduleSymbol)
905
- ? undefined
906
- : resolveExportByName(c, moduleSymbol, "default" /* InternalSymbolName.Default */, node, dontResolveAlias);
904
+ // Also when the value declaration is a shorthand ambient module (`declare module "m";`): a declaration of
905
+ // the same module with a body merges into the symbol and can export a default.
906
+ const exportDefaultSymbol = resolveExportByName(c, moduleSymbol, "default" /* InternalSymbolName.Default */, node, dontResolveAlias);
907
907
  if (specifier === undefined)
908
908
  return exportDefaultSymbol;
909
909
  let attributes;
@@ -112,6 +112,13 @@ export declare function getEvolvingArrayType(c: Checker, elementType: Type): Typ
112
112
  export declare function isEvolvingArrayOperationTarget(c: Checker, node: Node): boolean;
113
113
  /** The array type an evolving array settles on; `any[]` while it has no elements. */
114
114
  export declare function finalizeEvolvingArrayType(c: Checker, t: Type): Type;
115
+ /**
116
+ * Ends the narrowing stopped in `body`, whose checks have finished: what follows the body starts over.
117
+ * A walk that stopped in another body keeps its body's narrowing stopped.
118
+ */
119
+ export declare function endFlowAnalysisStop(c: Checker, body: Node): void;
120
+ /** Whether `node` is the body of a function or a module, where flow analysis starts over (flowAnalysisDisabled). */
121
+ export declare function isFunctionOrModuleBlock(node: Node): boolean;
115
122
  /**
116
123
  * The type of `symbol` at `location`. A destructured variable of a const declaration or unassigned
117
124
  * parameter is narrowed by checks of its siblings: in `const { kind, payload } = action`, testing
@@ -182,7 +182,7 @@ export function getFlowTypeOfReference(c, reference, declaredType, initialType,
182
182
  }
183
183
  /** `getFlowTypeOfReference` at `flowNode`, for pseudo-references such as binding patterns. */
184
184
  export function getFlowTypeOfReferenceAt(c, reference, declaredType, initialType, flowContainer, flowNode) {
185
- if (c.flowAnalysisDisabled)
185
+ if (c.flowAnalysisDisabled && isInFlowAnalysisStop(c, reference))
186
186
  return c.errorType;
187
187
  if (flowNode === undefined)
188
188
  return declaredType;
@@ -211,7 +211,7 @@ export function getFlowTypeOfReferenceAt(c, reference, declaredType, initialType
211
211
  f.steps >= minFlowWalkSteps &&
212
212
  !f.incomplete &&
213
213
  !f.positional &&
214
- !c.flowAnalysisDisabled &&
214
+ !(c.flowAnalysisDisabled && isInFlowAnalysisStop(c, reference)) &&
215
215
  c.resolutionCycleCount === cycles) {
216
216
  const walk = f.walk;
217
217
  if (walk !== undefined) {
@@ -248,6 +248,7 @@ function complete(f, t) {
248
248
  export function getTypeAtFlowNode(c, f, start) {
249
249
  if (f.depth === 2000) {
250
250
  c.flowAnalysisDisabled = true;
251
+ c.flowAnalysisDisabledBody = getContainingFlowBody(f.reference);
251
252
  reportFlowControlError(c, f.reference);
252
253
  return complete(f, c.errorType);
253
254
  }
@@ -2759,17 +2760,43 @@ function getContextFreeTypeOfExpression(c, node) {
2759
2760
  links.contextFreeType = t;
2760
2761
  return t;
2761
2762
  }
2762
- function reportFlowControlError(c, node) {
2763
+ /** The body whose walk reached the flow limit, or undefined when no body is stopped. */
2764
+ function getContainingFlowBody(node) {
2763
2765
  let block = node;
2764
2766
  while (block !== undefined && !isFunctionOrModuleBlock(block))
2765
2767
  block = block.parent;
2768
+ return block;
2769
+ }
2770
+ /** Whether `node` is in the body whose walk reached the flow limit, or in a body it contains. */
2771
+ function isInFlowAnalysisStop(c, node) {
2772
+ const stop = c.flowAnalysisDisabledBody;
2773
+ if (stop === undefined)
2774
+ return false;
2775
+ for (let n = node; n !== undefined; n = n.parent)
2776
+ if (n === stop)
2777
+ return true;
2778
+ return false;
2779
+ }
2780
+ /**
2781
+ * Ends the narrowing stopped in `body`, whose checks have finished: what follows the body starts over.
2782
+ * A walk that stopped in another body keeps its body's narrowing stopped.
2783
+ */
2784
+ export function endFlowAnalysisStop(c, body) {
2785
+ if (c.flowAnalysisDisabledBody !== body)
2786
+ return;
2787
+ c.flowAnalysisDisabled = false;
2788
+ c.flowAnalysisDisabledBody = undefined;
2789
+ }
2790
+ function reportFlowControlError(c, node) {
2791
+ const block = getContainingFlowBody(node);
2766
2792
  if (block === undefined)
2767
2793
  return;
2768
2794
  const file = getSourceFileOfNode(node);
2769
2795
  const span = getRangeOfTokenAtPosition(file, getStatements(block).pos);
2770
2796
  c.addDiagnostic(new Diagnostic(file.diagnosticFile(), span.pos, span.end, 724 /* Diagnostics.The_containing_function_or_module_body_is_too_large_for_control_flow_analysis */, []));
2771
2797
  }
2772
- function isFunctionOrModuleBlock(node) {
2798
+ /** Whether `node` is the body of a function or a module, where flow analysis starts over (flowAnalysisDisabled). */
2799
+ export function isFunctionOrModuleBlock(node) {
2773
2800
  return (node.kind === 308 /* SyntaxKind.SourceFile */ ||
2774
2801
  node.kind === 270 /* SyntaxKind.ModuleBlock */ ||
2775
2802
  (node.kind === 243 /* SyntaxKind.Block */ && node.parent !== undefined && isFunctionLike(node.parent)));
@@ -43,6 +43,11 @@ import { checkContextualDeprecations, checkDeprecatedTypeReference } from './che
43
43
  * identifiers, which need the whole file checked first.
44
44
  */
45
45
  export function checkSourceFile(c, file, checkUnused = false) {
46
+ // A source file is a module body: narrowing stopped by a body too large for control flow analysis
47
+ // (flowAnalysisDisabled, checker-narrowing.ts) starts over in the next check. A checker checks many files
48
+ // (parallel-check.ts), so its diagnostics of a file must not depend on the files it checked before it.
49
+ c.flowAnalysisDisabled = false;
50
+ c.flowAnalysisDisabledBody = undefined;
46
51
  const links = c.nodeLinks(file);
47
52
  if ((links.flags & 1 /* NodeCheckFlags.TypeChecked */) === 0) {
48
53
  links.flags |= 1 /* NodeCheckFlags.TypeChecked */;
@@ -52,6 +57,9 @@ export function checkSourceFile(c, file, checkUnused = false) {
52
57
  links.flags |= 2 /* NodeCheckFlags.UnusedChecked */;
53
58
  checkUnusedIdentifiers(c, file);
54
59
  }
60
+ // Every body of the file has finished its checks; a stop recorded for one of them ends with the file.
61
+ c.flowAnalysisDisabled = false;
62
+ c.flowAnalysisDisabledBody = undefined;
55
63
  }
56
64
  function checkSourceFileWorker(c, file) {
57
65
  c.fileBeingChecked = file;
@@ -89,6 +97,10 @@ function checkSourceFileWorker(c, file) {
89
97
  */
90
98
  export function checkSourceFileRange(c, file, start, end) {
91
99
  const statements = getStatements(file).nodes;
100
+ // A range of a file's statements is part of its module body: narrowing stopped by a body too large for
101
+ // control flow analysis (flowAnalysisDisabled, checker-narrowing.ts) starts over in the next range.
102
+ c.flowAnalysisDisabled = false;
103
+ c.flowAnalysisDisabledBody = undefined;
92
104
  c.fileBeingChecked = file;
93
105
  for (let i = start; i < end; i++)
94
106
  checkSourceElement(c, statements[i]);
@@ -106,6 +118,9 @@ export function checkSourceFileRange(c, file, start, end) {
106
118
  produceDeferredDiagnostics(c);
107
119
  // Deferred nodes of a later range are queued again (checkNodeDeferred).
108
120
  c.nodeLinks(file).deferredNodes = undefined;
121
+ // The range's bodies have finished their checks; a stop recorded for one of them ends with the range.
122
+ c.flowAnalysisDisabled = false;
123
+ c.flowAnalysisDisabledBody = undefined;
109
124
  return c.diagnostics.forFile(file.fileName).length > 0;
110
125
  }
111
126
  /**
@@ -7,7 +7,7 @@ import { checkIteratedTypeOrElementType } from './checker-iteration.js';
7
7
  import { checkDestructuringAssignment } from './checker-destructuring.js';
8
8
  import { checkReferenceExpression, checkTestingKnownTruthyType, checkTruthinessExpression, isTypeEqualityComparableTo, } from './checker-operators.js';
9
9
  import { checkTypeComparableTo, isTypeAssignableTo } from './relations.js';
10
- import { isReachableFlowNode } from './checker-narrowing.js';
10
+ import { endFlowAnalysisStop, isFunctionOrModuleBlock, isReachableFlowNode } from './checker-narrowing.js';
11
11
  import { getIndexType, isTypeAssignableToKindEx as isTypeAssignableToKind } from './checker-type-operators.js';
12
12
  import { getExtractStringType } from './checker-symbol-types.js';
13
13
  import { checkSourceElement } from './checker-source-file.js';
@@ -21,8 +21,17 @@ import { checkExternalEmitHelpers } from './checker-emit-helpers.js';
21
21
  import { typeToString } from './type-printer.js';
22
22
  import { getNonNullableType, hasTypeFacts } from './checker-flow.js';
23
23
  export function checkBlock(c, node) {
24
- for (const s of getStatements(node).nodes)
24
+ const statements = getStatements(node).nodes;
25
+ if (!isFunctionOrModuleBlock(node)) {
26
+ for (const s of statements)
27
+ checkSourceElement(c, s);
28
+ return;
29
+ }
30
+ // A body too large for control flow analysis stops narrowing in the rest of that body only
31
+ // (flowAnalysisDisabled, checker-narrowing.ts); what follows the body starts over.
32
+ for (const s of statements)
25
33
  checkSourceElement(c, s);
34
+ endFlowAnalysisStop(c, node);
26
35
  }
27
36
  export function checkExpressionStatement(c, node) {
28
37
  checkExpression(c, getExpression(node));
@@ -466,6 +466,8 @@ export declare class Checker implements TypeChecking {
466
466
  /** `{ meta: ImportMeta }`, the type of `import.meta` for symbol queries. */
467
467
  importMetaExpressionType: Type | undefined;
468
468
  flowAnalysisDisabled: boolean;
469
+ /** The body whose walk reached the flow limit (flowAnalysisDisabled), until its checks finish. */
470
+ flowAnalysisDisabledBody: Node | undefined;
469
471
  inlineLevel: number;
470
472
  /**
471
473
  * Shared flow nodes cached by the walks in progress, with their types: the first
@@ -612,6 +612,8 @@ export class Checker {
612
612
  importMetaExpressionType = undefined;
613
613
  // Control flow analysis
614
614
  flowAnalysisDisabled = false;
615
+ /** The body whose walk reached the flow limit (flowAnalysisDisabled), until its checks finish. */
616
+ flowAnalysisDisabledBody = undefined;
615
617
  inlineLevel = 0;
616
618
  /**
617
619
  * Shared flow nodes cached by the walks in progress, with their types: the first
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tsc-ts",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "The TypeScript 7 compiler, ported to TypeScript and compiled to a native executable.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -40,12 +40,12 @@
40
40
  "node": ">=20"
41
41
  },
42
42
  "optionalDependencies": {
43
- "@tsc-ts/darwin-arm64": "0.1.2",
44
- "@tsc-ts/darwin-x64": "0.1.2",
45
- "@tsc-ts/linux-arm64-gnu": "0.1.2",
46
- "@tsc-ts/linux-arm64-musl": "0.1.2",
47
- "@tsc-ts/linux-x64-gnu": "0.1.2",
48
- "@tsc-ts/linux-x64-musl": "0.1.2",
49
- "@tsc-ts/win32-x64-msvc": "0.1.2"
43
+ "@tsc-ts/darwin-arm64": "0.1.4",
44
+ "@tsc-ts/darwin-x64": "0.1.4",
45
+ "@tsc-ts/linux-arm64-gnu": "0.1.4",
46
+ "@tsc-ts/linux-arm64-musl": "0.1.4",
47
+ "@tsc-ts/linux-x64-gnu": "0.1.4",
48
+ "@tsc-ts/linux-x64-musl": "0.1.4",
49
+ "@tsc-ts/win32-x64-msvc": "0.1.4"
50
50
  }
51
51
  }
@@ -114,6 +114,7 @@ import {
114
114
  } from './checker-jsdoc.js';
115
115
  import { getEffectiveCheckNode, isFunctionLike, nodeIsMissing } from './ast-utilities.js';
116
116
  import {
117
+ endFlowAnalysisStop,
117
118
  getFlowTypeOfReferenceAt,
118
119
  isConstantReference,
119
120
  isReachableFlowNode,
@@ -437,6 +438,10 @@ export function checkFunctionLikeBody(c: Checker, node: Node): void {
437
438
  if (returnType !== undefined)
438
439
  checkReturnExpression(c, node, unwrapReturnType(c, returnType, functionFlags), body, body, exprType, false);
439
440
  }
441
+ // A body too large for control flow analysis stops narrowing in the rest of that body only
442
+ // (flowAnalysisDisabled, checker-narrowing.ts); what follows the body starts over. Inferring the
443
+ // return type above checks the body's return expressions, which can reach the limit.
444
+ endFlowAnalysisStop(c, body);
440
445
  }
441
446
 
442
447
  /** Checks a generator's or async function's return type annotation. */
@@ -853,9 +853,13 @@ class InferenceState {
853
853
  const sourceAlias = source.alias;
854
854
  const targetAlias = target.alias;
855
855
  if (sourceAlias !== undefined && targetAlias !== undefined && sourceAlias.symbol === targetAlias.symbol) {
856
- // Instantiations of the same alias infer between their type arguments, once for each pair of types, as tsc does.
856
+ // Instantiations of the same alias infer between their type arguments, once for each pair of types, as tsc
857
+ // does. The pair is memoized, which keeps inference from type arguments from repeating exponentially (tsgo
858
+ // #64378), but it does not take part in invokeOnce's recursion-identity check: the identities of a recursive
859
+ // alias repeat through its properties, and below such a property that check reports a circularity and skips
860
+ // an inference that tsc 7.0.2 makes (#418).
857
861
  if (sourceAlias.typeArguments.length !== 0 || targetAlias.typeArguments.length !== 0)
858
- this.invokeOnce(source, target, InferenceAction.AliasTypeArguments);
862
+ this.invokeOnce(source, target, InferenceAction.AliasTypeArguments, false);
859
863
  return;
860
864
  }
861
865
  if (source === target && (source.flags & TypeFlags.UnionOrIntersection) !== 0) {
@@ -1059,8 +1063,13 @@ class InferenceState {
1059
1063
  else this.inferFromTypes(source, target);
1060
1064
  }
1061
1065
 
1062
- /** Runs `action` once per source and target pair, stopping at circularities. */
1063
- invokeOnce(source: Type, target: Type, action: InferenceAction): void {
1066
+ /**
1067
+ * Runs `action` once per source and target pair, stopping at circularities. With `checkRecursion` false, the pair
1068
+ * is still memoized but its action neither joins the recursion stacks nor stops at the recursion identities the
1069
+ * stacks hold; alias type arguments, which infer only between the type arguments of two instantiations of one
1070
+ * declaration, are such a pair.
1071
+ */
1072
+ invokeOnce(source: Type, target: Type, action: InferenceAction, checkRecursion = true): void {
1064
1073
  // As in tsc, a pair is visited once for each priority and variance it is inferred with.
1065
1074
  const mode = visitMode(this.priority, this.contravariant, this.bivariant);
1066
1075
  const key = visitKey(source, target);
@@ -1076,19 +1085,21 @@ class InferenceState {
1076
1085
  const caches = this.c.inferenceCaches;
1077
1086
  const stackDepth = this.stackDepth;
1078
1087
  const start = this.stackBase;
1079
- const end = start + stackDepth + 1;
1080
- setStackEntry(caches.invokeSourceStack, end - 1, source);
1081
- setStackEntry(caches.invokeTargetStack, end - 1, target);
1082
- setNumberEntry(caches.invokeModeStack, end - 1, mode);
1083
- setNumberEntry(caches.invokeSourceIdentities, end - 1, 0);
1084
- setNumberEntry(caches.invokeTargetIdentities, end - 1, 0);
1085
- this.stackDepth = stackDepth + 1;
1086
- caches.invokeStackDepth = end;
1087
- if (isDeeplyNestedStackEntry(this.c, caches.invokeSourceStack, caches.invokeSourceIdentities, start, end, 2))
1088
- this.expandingFlags |= ExpandingFlags.Source;
1089
- if (isDeeplyNestedStackEntry(this.c, caches.invokeTargetStack, caches.invokeTargetIdentities, start, end, 2))
1090
- this.expandingFlags |= ExpandingFlags.Target;
1091
- if (this.expandingFlags !== ExpandingFlags.Both) {
1088
+ const end = start + stackDepth + (checkRecursion ? 1 : 0);
1089
+ if (checkRecursion) {
1090
+ setStackEntry(caches.invokeSourceStack, end - 1, source);
1091
+ setStackEntry(caches.invokeTargetStack, end - 1, target);
1092
+ setNumberEntry(caches.invokeModeStack, end - 1, mode);
1093
+ setNumberEntry(caches.invokeSourceIdentities, end - 1, 0);
1094
+ setNumberEntry(caches.invokeTargetIdentities, end - 1, 0);
1095
+ this.stackDepth = stackDepth + 1;
1096
+ caches.invokeStackDepth = end;
1097
+ if (isDeeplyNestedStackEntry(this.c, caches.invokeSourceStack, caches.invokeSourceIdentities, start, end, 2))
1098
+ this.expandingFlags |= ExpandingFlags.Source;
1099
+ if (isDeeplyNestedStackEntry(this.c, caches.invokeTargetStack, caches.invokeTargetIdentities, start, end, 2))
1100
+ this.expandingFlags |= ExpandingFlags.Target;
1101
+ }
1102
+ if (!checkRecursion || this.expandingFlags !== ExpandingFlags.Both) {
1092
1103
  switch (action) {
1093
1104
  case InferenceAction.AliasTypeArguments:
1094
1105
  this.inferFromAliasTypeArguments(source, target);
@@ -1107,9 +1118,11 @@ class InferenceState {
1107
1118
  break;
1108
1119
  }
1109
1120
  } else this.inferencePriority = InferencePriority.Circularity;
1110
- this.stackDepth = stackDepth;
1111
- caches.invokeStackDepth = end - 1;
1112
- this.expandingFlags = saveExpandingFlags;
1121
+ if (checkRecursion) {
1122
+ this.stackDepth = stackDepth;
1123
+ caches.invokeStackDepth = end - 1;
1124
+ this.expandingFlags = saveExpandingFlags;
1125
+ }
1113
1126
  this.setVisited(source, target, key, mode, this.inferencePriority);
1114
1127
  this.inferencePriority = Math.min(this.inferencePriority, saveInferencePriority);
1115
1128
  }
@@ -1331,9 +1331,9 @@ function getTargetOfModuleDefault(
1331
1331
  ): Symbol | undefined {
1332
1332
  const file = getSourceFileDeclaration(moduleSymbol);
1333
1333
  const specifier = getModuleSpecifierForImportOrExport(node);
1334
- const exportDefaultSymbol = isShorthandAmbientModuleSymbol(moduleSymbol)
1335
- ? undefined
1336
- : resolveExportByName(c, moduleSymbol, InternalSymbolName.Default, node, dontResolveAlias);
1334
+ // Also when the value declaration is a shorthand ambient module (`declare module "m";`): a declaration of
1335
+ // the same module with a body merges into the symbol and can export a default.
1336
+ const exportDefaultSymbol = resolveExportByName(c, moduleSymbol, InternalSymbolName.Default, node, dontResolveAlias);
1337
1337
  if (specifier === undefined) return exportDefaultSymbol;
1338
1338
  let attributes: ImportAttributes | undefined;
1339
1339
  if (node.kind === SyntaxKind.ImportClause) attributes = getImportAttributes(node.parent!);
@@ -369,7 +369,7 @@ export function getFlowTypeOfReferenceAt(
369
369
  flowContainer: Node | undefined,
370
370
  flowNode: FlowNode | undefined,
371
371
  ): Type {
372
- if (c.flowAnalysisDisabled) return c.errorType;
372
+ if (c.flowAnalysisDisabled && isInFlowAnalysisStop(c, reference)) return c.errorType;
373
373
  if (flowNode === undefined) return declaredType;
374
374
  let f: FlowState;
375
375
  const stateDepth = c.flowStateDepth;
@@ -396,7 +396,7 @@ export function getFlowTypeOfReferenceAt(
396
396
  f.steps >= minFlowWalkSteps &&
397
397
  !f.incomplete &&
398
398
  !f.positional &&
399
- !c.flowAnalysisDisabled &&
399
+ !(c.flowAnalysisDisabled && isInFlowAnalysisStop(c, reference)) &&
400
400
  c.resolutionCycleCount === cycles
401
401
  ) {
402
402
  const walk = f.walk;
@@ -438,6 +438,7 @@ function complete(f: FlowState, t: Type): Type {
438
438
  export function getTypeAtFlowNode(c: Checker, f: FlowState, start: FlowNode): Type {
439
439
  if (f.depth === 2000) {
440
440
  c.flowAnalysisDisabled = true;
441
+ c.flowAnalysisDisabledBody = getContainingFlowBody(f.reference);
441
442
  reportFlowControlError(c, f.reference);
442
443
  return complete(f, c.errorType);
443
444
  }
@@ -3148,9 +3149,33 @@ function getContextFreeTypeOfExpression(c: Checker, node: Node): Type {
3148
3149
  return t;
3149
3150
  }
3150
3151
 
3151
- function reportFlowControlError(c: Checker, node: Node): void {
3152
+ /** The body whose walk reached the flow limit, or undefined when no body is stopped. */
3153
+ function getContainingFlowBody(node: Node): Node | undefined {
3152
3154
  let block: Node | undefined = node;
3153
3155
  while (block !== undefined && !isFunctionOrModuleBlock(block)) block = block.parent;
3156
+ return block;
3157
+ }
3158
+
3159
+ /** Whether `node` is in the body whose walk reached the flow limit, or in a body it contains. */
3160
+ function isInFlowAnalysisStop(c: Checker, node: Node): boolean {
3161
+ const stop = c.flowAnalysisDisabledBody;
3162
+ if (stop === undefined) return false;
3163
+ for (let n: Node | undefined = node; n !== undefined; n = n.parent) if (n === stop) return true;
3164
+ return false;
3165
+ }
3166
+
3167
+ /**
3168
+ * Ends the narrowing stopped in `body`, whose checks have finished: what follows the body starts over.
3169
+ * A walk that stopped in another body keeps its body's narrowing stopped.
3170
+ */
3171
+ export function endFlowAnalysisStop(c: Checker, body: Node): void {
3172
+ if (c.flowAnalysisDisabledBody !== body) return;
3173
+ c.flowAnalysisDisabled = false;
3174
+ c.flowAnalysisDisabledBody = undefined;
3175
+ }
3176
+
3177
+ function reportFlowControlError(c: Checker, node: Node): void {
3178
+ const block = getContainingFlowBody(node);
3154
3179
  if (block === undefined) return;
3155
3180
  const file = getSourceFileOfNode(node);
3156
3181
  const span = getRangeOfTokenAtPosition(file, getStatements(block)!.pos);
@@ -3165,7 +3190,8 @@ function reportFlowControlError(c: Checker, node: Node): void {
3165
3190
  );
3166
3191
  }
3167
3192
 
3168
- function isFunctionOrModuleBlock(node: Node): boolean {
3193
+ /** Whether `node` is the body of a function or a module, where flow analysis starts over (flowAnalysisDisabled). */
3194
+ export function isFunctionOrModuleBlock(node: Node): boolean {
3169
3195
  return (
3170
3196
  node.kind === SyntaxKind.SourceFile ||
3171
3197
  node.kind === SyntaxKind.ModuleBlock ||
@@ -132,6 +132,11 @@ import { checkContextualDeprecations, checkDeprecatedTypeReference } from './che
132
132
  * identifiers, which need the whole file checked first.
133
133
  */
134
134
  export function checkSourceFile(c: Checker, file: SourceFile, checkUnused = false): void {
135
+ // A source file is a module body: narrowing stopped by a body too large for control flow analysis
136
+ // (flowAnalysisDisabled, checker-narrowing.ts) starts over in the next check. A checker checks many files
137
+ // (parallel-check.ts), so its diagnostics of a file must not depend on the files it checked before it.
138
+ c.flowAnalysisDisabled = false;
139
+ c.flowAnalysisDisabledBody = undefined;
135
140
  const links = c.nodeLinks(file);
136
141
  if ((links.flags & NodeCheckFlags.TypeChecked) === 0) {
137
142
  links.flags |= NodeCheckFlags.TypeChecked;
@@ -141,6 +146,9 @@ export function checkSourceFile(c: Checker, file: SourceFile, checkUnused = fals
141
146
  links.flags |= NodeCheckFlags.UnusedChecked;
142
147
  checkUnusedIdentifiers(c, file);
143
148
  }
149
+ // Every body of the file has finished its checks; a stop recorded for one of them ends with the file.
150
+ c.flowAnalysisDisabled = false;
151
+ c.flowAnalysisDisabledBody = undefined;
144
152
  }
145
153
 
146
154
  function checkSourceFileWorker(c: Checker, file: SourceFile): void {
@@ -176,6 +184,10 @@ function checkSourceFileWorker(c: Checker, file: SourceFile): void {
176
184
  */
177
185
  export function checkSourceFileRange(c: Checker, file: SourceFile, start: number, end: number): boolean {
178
186
  const statements = getStatements(file)!.nodes;
187
+ // A range of a file's statements is part of its module body: narrowing stopped by a body too large for
188
+ // control flow analysis (flowAnalysisDisabled, checker-narrowing.ts) starts over in the next range.
189
+ c.flowAnalysisDisabled = false;
190
+ c.flowAnalysisDisabledBody = undefined;
179
191
  c.fileBeingChecked = file;
180
192
  for (let i = start; i < end; i++) checkSourceElement(c, statements[i]!);
181
193
  checkDeferredNodes(c, file);
@@ -191,6 +203,9 @@ export function checkSourceFileRange(c: Checker, file: SourceFile, start: number
191
203
  produceDeferredDiagnostics(c);
192
204
  // Deferred nodes of a later range are queued again (checkNodeDeferred).
193
205
  c.nodeLinks(file).deferredNodes = undefined;
206
+ // The range's bodies have finished their checks; a stop recorded for one of them ends with the range.
207
+ c.flowAnalysisDisabled = false;
208
+ c.flowAnalysisDisabledBody = undefined;
194
209
  return c.diagnostics.forFile(file.fileName).length > 0;
195
210
  }
196
211
 
@@ -42,7 +42,7 @@ import {
42
42
  isTypeEqualityComparableTo,
43
43
  } from './checker-operators.js';
44
44
  import { checkTypeComparableTo, isTypeAssignableTo } from './relations.js';
45
- import { isReachableFlowNode } from './checker-narrowing.js';
45
+ import { endFlowAnalysisStop, isFunctionOrModuleBlock, isReachableFlowNode } from './checker-narrowing.js';
46
46
  import { getIndexType, isTypeAssignableToKindEx as isTypeAssignableToKind } from './checker-type-operators.js';
47
47
  import { getExtractStringType } from './checker-symbol-types.js';
48
48
  import { checkSourceElement } from './checker-source-file.js';
@@ -57,7 +57,15 @@ import { typeToString } from './type-printer.js';
57
57
  import { TypeFacts, getNonNullableType, hasTypeFacts } from './checker-flow.js';
58
58
 
59
59
  export function checkBlock(c: Checker, node: Node): void {
60
- for (const s of getStatements(node)!.nodes) checkSourceElement(c, s);
60
+ const statements = getStatements(node)!.nodes;
61
+ if (!isFunctionOrModuleBlock(node)) {
62
+ for (const s of statements) checkSourceElement(c, s);
63
+ return;
64
+ }
65
+ // A body too large for control flow analysis stops narrowing in the rest of that body only
66
+ // (flowAnalysisDisabled, checker-narrowing.ts); what follows the body starts over.
67
+ for (const s of statements) checkSourceElement(c, s);
68
+ endFlowAnalysisStop(c, node);
61
69
  }
62
70
 
63
71
  export function checkExpressionStatement(c: Checker, node: Node): void {
@@ -755,6 +755,8 @@ export class Checker implements TypeChecking {
755
755
 
756
756
  // Control flow analysis
757
757
  flowAnalysisDisabled = false;
758
+ /** The body whose walk reached the flow limit (flowAnalysisDisabled), until its checks finish. */
759
+ flowAnalysisDisabledBody: Node | undefined = undefined;
758
760
  inlineLevel = 0;
759
761
  /**
760
762
  * Shared flow nodes cached by the walks in progress, with their types: the first
@@ -2161,7 +2161,8 @@ function checkSignals(
2161
2161
  }
2162
2162
  signals.library = library / ranges;
2163
2163
  signals.own = own / ranges;
2164
- signals.redundancy = (library - unionChars) / (library + own);
2164
+ // Every range can be empty (all of a program's files in the shared queue), so `library + own` can be 0.
2165
+ signals.redundancy = library + own === 0 ? 0 : (library - unionChars) / (library + own);
2165
2166
  signals.declarationShare = declarationFiles / files.length;
2166
2167
  signals.nodeModulesShare = nodeModulesFiles / files.length;
2167
2168
  if (config !== undefined) config.signals = signals;
@@ -282,10 +282,17 @@ export function readRecord(file: string, now: number = Math.floor(Date.now() / 1
282
282
  return dropOldSamples(record, now);
283
283
  }
284
284
 
285
+ /**
286
+ * A parsed value whose fields are read as `unknown` until they are checked. A native build checks a
287
+ * cast to a record type such as `Partial<RunRecord>` when it converts the value, and throws on a field
288
+ * of the wrong type, so a malformed record file would end the run instead of being ignored.
289
+ */
290
+ type Unchecked<T> = { [K in keyof T]?: unknown };
291
+
285
292
  /** `parsed` as a record, or `undefined` where it is not one this build writes. */
286
293
  function recordOf(parsed: unknown): RunRecord | undefined {
287
294
  if (typeof parsed !== 'object' || parsed === null) return undefined;
288
- const r = parsed as Partial<RunRecord>;
295
+ const r = parsed as Unchecked<RunRecord>;
289
296
  if (r.format !== recordFormat || typeof r.runs !== 'number' || !isShape(r.shape) || !isChoice(r.chosen))
290
297
  return undefined;
291
298
  const samples = Array.isArray(r.samples) ? r.samples.filter(isSample) : [];
@@ -304,7 +311,7 @@ function recordOf(parsed: unknown): RunRecord | undefined {
304
311
 
305
312
  function isShape(value: unknown): value is RecordShape {
306
313
  if (typeof value !== 'object' || value === null) return false;
307
- const s = value as Partial<RecordShape>;
314
+ const s = value as Unchecked<RecordShape>;
308
315
  return (
309
316
  isNumber(s.files) &&
310
317
  isNumber(s.chars) &&
@@ -318,7 +325,7 @@ function isShape(value: unknown): value is RecordShape {
318
325
 
319
326
  function isChoice(value: unknown): value is RecordChoice {
320
327
  if (typeof value !== 'object' || value === null) return false;
321
- const c = value as Partial<RecordChoice>;
328
+ const c = value as Unchecked<RecordChoice>;
322
329
  return (
323
330
  isNumber(c.checkers) &&
324
331
  typeof c.share === 'boolean' &&
@@ -331,7 +338,7 @@ function isChoice(value: unknown): value is RecordChoice {
331
338
 
332
339
  function isRejection(value: unknown): value is RecordRejection {
333
340
  if (typeof value !== 'object' || value === null) return false;
334
- const r = value as Partial<RecordRejection>;
341
+ const r = value as Unchecked<RecordRejection>;
335
342
  return (
336
343
  isNumber(r.checkers) &&
337
344
  typeof r.share === 'boolean' &&
@@ -342,7 +349,7 @@ function isRejection(value: unknown): value is RecordRejection {
342
349
 
343
350
  function isSample(value: unknown): value is RecordSample {
344
351
  if (typeof value !== 'object' || value === null) return false;
345
- const s = value as Partial<RecordSample>;
352
+ const s = value as Unchecked<RecordSample>;
346
353
  return (
347
354
  isNumber(s.run) &&
348
355
  isNumber(s.at) &&