fallow-type-aware 0.0.0-bootstrap.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.
@@ -0,0 +1,2142 @@
1
+ import { createHash } from "node:crypto";
2
+ import { existsSync, readFileSync } from "node:fs";
3
+ import path from "node:path";
4
+
5
+ import { API, SignatureKind, SymbolFlags } from "typescript/unstable/sync";
6
+ import {
7
+ isCallExpression,
8
+ isClassDeclaration,
9
+ isClassExpression,
10
+ isDecorator,
11
+ isElementAccessExpression,
12
+ isExportDeclaration,
13
+ isExportSpecifier,
14
+ isFunctionBody,
15
+ isGetAccessorDeclaration,
16
+ isIdentifier,
17
+ isImportDeclaration,
18
+ isImportClause,
19
+ isImportSpecifier,
20
+ isImportTypeNode,
21
+ isNamespaceImport,
22
+ isPropertyAccessExpression,
23
+ isPrivateIdentifier,
24
+ isSetAccessorDeclaration,
25
+ isStringLiteralLikeNode,
26
+ isTypeNode,
27
+ isTypeQueryNode,
28
+ } from "typescript/unstable/ast/is";
29
+
30
+ import { canonicalFileIdentity } from "./file-identity.mjs";
31
+ import { projectResult, projectState } from "./project-state.mjs";
32
+ import {
33
+ declarationName,
34
+ declarationNamespaces,
35
+ declarationsForSymbol,
36
+ findDeclaration,
37
+ isDeclaration,
38
+ isProjectSource,
39
+ nodeText,
40
+ ownerDeclaration,
41
+ projectExportIndex,
42
+ projectSourceFiles,
43
+ relativePath,
44
+ resolveAlias,
45
+ semanticQueryIdentity,
46
+ sourceFileIdentity,
47
+ stableDeclarationKey,
48
+ stableSymbolIdentity,
49
+ symbolForDeclaration,
50
+ } from "./semantic-identity.mjs";
51
+ import { analyzeSymbolImpact } from "./symbol-impact.mjs";
52
+ import { analyzeTypeCoupling } from "./type-coupling.mjs";
53
+
54
+ const ATTACHED_COMMENT_PATTERN = /\/\/|\/\*/u;
55
+ const MAX_SYMBOL_USE_EVIDENCE = 1;
56
+ const DECLARATION_KINDS = new Set([
57
+ "export",
58
+ "class",
59
+ "interface",
60
+ "type_alias",
61
+ "enum",
62
+ "function",
63
+ "namespace",
64
+ "variable",
65
+ "class_method",
66
+ "class_property",
67
+ ]);
68
+
69
+ const compareText = (left, right) => Buffer.compare(Buffer.from(left), Buffer.from(right));
70
+ const locationKey = ({ path: filePath, line, col }) => `${filePath}\0${line}\0${col}`;
71
+
72
+ const location = (root, node) => {
73
+ const sourceFile = node.getSourceFile();
74
+ const start = node.getStart(sourceFile);
75
+ const { line } = sourceFile.getLineAndCharacterOfPosition(start);
76
+ const lineStart = sourceFile.getPositionOfLineAndCharacter(line, 0);
77
+ return {
78
+ path: relativePath(root, sourceFile.fileName),
79
+ line: line + 1,
80
+ col: Buffer.byteLength(sourceFile.text.slice(lineStart, start), "utf8"),
81
+ };
82
+ };
83
+
84
+ const visit = (node, callback) => {
85
+ callback(node);
86
+ node.forEachChild((child) => {
87
+ visit(child, callback);
88
+ return undefined;
89
+ });
90
+ };
91
+
92
+ const conservativeAssertion = (operation) =>
93
+ ({
94
+ "symbol-use": "no-confirmed-use",
95
+ "symbol-trace": "no-references-found",
96
+ "api-surface": "no-leak-confirmed",
97
+ "symbol-impact": "no-consumers-found",
98
+ "type-coupling": "no-coupling-found",
99
+ })[operation];
100
+
101
+ const unavailable = (query, reasonCode, action) => ({
102
+ queryId: query.id,
103
+ operation: query.operation,
104
+ assertion: conservativeAssertion(query.operation),
105
+ status: "unavailable",
106
+ reasonCode,
107
+ actions: [action],
108
+ evidence: [],
109
+ totalEvidenceCount: 0,
110
+ truncated: false,
111
+ omissions: [{ reason_code: reasonCode, count: 1 }],
112
+ data: {},
113
+ });
114
+
115
+ const REASON_ACTIONS = new Map([
116
+ ["dynamic-behavior", "Review dynamic imports and registrations before changing this symbol."],
117
+ [
118
+ "virtual-dispatch",
119
+ "Inspect interface, inherited, and virtual call sites before changing this implementation.",
120
+ ],
121
+ [
122
+ "dynamic-member-access",
123
+ "Replace or review computed, reflective, and string-dispatched member access before retrying.",
124
+ ],
125
+ [
126
+ "decorated-declaration",
127
+ "Review the class or member decorator contract before removing this declaration.",
128
+ ],
129
+ [
130
+ "optional-contract",
131
+ "Review the optional interface or inherited contract before removing this declaration.",
132
+ ],
133
+ [
134
+ "accessor-pair",
135
+ "Retain paired accessors unless the getter and setter are analyzed and removed together.",
136
+ ],
137
+ [
138
+ "overload-set",
139
+ "Retain overloaded members unless every declaration in the overload set is changed together.",
140
+ ],
141
+ ["attached-comment", "Review the attached declaration comment before removing this member."],
142
+ ["abstract-declaration", "Retain abstract declarations because they define a class contract."],
143
+ [
144
+ "incomplete-project-coverage",
145
+ "Repair every owning TypeScript project or select the complete project set and retry.",
146
+ ],
147
+ [
148
+ "framework-contract-provenance",
149
+ "Use a project layout that exposes the framework declaration's exact package provenance.",
150
+ ],
151
+ [
152
+ "ambiguous-project",
153
+ "Select projects that resolve this declaration to one consistent symbol identity.",
154
+ ],
155
+ [
156
+ "blocking-diagnostics",
157
+ "Repair structural TypeScript diagnostics in every selected project and retry.",
158
+ ],
159
+ [
160
+ "unknown-entry-point",
161
+ "Refresh the package entry points or pass project-relative source entry points.",
162
+ ],
163
+ ]);
164
+ const DEFAULT_REASON_ACTION =
165
+ "Narrow the query to a specific symbol, entry point, or healthy TypeScript project and retry.";
166
+ const REASON_PRIORITY = new Map([
167
+ ["blocking-diagnostics", 0],
168
+ ["incomplete-project-coverage", 1],
169
+ ["framework-contract-provenance", 2],
170
+ ["ambiguous-project", 3],
171
+ ["unknown-symbol", 4],
172
+ ["unknown-entry-point", 5],
173
+ ["decorated-declaration", 6],
174
+ ["optional-contract", 7],
175
+ ["accessor-pair", 8],
176
+ ["overload-set", 9],
177
+ ["attached-comment", 10],
178
+ ["abstract-declaration", 11],
179
+ ["dynamic-member-access", 12],
180
+ ["virtual-dispatch", 13],
181
+ ["dynamic-behavior", 14],
182
+ ["evidence-limit", 15],
183
+ ]);
184
+
185
+ const actionForReason = (reasonCode) => REASON_ACTIONS.get(reasonCode) ?? DEFAULT_REASON_ACTION;
186
+
187
+ const orderedEvidence = (evidence) =>
188
+ [...evidence].toSorted((left, right) => compareText(JSON.stringify(left), JSON.stringify(right)));
189
+
190
+ const evidenceLimitOmission = (totalEvidence, returnedEvidenceCount) =>
191
+ totalEvidence > returnedEvidenceCount
192
+ ? [{ reason_code: "evidence-limit", count: totalEvidence - returnedEvidenceCount }]
193
+ : [];
194
+
195
+ const combineOmissions = (omissions) => {
196
+ const counts = new Map();
197
+ for (const omission of omissions.filter((entry) => entry.count > 0)) {
198
+ counts.set(omission.reason_code, (counts.get(omission.reason_code) ?? 0) + omission.count);
199
+ }
200
+ return [...counts].map(([reason_code, count]) => ({ reason_code, count }));
201
+ };
202
+
203
+ const resultStatus = (partial) => (partial ? "partial" : "complete");
204
+ const resultReason = (omissions) => (omissions.length > 0 ? omissions[0].reason_code : null);
205
+ const resultActions = (omissions) =>
206
+ omissions.map((omission) => actionForReason(omission.reason_code));
207
+
208
+ const compareOmissions = (left, right) =>
209
+ (REASON_PRIORITY.get(left.reason_code) ?? 10) - (REASON_PRIORITY.get(right.reason_code) ?? 10) ||
210
+ compareText(left.reason_code, right.reason_code);
211
+
212
+ const boundedResult = ({
213
+ query,
214
+ assertion,
215
+ evidence,
216
+ data,
217
+ evidenceLimit,
218
+ totalEvidenceCount,
219
+ omissions: extraOmissions = [],
220
+ }) => {
221
+ const ordered = orderedEvidence(evidence);
222
+ const totalEvidence = totalEvidenceCount === undefined ? ordered.length : totalEvidenceCount;
223
+ const returnedEvidenceCount = Math.min(ordered.length, evidenceLimit);
224
+ const omissions = combineOmissions([
225
+ ...evidenceLimitOmission(totalEvidence, returnedEvidenceCount),
226
+ ...extraOmissions,
227
+ ]).toSorted(compareOmissions);
228
+ const partial = omissions.length > 0;
229
+ const truncated = omissions.some((omission) => omission.reason_code === "evidence-limit");
230
+ return {
231
+ queryId: query.id,
232
+ operation: query.operation,
233
+ assertion,
234
+ status: resultStatus(partial),
235
+ reasonCode: resultReason(omissions),
236
+ actions: resultActions(omissions),
237
+ evidence: ordered.slice(0, evidenceLimit),
238
+ totalEvidenceCount: totalEvidence,
239
+ truncated,
240
+ omissions,
241
+ data,
242
+ };
243
+ };
244
+
245
+ const TYPE_POSITION_RULES = [
246
+ [isTypeQueryNode, () => false],
247
+ [isImportTypeNode, (node) => !node.isTypeOf],
248
+ [
249
+ (node) => isImportSpecifier(node) || isExportSpecifier(node),
250
+ (node) => Boolean(node.isTypeOnly || node.parent?.parent?.isTypeOnly),
251
+ ],
252
+ [isTypeNode, () => true],
253
+ [
254
+ (node) => isDeclaration(node) || isImportDeclaration(node) || isExportDeclaration(node),
255
+ () => false,
256
+ ],
257
+ ];
258
+
259
+ const typePositionAt = (node) => {
260
+ const rule = TYPE_POSITION_RULES.find(([matches]) => matches(node));
261
+ return rule?.[1](node);
262
+ };
263
+
264
+ const isTypePosition = (node) => {
265
+ let current = node.parent;
266
+ while (current) {
267
+ const result = typePositionAt(current);
268
+ if (result !== undefined) return result;
269
+ current = current.parent;
270
+ }
271
+ return false;
272
+ };
273
+
274
+ const referenceRoleAt = (node) => {
275
+ const rules = [
276
+ [isImportSpecifier, node.isTypeOnly ? "type-import" : "import"],
277
+ [isExportSpecifier, node.isTypeOnly ? "type-re-export" : "re-export"],
278
+ [isCallExpression, "call"],
279
+ [isPropertyAccessExpression, "property-access"],
280
+ ];
281
+ return rules.find(([matches]) => matches(node))?.[1];
282
+ };
283
+
284
+ const referenceRole = (node) => {
285
+ if (isTypePosition(node)) return "type-reference";
286
+ const role = referenceRoleAt(node);
287
+ if (role) return role;
288
+ if (isReferenceBoundary(node)) return "value-reference";
289
+ return referenceRole(node.parent);
290
+ };
291
+
292
+ const isReferenceBoundary = (node) => !node.parent || isDeclaration(node);
293
+ const isAliasSpecifier = (node) => isImportSpecifier(node) || isExportSpecifier(node);
294
+
295
+ const aliasNode = (node) => {
296
+ if (!node) return undefined;
297
+ if (isDeclaration(node)) return undefined;
298
+ if (isAliasSpecifier(node)) return node;
299
+ return aliasNode(node.parent);
300
+ };
301
+
302
+ const aliasRelation = (alias) => (isImportSpecifier(alias) ? "import-alias" : "re-export");
303
+ const aliasNames = (alias) => {
304
+ const aliasName = nodeText(alias.name);
305
+ const fromName = nodeText(alias.propertyName) ?? aliasName;
306
+ return { fromName, toName: aliasName ?? fromName };
307
+ };
308
+
309
+ const aliasHop = (root, node) => {
310
+ const alias = aliasNode(node);
311
+ if (!alias) return undefined;
312
+ const names = aliasNames(alias);
313
+ return {
314
+ ...location(root, alias),
315
+ from_name: names.fromName,
316
+ to_name: names.toName,
317
+ relation: aliasRelation(alias),
318
+ };
319
+ };
320
+
321
+ const semanticUse = (root, node, namespace) => {
322
+ const hop = aliasHop(root, node);
323
+ return {
324
+ ...location(root, node),
325
+ role: referenceRole(node),
326
+ source: "checker",
327
+ namespace,
328
+ via: hop ? [hop] : [],
329
+ };
330
+ };
331
+
332
+ const isDefaultImportReference = (node) => isImportClause(node.parent) && node.parent.name === node;
333
+
334
+ const candidateNameMatches = (candidateNames, name) => !candidateNames || candidateNames.has(name);
335
+
336
+ const isIdentifierReference = (node, candidateNames, includeDefaultImports) => {
337
+ if (!isIdentifier(node)) return false;
338
+ if (candidateNameMatches(candidateNames, node.text)) return true;
339
+ return includeDefaultImports && isDefaultImportReference(node);
340
+ };
341
+
342
+ const isElementNameNode = (node) =>
343
+ isStringLiteralLikeNode(node) &&
344
+ isElementAccessExpression(node.parent) &&
345
+ node.parent.argumentExpression === node;
346
+
347
+ const isElementReference = (node, candidateNames) =>
348
+ isElementNameNode(node) && candidateNameMatches(candidateNames, node.text);
349
+
350
+ const entriesByName = (entries) => {
351
+ const byName = new Map();
352
+ for (const entry of entries) {
353
+ const names = new Set([entry.query.symbol.localName, entry.query.symbol.exportedName]);
354
+ for (const name of names) {
355
+ const named = byName.get(name) ?? [];
356
+ named.push(entry);
357
+ byName.set(name, named);
358
+ }
359
+ }
360
+ return byName;
361
+ };
362
+
363
+ const symbolUseBatchState = (resolvedQueries, evidenceLimit) => {
364
+ const classMemberEntries = resolvedQueries.filter(({ query }) =>
365
+ ["class_method", "class_property"].includes(query.symbol.declarationKind),
366
+ );
367
+ return {
368
+ evidenceByQuery: new Map(resolvedQueries.map(({ query }) => [query.id, []])),
369
+ totalByQuery: new Map(resolvedQueries.map(({ query }) => [query.id, 0])),
370
+ directFilesByQuery: new Map(resolvedQueries.map(({ query }) => [query.id, new Set()])),
371
+ aliasHopTotalByQuery: new Map(resolvedQueries.map(({ query }) => [query.id, 0])),
372
+ contractRelationsByQuery: new Map(resolvedQueries.map(({ query }) => [query.id, []])),
373
+ uncertaintiesByQuery: new Map(resolvedQueries.map(({ query }) => [query.id, new Set()])),
374
+ declarationLocationsByQuery: new Map(resolvedQueries.map(({ query }) => [query.id, new Set()])),
375
+ targetsByKey: new Map(),
376
+ exportEntriesByModule: new Map(),
377
+ exportEntriesByProject: new Map(),
378
+ candidateNames: new Set(),
379
+ classMemberEntries,
380
+ classMemberEntriesByName: entriesByName(classMemberEntries),
381
+ classMemberEntrySetsByProject: new Map(),
382
+ projectsWithUnknownDynamicAccess: new Set(),
383
+ evidenceLimit,
384
+ };
385
+ };
386
+
387
+ const addSymbolTarget = (root, state, entry, declaration, namespace) => {
388
+ if (!declarationNamespaces(declaration).has(namespace)) return;
389
+ state.declarationLocationsByQuery
390
+ .get(entry.query.id)
391
+ .add(locationKey(location(root, declaration.name ?? declaration)));
392
+ const key = stableDeclarationKey(declaration, namespace);
393
+ const targets = state.targetsByKey.get(key) ?? [];
394
+ targets.push(entry);
395
+ state.targetsByKey.set(key, targets);
396
+ };
397
+
398
+ const registerSymbolTargets = (root, state, entry) => {
399
+ state.candidateNames.add(entry.query.symbol.localName);
400
+ state.candidateNames.add(entry.query.symbol.exportedName);
401
+ if (entry.query.symbol.declarationKind === "export") {
402
+ const moduleIdentity = canonicalFileIdentity(entry.query.symbol.absolutePath);
403
+ const entries = state.exportEntriesByModule.get(moduleIdentity) ?? new Set();
404
+ entries.add(entry);
405
+ state.exportEntriesByModule.set(moduleIdentity, entries);
406
+ entry.resolved.ownerContexts.forEach(({ project }) => {
407
+ const projectEntries = state.exportEntriesByProject.get(project) ?? new Set();
408
+ projectEntries.add(entry);
409
+ state.exportEntriesByProject.set(project, projectEntries);
410
+ });
411
+ }
412
+ entry.resolved.ownerContexts.forEach(({ project, declaration }) => {
413
+ const symbol = resolveAlias(project.checker, symbolForDeclaration(project, declaration));
414
+ declarationsForSymbol(project, symbol).forEach((target) => {
415
+ const namespaces =
416
+ entry.query.symbol.declarationKind === "export"
417
+ ? declarationNamespaces(target)
418
+ : new Set([entry.query.symbol.namespace]);
419
+ namespaces.forEach((namespace) => addSymbolTarget(root, state, entry, target, namespace));
420
+ });
421
+ });
422
+ state.contractRelationsByQuery.set(entry.query.id, entry.resolved.contractRelations);
423
+ };
424
+
425
+ const matchingSymbolEntries = (project, symbol, namespace, targetsByKey) => {
426
+ return new Set(
427
+ declarationsForSymbol(project, symbol)
428
+ .filter((declaration) => declarationNamespaces(declaration).has(namespace))
429
+ .flatMap((declaration) => {
430
+ const key = stableDeclarationKey(declaration, namespace);
431
+ return targetsByKey.get(key) ?? [];
432
+ }),
433
+ );
434
+ };
435
+
436
+ const isDeclarationReference = (root, state, entry, node) => {
437
+ const locations = state.declarationLocationsByQuery.get(entry.query.id);
438
+ if (locations.has(locationKey(location(root, node)))) return true;
439
+ return node === entry.resolved.declaration.name || node.parent === entry.resolved.declaration;
440
+ };
441
+
442
+ const moduleEdgeDeclaration = (node) => {
443
+ let current = node;
444
+ while (current) {
445
+ if (isImportDeclaration(current) || isExportDeclaration(current)) return current;
446
+ current = current.parent;
447
+ }
448
+ return undefined;
449
+ };
450
+
451
+ const ancestorImportType = (node) => {
452
+ let current = node;
453
+ while (current) {
454
+ if (isImportTypeNode(current)) return current;
455
+ current = current.parent;
456
+ }
457
+ return undefined;
458
+ };
459
+
460
+ const importTypeSpecifier = (node) => {
461
+ const argument = ancestorImportType(node)?.argument;
462
+ const literal = argument?.literal;
463
+ return literal && isStringLiteralLikeNode(literal) ? literal : undefined;
464
+ };
465
+
466
+ const isDynamicImportCall = (node) =>
467
+ isCallExpression(node) && node.expression.getText(node.getSourceFile()) === "import";
468
+
469
+ const moduleIdentityForSpecifier = (project, specifier) => {
470
+ if (!specifier) return undefined;
471
+ const moduleSymbol = project.checker.getSymbolAtLocation(specifier);
472
+ const moduleDeclaration = declarationsForSymbol(
473
+ project,
474
+ resolveAlias(project.checker, moduleSymbol),
475
+ )[0];
476
+ return moduleDeclaration ? sourceFileIdentity(moduleDeclaration.getSourceFile()) : undefined;
477
+ };
478
+
479
+ const namespaceImportDeclaration = (project, node) => {
480
+ const parent = node.parent;
481
+ if (!parent) return undefined;
482
+ const namespace = isPropertyAccessExpression(parent)
483
+ ? parent.name === node
484
+ ? parent.expression
485
+ : undefined
486
+ : isElementAccessExpression(parent) && parent.argumentExpression === node
487
+ ? parent.expression
488
+ : undefined;
489
+ if (!namespace) return undefined;
490
+ if (!isIdentifier(namespace)) return undefined;
491
+ const symbol = project.checker.getSymbolAtLocation(namespace);
492
+ return symbol?.declarations
493
+ ?.map((declaration) => declaration.resolve(project))
494
+ .find(isNamespaceImport);
495
+ };
496
+
497
+ const referencedModuleIdentity = (project, node) => {
498
+ const declaration =
499
+ moduleEdgeDeclaration(node) ?? moduleEdgeDeclaration(namespaceImportDeclaration(project, node));
500
+ const declarationSpecifier = declaration?.moduleSpecifier;
501
+ const specifier =
502
+ declarationSpecifier && isStringLiteralLikeNode(declarationSpecifier)
503
+ ? declarationSpecifier
504
+ : importTypeSpecifier(node);
505
+ return moduleIdentityForSpecifier(project, specifier);
506
+ };
507
+
508
+ const isExactExportReference = (project, entry, node) =>
509
+ entry.query.symbol.declarationKind !== "export" ||
510
+ referencedModuleIdentity(project, node) ===
511
+ canonicalFileIdentity(entry.query.symbol.absolutePath);
512
+
513
+ const recordSymbolUse = (root, state, project, entry, node, namespace) => {
514
+ if (isDeclarationReference(root, state, entry, node)) return;
515
+ if (!isExactExportReference(project, entry, node)) return;
516
+ const evidence = state.evidenceByQuery.get(entry.query.id);
517
+ state.totalByQuery.set(entry.query.id, state.totalByQuery.get(entry.query.id) + 1);
518
+ state.directFilesByQuery.get(entry.query.id).add(sourceFileIdentity(node.getSourceFile()));
519
+ const use = semanticUse(root, node, namespace);
520
+ if (use.via.length > 0) {
521
+ state.aliasHopTotalByQuery.set(
522
+ entry.query.id,
523
+ state.aliasHopTotalByQuery.get(entry.query.id) + use.via.length,
524
+ );
525
+ }
526
+ const limit =
527
+ entry.query.operation === "symbol-use" ? MAX_SYMBOL_USE_EVIDENCE : state.evidenceLimit;
528
+ if (evidence.length < limit) {
529
+ evidence.push(use);
530
+ }
531
+ };
532
+
533
+ const projectOwnsEntry = (project, entry) =>
534
+ entry.resolved.ownerContexts.some((context) => context.project === project);
535
+
536
+ const classMemberEntrySetForProject = (state, project) => {
537
+ const cached = state.classMemberEntrySetsByProject.get(project);
538
+ if (cached) return cached;
539
+ const entries = new Set(
540
+ state.classMemberEntries.filter((entry) => projectOwnsEntry(project, entry)),
541
+ );
542
+ state.classMemberEntrySetsByProject.set(project, entries);
543
+ return entries;
544
+ };
545
+
546
+ const recordSymbolUncertainty = (state, entry, reasonCode) => {
547
+ state.uncertaintiesByQuery.get(entry.query.id).add(reasonCode);
548
+ };
549
+
550
+ const isExactElementReference = (node) => isElementNameNode(node) && typeof node.text === "string";
551
+
552
+ const recordStringDispatchedMemberAccess = (state, projectEntries, node) => {
553
+ if (!isStringLiteralLikeNode(node) || isExactElementReference(node)) return;
554
+ (state.classMemberEntriesByName.get(node.text) ?? [])
555
+ .filter((entry) => projectEntries.has(entry))
556
+ .forEach((entry) => recordSymbolUncertainty(state, entry, "dynamic-member-access"));
557
+ };
558
+
559
+ const recordComputedMemberAccess = (state, project, entries, node) => {
560
+ if (
561
+ entries.size === 0 ||
562
+ state.projectsWithUnknownDynamicAccess.has(project) ||
563
+ !isElementAccessExpression(node) ||
564
+ node.argumentExpression === undefined ||
565
+ isStringLiteralLikeNode(node.argumentExpression)
566
+ ) {
567
+ return;
568
+ }
569
+ const receiverType = project.checker.getTypeAtLocation(node.expression);
570
+ const receiverProperties = project.checker.getPropertiesOfType(receiverType);
571
+ if (receiverProperties.length === 0) {
572
+ entries.forEach((entry) => recordSymbolUncertainty(state, entry, "dynamic-member-access"));
573
+ state.projectsWithUnknownDynamicAccess.add(project);
574
+ return;
575
+ }
576
+ const matched = new Set(
577
+ receiverProperties
578
+ .filter((property) => state.candidateNames.has(property.name))
579
+ .flatMap((property) => [
580
+ ...matchingSymbolEntries(project, property, "value", state.targetsByKey),
581
+ ]),
582
+ );
583
+ matched.forEach((entry) => recordSymbolUncertainty(state, entry, "dynamic-member-access"));
584
+ };
585
+
586
+ const recordDynamicImportUncertainty = (state, project, node) => {
587
+ if (!isDynamicImportCall(node)) return;
588
+ const specifier = node.arguments[0];
589
+ if (!specifier || !isStringLiteralLikeNode(specifier)) {
590
+ (state.exportEntriesByProject.get(project) ?? []).forEach((entry) =>
591
+ recordSymbolUncertainty(state, entry, "dynamic-behavior"),
592
+ );
593
+ return;
594
+ }
595
+ const moduleIdentity = moduleIdentityForSpecifier(project, specifier);
596
+ if (!moduleIdentity) return;
597
+ (state.exportEntriesByModule.get(moduleIdentity) ?? []).forEach((entry) =>
598
+ recordSymbolUncertainty(state, entry, "dynamic-behavior"),
599
+ );
600
+ };
601
+
602
+ const referenceNamespaces = (project, node, symbol) => {
603
+ const alias = aliasNode(node);
604
+ if (!alias || !isExportSpecifier(alias)) {
605
+ return [isTypePosition(node) ? "type" : "value"];
606
+ }
607
+ if (alias.isTypeOnly || alias.parent?.parent?.isTypeOnly) return ["type"];
608
+ const namespaces = new Set(
609
+ declarationsForSymbol(project, symbol).flatMap((declaration) => [
610
+ ...declarationNamespaces(declaration),
611
+ ]),
612
+ );
613
+ return namespaces.size > 0 ? [...namespaces] : [isTypePosition(node) ? "type" : "value"];
614
+ };
615
+
616
+ const scanSymbolUseFile = (root, state, project, sourceFile, includeDefaultImports) => {
617
+ const projectEntries = classMemberEntrySetForProject(state, project);
618
+ const nodes = [];
619
+ visit(sourceFile, (node) => {
620
+ recordDynamicImportUncertainty(state, project, node);
621
+ if (projectEntries.size > 0) {
622
+ recordStringDispatchedMemberAccess(state, projectEntries, node);
623
+ recordComputedMemberAccess(state, project, projectEntries, node);
624
+ }
625
+ if (isIdentifierReference(node, state.candidateNames, includeDefaultImports)) {
626
+ nodes.push(node);
627
+ return;
628
+ }
629
+ if (isElementReference(node, state.candidateNames)) nodes.push(node);
630
+ });
631
+ const symbols = project.checker.getSymbolAtLocation(nodes);
632
+ nodes.forEach((node, index) => {
633
+ const symbol = resolveAlias(project.checker, symbols[index]);
634
+ referenceNamespaces(project, node, symbol).forEach((namespace) => {
635
+ const entries = matchingSymbolEntries(project, symbol, namespace, state.targetsByKey);
636
+ entries.forEach((entry) => recordSymbolUse(root, state, project, entry, node, namespace));
637
+ });
638
+ });
639
+ };
640
+
641
+ const scanSymbolUseProjects = (root, state, projects, includeDefaultImports) => {
642
+ const scannedFiles = new Set();
643
+ let sourceScanCount = 0;
644
+ for (const project of projects) {
645
+ for (const sourceFile of projectSourceFiles(project)) {
646
+ const fileIdentity = sourceFileIdentity(sourceFile);
647
+ if (scannedFiles.has(fileIdentity)) continue;
648
+ scannedFiles.add(fileIdentity);
649
+ sourceScanCount += 1;
650
+ scanSymbolUseFile(root, state, project, sourceFile, includeDefaultImports);
651
+ }
652
+ }
653
+ return sourceScanCount;
654
+ };
655
+
656
+ const batchSymbolUseEvidence = (root, resolvedQueries, scanProjects, evidenceLimit) => {
657
+ const state = symbolUseBatchState(resolvedQueries, evidenceLimit);
658
+ resolvedQueries.forEach((entry) => registerSymbolTargets(root, state, entry));
659
+ const includeDefaultImports = resolvedQueries.some(
660
+ ({ query }) => query.symbol.exportedName === "default",
661
+ );
662
+ const sourceScanCount = scanSymbolUseProjects(root, state, scanProjects, includeDefaultImports);
663
+ return {
664
+ evidenceByQuery: state.evidenceByQuery,
665
+ totalByQuery: state.totalByQuery,
666
+ directFilesByQuery: state.directFilesByQuery,
667
+ aliasHopTotalByQuery: state.aliasHopTotalByQuery,
668
+ contractRelationsByQuery: state.contractRelationsByQuery,
669
+ uncertaintiesByQuery: state.uncertaintiesByQuery,
670
+ sourceScanCount,
671
+ };
672
+ };
673
+
674
+ const selectProjectForSymbol = (snapshot, explicitProjects, absolutePath, allowDefaultFallback) =>
675
+ explicitProjects.find((project) => project.program.getSourceFile(absolutePath)) ??
676
+ (allowDefaultFallback ? snapshot.getDefaultProjectForFile(absolutePath) : undefined);
677
+
678
+ const symbolResolutionError = (query, reasonCode, action) => ({
679
+ error: unavailable(query, reasonCode, action),
680
+ });
681
+
682
+ const isCompleteProjectState = (state) => state?.status === "complete";
683
+ const selectedProjectName = (state) => state?.config ?? "the selected project";
684
+
685
+ const owningProjectContexts = (statesByProject, absolutePath) =>
686
+ [...statesByProject.entries()]
687
+ .filter(([project]) => project.program.getSourceFile(absolutePath))
688
+ .map(([project, state]) => ({ project, state }));
689
+
690
+ const selectSymbolContext = (
691
+ snapshot,
692
+ explicitProjects,
693
+ statesByProject,
694
+ query,
695
+ allowDefaultFallback,
696
+ ) => {
697
+ if (!DECLARATION_KINDS.has(query.symbol.declarationKind)) {
698
+ return symbolResolutionError(
699
+ query,
700
+ "unsupported-syntax",
701
+ "Use a supported declaration kind or retain the syntactic finding.",
702
+ );
703
+ }
704
+ let owners = owningProjectContexts(statesByProject, query.symbol.absolutePath);
705
+ if (owners.length === 0) {
706
+ const fallback = selectProjectForSymbol(
707
+ snapshot,
708
+ explicitProjects,
709
+ query.symbol.absolutePath,
710
+ allowDefaultFallback,
711
+ );
712
+ const fallbackState = statesByProject.get(fallback);
713
+ if (fallback && fallbackState) owners = [{ project: fallback, state: fallbackState }];
714
+ }
715
+ if (owners.length === 0) {
716
+ return symbolResolutionError(
717
+ query,
718
+ "no-project",
719
+ "Pass a tsconfig containing the declaration with --type-aware-project.",
720
+ );
721
+ }
722
+ const completeOwners = owners.filter(({ state }) => isCompleteProjectState(state));
723
+ if (completeOwners.length === 0) {
724
+ return {
725
+ ...owners[0],
726
+ ...symbolResolutionError(
727
+ query,
728
+ "blocking-diagnostics",
729
+ `Repair structural diagnostics in ${selectedProjectName(owners[0].state)} and retry.`,
730
+ ),
731
+ };
732
+ }
733
+ return { owners, completeOwners };
734
+ };
735
+
736
+ const resolvedAnchorTarget = (project, anchor, requestedSymbol) => {
737
+ if (!anchor) return undefined;
738
+ if (!isExportSpecifier(anchor)) {
739
+ return declarationNamespaces(anchor).has(requestedSymbol.namespace)
740
+ ? { declaration: anchor, namespace: requestedSymbol.namespace }
741
+ : undefined;
742
+ }
743
+ const checkerSymbol = project.checker.getSymbolAtLocation(anchor.name);
744
+ const declarations = declarationsForSymbol(project, resolveAlias(project.checker, checkerSymbol));
745
+ const exact = declarations.find((node) =>
746
+ declarationNamespaces(node).has(requestedSymbol.namespace),
747
+ );
748
+ if (exact) return { declaration: exact, namespace: requestedSymbol.namespace };
749
+ if (requestedSymbol.declarationKind !== "export") return undefined;
750
+ const fallback = declarations
751
+ .flatMap((declaration) =>
752
+ [...declarationNamespaces(declaration)].map((namespace) => ({ declaration, namespace })),
753
+ )
754
+ .toSorted((left, right) =>
755
+ compareText(
756
+ stableDeclarationKey(left.declaration, left.namespace),
757
+ stableDeclarationKey(right.declaration, right.namespace),
758
+ ),
759
+ )[0];
760
+ return fallback;
761
+ };
762
+
763
+ const requiresExportIndex = (symbol) =>
764
+ symbol.declarationKind === "export" || symbol.exportedName !== symbol.localName;
765
+
766
+ const exportAliasMatches = (project, symbol, target) => {
767
+ if (!target) return false;
768
+ if (!requiresExportIndex(symbol)) return true;
769
+ const candidateKey = stableDeclarationKey(target.declaration, target.namespace);
770
+ return projectExportIndex(project).has(`${symbol.exportedName}\0${candidateKey}`);
771
+ };
772
+
773
+ const symbolQueryAnchor = (project, query) => {
774
+ const sourceFile = project.program.getSourceFile(query.symbol.absolutePath);
775
+ return sourceFile ? findDeclaration(sourceFile, query.symbol) : undefined;
776
+ };
777
+
778
+ const symbolQueryResolved = (project, symbol, anchor, target) =>
779
+ Boolean(anchor) && exportAliasMatches(project, symbol, target);
780
+
781
+ const modifierNamed = (node, name) =>
782
+ Boolean(node?.modifiers?.some((modifier) => modifier.getText(node.getSourceFile()) === name));
783
+
784
+ const hasDecorator = (node) =>
785
+ Boolean(node?.decorators?.length) ||
786
+ Boolean(node?.modifiers?.some((modifier) => isDecorator(modifier)));
787
+
788
+ const accessorPairExists = (declaration) => {
789
+ if (!isGetAccessorDeclaration(declaration) && !isSetAccessorDeclaration(declaration)) {
790
+ return false;
791
+ }
792
+ const owner = ownerDeclaration(declaration);
793
+ const name = declarationName(declaration);
794
+ return Boolean(
795
+ owner?.members?.some(
796
+ (member) =>
797
+ member !== declaration &&
798
+ declarationName(member) === name &&
799
+ (isGetAccessorDeclaration(member) || isSetAccessorDeclaration(member)),
800
+ ),
801
+ );
802
+ };
803
+
804
+ const sameNamedMemberCount = (declaration) => {
805
+ const owner = ownerDeclaration(declaration);
806
+ const name = declarationName(declaration);
807
+ return owner?.members?.filter((member) => declarationName(member) === name).length ?? 0;
808
+ };
809
+
810
+ const hasAttachedComment = (declaration) => {
811
+ const sourceFile = declaration.getSourceFile();
812
+ const trivia = sourceFile.text.slice(
813
+ declaration.getFullStart(),
814
+ declaration.getStart(sourceFile),
815
+ );
816
+ return ATTACHED_COMMENT_PATTERN.test(trivia);
817
+ };
818
+
819
+ const optionalContractDeclaration = (symbol, declaration) =>
820
+ (symbol.flags & SymbolFlags.Optional) !== 0 || declaration?.questionToken !== undefined;
821
+
822
+ const heritageRelation = (clause, declaration) => {
823
+ const text = clause.getText(clause.getSourceFile());
824
+ if (text.startsWith("implements")) return "interface-implementation";
825
+ if (modifierNamed(declaration, "abstract")) return "abstract-implementation";
826
+ return "override";
827
+ };
828
+
829
+ const contractRelationsFor = (root, project, declaration, memberName) => {
830
+ const owner = ownerDeclaration(declaration);
831
+ if (!owner || (!isClassDeclaration(owner) && !isClassExpression(owner))) return [];
832
+ const relations = [];
833
+ for (const clause of owner.heritageClauses ?? []) {
834
+ for (const heritageType of clause.types ?? []) {
835
+ const type = project.checker.getTypeAtLocation(heritageType);
836
+ const symbol = project.checker
837
+ .getPropertiesOfType(type)
838
+ .find((property) => property.name === memberName);
839
+ if (!symbol) continue;
840
+ for (const contractDeclaration of declarationsForSymbol(project, symbol)) {
841
+ const optional = optionalContractDeclaration(symbol, contractDeclaration);
842
+ relations.push({
843
+ relation: optional ? "optional-contract" : heritageRelation(clause, contractDeclaration),
844
+ declaration: stableSymbolIdentity(root, contractDeclaration, "value", memberName),
845
+ optional,
846
+ });
847
+ }
848
+ }
849
+ }
850
+ return uniqueSorted(relations);
851
+ };
852
+
853
+ const packageNameForDeclaration = (declaration) => {
854
+ const normalized = declaration.getSourceFile().fileName.replaceAll("\\", "/");
855
+ const marker = "/node_modules/";
856
+ const index = normalized.lastIndexOf(marker);
857
+ if (index < 0) return undefined;
858
+ const segments = normalized.slice(index + marker.length).split("/");
859
+ if (segments[0]?.startsWith("@")) {
860
+ return segments.length >= 2 ? `${segments[0]}/${segments[1]}` : undefined;
861
+ }
862
+ return segments[0] || undefined;
863
+ };
864
+
865
+ const declarationIsProjectLocal = (root, declaration) => {
866
+ const relative = path.relative(root, declaration.getSourceFile().fileName);
867
+ return relative === "" || (!path.isAbsolute(relative) && !relative.startsWith(`..${path.sep}`));
868
+ };
869
+
870
+ const frameworkContractRelationsFor = (root, project, declaration, contracts, memberName) => {
871
+ const owner = ownerDeclaration(declaration);
872
+ if (!owner || (!isClassDeclaration(owner) && !isClassExpression(owner))) {
873
+ return { relations: [], provenanceUnknown: false };
874
+ }
875
+ const matchingContracts = contracts.filter((contract) => contract.members.includes(memberName));
876
+ const relations = [];
877
+ let provenanceUnknown = false;
878
+ for (const clause of owner.heritageClauses ?? []) {
879
+ const clauseRelation = clause.getText(clause.getSourceFile()).startsWith("implements")
880
+ ? "implements"
881
+ : "extends";
882
+ for (const heritageType of clause.types ?? []) {
883
+ const rawSymbol =
884
+ project.checker.getSymbolAtLocation(heritageType.expression) ??
885
+ project.checker.getTypeAtLocation(heritageType).aliasSymbol ??
886
+ project.checker.getTypeAtLocation(heritageType).symbol;
887
+ const symbol = resolveAlias(project.checker, rawSymbol);
888
+ for (const heritageDeclaration of declarationsForSymbol(project, symbol)) {
889
+ const declarationPackage = packageNameForDeclaration(heritageDeclaration);
890
+ for (const contract of matchingContracts) {
891
+ if (
892
+ contract.relation !== clauseRelation ||
893
+ contract.heritageSymbol !== declarationName(heritageDeclaration)
894
+ ) {
895
+ continue;
896
+ }
897
+ if (!declarationPackage) {
898
+ provenanceUnknown ||= !declarationIsProjectLocal(root, heritageDeclaration);
899
+ continue;
900
+ }
901
+ if (contract.package !== declarationPackage) continue;
902
+ relations.push({
903
+ framework: contract.framework,
904
+ package: contract.package,
905
+ relation: contract.relation,
906
+ declaration: stableSymbolIdentity(
907
+ root,
908
+ heritageDeclaration,
909
+ "value",
910
+ contract.heritageSymbol,
911
+ ),
912
+ });
913
+ }
914
+ }
915
+ }
916
+ }
917
+ return { relations: uniqueSorted(relations), provenanceUnknown };
918
+ };
919
+
920
+ const declarationEditGuard = (declaration) => {
921
+ const sourceFile = declaration.getSourceFile();
922
+ const start = declaration.getStart(sourceFile);
923
+ const end = declaration.end;
924
+ const text = sourceFile.text.slice(start, end);
925
+ return {
926
+ start: Buffer.byteLength(sourceFile.text.slice(0, start), "utf8"),
927
+ end: Buffer.byteLength(sourceFile.text.slice(0, end), "utf8"),
928
+ declaration_sha256: createHash("sha256").update(text).digest("hex"),
929
+ };
930
+ };
931
+
932
+ const resolvedOwnerContext = (query, owner) => {
933
+ const anchor = symbolQueryAnchor(owner.project, query);
934
+ const target = resolvedAnchorTarget(owner.project, anchor, query.symbol);
935
+ return symbolQueryResolved(owner.project, query.symbol, anchor, target)
936
+ ? { ...owner, ...target }
937
+ : undefined;
938
+ };
939
+
940
+ const resolvedIdentityKey = (context) =>
941
+ stableDeclarationKey(context.declaration, context.namespace);
942
+
943
+ const resolutionOmissions = (owners, completeOwners, resolvedOwners, identityCount) => [
944
+ {
945
+ reason_code: "incomplete-project-coverage",
946
+ count: owners.length - completeOwners.length,
947
+ },
948
+ {
949
+ reason_code: "unknown-symbol",
950
+ count: completeOwners.length - resolvedOwners.length,
951
+ },
952
+ {
953
+ reason_code: "ambiguous-project",
954
+ count: Math.max(0, identityCount - 1),
955
+ },
956
+ ];
957
+
958
+ const resolveSymbolQuery = (
959
+ root,
960
+ snapshot,
961
+ explicitProjects,
962
+ statesByProject,
963
+ query,
964
+ allowDefaultFallback,
965
+ ) => {
966
+ const context = selectSymbolContext(
967
+ snapshot,
968
+ explicitProjects,
969
+ statesByProject,
970
+ query,
971
+ allowDefaultFallback,
972
+ );
973
+ if (context.error) return context;
974
+ const ownerContexts = context.completeOwners
975
+ .map((owner) => resolvedOwnerContext(query, owner))
976
+ .filter(Boolean);
977
+ if (ownerContexts.length === 0) {
978
+ return {
979
+ ...context.completeOwners[0],
980
+ ...symbolResolutionError(
981
+ query,
982
+ "unknown-symbol",
983
+ "Refresh the syntactic result and retry with its exact declaration identity.",
984
+ ),
985
+ };
986
+ }
987
+ const identityCount = new Set(ownerContexts.map(resolvedIdentityKey)).size;
988
+ const primary = ownerContexts[0];
989
+ const contractRelations = uniqueSorted(
990
+ ownerContexts.flatMap(({ project, declaration }) =>
991
+ contractRelationsFor(root, project, declaration, query.symbol.localName),
992
+ ),
993
+ );
994
+ const frameworkContractEvidence = ownerContexts.map(({ project, declaration }) =>
995
+ frameworkContractRelationsFor(
996
+ root,
997
+ project,
998
+ declaration,
999
+ query.frameworkContracts ?? [],
1000
+ query.symbol.localName,
1001
+ ),
1002
+ );
1003
+ const frameworkContractRelations = uniqueSorted(
1004
+ frameworkContractEvidence.flatMap(({ relations }) => relations),
1005
+ );
1006
+ const declarationOwnerNode = ownerDeclaration(primary.declaration);
1007
+ const declarationUncertainties = new Set();
1008
+ if (frameworkContractEvidence.some(({ provenanceUnknown }) => provenanceUnknown)) {
1009
+ declarationUncertainties.add("framework-contract-provenance");
1010
+ }
1011
+ if (hasDecorator(primary.declaration) || hasDecorator(declarationOwnerNode)) {
1012
+ declarationUncertainties.add("decorated-declaration");
1013
+ }
1014
+ if (primary.declaration.name && !isIdentifier(primary.declaration.name)) {
1015
+ declarationUncertainties.add("dynamic-member-access");
1016
+ }
1017
+ if (contractRelations.some((relation) => relation.optional)) {
1018
+ declarationUncertainties.add("optional-contract");
1019
+ }
1020
+ if (accessorPairExists(primary.declaration)) {
1021
+ declarationUncertainties.add("accessor-pair");
1022
+ }
1023
+ if (sameNamedMemberCount(primary.declaration) > 1 && !accessorPairExists(primary.declaration)) {
1024
+ declarationUncertainties.add("overload-set");
1025
+ }
1026
+ if (hasAttachedComment(primary.declaration)) {
1027
+ declarationUncertainties.add("attached-comment");
1028
+ }
1029
+ if (modifierNamed(primary.declaration, "abstract")) {
1030
+ declarationUncertainties.add("abstract-declaration");
1031
+ }
1032
+ return {
1033
+ ...primary,
1034
+ ownerContexts,
1035
+ owningProjects: context.owners.map(({ state }) => state.config).toSorted(compareText),
1036
+ contractRelations,
1037
+ frameworkContractRelations,
1038
+ declarationUncertainties,
1039
+ editGuard: declarationEditGuard(primary.declaration),
1040
+ omissions: resolutionOmissions(
1041
+ context.owners,
1042
+ context.completeOwners,
1043
+ ownerContexts,
1044
+ identityCount,
1045
+ ),
1046
+ };
1047
+ };
1048
+
1049
+ const analyzeSymbolUse = (
1050
+ root,
1051
+ query,
1052
+ resolved,
1053
+ evidenceLimit,
1054
+ evidence,
1055
+ totalEvidenceCount,
1056
+ batchUncertainties,
1057
+ ) => {
1058
+ const uncertainties = new Set([...resolved.declarationUncertainties, ...batchUncertainties]);
1059
+ const requiredContracts = resolved.contractRelations.filter((relation) => !relation.optional);
1060
+ const frameworkContracts = resolved.frameworkContractRelations;
1061
+ const omissions = [
1062
+ ...resolved.omissions,
1063
+ ...[...uncertainties].map((reason_code) => ({ reason_code, count: 1 })),
1064
+ ];
1065
+ const closedWorldEligible =
1066
+ totalEvidenceCount === 0 &&
1067
+ requiredContracts.length === 0 &&
1068
+ frameworkContracts.length === 0 &&
1069
+ omissions.every((omission) => omission.count === 0);
1070
+ const assertion =
1071
+ totalEvidenceCount > 0
1072
+ ? "confirmed-used"
1073
+ : requiredContracts.length > 0 || frameworkContracts.length > 0
1074
+ ? "contract-preserved"
1075
+ : closedWorldEligible
1076
+ ? "confirmed-no-static-references"
1077
+ : "no-confirmed-use";
1078
+ return boundedResult({
1079
+ query,
1080
+ assertion,
1081
+ evidence,
1082
+ evidenceLimit,
1083
+ totalEvidenceCount,
1084
+ omissions,
1085
+ data: {
1086
+ symbol: semanticQueryIdentity(root, query, resolved),
1087
+ selected_project: resolved.state.config,
1088
+ owning_projects: resolved.owningProjects,
1089
+ total_reference_count: totalEvidenceCount,
1090
+ contract_relations: resolved.contractRelations,
1091
+ framework_contract_relations: frameworkContracts,
1092
+ closed_world_eligible: closedWorldEligible,
1093
+ edit_guard: resolved.editGuard,
1094
+ },
1095
+ });
1096
+ };
1097
+
1098
+ const analyzeSymbolTrace = (
1099
+ root,
1100
+ query,
1101
+ resolved,
1102
+ evidenceLimit,
1103
+ references,
1104
+ totalReferenceCount,
1105
+ totalAliasHopCount,
1106
+ ) => {
1107
+ const declaration = {
1108
+ ...location(root, resolved.declaration),
1109
+ role: "declaration",
1110
+ source: "checker",
1111
+ };
1112
+ const aliasHops = references
1113
+ .flatMap((entry) => entry.via)
1114
+ .toSorted((left, right) => compareText(JSON.stringify(left), JSON.stringify(right)));
1115
+ return boundedResult({
1116
+ query,
1117
+ assertion: totalReferenceCount > 0 ? "references-found" : "no-references-found",
1118
+ evidence: [declaration, ...references],
1119
+ evidenceLimit,
1120
+ totalEvidenceCount: totalReferenceCount + 1,
1121
+ data: {
1122
+ symbol: semanticQueryIdentity(root, query, resolved),
1123
+ selected_project: resolved.state.config,
1124
+ alias_hops: aliasHops.slice(0, evidenceLimit),
1125
+ total_alias_hop_count: totalAliasHopCount,
1126
+ checker_evidence_count: totalReferenceCount,
1127
+ graph_evidence_count: totalAliasHopCount,
1128
+ },
1129
+ });
1130
+ };
1131
+
1132
+ const packageTargets = (value, targets) => {
1133
+ if (typeof value === "string") {
1134
+ targets.add(value);
1135
+ return;
1136
+ }
1137
+ packageTargetChildren(value).forEach((entry) => packageTargets(entry, targets));
1138
+ };
1139
+
1140
+ const packageTargetChildren = (value) => {
1141
+ if (Array.isArray(value)) return value;
1142
+ if (value === null || typeof value !== "object") return [];
1143
+ return Object.values(value);
1144
+ };
1145
+
1146
+ const sourceCandidatesForTarget = (root, target) => {
1147
+ const normalized = target.replace(/^\.\//u, "");
1148
+ const extensionless = normalized.replace(/(?:\.d)?\.[cm]?[jt]sx?$/u, "");
1149
+ const withoutDist = extensionless.replace(/^dist\//u, "src/");
1150
+ return [normalized, extensionless, withoutDist]
1151
+ .flatMap((entry) => [entry, `${entry}.ts`, `${entry}.tsx`, `${entry}/index.ts`])
1152
+ .map((entry) => path.resolve(root, entry));
1153
+ };
1154
+
1155
+ const wildcardPattern = (target) => {
1156
+ const normalized = target.replace(/^\.\//u, "").replace(/^dist\//u, "src/");
1157
+ const sourceTarget = normalized.replace(/(?:\.d)?\.[cm]?[jt]sx?$/u, ".ts");
1158
+ const escaped = sourceTarget
1159
+ .split("*")
1160
+ .map((part) => part.replace(/[\\^$.*+?()[\]{}|]/gu, "\\$&"))
1161
+ .join("[^/]+");
1162
+ return new RegExp(`^${escaped}$`, "u");
1163
+ };
1164
+
1165
+ const pathWithin = (root, candidate) =>
1166
+ candidate === root || candidate.startsWith(`${root}${path.sep}`);
1167
+
1168
+ const packageAt = (directory) => {
1169
+ const packageFile = path.join(directory, "package.json");
1170
+ if (!existsSync(packageFile)) return undefined;
1171
+ try {
1172
+ return { root: directory, json: JSON.parse(readFileSync(packageFile, "utf8")) };
1173
+ } catch {
1174
+ return { root: directory, json: {} };
1175
+ }
1176
+ };
1177
+
1178
+ const projectPackage = (root, project) => {
1179
+ const normalizedRoot = path.resolve(root);
1180
+ let current = path.dirname(project.configFileName);
1181
+ while (pathWithin(normalizedRoot, current)) {
1182
+ const found = packageAt(current);
1183
+ if (found) return found;
1184
+ if (current === normalizedRoot) break;
1185
+ current = path.dirname(current);
1186
+ }
1187
+ return { root: normalizedRoot, json: {} };
1188
+ };
1189
+
1190
+ const ancestorRoots = (root, start) => {
1191
+ const normalizedRoot = path.resolve(root);
1192
+ const roots = [];
1193
+ let current = path.resolve(start);
1194
+ while (current === normalizedRoot || current.startsWith(`${normalizedRoot}${path.sep}`)) {
1195
+ roots.push(current);
1196
+ if (current === normalizedRoot) break;
1197
+ current = path.dirname(current);
1198
+ }
1199
+ return roots;
1200
+ };
1201
+
1202
+ const sourceFileIndex = (project) =>
1203
+ new Map(
1204
+ projectSourceFiles(project).map((sourceFile) => [sourceFileIdentity(sourceFile), sourceFile]),
1205
+ );
1206
+
1207
+ const requestedEntryPoints = (requested, sourceFiles) =>
1208
+ requested
1209
+ .map((entry) => sourceFiles.get(canonicalFileIdentity(entry.absolutePath)))
1210
+ .filter(Boolean);
1211
+
1212
+ const packageEntryTargets = (packageJson) => {
1213
+ const targets = new Set();
1214
+ ["exports", "types", "typings", "module", "main"].forEach((field) =>
1215
+ packageTargets(packageJson[field], targets),
1216
+ );
1217
+ if (targets.size === 0) targets.add("src/index.ts");
1218
+ return targets;
1219
+ };
1220
+
1221
+ const addEntry = (entries, sourceFile) => {
1222
+ if (sourceFile && !entries.includes(sourceFile)) entries.push(sourceFile);
1223
+ };
1224
+
1225
+ const addWildcardEntries = (entries, sourceFiles, packageRoot, target) => {
1226
+ const pattern = wildcardPattern(target);
1227
+ for (const sourceFile of sourceFiles.values()) {
1228
+ if (pattern.test(relativePath(packageRoot, sourceFile.fileName))) {
1229
+ addEntry(entries, sourceFile);
1230
+ }
1231
+ }
1232
+ };
1233
+
1234
+ const addDirectEntry = (entries, sourceFiles, packageRoot, target) => {
1235
+ const sourceFile = sourceCandidatesForTarget(packageRoot, target)
1236
+ .map((candidate) => sourceFiles.get(canonicalFileIdentity(candidate)))
1237
+ .find(Boolean);
1238
+ addEntry(entries, sourceFile);
1239
+ };
1240
+
1241
+ const addPackageEntry = (entries, sourceFiles, packageRoot, target) => {
1242
+ if (target.includes("*")) {
1243
+ addWildcardEntries(entries, sourceFiles, packageRoot, target);
1244
+ return;
1245
+ }
1246
+ addDirectEntry(entries, sourceFiles, packageRoot, target);
1247
+ };
1248
+
1249
+ const addPathEntry = (entries, sourceFiles, roots, target) => {
1250
+ roots.forEach((candidateRoot) =>
1251
+ sourceCandidatesForTarget(candidateRoot, target).forEach((candidate) =>
1252
+ addEntry(entries, sourceFiles.get(canonicalFileIdentity(candidate))),
1253
+ ),
1254
+ );
1255
+ };
1256
+
1257
+ const packagePathTargets = (project, packageJson) => {
1258
+ const packageName = packageJson.name;
1259
+ if (typeof packageName !== "string") return [];
1260
+ return project.compilerOptions.paths?.[packageName] ?? [];
1261
+ };
1262
+
1263
+ const discoverEntryPoints = (root, project, requested) => {
1264
+ const sourceFiles = sourceFileIndex(project);
1265
+ if (requested.length > 0) return requestedEntryPoints(requested, sourceFiles);
1266
+ const packageInfo = projectPackage(root, project);
1267
+ const entries = [];
1268
+ packageEntryTargets(packageInfo.json).forEach((target) =>
1269
+ addPackageEntry(entries, sourceFiles, packageInfo.root, target),
1270
+ );
1271
+ const roots = ancestorRoots(root, packageInfo.root);
1272
+ packagePathTargets(project, packageInfo.json).forEach((target) =>
1273
+ addPathEntry(entries, sourceFiles, roots, target),
1274
+ );
1275
+ return entries;
1276
+ };
1277
+
1278
+ const isPrivateMember = (node) =>
1279
+ Boolean(node.name && isPrivateIdentifier(node.name)) ||
1280
+ Boolean(node.modifiers?.some((modifier) => modifier.getText(node.getSourceFile()) === "private"));
1281
+
1282
+ const skipTypeReferenceNode = (declaration, node) => {
1283
+ if (isFunctionBody(node)) return true;
1284
+ return node !== declaration && isDeclaration(node) && isPrivateMember(node);
1285
+ };
1286
+
1287
+ const scanTypeReferenceNode = (declaration, nodes, node) => {
1288
+ if (skipTypeReferenceNode(declaration, node)) return;
1289
+ if (isIdentifier(node) && isTypePosition(node)) nodes.push(node);
1290
+ node.forEachChild((child) => {
1291
+ scanTypeReferenceNode(declaration, nodes, child);
1292
+ return undefined;
1293
+ });
1294
+ };
1295
+
1296
+ const typeReferenceNodes = (declaration) => {
1297
+ const nodes = [];
1298
+ scanTypeReferenceNode(declaration, nodes, declaration);
1299
+ return nodes;
1300
+ };
1301
+
1302
+ const publicExportsFrom = (project, sourceFile) => {
1303
+ const moduleSymbol = project.checker.getSymbolAtLocation(sourceFile);
1304
+ if (!moduleSymbol) return [];
1305
+ return project.checker.getExportsOfModule(moduleSymbol).flatMap((exportedSymbol) => {
1306
+ const target = resolveAlias(project.checker, exportedSymbol);
1307
+ return declarationsForSymbol(project, target)
1308
+ .filter((declaration) => isProjectSource(project, declaration.getSourceFile()))
1309
+ .map((declaration) => {
1310
+ const namespace = declarationNamespaces(declaration).has("type") ? "type" : "value";
1311
+ return {
1312
+ exportedName: exportedSymbol.name,
1313
+ declaration,
1314
+ namespace,
1315
+ key: stableDeclarationKey(declaration, namespace),
1316
+ };
1317
+ });
1318
+ });
1319
+ };
1320
+
1321
+ const collectPublicExports = (project, entryPoints) => {
1322
+ const exports = [];
1323
+ const publicKeys = new Set();
1324
+ entryPoints
1325
+ .flatMap((sourceFile) => publicExportsFrom(project, sourceFile))
1326
+ .forEach((entry) => {
1327
+ publicKeys.add(entry.key);
1328
+ exports.push(entry);
1329
+ });
1330
+ return { exports, publicKeys };
1331
+ };
1332
+
1333
+ const publicTypeDeclaration = (project, declaration) =>
1334
+ isProjectSource(project, declaration.getSourceFile()) &&
1335
+ isDeclaration(declaration) &&
1336
+ declarationNamespaces(declaration).has("type");
1337
+
1338
+ const safeCheckerValue = (operation, fallback) => {
1339
+ try {
1340
+ return operation() ?? fallback;
1341
+ } catch {
1342
+ return fallback;
1343
+ }
1344
+ };
1345
+
1346
+ const checkerNamedTypeDeclarations = (project, type) => {
1347
+ const symbols = [type.getAliasSymbol(), type.getSymbol()].filter(Boolean);
1348
+ const declarations = symbols.flatMap((symbol) =>
1349
+ declarationsForSymbol(project, resolveAlias(project.checker, symbol)),
1350
+ );
1351
+ return uniqueSorted(
1352
+ declarations.filter(
1353
+ (declaration) => isDeclaration(declaration) && declarationNamespaces(declaration).has("type"),
1354
+ ),
1355
+ (declaration) => stableDeclarationKey(declaration, "type"),
1356
+ );
1357
+ };
1358
+
1359
+ const signatureTypes = (project, type, anchor) =>
1360
+ [SignatureKind.Call, SignatureKind.Construct].flatMap((kind) =>
1361
+ safeCheckerValue(() => project.checker.getSignaturesOfType(type, kind), []).flatMap(
1362
+ (signature) => [
1363
+ safeCheckerValue(() => project.checker.getReturnTypeOfSignature(signature), undefined),
1364
+ ...signature.getParameters().map((parameter) => {
1365
+ const declaration =
1366
+ declarationsForSymbol(project, parameter).find((candidate) =>
1367
+ isProjectSource(project, candidate.getSourceFile()),
1368
+ ) ?? anchor;
1369
+ return safeCheckerValue(
1370
+ () => project.checker.getTypeOfSymbolAtLocation(parameter, declaration),
1371
+ undefined,
1372
+ );
1373
+ }),
1374
+ ],
1375
+ ),
1376
+ );
1377
+
1378
+ const checkerTypeChildren = (project, type, anchor, hasNamedDeclaration) => {
1379
+ const structural = [
1380
+ ...(type.getTypes() ?? []),
1381
+ ...safeCheckerValue(() => type.getAliasTypeArguments(), []),
1382
+ ...safeCheckerValue(() => project.checker.getTypeArguments(type), []),
1383
+ ].filter(Boolean);
1384
+ if (hasNamedDeclaration) return structural;
1385
+ const direct = [...structural, ...signatureTypes(project, type, anchor)];
1386
+ if (direct.length > 0) return direct;
1387
+ return safeCheckerValue(() => project.checker.getPropertiesOfType(type), []).flatMap(
1388
+ (property) => {
1389
+ const declaration =
1390
+ declarationsForSymbol(project, property).find((candidate) =>
1391
+ isProjectSource(project, candidate.getSourceFile()),
1392
+ ) ?? anchor;
1393
+ const propertyType = safeCheckerValue(
1394
+ () => project.checker.getTypeOfSymbolAtLocation(property, declaration),
1395
+ undefined,
1396
+ );
1397
+ return propertyType ? [propertyType] : [];
1398
+ },
1399
+ );
1400
+ };
1401
+
1402
+ const publicApiEdge = (root, exported, declaration, evidence) => ({
1403
+ source: stableSymbolIdentity(
1404
+ root,
1405
+ exported.declaration,
1406
+ exported.namespace,
1407
+ exported.exportedName,
1408
+ ),
1409
+ target: stableSymbolIdentity(
1410
+ root,
1411
+ declaration,
1412
+ "type",
1413
+ declarationName(declaration) ?? "default",
1414
+ ),
1415
+ relation: "public API depends on",
1416
+ evidence,
1417
+ });
1418
+
1419
+ const recordPublicApiEdge = (root, state, exported, declaration, evidence) => {
1420
+ const targetKey = stableDeclarationKey(declaration, "type");
1421
+ if (targetKey === exported.key) return;
1422
+ const edgeKey = `${exported.key}\0${targetKey}`;
1423
+ if (state.seenEdges.has(edgeKey)) return;
1424
+ state.seenEdges.add(edgeKey);
1425
+ const edge = publicApiEdge(root, exported, declaration, evidence);
1426
+ state.edges.push(edge);
1427
+ if (!state.publicKeys.has(targetKey)) {
1428
+ state.leaks.push({
1429
+ exposed_symbol: edge.source,
1430
+ private_declaration: edge.target,
1431
+ relation: "public-signature-private-type",
1432
+ evidence,
1433
+ });
1434
+ }
1435
+ };
1436
+
1437
+ const scanCheckerType = (root, project, state, exported) => {
1438
+ const symbol = symbolForDeclaration(project, exported.declaration);
1439
+ if (!symbol) return;
1440
+ const initial = safeCheckerValue(
1441
+ () =>
1442
+ project.checker.getTypeOfSymbolAtLocation(
1443
+ resolveAlias(project.checker, symbol),
1444
+ exported.declaration,
1445
+ ),
1446
+ undefined,
1447
+ );
1448
+ if (!initial) return;
1449
+ const pending = [initial];
1450
+ const seen = new Set();
1451
+ const evidence = location(root, exported.declaration);
1452
+ while (pending.length > 0) {
1453
+ const type = pending.pop();
1454
+ if (!type || seen.has(type.id)) continue;
1455
+ seen.add(type.id);
1456
+ const namedDeclarations = checkerNamedTypeDeclarations(project, type);
1457
+ const declarations = namedDeclarations.filter((declaration) =>
1458
+ isProjectSource(project, declaration.getSourceFile()),
1459
+ );
1460
+ declarations.forEach((declaration) =>
1461
+ recordPublicApiEdge(root, state, exported, declaration, evidence),
1462
+ );
1463
+ pending.push(
1464
+ ...checkerTypeChildren(project, type, exported.declaration, namedDeclarations.length > 0),
1465
+ );
1466
+ }
1467
+ };
1468
+
1469
+ const scanPublicExport = (root, project, state, exported) => {
1470
+ const nodes = typeReferenceNodes(exported.declaration);
1471
+ const symbols = project.checker.getSymbolAtLocation(nodes);
1472
+ nodes.forEach((node, index) => {
1473
+ const target = resolveAlias(project.checker, symbols[index]);
1474
+ declarationsForSymbol(project, target)
1475
+ .filter((declaration) => publicTypeDeclaration(project, declaration))
1476
+ .forEach((declaration) =>
1477
+ recordPublicApiEdge(root, state, exported, declaration, location(root, node)),
1478
+ );
1479
+ });
1480
+ scanCheckerType(root, project, state, exported);
1481
+ };
1482
+
1483
+ const collectPublicApiEdges = (root, project, exports, publicKeys) => {
1484
+ const state = { edges: [], leaks: [], seenEdges: new Set(), publicKeys };
1485
+ exports.forEach((exported) => scanPublicExport(root, project, state, exported));
1486
+ return state;
1487
+ };
1488
+
1489
+ const signatureFingerprint = (project, entry) => {
1490
+ let signature;
1491
+ try {
1492
+ const symbol = symbolForDeclaration(project, entry.declaration);
1493
+ const type = symbol
1494
+ ? project.checker.getTypeOfSymbolAtLocation(symbol, entry.declaration)
1495
+ : undefined;
1496
+ signature = type
1497
+ ? project.checker.typeToString(type, entry.declaration)
1498
+ : entry.declaration.getText(entry.declaration.getSourceFile());
1499
+ } catch {
1500
+ signature = entry.declaration.getText(entry.declaration.getSourceFile());
1501
+ }
1502
+ return `sha256:${createHash("sha256").update(signature.replace(/\s+/gu, " ")).digest("hex")}`;
1503
+ };
1504
+
1505
+ const comparableSymbolIdentity = ({ path: filePath, namespace, local_name, line, col }) =>
1506
+ JSON.stringify([filePath, namespace, local_name, line, col]);
1507
+
1508
+ const sameSymbolIdentity = (left, right) =>
1509
+ comparableSymbolIdentity(left) === comparableSymbolIdentity(right);
1510
+
1511
+ const referencedTypes = (edges, exposed) =>
1512
+ edges
1513
+ .filter((edge) => sameSymbolIdentity(edge.source, exposed))
1514
+ .map((edge) => ({ declaration: edge.target, relation: edge.relation }))
1515
+ .toSorted((left, right) => compareText(JSON.stringify(left), JSON.stringify(right)));
1516
+
1517
+ const publicApiEntry = (root, project, edges, entry) => {
1518
+ const exposed = stableSymbolIdentity(
1519
+ root,
1520
+ entry.declaration,
1521
+ entry.namespace,
1522
+ entry.exportedName,
1523
+ );
1524
+ return {
1525
+ exposed,
1526
+ origin: exposed,
1527
+ signature_fingerprint: signatureFingerprint(project, entry),
1528
+ referenced_types: referencedTypes(edges, exposed),
1529
+ };
1530
+ };
1531
+
1532
+ const publicApiGraph = (root, project, entryPoints, includeEntries = false) => {
1533
+ const { exports, publicKeys } = collectPublicExports(project, entryPoints);
1534
+ const { edges, leaks } = collectPublicApiEdges(root, project, exports, publicKeys);
1535
+ const entries = includeEntries
1536
+ ? exports.map((entry) => publicApiEntry(root, project, edges, entry))
1537
+ : [];
1538
+ return { exports, entries, edges, leaks };
1539
+ };
1540
+
1541
+ const graphProjects = (snapshot, explicitProjects) =>
1542
+ (explicitProjects.length > 0 ? explicitProjects : snapshot.getProjects()).filter(
1543
+ (project) => project.program.getSourceFileNames().length > 0,
1544
+ );
1545
+
1546
+ const readyProjectStates = (query, states) => {
1547
+ const ready = states.filter((state) => state.status === "complete");
1548
+ if (ready.length > 0) return { ready };
1549
+ return {
1550
+ error: unavailable(
1551
+ query,
1552
+ states.length === 0 ? "no-project" : "blocking-diagnostics",
1553
+ "Pass a healthy tsconfig containing the package entry points and retry.",
1554
+ ),
1555
+ };
1556
+ };
1557
+
1558
+ const graphCollection = () => ({
1559
+ exports: [],
1560
+ entries: [],
1561
+ leaks: [],
1562
+ edges: [],
1563
+ resolvedEntryPoints: new Set(),
1564
+ });
1565
+
1566
+ const collectApiSurfaceGraph = (root, query, ready) => {
1567
+ const collected = graphCollection();
1568
+ ready.forEach((state) => {
1569
+ const entryPoints = discoverEntryPoints(root, state.project, query.entryPoints);
1570
+ entryPoints.forEach((entryPoint) =>
1571
+ collected.resolvedEntryPoints.add(sourceFileIdentity(entryPoint)),
1572
+ );
1573
+ const graph = publicApiGraph(root, state.project, entryPoints, true);
1574
+ collected.exports.push(
1575
+ ...graph.exports.map((entry) =>
1576
+ stableSymbolIdentity(root, entry.declaration, entry.namespace, entry.exportedName),
1577
+ ),
1578
+ );
1579
+ collected.entries.push(...graph.entries);
1580
+ collected.leaks.push(...graph.leaks);
1581
+ collected.edges.push(...graph.edges);
1582
+ });
1583
+ return collected;
1584
+ };
1585
+
1586
+ const uniqueSorted = (values, projection = (value) => value) =>
1587
+ [...new Map(values.map((value) => [JSON.stringify(projection(value)), value])).values()].toSorted(
1588
+ (left, right) =>
1589
+ compareText(JSON.stringify(projection(left)), JSON.stringify(projection(right))),
1590
+ );
1591
+
1592
+ const missingEntryPoints = (query, resolvedEntryPoints) =>
1593
+ query.entryPoints.filter(
1594
+ (entryPoint) => !resolvedEntryPoints.has(canonicalFileIdentity(entryPoint.absolutePath)),
1595
+ ).length;
1596
+
1597
+ const confirmedPrivateLeakIds = (query, leaks) => {
1598
+ const confirmedLeakKeys = new Set(
1599
+ leaks.map((leak) =>
1600
+ JSON.stringify([
1601
+ leak.evidence.path,
1602
+ leak.exposed_symbol.exported_name,
1603
+ leak.private_declaration.local_name,
1604
+ ]),
1605
+ ),
1606
+ );
1607
+ return query.privateLeakCandidates
1608
+ .filter((candidate) =>
1609
+ confirmedLeakKeys.has(
1610
+ JSON.stringify([candidate.path, candidate.exportName, candidate.typeName]),
1611
+ ),
1612
+ )
1613
+ .map((candidate) => candidate.id)
1614
+ .toSorted((left, right) => left - right);
1615
+ };
1616
+
1617
+ const boundedApiEntries = (entries, evidenceLimit) => {
1618
+ let omissionCount = 0;
1619
+ const bounded = entries.slice(0, evidenceLimit).map((entry) => {
1620
+ omissionCount += Math.max(0, entry.referenced_types.length - evidenceLimit);
1621
+ return {
1622
+ ...entry,
1623
+ referenced_types: entry.referenced_types.slice(0, evidenceLimit),
1624
+ total_referenced_type_count: entry.referenced_types.length,
1625
+ };
1626
+ });
1627
+ return { bounded, omissionCount };
1628
+ };
1629
+
1630
+ const apiEvidenceOmissionCount = (collections, evidenceLimit, nestedCount) =>
1631
+ collections.reduce(
1632
+ (count, entries) => count + Math.max(0, entries.length - evidenceLimit),
1633
+ nestedCount,
1634
+ );
1635
+
1636
+ const apiEvidence = (leaks, edges) => (leaks.length > 0 ? leaks : edges);
1637
+ const apiAssertion = (leaks) => (leaks.length > 0 ? "leak-confirmed" : "no-leak-confirmed");
1638
+ const apiConfirmationComplete = (unavailableCount, missingCount) =>
1639
+ unavailableCount === 0 && missingCount === 0;
1640
+
1641
+ const analyzeApiSurface = (root, query, states, evidenceLimit) => {
1642
+ const readiness = readyProjectStates(query, states);
1643
+ if (readiness.error) return readiness.error;
1644
+ const collected = collectApiSurfaceGraph(root, query, readiness.ready);
1645
+ const orderedExports = uniqueSorted(collected.exports);
1646
+ const orderedEntries = uniqueSorted(collected.entries);
1647
+ const orderedLeaks = uniqueSorted(collected.leaks);
1648
+ const orderedEdges = uniqueSorted(collected.edges);
1649
+ const missingEntryPointCount = missingEntryPoints(query, collected.resolvedEntryPoints);
1650
+ const confirmedCandidateIds = confirmedPrivateLeakIds(query, orderedLeaks);
1651
+ const unavailableProjectCount = states.length - readiness.ready.length;
1652
+ const boundedEntries = boundedApiEntries(orderedEntries, evidenceLimit);
1653
+ const omissionCount = apiEvidenceOmissionCount(
1654
+ [orderedExports, orderedEntries, orderedLeaks, orderedEdges],
1655
+ evidenceLimit,
1656
+ boundedEntries.omissionCount,
1657
+ );
1658
+ return boundedResult({
1659
+ query,
1660
+ assertion: apiAssertion(orderedLeaks),
1661
+ evidence: apiEvidence(orderedLeaks, orderedEdges),
1662
+ evidenceLimit,
1663
+ data: {
1664
+ exports: orderedExports.slice(0, evidenceLimit),
1665
+ total_export_count: orderedExports.length,
1666
+ entries: boundedEntries.bounded,
1667
+ total_entry_count: orderedEntries.length,
1668
+ leaks: orderedLeaks.slice(0, evidenceLimit),
1669
+ private_leak_confirmation: {
1670
+ requested_candidate_count: query.privateLeakCandidates.length,
1671
+ confirmation_complete: apiConfirmationComplete(
1672
+ unavailableProjectCount,
1673
+ missingEntryPointCount,
1674
+ ),
1675
+ confirmed_candidate_ids: confirmedCandidateIds,
1676
+ },
1677
+ total_leak_count: orderedLeaks.length,
1678
+ public_signature_edges: orderedEdges.slice(0, evidenceLimit),
1679
+ total_public_signature_edge_count: orderedEdges.length,
1680
+ },
1681
+ omissions: [
1682
+ {
1683
+ reason_code: "evidence-limit",
1684
+ count: omissionCount,
1685
+ },
1686
+ { reason_code: "blocking-diagnostics", count: unavailableProjectCount },
1687
+ { reason_code: "unknown-entry-point", count: missingEntryPointCount },
1688
+ ],
1689
+ });
1690
+ };
1691
+
1692
+ const emptySemanticAnalysis = (queries) => ({
1693
+ selectedTsconfigs: [],
1694
+ projectResults: [],
1695
+ results: queries.map((query) =>
1696
+ unavailable(
1697
+ query,
1698
+ "no-project",
1699
+ "Pass an explicit tsconfig with --type-aware-project and retry.",
1700
+ ),
1701
+ ),
1702
+ phaseTimings: { project_setup: 0, diagnostics: 0, semantic_queries: 0 },
1703
+ warnings: [],
1704
+ });
1705
+
1706
+ const resolveSemanticQueries = ({
1707
+ root,
1708
+ snapshot,
1709
+ explicitProjects,
1710
+ statesByProject,
1711
+ queries,
1712
+ allowDefaultFallback,
1713
+ }) => {
1714
+ const resolvedByQuery = new Map();
1715
+ for (const query of queries) {
1716
+ if (!query.symbol) continue;
1717
+ resolvedByQuery.set(
1718
+ query.id,
1719
+ resolveSymbolQuery(
1720
+ root,
1721
+ snapshot,
1722
+ explicitProjects,
1723
+ statesByProject,
1724
+ query,
1725
+ allowDefaultFallback,
1726
+ ),
1727
+ );
1728
+ }
1729
+ return resolvedByQuery;
1730
+ };
1731
+
1732
+ const runSymbolUseBatch = (root, queries, resolvedByQuery, semanticProjects, evidenceLimit) => {
1733
+ const resolvedSymbolUses = queries
1734
+ .filter((query) => query.symbol)
1735
+ .flatMap((query) => {
1736
+ const resolved = resolvedByQuery.get(query.id);
1737
+ return resolved?.error ? [] : [{ query, resolved }];
1738
+ });
1739
+ if (resolvedSymbolUses.length === 0) {
1740
+ return {
1741
+ evidenceByQuery: new Map(),
1742
+ totalByQuery: new Map(),
1743
+ directFilesByQuery: new Map(),
1744
+ aliasHopTotalByQuery: new Map(),
1745
+ contractRelationsByQuery: new Map(),
1746
+ uncertaintiesByQuery: new Map(),
1747
+ sourceScanCount: 0,
1748
+ };
1749
+ }
1750
+ return batchSymbolUseEvidence(root, resolvedSymbolUses, semanticProjects, evidenceLimit);
1751
+ };
1752
+
1753
+ const analyzeGraphSemanticQuery = (root, query, graphStates, evidenceLimit) => {
1754
+ if (query.operation === "api-surface") {
1755
+ return analyzeApiSurface(root, query, graphStates, evidenceLimit);
1756
+ }
1757
+ return analyzeTypeCoupling(
1758
+ { root, query, states: graphStates, evidenceLimit },
1759
+ {
1760
+ boundedResult,
1761
+ discoverEntryPoints,
1762
+ missingEntryPoints,
1763
+ publicApiGraph,
1764
+ readyProjectStates,
1765
+ uniqueSorted,
1766
+ },
1767
+ );
1768
+ };
1769
+
1770
+ const analyzeSymbolUseQuery = (root, query, resolved, evidenceLimit, symbolUseBatch) => {
1771
+ const evidence = symbolUseBatch.evidenceByQuery.get(query.id);
1772
+ const total = symbolUseBatch.totalByQuery.get(query.id);
1773
+ return analyzeSymbolUse(
1774
+ root,
1775
+ query,
1776
+ resolved,
1777
+ evidenceLimit,
1778
+ evidence === undefined ? [] : evidence,
1779
+ total === undefined ? 0 : total,
1780
+ symbolUseBatch.uncertaintiesByQuery.get(query.id) ?? new Set(),
1781
+ );
1782
+ };
1783
+
1784
+ const analyzeResolvedSemanticQuery = ({
1785
+ root,
1786
+ query,
1787
+ resolved,
1788
+ evidenceLimit,
1789
+ symbolUseBatch,
1790
+ semanticProjects,
1791
+ }) => {
1792
+ if (resolved.error) return resolved.error;
1793
+ if (query.operation === "symbol-use") {
1794
+ return analyzeSymbolUseQuery(root, query, resolved, evidenceLimit, symbolUseBatch);
1795
+ }
1796
+ const references = symbolUseBatch.evidenceByQuery.get(query.id) ?? [];
1797
+ const totalReferenceCount = symbolUseBatch.totalByQuery.get(query.id) ?? 0;
1798
+ if (query.operation === "symbol-trace") {
1799
+ return analyzeSymbolTrace(
1800
+ root,
1801
+ query,
1802
+ resolved,
1803
+ evidenceLimit,
1804
+ references,
1805
+ totalReferenceCount,
1806
+ symbolUseBatch.aliasHopTotalByQuery.get(query.id) ?? 0,
1807
+ );
1808
+ }
1809
+ return analyzeSymbolImpact(
1810
+ {
1811
+ root,
1812
+ query,
1813
+ resolved,
1814
+ evidenceLimit,
1815
+ scanProjects: semanticProjects,
1816
+ directReferenceFiles: symbolUseBatch.directFilesByQuery.get(query.id) ?? new Set(),
1817
+ },
1818
+ { boundedResult },
1819
+ );
1820
+ };
1821
+
1822
+ const analyzeSemanticQuery = (context, query) => {
1823
+ if (query.operation === "api-surface" || query.operation === "type-coupling") {
1824
+ return analyzeGraphSemanticQuery(
1825
+ context.root,
1826
+ query,
1827
+ context.graphStates,
1828
+ context.evidenceLimit,
1829
+ );
1830
+ }
1831
+ return analyzeResolvedSemanticQuery({
1832
+ ...context,
1833
+ query,
1834
+ resolved: context.resolvedByQuery.get(query.id),
1835
+ });
1836
+ };
1837
+
1838
+ const recordAbstainedProjectOutcome = (state, result) => {
1839
+ state.abstained_count += 1;
1840
+ const projectFailure = new Set([
1841
+ "no-project",
1842
+ "ambiguous-project",
1843
+ "blocking-diagnostics",
1844
+ "unknown-symbol",
1845
+ "incomplete-project-coverage",
1846
+ ]);
1847
+ if (!projectFailure.has(result?.reasonCode)) return;
1848
+ if (state.reason_code !== null) return;
1849
+ state.reason_code =
1850
+ result === undefined || result.reasonCode === null ? "unsupported-syntax" : result.reasonCode;
1851
+ };
1852
+
1853
+ const recordProjectOutcome = (state, query, result) => {
1854
+ state.candidate_count += 1;
1855
+ if (result.assertion === "confirmed-used") {
1856
+ state.confirmed_used_count += 1;
1857
+ return;
1858
+ }
1859
+ if (result.assertion === "contract-preserved") {
1860
+ state.contract_preserved_count += 1;
1861
+ return;
1862
+ }
1863
+ if (result.assertion === "confirmed-no-static-references") {
1864
+ state.no_static_references_count += 1;
1865
+ if (result.data.closed_world_eligible && query.symbol.declarationKind === "class_method") {
1866
+ state.fix_eligible_count += 1;
1867
+ }
1868
+ return;
1869
+ }
1870
+ if (result.status === "complete") {
1871
+ state.unresolved_count += 1;
1872
+ return;
1873
+ }
1874
+ recordAbstainedProjectOutcome(state, result);
1875
+ };
1876
+
1877
+ const markUnavailableProject = (state) => {
1878
+ state.status = "unavailable";
1879
+ };
1880
+
1881
+ const hasAbstainedSemanticQuery = (state) =>
1882
+ state.status === "complete" && state.abstained_count > 0;
1883
+
1884
+ const recordQueryProjectOutcome = (query, resolvedByQuery, resultsByQuery) => {
1885
+ if (query.operation !== "symbol-use") return;
1886
+ const resolved = resolvedByQuery.get(query.id);
1887
+ if (resolved === undefined) return;
1888
+ const states =
1889
+ resolved.ownerContexts?.map(({ state }) => state) ?? [resolved.state].filter(Boolean);
1890
+ [...new Set(states)].forEach((state) =>
1891
+ recordProjectOutcome(state, query, resultsByQuery.get(query.id)),
1892
+ );
1893
+ };
1894
+
1895
+ const recordProjectOutcomes = (states, queries, resolvedByQuery, results) => {
1896
+ const resultsByQuery = new Map(results.map((result) => [result.queryId, result]));
1897
+ queries.forEach((query) => recordQueryProjectOutcome(query, resolvedByQuery, resultsByQuery));
1898
+ states.filter(hasAbstainedSemanticQuery).forEach(markUnavailableProject);
1899
+ };
1900
+
1901
+ const recordProgramReuse = (states, graphStates, queries, resolvedByQuery) => {
1902
+ const graphQueryCount = queries.filter(
1903
+ ({ operation }) => operation === "api-surface" || operation === "type-coupling",
1904
+ ).length;
1905
+ const graphProjectSet = new Set(graphStates.map(({ project }) => project));
1906
+ for (const state of states) {
1907
+ let queryCount = graphProjectSet.has(state.project) ? graphQueryCount : 0;
1908
+ for (const query of queries) {
1909
+ if (!query.symbol) continue;
1910
+ const resolved = resolvedByQuery.get(query.id);
1911
+ if (resolved?.ownerContexts?.some(({ project }) => project === state.project)) {
1912
+ queryCount += 1;
1913
+ }
1914
+ }
1915
+ state.program_reused = queryCount > 1;
1916
+ }
1917
+ };
1918
+
1919
+ const emptyRequestedAnalysis = () => ({
1920
+ selectedTsconfigs: [],
1921
+ projectResults: [],
1922
+ results: [],
1923
+ phaseTimings: { project_setup: 0, diagnostics: 0, semantic_queries: 0 },
1924
+ warnings: [],
1925
+ });
1926
+
1927
+ const semanticOpenFiles = (queries) =>
1928
+ queries.filter((query) => query.symbol).map((query) => query.symbol.absolutePath);
1929
+
1930
+ const semanticOpenProjects = (root, projects) => {
1931
+ if (projects.length > 0) return projects.map((project) => project.absolutePath);
1932
+ const conventionalProject = path.join(root, "tsconfig.json");
1933
+ return existsSync(conventionalProject) ? [conventionalProject] : [];
1934
+ };
1935
+
1936
+ const semanticProjectSelection = (snapshot, openProjects, openFiles, allowDefaultFallback) => {
1937
+ const explicitProjects = openProjects
1938
+ .map((project) => snapshot.getProject(project))
1939
+ .filter(Boolean);
1940
+ const symbolProjects = openFiles
1941
+ .map((file) => selectProjectForSymbol(snapshot, explicitProjects, file, allowDefaultFallback))
1942
+ .filter(Boolean);
1943
+ const selectedProjects = [
1944
+ ...new Set([...graphProjects(snapshot, explicitProjects), ...symbolProjects]),
1945
+ ];
1946
+ return { explicitProjects, selectedProjects };
1947
+ };
1948
+
1949
+ const semanticSetup = (root, projects, queries, api, sessionState) => {
1950
+ const openFiles = semanticOpenFiles(queries);
1951
+ const openProjects = semanticOpenProjects(root, projects);
1952
+ const openFileSet = new Set(openFiles);
1953
+ const openProjectSet = new Set(openProjects);
1954
+ const newlyOpenedFiles = sessionState
1955
+ ? openFiles.filter((file) => !sessionState.openFiles.has(file))
1956
+ : openFiles;
1957
+ const newlyOpenedProjects = sessionState
1958
+ ? openProjects.filter((project) => !sessionState.openProjects.has(project))
1959
+ : openProjects;
1960
+ const closeFiles = sessionState
1961
+ ? [...sessionState.openFiles].filter((file) => !openFileSet.has(file))
1962
+ : [];
1963
+ const closeProjects = sessionState
1964
+ ? [...sessionState.openProjects].filter((project) => !openProjectSet.has(project))
1965
+ : [];
1966
+ const snapshot = api.updateSnapshot({
1967
+ openFiles: [...new Set(newlyOpenedFiles)],
1968
+ openProjects: newlyOpenedProjects,
1969
+ closeFiles,
1970
+ closeProjects,
1971
+ ...(sessionState?.fileChanges ? { fileChanges: sessionState.fileChanges } : {}),
1972
+ });
1973
+ if (sessionState) {
1974
+ const previousSnapshot = sessionState.snapshot;
1975
+ sessionState.snapshot = snapshot;
1976
+ sessionState.openFiles = openFileSet;
1977
+ sessionState.openProjects = openProjectSet;
1978
+ previousSnapshot?.dispose();
1979
+ }
1980
+ const allowDefaultFallback = projects.length === 0;
1981
+ return {
1982
+ snapshot,
1983
+ openFiles,
1984
+ allowDefaultFallback,
1985
+ ...semanticProjectSelection(snapshot, openProjects, openFiles, allowDefaultFallback),
1986
+ };
1987
+ };
1988
+
1989
+ const semanticStates = (root, projects, selectedProjects) => {
1990
+ const source = projects.length > 0 ? "explicit" : "auto";
1991
+ return selectedProjects.map((project) => projectState(root, project, source));
1992
+ };
1993
+
1994
+ const graphProjectStates = (states, explicitProjects) => {
1995
+ const explicitProjectSet = new Set(explicitProjects);
1996
+ if (explicitProjectSet.size === 0) return states;
1997
+ return states.filter((state) => explicitProjectSet.has(state.project));
1998
+ };
1999
+
2000
+ const executeSemanticAnalysis = ({
2001
+ root,
2002
+ projects,
2003
+ queries,
2004
+ evidenceLimit,
2005
+ setupStartedAt,
2006
+ setup,
2007
+ }) => {
2008
+ if (setup.selectedProjects.length === 0) return emptySemanticAnalysis(queries);
2009
+ const projectSetupMs = performance.now() - setupStartedAt;
2010
+ const diagnosticsStartedAt = performance.now();
2011
+ const states = semanticStates(root, projects, setup.selectedProjects);
2012
+ const diagnosticsMs = performance.now() - diagnosticsStartedAt;
2013
+ const statesByProject = new Map(states.map((state) => [state.project, state]));
2014
+ const graphStates = graphProjectStates(states, setup.explicitProjects);
2015
+ const queryStartedAt = performance.now();
2016
+ const resolvedByQuery = resolveSemanticQueries({
2017
+ root,
2018
+ snapshot: setup.snapshot,
2019
+ explicitProjects: setup.explicitProjects,
2020
+ statesByProject,
2021
+ queries,
2022
+ allowDefaultFallback: setup.allowDefaultFallback,
2023
+ });
2024
+ const semanticProjects = states
2025
+ .filter((state) => state.status === "complete")
2026
+ .map((state) => state.project);
2027
+ const symbolUseBatch = runSymbolUseBatch(
2028
+ root,
2029
+ queries,
2030
+ resolvedByQuery,
2031
+ semanticProjects,
2032
+ evidenceLimit,
2033
+ );
2034
+ const context = {
2035
+ root,
2036
+ graphStates,
2037
+ evidenceLimit,
2038
+ resolvedByQuery,
2039
+ symbolUseBatch,
2040
+ semanticProjects,
2041
+ };
2042
+ const results = queries.map((query) => analyzeSemanticQuery(context, query));
2043
+ recordProjectOutcomes(states, queries, resolvedByQuery, results);
2044
+ recordProgramReuse(states, graphStates, queries, resolvedByQuery);
2045
+ return {
2046
+ selectedTsconfigs: states.map((state) => state.config),
2047
+ projectResults: states.map(projectResult),
2048
+ results,
2049
+ phaseTimings: {
2050
+ project_setup: projectSetupMs,
2051
+ diagnostics: diagnosticsMs,
2052
+ semantic_queries: performance.now() - queryStartedAt,
2053
+ },
2054
+ warnings: [],
2055
+ sourceScanCount: symbolUseBatch.sourceScanCount,
2056
+ referenceScanCount: 0,
2057
+ };
2058
+ };
2059
+
2060
+ const executeDisposableSemanticAnalysis = (input) => {
2061
+ try {
2062
+ return executeSemanticAnalysis(input);
2063
+ } finally {
2064
+ input.setup.snapshot.dispose();
2065
+ }
2066
+ };
2067
+
2068
+ export const analyzeSemanticQueries = (
2069
+ { root, projects, queries, evidenceLimit },
2070
+ { createApi = (cwd) => new API({ cwd }) } = {},
2071
+ ) => {
2072
+ if (queries.length === 0) return emptyRequestedAnalysis();
2073
+ const setupStartedAt = performance.now();
2074
+ const api = createApi(root);
2075
+ try {
2076
+ return executeDisposableSemanticAnalysis({
2077
+ root,
2078
+ projects,
2079
+ queries,
2080
+ evidenceLimit,
2081
+ setupStartedAt,
2082
+ setup: semanticSetup(root, projects, queries, api),
2083
+ });
2084
+ } finally {
2085
+ api.close();
2086
+ }
2087
+ };
2088
+
2089
+ export const createSemanticSession = (root, { createApi = (cwd) => new API({ cwd }) } = {}) => {
2090
+ const api = createApi(root);
2091
+ const state = {
2092
+ openFiles: new Set(),
2093
+ openProjects: new Set(),
2094
+ fileChanges: undefined,
2095
+ revision: 0,
2096
+ analyzed: false,
2097
+ closed: false,
2098
+ snapshot: undefined,
2099
+ };
2100
+ return {
2101
+ analyze(request, { revision, fileChanges } = {}) {
2102
+ if (state.closed) throw new Error("semantic session is closed");
2103
+ if (request.root !== root) throw new Error("semantic session root mismatch");
2104
+ if (!Number.isSafeInteger(revision) || revision <= state.revision) {
2105
+ throw new Error("semantic session revision must increase");
2106
+ }
2107
+ state.fileChanges = fileChanges;
2108
+ const setupStartedAt = performance.now();
2109
+ const result = executeSemanticAnalysis({
2110
+ root,
2111
+ projects: request.projects,
2112
+ queries: request.queries,
2113
+ evidenceLimit: request.evidenceLimit,
2114
+ setupStartedAt,
2115
+ setup: semanticSetup(root, request.projects, request.queries, api, state),
2116
+ });
2117
+ const invalidationKind = fileChanges?.invalidateAll
2118
+ ? "full"
2119
+ : fileChanges
2120
+ ? "incremental"
2121
+ : state.analyzed
2122
+ ? "none"
2123
+ : "full";
2124
+ result.projectResults = result.projectResults.map((project) => ({
2125
+ ...project,
2126
+ program_reused_from_previous_snapshot: state.analyzed && invalidationKind !== "full",
2127
+ snapshot_revision: revision,
2128
+ invalidation_kind: invalidationKind,
2129
+ }));
2130
+ state.revision = revision;
2131
+ state.analyzed = true;
2132
+ state.fileChanges = undefined;
2133
+ return result;
2134
+ },
2135
+ close() {
2136
+ if (state.closed) return;
2137
+ state.closed = true;
2138
+ state.snapshot?.dispose();
2139
+ api.close();
2140
+ },
2141
+ };
2142
+ };