@skastr0/quartz-engine 0.2.0
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/LICENSE +21 -0
- package/dist/analyzer.d.ts +48 -0
- package/dist/context.d.ts +21 -0
- package/dist/contracts.d.ts +440 -0
- package/dist/diagnostics.d.ts +3 -0
- package/dist/discovery.d.ts +2 -0
- package/dist/errors.d.ts +6 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +2949 -0
- package/dist/leaf-operations.d.ts +26 -0
- package/dist/reference-operations.d.ts +11 -0
- package/dist/transform-search/index.d.ts +8 -0
- package/dist/types.d.ts +30 -0
- package/dist/verification-operations.d.ts +19 -0
- package/dist/virtual-files.d.ts +43 -0
- package/dist/workspace.d.ts +25 -0
- package/package.json +48 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2949 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// src/index.ts
|
|
3
|
+
import { version as version2 } from "typescript";
|
|
4
|
+
|
|
5
|
+
// src/context.ts
|
|
6
|
+
import { resolve as resolve3 } from "path";
|
|
7
|
+
|
|
8
|
+
// src/discovery.ts
|
|
9
|
+
import { execFileSync } from "child_process";
|
|
10
|
+
import { basename, dirname, join, relative, resolve } from "path";
|
|
11
|
+
import { readdirSync } from "fs";
|
|
12
|
+
|
|
13
|
+
// src/errors.ts
|
|
14
|
+
class QuartzEngineError extends Error {
|
|
15
|
+
code;
|
|
16
|
+
cause;
|
|
17
|
+
constructor(code, message, cause) {
|
|
18
|
+
super(message);
|
|
19
|
+
this.name = "QuartzEngineError";
|
|
20
|
+
this.code = code;
|
|
21
|
+
this.cause = cause;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// src/discovery.ts
|
|
26
|
+
var ignoredDirectories = new Set([
|
|
27
|
+
"node_modules",
|
|
28
|
+
".git",
|
|
29
|
+
"dist",
|
|
30
|
+
"build",
|
|
31
|
+
".next",
|
|
32
|
+
".nuxt",
|
|
33
|
+
".output",
|
|
34
|
+
"coverage",
|
|
35
|
+
".turbo",
|
|
36
|
+
".cache"
|
|
37
|
+
]);
|
|
38
|
+
var discoverPackages = (rootDirectory, selectedTsconfigPaths) => {
|
|
39
|
+
const resolvedRoot = resolve(rootDirectory);
|
|
40
|
+
try {
|
|
41
|
+
const tsconfigPaths = selectedTsconfigPaths === undefined ? findTsconfigs(resolvedRoot) : selectedTsconfigPaths.map((tsconfigPath) => resolve(resolvedRoot, tsconfigPath));
|
|
42
|
+
return toPackageInfo(resolvedRoot, tsconfigPaths);
|
|
43
|
+
} catch (cause) {
|
|
44
|
+
throw new QuartzEngineError("WORKSPACE_OPEN_FAILED", `Could not discover TypeScript packages under ${resolvedRoot}`, cause);
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
var findTsconfigs = (rootDirectory) => {
|
|
48
|
+
const trackedFiles = getGitTrackedFiles(rootDirectory);
|
|
49
|
+
if (trackedFiles.length > 0) {
|
|
50
|
+
return trackedFiles.filter((file) => basename(file) === "tsconfig.json" && !file.includes("node_modules")).map((file) => join(rootDirectory, file));
|
|
51
|
+
}
|
|
52
|
+
return walkForTsconfigs(rootDirectory);
|
|
53
|
+
};
|
|
54
|
+
var getGitTrackedFiles = (rootDirectory) => {
|
|
55
|
+
try {
|
|
56
|
+
const stdout = execFileSync("git", ["ls-files", "--cached", "--others", "--exclude-standard"], {
|
|
57
|
+
cwd: rootDirectory,
|
|
58
|
+
encoding: "utf8",
|
|
59
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
60
|
+
});
|
|
61
|
+
return stdout.trim().split(`
|
|
62
|
+
`).filter((file) => file.length > 0);
|
|
63
|
+
} catch {
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
var walkForTsconfigs = (directory) => readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
|
|
68
|
+
const path = join(directory, entry.name);
|
|
69
|
+
if (entry.isDirectory())
|
|
70
|
+
return ignoredDirectories.has(entry.name) ? [] : walkForTsconfigs(path);
|
|
71
|
+
return entry.name === "tsconfig.json" ? [path] : [];
|
|
72
|
+
});
|
|
73
|
+
var toPackageInfo = (rootDirectory, tsconfigPaths) => [...new Set(tsconfigPaths.map((tsconfigPath) => resolve(rootDirectory, tsconfigPath)))].map((tsconfigPath) => {
|
|
74
|
+
const packagePath = dirname(tsconfigPath);
|
|
75
|
+
const relativePath = relative(rootDirectory, packagePath);
|
|
76
|
+
return {
|
|
77
|
+
name: relativePath === "" ? "(root)" : relativePath,
|
|
78
|
+
path: packagePath,
|
|
79
|
+
tsconfigPath
|
|
80
|
+
};
|
|
81
|
+
}).sort((left, right) => {
|
|
82
|
+
if (left.name === "(root)")
|
|
83
|
+
return -1;
|
|
84
|
+
if (right.name === "(root)")
|
|
85
|
+
return 1;
|
|
86
|
+
return left.name.localeCompare(right.name);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// src/workspace.ts
|
|
90
|
+
import { existsSync } from "fs";
|
|
91
|
+
import { AsyncLocalStorage } from "async_hooks";
|
|
92
|
+
import { createRequire } from "module";
|
|
93
|
+
import { dirname as dirname2, join as join2, resolve as resolve2 } from "path";
|
|
94
|
+
import { pathToFileURL } from "url";
|
|
95
|
+
import { version } from "typescript";
|
|
96
|
+
import { API } from "typescript/unstable/async";
|
|
97
|
+
|
|
98
|
+
// src/diagnostics.ts
|
|
99
|
+
import { DiagnosticCategory } from "typescript/unstable/async";
|
|
100
|
+
var severityFor = (category) => {
|
|
101
|
+
switch (category) {
|
|
102
|
+
case DiagnosticCategory.Error:
|
|
103
|
+
return "error";
|
|
104
|
+
case DiagnosticCategory.Warning:
|
|
105
|
+
return "warning";
|
|
106
|
+
case DiagnosticCategory.Suggestion:
|
|
107
|
+
return "suggestion";
|
|
108
|
+
case DiagnosticCategory.Message:
|
|
109
|
+
return "message";
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
var lineAndColumnAt = (text, position) => {
|
|
113
|
+
const bounded = Math.max(0, Math.min(position, text.length));
|
|
114
|
+
let line = 1;
|
|
115
|
+
let lineStart = 0;
|
|
116
|
+
for (let index = 0;index < bounded; index += 1) {
|
|
117
|
+
if (text.charCodeAt(index) === 10) {
|
|
118
|
+
line += 1;
|
|
119
|
+
lineStart = index + 1;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return { line, column: bounded - lineStart + 1 };
|
|
123
|
+
};
|
|
124
|
+
var diagnosticKey = (diagnostic) => [diagnostic.fileName ?? "", diagnostic.pos, diagnostic.end, diagnostic.code, diagnostic.category, diagnostic.text].join("\x00");
|
|
125
|
+
var collectDiagnostics = async (project) => {
|
|
126
|
+
const diagnosticGroups = await Promise.all([
|
|
127
|
+
project.program.getConfigFileParsingDiagnostics(),
|
|
128
|
+
project.program.getSyntacticDiagnostics(),
|
|
129
|
+
project.program.getSemanticDiagnostics()
|
|
130
|
+
]);
|
|
131
|
+
const diagnostics = diagnosticGroups.flat();
|
|
132
|
+
const unique = new Map;
|
|
133
|
+
for (const diagnostic of diagnostics)
|
|
134
|
+
unique.set(diagnosticKey(diagnostic), diagnostic);
|
|
135
|
+
const sourceTextByFile = new Map;
|
|
136
|
+
const sourceTextFor = (fileName) => {
|
|
137
|
+
const cached = sourceTextByFile.get(fileName);
|
|
138
|
+
if (cached !== undefined)
|
|
139
|
+
return cached;
|
|
140
|
+
const pending = project.program.getSourceFile(fileName).then((sourceFile) => sourceFile?.text ?? null);
|
|
141
|
+
sourceTextByFile.set(fileName, pending);
|
|
142
|
+
return pending;
|
|
143
|
+
};
|
|
144
|
+
const mapped = await Promise.all([...unique.values()].map(async (diagnostic) => {
|
|
145
|
+
if (diagnostic.fileName === undefined) {
|
|
146
|
+
return {
|
|
147
|
+
file: null,
|
|
148
|
+
line: null,
|
|
149
|
+
column: null,
|
|
150
|
+
endLine: null,
|
|
151
|
+
endColumn: null,
|
|
152
|
+
code: diagnostic.code,
|
|
153
|
+
severity: severityFor(diagnostic.category),
|
|
154
|
+
message: diagnostic.text
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
const sourceText = await sourceTextFor(diagnostic.fileName);
|
|
158
|
+
const start = sourceText === null ? null : lineAndColumnAt(sourceText, diagnostic.pos);
|
|
159
|
+
const end = sourceText === null ? null : lineAndColumnAt(sourceText, diagnostic.end);
|
|
160
|
+
return {
|
|
161
|
+
file: diagnostic.fileName,
|
|
162
|
+
line: start?.line ?? null,
|
|
163
|
+
column: start?.column ?? null,
|
|
164
|
+
endLine: end?.line ?? null,
|
|
165
|
+
endColumn: end?.column ?? null,
|
|
166
|
+
code: diagnostic.code,
|
|
167
|
+
severity: severityFor(diagnostic.category),
|
|
168
|
+
message: diagnostic.text
|
|
169
|
+
};
|
|
170
|
+
}));
|
|
171
|
+
return mapped.sort((left, right) => {
|
|
172
|
+
const fileOrder = (left.file ?? "").localeCompare(right.file ?? "");
|
|
173
|
+
if (fileOrder !== 0)
|
|
174
|
+
return fileOrder;
|
|
175
|
+
return (left.line ?? 0) - (right.line ?? 0) || (left.column ?? 0) - (right.column ?? 0) || left.code - right.code;
|
|
176
|
+
});
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
// src/workspace.ts
|
|
180
|
+
var resolveTypeScriptExecutable = () => {
|
|
181
|
+
const platformPackage = `@typescript/typescript-${process.platform}-${process.arch}`;
|
|
182
|
+
const executableName = process.platform === "win32" ? "tsc.exe" : "tsc";
|
|
183
|
+
const executableBase = pathToFileURL(join2(dirname2(process.execPath), "__quartz_resolver.cjs")).href;
|
|
184
|
+
const resolvers = [createRequire(import.meta.url), createRequire(executableBase)];
|
|
185
|
+
for (const resolver of resolvers) {
|
|
186
|
+
const packageJsonCandidates = [];
|
|
187
|
+
try {
|
|
188
|
+
const typescriptPackageJson = resolver.resolve("typescript/package.json");
|
|
189
|
+
packageJsonCandidates.push(createRequire(pathToFileURL(typescriptPackageJson)).resolve(`${platformPackage}/package.json`));
|
|
190
|
+
} catch {}
|
|
191
|
+
try {
|
|
192
|
+
packageJsonCandidates.push(resolver.resolve(`${platformPackage}/package.json`));
|
|
193
|
+
} catch {}
|
|
194
|
+
for (const packageJson of packageJsonCandidates) {
|
|
195
|
+
const executable = join2(dirname2(packageJson), "lib", executableName);
|
|
196
|
+
if (existsSync(executable))
|
|
197
|
+
return executable;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
throw new QuartzEngineError("WORKSPACE_OPEN_FAILED", `Unable to resolve ${platformPackage}. Reinstall Quartz with platform dependencies enabled.`);
|
|
201
|
+
};
|
|
202
|
+
var createRevisionState = (snapshot, revision) => ({
|
|
203
|
+
snapshot,
|
|
204
|
+
revision,
|
|
205
|
+
drained: Promise.withResolvers(),
|
|
206
|
+
readers: 0,
|
|
207
|
+
retired: false,
|
|
208
|
+
disposePromise: null
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
class QuartzWorkspace {
|
|
212
|
+
root;
|
|
213
|
+
configFile;
|
|
214
|
+
configFiles;
|
|
215
|
+
#api;
|
|
216
|
+
#current;
|
|
217
|
+
#mutationTail = Promise.resolve();
|
|
218
|
+
#closing = false;
|
|
219
|
+
#closed = false;
|
|
220
|
+
#closePromise = null;
|
|
221
|
+
#retirements = new Set;
|
|
222
|
+
#activeOperations = 0;
|
|
223
|
+
#operationsDrained = null;
|
|
224
|
+
#revisionLease = new AsyncLocalStorage;
|
|
225
|
+
constructor(root, configFiles, api, state) {
|
|
226
|
+
this.root = root;
|
|
227
|
+
this.configFiles = configFiles;
|
|
228
|
+
this.configFile = configFiles[0];
|
|
229
|
+
this.#api = api;
|
|
230
|
+
this.#current = state;
|
|
231
|
+
}
|
|
232
|
+
static async open(root, options = {}) {
|
|
233
|
+
const resolvedRoot = resolve2(root);
|
|
234
|
+
const primaryConfig = resolve2(resolvedRoot, options.tsconfigPath ?? "tsconfig.json");
|
|
235
|
+
const configFiles = [
|
|
236
|
+
primaryConfig,
|
|
237
|
+
...(options.tsconfigPaths ?? []).map((configFile) => resolve2(resolvedRoot, configFile))
|
|
238
|
+
].filter((configFile, index, all) => all.indexOf(configFile) === index);
|
|
239
|
+
const api = new API({
|
|
240
|
+
cwd: resolvedRoot,
|
|
241
|
+
tsserverPath: options.tsserverPath ?? resolveTypeScriptExecutable(),
|
|
242
|
+
...options.collectTiming === undefined ? {} : { collectTiming: options.collectTiming }
|
|
243
|
+
});
|
|
244
|
+
try {
|
|
245
|
+
const snapshot = await api.updateSnapshot({ openProjects: configFiles });
|
|
246
|
+
const missingConfig = configFiles.find((configFile) => snapshot.getProject(configFile) === undefined);
|
|
247
|
+
if (missingConfig !== undefined) {
|
|
248
|
+
await snapshot.dispose();
|
|
249
|
+
throw new QuartzEngineError("WORKSPACE_OPEN_FAILED", `TypeScript did not load the configured project at ${missingConfig}`);
|
|
250
|
+
}
|
|
251
|
+
return new QuartzWorkspace(resolvedRoot, configFiles, api, createRevisionState(snapshot, 1));
|
|
252
|
+
} catch (cause) {
|
|
253
|
+
await api.close().catch(() => {
|
|
254
|
+
return;
|
|
255
|
+
});
|
|
256
|
+
if (cause instanceof QuartzEngineError)
|
|
257
|
+
throw cause;
|
|
258
|
+
throw new QuartzEngineError("WORKSPACE_OPEN_FAILED", `Failed to open Quartz workspace at ${resolvedRoot}`, cause);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
get metadata() {
|
|
262
|
+
return {
|
|
263
|
+
root: this.root,
|
|
264
|
+
configFile: this.configFile,
|
|
265
|
+
configFiles: this.configFiles,
|
|
266
|
+
revision: this.#current?.revision ?? 0,
|
|
267
|
+
analysisTypescriptVersion: version,
|
|
268
|
+
closed: this.#closed
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
async diagnostics(configFile = this.configFile) {
|
|
272
|
+
return this.withProject((project) => collectDiagnostics(project), configFile);
|
|
273
|
+
}
|
|
274
|
+
getTimingInfo() {
|
|
275
|
+
this.#assertAcceptingWork();
|
|
276
|
+
return this.#api.getTimingInfo();
|
|
277
|
+
}
|
|
278
|
+
resetTimingInfo() {
|
|
279
|
+
this.#assertAcceptingWork();
|
|
280
|
+
return this.#api.resetTimingInfo();
|
|
281
|
+
}
|
|
282
|
+
withProject(operation, configFile = this.configFile) {
|
|
283
|
+
const resolvedConfig = resolve2(configFile);
|
|
284
|
+
return this.#withProject((state) => {
|
|
285
|
+
const project = state.snapshot.getProject(resolvedConfig);
|
|
286
|
+
if (project === undefined) {
|
|
287
|
+
throw new QuartzEngineError("WORKSPACE_OPEN_FAILED", `Project is not open at ${resolvedConfig}`);
|
|
288
|
+
}
|
|
289
|
+
return operation(project, state.revision);
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
async withVirtualFile(tsconfigPath, filePath, content, operation) {
|
|
293
|
+
this.#assertAcceptingWork();
|
|
294
|
+
this.#beginOperation();
|
|
295
|
+
const resolvedConfig = resolve2(tsconfigPath);
|
|
296
|
+
const resolvedFile = resolve2(filePath);
|
|
297
|
+
const inheritedLease = this.#revisionLease.getStore();
|
|
298
|
+
const base = inheritedLease?.active === true ? inheritedLease.state : this.#requireCurrent();
|
|
299
|
+
const lease = { state: base, active: true };
|
|
300
|
+
base.readers += 1;
|
|
301
|
+
try {
|
|
302
|
+
let result;
|
|
303
|
+
await this.#revisionLease.run(lease, async () => {
|
|
304
|
+
await this.#api.runWithTemporaryFileUpdate(base.snapshot, resolvedFile, content, async (temporarySnapshot) => {
|
|
305
|
+
this.#assertAcceptingWork();
|
|
306
|
+
const project = await temporarySnapshot.getDefaultProjectForFile(resolvedFile) ?? temporarySnapshot.getProject(resolvedConfig);
|
|
307
|
+
if (project === undefined) {
|
|
308
|
+
throw new QuartzEngineError("WORKSPACE_REFRESH_FAILED", `TypeScript did not load a project for temporary file ${resolvedFile}`);
|
|
309
|
+
}
|
|
310
|
+
result = await operation(project, resolvedFile);
|
|
311
|
+
});
|
|
312
|
+
});
|
|
313
|
+
return result;
|
|
314
|
+
} catch (cause) {
|
|
315
|
+
if (cause instanceof QuartzEngineError)
|
|
316
|
+
throw cause;
|
|
317
|
+
throw new QuartzEngineError("WORKSPACE_REFRESH_FAILED", `Temporary file analysis failed for ${resolvedFile}`, cause);
|
|
318
|
+
} finally {
|
|
319
|
+
lease.active = false;
|
|
320
|
+
this.#releaseState(base);
|
|
321
|
+
this.#endOperation();
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
refresh(changes) {
|
|
325
|
+
this.#assertAcceptingWork();
|
|
326
|
+
return this.#enqueueMutation(async () => {
|
|
327
|
+
try {
|
|
328
|
+
const previous = this.#requireCurrent();
|
|
329
|
+
const snapshot = await this.#api.updateSnapshot({
|
|
330
|
+
fileChanges: changes === undefined ? { invalidateAll: true } : {
|
|
331
|
+
...changes.changed === undefined ? {} : { changed: changes.changed.map((path) => resolve2(path)) },
|
|
332
|
+
...changes.created === undefined ? {} : { created: changes.created.map((path) => resolve2(path)) },
|
|
333
|
+
...changes.deleted === undefined ? {} : { deleted: changes.deleted.map((path) => resolve2(path)) }
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
const missingConfig = this.configFiles.find((configFile) => snapshot.getProject(configFile) === undefined);
|
|
337
|
+
if (missingConfig !== undefined) {
|
|
338
|
+
await snapshot.dispose();
|
|
339
|
+
throw new QuartzEngineError("WORKSPACE_REFRESH_FAILED", `TypeScript lost the configured project at ${missingConfig}`);
|
|
340
|
+
}
|
|
341
|
+
this.#current = createRevisionState(snapshot, previous.revision + 1);
|
|
342
|
+
this.#retire(previous);
|
|
343
|
+
return this.metadata;
|
|
344
|
+
} catch (cause) {
|
|
345
|
+
if (cause instanceof QuartzEngineError)
|
|
346
|
+
throw cause;
|
|
347
|
+
throw new QuartzEngineError("WORKSPACE_REFRESH_FAILED", `Failed to refresh ${this.root}`, cause);
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
close() {
|
|
352
|
+
if (this.#closePromise !== null)
|
|
353
|
+
return this.#closePromise;
|
|
354
|
+
this.#closing = true;
|
|
355
|
+
this.#closePromise = (async () => {
|
|
356
|
+
await this.#operationsDrained?.promise;
|
|
357
|
+
await this.#enqueueMutation(async () => {
|
|
358
|
+
const current = this.#current;
|
|
359
|
+
this.#current = null;
|
|
360
|
+
if (current !== null)
|
|
361
|
+
this.#retire(current);
|
|
362
|
+
await Promise.all([...this.#retirements]);
|
|
363
|
+
await this.#api.close();
|
|
364
|
+
this.#closed = true;
|
|
365
|
+
});
|
|
366
|
+
})();
|
|
367
|
+
return this.#closePromise;
|
|
368
|
+
}
|
|
369
|
+
async#withProject(operation) {
|
|
370
|
+
this.#assertAcceptingWork();
|
|
371
|
+
this.#beginOperation();
|
|
372
|
+
const state = this.#requireCurrent();
|
|
373
|
+
const lease = { state, active: true };
|
|
374
|
+
state.readers += 1;
|
|
375
|
+
try {
|
|
376
|
+
return await this.#revisionLease.run(lease, () => operation(state));
|
|
377
|
+
} finally {
|
|
378
|
+
lease.active = false;
|
|
379
|
+
this.#releaseState(state);
|
|
380
|
+
this.#endOperation();
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
#beginOperation() {
|
|
384
|
+
if (this.#activeOperations === 0)
|
|
385
|
+
this.#operationsDrained = Promise.withResolvers();
|
|
386
|
+
this.#activeOperations += 1;
|
|
387
|
+
}
|
|
388
|
+
#endOperation() {
|
|
389
|
+
this.#activeOperations -= 1;
|
|
390
|
+
if (this.#activeOperations === 0) {
|
|
391
|
+
this.#operationsDrained?.resolve();
|
|
392
|
+
this.#operationsDrained = null;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
#releaseState(state) {
|
|
396
|
+
state.readers -= 1;
|
|
397
|
+
if (state.retired && state.readers === 0)
|
|
398
|
+
state.drained.resolve();
|
|
399
|
+
}
|
|
400
|
+
#enqueueMutation(operation) {
|
|
401
|
+
const result = this.#mutationTail.then(operation);
|
|
402
|
+
this.#mutationTail = result.then(() => {
|
|
403
|
+
return;
|
|
404
|
+
}, () => {
|
|
405
|
+
return;
|
|
406
|
+
});
|
|
407
|
+
return result;
|
|
408
|
+
}
|
|
409
|
+
#retire(state) {
|
|
410
|
+
if (state.disposePromise !== null)
|
|
411
|
+
return;
|
|
412
|
+
state.retired = true;
|
|
413
|
+
if (state.readers === 0)
|
|
414
|
+
state.drained.resolve();
|
|
415
|
+
const retirement = state.drained.promise.then(() => state.snapshot.dispose());
|
|
416
|
+
state.disposePromise = retirement;
|
|
417
|
+
this.#retirements.add(retirement);
|
|
418
|
+
retirement.then(() => this.#retirements.delete(retirement), () => this.#retirements.delete(retirement));
|
|
419
|
+
}
|
|
420
|
+
#assertAcceptingWork() {
|
|
421
|
+
if (this.#closing || this.#closed) {
|
|
422
|
+
throw new QuartzEngineError("WORKSPACE_CLOSED", `Quartz workspace at ${this.root} is closed`);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
#requireCurrent() {
|
|
426
|
+
const current = this.#current;
|
|
427
|
+
if (current === null) {
|
|
428
|
+
throw new QuartzEngineError("WORKSPACE_CLOSED", `Quartz workspace at ${this.root} is closed`);
|
|
429
|
+
}
|
|
430
|
+
return current;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
var openQuartzWorkspace = (root, options) => QuartzWorkspace.open(root, options);
|
|
434
|
+
|
|
435
|
+
// src/context.ts
|
|
436
|
+
class AnalyzerContext {
|
|
437
|
+
defaultConfigPath;
|
|
438
|
+
root;
|
|
439
|
+
packages;
|
|
440
|
+
workspace;
|
|
441
|
+
#dirty = false;
|
|
442
|
+
#dirtyGeneration = 0;
|
|
443
|
+
#dirtyRefresh = null;
|
|
444
|
+
#revisionCache = new Map;
|
|
445
|
+
constructor(root, packages, workspace, defaultConfigPath) {
|
|
446
|
+
this.defaultConfigPath = defaultConfigPath;
|
|
447
|
+
this.root = root;
|
|
448
|
+
this.packages = packages;
|
|
449
|
+
this.workspace = workspace;
|
|
450
|
+
}
|
|
451
|
+
static async open(root, options = {}) {
|
|
452
|
+
const resolvedRoot = resolve3(root);
|
|
453
|
+
const selectedTsconfigPaths = options.tsconfigPath !== undefined ? [options.tsconfigPath, ...options.tsconfigPaths ?? []] : options.tsconfigPaths === undefined ? undefined : ["tsconfig.json", ...options.tsconfigPaths];
|
|
454
|
+
const packages = discoverPackages(resolvedRoot, selectedTsconfigPaths);
|
|
455
|
+
if (packages.length === 0) {
|
|
456
|
+
throw new QuartzEngineError("WORKSPACE_OPEN_FAILED", `No tsconfig.json found under ${resolvedRoot}`);
|
|
457
|
+
}
|
|
458
|
+
const primaryConfig = options.tsconfigPath === undefined ? (packages.find((pkg) => pkg.name === "(root)") ?? packages[0]).tsconfigPath : resolve3(resolvedRoot, options.tsconfigPath);
|
|
459
|
+
const defaultConfigPath = options.tsconfigPath === undefined ? packages.length === 1 || packages.some((pkg) => pkg.name === "(root)") ? primaryConfig : undefined : primaryConfig;
|
|
460
|
+
const workspace = await openQuartzWorkspace(resolvedRoot, {
|
|
461
|
+
...options,
|
|
462
|
+
tsconfigPath: primaryConfig,
|
|
463
|
+
tsconfigPaths: packages.map((pkg) => pkg.tsconfigPath)
|
|
464
|
+
});
|
|
465
|
+
return new AnalyzerContext(resolvedRoot, packages, workspace, defaultConfigPath);
|
|
466
|
+
}
|
|
467
|
+
package(packageName) {
|
|
468
|
+
if (packageName === undefined || packageName.length === 0) {
|
|
469
|
+
const defaultPackage = this.defaultConfigPath === undefined ? undefined : this.packages.find((pkg2) => pkg2.tsconfigPath === this.defaultConfigPath);
|
|
470
|
+
if (defaultPackage !== undefined)
|
|
471
|
+
return defaultPackage;
|
|
472
|
+
if (this.packages.length === 1)
|
|
473
|
+
return this.packages[0];
|
|
474
|
+
throw new QuartzEngineError("WORKSPACE_OPEN_FAILED", `Multiple packages found. Please specify a package: ${this.packages.map((pkg2) => pkg2.name).join(", ")}`);
|
|
475
|
+
}
|
|
476
|
+
const normalized = packageName.replace(/^\//, "");
|
|
477
|
+
const pkg = this.packages.find((candidate) => candidate.name === packageName || candidate.name === normalized || candidate.path.endsWith(packageName));
|
|
478
|
+
if (pkg === undefined) {
|
|
479
|
+
throw new QuartzEngineError("WORKSPACE_OPEN_FAILED", `Unknown TypeScript package: ${packageName}. Available: ${this.packages.map((candidate) => candidate.name).join(", ")}`);
|
|
480
|
+
}
|
|
481
|
+
return pkg;
|
|
482
|
+
}
|
|
483
|
+
async withProject(operation, packageName) {
|
|
484
|
+
await this.#ensureFresh();
|
|
485
|
+
const pkg = this.package(packageName);
|
|
486
|
+
return this.workspace.withProject((project, revision) => operation(project, pkg, revision), pkg.tsconfigPath);
|
|
487
|
+
}
|
|
488
|
+
cacheForRevision(key, revision, load) {
|
|
489
|
+
const cached = this.#revisionCache.get(key);
|
|
490
|
+
if (cached?.revision === revision)
|
|
491
|
+
return cached.value;
|
|
492
|
+
const value = load();
|
|
493
|
+
this.#revisionCache.set(key, { revision, value });
|
|
494
|
+
return value;
|
|
495
|
+
}
|
|
496
|
+
async refresh(changes) {
|
|
497
|
+
const generationAtStart = this.#dirtyGeneration;
|
|
498
|
+
const metadata = await this.workspace.refresh(changes);
|
|
499
|
+
this.#revisionCache.clear();
|
|
500
|
+
if (this.#dirtyGeneration === generationAtStart)
|
|
501
|
+
this.#dirty = false;
|
|
502
|
+
else
|
|
503
|
+
this.#dirty = true;
|
|
504
|
+
return metadata;
|
|
505
|
+
}
|
|
506
|
+
refreshPackage(packageName) {
|
|
507
|
+
const pkg = this.package(packageName);
|
|
508
|
+
return this.refresh({ changed: [pkg.tsconfigPath] });
|
|
509
|
+
}
|
|
510
|
+
markDirty() {
|
|
511
|
+
this.#dirty = true;
|
|
512
|
+
this.#dirtyGeneration += 1;
|
|
513
|
+
}
|
|
514
|
+
async#ensureFresh() {
|
|
515
|
+
if (!this.#dirty)
|
|
516
|
+
return;
|
|
517
|
+
const refresh = this.#dirtyRefresh ??= this.refresh();
|
|
518
|
+
try {
|
|
519
|
+
await refresh;
|
|
520
|
+
} finally {
|
|
521
|
+
if (this.#dirtyRefresh === refresh)
|
|
522
|
+
this.#dirtyRefresh = null;
|
|
523
|
+
}
|
|
524
|
+
if (this.#dirty)
|
|
525
|
+
await this.#ensureFresh();
|
|
526
|
+
}
|
|
527
|
+
close() {
|
|
528
|
+
return this.workspace.close();
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// src/leaf-operations.ts
|
|
533
|
+
import { isAbsolute, relative as relative3, resolve as resolve5 } from "path";
|
|
534
|
+
import {
|
|
535
|
+
DiagnosticCategory as DiagnosticCategory2,
|
|
536
|
+
ModifierFlags,
|
|
537
|
+
NodeBuilderFlags,
|
|
538
|
+
SignatureKind,
|
|
539
|
+
SymbolFlags as SymbolFlags2
|
|
540
|
+
} from "typescript/unstable/async";
|
|
541
|
+
import { SyntaxKind } from "typescript/unstable/ast";
|
|
542
|
+
import {
|
|
543
|
+
isClassDeclaration,
|
|
544
|
+
isEnumDeclaration,
|
|
545
|
+
isExportSpecifier,
|
|
546
|
+
isFunctionDeclaration,
|
|
547
|
+
isIdentifier,
|
|
548
|
+
isInterfaceDeclaration,
|
|
549
|
+
isTypeAliasDeclaration,
|
|
550
|
+
isTypeReferenceNode,
|
|
551
|
+
isVariableDeclaration,
|
|
552
|
+
isVariableStatement
|
|
553
|
+
} from "typescript/unstable/ast/is";
|
|
554
|
+
|
|
555
|
+
// src/virtual-files.ts
|
|
556
|
+
import { existsSync as existsSync2 } from "fs";
|
|
557
|
+
import { dirname as dirname3, extname, join as join3, relative as relative2, resolve as resolve4 } from "path";
|
|
558
|
+
import { SymbolFlags } from "typescript/unstable/async";
|
|
559
|
+
var resolveVirtualFileDirectory = (packageRoot) => {
|
|
560
|
+
const root = resolve4(packageRoot);
|
|
561
|
+
for (const candidate of ["types", "src", "lib", "source"]) {
|
|
562
|
+
const directory = join3(root, candidate);
|
|
563
|
+
if (existsSync2(directory))
|
|
564
|
+
return directory;
|
|
565
|
+
}
|
|
566
|
+
return root;
|
|
567
|
+
};
|
|
568
|
+
var modulePathFor = (from, fileName) => {
|
|
569
|
+
const withoutExtension = fileName.slice(0, -extname(fileName).length);
|
|
570
|
+
const path = relative2(dirname3(from), withoutExtension).replaceAll("\\", "/");
|
|
571
|
+
return path.startsWith(".") ? path : `./${path}`;
|
|
572
|
+
};
|
|
573
|
+
var isSourceFileIn = (fileName, root) => {
|
|
574
|
+
const resolved = resolve4(fileName);
|
|
575
|
+
const base = resolve4(root);
|
|
576
|
+
return resolved === base || resolved.startsWith(`${base}/`);
|
|
577
|
+
};
|
|
578
|
+
var isIndexFile = (fileName) => /(?:^|\/)index\.[cm]?[jt]sx?$/.test(fileName.replaceAll("\\", "/"));
|
|
579
|
+
var packageSources = async (project, packageRoot, virtualFilePath) => {
|
|
580
|
+
const files = (await project.program.getSourceFileNames()).filter((fileName) => fileName !== virtualFilePath && isSourceFileIn(fileName, packageRoot) && !fileName.endsWith(".d.ts") && /\.[cm]?[jt]sx?$/.test(fileName)).sort((left, right) => {
|
|
581
|
+
const leftRelative = relative2(packageRoot, left);
|
|
582
|
+
const rightRelative = relative2(packageRoot, right);
|
|
583
|
+
const rank = (value) => value === "index.ts" || value === "index.tsx" ? 0 : value.startsWith("src/") ? 1 : 2;
|
|
584
|
+
return rank(leftRelative) - rank(rightRelative) || leftRelative.localeCompare(rightRelative);
|
|
585
|
+
});
|
|
586
|
+
const loaded = (await Promise.all(files.map((fileName) => project.program.getSourceFile(fileName)))).filter((source) => source !== undefined);
|
|
587
|
+
const entry = loaded.find((source) => isIndexFile(source.fileName));
|
|
588
|
+
return entry === undefined ? loaded : [entry];
|
|
589
|
+
};
|
|
590
|
+
var synthesizePackageImports = async (project, packageRoot, virtualFilePath) => {
|
|
591
|
+
const sources = await packageSources(project, packageRoot, virtualFilePath);
|
|
592
|
+
if (sources.length === 0)
|
|
593
|
+
return { content: "", lineOffset: 0 };
|
|
594
|
+
const claimedNames = new Set;
|
|
595
|
+
const lines = [];
|
|
596
|
+
for (const source of sources) {
|
|
597
|
+
const moduleSymbol = await project.checker.getSymbolAtLocation(source);
|
|
598
|
+
if (moduleSymbol === undefined)
|
|
599
|
+
continue;
|
|
600
|
+
const exported = await project.checker.getExportsOfModule(moduleSymbol);
|
|
601
|
+
const typeNames = [];
|
|
602
|
+
const valueNames = [];
|
|
603
|
+
for (const symbol of exported) {
|
|
604
|
+
if (symbol.name === "default" || claimedNames.has(symbol.name) || !/^[A-Za-z_$][\w$]*$/.test(symbol.name))
|
|
605
|
+
continue;
|
|
606
|
+
claimedNames.add(symbol.name);
|
|
607
|
+
if ((symbol.flags & SymbolFlags.Value) !== SymbolFlags.None)
|
|
608
|
+
valueNames.push(symbol.name);
|
|
609
|
+
else
|
|
610
|
+
typeNames.push(symbol.name);
|
|
611
|
+
}
|
|
612
|
+
const modulePath = modulePathFor(virtualFilePath, source.fileName);
|
|
613
|
+
if (typeNames.length > 0)
|
|
614
|
+
lines.push(`import type { ${typeNames.sort().join(", ")} } from ${JSON.stringify(modulePath)}`);
|
|
615
|
+
if (valueNames.length > 0)
|
|
616
|
+
lines.push(`import { ${valueNames.sort().join(", ")} } from ${JSON.stringify(modulePath)}`);
|
|
617
|
+
}
|
|
618
|
+
return { content: lines.length === 0 ? "" : `${lines.join(`
|
|
619
|
+
`)}
|
|
620
|
+
`, lineOffset: lines.length };
|
|
621
|
+
};
|
|
622
|
+
var createVirtualFileRegistry = (root, prefix = "__quartz_snippet_") => {
|
|
623
|
+
const directory = resolve4(root);
|
|
624
|
+
const files = new Map;
|
|
625
|
+
let sequence = 0;
|
|
626
|
+
const acquire = (content, extension = ".ts") => {
|
|
627
|
+
const token = Symbol("virtual-file");
|
|
628
|
+
const id = `${Date.now().toString(36)}_${(sequence++).toString(36)}`;
|
|
629
|
+
const path = join3(directory, `${prefix}${id}${extension}`);
|
|
630
|
+
const entry = { path, content, token };
|
|
631
|
+
files.set(path, entry);
|
|
632
|
+
let disposed = false;
|
|
633
|
+
return {
|
|
634
|
+
...entry,
|
|
635
|
+
dispose: () => {
|
|
636
|
+
if (disposed)
|
|
637
|
+
return;
|
|
638
|
+
disposed = true;
|
|
639
|
+
const current = files.get(path);
|
|
640
|
+
if (current?.token === token)
|
|
641
|
+
files.delete(path);
|
|
642
|
+
}
|
|
643
|
+
};
|
|
644
|
+
};
|
|
645
|
+
return {
|
|
646
|
+
acquire,
|
|
647
|
+
create: acquire,
|
|
648
|
+
get: (path) => files.get(path)?.content,
|
|
649
|
+
has: (path) => files.has(path),
|
|
650
|
+
entries: () => [...files.values()],
|
|
651
|
+
get size() {
|
|
652
|
+
return files.size;
|
|
653
|
+
},
|
|
654
|
+
clear: () => files.clear()
|
|
655
|
+
};
|
|
656
|
+
};
|
|
657
|
+
var withVirtualFile = async (registry, content, operation, extension = ".ts") => {
|
|
658
|
+
const lease = registry.acquire(content, extension);
|
|
659
|
+
try {
|
|
660
|
+
return await operation(lease);
|
|
661
|
+
} finally {
|
|
662
|
+
lease.dispose();
|
|
663
|
+
}
|
|
664
|
+
};
|
|
665
|
+
|
|
666
|
+
// src/leaf-operations.ts
|
|
667
|
+
var TYPE_FLAGS = NodeBuilderFlags.NoTruncation | NodeBuilderFlags.UseStructuralFallback | NodeBuilderFlags.WriteTypeArgumentsOfSignature | NodeBuilderFlags.InTypeAlias | NodeBuilderFlags.UseAliasDefinedOutsideCurrentScope;
|
|
668
|
+
var EXPAND_FLAGS = TYPE_FLAGS | NodeBuilderFlags.WriteArrayAsGenericType;
|
|
669
|
+
var MAX_PROPERTIES = 50;
|
|
670
|
+
var MAX_NODE_TEXT = 100;
|
|
671
|
+
var kindToString = (kind) => {
|
|
672
|
+
switch (kind) {
|
|
673
|
+
case SyntaxKind.InterfaceDeclaration:
|
|
674
|
+
return "interface";
|
|
675
|
+
case SyntaxKind.TypeAliasDeclaration:
|
|
676
|
+
return "type";
|
|
677
|
+
case SyntaxKind.ClassDeclaration:
|
|
678
|
+
return "class";
|
|
679
|
+
case SyntaxKind.FunctionDeclaration:
|
|
680
|
+
return "function";
|
|
681
|
+
case SyntaxKind.VariableDeclaration:
|
|
682
|
+
return "variable";
|
|
683
|
+
case SyntaxKind.EnumDeclaration:
|
|
684
|
+
return "enum";
|
|
685
|
+
case SyntaxKind.ModuleDeclaration:
|
|
686
|
+
return "module";
|
|
687
|
+
default:
|
|
688
|
+
return (SyntaxKind[kind] ?? "unknown").replace(/Declaration$/, "").replace(/Statement$/, "").toLowerCase();
|
|
689
|
+
}
|
|
690
|
+
};
|
|
691
|
+
var sourceFilesFor = (context, project, packageInfo, revision) => context.cacheForRevision(`source-files:${packageInfo.tsconfigPath}`, revision, async () => {
|
|
692
|
+
const root = resolve5(packageInfo.path);
|
|
693
|
+
const names = await project.program.getSourceFileNames();
|
|
694
|
+
const files = await Promise.all(names.map(async (name) => {
|
|
695
|
+
const source = await project.program.getSourceFile(name);
|
|
696
|
+
if (source === undefined || await project.program.isSourceFileFromExternalLibrary(source))
|
|
697
|
+
return;
|
|
698
|
+
const file = resolve5(source.fileName);
|
|
699
|
+
return file === root || file.startsWith(`${root}/`) ? source : undefined;
|
|
700
|
+
}));
|
|
701
|
+
return files.filter((source) => source !== undefined);
|
|
702
|
+
});
|
|
703
|
+
var relativePath = (root, file) => {
|
|
704
|
+
const absolute = resolve5(file);
|
|
705
|
+
const base = resolve5(root);
|
|
706
|
+
return absolute === base || absolute.startsWith(`${base}/`) ? relative3(base, absolute) : absolute;
|
|
707
|
+
};
|
|
708
|
+
var declarationName = (node) => {
|
|
709
|
+
if (isClassDeclaration(node) || isEnumDeclaration(node) || isFunctionDeclaration(node) || isInterfaceDeclaration(node) || isTypeAliasDeclaration(node)) {
|
|
710
|
+
return node.name?.text;
|
|
711
|
+
}
|
|
712
|
+
if (isVariableDeclaration(node) && isIdentifier(node.name))
|
|
713
|
+
return node.name.text;
|
|
714
|
+
return;
|
|
715
|
+
};
|
|
716
|
+
var declarationSymbol = async (project, node) => {
|
|
717
|
+
if (isClassDeclaration(node) || isEnumDeclaration(node) || isFunctionDeclaration(node) || isInterfaceDeclaration(node) || isTypeAliasDeclaration(node)) {
|
|
718
|
+
return node.name === undefined ? undefined : project.checker.getSymbolAtLocation(node.name);
|
|
719
|
+
}
|
|
720
|
+
if (isVariableDeclaration(node) && isIdentifier(node.name))
|
|
721
|
+
return project.checker.getSymbolAtLocation(node.name);
|
|
722
|
+
return;
|
|
723
|
+
};
|
|
724
|
+
var declarationsIn = (source) => {
|
|
725
|
+
const declarations = [];
|
|
726
|
+
for (const statement of source.statements) {
|
|
727
|
+
if (isClassDeclaration(statement) || isEnumDeclaration(statement) || isFunctionDeclaration(statement) || isInterfaceDeclaration(statement) || isTypeAliasDeclaration(statement)) {
|
|
728
|
+
declarations.push(statement);
|
|
729
|
+
} else if (isVariableStatement(statement)) {
|
|
730
|
+
declarations.push(...statement.declarationList.declarations);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
return declarations;
|
|
734
|
+
};
|
|
735
|
+
var isExported = (node) => {
|
|
736
|
+
const modifiers = node.modifierFlags;
|
|
737
|
+
return modifiers !== undefined && (modifiers & ModifierFlags.Export) !== 0;
|
|
738
|
+
};
|
|
739
|
+
var isDefault = (node) => {
|
|
740
|
+
const modifiers = node.modifierFlags;
|
|
741
|
+
return modifiers !== undefined && (modifiers & ModifierFlags.Default) !== 0;
|
|
742
|
+
};
|
|
743
|
+
var exportMatchesFor = async (project, source) => {
|
|
744
|
+
const moduleSymbol = await project.checker.getSymbolAtLocation(source);
|
|
745
|
+
if (moduleSymbol === undefined)
|
|
746
|
+
return [];
|
|
747
|
+
const exported = await project.checker.getExportsOfModule(moduleSymbol);
|
|
748
|
+
const matches = [];
|
|
749
|
+
for (const exportedSymbol of exported) {
|
|
750
|
+
const resolvedSymbol = (exportedSymbol.flags & SymbolFlags2.Alias) !== SymbolFlags2.None ? await project.checker.getAliasedSymbol(exportedSymbol) : exportedSymbol;
|
|
751
|
+
for (const handle of resolvedSymbol.declarations) {
|
|
752
|
+
const node = await handle.resolve(project);
|
|
753
|
+
if (node === undefined)
|
|
754
|
+
continue;
|
|
755
|
+
const name = declarationName(node);
|
|
756
|
+
if (name !== undefined) {
|
|
757
|
+
matches.push({ node, symbol: await declarationSymbol(project, node) ?? resolvedSymbol, exportedName: exportedSymbol.name });
|
|
758
|
+
break;
|
|
759
|
+
}
|
|
760
|
+
if (isExportSpecifier(node)) {
|
|
761
|
+
const target = await project.checker.getAliasedSymbol(exportedSymbol);
|
|
762
|
+
for (const targetHandle of target.declarations) {
|
|
763
|
+
const targetNode = await targetHandle.resolve(project);
|
|
764
|
+
if (targetNode === undefined)
|
|
765
|
+
continue;
|
|
766
|
+
const targetName = declarationName(targetNode);
|
|
767
|
+
if (targetName !== undefined) {
|
|
768
|
+
matches.push({ node: targetNode, symbol: await declarationSymbol(project, targetNode) ?? target, exportedName: exportedSymbol.name });
|
|
769
|
+
break;
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
break;
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
return matches;
|
|
777
|
+
};
|
|
778
|
+
var exportedMatches = async (project, sourceFiles) => {
|
|
779
|
+
const all = await Promise.all(sourceFiles.map((source) => exportMatchesFor(project, source)));
|
|
780
|
+
return all.flat();
|
|
781
|
+
};
|
|
782
|
+
var exportedMatchesFor = (context, project, packageInfo, revision, sourceFiles) => context.cacheForRevision(`exported-matches:${packageInfo.tsconfigPath}`, revision, () => exportedMatches(project, sourceFiles));
|
|
783
|
+
var parseFileReference = (value) => {
|
|
784
|
+
if (!value.startsWith("@file:"))
|
|
785
|
+
return null;
|
|
786
|
+
const rest = value.slice(6);
|
|
787
|
+
const index = rest.lastIndexOf(":");
|
|
788
|
+
return index < 0 ? { file: rest, symbol: "*" } : { file: rest.slice(0, index), symbol: rest.slice(index + 1) };
|
|
789
|
+
};
|
|
790
|
+
var findMatch = async (project, sourceFiles, root, symbolName, matches) => {
|
|
791
|
+
const reference = parseFileReference(symbolName);
|
|
792
|
+
if (reference !== null) {
|
|
793
|
+
const source = sourceFiles.find((candidate) => resolve5(candidate.fileName) === resolve5(root, reference.file) || candidate.fileName.endsWith(reference.file));
|
|
794
|
+
if (source === undefined || reference.symbol === "*")
|
|
795
|
+
return null;
|
|
796
|
+
const [rootName2, ...members] = reference.symbol.split(".");
|
|
797
|
+
const local = declarationsIn(source).find((node) => declarationName(node) === rootName2);
|
|
798
|
+
if (local === undefined)
|
|
799
|
+
return null;
|
|
800
|
+
const symbol = await declarationSymbol(project, local);
|
|
801
|
+
if (symbol === undefined)
|
|
802
|
+
return null;
|
|
803
|
+
let current2 = { node: local, symbol };
|
|
804
|
+
for (const member of members) {
|
|
805
|
+
const type = await typeForNode(current2.node, current2.symbol, project);
|
|
806
|
+
const property = await project.checker.getPropertyOfType(type, member);
|
|
807
|
+
if (property === undefined)
|
|
808
|
+
return null;
|
|
809
|
+
const node = await property.declarations[0]?.resolve(project);
|
|
810
|
+
if (node === undefined)
|
|
811
|
+
return null;
|
|
812
|
+
current2 = { node, symbol: property };
|
|
813
|
+
}
|
|
814
|
+
return current2;
|
|
815
|
+
}
|
|
816
|
+
const parts = symbolName.split(".");
|
|
817
|
+
const rootName = parts[0];
|
|
818
|
+
const match = matches.find((candidate) => candidate.exportedName === rootName || declarationName(candidate.node) === rootName);
|
|
819
|
+
if (match === undefined)
|
|
820
|
+
return null;
|
|
821
|
+
let current = match;
|
|
822
|
+
for (const member of parts.slice(1)) {
|
|
823
|
+
const type = await typeForNode(current.node, current.symbol, project);
|
|
824
|
+
const property = await project.checker.getPropertyOfType(type, member);
|
|
825
|
+
if (property === undefined)
|
|
826
|
+
return null;
|
|
827
|
+
const node = await property.declarations[0]?.resolve(project);
|
|
828
|
+
if (node === undefined)
|
|
829
|
+
return null;
|
|
830
|
+
current = { node, symbol: property };
|
|
831
|
+
}
|
|
832
|
+
return current;
|
|
833
|
+
};
|
|
834
|
+
var typeForNode = async (node, symbol, project) => {
|
|
835
|
+
if (isClassDeclaration(node) || isEnumDeclaration(node) || isInterfaceDeclaration(node) || isTypeAliasDeclaration(node)) {
|
|
836
|
+
return project.checker.getDeclaredTypeOfSymbol(symbol);
|
|
837
|
+
}
|
|
838
|
+
return project.checker.getTypeOfSymbol(symbol);
|
|
839
|
+
};
|
|
840
|
+
var hasOptional = (symbol) => (symbol.flags & SymbolFlags2.Optional) !== SymbolFlags2.None;
|
|
841
|
+
var propertiesFor = async (type, project, location, root, includeFrom, flags = TYPE_FLAGS) => {
|
|
842
|
+
const properties = await project.checker.getPropertiesOfType(type);
|
|
843
|
+
if (properties.length === 0 || properties.length > MAX_PROPERTIES)
|
|
844
|
+
return [];
|
|
845
|
+
const declarations = await Promise.all(properties.map(async (property) => {
|
|
846
|
+
const handle = property.declarations[0];
|
|
847
|
+
return handle === undefined ? undefined : await handle.resolve(project);
|
|
848
|
+
}));
|
|
849
|
+
const packageRoot = resolve5(root);
|
|
850
|
+
const included = [];
|
|
851
|
+
for (let index = 0;index < properties.length; index += 1) {
|
|
852
|
+
const property = properties[index];
|
|
853
|
+
const declaration = declarations[index];
|
|
854
|
+
if (includeFrom && declaration !== undefined && resolve5(declaration.getSourceFile().fileName).includes(`${packageRoot}/node_modules/`)) {
|
|
855
|
+
continue;
|
|
856
|
+
}
|
|
857
|
+
included.push({ property, declaration });
|
|
858
|
+
}
|
|
859
|
+
if (included.length === 0)
|
|
860
|
+
return [];
|
|
861
|
+
const propertyTypes = await project.checker.getTypeOfSymbol(included.map((item) => item.property));
|
|
862
|
+
const typeByProperty = new Map;
|
|
863
|
+
for (let index = 0;index < included.length; index += 1) {
|
|
864
|
+
typeByProperty.set(included[index].property, propertyTypes[index]);
|
|
865
|
+
}
|
|
866
|
+
return Promise.all(included.map(async ({ property, declaration }) => {
|
|
867
|
+
const propertyType = typeByProperty.get(property);
|
|
868
|
+
const result = {
|
|
869
|
+
name: property.name,
|
|
870
|
+
type: await project.checker.typeToString(propertyType, declaration ?? location, flags),
|
|
871
|
+
...hasOptional(property) ? { optional: true } : {}
|
|
872
|
+
};
|
|
873
|
+
if (includeFrom && declaration !== undefined) {
|
|
874
|
+
return { ...result, from: relativePath(root, declaration.getSourceFile().fileName) };
|
|
875
|
+
}
|
|
876
|
+
return result;
|
|
877
|
+
}));
|
|
878
|
+
};
|
|
879
|
+
var makeTypeInfo = async (match, project, packageInfo, root) => {
|
|
880
|
+
const type = await typeForNode(match.node, match.symbol, project);
|
|
881
|
+
const kind = declarationName(match.node) === undefined ? SyntaxKind[match.node.kind] ?? "unknown" : kindToString(match.node.kind);
|
|
882
|
+
const info = {
|
|
883
|
+
name: match.symbol.name,
|
|
884
|
+
kind,
|
|
885
|
+
type: await project.checker.typeToString(type, match.node, TYPE_FLAGS),
|
|
886
|
+
location: { file: relativePath(root, match.node.getSourceFile().fileName), line: match.node.getSourceFile().getLineAndCharacterOfPosition(match.node.getStart()).line + 1 },
|
|
887
|
+
package: packageInfo.name
|
|
888
|
+
};
|
|
889
|
+
const withSignature = isClassDeclaration(match.node) ? { ...info, signature: `class ${match.symbol.name}` } : isInterfaceDeclaration(match.node) ? { ...info, signature: `interface ${match.symbol.name}` } : isTypeAliasDeclaration(match.node) ? { ...info, signature: `type ${match.symbol.name}` } : info;
|
|
890
|
+
let result = withSignature;
|
|
891
|
+
if (isFunctionDeclaration(match.node)) {
|
|
892
|
+
const signatures = await project.checker.getSignaturesOfType(type, SignatureKind.Call);
|
|
893
|
+
const texts = await Promise.all(signatures.map(async (signature2) => signature2.declaration === undefined ? undefined : (await signature2.declaration.resolve(project))?.getText()));
|
|
894
|
+
const signature = texts.filter((text) => text !== undefined).join(`
|
|
895
|
+
`);
|
|
896
|
+
if (signature.length > 0)
|
|
897
|
+
result = { ...result, signature };
|
|
898
|
+
}
|
|
899
|
+
const properties = await propertiesFor(type, project, match.node, root, true);
|
|
900
|
+
return properties.length > 0 ? { ...result, properties } : result;
|
|
901
|
+
};
|
|
902
|
+
var metadataFor = async (project, source) => {
|
|
903
|
+
const matches = await exportMatchesFor(project, source);
|
|
904
|
+
const direct = declarationsIn(source).flatMap((node) => {
|
|
905
|
+
const name = declarationName(node);
|
|
906
|
+
if (name === undefined)
|
|
907
|
+
return [];
|
|
908
|
+
return [{ node, name, exported: isExported(source.statements.find((statement) => statement.pos <= node.pos && statement.end >= node.end) ?? node), isDefaultExport: isDefault(node) }];
|
|
909
|
+
});
|
|
910
|
+
const aliases = matches.flatMap((match) => {
|
|
911
|
+
const name = declarationName(match.node);
|
|
912
|
+
if (name === undefined)
|
|
913
|
+
return [];
|
|
914
|
+
return [{ node: match.node, name, exported: true, isDefaultExport: match.exportedName === "default", ...match.exportedName !== name && match.exportedName !== "default" ? { exportedAs: match.exportedName } : {} }];
|
|
915
|
+
});
|
|
916
|
+
const byNode = new Map;
|
|
917
|
+
for (const item of [...direct, ...aliases])
|
|
918
|
+
byNode.set(item.node.pos, item);
|
|
919
|
+
return [...byNode.values()];
|
|
920
|
+
};
|
|
921
|
+
var diagnosticCategory = (category) => DiagnosticCategory2[category];
|
|
922
|
+
var sourceLocation = (source, position) => {
|
|
923
|
+
if (source === undefined || position < 0)
|
|
924
|
+
return;
|
|
925
|
+
const point = source.getLineAndCharacterOfPosition(Math.min(position, source.text.length));
|
|
926
|
+
return { line: point.line + 1, column: point.character + 1 };
|
|
927
|
+
};
|
|
928
|
+
var mapDiagnostic = async (diagnostic, project, root) => {
|
|
929
|
+
const source = diagnostic.fileName === undefined ? undefined : await project.program.getSourceFile(diagnostic.fileName);
|
|
930
|
+
const location = sourceLocation(source, diagnostic.pos);
|
|
931
|
+
const category = diagnosticCategory(diagnostic.category);
|
|
932
|
+
return {
|
|
933
|
+
message: diagnostic.text,
|
|
934
|
+
code: diagnostic.code,
|
|
935
|
+
...category === undefined ? {} : { category },
|
|
936
|
+
...diagnostic.fileName === undefined ? {} : { file: relativePath(root, diagnostic.fileName) },
|
|
937
|
+
...location === undefined ? {} : { line: location.line, column: location.column }
|
|
938
|
+
};
|
|
939
|
+
};
|
|
940
|
+
var findNodeAt = (node, position) => {
|
|
941
|
+
let best = node;
|
|
942
|
+
node.forEachChild((child) => {
|
|
943
|
+
if (child.pos <= position && position <= child.end)
|
|
944
|
+
best = findNodeAt(child, position);
|
|
945
|
+
});
|
|
946
|
+
return best;
|
|
947
|
+
};
|
|
948
|
+
var semanticTypeNode = (node) => {
|
|
949
|
+
let current = node.parent;
|
|
950
|
+
while (current !== undefined && current.kind !== SyntaxKind.SourceFile) {
|
|
951
|
+
if (isTypeReferenceNode(current)) {
|
|
952
|
+
return node.pos >= current.typeName.pos && node.end <= current.typeName.end ? current : node;
|
|
953
|
+
}
|
|
954
|
+
current = current.parent;
|
|
955
|
+
}
|
|
956
|
+
return node;
|
|
957
|
+
};
|
|
958
|
+
var evaluateTypeExpression = async (expression, context, packageName, registry) => {
|
|
959
|
+
const normalized = expression.trim();
|
|
960
|
+
if (normalized.length === 0)
|
|
961
|
+
return { error: `Could not evaluate type expression: ${expression}` };
|
|
962
|
+
const pkg = context.package(packageName);
|
|
963
|
+
return withVirtualFile(registry, "", async (lease) => {
|
|
964
|
+
const imports = await context.withProject((project) => synthesizePackageImports(project, pkg.path, lease.path), packageName);
|
|
965
|
+
const content = `${imports.content}type __QuartzEval = ${normalized}
|
|
966
|
+
const __QuartzEvalValue: ${normalized} = undefined as unknown as ${normalized}
|
|
967
|
+
void __QuartzEvalValue
|
|
968
|
+
`;
|
|
969
|
+
return context.workspace.withVirtualFile(pkg.tsconfigPath, lease.path, content, async (project, filePath) => {
|
|
970
|
+
const source = await project.program.getSourceFile(filePath);
|
|
971
|
+
const declaration = source?.statements.filter(isVariableStatement).flatMap((statement) => statement.declarationList.declarations).find((candidate) => isIdentifier(candidate.name) && candidate.name.text === "__QuartzEvalValue");
|
|
972
|
+
if (source === undefined || declaration === undefined) {
|
|
973
|
+
return { error: `Could not evaluate type expression: ${expression}` };
|
|
974
|
+
}
|
|
975
|
+
const diagnostics = (await project.program.getSemanticDiagnostics(filePath)).filter((diagnostic) => diagnostic.category === DiagnosticCategory2.Error);
|
|
976
|
+
if (diagnostics[0] !== undefined)
|
|
977
|
+
return { error: diagnostics[0].text };
|
|
978
|
+
const type = await project.checker.getTypeAtLocation(declaration);
|
|
979
|
+
const [result, expanded] = await Promise.all([
|
|
980
|
+
project.checker.typeToString(type, declaration, TYPE_FLAGS),
|
|
981
|
+
project.checker.typeToString(type, declaration, EXPAND_FLAGS)
|
|
982
|
+
]);
|
|
983
|
+
return {
|
|
984
|
+
result,
|
|
985
|
+
expanded: expanded === result ? result : expanded
|
|
986
|
+
};
|
|
987
|
+
});
|
|
988
|
+
});
|
|
989
|
+
};
|
|
990
|
+
var createLeafOperations = (context) => {
|
|
991
|
+
const virtualFiles = createVirtualFileRegistry(resolveVirtualFileDirectory(context.root), "__quartz_type_eval_");
|
|
992
|
+
const getPackages = async () => context.packages;
|
|
993
|
+
const listSymbols = (options = {}) => context.withProject(async (project, pkg, revision) => {
|
|
994
|
+
const sourceFiles = await sourceFilesFor(context, project, pkg, revision);
|
|
995
|
+
const matches = await exportedMatchesFor(context, project, pkg, revision, sourceFiles);
|
|
996
|
+
const namePattern = options.pattern === undefined ? undefined : new RegExp(options.pattern, "i");
|
|
997
|
+
const filePattern = options.file === undefined ? undefined : new RegExp(options.file, "i");
|
|
998
|
+
const all = [];
|
|
999
|
+
for (const match of matches) {
|
|
1000
|
+
const name = match.exportedName === "default" ? declarationName(match.node) ?? "default" : match.exportedName ?? declarationName(match.node);
|
|
1001
|
+
if (name === undefined)
|
|
1002
|
+
continue;
|
|
1003
|
+
const file = relativePath(context.root, match.node.getSourceFile().fileName);
|
|
1004
|
+
const isIndexExport = /(?:^|\/)index(?:\.[cm]?[jt]sx?)?$/.test(file);
|
|
1005
|
+
if (options.indexOnly === true && !isIndexExport)
|
|
1006
|
+
continue;
|
|
1007
|
+
if (filePattern !== undefined && !filePattern.test(file))
|
|
1008
|
+
continue;
|
|
1009
|
+
const kind = kindToString(match.node.kind);
|
|
1010
|
+
if (options.kind !== undefined && options.kind !== "all" && options.kind !== kind)
|
|
1011
|
+
continue;
|
|
1012
|
+
if (namePattern !== undefined && !namePattern.test(name))
|
|
1013
|
+
continue;
|
|
1014
|
+
all.push({ name, kind, file, line: match.node.getSourceFile().getLineAndCharacterOfPosition(match.node.getStart()).line + 1, package: pkg.name, isIndexExport });
|
|
1015
|
+
}
|
|
1016
|
+
all.sort((left, right) => left.isIndexExport === right.isIndexExport ? left.name.localeCompare(right.name) : left.isIndexExport ? -1 : 1);
|
|
1017
|
+
const limit = options.limit ?? 100;
|
|
1018
|
+
return { symbols: all.slice(0, limit), total: all.length, truncated: all.length > limit, package: pkg.name };
|
|
1019
|
+
}, options.packageName);
|
|
1020
|
+
const getTypeInfo = (symbolName, packageName) => context.withProject(async (project, pkg, revision) => {
|
|
1021
|
+
const sourceFiles = await sourceFilesFor(context, project, pkg, revision);
|
|
1022
|
+
const matches = await exportedMatchesFor(context, project, pkg, revision, sourceFiles);
|
|
1023
|
+
const match = await findMatch(project, sourceFiles, context.root, symbolName, matches);
|
|
1024
|
+
return match === null ? null : makeTypeInfo(match, project, pkg, context.root);
|
|
1025
|
+
}, packageName);
|
|
1026
|
+
const expandType = (symbolName, packageName) => context.withProject(async (project, pkg, revision) => {
|
|
1027
|
+
const sourceFiles = await sourceFilesFor(context, project, pkg, revision);
|
|
1028
|
+
const matches = await exportedMatchesFor(context, project, pkg, revision, sourceFiles);
|
|
1029
|
+
const match = await findMatch(project, sourceFiles, context.root, symbolName, matches);
|
|
1030
|
+
if (match === null)
|
|
1031
|
+
return null;
|
|
1032
|
+
const type = await typeForNode(match.node, match.symbol, project);
|
|
1033
|
+
const [rendered, properties] = await Promise.all([
|
|
1034
|
+
project.checker.typeToString(type, match.node, EXPAND_FLAGS),
|
|
1035
|
+
propertiesFor(type, project, match.node, context.root, true, EXPAND_FLAGS)
|
|
1036
|
+
]);
|
|
1037
|
+
return { original: rendered, expanded: rendered, properties };
|
|
1038
|
+
}, packageName);
|
|
1039
|
+
const searchTypes = (options) => context.withProject(async (project, pkg, revision) => {
|
|
1040
|
+
const sourceFiles = await sourceFilesFor(context, project, pkg, revision);
|
|
1041
|
+
const matches = await exportedMatchesFor(context, project, pkg, revision, sourceFiles);
|
|
1042
|
+
const pattern = options.pattern ?? options.query;
|
|
1043
|
+
const regex = pattern === undefined ? undefined : new RegExp(pattern, "i");
|
|
1044
|
+
const results = [];
|
|
1045
|
+
for (const match of matches) {
|
|
1046
|
+
const name = match.exportedName === "default" ? declarationName(match.node) ?? "default" : match.exportedName ?? declarationName(match.node);
|
|
1047
|
+
if (name === undefined || regex !== undefined && !regex.test(name))
|
|
1048
|
+
continue;
|
|
1049
|
+
const type = await typeForNode(match.node, match.symbol, project);
|
|
1050
|
+
if (options.hasProperty !== undefined && await project.checker.getPropertyOfType(type, options.hasProperty) === undefined)
|
|
1051
|
+
continue;
|
|
1052
|
+
if (options.extends !== undefined) {
|
|
1053
|
+
const bases = await type.getBaseTypes();
|
|
1054
|
+
if (bases === undefined || !(await Promise.all(bases.map(async (base) => await (await base.getSymbol())?.name === options.extends))).some(Boolean))
|
|
1055
|
+
continue;
|
|
1056
|
+
}
|
|
1057
|
+
results.push(await makeTypeInfo(match, project, pkg, context.root));
|
|
1058
|
+
if (results.length >= (options.limit ?? 25))
|
|
1059
|
+
break;
|
|
1060
|
+
}
|
|
1061
|
+
return results;
|
|
1062
|
+
}, options.packageName);
|
|
1063
|
+
const evalType = (expression, packageName) => evaluateTypeExpression(expression, context, packageName, virtualFiles);
|
|
1064
|
+
const getFileDeclarations = (file, options = {}) => context.withProject(async (project, pkg, revision) => {
|
|
1065
|
+
const sourceFiles = await sourceFilesFor(context, project, pkg, revision);
|
|
1066
|
+
const target = isAbsolute(file) ? resolve5(file) : resolve5(context.root, file);
|
|
1067
|
+
const source = sourceFiles.find((candidate) => resolve5(candidate.fileName) === target || candidate.fileName.endsWith(file));
|
|
1068
|
+
if (source === undefined)
|
|
1069
|
+
return null;
|
|
1070
|
+
const filter = options.symbol === undefined ? undefined : new RegExp(options.symbol, "i");
|
|
1071
|
+
const metadata = await metadataFor(project, source);
|
|
1072
|
+
const declarations = [];
|
|
1073
|
+
for (const item of metadata) {
|
|
1074
|
+
if (filter !== undefined && !filter.test(item.name))
|
|
1075
|
+
continue;
|
|
1076
|
+
if (!item.exported && options.includePrivate !== true)
|
|
1077
|
+
continue;
|
|
1078
|
+
let info = { name: item.name, kind: kindToString(item.node.kind), line: source.getLineAndCharacterOfPosition(item.node.getStart()).line + 1, exported: item.exported, isDefaultExport: item.isDefaultExport, ...item.exportedAs === undefined ? {} : { exportedAs: item.exportedAs } };
|
|
1079
|
+
const symbol = await declarationSymbol(project, item.node);
|
|
1080
|
+
if (symbol !== undefined) {
|
|
1081
|
+
const type = await typeForNode(item.node, symbol, project);
|
|
1082
|
+
if (info.kind !== "class" && info.kind !== "interface" && info.kind !== "enum")
|
|
1083
|
+
info = { ...info, type: await project.checker.typeToString(type, item.node, TYPE_FLAGS) };
|
|
1084
|
+
if (info.kind === "function") {
|
|
1085
|
+
const signatures = await project.checker.getSignaturesOfType(type, SignatureKind.Call);
|
|
1086
|
+
const parts = await Promise.all(signatures.map(async (signature) => {
|
|
1087
|
+
const params = await signature.getParameters();
|
|
1088
|
+
const rendered = await Promise.all(params.map(async (parameter) => `${parameter.name}: ${await project.checker.typeToString(await project.checker.getTypeOfSymbolAtLocation(parameter, item.node), item.node, TYPE_FLAGS)}`));
|
|
1089
|
+
return `(${rendered.join(", ")}) => ${await project.checker.typeToString(await project.checker.getReturnTypeOfSignature(signature), item.node, TYPE_FLAGS)}`;
|
|
1090
|
+
}));
|
|
1091
|
+
if (parts.length > 0)
|
|
1092
|
+
info = { ...info, signature: parts.join(" | ") };
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
declarations.push(info);
|
|
1096
|
+
}
|
|
1097
|
+
declarations.sort((left, right) => left.exported === right.exported ? left.name.localeCompare(right.name) : left.exported ? -1 : 1);
|
|
1098
|
+
return { file: relativePath(context.root, source.fileName), package: pkg.name, declarations, total: declarations.length };
|
|
1099
|
+
}, options.packageName);
|
|
1100
|
+
const checkCompatibility = (from, to, packageName) => context.withProject(async (project, pkg, revision) => {
|
|
1101
|
+
const sourceFiles = await sourceFilesFor(context, project, pkg, revision);
|
|
1102
|
+
const matches = await exportedMatchesFor(context, project, pkg, revision, sourceFiles);
|
|
1103
|
+
const fromMatch = await findMatch(project, sourceFiles, context.root, from, matches);
|
|
1104
|
+
const toMatch = await findMatch(project, sourceFiles, context.root, to, matches);
|
|
1105
|
+
if (fromMatch === null || toMatch === null) {
|
|
1106
|
+
const missing = fromMatch === null ? from : to;
|
|
1107
|
+
const message = `Symbol "${missing}" not found`;
|
|
1108
|
+
return { compatible: false, from, to, reason: message, issues: [{ kind: "other", message }] };
|
|
1109
|
+
}
|
|
1110
|
+
const fromType = await typeForNode(fromMatch.node, fromMatch.symbol, project);
|
|
1111
|
+
const toType = await typeForNode(toMatch.node, toMatch.symbol, project);
|
|
1112
|
+
const [fromText, toText] = await Promise.all([
|
|
1113
|
+
project.checker.typeToString(fromType, fromMatch.node),
|
|
1114
|
+
project.checker.typeToString(toType, toMatch.node)
|
|
1115
|
+
]);
|
|
1116
|
+
if (await project.checker.isTypeAssignableTo(fromType, toType))
|
|
1117
|
+
return { compatible: true, from: fromText, to: toText };
|
|
1118
|
+
const issues = [];
|
|
1119
|
+
const reasons = [];
|
|
1120
|
+
const [fromProperties, targetProperties] = await Promise.all([
|
|
1121
|
+
project.checker.getPropertiesOfType(fromType),
|
|
1122
|
+
project.checker.getPropertiesOfType(toType)
|
|
1123
|
+
]);
|
|
1124
|
+
const fromNames = new Set(fromProperties.map((property) => property.name));
|
|
1125
|
+
const missingRequired = targetProperties.filter((property) => !hasOptional(property) && !fromNames.has(property.name));
|
|
1126
|
+
if (missingRequired.length > 0) {
|
|
1127
|
+
const missingTypes = await project.checker.getTypeOfSymbol(missingRequired);
|
|
1128
|
+
const expectedTexts = await Promise.all(missingTypes.map((missingType) => project.checker.typeToString(missingType, toMatch.node)));
|
|
1129
|
+
for (let index = 0;index < missingRequired.length; index += 1) {
|
|
1130
|
+
const property = missingRequired[index];
|
|
1131
|
+
const expected = expectedTexts[index];
|
|
1132
|
+
const message = `Property '${property.name}' is missing in type '${fromText}' but required in type '${toText}' (expected: ${expected})`;
|
|
1133
|
+
reasons.push(message);
|
|
1134
|
+
issues.push({ kind: "missing_property", property: property.name, expectedType: expected, message });
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
for (const property of fromProperties) {
|
|
1138
|
+
const target = await project.checker.getPropertyOfType(toType, property.name);
|
|
1139
|
+
if (target === undefined)
|
|
1140
|
+
continue;
|
|
1141
|
+
const [actualType, expectedType] = await Promise.all([
|
|
1142
|
+
project.checker.getTypeOfSymbolAtLocation(property, fromMatch.node),
|
|
1143
|
+
project.checker.getTypeOfSymbolAtLocation(target, toMatch.node)
|
|
1144
|
+
]);
|
|
1145
|
+
if (await project.checker.isTypeAssignableTo(actualType, expectedType))
|
|
1146
|
+
continue;
|
|
1147
|
+
const [actual, expected] = await Promise.all([
|
|
1148
|
+
project.checker.typeToString(actualType, fromMatch.node),
|
|
1149
|
+
project.checker.typeToString(expectedType, toMatch.node)
|
|
1150
|
+
]);
|
|
1151
|
+
const message = `Property '${property.name}' has incompatible types: '${actual}' is not assignable to '${expected}'`;
|
|
1152
|
+
reasons.push(message);
|
|
1153
|
+
issues.push({ kind: "type_mismatch", property: property.name, actualType: actual, expectedType: expected, message });
|
|
1154
|
+
}
|
|
1155
|
+
if (reasons.length === 0) {
|
|
1156
|
+
const message = `Type '${fromText}' is not assignable to type '${toText}'`;
|
|
1157
|
+
reasons.push(message);
|
|
1158
|
+
issues.push({ kind: "other", message });
|
|
1159
|
+
}
|
|
1160
|
+
return { compatible: false, from: fromText, to: toText, reason: reasons.join("; "), issues };
|
|
1161
|
+
}, packageName);
|
|
1162
|
+
const getDiagnostics = (packageNameOrOptions) => context.withProject(async (project, pkg) => {
|
|
1163
|
+
const options = typeof packageNameOrOptions === "object" ? packageNameOrOptions : undefined;
|
|
1164
|
+
const diagnostics = (await Promise.all([project.program.getConfigFileParsingDiagnostics(), project.program.getSyntacticDiagnostics(), project.program.getSemanticDiagnostics()])).flat();
|
|
1165
|
+
const packageRoot = resolve5(pkg.path);
|
|
1166
|
+
const unique = new Map;
|
|
1167
|
+
for (const diagnostic of diagnostics) {
|
|
1168
|
+
if (diagnostic.fileName !== undefined && !(resolve5(diagnostic.fileName) === packageRoot || resolve5(diagnostic.fileName).startsWith(`${packageRoot}/`)))
|
|
1169
|
+
continue;
|
|
1170
|
+
unique.set([diagnostic.fileName ?? "", diagnostic.pos, diagnostic.code, diagnostic.text].join("\x00"), diagnostic);
|
|
1171
|
+
}
|
|
1172
|
+
const errors = await Promise.all([...unique.values()].map((diagnostic) => mapDiagnostic(diagnostic, project, context.root)));
|
|
1173
|
+
if (options?.explain !== true)
|
|
1174
|
+
return errors;
|
|
1175
|
+
const explained = errors.map((error) => ({ ...error, explanation: null }));
|
|
1176
|
+
return { totalErrors: explained.length, explained: 0, truncated: false, errors: explained };
|
|
1177
|
+
}, typeof packageNameOrOptions === "string" ? packageNameOrOptions : packageNameOrOptions?.packageName);
|
|
1178
|
+
const getTypeAtPosition = (filePath, line, column, packageName) => context.withProject(async (project, pkg, revision) => {
|
|
1179
|
+
const sourceFiles = await sourceFilesFor(context, project, pkg, revision);
|
|
1180
|
+
const targetPath = isAbsolute(filePath) ? resolve5(filePath) : resolve5(context.root, filePath);
|
|
1181
|
+
const source = sourceFiles.find((candidate) => resolve5(candidate.fileName) === targetPath || candidate.fileName.endsWith(filePath));
|
|
1182
|
+
if (source === undefined || line < 1 || column < 1)
|
|
1183
|
+
return null;
|
|
1184
|
+
let position;
|
|
1185
|
+
try {
|
|
1186
|
+
position = source.getPositionOfLineAndCharacter(line - 1, column - 1);
|
|
1187
|
+
} catch {
|
|
1188
|
+
return null;
|
|
1189
|
+
}
|
|
1190
|
+
if (position > source.text.length)
|
|
1191
|
+
return null;
|
|
1192
|
+
const node = semanticTypeNode(findNodeAt(source, position));
|
|
1193
|
+
const type = await project.checker.getTypeAtLocation(node);
|
|
1194
|
+
const point = source.getLineAndCharacterOfPosition(node.getStart());
|
|
1195
|
+
const [typeText, expandedText] = await Promise.all([
|
|
1196
|
+
project.checker.typeToString(type, node),
|
|
1197
|
+
project.checker.typeToString(type, node, EXPAND_FLAGS)
|
|
1198
|
+
]);
|
|
1199
|
+
const text = node.getText(source);
|
|
1200
|
+
const nodeText = text.length > MAX_NODE_TEXT ? `${text.slice(0, MAX_NODE_TEXT)}...` : text;
|
|
1201
|
+
return {
|
|
1202
|
+
type: typeText,
|
|
1203
|
+
expanded: expandedText === typeText ? typeText : expandedText,
|
|
1204
|
+
nodeKind: SyntaxKind[node.kind] ?? "unknown",
|
|
1205
|
+
nodeText,
|
|
1206
|
+
location: {
|
|
1207
|
+
file: relativePath(context.root, source.fileName),
|
|
1208
|
+
line: point.line + 1,
|
|
1209
|
+
column: point.character + 1
|
|
1210
|
+
}
|
|
1211
|
+
};
|
|
1212
|
+
}, packageName);
|
|
1213
|
+
const explainType = async (expression, packageName) => {
|
|
1214
|
+
const result = await evaluateTypeExpression(expression, context, packageName, virtualFiles);
|
|
1215
|
+
const final = "error" in result ? `Error: ${result.error}` : result.expanded;
|
|
1216
|
+
return { expression, steps: [{ step: 1, description: `Expand ${expression}`, expression, result: final }], final };
|
|
1217
|
+
};
|
|
1218
|
+
return { getPackages, listSymbols, getTypeInfo, expandType, searchTypes, evalType, getFileDeclarations, checkCompatibility, getDiagnostics, getTypeAtPosition, explainType };
|
|
1219
|
+
};
|
|
1220
|
+
|
|
1221
|
+
// src/reference-operations.ts
|
|
1222
|
+
import { relative as relative4, resolve as resolve6 } from "path";
|
|
1223
|
+
import { SyntaxKind as SyntaxKind2 } from "typescript/unstable/ast";
|
|
1224
|
+
import { isTypeReferenceNode as isTypeReferenceNode2 } from "typescript/unstable/ast/is";
|
|
1225
|
+
var PRIMITIVE_NAMES = {
|
|
1226
|
+
string: true,
|
|
1227
|
+
number: true,
|
|
1228
|
+
boolean: true,
|
|
1229
|
+
undefined: true,
|
|
1230
|
+
null: true,
|
|
1231
|
+
void: true,
|
|
1232
|
+
any: true,
|
|
1233
|
+
never: true,
|
|
1234
|
+
object: true,
|
|
1235
|
+
symbol: true,
|
|
1236
|
+
bigint: true,
|
|
1237
|
+
Date: true,
|
|
1238
|
+
Array: true,
|
|
1239
|
+
Object: true,
|
|
1240
|
+
String: true,
|
|
1241
|
+
Number: true,
|
|
1242
|
+
Boolean: true,
|
|
1243
|
+
Promise: true,
|
|
1244
|
+
Map: true,
|
|
1245
|
+
Set: true,
|
|
1246
|
+
WeakMap: true,
|
|
1247
|
+
WeakSet: true,
|
|
1248
|
+
Error: true,
|
|
1249
|
+
Function: true
|
|
1250
|
+
};
|
|
1251
|
+
var MAX_RENAME_LOCATIONS = 100;
|
|
1252
|
+
var MAX_STRING_OR_COMMENT_LOCATIONS = 20;
|
|
1253
|
+
var MAX_REFERENCE_RESULTS_PER_FILE = 100;
|
|
1254
|
+
var MAX_GRAPH_DEPTH = 4;
|
|
1255
|
+
var createReferenceOperations = (context) => ({
|
|
1256
|
+
findRelated: (symbolName, packageName) => context.withProject(async (project, pkg, revision) => {
|
|
1257
|
+
const target = await findTarget(context, project, pkg, revision, symbolName);
|
|
1258
|
+
if (target === null)
|
|
1259
|
+
return null;
|
|
1260
|
+
return {
|
|
1261
|
+
symbol: symbolName,
|
|
1262
|
+
referencedBy: await findIncomingReferences(project, target),
|
|
1263
|
+
references: await findOutgoingReferences(project, target)
|
|
1264
|
+
};
|
|
1265
|
+
}, packageName),
|
|
1266
|
+
generateGraph: (symbolName, options = {}) => context.withProject(async (project, pkg, revision) => {
|
|
1267
|
+
const depth = Math.max(0, Math.min(options.depth ?? 2, MAX_GRAPH_DEPTH));
|
|
1268
|
+
const format = options.format ?? "mermaid";
|
|
1269
|
+
const targets = targetIndexFor(context, project, pkg, revision);
|
|
1270
|
+
const rootTarget = await targets.lookup(symbolName);
|
|
1271
|
+
if (rootTarget === null)
|
|
1272
|
+
return null;
|
|
1273
|
+
const nodes = [symbolName];
|
|
1274
|
+
const nodeIds = new Set([String(rootTarget.symbol.id)]);
|
|
1275
|
+
const edges = [];
|
|
1276
|
+
const edgeKeys = new Set;
|
|
1277
|
+
const visited = new Set;
|
|
1278
|
+
const visit = async (target, displayName, currentDepth) => {
|
|
1279
|
+
const targetId = String(target.symbol.id);
|
|
1280
|
+
if (visited.has(targetId) || currentDepth > depth)
|
|
1281
|
+
return;
|
|
1282
|
+
visited.add(targetId);
|
|
1283
|
+
const outgoing = await findOutgoingReferences(project, target);
|
|
1284
|
+
if (currentDepth >= depth)
|
|
1285
|
+
return;
|
|
1286
|
+
for (const reference of outgoing) {
|
|
1287
|
+
const child = await targets.lookup(reference.symbol);
|
|
1288
|
+
if (child === null)
|
|
1289
|
+
continue;
|
|
1290
|
+
const childId = String(child.symbol.id);
|
|
1291
|
+
const childName = child.symbol.name;
|
|
1292
|
+
if (!nodeIds.has(childId)) {
|
|
1293
|
+
nodes.push(childName);
|
|
1294
|
+
}
|
|
1295
|
+
const edgeKey = `${targetId}:${childId}:${reference.context}`;
|
|
1296
|
+
if (!edgeKeys.has(edgeKey)) {
|
|
1297
|
+
edgeKeys.add(edgeKey);
|
|
1298
|
+
edges.push({ from: displayName, to: childName, label: reference.context });
|
|
1299
|
+
}
|
|
1300
|
+
await visit(child, childName, currentDepth + 1);
|
|
1301
|
+
}
|
|
1302
|
+
};
|
|
1303
|
+
await visit(rootTarget, symbolName, 0);
|
|
1304
|
+
return {
|
|
1305
|
+
root: symbolName,
|
|
1306
|
+
format,
|
|
1307
|
+
depth,
|
|
1308
|
+
nodes,
|
|
1309
|
+
edges,
|
|
1310
|
+
graph: format === "mermaid" ? toMermaid(edges) : toDot(edges)
|
|
1311
|
+
};
|
|
1312
|
+
}, options.packageName),
|
|
1313
|
+
previewRefactor: (options) => context.withProject(async (project, pkg, revision) => {
|
|
1314
|
+
if (options.action !== "rename") {
|
|
1315
|
+
throw new Error(`Unsupported refactor action: ${options.action}`);
|
|
1316
|
+
}
|
|
1317
|
+
const target = await findTarget(context, project, pkg, revision, options.symbol);
|
|
1318
|
+
if (target === null) {
|
|
1319
|
+
throw new Error(`Symbol "${options.symbol}" not found`);
|
|
1320
|
+
}
|
|
1321
|
+
const sites = await findRenameSites(project, target);
|
|
1322
|
+
const locations = [];
|
|
1323
|
+
const predictedErrors = [];
|
|
1324
|
+
const affectedFiles = new Set;
|
|
1325
|
+
for (const site of sites) {
|
|
1326
|
+
const sourceFile = site.sourceFile;
|
|
1327
|
+
const file = relativePath2(context.root, sourceFile.fileName);
|
|
1328
|
+
const position = sourceFile.getLineAndCharacterOfPosition(site.node.getStart(sourceFile));
|
|
1329
|
+
const line = position.line + 1;
|
|
1330
|
+
const lineText = getLineText(sourceFile, position.line);
|
|
1331
|
+
const before = lineText.trim();
|
|
1332
|
+
const after = replaceSiteOnTrimmedLine(lineText, before, position.character, site.node.getText(sourceFile), options.to);
|
|
1333
|
+
affectedFiles.add(sourceFile.fileName);
|
|
1334
|
+
addRenameSafetyError(predictedErrors, sourceFile, file, pkg.path, line);
|
|
1335
|
+
locations.push({ file, line, column: position.character + 1, before, after });
|
|
1336
|
+
}
|
|
1337
|
+
const stringLiteralLocations = [];
|
|
1338
|
+
const commentLocations = [];
|
|
1339
|
+
for (const fileName of affectedFiles) {
|
|
1340
|
+
const sourceFile = await project.program.getSourceFile(fileName);
|
|
1341
|
+
if (sourceFile === undefined)
|
|
1342
|
+
continue;
|
|
1343
|
+
collectStringLiteralLocations(sourceFile, context.root, options.symbol, stringLiteralLocations);
|
|
1344
|
+
collectCommentLocations(sourceFile, context.root, options.symbol, commentLocations);
|
|
1345
|
+
}
|
|
1346
|
+
const safetyNotes = [];
|
|
1347
|
+
if (stringLiteralLocations.length > 0) {
|
|
1348
|
+
safetyNotes.push(`${stringLiteralLocations.length} string literal(s) contain "${options.symbol}" and won't be renamed automatically`);
|
|
1349
|
+
}
|
|
1350
|
+
if (commentLocations.length > 0) {
|
|
1351
|
+
safetyNotes.push(`${commentLocations.length} comment(s) contain "${options.symbol}" and may need manual review`);
|
|
1352
|
+
}
|
|
1353
|
+
return {
|
|
1354
|
+
action: "rename",
|
|
1355
|
+
from: options.symbol,
|
|
1356
|
+
to: options.to,
|
|
1357
|
+
locations: locations.slice(0, MAX_RENAME_LOCATIONS),
|
|
1358
|
+
totalLocations: locations.length,
|
|
1359
|
+
predictedErrors,
|
|
1360
|
+
confidence: predictedErrors.length > 0 ? "low" : "high",
|
|
1361
|
+
safe: predictedErrors.length === 0 && safetyNotes.length === 0,
|
|
1362
|
+
safetyNotes,
|
|
1363
|
+
stringLiteralLocations: stringLiteralLocations.slice(0, MAX_STRING_OR_COMMENT_LOCATIONS),
|
|
1364
|
+
commentLocations: commentLocations.slice(0, MAX_STRING_OR_COMMENT_LOCATIONS)
|
|
1365
|
+
};
|
|
1366
|
+
}, options.packageName)
|
|
1367
|
+
});
|
|
1368
|
+
var findTarget = (context, project, pkg, revision, requestedName) => targetIndexFor(context, project, pkg, revision).lookup(requestedName);
|
|
1369
|
+
var targetIndexFor = (context, project, pkg, revision) => context.cacheForRevision(`target-index:${pkg.tsconfigPath}`, revision, () => {
|
|
1370
|
+
const memo = new Map;
|
|
1371
|
+
let declarationIndex;
|
|
1372
|
+
const loadDeclarationIndex = () => {
|
|
1373
|
+
declarationIndex ??= buildDeclarationTargetIndex(project, pkg.path, context.root);
|
|
1374
|
+
return declarationIndex;
|
|
1375
|
+
};
|
|
1376
|
+
return {
|
|
1377
|
+
lookup: (requestedName) => {
|
|
1378
|
+
const cached = memo.get(requestedName);
|
|
1379
|
+
if (cached !== undefined)
|
|
1380
|
+
return cached;
|
|
1381
|
+
const pending = (async () => {
|
|
1382
|
+
const byName = await loadDeclarationIndex();
|
|
1383
|
+
const fromIndex = byName.get(requestedName);
|
|
1384
|
+
if (fromIndex !== undefined)
|
|
1385
|
+
return fromIndex;
|
|
1386
|
+
return findTargetByScan(project, pkg.path, context.root, requestedName);
|
|
1387
|
+
})();
|
|
1388
|
+
memo.set(requestedName, pending);
|
|
1389
|
+
return pending;
|
|
1390
|
+
}
|
|
1391
|
+
};
|
|
1392
|
+
});
|
|
1393
|
+
var buildDeclarationTargetIndex = async (project, packagePath, rootPath) => {
|
|
1394
|
+
const byName = new Map;
|
|
1395
|
+
const sourceFiles = await projectSourceFiles(project, packagePath);
|
|
1396
|
+
for (const sourceFile of sourceFiles) {
|
|
1397
|
+
const named = [];
|
|
1398
|
+
visit(sourceFile, (node) => {
|
|
1399
|
+
if (node.kind === SyntaxKind2.Identifier && declarationForName(node) !== undefined) {
|
|
1400
|
+
named.push(node);
|
|
1401
|
+
return;
|
|
1402
|
+
}
|
|
1403
|
+
if (node.kind === SyntaxKind2.ExportSpecifier) {
|
|
1404
|
+
const exportName = node.name;
|
|
1405
|
+
if (exportName.kind === SyntaxKind2.Identifier)
|
|
1406
|
+
named.push(exportName);
|
|
1407
|
+
}
|
|
1408
|
+
});
|
|
1409
|
+
if (named.length === 0)
|
|
1410
|
+
continue;
|
|
1411
|
+
const symbols = await project.checker.getSymbolAtLocation(named);
|
|
1412
|
+
for (let index = 0;index < named.length; index += 1) {
|
|
1413
|
+
const nameNode = named[index];
|
|
1414
|
+
const key = nameNode.getText(sourceFile);
|
|
1415
|
+
if (byName.has(key))
|
|
1416
|
+
continue;
|
|
1417
|
+
const symbol = symbols[index];
|
|
1418
|
+
if (symbol === undefined)
|
|
1419
|
+
continue;
|
|
1420
|
+
const resolved = await resolveSymbol(project, symbol);
|
|
1421
|
+
const declaration = declarationForName(nameNode) ?? await findDeclarationForSymbol(sourceFile, project, resolved);
|
|
1422
|
+
if (declaration !== undefined) {
|
|
1423
|
+
byName.set(key, { symbol: resolved, declaration, packagePath, rootPath });
|
|
1424
|
+
continue;
|
|
1425
|
+
}
|
|
1426
|
+
const first = resolved.declarations[0];
|
|
1427
|
+
const fallback = first === undefined ? undefined : await first.resolve(project);
|
|
1428
|
+
if (fallback !== undefined)
|
|
1429
|
+
byName.set(key, { symbol: resolved, declaration: fallback, packagePath, rootPath });
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
if (!byName.has("default")) {
|
|
1433
|
+
for (const sourceFile of sourceFiles) {
|
|
1434
|
+
const moduleSymbol = await project.checker.getSymbolAtLocation(sourceFile);
|
|
1435
|
+
if (moduleSymbol === undefined)
|
|
1436
|
+
continue;
|
|
1437
|
+
const exports = await project.checker.getExportsOfModule(moduleSymbol);
|
|
1438
|
+
const defaultExport = exports.find((candidate) => candidate.name === "default");
|
|
1439
|
+
if (defaultExport === undefined)
|
|
1440
|
+
continue;
|
|
1441
|
+
const resolved = await resolveSymbol(project, defaultExport);
|
|
1442
|
+
const declaration = await findDeclarationForSymbol(sourceFile, project, resolved);
|
|
1443
|
+
if (declaration !== undefined) {
|
|
1444
|
+
byName.set("default", { symbol: resolved, declaration, packagePath, rootPath });
|
|
1445
|
+
break;
|
|
1446
|
+
}
|
|
1447
|
+
const first = resolved.declarations[0];
|
|
1448
|
+
const node = first === undefined ? undefined : await first.resolve(project);
|
|
1449
|
+
if (node !== undefined) {
|
|
1450
|
+
byName.set("default", { symbol: resolved, declaration: node, packagePath, rootPath });
|
|
1451
|
+
break;
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
return byName;
|
|
1456
|
+
};
|
|
1457
|
+
var findTargetByScan = async (project, packagePath, rootPath, requestedName) => {
|
|
1458
|
+
const sourceFiles = await projectSourceFiles(project, packagePath);
|
|
1459
|
+
let canonical;
|
|
1460
|
+
for (const sourceFile of sourceFiles) {
|
|
1461
|
+
const identifiers = [];
|
|
1462
|
+
visit(sourceFile, (node) => {
|
|
1463
|
+
if (node.kind === SyntaxKind2.Identifier && node.getText(sourceFile) === requestedName)
|
|
1464
|
+
identifiers.push(node);
|
|
1465
|
+
});
|
|
1466
|
+
if (identifiers.length === 0)
|
|
1467
|
+
continue;
|
|
1468
|
+
const symbols = await project.checker.getSymbolAtLocation(identifiers);
|
|
1469
|
+
for (let index = 0;index < identifiers.length; index += 1) {
|
|
1470
|
+
const identifier = identifiers[index];
|
|
1471
|
+
const symbol = symbols[index];
|
|
1472
|
+
if (symbol === undefined)
|
|
1473
|
+
continue;
|
|
1474
|
+
const resolved = await resolveSymbol(project, symbol);
|
|
1475
|
+
canonical ??= resolved;
|
|
1476
|
+
if (resolved.id !== canonical.id)
|
|
1477
|
+
continue;
|
|
1478
|
+
const declaration = declarationForName(identifier);
|
|
1479
|
+
if (declaration !== undefined)
|
|
1480
|
+
return { symbol: resolved, declaration, packagePath, rootPath };
|
|
1481
|
+
}
|
|
1482
|
+
if (canonical !== undefined)
|
|
1483
|
+
break;
|
|
1484
|
+
}
|
|
1485
|
+
if (canonical !== undefined) {
|
|
1486
|
+
for (const sourceFile of sourceFiles) {
|
|
1487
|
+
const declaration2 = await findDeclarationForSymbol(sourceFile, project, canonical);
|
|
1488
|
+
if (declaration2 !== undefined)
|
|
1489
|
+
return { symbol: canonical, declaration: declaration2, packagePath, rootPath };
|
|
1490
|
+
}
|
|
1491
|
+
const first = canonical.declarations[0];
|
|
1492
|
+
const declaration = first === undefined ? undefined : await first.resolve(project);
|
|
1493
|
+
if (declaration !== undefined)
|
|
1494
|
+
return { symbol: canonical, declaration, packagePath, rootPath };
|
|
1495
|
+
}
|
|
1496
|
+
if (requestedName === "default") {
|
|
1497
|
+
for (const sourceFile of sourceFiles) {
|
|
1498
|
+
const moduleSymbol = await project.checker.getSymbolAtLocation(sourceFile);
|
|
1499
|
+
if (moduleSymbol === undefined)
|
|
1500
|
+
continue;
|
|
1501
|
+
const exports = await project.checker.getExportsOfModule(moduleSymbol);
|
|
1502
|
+
const defaultExport = exports.find((candidate) => candidate.name === "default");
|
|
1503
|
+
if (defaultExport === undefined)
|
|
1504
|
+
continue;
|
|
1505
|
+
const resolved = await resolveSymbol(project, defaultExport);
|
|
1506
|
+
const declaration = await findDeclarationForSymbol(sourceFile, project, resolved);
|
|
1507
|
+
if (declaration !== undefined)
|
|
1508
|
+
return { symbol: resolved, declaration, packagePath, rootPath };
|
|
1509
|
+
const first = resolved.declarations[0];
|
|
1510
|
+
const node = first === undefined ? undefined : await first.resolve(project);
|
|
1511
|
+
if (node !== undefined)
|
|
1512
|
+
return { symbol: resolved, declaration: node, packagePath, rootPath };
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
return null;
|
|
1516
|
+
};
|
|
1517
|
+
var findDeclarationForSymbol = async (sourceFile, project, target) => {
|
|
1518
|
+
for (const handle of target.declarations) {
|
|
1519
|
+
const declaration = await handle.resolve(project);
|
|
1520
|
+
if (declaration !== undefined && declaration.getSourceFile().fileName === sourceFile.fileName)
|
|
1521
|
+
return declaration;
|
|
1522
|
+
}
|
|
1523
|
+
return;
|
|
1524
|
+
};
|
|
1525
|
+
var findIncomingReferences = async (project, target) => {
|
|
1526
|
+
const results = [];
|
|
1527
|
+
const seen = new Set;
|
|
1528
|
+
const perFileCounts = new Map;
|
|
1529
|
+
const add = async (node, symbol = target.symbol) => {
|
|
1530
|
+
if (node.kind !== SyntaxKind2.Identifier || isImportReference(node))
|
|
1531
|
+
return;
|
|
1532
|
+
const sourceFile = node.getSourceFile();
|
|
1533
|
+
if (!isProjectSourceFile(sourceFile.fileName, target.packagePath))
|
|
1534
|
+
return;
|
|
1535
|
+
const fileCount = perFileCounts.get(sourceFile.fileName) ?? 0;
|
|
1536
|
+
if (fileCount >= MAX_REFERENCE_RESULTS_PER_FILE)
|
|
1537
|
+
return;
|
|
1538
|
+
if (!await matchesCanonicalSymbol(project, symbol, target.symbol))
|
|
1539
|
+
return;
|
|
1540
|
+
if (isTargetDeclarationName(node, target))
|
|
1541
|
+
return;
|
|
1542
|
+
const containing = await findContainingSymbol(project, node.parent, target.symbol);
|
|
1543
|
+
const position = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
|
|
1544
|
+
const context = classifyReferenceContext(node.parent);
|
|
1545
|
+
const key = `${sourceFile.fileName}:${node.getStart(sourceFile)}:${containing}:${context}`;
|
|
1546
|
+
if (seen.has(key))
|
|
1547
|
+
return;
|
|
1548
|
+
seen.add(key);
|
|
1549
|
+
perFileCounts.set(sourceFile.fileName, fileCount + 1);
|
|
1550
|
+
results.push({
|
|
1551
|
+
symbol: containing,
|
|
1552
|
+
context,
|
|
1553
|
+
file: relativePath2(target.rootPath, sourceFile.fileName),
|
|
1554
|
+
line: position.line + 1
|
|
1555
|
+
});
|
|
1556
|
+
};
|
|
1557
|
+
const declarationName2 = declarationNameNode(target.declaration) ?? target.declaration;
|
|
1558
|
+
const declarationPosition = declarationName2.getStart(declarationName2.getSourceFile());
|
|
1559
|
+
try {
|
|
1560
|
+
const referenced = await project.checker.getReferencedSymbolsForNode(declarationName2, declarationPosition);
|
|
1561
|
+
for (const entry of referenced) {
|
|
1562
|
+
const handles = [entry.definition, ...entry.references];
|
|
1563
|
+
for (const handle of handles) {
|
|
1564
|
+
const node = await handle.resolve(project);
|
|
1565
|
+
if (node !== undefined)
|
|
1566
|
+
await add(node, entry.symbol ?? target.symbol);
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
if (results.length > 0)
|
|
1570
|
+
return results;
|
|
1571
|
+
} catch {}
|
|
1572
|
+
const sourceFiles = await projectSourceFiles(project, target.packagePath);
|
|
1573
|
+
for (const sourceFile of sourceFiles) {
|
|
1574
|
+
const handles = await project.checker.getReferencesToSymbolInFile(sourceFile.fileName, target.symbol);
|
|
1575
|
+
for (const handle of handles) {
|
|
1576
|
+
const node = await handle.resolve(project);
|
|
1577
|
+
if (node !== undefined)
|
|
1578
|
+
await add(node);
|
|
1579
|
+
}
|
|
1580
|
+
const typeNodes = [];
|
|
1581
|
+
visit(sourceFile, (node) => {
|
|
1582
|
+
if (node.kind === SyntaxKind2.Identifier && isTypeReferencePosition(node))
|
|
1583
|
+
typeNodes.push(node);
|
|
1584
|
+
});
|
|
1585
|
+
if (typeNodes.length === 0)
|
|
1586
|
+
continue;
|
|
1587
|
+
const symbols = await project.checker.getSymbolAtLocation(typeNodes);
|
|
1588
|
+
for (let index = 0;index < typeNodes.length; index += 1) {
|
|
1589
|
+
await add(typeNodes[index], symbols[index]);
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
return results;
|
|
1593
|
+
};
|
|
1594
|
+
var findOutgoingReferences = async (project, target) => {
|
|
1595
|
+
const references = [];
|
|
1596
|
+
const seen = new Set;
|
|
1597
|
+
const add = async (symbol, context) => {
|
|
1598
|
+
if (symbol === undefined)
|
|
1599
|
+
return;
|
|
1600
|
+
const resolved = await resolveSymbol(project, symbol);
|
|
1601
|
+
if (resolved.id === target.symbol.id || PRIMITIVE_NAMES[resolved.name] === true)
|
|
1602
|
+
return;
|
|
1603
|
+
const key = `${resolved.id}:${context}`;
|
|
1604
|
+
if (seen.has(key))
|
|
1605
|
+
return;
|
|
1606
|
+
seen.add(key);
|
|
1607
|
+
references.push({ symbol: resolved.name, context });
|
|
1608
|
+
};
|
|
1609
|
+
const type = await project.checker.getTypeAtLocation(target.declaration);
|
|
1610
|
+
if (await skipOutgoingType(type))
|
|
1611
|
+
return references;
|
|
1612
|
+
const properties = await project.checker.getPropertiesOfType(type);
|
|
1613
|
+
for (const property of properties.slice(0, 50)) {
|
|
1614
|
+
const declarationHandle = property.declarations[0];
|
|
1615
|
+
if (declarationHandle === undefined)
|
|
1616
|
+
continue;
|
|
1617
|
+
const declaration = await declarationHandle.resolve(project);
|
|
1618
|
+
if (declaration === undefined)
|
|
1619
|
+
continue;
|
|
1620
|
+
const propertyType = await project.checker.getTypeOfSymbolAtLocation(property, declaration);
|
|
1621
|
+
await add(await propertyType.getSymbol() ?? await propertyType.getAliasSymbol(), `property "${property.name}"`);
|
|
1622
|
+
}
|
|
1623
|
+
if (type.isClassOrInterface()) {
|
|
1624
|
+
const baseTypes = await project.checker.getBaseTypes(type);
|
|
1625
|
+
for (const baseType of baseTypes)
|
|
1626
|
+
await add(await baseType.getSymbol() ?? await baseType.getAliasSymbol(), "extends");
|
|
1627
|
+
}
|
|
1628
|
+
return references;
|
|
1629
|
+
};
|
|
1630
|
+
var findRenameSites = async (project, target) => {
|
|
1631
|
+
const sites = [];
|
|
1632
|
+
const seen = new Set;
|
|
1633
|
+
const declarationName2 = declarationNameNode(target.declaration) ?? target.declaration;
|
|
1634
|
+
const position = declarationName2.getStart(declarationName2.getSourceFile());
|
|
1635
|
+
try {
|
|
1636
|
+
const referenced = await project.checker.getReferencedSymbolsForNode(declarationName2, position);
|
|
1637
|
+
for (const entry of referenced) {
|
|
1638
|
+
const handles = [entry.definition, ...entry.references];
|
|
1639
|
+
for (const handle of handles) {
|
|
1640
|
+
const node = await handle.resolve(project);
|
|
1641
|
+
if (node === undefined || node.kind !== SyntaxKind2.Identifier)
|
|
1642
|
+
continue;
|
|
1643
|
+
const sourceFile = node.getSourceFile();
|
|
1644
|
+
if (!isProjectSourceFile(sourceFile.fileName, target.packagePath))
|
|
1645
|
+
continue;
|
|
1646
|
+
addSite(sites, seen, sourceFile, node);
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1649
|
+
if (sites.length > 0)
|
|
1650
|
+
return sites;
|
|
1651
|
+
} catch {}
|
|
1652
|
+
const sourceFiles = await projectSourceFiles(project, target.packagePath);
|
|
1653
|
+
const resolvedTarget = await resolveSymbol(project, target.symbol);
|
|
1654
|
+
for (const sourceFile of sourceFiles) {
|
|
1655
|
+
const nodes = [];
|
|
1656
|
+
visit(sourceFile, (node) => {
|
|
1657
|
+
if (node.kind === SyntaxKind2.Identifier)
|
|
1658
|
+
nodes.push(node);
|
|
1659
|
+
});
|
|
1660
|
+
if (nodes.length === 0)
|
|
1661
|
+
continue;
|
|
1662
|
+
const symbols = await project.checker.getSymbolAtLocation(nodes);
|
|
1663
|
+
for (let index = 0;index < nodes.length; index += 1) {
|
|
1664
|
+
const node = nodes[index];
|
|
1665
|
+
const symbol = symbols[index];
|
|
1666
|
+
if (symbol === undefined)
|
|
1667
|
+
continue;
|
|
1668
|
+
const resolved = await resolveSymbol(project, symbol);
|
|
1669
|
+
if (resolved.id !== resolvedTarget.id)
|
|
1670
|
+
continue;
|
|
1671
|
+
addSite(sites, seen, sourceFile, node);
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
return sites;
|
|
1675
|
+
};
|
|
1676
|
+
var addSite = (sites, seen, sourceFile, node) => {
|
|
1677
|
+
const key = `${sourceFile.fileName}:${node.pos}`;
|
|
1678
|
+
if (seen.has(key))
|
|
1679
|
+
return;
|
|
1680
|
+
seen.add(key);
|
|
1681
|
+
sites.push({ sourceFile, node });
|
|
1682
|
+
};
|
|
1683
|
+
var findContainingSymbol = async (project, start, target) => {
|
|
1684
|
+
let current = start;
|
|
1685
|
+
while (current !== undefined && current.kind !== SyntaxKind2.SourceFile) {
|
|
1686
|
+
const nameNode = declarationNameNode(current);
|
|
1687
|
+
if (nameNode !== undefined) {
|
|
1688
|
+
const symbol = await project.checker.getSymbolAtLocation(nameNode);
|
|
1689
|
+
if (symbol !== undefined && !await matchesCanonicalSymbol(project, symbol, target))
|
|
1690
|
+
return symbol.name;
|
|
1691
|
+
}
|
|
1692
|
+
current = current.parent;
|
|
1693
|
+
}
|
|
1694
|
+
const sourceFile = start.getSourceFile();
|
|
1695
|
+
const moduleSymbol = await project.checker.getSymbolAtLocation(sourceFile);
|
|
1696
|
+
return moduleSymbol?.name ?? "anonymous";
|
|
1697
|
+
};
|
|
1698
|
+
var resolveSymbol = async (project, symbol) => {
|
|
1699
|
+
try {
|
|
1700
|
+
return await project.checker.getAliasedSymbol(symbol);
|
|
1701
|
+
} catch {
|
|
1702
|
+
return symbol;
|
|
1703
|
+
}
|
|
1704
|
+
};
|
|
1705
|
+
var matchesCanonicalSymbol = async (project, candidate, target) => (await resolveSymbol(project, candidate)).id === target.id;
|
|
1706
|
+
var projectSourceFiles = async (project, packagePath) => {
|
|
1707
|
+
const names = await project.program.getSourceFileNames();
|
|
1708
|
+
const files = [];
|
|
1709
|
+
for (const name of names) {
|
|
1710
|
+
if (!isProjectSourceFile(name, packagePath))
|
|
1711
|
+
continue;
|
|
1712
|
+
const sourceFile = await project.program.getSourceFile(name);
|
|
1713
|
+
if (sourceFile !== undefined)
|
|
1714
|
+
files.push(sourceFile);
|
|
1715
|
+
}
|
|
1716
|
+
return files;
|
|
1717
|
+
};
|
|
1718
|
+
var isProjectSourceFile = (fileName, packagePath) => {
|
|
1719
|
+
const file = resolve6(fileName);
|
|
1720
|
+
const root = resolve6(packagePath);
|
|
1721
|
+
return (file === root || file.startsWith(`${root}/`)) && !file.includes("/node_modules/");
|
|
1722
|
+
};
|
|
1723
|
+
var relativePath2 = (root, fileName) => {
|
|
1724
|
+
const file = resolve6(fileName);
|
|
1725
|
+
const normalizedRoot = resolve6(root);
|
|
1726
|
+
return file === normalizedRoot || file.startsWith(`${normalizedRoot}/`) ? relative4(normalizedRoot, file) : file;
|
|
1727
|
+
};
|
|
1728
|
+
var visit = (node, callback) => {
|
|
1729
|
+
callback(node);
|
|
1730
|
+
node.forEachChild((child) => {
|
|
1731
|
+
visit(child, callback);
|
|
1732
|
+
});
|
|
1733
|
+
};
|
|
1734
|
+
var declarationForName = (nameNode) => {
|
|
1735
|
+
const parent = nameNode.parent;
|
|
1736
|
+
if (declarationNameNode(parent) === nameNode)
|
|
1737
|
+
return parent;
|
|
1738
|
+
return;
|
|
1739
|
+
};
|
|
1740
|
+
var isImportReference = (node) => {
|
|
1741
|
+
let current = node.parent;
|
|
1742
|
+
while (current !== undefined && current.kind !== SyntaxKind2.SourceFile) {
|
|
1743
|
+
if (current.kind === SyntaxKind2.ImportDeclaration)
|
|
1744
|
+
return true;
|
|
1745
|
+
current = current.parent;
|
|
1746
|
+
}
|
|
1747
|
+
return false;
|
|
1748
|
+
};
|
|
1749
|
+
var declarationNameNode = (node) => {
|
|
1750
|
+
switch (node.kind) {
|
|
1751
|
+
case SyntaxKind2.ClassDeclaration:
|
|
1752
|
+
case SyntaxKind2.ClassExpression:
|
|
1753
|
+
case SyntaxKind2.FunctionDeclaration:
|
|
1754
|
+
case SyntaxKind2.FunctionExpression:
|
|
1755
|
+
case SyntaxKind2.InterfaceDeclaration:
|
|
1756
|
+
case SyntaxKind2.TypeAliasDeclaration:
|
|
1757
|
+
case SyntaxKind2.EnumDeclaration:
|
|
1758
|
+
case SyntaxKind2.VariableDeclaration:
|
|
1759
|
+
case SyntaxKind2.MethodDeclaration:
|
|
1760
|
+
case SyntaxKind2.MethodSignature:
|
|
1761
|
+
case SyntaxKind2.PropertyDeclaration:
|
|
1762
|
+
case SyntaxKind2.PropertySignature:
|
|
1763
|
+
case SyntaxKind2.EnumMember:
|
|
1764
|
+
case SyntaxKind2.ImportEqualsDeclaration:
|
|
1765
|
+
return node.name;
|
|
1766
|
+
default:
|
|
1767
|
+
return;
|
|
1768
|
+
}
|
|
1769
|
+
};
|
|
1770
|
+
var isTargetDeclarationName = (node, target) => {
|
|
1771
|
+
const declaration = declarationForName(node);
|
|
1772
|
+
return declaration !== undefined && declaration.pos === target.declaration.pos;
|
|
1773
|
+
};
|
|
1774
|
+
var isTypeReferencePosition = (node) => {
|
|
1775
|
+
const parent = node.parent;
|
|
1776
|
+
return parent !== undefined && (parent.kind === SyntaxKind2.TypeReference || parent.kind === SyntaxKind2.ExpressionWithTypeArguments || isTypeReferenceNode2(parent));
|
|
1777
|
+
};
|
|
1778
|
+
var classifyReferenceContext = (parent) => {
|
|
1779
|
+
switch (parent.kind) {
|
|
1780
|
+
case SyntaxKind2.HeritageClause:
|
|
1781
|
+
return "extends";
|
|
1782
|
+
case SyntaxKind2.TypeReference:
|
|
1783
|
+
case SyntaxKind2.ExpressionWithTypeArguments:
|
|
1784
|
+
return "type reference";
|
|
1785
|
+
case SyntaxKind2.PropertyAccessExpression:
|
|
1786
|
+
return "property access";
|
|
1787
|
+
case SyntaxKind2.CallExpression:
|
|
1788
|
+
return "call";
|
|
1789
|
+
default:
|
|
1790
|
+
return "usage";
|
|
1791
|
+
}
|
|
1792
|
+
};
|
|
1793
|
+
var skipOutgoingType = async (type) => {
|
|
1794
|
+
if (type.isIntrinsicType() || type.isLiteralType())
|
|
1795
|
+
return true;
|
|
1796
|
+
if (type.isUnionType()) {
|
|
1797
|
+
const members = await type.getTypes();
|
|
1798
|
+
return members.every((member) => member.isIntrinsicType() || member.isLiteralType());
|
|
1799
|
+
}
|
|
1800
|
+
return false;
|
|
1801
|
+
};
|
|
1802
|
+
var toMermaid = (edges) => {
|
|
1803
|
+
const lines = ["graph TD"];
|
|
1804
|
+
const seen = new Set;
|
|
1805
|
+
for (const edge of edges) {
|
|
1806
|
+
const from = sanitizeMermaidId(edge.from);
|
|
1807
|
+
const to = sanitizeMermaidId(edge.to);
|
|
1808
|
+
const key = `${from}-->${to}`;
|
|
1809
|
+
if (seen.has(key))
|
|
1810
|
+
continue;
|
|
1811
|
+
seen.add(key);
|
|
1812
|
+
const label = edge.label?.replace(/"/g, "'").replace(/[|[\]]/g, "");
|
|
1813
|
+
lines.push(label === undefined ? ` ${from} --> ${to}` : ` ${from} -->|${label}| ${to}`);
|
|
1814
|
+
}
|
|
1815
|
+
return lines.join(`
|
|
1816
|
+
`);
|
|
1817
|
+
};
|
|
1818
|
+
var toDot = (edges) => {
|
|
1819
|
+
const lines = ["digraph G {", " rankdir=TB;", " node [shape=box];"];
|
|
1820
|
+
const seen = new Set;
|
|
1821
|
+
for (const edge of edges) {
|
|
1822
|
+
const key = `${edge.from}->${edge.to}`;
|
|
1823
|
+
if (seen.has(key))
|
|
1824
|
+
continue;
|
|
1825
|
+
seen.add(key);
|
|
1826
|
+
const from = `"${edge.from.replace(/"/g, "\\\"")}"`;
|
|
1827
|
+
const to = `"${edge.to.replace(/"/g, "\\\"")}"`;
|
|
1828
|
+
const label = edge.label?.replace(/"/g, "\\\"");
|
|
1829
|
+
lines.push(label === undefined ? ` ${from} -> ${to};` : ` ${from} -> ${to} [label="${label}"];`);
|
|
1830
|
+
}
|
|
1831
|
+
lines.push("}");
|
|
1832
|
+
return lines.join(`
|
|
1833
|
+
`);
|
|
1834
|
+
};
|
|
1835
|
+
var sanitizeMermaidId = (name) => name.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
1836
|
+
var addRenameSafetyError = (errors, sourceFile, file, packagePath, line) => {
|
|
1837
|
+
if (errors.some((error) => error.file === file && error.line === line))
|
|
1838
|
+
return;
|
|
1839
|
+
if (sourceFile.isDeclarationFile)
|
|
1840
|
+
errors.push({ file, line, message: "Cannot rename: declaration file (.d.ts)" });
|
|
1841
|
+
else if (!isProjectSourceFile(sourceFile.fileName, packagePath))
|
|
1842
|
+
errors.push({ file, line, message: "Cannot rename: file is outside package boundary" });
|
|
1843
|
+
};
|
|
1844
|
+
var getLineText = (sourceFile, line) => {
|
|
1845
|
+
const starts = sourceFile.getLineStarts();
|
|
1846
|
+
const start = starts[line] ?? 0;
|
|
1847
|
+
const end = starts[line + 1] ?? sourceFile.text.length;
|
|
1848
|
+
return sourceFile.text.slice(start, end).replace(/\r$/, "");
|
|
1849
|
+
};
|
|
1850
|
+
var replaceSiteOnTrimmedLine = (lineText, trimmed, character, token, replacement) => {
|
|
1851
|
+
const trimOffset = lineText.length - lineText.trimStart().length;
|
|
1852
|
+
const relativeCharacter = character - trimOffset;
|
|
1853
|
+
if (relativeCharacter >= 0 && relativeCharacter + token.length <= trimmed.length && trimmed.slice(relativeCharacter, relativeCharacter + token.length) === token) {
|
|
1854
|
+
return `${trimmed.slice(0, relativeCharacter)}${replacement}${trimmed.slice(relativeCharacter + token.length)}`;
|
|
1855
|
+
}
|
|
1856
|
+
return trimmed.replace(new RegExp(`\\b${escapeRegex(token)}\\b`, "g"), replacement);
|
|
1857
|
+
};
|
|
1858
|
+
var collectStringLiteralLocations = (sourceFile, root, symbolName, results) => {
|
|
1859
|
+
const regex = new RegExp(`\\b${escapeRegex(symbolName)}\\b`);
|
|
1860
|
+
visit(sourceFile, (node) => {
|
|
1861
|
+
if (node.kind !== SyntaxKind2.StringLiteral)
|
|
1862
|
+
return;
|
|
1863
|
+
const content = node.getText(sourceFile).slice(1, -1);
|
|
1864
|
+
if (!regex.test(content))
|
|
1865
|
+
return;
|
|
1866
|
+
const position = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
|
|
1867
|
+
results.push({ file: relativePath2(root, sourceFile.fileName), line: position.line + 1, content: content.length > 50 ? `${content.slice(0, 50)}...` : content });
|
|
1868
|
+
});
|
|
1869
|
+
};
|
|
1870
|
+
var collectCommentLocations = (sourceFile, root, symbolName, results) => {
|
|
1871
|
+
const regex = new RegExp(`\\b${escapeRegex(symbolName)}\\b`);
|
|
1872
|
+
const lines = sourceFile.text.split(/\n/);
|
|
1873
|
+
const file = relativePath2(root, sourceFile.fileName);
|
|
1874
|
+
for (let index = 0;index < lines.length; index += 1) {
|
|
1875
|
+
const line = lines[index].replace(/\r$/, "");
|
|
1876
|
+
const singleLine = line.match(/\/\/(.*)$/);
|
|
1877
|
+
if (singleLine !== null && regex.test(singleLine[1])) {
|
|
1878
|
+
results.push({ file, line: index + 1, content: truncateComment(singleLine[1].trim()) });
|
|
1879
|
+
continue;
|
|
1880
|
+
}
|
|
1881
|
+
if ((line.includes("/*") || line.includes("*")) && regex.test(line)) {
|
|
1882
|
+
const trimmed = line.trim();
|
|
1883
|
+
if (trimmed.startsWith("*") || trimmed.startsWith("/*") || trimmed.startsWith("//"))
|
|
1884
|
+
results.push({ file, line: index + 1, content: truncateComment(trimmed) });
|
|
1885
|
+
}
|
|
1886
|
+
}
|
|
1887
|
+
};
|
|
1888
|
+
var truncateComment = (content) => content.length > 50 ? `${content.slice(0, 50)}...` : content;
|
|
1889
|
+
var escapeRegex = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1890
|
+
|
|
1891
|
+
// src/transform-search/index.ts
|
|
1892
|
+
import { join as join4, relative as relative5 } from "path";
|
|
1893
|
+
import {
|
|
1894
|
+
ModifierFlags as ModifierFlags2,
|
|
1895
|
+
SignatureKind as SignatureKind2,
|
|
1896
|
+
TypeFlags
|
|
1897
|
+
} from "typescript/unstable/async";
|
|
1898
|
+
import {
|
|
1899
|
+
SyntaxKind as SyntaxKind3
|
|
1900
|
+
} from "typescript/unstable/ast";
|
|
1901
|
+
import {
|
|
1902
|
+
isArrowFunction,
|
|
1903
|
+
isCallExpression,
|
|
1904
|
+
isCallSignatureDeclaration,
|
|
1905
|
+
isClassDeclaration as isClassDeclaration2,
|
|
1906
|
+
isConstructorDeclaration,
|
|
1907
|
+
isFunctionDeclaration as isFunctionDeclaration2,
|
|
1908
|
+
isFunctionExpression,
|
|
1909
|
+
isInterfaceDeclaration as isInterfaceDeclaration2,
|
|
1910
|
+
isMethodDeclaration,
|
|
1911
|
+
isMethodSignatureDeclaration,
|
|
1912
|
+
isNewExpression,
|
|
1913
|
+
isObjectLiteralExpression,
|
|
1914
|
+
isPropertyAssignment,
|
|
1915
|
+
isPropertySignatureDeclaration,
|
|
1916
|
+
isTypeAliasDeclaration as isTypeAliasDeclaration2,
|
|
1917
|
+
isVariableDeclaration as isVariableDeclaration2,
|
|
1918
|
+
isVariableStatement as isVariableStatement2
|
|
1919
|
+
} from "typescript/unstable/ast/is";
|
|
1920
|
+
var modifierFlagsOf = (node) => node.modifierFlags ?? ModifierFlags2.None;
|
|
1921
|
+
var hasModifier = (node, flag) => (modifierFlagsOf(node) & flag) !== ModifierFlags2.None;
|
|
1922
|
+
var textOfName = (node, sourceFile) => node.name?.getText(sourceFile).replace(/^["']|["']$/g, "") ?? "anonymous";
|
|
1923
|
+
var variableStatement = (node) => {
|
|
1924
|
+
let current = node.parent;
|
|
1925
|
+
while (current !== undefined && current.kind !== SyntaxKind3.SourceFile) {
|
|
1926
|
+
if (isVariableStatement2(current))
|
|
1927
|
+
return current;
|
|
1928
|
+
current = current.parent;
|
|
1929
|
+
}
|
|
1930
|
+
return null;
|
|
1931
|
+
};
|
|
1932
|
+
var exportedFrom = (node) => {
|
|
1933
|
+
let current = node;
|
|
1934
|
+
while (current !== undefined && current.kind !== SyntaxKind3.SourceFile) {
|
|
1935
|
+
if (hasModifier(current, ModifierFlags2.Export))
|
|
1936
|
+
return true;
|
|
1937
|
+
current = current.parent;
|
|
1938
|
+
}
|
|
1939
|
+
const statement = variableStatement(node);
|
|
1940
|
+
return statement !== null && hasModifier(statement, ModifierFlags2.Export);
|
|
1941
|
+
};
|
|
1942
|
+
var containerNameOf = (node, sourceFile) => {
|
|
1943
|
+
let current = node.parent;
|
|
1944
|
+
while (current !== undefined && current.kind !== SyntaxKind3.SourceFile) {
|
|
1945
|
+
if (isClassDeclaration2(current) || isInterfaceDeclaration2(current) || isTypeAliasDeclaration2(current)) {
|
|
1946
|
+
return textOfName(current, sourceFile);
|
|
1947
|
+
}
|
|
1948
|
+
if (isObjectLiteralExpression(current) && current.parent !== undefined && isVariableDeclaration2(current.parent)) {
|
|
1949
|
+
return textOfName(current.parent, sourceFile);
|
|
1950
|
+
}
|
|
1951
|
+
current = current.parent;
|
|
1952
|
+
}
|
|
1953
|
+
return null;
|
|
1954
|
+
};
|
|
1955
|
+
var returnTypeNodeOf = (node) => node.type ?? null;
|
|
1956
|
+
var nodeName = (node, sourceFile) => {
|
|
1957
|
+
const named = node;
|
|
1958
|
+
return named.name === undefined ? "anonymous" : named.name.getText(sourceFile).replace(/^["']|["']$/g, "");
|
|
1959
|
+
};
|
|
1960
|
+
var enclosingNameNode = (node) => {
|
|
1961
|
+
let current = node.parent;
|
|
1962
|
+
while (current !== undefined && current.kind !== SyntaxKind3.SourceFile) {
|
|
1963
|
+
const name = current.name;
|
|
1964
|
+
if (name !== undefined)
|
|
1965
|
+
return name;
|
|
1966
|
+
current = current.parent;
|
|
1967
|
+
}
|
|
1968
|
+
return null;
|
|
1969
|
+
};
|
|
1970
|
+
var enumerate = (sourceFiles) => {
|
|
1971
|
+
const candidates = [];
|
|
1972
|
+
const append = (sourceFile, callable, kind, name, symbolNode) => {
|
|
1973
|
+
const containerName = containerNameOf(callable, sourceFile);
|
|
1974
|
+
candidates.push({
|
|
1975
|
+
callableId: `declaration:${candidates.length}`,
|
|
1976
|
+
signatureKey: null,
|
|
1977
|
+
compilerSignature: null,
|
|
1978
|
+
symbolNode,
|
|
1979
|
+
sourceFile,
|
|
1980
|
+
callable,
|
|
1981
|
+
name,
|
|
1982
|
+
kind,
|
|
1983
|
+
exported: exportedFrom(callable),
|
|
1984
|
+
deprecated: /@deprecated\b/.test(callable.getFullText(sourceFile)),
|
|
1985
|
+
containerName,
|
|
1986
|
+
params: callable.parameters,
|
|
1987
|
+
returnNode: returnTypeNodeOf(callable)
|
|
1988
|
+
});
|
|
1989
|
+
};
|
|
1990
|
+
const visit2 = (node, sourceFile) => {
|
|
1991
|
+
if (isFunctionDeclaration2(node) && node.name !== undefined) {
|
|
1992
|
+
const implementationExists = sourceFile.statements.some((statement) => isFunctionDeclaration2(statement) && statement.name?.getText(sourceFile) === node.name?.getText(sourceFile) && statement.body !== undefined);
|
|
1993
|
+
if (node.body !== undefined || !implementationExists)
|
|
1994
|
+
append(sourceFile, node, "Function", nodeName(node, sourceFile), node.name);
|
|
1995
|
+
} else if (isVariableDeclaration2(node) && node.initializer !== undefined && (isArrowFunction(node.initializer) || isFunctionExpression(node.initializer))) {
|
|
1996
|
+
append(sourceFile, node.initializer, "VariableCallable", nodeName(node, sourceFile), node.name);
|
|
1997
|
+
} else if (isMethodDeclaration(node)) {
|
|
1998
|
+
const kind = isClassDeclaration2(node.parent) ? hasModifier(node, ModifierFlags2.Static) ? "StaticMethod" : "ClassMethod" : "ObjectMethod";
|
|
1999
|
+
append(sourceFile, node, kind, nodeName(node, sourceFile), node.name);
|
|
2000
|
+
} else if (isConstructorDeclaration(node)) {
|
|
2001
|
+
const className = isClassDeclaration2(node.parent) ? node.parent.name ?? null : null;
|
|
2002
|
+
append(sourceFile, node, "Constructor", "constructor", className);
|
|
2003
|
+
} else if (isMethodSignatureDeclaration(node) || isCallSignatureDeclaration(node)) {
|
|
2004
|
+
const symbolNode = isMethodSignatureDeclaration(node) ? node.name : enclosingNameNode(node);
|
|
2005
|
+
append(sourceFile, node, isMethodSignatureDeclaration(node) ? "InterfaceMethod" : "TypeLiteralMethod", nodeName(node, sourceFile), symbolNode);
|
|
2006
|
+
} else if (isPropertySignatureDeclaration(node) && node.type !== undefined && (node.type.kind === SyntaxKind3.FunctionType || node.type.kind === SyntaxKind3.ConstructorType)) {
|
|
2007
|
+
append(sourceFile, node.type, "CallableProperty", nodeName(node, sourceFile), node.name);
|
|
2008
|
+
} else if (isPropertyAssignment(node) && (isArrowFunction(node.initializer) || isFunctionExpression(node.initializer))) {
|
|
2009
|
+
append(sourceFile, node.initializer, "ObjectMethod", nodeName(node, sourceFile), node.name);
|
|
2010
|
+
}
|
|
2011
|
+
node.forEachChild((child) => visit2(child, sourceFile));
|
|
2012
|
+
};
|
|
2013
|
+
for (const sourceFile of sourceFiles)
|
|
2014
|
+
visit2(sourceFile, sourceFile);
|
|
2015
|
+
return candidates;
|
|
2016
|
+
};
|
|
2017
|
+
var signatureKeyForNode = (node) => {
|
|
2018
|
+
const sourceFile = node.getSourceFile();
|
|
2019
|
+
return `${sourceFile.fileName}:${node.getStart(sourceFile)}:${node.getEnd()}:${node.kind}`;
|
|
2020
|
+
};
|
|
2021
|
+
var needsSignatureExpansion = (group) => group.seedCount > 1 || (group.symbol?.declarations.length ?? 0) > 1;
|
|
2022
|
+
var symbolsForSeeds = async (project, seeds) => {
|
|
2023
|
+
const indexes = [];
|
|
2024
|
+
const nodes = [];
|
|
2025
|
+
for (let index = 0;index < seeds.length; index += 1) {
|
|
2026
|
+
const node = seeds[index].symbolNode;
|
|
2027
|
+
if (node === null)
|
|
2028
|
+
continue;
|
|
2029
|
+
indexes.push(index);
|
|
2030
|
+
nodes.push(node);
|
|
2031
|
+
}
|
|
2032
|
+
const symbols = nodes.length === 0 ? [] : await project.checker.getSymbolAtLocation(nodes);
|
|
2033
|
+
const bySeed = new Map;
|
|
2034
|
+
for (let index = 0;index < symbols.length; index += 1) {
|
|
2035
|
+
const symbol = symbols[index];
|
|
2036
|
+
if (symbol !== undefined)
|
|
2037
|
+
bySeed.set(indexes[index], symbol);
|
|
2038
|
+
}
|
|
2039
|
+
return bySeed;
|
|
2040
|
+
};
|
|
2041
|
+
var groupCandidateSeeds = (seeds, symbols) => {
|
|
2042
|
+
const groups = new Map;
|
|
2043
|
+
for (let index = 0;index < seeds.length; index += 1) {
|
|
2044
|
+
const seed = seeds[index];
|
|
2045
|
+
const symbol = symbols.get(index) ?? null;
|
|
2046
|
+
const callableId = symbol === null ? seed.callableId : `${symbol.id}:${seed.kind}`;
|
|
2047
|
+
const existing = groups.get(callableId);
|
|
2048
|
+
groups.set(callableId, existing === undefined ? { seed, symbol, seedCount: 1 } : { ...existing, seedCount: existing.seedCount + 1 });
|
|
2049
|
+
}
|
|
2050
|
+
return groups;
|
|
2051
|
+
};
|
|
2052
|
+
var valueTypesForGroups = async (project, groups) => {
|
|
2053
|
+
const entries = [...groups.entries()].filter((entry) => entry[1].symbol !== null && entry[1].seed.kind !== "TypeLiteralMethod" && needsSignatureExpansion(entry[1]));
|
|
2054
|
+
const types = entries.length === 0 ? [] : await project.checker.getTypeOfSymbol(entries.map(([, group]) => group.symbol));
|
|
2055
|
+
return new Map(entries.map(([callableId], index) => [callableId, types[index]]));
|
|
2056
|
+
};
|
|
2057
|
+
var expandCandidateGroup = async (project, callableId, group, valueTypes) => {
|
|
2058
|
+
if (group.symbol === null || !needsSignatureExpansion(group)) {
|
|
2059
|
+
return [{ ...group.seed, callableId, signatureKey: signatureKeyForNode(group.seed.callable) }];
|
|
2060
|
+
}
|
|
2061
|
+
const type = group.seed.kind === "TypeLiteralMethod" ? await project.checker.getDeclaredTypeOfSymbol(group.symbol) : valueTypes.get(callableId);
|
|
2062
|
+
const signatureKind = group.seed.kind === "Constructor" || group.seed.callable.kind === SyntaxKind3.ConstructorType ? SignatureKind2.Construct : SignatureKind2.Call;
|
|
2063
|
+
const signatures = type.isErrorType() ? [] : await project.checker.getSignaturesOfType(type, signatureKind);
|
|
2064
|
+
const candidates = [];
|
|
2065
|
+
for (const signature of signatures) {
|
|
2066
|
+
const declaration = await signature.declaration?.resolve(project);
|
|
2067
|
+
const callable = declaration;
|
|
2068
|
+
if (callable === undefined || callable.parameters === undefined)
|
|
2069
|
+
continue;
|
|
2070
|
+
const sourceFile = callable.getSourceFile();
|
|
2071
|
+
candidates.push({
|
|
2072
|
+
...group.seed,
|
|
2073
|
+
callableId,
|
|
2074
|
+
signatureKey: signatureKeyForNode(callable),
|
|
2075
|
+
compilerSignature: signature,
|
|
2076
|
+
sourceFile,
|
|
2077
|
+
callable,
|
|
2078
|
+
deprecated: /@deprecated\b/.test(callable.getFullText(sourceFile)),
|
|
2079
|
+
params: callable.parameters,
|
|
2080
|
+
returnNode: returnTypeNodeOf(callable)
|
|
2081
|
+
});
|
|
2082
|
+
}
|
|
2083
|
+
return candidates.length === 0 ? [{ ...group.seed, callableId, signatureKey: null, compilerSignature: null }] : candidates;
|
|
2084
|
+
};
|
|
2085
|
+
var compilerCandidates = async (project, seeds) => {
|
|
2086
|
+
const groups = groupCandidateSeeds(seeds, await symbolsForSeeds(project, seeds));
|
|
2087
|
+
const valueTypes = await valueTypesForGroups(project, groups);
|
|
2088
|
+
const candidates = [];
|
|
2089
|
+
for (const [callableId, group] of groups) {
|
|
2090
|
+
candidates.push(...await expandCandidateGroup(project, callableId, group, valueTypes));
|
|
2091
|
+
}
|
|
2092
|
+
return candidates;
|
|
2093
|
+
};
|
|
2094
|
+
var typeText = (node, sourceFile) => node === null ? "unknown" : node.getText(sourceFile).trim();
|
|
2095
|
+
var wrapperOf = (text) => {
|
|
2096
|
+
const match = /^(Promise|PromiseLike|Effect|Observable|Task)\s*</.exec(text.replace(/\s/g, ""));
|
|
2097
|
+
return match === null ? null : match[1];
|
|
2098
|
+
};
|
|
2099
|
+
var unwrapText = (text, wrapper) => {
|
|
2100
|
+
if (wrapper === null)
|
|
2101
|
+
return text;
|
|
2102
|
+
const compact = text.replace(/\s/g, "");
|
|
2103
|
+
return compact.slice(wrapper.length + 1, compact.endsWith(">") ? -1 : undefined);
|
|
2104
|
+
};
|
|
2105
|
+
var erased = (type) => type !== null && (type.flags & TypeFlags.AnyOrUnknown) !== 0;
|
|
2106
|
+
var lineFor = (candidate) => candidate.sourceFile.getLineAndCharacterOfPosition(candidate.callable.getStart(candidate.sourceFile)).line + 1;
|
|
2107
|
+
var signatureFor = (candidate, resolvedReturnText) => {
|
|
2108
|
+
const params = candidate.params.map((param) => param.getText(candidate.sourceFile).trim()).join(", ");
|
|
2109
|
+
const returnText = resolvedReturnText ?? (candidate.returnNode === null ? "unknown" : typeText(candidate.returnNode, candidate.sourceFile));
|
|
2110
|
+
return `${candidate.name}(${params}): ${returnText}`;
|
|
2111
|
+
};
|
|
2112
|
+
var compareMatches = (left, right) => right.score - left.score || left.candidate.name.localeCompare(right.candidate.name) || left.candidate.sourceFile.fileName.localeCompare(right.candidate.sourceFile.fileName) || lineFor(left.candidate) - lineFor(right.candidate);
|
|
2113
|
+
var groupMatchesByCallable = (matches) => {
|
|
2114
|
+
const byCallable = new Map;
|
|
2115
|
+
const groups = [];
|
|
2116
|
+
for (const match of matches) {
|
|
2117
|
+
const existing = byCallable.get(match.candidate.callableId);
|
|
2118
|
+
if (existing === undefined) {
|
|
2119
|
+
const group = [match];
|
|
2120
|
+
byCallable.set(match.candidate.callableId, group);
|
|
2121
|
+
groups.push(group);
|
|
2122
|
+
} else {
|
|
2123
|
+
existing.push(match);
|
|
2124
|
+
}
|
|
2125
|
+
}
|
|
2126
|
+
return groups;
|
|
2127
|
+
};
|
|
2128
|
+
var confidenceFor = (match) => {
|
|
2129
|
+
if (match.verified && (match.exactFrom || match.exactTo))
|
|
2130
|
+
return "high";
|
|
2131
|
+
if (!match.partial && (match.exactFrom || match.exactTo))
|
|
2132
|
+
return "medium";
|
|
2133
|
+
return "low";
|
|
2134
|
+
};
|
|
2135
|
+
var queryTypeFor = async (raw, sourceFiles, checker, availableTypes) => {
|
|
2136
|
+
const normalized = raw.trim();
|
|
2137
|
+
const exact = availableTypes.get(normalized);
|
|
2138
|
+
if (exact !== undefined) {
|
|
2139
|
+
return { raw: normalized, type: exact.type, exactText: normalized, resolved: !exact.type.isErrorType() };
|
|
2140
|
+
}
|
|
2141
|
+
let declarationType = null;
|
|
2142
|
+
let declarationName2 = null;
|
|
2143
|
+
const visit2 = (node) => {
|
|
2144
|
+
if (declarationType !== null)
|
|
2145
|
+
return;
|
|
2146
|
+
if (isTypeAliasDeclaration2(node) && node.name.getText(node.getSourceFile()) === normalized) {
|
|
2147
|
+
declarationType = node.type;
|
|
2148
|
+
declarationName2 = node.name;
|
|
2149
|
+
} else if ((node.kind === SyntaxKind3.InterfaceDeclaration || node.kind === SyntaxKind3.ClassDeclaration) && node.name?.getText(node.getSourceFile()) === normalized) {
|
|
2150
|
+
declarationName2 = node.name;
|
|
2151
|
+
}
|
|
2152
|
+
node.forEachChild(visit2);
|
|
2153
|
+
};
|
|
2154
|
+
for (const sourceFile of sourceFiles)
|
|
2155
|
+
visit2(sourceFile);
|
|
2156
|
+
if (declarationType !== null) {
|
|
2157
|
+
const type = await checker.getTypeAtLocation(declarationType);
|
|
2158
|
+
return { raw: normalized, type, exactText: normalized, resolved: !type.isErrorType() };
|
|
2159
|
+
}
|
|
2160
|
+
if (declarationName2 !== null) {
|
|
2161
|
+
const symbol = await checker.getSymbolAtLocation(declarationName2);
|
|
2162
|
+
if (symbol !== undefined) {
|
|
2163
|
+
const type = await checker.getDeclaredTypeOfSymbol(symbol);
|
|
2164
|
+
return { raw: normalized, type, exactText: normalized, resolved: !type.isErrorType() };
|
|
2165
|
+
}
|
|
2166
|
+
}
|
|
2167
|
+
return { raw: normalized, type: null, exactText: null, resolved: false };
|
|
2168
|
+
};
|
|
2169
|
+
var createExplanation = (match) => {
|
|
2170
|
+
const from = match.from === null ? undefined : {
|
|
2171
|
+
description: `${match.from.paramName} accepts ${match.from.queryType}`,
|
|
2172
|
+
paramName: match.from.paramName,
|
|
2173
|
+
paramIndex: match.from.paramIndex,
|
|
2174
|
+
compatibility: match.from.exact ? "exact" : "assignable"
|
|
2175
|
+
};
|
|
2176
|
+
const to = match.to === null ? undefined : {
|
|
2177
|
+
description: `returns ${match.to.queryType}`,
|
|
2178
|
+
compatibility: match.to.exact ? "exact" : "assignable",
|
|
2179
|
+
...match.to.unwrapped && match.to.wrapper !== null ? { unwrapped: { wrapper: match.to.wrapper, originalType: match.to.returnType } } : {}
|
|
2180
|
+
};
|
|
2181
|
+
return {
|
|
2182
|
+
summary: [from === undefined ? null : `accepts ${match.from?.queryType}`, to === undefined ? null : `returns ${match.to?.queryType}`].filter(Boolean).join(" and "),
|
|
2183
|
+
details: {
|
|
2184
|
+
...from === undefined ? {} : { fromMatch: from },
|
|
2185
|
+
...to === undefined ? {} : { toMatch: to },
|
|
2186
|
+
verification: { method: match.verification.method === "synthetic" ? "synthetic" : "assignability-only", passed: match.verification.status === "verified" }
|
|
2187
|
+
},
|
|
2188
|
+
confidence: match.confidence
|
|
2189
|
+
};
|
|
2190
|
+
};
|
|
2191
|
+
var syntheticSequence = 0;
|
|
2192
|
+
var diagnosticText = (diagnostic) => typeof diagnostic.text === "string" ? diagnostic.text : String(diagnostic.text);
|
|
2193
|
+
var findSyntheticCall = (sourceFile) => {
|
|
2194
|
+
let found = null;
|
|
2195
|
+
const visit2 = (node) => {
|
|
2196
|
+
if (found !== null)
|
|
2197
|
+
return;
|
|
2198
|
+
if (isCallExpression(node) || isNewExpression(node)) {
|
|
2199
|
+
found = node;
|
|
2200
|
+
return;
|
|
2201
|
+
}
|
|
2202
|
+
node.forEachChild(visit2);
|
|
2203
|
+
};
|
|
2204
|
+
sourceFile.forEachChild(visit2);
|
|
2205
|
+
return found;
|
|
2206
|
+
};
|
|
2207
|
+
var syntheticCall = (match) => {
|
|
2208
|
+
const candidate = match.candidate;
|
|
2209
|
+
if (!candidate.exported || match.from === null || match.to === null)
|
|
2210
|
+
return null;
|
|
2211
|
+
if (candidate.kind === "InterfaceMethod" || candidate.kind === "TypeLiteralMethod")
|
|
2212
|
+
return null;
|
|
2213
|
+
const args = candidate.params.map((_, index) => index === match.from?.paramIndex ? "__input" : "undefined as never").join(", ");
|
|
2214
|
+
if (candidate.kind === "Constructor") {
|
|
2215
|
+
return candidate.containerName === null ? null : `new ${candidate.containerName}(${args})`;
|
|
2216
|
+
}
|
|
2217
|
+
if (candidate.containerName === null)
|
|
2218
|
+
return `${candidate.name}(${args})`;
|
|
2219
|
+
if (candidate.kind === "StaticMethod" || candidate.kind === "ObjectMethod") {
|
|
2220
|
+
return `${candidate.containerName}.${candidate.name}(${args})`;
|
|
2221
|
+
}
|
|
2222
|
+
return `(null as unknown as (typeof ${candidate.containerName})["prototype"]).${candidate.name}(${args})`;
|
|
2223
|
+
};
|
|
2224
|
+
var verifySyntheticMatch = async (context, project, packageInfo, match, options) => {
|
|
2225
|
+
const call = syntheticCall(match);
|
|
2226
|
+
if (call === null || options.from === undefined || options.to === undefined) {
|
|
2227
|
+
return {
|
|
2228
|
+
verification: { status: "unverifiable", method: null, reason: "not_importable" },
|
|
2229
|
+
selectedSignatureKey: null
|
|
2230
|
+
};
|
|
2231
|
+
}
|
|
2232
|
+
const virtualFilePath = join4(resolveVirtualFileDirectory(packageInfo.path), `__quartz_transform_verify_${(syntheticSequence++).toString(36)}.ts`);
|
|
2233
|
+
const imports = await synthesizePackageImports(project, packageInfo.path, virtualFilePath);
|
|
2234
|
+
const assignment = match.to?.unwrapped && (match.to.wrapper === "Promise" || match.to.wrapper === "PromiseLike") ? `async function __quartzVerify() {
|
|
2235
|
+
const __output: __QueryTo = await ${call}
|
|
2236
|
+
}` : `const __output: __QueryTo = ${call}`;
|
|
2237
|
+
const syntheticCode = `${imports.content}type __QueryFrom = ${options.from}
|
|
2238
|
+
type __QueryTo = ${options.to}
|
|
2239
|
+
declare const __input: __QueryFrom
|
|
2240
|
+
${assignment}
|
|
2241
|
+
`;
|
|
2242
|
+
const syntheticResult = await context.workspace.withVirtualFile(packageInfo.tsconfigPath, virtualFilePath, syntheticCode, async (syntheticProject, filePath) => {
|
|
2243
|
+
const diagnostics = (await Promise.all([
|
|
2244
|
+
syntheticProject.program.getSyntacticDiagnostics(filePath),
|
|
2245
|
+
syntheticProject.program.getBindDiagnostics(filePath),
|
|
2246
|
+
syntheticProject.program.getSemanticDiagnostics(filePath)
|
|
2247
|
+
])).flat();
|
|
2248
|
+
if (diagnostics.length > 0)
|
|
2249
|
+
return { diagnostics, selectedSignatureKey: null };
|
|
2250
|
+
const sourceFile = await syntheticProject.program.getSourceFile(filePath);
|
|
2251
|
+
const callNode = sourceFile === undefined ? null : findSyntheticCall(sourceFile);
|
|
2252
|
+
if (callNode === null)
|
|
2253
|
+
return { diagnostics, selectedSignatureKey: null };
|
|
2254
|
+
const signature = await syntheticProject.checker.getResolvedSignature(callNode);
|
|
2255
|
+
if (await syntheticProject.checker.isUnknownSignature(signature)) {
|
|
2256
|
+
return { diagnostics, selectedSignatureKey: null };
|
|
2257
|
+
}
|
|
2258
|
+
const declaration = await signature.declaration?.resolve(syntheticProject);
|
|
2259
|
+
return {
|
|
2260
|
+
diagnostics,
|
|
2261
|
+
selectedSignatureKey: declaration === undefined ? null : signatureKeyForNode(declaration)
|
|
2262
|
+
};
|
|
2263
|
+
});
|
|
2264
|
+
const failed = syntheticResult.diagnostics.length > 0 || syntheticResult.selectedSignatureKey === null;
|
|
2265
|
+
return {
|
|
2266
|
+
verification: {
|
|
2267
|
+
status: failed ? "unverified" : "verified",
|
|
2268
|
+
method: "synthetic",
|
|
2269
|
+
reason: failed ? "synthetic_check_failed" : "synthetic_check_passed",
|
|
2270
|
+
...syntheticResult.diagnostics.length > 0 && options.includeDiagnostics === true ? { diagnostics: syntheticResult.diagnostics.map((diagnostic) => ({ code: diagnostic.code, message: diagnosticText(diagnostic) })) } : {},
|
|
2271
|
+
...options.includeSyntheticCode === true ? { syntheticCode } : {}
|
|
2272
|
+
},
|
|
2273
|
+
selectedSignatureKey: syntheticResult.selectedSignatureKey
|
|
2274
|
+
};
|
|
2275
|
+
};
|
|
2276
|
+
var SYNTHETIC_OVERSCAN = 10;
|
|
2277
|
+
var SYNTHETIC_VERIFICATION_BATCH_SIZE = 8;
|
|
2278
|
+
var SOURCE_FILE_BATCH_SIZE = 32;
|
|
2279
|
+
var verifyUnfilteredGroups = async (groups, limit, verify) => {
|
|
2280
|
+
const prefixLength = Math.min(groups.length, limit + SYNTHETIC_OVERSCAN);
|
|
2281
|
+
const verifiedPrefix = await Promise.all(groups.slice(0, prefixLength).map(verify));
|
|
2282
|
+
return [...verifiedPrefix, ...groups.slice(prefixLength).map((group) => group[0])].sort(compareMatches);
|
|
2283
|
+
};
|
|
2284
|
+
var verifyTrustedGroups = async (groups, limit, verify) => {
|
|
2285
|
+
if (limit === 0)
|
|
2286
|
+
return [];
|
|
2287
|
+
const matches = [];
|
|
2288
|
+
let verifiedCount = 0;
|
|
2289
|
+
let start = 0;
|
|
2290
|
+
while (start < groups.length) {
|
|
2291
|
+
if (verifiedCount >= limit) {
|
|
2292
|
+
const threshold = matches.filter((match) => match.verification.status === "verified").sort(compareMatches)[limit - 1];
|
|
2293
|
+
const nextUpperBound = groups[start]?.[0];
|
|
2294
|
+
if (threshold === undefined || nextUpperBound === undefined || compareMatches(nextUpperBound, threshold) >= 0) {
|
|
2295
|
+
break;
|
|
2296
|
+
}
|
|
2297
|
+
}
|
|
2298
|
+
const batchSize = verifiedCount < limit ? Math.min(SYNTHETIC_VERIFICATION_BATCH_SIZE, limit - verifiedCount) : 1;
|
|
2299
|
+
const batch = await Promise.all(groups.slice(start, start + batchSize).map(verify));
|
|
2300
|
+
matches.push(...batch);
|
|
2301
|
+
verifiedCount += batch.filter((match) => match.verification.status === "verified").length;
|
|
2302
|
+
start += batchSize;
|
|
2303
|
+
}
|
|
2304
|
+
return matches.sort(compareMatches);
|
|
2305
|
+
};
|
|
2306
|
+
var loadTransformIndex = async (project) => {
|
|
2307
|
+
const sourceNames = await project.program.getSourceFileNames();
|
|
2308
|
+
const sourceFiles = [];
|
|
2309
|
+
for (let start = 0;start < sourceNames.length; start += SOURCE_FILE_BATCH_SIZE) {
|
|
2310
|
+
const batch = await Promise.all(sourceNames.slice(start, start + SOURCE_FILE_BATCH_SIZE).map((name) => project.program.getSourceFile(name)));
|
|
2311
|
+
sourceFiles.push(...batch.filter((sourceFile) => sourceFile !== undefined && !sourceFile.isDeclarationFile && !sourceFile.fileName.includes("node_modules")));
|
|
2312
|
+
}
|
|
2313
|
+
const allCandidates = await compilerCandidates(project, enumerate(sourceFiles));
|
|
2314
|
+
const availableNodes = [];
|
|
2315
|
+
for (const candidate of allCandidates) {
|
|
2316
|
+
for (const param of candidate.params) {
|
|
2317
|
+
const type = param.type;
|
|
2318
|
+
if (type !== undefined)
|
|
2319
|
+
availableNodes.push(type);
|
|
2320
|
+
}
|
|
2321
|
+
if (candidate.returnNode !== null)
|
|
2322
|
+
availableNodes.push(candidate.returnNode);
|
|
2323
|
+
}
|
|
2324
|
+
const availableTypes = new Map;
|
|
2325
|
+
const byNode = new Map;
|
|
2326
|
+
const checkerNodes = [...availableNodes, ...allCandidates.map((candidate) => candidate.callable)];
|
|
2327
|
+
if (checkerNodes.length > 0) {
|
|
2328
|
+
const values = await project.checker.getTypeAtLocation(checkerNodes);
|
|
2329
|
+
for (let index = 0;index < checkerNodes.length; index += 1) {
|
|
2330
|
+
const node = checkerNodes[index];
|
|
2331
|
+
const value = values[index];
|
|
2332
|
+
byNode.set(node, value);
|
|
2333
|
+
if (index < availableNodes.length) {
|
|
2334
|
+
const typeNode = availableNodes[index];
|
|
2335
|
+
if (!availableTypes.has(typeNode.getText(typeNode.getSourceFile()).trim())) {
|
|
2336
|
+
availableTypes.set(typeNode.getText(typeNode.getSourceFile()).trim(), { node: typeNode, type: value });
|
|
2337
|
+
}
|
|
2338
|
+
}
|
|
2339
|
+
}
|
|
2340
|
+
}
|
|
2341
|
+
const inferredReturnTypes = new Map;
|
|
2342
|
+
const inferredReturnTexts = new Map;
|
|
2343
|
+
for (const candidate of allCandidates) {
|
|
2344
|
+
let returnType = candidate.returnNode === null ? null : byNode.get(candidate.returnNode) ?? null;
|
|
2345
|
+
if (candidate.returnNode === null) {
|
|
2346
|
+
if (candidate.compilerSignature !== null) {
|
|
2347
|
+
returnType = await project.checker.getReturnTypeOfSignature(candidate.compilerSignature);
|
|
2348
|
+
} else {
|
|
2349
|
+
const callableType = byNode.get(candidate.callable);
|
|
2350
|
+
if (callableType !== undefined) {
|
|
2351
|
+
const signatures = await project.checker.getSignaturesOfType(callableType, SignatureKind2.Call);
|
|
2352
|
+
const signature = signatures[0];
|
|
2353
|
+
if (signature !== undefined)
|
|
2354
|
+
returnType = await project.checker.getReturnTypeOfSignature(signature);
|
|
2355
|
+
}
|
|
2356
|
+
}
|
|
2357
|
+
if (returnType !== null) {
|
|
2358
|
+
inferredReturnTexts.set(candidate, await project.checker.typeToString(returnType, candidate.callable));
|
|
2359
|
+
}
|
|
2360
|
+
}
|
|
2361
|
+
inferredReturnTypes.set(candidate, returnType);
|
|
2362
|
+
}
|
|
2363
|
+
return { sourceFiles, allCandidates, availableTypes, byNode, inferredReturnTypes, inferredReturnTexts };
|
|
2364
|
+
};
|
|
2365
|
+
var createTransformSearchOperation = (context) => async (options) => {
|
|
2366
|
+
const started = performance.now();
|
|
2367
|
+
const paramPosition = options.paramPosition ?? 0;
|
|
2368
|
+
const unwrapReturn = options.unwrapReturn ?? true;
|
|
2369
|
+
const exportedOnly = options.exportedOnly ?? true;
|
|
2370
|
+
const limit = Math.max(0, options.limit ?? 25);
|
|
2371
|
+
if (options.from === undefined && options.to === undefined)
|
|
2372
|
+
throw new Error("At least one of 'from' or 'to' is required");
|
|
2373
|
+
return context.withProject(async (project, pkg, revision) => {
|
|
2374
|
+
const index = await context.cacheForRevision(`transform-index:${pkg.tsconfigPath}`, revision, () => loadTransformIndex(project));
|
|
2375
|
+
const { sourceFiles, availableTypes, byNode, inferredReturnTypes, inferredReturnTexts } = index;
|
|
2376
|
+
const candidates = index.allCandidates.filter((candidate) => !exportedOnly || candidate.exported);
|
|
2377
|
+
const fromQuery = options.from === undefined ? null : await queryTypeFor(options.from, sourceFiles, project.checker, availableTypes);
|
|
2378
|
+
const toQuery = options.to === undefined ? null : await queryTypeFor(options.to, sourceFiles, project.checker, availableTypes);
|
|
2379
|
+
const unresolved = fromQuery?.resolved === false ? { field: "from", query: fromQuery } : toQuery?.resolved === false ? { field: "to", query: toQuery } : null;
|
|
2380
|
+
if (unresolved !== null) {
|
|
2381
|
+
throw new QuartzEngineError("TRANSFORM_QUERY_UNRESOLVED", `Could not resolve transform-search ${unresolved.field} type ${JSON.stringify(unresolved.query.raw)}. Declare an exported named type or alias and retry.`);
|
|
2382
|
+
}
|
|
2383
|
+
const matches = [];
|
|
2384
|
+
for (const candidate of candidates) {
|
|
2385
|
+
const positions = paramPosition === "any" ? candidate.params.map((_, index2) => index2) : [paramPosition];
|
|
2386
|
+
let fromMatch = null;
|
|
2387
|
+
let fromAssignable = options.from === undefined;
|
|
2388
|
+
let fromErased = false;
|
|
2389
|
+
let exactFrom = options.from === undefined;
|
|
2390
|
+
if (fromQuery !== null) {
|
|
2391
|
+
for (const index2 of positions) {
|
|
2392
|
+
const param = candidate.params[index2];
|
|
2393
|
+
if (param === undefined)
|
|
2394
|
+
continue;
|
|
2395
|
+
const paramType = byNode.get(param.type ?? param) ?? null;
|
|
2396
|
+
const paramErased = erased(paramType);
|
|
2397
|
+
if (!options.allowTypeErasure && paramErased)
|
|
2398
|
+
continue;
|
|
2399
|
+
const assignable = fromQuery.type !== null && paramType !== null ? await project.checker.isTypeAssignableTo(fromQuery.type, paramType) : typeText(param.type ?? null, candidate.sourceFile) === fromQuery.raw;
|
|
2400
|
+
if (!assignable)
|
|
2401
|
+
continue;
|
|
2402
|
+
const paramName = param.name?.getText(candidate.sourceFile) ?? `arg${index2}`;
|
|
2403
|
+
const paramText = typeText(param.type ?? null, candidate.sourceFile);
|
|
2404
|
+
const exact = paramText.replace(/\s/g, "") === fromQuery.raw.replace(/\s/g, "");
|
|
2405
|
+
fromMatch = { matched: true, paramIndex: index2, paramName, queryType: fromQuery.raw, paramType: paramText, exact, ...paramErased ? { typeErasure: true } : {} };
|
|
2406
|
+
fromAssignable = true;
|
|
2407
|
+
fromErased = paramErased;
|
|
2408
|
+
exactFrom = exact;
|
|
2409
|
+
break;
|
|
2410
|
+
}
|
|
2411
|
+
}
|
|
2412
|
+
if (fromQuery !== null && !fromAssignable)
|
|
2413
|
+
continue;
|
|
2414
|
+
let toMatch = null;
|
|
2415
|
+
let toAssignable = options.to === undefined;
|
|
2416
|
+
let toErased = false;
|
|
2417
|
+
let exactTo = options.to === undefined;
|
|
2418
|
+
let returnType = inferredReturnTypes.get(candidate) ?? null;
|
|
2419
|
+
let returnText = candidate.returnNode === null ? inferredReturnTexts.get(candidate) ?? "unknown" : typeText(candidate.returnNode, candidate.sourceFile);
|
|
2420
|
+
if (candidate.kind === "Constructor" && candidate.containerName !== null)
|
|
2421
|
+
returnText = candidate.containerName;
|
|
2422
|
+
const wrapper = wrapperOf(returnText);
|
|
2423
|
+
let comparisonText = returnText;
|
|
2424
|
+
let unwrapped = false;
|
|
2425
|
+
if (unwrapReturn && wrapper !== null) {
|
|
2426
|
+
comparisonText = unwrapText(returnText, wrapper);
|
|
2427
|
+
if (returnType !== null && returnType.isTypeReference()) {
|
|
2428
|
+
const args = await project.checker.getTypeArguments(returnType);
|
|
2429
|
+
if (args[0] !== undefined)
|
|
2430
|
+
returnType = args[0];
|
|
2431
|
+
}
|
|
2432
|
+
unwrapped = true;
|
|
2433
|
+
}
|
|
2434
|
+
if (toQuery !== null) {
|
|
2435
|
+
const returnErased = erased(returnType);
|
|
2436
|
+
if (!options.allowTypeErasure && returnErased)
|
|
2437
|
+
continue;
|
|
2438
|
+
const constructorExact = candidate.kind === "Constructor" && candidate.containerName === toQuery.raw;
|
|
2439
|
+
const assignable = constructorExact || (toQuery.type !== null && returnType !== null ? await project.checker.isTypeAssignableTo(returnType, toQuery.type) : comparisonText.replace(/\s/g, "") === toQuery.raw.replace(/\s/g, ""));
|
|
2440
|
+
if (!assignable)
|
|
2441
|
+
continue;
|
|
2442
|
+
const exact = constructorExact || comparisonText.replace(/\s/g, "") === toQuery.raw.replace(/\s/g, "");
|
|
2443
|
+
toMatch = { matched: true, returnType: returnText, queryType: toQuery.raw, exact, unwrapped, wrapper, ...returnErased ? { typeErasure: true } : {} };
|
|
2444
|
+
toAssignable = true;
|
|
2445
|
+
toErased = returnErased;
|
|
2446
|
+
exactTo = exact;
|
|
2447
|
+
}
|
|
2448
|
+
const partial = fromQuery === null || toQuery === null || !fromQuery.resolved || !toQuery?.resolved;
|
|
2449
|
+
const assignabilityOk = !partial && fromAssignable && toAssignable && !fromErased && !toErased;
|
|
2450
|
+
const verification = assignabilityOk ? {
|
|
2451
|
+
status: "unverified",
|
|
2452
|
+
method: exactFrom && exactTo ? "exact_match" : "assignability_only",
|
|
2453
|
+
reason: "assignability_pending_synthetic"
|
|
2454
|
+
} : {
|
|
2455
|
+
status: "unverified",
|
|
2456
|
+
method: "assignability_only",
|
|
2457
|
+
reason: partial ? "partial_query" : fromErased || toErased ? "type_erasure" : "synthetic_check_failed"
|
|
2458
|
+
};
|
|
2459
|
+
const score = (exactFrom ? 40 : fromMatch === null ? 0 : 24) + (exactTo ? 40 : toMatch === null ? 0 : 24) + (assignabilityOk ? 20 : 0) + (candidate.exported ? 8 : 0) - (candidate.deprecated ? 15 : 0) + (candidate.kind === "Function" ? 2 : 0) - (toMatch?.unwrapped ? 10 : 0);
|
|
2460
|
+
const confidence = confidenceFor({ exactFrom, exactTo, verified: false, partial });
|
|
2461
|
+
matches.push({ candidate, from: fromMatch, to: toMatch, fromAssignable, toAssignable, returnText, verification, score, confidence });
|
|
2462
|
+
}
|
|
2463
|
+
matches.sort(compareMatches);
|
|
2464
|
+
const matchGroups = groupMatchesByCallable(matches);
|
|
2465
|
+
const assignabilityMs = performance.now() - started;
|
|
2466
|
+
const syntheticStarted = performance.now();
|
|
2467
|
+
const trustFiltered = options.verifiedOnly === true || options.minVerificationStatus === "verified";
|
|
2468
|
+
const finalizeVerification = (match, verification) => {
|
|
2469
|
+
const verified = verification.status === "verified";
|
|
2470
|
+
return {
|
|
2471
|
+
...match,
|
|
2472
|
+
verification,
|
|
2473
|
+
confidence: verification.reason === "synthetic_check_failed" ? "low" : confidenceFor({
|
|
2474
|
+
exactFrom: match.from?.exact === true,
|
|
2475
|
+
exactTo: match.to?.exact === true,
|
|
2476
|
+
verified,
|
|
2477
|
+
partial: match.from === null || match.to === null || !fromQuery?.resolved || !toQuery?.resolved
|
|
2478
|
+
})
|
|
2479
|
+
};
|
|
2480
|
+
};
|
|
2481
|
+
const verifyPending = async (group) => {
|
|
2482
|
+
const provisional = group[0];
|
|
2483
|
+
if (provisional.verification.reason !== "assignability_pending_synthetic") {
|
|
2484
|
+
return finalizeVerification(provisional, provisional.verification);
|
|
2485
|
+
}
|
|
2486
|
+
const synthetic = await verifySyntheticMatch(context, project, pkg, provisional, options);
|
|
2487
|
+
const selected = synthetic.selectedSignatureKey === null ? undefined : group.find((match) => match.candidate.signatureKey === synthetic.selectedSignatureKey);
|
|
2488
|
+
if (synthetic.verification.status === "verified" && selected === undefined) {
|
|
2489
|
+
return finalizeVerification(provisional, {
|
|
2490
|
+
...synthetic.verification,
|
|
2491
|
+
status: "unverified",
|
|
2492
|
+
reason: "synthetic_check_failed"
|
|
2493
|
+
});
|
|
2494
|
+
}
|
|
2495
|
+
return finalizeVerification(selected ?? provisional, synthetic.verification);
|
|
2496
|
+
};
|
|
2497
|
+
const rankedMatches = trustFiltered ? await verifyTrustedGroups(matchGroups, limit, verifyPending) : await verifyUnfilteredGroups(matchGroups, limit, verifyPending);
|
|
2498
|
+
const syntheticMs = performance.now() - syntheticStarted;
|
|
2499
|
+
const statusCounts = { verified: 0, unverified: 0, unverifiable: 0 };
|
|
2500
|
+
for (const match of rankedMatches)
|
|
2501
|
+
statusCounts[match.verification.status] += 1;
|
|
2502
|
+
const filtered = rankedMatches.filter((match) => {
|
|
2503
|
+
if (match.verification.reason === "synthetic_check_failed" && options.includeFailedVerification !== true)
|
|
2504
|
+
return false;
|
|
2505
|
+
if (options.verifiedOnly && match.verification.status !== "verified")
|
|
2506
|
+
return false;
|
|
2507
|
+
if (options.minVerificationStatus !== undefined) {
|
|
2508
|
+
const order = { unverifiable: 0, unverified: 1, verified: 2 };
|
|
2509
|
+
if (order[match.verification.status] < order[options.minVerificationStatus])
|
|
2510
|
+
return false;
|
|
2511
|
+
}
|
|
2512
|
+
return true;
|
|
2513
|
+
}).slice(0, limit);
|
|
2514
|
+
const results = filtered.map((match) => ({
|
|
2515
|
+
name: match.candidate.containerName === null ? match.candidate.name : `${match.candidate.containerName}.${match.candidate.name}`,
|
|
2516
|
+
signature: signatureFor(match.candidate, match.returnText),
|
|
2517
|
+
kind: match.candidate.kind,
|
|
2518
|
+
file: relative5(pkg.path, match.candidate.sourceFile.fileName).replaceAll("\\", "/"),
|
|
2519
|
+
line: lineFor(match.candidate),
|
|
2520
|
+
exported: match.candidate.exported,
|
|
2521
|
+
deprecated: match.candidate.deprecated,
|
|
2522
|
+
score: match.score,
|
|
2523
|
+
confidence: match.confidence,
|
|
2524
|
+
explanation: createExplanation(match),
|
|
2525
|
+
verification: match.verification,
|
|
2526
|
+
matchDetails: { fromMatch: match.from, toMatch: match.to }
|
|
2527
|
+
}));
|
|
2528
|
+
const totalMs = performance.now() - started;
|
|
2529
|
+
return {
|
|
2530
|
+
results,
|
|
2531
|
+
query: {
|
|
2532
|
+
from: options.from?.trim() ?? null,
|
|
2533
|
+
to: options.to?.trim() ?? null,
|
|
2534
|
+
options: {
|
|
2535
|
+
paramPosition,
|
|
2536
|
+
unwrapReturn,
|
|
2537
|
+
exportedOnly,
|
|
2538
|
+
...options.verifiedOnly === undefined ? {} : { verifiedOnly: options.verifiedOnly },
|
|
2539
|
+
...options.minVerificationStatus === undefined ? {} : { minVerificationStatus: options.minVerificationStatus },
|
|
2540
|
+
...options.includeDiagnostics === undefined ? {} : { includeDiagnostics: options.includeDiagnostics },
|
|
2541
|
+
...options.includeSyntheticCode === undefined ? {} : { includeSyntheticCode: options.includeSyntheticCode },
|
|
2542
|
+
...options.includeFailedVerification === undefined ? {} : { includeFailedVerification: options.includeFailedVerification }
|
|
2543
|
+
}
|
|
2544
|
+
},
|
|
2545
|
+
stats: {
|
|
2546
|
+
totalCandidates: new Set(candidates.map((candidate) => candidate.callableId)).size,
|
|
2547
|
+
assignableMatches: matchGroups.length,
|
|
2548
|
+
verifiedMatches: statusCounts.verified,
|
|
2549
|
+
verification: statusCounts,
|
|
2550
|
+
returned: results.length,
|
|
2551
|
+
timing: { indexLookupMs: 0, resolutionMs: 0, assignabilityMs: Math.max(0, assignabilityMs), syntheticMs: Math.max(0, syntheticMs), totalMs }
|
|
2552
|
+
}
|
|
2553
|
+
};
|
|
2554
|
+
}, options.packageName);
|
|
2555
|
+
};
|
|
2556
|
+
|
|
2557
|
+
// src/verification-operations.ts
|
|
2558
|
+
import { DiagnosticCategory as DiagnosticCategory3 } from "typescript/unstable/async";
|
|
2559
|
+
var workspaceWithVirtualFile = (context) => {
|
|
2560
|
+
const workspace = context.workspace;
|
|
2561
|
+
if (typeof workspace.withVirtualFile !== "function") {
|
|
2562
|
+
throw new Error("checkSnippet requires QuartzWorkspace.withVirtualFile(tsconfigPath, filePath, content, operation), which runs runWithTemporaryFileUpdate against the immutable base snapshot");
|
|
2563
|
+
}
|
|
2564
|
+
return workspace;
|
|
2565
|
+
};
|
|
2566
|
+
var lineAndColumn = (source, position) => {
|
|
2567
|
+
const location = source.getLineAndCharacterOfPosition(Math.max(0, position));
|
|
2568
|
+
return { line: location.line + 1, column: location.character + 1 };
|
|
2569
|
+
};
|
|
2570
|
+
var snippetDiagnostics = async (project, filePath, lineOffset = 0) => {
|
|
2571
|
+
const diagnostics = (await Promise.all([
|
|
2572
|
+
project.program.getSyntacticDiagnostics(filePath),
|
|
2573
|
+
project.program.getBindDiagnostics(filePath),
|
|
2574
|
+
project.program.getSemanticDiagnostics(filePath)
|
|
2575
|
+
])).flat();
|
|
2576
|
+
const sourceFile = await project.program.getSourceFile(filePath);
|
|
2577
|
+
const seen = new Set;
|
|
2578
|
+
const result = [];
|
|
2579
|
+
for (const diagnostic of diagnostics) {
|
|
2580
|
+
const key = `${diagnostic.code}\x00${diagnostic.pos}\x00${diagnostic.end}\x00${diagnostic.text}`;
|
|
2581
|
+
if (seen.has(key))
|
|
2582
|
+
continue;
|
|
2583
|
+
seen.add(key);
|
|
2584
|
+
const location = sourceFile === undefined ? { line: 1, column: 1 } : lineAndColumn(sourceFile, diagnostic.pos);
|
|
2585
|
+
result.push({
|
|
2586
|
+
message: diagnostic.text,
|
|
2587
|
+
line: Math.max(1, location.line - lineOffset),
|
|
2588
|
+
column: location.column,
|
|
2589
|
+
severity: diagnostic.category === DiagnosticCategory3.Error ? "error" : "warning"
|
|
2590
|
+
});
|
|
2591
|
+
}
|
|
2592
|
+
return result.sort((left, right) => left.line - right.line || left.column - right.column || left.message.localeCompare(right.message));
|
|
2593
|
+
};
|
|
2594
|
+
var typePatterns = [
|
|
2595
|
+
/Type '([^']+)' is not assignable to type '([^']+)'/,
|
|
2596
|
+
/Argument of type '([^']+)' is not assignable to parameter of type '([^']+)'/,
|
|
2597
|
+
/Type ([\w$]+(?:\.[\w$]+)*) is not assignable to type ([\w$]+(?:\.[\w$]+)*)/,
|
|
2598
|
+
/Argument of type ([\w$]+(?:\.[\w$]+)*) is not assignable to parameter of type ([\w$]+(?:\.[\w$]+)*)/,
|
|
2599
|
+
/Property '[^']+' does not exist on type '([^']+)'/,
|
|
2600
|
+
/Property '[^']+' is missing in type '([^']+)' but required in type '([^']+)'/,
|
|
2601
|
+
/Property ([\w$]+) is missing in type ([\w$]+(?:\.[\w$]+)*) but required in type ([\w$]+(?:\.[\w$]+)*)/,
|
|
2602
|
+
/Type '([^']+)' has no properties in common with type '([^']+)'/
|
|
2603
|
+
];
|
|
2604
|
+
var propertyPatterns = [
|
|
2605
|
+
/Property '([^']+)' does not exist/,
|
|
2606
|
+
/Property '([^']+)' is missing/,
|
|
2607
|
+
/Property ([\w$]+) does not exist/,
|
|
2608
|
+
/Property ([\w$]+) is missing/,
|
|
2609
|
+
/Did you mean '([^']+)'\?/
|
|
2610
|
+
];
|
|
2611
|
+
var extractErrorParts = (message) => {
|
|
2612
|
+
const types = [];
|
|
2613
|
+
const properties = [];
|
|
2614
|
+
for (const pattern of typePatterns) {
|
|
2615
|
+
const match = pattern.exec(message);
|
|
2616
|
+
if (match === null)
|
|
2617
|
+
continue;
|
|
2618
|
+
for (const value of match.slice(1)) {
|
|
2619
|
+
if (value !== undefined && !(value.startsWith("{") && value.endsWith("}")))
|
|
2620
|
+
types.push(value);
|
|
2621
|
+
}
|
|
2622
|
+
break;
|
|
2623
|
+
}
|
|
2624
|
+
for (const pattern of propertyPatterns) {
|
|
2625
|
+
const match = pattern.exec(message);
|
|
2626
|
+
if (match?.[1] !== undefined)
|
|
2627
|
+
properties.push(match[1]);
|
|
2628
|
+
}
|
|
2629
|
+
return { types, properties };
|
|
2630
|
+
};
|
|
2631
|
+
var asDiagnostics = (value) => ("errors" in value) ? value.errors : value;
|
|
2632
|
+
var skipped = (summary) => ({ ran: false, passed: null, blocking: false, summary });
|
|
2633
|
+
var toVerifyDiagnostic = (diagnostic) => ({
|
|
2634
|
+
file: diagnostic.file ?? "",
|
|
2635
|
+
line: diagnostic.line ?? 1,
|
|
2636
|
+
column: diagnostic.column ?? 1,
|
|
2637
|
+
message: diagnostic.message,
|
|
2638
|
+
code: diagnostic.code
|
|
2639
|
+
});
|
|
2640
|
+
var createVerificationOperations = (context, dependencies) => {
|
|
2641
|
+
const checkSnippet = async (code, packageName) => {
|
|
2642
|
+
const pkg = context.package(packageName);
|
|
2643
|
+
const registry = dependencies.virtualFiles ?? createVirtualFileRegistry(resolveVirtualFileDirectory(pkg.path));
|
|
2644
|
+
const workspace = workspaceWithVirtualFile(context);
|
|
2645
|
+
return withVirtualFile(registry, code, async (lease) => {
|
|
2646
|
+
const imports = typeof context.withProject !== "function" ? { content: "", lineOffset: 0 } : await context.withProject((project) => synthesizePackageImports(project, pkg.path, lease.path), packageName);
|
|
2647
|
+
return workspace.withVirtualFile(pkg.tsconfigPath, lease.path, `${imports.content}${lease.content}`, async (project, filePath) => {
|
|
2648
|
+
const errors = await snippetDiagnostics(project, filePath, imports.lineOffset);
|
|
2649
|
+
return errors.length === 0 ? { valid: true } : { valid: false, errors };
|
|
2650
|
+
});
|
|
2651
|
+
});
|
|
2652
|
+
};
|
|
2653
|
+
const explainError = async (options) => {
|
|
2654
|
+
let code = options.code ?? 0;
|
|
2655
|
+
let message = options.message ?? "";
|
|
2656
|
+
if (message.length === 0 && (options.code !== undefined || options.file !== undefined && options.line !== undefined)) {
|
|
2657
|
+
const diagnostics = asDiagnostics(await dependencies.diagnostics(options.packageName));
|
|
2658
|
+
const match = options.file !== undefined && options.line !== undefined ? diagnostics.find((diagnostic) => diagnostic.file?.endsWith(options.file) && diagnostic.line === options.line && (options.code === undefined || diagnostic.code === options.code)) : diagnostics.find((diagnostic) => diagnostic.code === options.code);
|
|
2659
|
+
if (match !== undefined) {
|
|
2660
|
+
code = match.code;
|
|
2661
|
+
message = match.message;
|
|
2662
|
+
}
|
|
2663
|
+
}
|
|
2664
|
+
if (message.length === 0)
|
|
2665
|
+
return null;
|
|
2666
|
+
const extracted = extractErrorParts(message);
|
|
2667
|
+
const explanationIssues = [];
|
|
2668
|
+
const suggestions = [];
|
|
2669
|
+
const result = { error: { code, message }, explanation: message, issues: explanationIssues, suggestions };
|
|
2670
|
+
const firstType = extracted.types[0];
|
|
2671
|
+
const secondType = extracted.types[1];
|
|
2672
|
+
if ((code === 2322 || code === 2345) && firstType !== undefined && secondType !== undefined) {
|
|
2673
|
+
let compatibility;
|
|
2674
|
+
try {
|
|
2675
|
+
const packageOption = options.packageName === undefined ? {} : { packageName: options.packageName };
|
|
2676
|
+
compatibility = await dependencies.compatibility(firstType, secondType, packageOption.packageName);
|
|
2677
|
+
} catch {
|
|
2678
|
+
compatibility = undefined;
|
|
2679
|
+
}
|
|
2680
|
+
const issues = compatibility?.issues ?? [];
|
|
2681
|
+
explanationIssues.push(...issues);
|
|
2682
|
+
if (!compatibility?.compatible) {
|
|
2683
|
+
result.explanation = `You're trying to use a value of type '${firstType}' where a value of type '${secondType}' is expected. These types are not compatible.`;
|
|
2684
|
+
}
|
|
2685
|
+
suggestions.push(`Add missing properties: ${secondType}`);
|
|
2686
|
+
suggestions.push(`Use Partial<${secondType}> if properties should be optional`);
|
|
2687
|
+
} else if (code === 2339 && firstType !== undefined && extracted.properties[0] !== undefined) {
|
|
2688
|
+
const property = extracted.properties[0];
|
|
2689
|
+
explanationIssues.push({ kind: "missing_property", property, message: `Property '${property}' does not exist on type '${firstType}'` });
|
|
2690
|
+
result.explanation = `You're trying to access property '${property}' on type '${firstType}', but this property doesn't exist.`;
|
|
2691
|
+
suggestions.push(`Check for typos in the property name`);
|
|
2692
|
+
suggestions.push(`Add property '${property}' to the type`);
|
|
2693
|
+
} else if (code === 2741 && extracted.properties[0] !== undefined && secondType !== undefined) {
|
|
2694
|
+
const property = extracted.properties[0];
|
|
2695
|
+
explanationIssues.push({ kind: "missing_property", property, message: `Property '${property}' is required but missing` });
|
|
2696
|
+
result.explanation = `Type '${firstType ?? "the source"}' is missing required property '${property}' that '${secondType}' expects.`;
|
|
2697
|
+
suggestions.push(`Add '${property}' to your object`);
|
|
2698
|
+
suggestions.push(`Make '${property}' optional in ${secondType} using '${property}?:'`);
|
|
2699
|
+
} else {
|
|
2700
|
+
explanationIssues.push({ kind: "other", message });
|
|
2701
|
+
suggestions.push("Review the types involved using type_expand");
|
|
2702
|
+
suggestions.push("Check type compatibility using type_compatible");
|
|
2703
|
+
}
|
|
2704
|
+
return result;
|
|
2705
|
+
};
|
|
2706
|
+
const verifyContract = async (options) => {
|
|
2707
|
+
const from = options.from?.trim() || undefined;
|
|
2708
|
+
const to = options.to?.trim() || undefined;
|
|
2709
|
+
const symbol = options.symbol?.trim() || undefined;
|
|
2710
|
+
const packageName = options.packageName?.trim() || undefined;
|
|
2711
|
+
const checks = {
|
|
2712
|
+
compatibility: skipped("Skipped because both from and to were not provided."),
|
|
2713
|
+
snippet: skipped("Skipped because no snippet was provided."),
|
|
2714
|
+
diagnostics: skipped("Skipped because includeDiagnostics was false."),
|
|
2715
|
+
transform: skipped("Skipped because both from and to were not provided.")
|
|
2716
|
+
};
|
|
2717
|
+
const evidence = {};
|
|
2718
|
+
const gaps = [];
|
|
2719
|
+
const nextSteps = new Set;
|
|
2720
|
+
const explanations = [];
|
|
2721
|
+
if (from === undefined !== (to === undefined)) {
|
|
2722
|
+
gaps.push("Only one side of the from/to contract was provided.");
|
|
2723
|
+
nextSteps.add("Provide both from and to to run compatibility and transform verification.");
|
|
2724
|
+
} else if (from !== undefined && to !== undefined) {
|
|
2725
|
+
const compatibility = await dependencies.compatibility(from, to, packageName);
|
|
2726
|
+
evidence.compatibility = compatibility;
|
|
2727
|
+
checks.compatibility = {
|
|
2728
|
+
ran: true,
|
|
2729
|
+
passed: compatibility.compatible,
|
|
2730
|
+
blocking: false,
|
|
2731
|
+
summary: compatibility.compatible ? `${from} is directly assignable to ${to}.` : `${from} is not directly assignable to ${to}; verified transform evidence can still satisfy a conversion contract.`,
|
|
2732
|
+
evidence: compatibility
|
|
2733
|
+
};
|
|
2734
|
+
if (!compatibility.compatible) {
|
|
2735
|
+
gaps.push("Direct assignability is not established for from -> to.");
|
|
2736
|
+
const explanationOptions = {
|
|
2737
|
+
code: 2322,
|
|
2738
|
+
message: compatibility.reason ?? `Type ${from} is not assignable to type ${to}.`,
|
|
2739
|
+
...packageName === undefined ? {} : { packageName }
|
|
2740
|
+
};
|
|
2741
|
+
const explanation = await explainError(explanationOptions);
|
|
2742
|
+
if (explanation !== null)
|
|
2743
|
+
explanations.push(explanation);
|
|
2744
|
+
}
|
|
2745
|
+
}
|
|
2746
|
+
if (options.snippet === undefined) {
|
|
2747
|
+
gaps.push("No snippet was supplied, so Quartz did not verify a concrete call site.");
|
|
2748
|
+
nextSteps.add("Add a minimal snippet that exercises the proposed contract at a call site.");
|
|
2749
|
+
} else {
|
|
2750
|
+
const snippet = await checkSnippet(options.snippet, packageName);
|
|
2751
|
+
evidence.snippet = snippet;
|
|
2752
|
+
checks.snippet = {
|
|
2753
|
+
ran: true,
|
|
2754
|
+
passed: snippet.valid,
|
|
2755
|
+
blocking: true,
|
|
2756
|
+
summary: snippet.valid ? "Snippet compiles under the package TypeScript project." : "Snippet has TypeScript errors.",
|
|
2757
|
+
evidence: snippet
|
|
2758
|
+
};
|
|
2759
|
+
if (!snippet.valid) {
|
|
2760
|
+
gaps.push("The supplied snippet does not compile.");
|
|
2761
|
+
nextSteps.add("Repair the snippet until check-snippet returns valid: true.");
|
|
2762
|
+
const firstError = snippet.errors?.[0];
|
|
2763
|
+
if (firstError !== undefined) {
|
|
2764
|
+
const explanationOptions = {
|
|
2765
|
+
message: firstError.message,
|
|
2766
|
+
...packageName === undefined ? {} : { packageName }
|
|
2767
|
+
};
|
|
2768
|
+
const explanation = await explainError(explanationOptions);
|
|
2769
|
+
if (explanation !== null)
|
|
2770
|
+
explanations.push(explanation);
|
|
2771
|
+
}
|
|
2772
|
+
}
|
|
2773
|
+
}
|
|
2774
|
+
if (options.includeDiagnostics !== false) {
|
|
2775
|
+
const diagnostics = asDiagnostics(await dependencies.diagnostics(packageName));
|
|
2776
|
+
const diagnosticEvidence = diagnostics.map(toVerifyDiagnostic);
|
|
2777
|
+
evidence.diagnostics = diagnosticEvidence;
|
|
2778
|
+
checks.diagnostics = {
|
|
2779
|
+
ran: true,
|
|
2780
|
+
passed: diagnosticEvidence.length === 0,
|
|
2781
|
+
blocking: true,
|
|
2782
|
+
summary: diagnosticEvidence.length === 0 ? "Package diagnostics are clean." : `Package has ${diagnosticEvidence.length} TypeScript diagnostic(s).`,
|
|
2783
|
+
evidence: diagnosticEvidence
|
|
2784
|
+
};
|
|
2785
|
+
if (diagnosticEvidence.length > 0) {
|
|
2786
|
+
gaps.push("The package has ambient TypeScript diagnostics.");
|
|
2787
|
+
nextSteps.add("Inspect diagnostics before trusting the contract in this project state.");
|
|
2788
|
+
}
|
|
2789
|
+
}
|
|
2790
|
+
if (from !== undefined && to !== undefined) {
|
|
2791
|
+
if (options.includeTransformEvidence === false) {
|
|
2792
|
+
gaps.push("Transform evidence was skipped by includeTransformEvidence: false.");
|
|
2793
|
+
if (symbol !== undefined)
|
|
2794
|
+
nextSteps.add("Enable transform evidence to verify that the requested symbol backs the contract.");
|
|
2795
|
+
} else {
|
|
2796
|
+
const transformOptions = {
|
|
2797
|
+
from,
|
|
2798
|
+
to,
|
|
2799
|
+
verifiedOnly: true,
|
|
2800
|
+
limit: options.transformLimit ?? 10,
|
|
2801
|
+
...packageName === undefined ? {} : { packageName }
|
|
2802
|
+
};
|
|
2803
|
+
const transformSearch = await dependencies.transformSearch(transformOptions);
|
|
2804
|
+
evidence.transformSearch = transformSearch;
|
|
2805
|
+
const verifiedResults = transformSearch.results.filter((result) => result.verification.status === "verified");
|
|
2806
|
+
const symbolMatched = symbol === undefined || verifiedResults.some((result) => result.name === symbol || result.name.endsWith(`.${symbol}`));
|
|
2807
|
+
const passed = verifiedResults.length > 0 && symbolMatched;
|
|
2808
|
+
checks.transform = {
|
|
2809
|
+
ran: true,
|
|
2810
|
+
passed,
|
|
2811
|
+
blocking: true,
|
|
2812
|
+
summary: passed ? "A compiler-verified transform satisfies the requested contract." : symbol === undefined ? "No compiler-verified transform candidate matched the requested contract." : `No compiler-verified transform candidate matched the requested symbol '${symbol}'.`,
|
|
2813
|
+
evidence: transformSearch
|
|
2814
|
+
};
|
|
2815
|
+
if (!passed) {
|
|
2816
|
+
gaps.push(symbol === undefined ? "No compiler-verified transform candidate matched the requested contract." : `No compiler-verified transform candidate matched the requested symbol '${symbol}'.`);
|
|
2817
|
+
nextSteps.add("Run transform-search with includeDiagnostics/includeSyntheticCode to inspect candidate verifier failures.");
|
|
2818
|
+
}
|
|
2819
|
+
}
|
|
2820
|
+
}
|
|
2821
|
+
if (explanations.length > 0)
|
|
2822
|
+
evidence.explanations = explanations;
|
|
2823
|
+
const directAssignable = checks.compatibility.passed === true;
|
|
2824
|
+
const transformPassed = checks.transform.passed === true;
|
|
2825
|
+
const snippetOk = checks.snippet.passed !== false;
|
|
2826
|
+
const diagnosticsOk = checks.diagnostics.passed !== false;
|
|
2827
|
+
const hasFromTo = from !== undefined && to !== undefined;
|
|
2828
|
+
const contractEvidenceOk = hasFromTo ? directAssignable || transformPassed : true;
|
|
2829
|
+
const hasPositiveEvidence = directAssignable || transformPassed || checks.snippet.passed === true;
|
|
2830
|
+
const blockingChecksPassed = Object.values(checks).every((check) => !check.blocking || check.passed === true);
|
|
2831
|
+
const ok = snippetOk && diagnosticsOk && contractEvidenceOk && hasPositiveEvidence && blockingChecksPassed;
|
|
2832
|
+
if (!ok)
|
|
2833
|
+
nextSteps.add("Treat this contract as untrusted until a blocking check passes.");
|
|
2834
|
+
return {
|
|
2835
|
+
schemaVersion: "verify-contract/v1",
|
|
2836
|
+
ok,
|
|
2837
|
+
contract: { ...from === undefined ? {} : { from }, ...to === undefined ? {} : { to }, ...symbol === undefined ? {} : { symbol }, package: context.package(packageName).name },
|
|
2838
|
+
checks,
|
|
2839
|
+
evidence,
|
|
2840
|
+
gaps,
|
|
2841
|
+
next_steps: [...nextSteps]
|
|
2842
|
+
};
|
|
2843
|
+
};
|
|
2844
|
+
return { checkSnippet, explainError, verifyContract };
|
|
2845
|
+
};
|
|
2846
|
+
|
|
2847
|
+
// src/analyzer.ts
|
|
2848
|
+
class QuartzAnalyzer {
|
|
2849
|
+
#context;
|
|
2850
|
+
#leaf;
|
|
2851
|
+
#references;
|
|
2852
|
+
#transformSearchOperation;
|
|
2853
|
+
#verification;
|
|
2854
|
+
#disposePromise = null;
|
|
2855
|
+
constructor(context) {
|
|
2856
|
+
this.#context = context;
|
|
2857
|
+
this.#leaf = createLeafOperations(context);
|
|
2858
|
+
this.#references = createReferenceOperations(context);
|
|
2859
|
+
this.#transformSearchOperation = createTransformSearchOperation(context);
|
|
2860
|
+
this.#verification = createVerificationOperations(context, {
|
|
2861
|
+
compatibility: this.#leaf.checkCompatibility,
|
|
2862
|
+
diagnostics: this.#leaf.getDiagnostics,
|
|
2863
|
+
transformSearch: this.#transformSearchOperation
|
|
2864
|
+
});
|
|
2865
|
+
}
|
|
2866
|
+
static async open(root, options) {
|
|
2867
|
+
return new QuartzAnalyzer(await AnalyzerContext.open(root, options));
|
|
2868
|
+
}
|
|
2869
|
+
get metadata() {
|
|
2870
|
+
return this.#context.workspace.metadata;
|
|
2871
|
+
}
|
|
2872
|
+
getTimingInfo = () => this.#context.workspace.getTimingInfo();
|
|
2873
|
+
resetTimingInfo = () => this.#context.workspace.resetTimingInfo();
|
|
2874
|
+
getPackages = async () => {
|
|
2875
|
+
this.#assertOpen();
|
|
2876
|
+
return this.#leaf.getPackages();
|
|
2877
|
+
};
|
|
2878
|
+
listSymbols = (options) => this.#leaf.listSymbols(options);
|
|
2879
|
+
getTypeInfo = (symbolName, packageName) => this.#leaf.getTypeInfo(symbolName, packageName);
|
|
2880
|
+
expandType = (symbolName, packageName) => this.#leaf.expandType(symbolName, packageName);
|
|
2881
|
+
findRelated = (symbolName, packageName) => this.#references.findRelated(symbolName, packageName);
|
|
2882
|
+
searchTypes = (options) => this.#leaf.searchTypes(options);
|
|
2883
|
+
evalType = (expression, packageName) => this.#leaf.evalType(expression, packageName);
|
|
2884
|
+
checkSnippet = (code, packageName) => this.#verification.checkSnippet(code, packageName);
|
|
2885
|
+
getFileDeclarations = (file, options) => this.#leaf.getFileDeclarations(file, options);
|
|
2886
|
+
checkCompatibility = (from, to, packageName) => this.#leaf.checkCompatibility(from, to, packageName);
|
|
2887
|
+
generateGraph = (symbol, options) => this.#references.generateGraph(symbol, options);
|
|
2888
|
+
previewRefactor = (options) => this.#references.previewRefactor(options);
|
|
2889
|
+
getDiagnostics = async (packageNameOrOptions) => {
|
|
2890
|
+
const packageName = typeof packageNameOrOptions === "string" ? packageNameOrOptions : packageNameOrOptions?.packageName;
|
|
2891
|
+
const diagnostics = await this.#leaf.getDiagnostics(packageName);
|
|
2892
|
+
if (typeof packageNameOrOptions !== "object" || packageNameOrOptions.explain !== true) {
|
|
2893
|
+
return diagnostics;
|
|
2894
|
+
}
|
|
2895
|
+
const raw = diagnostics;
|
|
2896
|
+
const errors = [];
|
|
2897
|
+
for (const diagnostic of raw.slice(0, 10)) {
|
|
2898
|
+
const explanation = await this.#verification.explainError({
|
|
2899
|
+
code: diagnostic.code,
|
|
2900
|
+
message: diagnostic.message,
|
|
2901
|
+
...diagnostic.file === undefined ? {} : { file: diagnostic.file },
|
|
2902
|
+
...diagnostic.line === undefined ? {} : { line: diagnostic.line },
|
|
2903
|
+
...packageName === undefined ? {} : { packageName }
|
|
2904
|
+
});
|
|
2905
|
+
errors.push({ ...diagnostic, explanation });
|
|
2906
|
+
}
|
|
2907
|
+
return {
|
|
2908
|
+
totalErrors: raw.length,
|
|
2909
|
+
explained: errors.length,
|
|
2910
|
+
truncated: raw.length > 10,
|
|
2911
|
+
errors
|
|
2912
|
+
};
|
|
2913
|
+
};
|
|
2914
|
+
getTypeAtPosition = (filePath, line, column, packageName) => this.#leaf.getTypeAtPosition(filePath, line, column, packageName);
|
|
2915
|
+
explainError = (options) => this.#verification.explainError(options);
|
|
2916
|
+
explainType = (expression, packageName) => this.#leaf.explainType(expression, packageName);
|
|
2917
|
+
transformSearch = (options) => this.#transformSearchOperation(options);
|
|
2918
|
+
verifyContract = (options) => this.#verification.verifyContract(options);
|
|
2919
|
+
async refresh(packageName) {
|
|
2920
|
+
if (packageName === undefined) {
|
|
2921
|
+
await this.#context.refresh();
|
|
2922
|
+
return "Refreshed all TypeScript projects";
|
|
2923
|
+
}
|
|
2924
|
+
const pkg = this.#context.package(packageName);
|
|
2925
|
+
await this.#context.refreshPackage(packageName);
|
|
2926
|
+
return `Refreshed package ${pkg.name}`;
|
|
2927
|
+
}
|
|
2928
|
+
async markDirty() {
|
|
2929
|
+
this.#context.markDirty();
|
|
2930
|
+
}
|
|
2931
|
+
dispose() {
|
|
2932
|
+
this.#disposePromise ??= this.#context.close();
|
|
2933
|
+
return this.#disposePromise;
|
|
2934
|
+
}
|
|
2935
|
+
#assertOpen() {
|
|
2936
|
+
if (this.#disposePromise !== null) {
|
|
2937
|
+
throw new QuartzEngineError("WORKSPACE_CLOSED", `Quartz analyzer at ${this.#context.root} is closed`);
|
|
2938
|
+
}
|
|
2939
|
+
}
|
|
2940
|
+
}
|
|
2941
|
+
var createTypeAnalyzer = (root, options) => QuartzAnalyzer.open(root, options);
|
|
2942
|
+
export {
|
|
2943
|
+
openQuartzWorkspace,
|
|
2944
|
+
createTypeAnalyzer,
|
|
2945
|
+
version2 as analysisTypeScriptVersion,
|
|
2946
|
+
QuartzWorkspace,
|
|
2947
|
+
QuartzEngineError,
|
|
2948
|
+
QuartzAnalyzer
|
|
2949
|
+
};
|