tsc-ts 0.1.2 → 0.1.3

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) {
@@ -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.3",
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.3",
44
+ "@tsc-ts/darwin-x64": "0.1.3",
45
+ "@tsc-ts/linux-arm64-gnu": "0.1.3",
46
+ "@tsc-ts/linux-arm64-musl": "0.1.3",
47
+ "@tsc-ts/linux-x64-gnu": "0.1.3",
48
+ "@tsc-ts/linux-x64-musl": "0.1.3",
49
+ "@tsc-ts/win32-x64-msvc": "0.1.3"
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. */
@@ -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) &&