carrick 0.3.105 → 0.3.107
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/dist/hook/refresh.js +8 -1
- package/dist/hook/refresh.js.map +1 -1
- package/package.json +6 -6
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/sidecar/dist/src/capture/anchors.d.ts +22 -2
- package/sidecar/dist/src/capture/anchors.js +178 -34
- package/sidecar/dist/src/capture/api.d.ts +42 -1
- package/sidecar/dist/src/capture/check-classify.d.ts +9 -1
- package/sidecar/dist/src/capture/check-classify.js +15 -0
- package/sidecar/dist/src/capture/check-poison.js +4 -14
- package/sidecar/dist/src/capture/check.js +13 -1
- package/sidecar/dist/src/capture/index.js +55 -29
- package/sidecar/dist/src/capture/installed-package.d.ts +2 -0
- package/sidecar/dist/src/capture/installed-package.js +2 -1
- package/sidecar/dist/src/capture/outside-root.d.ts +19 -0
- package/sidecar/dist/src/capture/outside-root.js +39 -2
- package/sidecar/dist/src/capture/self-check.js +12 -13
- package/sidecar/dist/src/capture/service-config.d.ts +2 -0
- package/sidecar/dist/src/capture/service-config.js +1 -1
- package/sidecar/dist/src/capture/specifiers.d.ts +14 -0
- package/sidecar/dist/src/capture/specifiers.js +25 -0
- package/sidecar/dist/src/failure-path.d.ts +34 -3
- package/sidecar/dist/src/failure-path.js +59 -35
- package/sidecar/dist/src/function-line-index.d.ts +30 -0
- package/sidecar/dist/src/function-line-index.js +162 -0
- package/sidecar/dist/src/index.d.ts +5 -0
- package/sidecar/dist/src/index.js +29 -5
- package/sidecar/dist/src/line-index.d.ts +9 -0
- package/sidecar/dist/src/line-index.js +26 -0
- package/sidecar/dist/src/printed-names.d.ts +43 -0
- package/sidecar/dist/src/printed-names.js +186 -0
- package/sidecar/dist/src/progress.d.ts +22 -0
- package/sidecar/dist/src/progress.js +31 -0
- package/sidecar/dist/src/retype.js +58 -128
- package/sidecar/dist/src/type-inferrer.d.ts +124 -13
- package/sidecar/dist/src/type-inferrer.js +531 -146
- package/sidecar/dist/src/type-structural-expander.js +10 -1
- package/sidecar/dist/src/types.d.ts +37 -6
- package/sidecar/dist/src/validators.d.ts +76 -0
- package/sidecar/dist/src/validators.js +8 -0
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The function a line names, from an index built once per source file
|
|
3
|
+
* (carrick#1915).
|
|
4
|
+
*
|
|
5
|
+
* A signature request carries only the line its function starts on, and a
|
|
6
|
+
* signature pass sends one request per unannotated slot: thousands of
|
|
7
|
+
* lookups, several per function and many per file. Answering each by walking
|
|
8
|
+
* the file's every node cost the file's size per slot, and was 81% of the
|
|
9
|
+
* pass on a 2,345-file program. Here the file is walked once, over the
|
|
10
|
+
* compiler's own nodes, and each lookup reads the few entries near its line.
|
|
11
|
+
*
|
|
12
|
+
* The index belongs to the compiler's source file node, not to the file's
|
|
13
|
+
* name: replacing a file's text gives it a new node, so a file rewritten for
|
|
14
|
+
* one reading (the unwidened reading, the retype check) and then restored is
|
|
15
|
+
* indexed again each time, and never answered from positions it no longer has.
|
|
16
|
+
*/
|
|
17
|
+
import { SyntaxKind, ts, } from 'ts-morph';
|
|
18
|
+
import { lineIndex } from './line-index.js';
|
|
19
|
+
/** How far, in lines, a function may start from the line that names it. */
|
|
20
|
+
const LINE_TOLERANCE = 2;
|
|
21
|
+
const FUNCTION_KINDS = new Set([
|
|
22
|
+
SyntaxKind.FunctionDeclaration,
|
|
23
|
+
SyntaxKind.ArrowFunction,
|
|
24
|
+
SyntaxKind.FunctionExpression,
|
|
25
|
+
SyntaxKind.MethodDeclaration,
|
|
26
|
+
]);
|
|
27
|
+
/** The kinds ts-morph's `Node.isStatement` accepts. */
|
|
28
|
+
const STATEMENT_KINDS = new Set([
|
|
29
|
+
SyntaxKind.Block,
|
|
30
|
+
SyntaxKind.BreakStatement,
|
|
31
|
+
SyntaxKind.ClassDeclaration,
|
|
32
|
+
SyntaxKind.ContinueStatement,
|
|
33
|
+
SyntaxKind.DebuggerStatement,
|
|
34
|
+
SyntaxKind.DoStatement,
|
|
35
|
+
SyntaxKind.EmptyStatement,
|
|
36
|
+
SyntaxKind.EnumDeclaration,
|
|
37
|
+
SyntaxKind.ExportAssignment,
|
|
38
|
+
SyntaxKind.ExportDeclaration,
|
|
39
|
+
SyntaxKind.ExpressionStatement,
|
|
40
|
+
SyntaxKind.ForInStatement,
|
|
41
|
+
SyntaxKind.ForOfStatement,
|
|
42
|
+
SyntaxKind.ForStatement,
|
|
43
|
+
SyntaxKind.FunctionDeclaration,
|
|
44
|
+
SyntaxKind.IfStatement,
|
|
45
|
+
SyntaxKind.ImportDeclaration,
|
|
46
|
+
SyntaxKind.ImportEqualsDeclaration,
|
|
47
|
+
SyntaxKind.InterfaceDeclaration,
|
|
48
|
+
SyntaxKind.LabeledStatement,
|
|
49
|
+
SyntaxKind.ModuleBlock,
|
|
50
|
+
SyntaxKind.ModuleDeclaration,
|
|
51
|
+
SyntaxKind.NotEmittedStatement,
|
|
52
|
+
SyntaxKind.ReturnStatement,
|
|
53
|
+
SyntaxKind.SwitchStatement,
|
|
54
|
+
SyntaxKind.ThrowStatement,
|
|
55
|
+
SyntaxKind.TryStatement,
|
|
56
|
+
SyntaxKind.TypeAliasDeclaration,
|
|
57
|
+
SyntaxKind.VariableStatement,
|
|
58
|
+
SyntaxKind.WhileStatement,
|
|
59
|
+
SyntaxKind.WithStatement,
|
|
60
|
+
]);
|
|
61
|
+
const indexes = new WeakMap();
|
|
62
|
+
/**
|
|
63
|
+
* The function whose declaration starts at, or within `LINE_TOLERANCE` lines
|
|
64
|
+
* of, the given line: the closest, then the smallest, then the first in the
|
|
65
|
+
* file. A function starting after the line is passed over when a statement
|
|
66
|
+
* that opens on the line or after it, on a line before the function's, does
|
|
67
|
+
* not contain the function. Undefined when no function is left.
|
|
68
|
+
*
|
|
69
|
+
* These are `TypeInferrer.findFunctionByLine`'s rules, which says why each is
|
|
70
|
+
* there; this is where they are computed.
|
|
71
|
+
*/
|
|
72
|
+
export function functionAtLine(sourceFile, line) {
|
|
73
|
+
if (!Number.isFinite(line))
|
|
74
|
+
return undefined;
|
|
75
|
+
const index = indexOf(sourceFile.compilerNode);
|
|
76
|
+
/** Statements opening inside the forward window. */
|
|
77
|
+
const windowStatements = [];
|
|
78
|
+
for (let at = Math.ceil(line); at <= line + LINE_TOLERANCE; at++) {
|
|
79
|
+
const opening = index.statementsByLine.get(at);
|
|
80
|
+
if (opening)
|
|
81
|
+
windowStatements.push(...opening);
|
|
82
|
+
}
|
|
83
|
+
const separatedFromAnchor = (fn) => windowStatements.some((statement) => statement.line < fn.line && !(statement.start <= fn.start && statement.end >= fn.end));
|
|
84
|
+
let best;
|
|
85
|
+
for (let at = Math.ceil(line - LINE_TOLERANCE); at <= line + LINE_TOLERANCE; at++) {
|
|
86
|
+
for (const fn of index.functionsByLine.get(at) ?? []) {
|
|
87
|
+
if (fn.line > line && separatedFromAnchor(fn))
|
|
88
|
+
continue;
|
|
89
|
+
if (best === undefined || isBetter(fn, best, line))
|
|
90
|
+
best = fn;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return best ? wrapped(sourceFile, best) : undefined;
|
|
94
|
+
}
|
|
95
|
+
/** Closer to the line, then smaller, then earlier in the file. */
|
|
96
|
+
function isBetter(fn, best, line) {
|
|
97
|
+
const delta = Math.abs(fn.line - line);
|
|
98
|
+
const bestDelta = Math.abs(best.line - line);
|
|
99
|
+
if (delta !== bestDelta)
|
|
100
|
+
return delta < bestDelta;
|
|
101
|
+
const size = fn.end - fn.start;
|
|
102
|
+
const bestSize = best.end - best.start;
|
|
103
|
+
if (size !== bestSize)
|
|
104
|
+
return size < bestSize;
|
|
105
|
+
return fn.order < best.order;
|
|
106
|
+
}
|
|
107
|
+
function indexOf(file) {
|
|
108
|
+
let index = indexes.get(file);
|
|
109
|
+
if (!index) {
|
|
110
|
+
index = buildIndex(file);
|
|
111
|
+
indexes.set(file, index);
|
|
112
|
+
}
|
|
113
|
+
return index;
|
|
114
|
+
}
|
|
115
|
+
function buildIndex(file) {
|
|
116
|
+
// Lines as ts-morph's `getStartLineNumber` counts them, which is what the
|
|
117
|
+
// scanner's line numbers were matched against.
|
|
118
|
+
const lineAt = lineIndex(file.text);
|
|
119
|
+
const functionsByLine = new Map();
|
|
120
|
+
const statementsByLine = new Map();
|
|
121
|
+
let order = 0;
|
|
122
|
+
const visit = (node) => {
|
|
123
|
+
const isFunction = FUNCTION_KINDS.has(node.kind);
|
|
124
|
+
const isStatement = STATEMENT_KINDS.has(node.kind);
|
|
125
|
+
if (isFunction || isStatement) {
|
|
126
|
+
const start = node.getStart(file);
|
|
127
|
+
const extent = { start, end: node.end, line: lineAt(start) };
|
|
128
|
+
if (isFunction) {
|
|
129
|
+
pushAt(functionsByLine, extent.line, { ...extent, node, order: order++ });
|
|
130
|
+
}
|
|
131
|
+
if (isStatement) {
|
|
132
|
+
pushAt(statementsByLine, extent.line, extent);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
ts.forEachChild(node, visit);
|
|
136
|
+
};
|
|
137
|
+
ts.forEachChild(file, visit);
|
|
138
|
+
return { functionsByLine, statementsByLine };
|
|
139
|
+
}
|
|
140
|
+
function pushAt(byLine, line, entry) {
|
|
141
|
+
const entries = byLine.get(line);
|
|
142
|
+
if (entries)
|
|
143
|
+
entries.push(entry);
|
|
144
|
+
else
|
|
145
|
+
byLine.set(line, [entry]);
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* The ts-morph node for an indexed function. Only this one is wrapped: the
|
|
149
|
+
* deepest node at the function's first character is the function or sits
|
|
150
|
+
* inside it, so its ancestors lead there, and the rest of the file's nodes are
|
|
151
|
+
* never given wrappers they would keep for the life of the project.
|
|
152
|
+
*/
|
|
153
|
+
function wrapped(sourceFile, fn) {
|
|
154
|
+
let node = sourceFile.getDescendantAtPos(fn.start);
|
|
155
|
+
while (node && node.compilerNode !== fn.node) {
|
|
156
|
+
node = node.getParent();
|
|
157
|
+
}
|
|
158
|
+
// An invariant, not a case: no shape is known where the climb misses. If
|
|
159
|
+
// one exists, the answer is still the function the index chose.
|
|
160
|
+
node ??= sourceFile.getFirstDescendant((descendant) => descendant.compilerNode === fn.node);
|
|
161
|
+
return node;
|
|
162
|
+
}
|
|
@@ -10,5 +10,10 @@
|
|
|
10
10
|
* - stdout is ONLY for JSON responses
|
|
11
11
|
* - stderr is for logging
|
|
12
12
|
* - Process stays alive between requests (warm standby)
|
|
13
|
+
* - One request at a time. A handler that runs the compiler blocks the event
|
|
14
|
+
* loop until it returns, so a request sent meanwhile waits in the pipe, and
|
|
15
|
+
* nothing can cancel the one that is running. The scanner kills a process
|
|
16
|
+
* that goes silent past its deadline; a long handler stays alive by writing
|
|
17
|
+
* `progress` frames as it finishes units of work (writeProgress).
|
|
13
18
|
*/
|
|
14
19
|
export {};
|
|
@@ -10,6 +10,11 @@
|
|
|
10
10
|
* - stdout is ONLY for JSON responses
|
|
11
11
|
* - stderr is for logging
|
|
12
12
|
* - Process stays alive between requests (warm standby)
|
|
13
|
+
* - One request at a time. A handler that runs the compiler blocks the event
|
|
14
|
+
* loop until it returns, so a request sent meanwhile waits in the pipe, and
|
|
15
|
+
* nothing can cancel the one that is running. The scanner kills a process
|
|
16
|
+
* that goes silent past its deadline; a long handler stays alive by writing
|
|
17
|
+
* `progress` frames as it finishes units of work (writeProgress).
|
|
13
18
|
*/
|
|
14
19
|
import * as path from 'node:path';
|
|
15
20
|
import * as readline from 'node:readline';
|
|
@@ -21,6 +26,7 @@ import { DefinitionResolver } from './definition-resolver.js';
|
|
|
21
26
|
import { captureStub, findDisqualifyingTopTypes, jsonWireDeclarations, runCheck, } from './capture/index.js';
|
|
22
27
|
import { Retyper } from './retype.js';
|
|
23
28
|
import { LibraryClaimsVerifier, httpCheck } from './library-claims.js';
|
|
29
|
+
import { PROGRESS_INTERVAL_MS, atMostEvery } from './progress.js';
|
|
24
30
|
// ===========================================================================
|
|
25
31
|
// Module-level state
|
|
26
32
|
// ===========================================================================
|
|
@@ -168,7 +174,11 @@ function handleBundle(request) {
|
|
|
168
174
|
log(`Bundling ${request.symbols.length} symbol(s)`);
|
|
169
175
|
// A symbol named by a path is bundled from the program of the project
|
|
170
176
|
// that owns that file (carrick#1604).
|
|
171
|
-
const results = [...byProject(request.symbols, (symbol) => symbol.source_file)].map(([key, symbols]) =>
|
|
177
|
+
const results = [...byProject(request.symbols, (symbol) => symbol.source_file)].map(([key, symbols]) => {
|
|
178
|
+
const { typeBundler } = projectComponents(key);
|
|
179
|
+
writeProgress(request.request_id, 'bundle', 'program ready');
|
|
180
|
+
return typeBundler.bundle(symbols);
|
|
181
|
+
});
|
|
172
182
|
const result = results.length === 1 ? results[0] : mergeBundles(results);
|
|
173
183
|
if (!result.success) {
|
|
174
184
|
return {
|
|
@@ -284,7 +294,13 @@ async function handleCheckV2Async(request) {
|
|
|
284
294
|
function handleInfer(request) {
|
|
285
295
|
try {
|
|
286
296
|
log(`Inferring ${request.requests.length} type(s)`);
|
|
287
|
-
|
|
297
|
+
// One count for the request, whichever programs its items span.
|
|
298
|
+
let done = 0;
|
|
299
|
+
const report = atMostEvery(PROGRESS_INTERVAL_MS, () => writeProgress(request.request_id, 'infer', `${done} of ${request.requests.length}`));
|
|
300
|
+
const results = [...byProject(request.requests, (item) => item.file_path)].map(([key, items]) => projectComponents(key).typeInferrer.infer(items, request.extraction_config, () => {
|
|
301
|
+
done += 1;
|
|
302
|
+
report();
|
|
303
|
+
}));
|
|
288
304
|
const result = results.length === 1
|
|
289
305
|
? results[0]
|
|
290
306
|
: (() => {
|
|
@@ -388,6 +404,8 @@ function handleVerifyLibraryClaims(request) {
|
|
|
388
404
|
const started = performance.now();
|
|
389
405
|
try {
|
|
390
406
|
const { claimsVerifier } = projectComponents();
|
|
407
|
+
// The verifier's budget starts now, and so does the scanner's wait.
|
|
408
|
+
writeProgress(request.request_id, 'verify_library_claims', 'program ready');
|
|
391
409
|
const fromDir = path.resolve(projectLoader.getRepoRoot(), request.from_dir);
|
|
392
410
|
log(`Verifying ${request.checks.length} library claim(s) from ${fromDir}`);
|
|
393
411
|
const { semantics, modules } = claimsVerifier.run(fromDir, request.checks, request.budget_ms ?? SEMANTICS_BUDGET_MS);
|
|
@@ -530,9 +548,15 @@ function writeResponse(response) {
|
|
|
530
548
|
process.stdout.write(json + '\n');
|
|
531
549
|
}
|
|
532
550
|
/**
|
|
533
|
-
* Write a non-terminal progress
|
|
534
|
-
*
|
|
535
|
-
*
|
|
551
|
+
* Write a non-terminal progress frame. Distinct `status: 'progress'` so
|
|
552
|
+
* clients skip it and wait for the terminal success/error frame; the scanner
|
|
553
|
+
* restarts its deadline for the request on each one (carrick#1914).
|
|
554
|
+
*
|
|
555
|
+
* A synchronous handler calls this between units of its work, with the event
|
|
556
|
+
* loop blocked. The frame still leaves at once: a write this small to a pipe
|
|
557
|
+
* with room in it completes inside the call, and the reader on the other end
|
|
558
|
+
* drains the pipe on its own thread, so there is room. It is written through
|
|
559
|
+
* the same stream as every answer, so a frame can never land inside one.
|
|
536
560
|
*/
|
|
537
561
|
function writeProgress(requestId, phase, message) {
|
|
538
562
|
process.stdout.write(JSON.stringify({ request_id: requestId, status: 'progress', phase, message }) + '\n');
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The 1-based line of a position in a text, counted by line feeds alone: one
|
|
3
|
+
* more than the `\n`s before the position. That is the count ts-morph's
|
|
4
|
+
* `getStartLineNumber` makes. A lone carriage return or a Unicode line
|
|
5
|
+
* separator ends a line for the compiler and not here.
|
|
6
|
+
*
|
|
7
|
+
* The text is read once; each lookup after that is a binary search.
|
|
8
|
+
*/
|
|
9
|
+
export declare function lineIndex(text: string): (pos: number) => number;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The 1-based line of a position in a text, counted by line feeds alone: one
|
|
3
|
+
* more than the `\n`s before the position. That is the count ts-morph's
|
|
4
|
+
* `getStartLineNumber` makes. A lone carriage return or a Unicode line
|
|
5
|
+
* separator ends a line for the compiler and not here.
|
|
6
|
+
*
|
|
7
|
+
* The text is read once; each lookup after that is a binary search.
|
|
8
|
+
*/
|
|
9
|
+
export function lineIndex(text) {
|
|
10
|
+
const starts = [0];
|
|
11
|
+
for (let i = 0; i < text.length; i++)
|
|
12
|
+
if (text[i] === '\n')
|
|
13
|
+
starts.push(i + 1);
|
|
14
|
+
return (pos) => {
|
|
15
|
+
let lo = 0;
|
|
16
|
+
let hi = starts.length - 1;
|
|
17
|
+
while (lo < hi) {
|
|
18
|
+
const mid = (lo + hi + 1) >> 1;
|
|
19
|
+
if (starts[mid] <= pos)
|
|
20
|
+
lo = mid;
|
|
21
|
+
else
|
|
22
|
+
hi = mid - 1;
|
|
23
|
+
}
|
|
24
|
+
return lo + 1;
|
|
25
|
+
};
|
|
26
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* carrick#1836: what each bare name in an inference's printed text meant.
|
|
3
|
+
*
|
|
4
|
+
* The structural printer (`type-structural-expander.ts`) hands some subtrees
|
|
5
|
+
* back to the compiler's own print with no enclosing declaration, and that
|
|
6
|
+
* print writes every named type by its bare name: a database client's row,
|
|
7
|
+
* which is a library's mapped type, prints `{ status: EntityStatus; ... }`.
|
|
8
|
+
* The file a request names often never imports `EntityStatus`, so the text
|
|
9
|
+
* names nothing where it is read (TS2304), and the member reads `any` in the
|
|
10
|
+
* capture's surface.
|
|
11
|
+
*
|
|
12
|
+
* The compiler knew the symbol when it printed the name, and only then. So
|
|
13
|
+
* every type print made while an inference runs is noted (`notePrintedType`),
|
|
14
|
+
* and when it ends (`PrintedTypes.namesIn`) the names its text prints that the
|
|
15
|
+
* request's file cannot resolve are looked up in those prints: the node
|
|
16
|
+
* builder gives each name it writes the symbol it wrote it for. Each such
|
|
17
|
+
* symbol is recorded as the module that declares it and its export path
|
|
18
|
+
* there. A name printed for two declarations is recorded twice, and the
|
|
19
|
+
* reader of the record decides what an ambiguous name means.
|
|
20
|
+
*
|
|
21
|
+
* Only the prints whose text writes such a name are read back, and only when
|
|
22
|
+
* the inference's text has one, so an inference whose names all resolve costs
|
|
23
|
+
* no more than keeping the list.
|
|
24
|
+
*/
|
|
25
|
+
import { ts, type Node, type Type } from 'ts-morph';
|
|
26
|
+
import type { PrintedName } from './types.js';
|
|
27
|
+
/**
|
|
28
|
+
* Note a type the compiler printed as `text`, so the inference running now can
|
|
29
|
+
* tell what the names in that print meant. Does nothing outside a recording.
|
|
30
|
+
*/
|
|
31
|
+
export declare function notePrintedType(type: Type, enclosing: Node | undefined, text: string): void;
|
|
32
|
+
/** The type prints one inference made. */
|
|
33
|
+
export declare class PrintedTypes {
|
|
34
|
+
private readonly prints;
|
|
35
|
+
/** Run `read`, noting every type it prints. */
|
|
36
|
+
during<T>(read: () => T): T;
|
|
37
|
+
/**
|
|
38
|
+
* The declarations behind each name `texts` print that `source` does not
|
|
39
|
+
* resolve, sorted, one entry per distinct declaration. `checker` must be
|
|
40
|
+
* the one the prints were made with: call this before the program changes.
|
|
41
|
+
*/
|
|
42
|
+
namesIn(texts: readonly string[], source: ts.SourceFile, checker: ts.TypeChecker): PrintedName[];
|
|
43
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* carrick#1836: what each bare name in an inference's printed text meant.
|
|
3
|
+
*
|
|
4
|
+
* The structural printer (`type-structural-expander.ts`) hands some subtrees
|
|
5
|
+
* back to the compiler's own print with no enclosing declaration, and that
|
|
6
|
+
* print writes every named type by its bare name: a database client's row,
|
|
7
|
+
* which is a library's mapped type, prints `{ status: EntityStatus; ... }`.
|
|
8
|
+
* The file a request names often never imports `EntityStatus`, so the text
|
|
9
|
+
* names nothing where it is read (TS2304), and the member reads `any` in the
|
|
10
|
+
* capture's surface.
|
|
11
|
+
*
|
|
12
|
+
* The compiler knew the symbol when it printed the name, and only then. So
|
|
13
|
+
* every type print made while an inference runs is noted (`notePrintedType`),
|
|
14
|
+
* and when it ends (`PrintedTypes.namesIn`) the names its text prints that the
|
|
15
|
+
* request's file cannot resolve are looked up in those prints: the node
|
|
16
|
+
* builder gives each name it writes the symbol it wrote it for. Each such
|
|
17
|
+
* symbol is recorded as the module that declares it and its export path
|
|
18
|
+
* there. A name printed for two declarations is recorded twice, and the
|
|
19
|
+
* reader of the record decides what an ambiguous name means.
|
|
20
|
+
*
|
|
21
|
+
* Only the prints whose text writes such a name are read back, and only when
|
|
22
|
+
* the inference's text has one, so an inference whose names all resolve costs
|
|
23
|
+
* no more than keeping the list.
|
|
24
|
+
*/
|
|
25
|
+
import { ts } from 'ts-morph';
|
|
26
|
+
/** The prints of the inference running now, when one is recording. */
|
|
27
|
+
let recording;
|
|
28
|
+
/**
|
|
29
|
+
* Note a type the compiler printed as `text`, so the inference running now can
|
|
30
|
+
* tell what the names in that print meant. Does nothing outside a recording.
|
|
31
|
+
*/
|
|
32
|
+
export function notePrintedType(type, enclosing, text) {
|
|
33
|
+
recording?.push({ type: type.compilerType, enclosing: enclosing?.compilerNode, text });
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Same flags the printers pass `typeToString` (`NoTruncation`, `InTypeAlias`),
|
|
37
|
+
* so the node read back is the one that was printed.
|
|
38
|
+
*/
|
|
39
|
+
const NODE_FLAGS = ts.NodeBuilderFlags.NoTruncation | ts.NodeBuilderFlags.InTypeAlias | ts.NodeBuilderFlags.IgnoreErrors;
|
|
40
|
+
const REFERENCE_MEANING = ts.SymbolFlags.Type | ts.SymbolFlags.Namespace | ts.SymbolFlags.Alias;
|
|
41
|
+
/** The type prints one inference made. */
|
|
42
|
+
export class PrintedTypes {
|
|
43
|
+
prints = [];
|
|
44
|
+
/** Run `read`, noting every type it prints. */
|
|
45
|
+
during(read) {
|
|
46
|
+
const outer = recording;
|
|
47
|
+
recording = this.prints;
|
|
48
|
+
try {
|
|
49
|
+
return read();
|
|
50
|
+
}
|
|
51
|
+
finally {
|
|
52
|
+
recording = outer;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* The declarations behind each name `texts` print that `source` does not
|
|
57
|
+
* resolve, sorted, one entry per distinct declaration. `checker` must be
|
|
58
|
+
* the one the prints were made with: call this before the program changes.
|
|
59
|
+
*/
|
|
60
|
+
namesIn(texts, source, checker) {
|
|
61
|
+
const wanted = new Set();
|
|
62
|
+
for (const text of texts) {
|
|
63
|
+
for (const name of referencedNames(text)) {
|
|
64
|
+
if (!checker.resolveName(name, source, REFERENCE_MEANING, false))
|
|
65
|
+
wanted.add(name);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (wanted.size === 0)
|
|
69
|
+
return [];
|
|
70
|
+
// Only a print whose text writes one of the names can say what it meant.
|
|
71
|
+
const alternatives = [...wanted].map((name) => name.replace(/\$/g, '\\$')).join('|');
|
|
72
|
+
const writes = new RegExp(`(?<![\\w$.])(?:${alternatives})(?![\\w$])`);
|
|
73
|
+
const found = new Map();
|
|
74
|
+
const identities = new Map();
|
|
75
|
+
const visited = new Map();
|
|
76
|
+
for (const { type, enclosing, text } of this.prints) {
|
|
77
|
+
if (!writes.test(text))
|
|
78
|
+
continue;
|
|
79
|
+
const at = visited.get(type) ?? new Set();
|
|
80
|
+
if (at.has(enclosing))
|
|
81
|
+
continue;
|
|
82
|
+
visited.set(type, at.add(enclosing));
|
|
83
|
+
let node;
|
|
84
|
+
try {
|
|
85
|
+
node = checker.typeToTypeNode(type, enclosing, NODE_FLAGS);
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
const visit = (current) => {
|
|
91
|
+
if (ts.isTypeReferenceNode(current)) {
|
|
92
|
+
const id = leftmost(current.typeName);
|
|
93
|
+
const name = ts.idText(id);
|
|
94
|
+
const symbol = wanted.has(name) ? symbolOf(id, checker) : undefined;
|
|
95
|
+
if (symbol) {
|
|
96
|
+
if (!identities.has(symbol))
|
|
97
|
+
identities.set(symbol, identityOf(symbol, checker));
|
|
98
|
+
const identity = identities.get(symbol);
|
|
99
|
+
if (identity) {
|
|
100
|
+
found.set(`${name}\0${identity.file}\0${identity.export_path.join('.')}`, { name, ...identity });
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
ts.forEachChild(current, visit);
|
|
105
|
+
};
|
|
106
|
+
if (node)
|
|
107
|
+
visit(node);
|
|
108
|
+
}
|
|
109
|
+
return [...found.entries()].sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)).map(([, entry]) => entry);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
function leftmost(name) {
|
|
113
|
+
return ts.isIdentifier(name) ? name : leftmost(name.left);
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* The leftmost names of the type references `text` makes, less the type
|
|
117
|
+
* parameters the text declares itself (`[K in ...]`, `infer U`).
|
|
118
|
+
*/
|
|
119
|
+
function referencedNames(text) {
|
|
120
|
+
const parsed = ts.createSourceFile('printed.ts', `type __Printed = ${text};`, ts.ScriptTarget.Latest, true);
|
|
121
|
+
const names = new Set();
|
|
122
|
+
const declared = new Set();
|
|
123
|
+
const visit = (node) => {
|
|
124
|
+
if (ts.isTypeParameterDeclaration(node))
|
|
125
|
+
declared.add(node.name.text);
|
|
126
|
+
if (ts.isTypeReferenceNode(node))
|
|
127
|
+
names.add(ts.idText(leftmost(node.typeName)));
|
|
128
|
+
ts.forEachChild(node, visit);
|
|
129
|
+
};
|
|
130
|
+
visit(parsed);
|
|
131
|
+
for (const name of declared)
|
|
132
|
+
names.delete(name);
|
|
133
|
+
names.delete('__Printed');
|
|
134
|
+
return names;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* The symbol a printed name was written for. The node builder sets it on
|
|
138
|
+
* every identifier it creates (`symbol` is not in the public typings); a name
|
|
139
|
+
* it copied from source carries none, and is read at its original node.
|
|
140
|
+
*/
|
|
141
|
+
function symbolOf(id, checker) {
|
|
142
|
+
const written = id.symbol;
|
|
143
|
+
if (written)
|
|
144
|
+
return written;
|
|
145
|
+
const original = ts.getOriginalNode(id);
|
|
146
|
+
return original !== id && original.parent ? checker.getSymbolAtLocation(original) : undefined;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* The module that declares `symbol`'s type and the export path to it there,
|
|
150
|
+
* or undefined for a type no module export reaches: a global, a type
|
|
151
|
+
* parameter, a declaration a function body holds, a module itself.
|
|
152
|
+
*/
|
|
153
|
+
function identityOf(symbol, checker) {
|
|
154
|
+
const resolve = (s) => s.flags & ts.SymbolFlags.Alias ? checker.getAliasedSymbol(s) : s;
|
|
155
|
+
const target = resolve(symbol);
|
|
156
|
+
if (!(target.flags & (ts.SymbolFlags.Type | ts.SymbolFlags.Namespace)) ||
|
|
157
|
+
target.flags & ts.SymbolFlags.TypeParameter) {
|
|
158
|
+
return undefined;
|
|
159
|
+
}
|
|
160
|
+
const declaration = target.declarations?.[0];
|
|
161
|
+
if (!declaration || ts.isSourceFile(declaration))
|
|
162
|
+
return undefined;
|
|
163
|
+
const file = declaration.getSourceFile();
|
|
164
|
+
const moduleSymbol = checker.getSymbolAtLocation(file);
|
|
165
|
+
if (!moduleSymbol)
|
|
166
|
+
return undefined;
|
|
167
|
+
const direct = checker.getExportsOfModule(moduleSymbol).filter((e) => resolve(e) === target);
|
|
168
|
+
if (direct.length > 0) {
|
|
169
|
+
const chosen = direct.find((e) => e.getName() === target.getName()) ?? direct[0];
|
|
170
|
+
return { file: file.fileName, export_path: [chosen.getName()] };
|
|
171
|
+
}
|
|
172
|
+
// A namespace member: `"<module>".Billing.Kind`, checked step by step.
|
|
173
|
+
const qualified = checker.getFullyQualifiedName(target);
|
|
174
|
+
const close = qualified.startsWith('"') ? qualified.indexOf('"', 1) : -1;
|
|
175
|
+
if (close < 0)
|
|
176
|
+
return undefined;
|
|
177
|
+
const exportPath = qualified.slice(close + 2).split('.');
|
|
178
|
+
let current = moduleSymbol;
|
|
179
|
+
for (const part of exportPath) {
|
|
180
|
+
const next = current
|
|
181
|
+
? checker.getExportsOfModule(current).find((e) => e.getName() === part)
|
|
182
|
+
: undefined;
|
|
183
|
+
current = next && resolve(next);
|
|
184
|
+
}
|
|
185
|
+
return current === target ? { file: file.fileName, export_path: exportPath } : undefined;
|
|
186
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pacing for the `progress` frames a long request writes (carrick#1914).
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* The least time between two progress frames of one request.
|
|
6
|
+
*
|
|
7
|
+
* The scanner's deadline for a request is how long the sidecar may stay
|
|
8
|
+
* silent about it, so a frame has to say that a unit of work finished, and
|
|
9
|
+
* has to be far more frequent than that deadline. It does not have to follow
|
|
10
|
+
* every unit: a batch can finish thousands in a second.
|
|
11
|
+
*/
|
|
12
|
+
export declare const PROGRESS_INTERVAL_MS = 1500;
|
|
13
|
+
/**
|
|
14
|
+
* `report`, held to one call per `intervalMs`: the first call goes through at
|
|
15
|
+
* once, and a call made sooner than `intervalMs` after the last one that went
|
|
16
|
+
* through is dropped.
|
|
17
|
+
*
|
|
18
|
+
* It is called by the work, between units, and never by a timer. A handler
|
|
19
|
+
* that stops finishing units stops reporting, which is what the reader on the
|
|
20
|
+
* other end is waiting to notice.
|
|
21
|
+
*/
|
|
22
|
+
export declare function atMostEvery<Args extends unknown[]>(intervalMs: number, report: (...args: Args) => void, now?: () => number): (...args: Args) => void;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pacing for the `progress` frames a long request writes (carrick#1914).
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* The least time between two progress frames of one request.
|
|
6
|
+
*
|
|
7
|
+
* The scanner's deadline for a request is how long the sidecar may stay
|
|
8
|
+
* silent about it, so a frame has to say that a unit of work finished, and
|
|
9
|
+
* has to be far more frequent than that deadline. It does not have to follow
|
|
10
|
+
* every unit: a batch can finish thousands in a second.
|
|
11
|
+
*/
|
|
12
|
+
export const PROGRESS_INTERVAL_MS = 1500;
|
|
13
|
+
/**
|
|
14
|
+
* `report`, held to one call per `intervalMs`: the first call goes through at
|
|
15
|
+
* once, and a call made sooner than `intervalMs` after the last one that went
|
|
16
|
+
* through is dropped.
|
|
17
|
+
*
|
|
18
|
+
* It is called by the work, between units, and never by a timer. A handler
|
|
19
|
+
* that stops finishing units stops reporting, which is what the reader on the
|
|
20
|
+
* other end is waiting to notice.
|
|
21
|
+
*/
|
|
22
|
+
export function atMostEvery(intervalMs, report, now = () => performance.now()) {
|
|
23
|
+
let last = Number.NEGATIVE_INFINITY;
|
|
24
|
+
return (...args) => {
|
|
25
|
+
const at = now();
|
|
26
|
+
if (at - last < intervalMs)
|
|
27
|
+
return;
|
|
28
|
+
last = at;
|
|
29
|
+
report(...args);
|
|
30
|
+
};
|
|
31
|
+
}
|