@optave/codegraph 3.11.1 → 3.12.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.
Files changed (176) hide show
  1. package/README.md +8 -8
  2. package/dist/db/migrations.d.ts.map +1 -1
  3. package/dist/db/migrations.js +7 -0
  4. package/dist/db/migrations.js.map +1 -1
  5. package/dist/domain/analysis/module-map.d.ts +2 -0
  6. package/dist/domain/analysis/module-map.d.ts.map +1 -1
  7. package/dist/domain/analysis/module-map.js +24 -2
  8. package/dist/domain/analysis/module-map.js.map +1 -1
  9. package/dist/domain/graph/builder/call-resolver.d.ts +73 -0
  10. package/dist/domain/graph/builder/call-resolver.d.ts.map +1 -0
  11. package/dist/domain/graph/builder/call-resolver.js +292 -0
  12. package/dist/domain/graph/builder/call-resolver.js.map +1 -0
  13. package/dist/domain/graph/builder/cha.d.ts +61 -0
  14. package/dist/domain/graph/builder/cha.d.ts.map +1 -0
  15. package/dist/domain/graph/builder/cha.js +143 -0
  16. package/dist/domain/graph/builder/cha.js.map +1 -0
  17. package/dist/domain/graph/builder/context.d.ts +3 -0
  18. package/dist/domain/graph/builder/context.d.ts.map +1 -1
  19. package/dist/domain/graph/builder/context.js +2 -0
  20. package/dist/domain/graph/builder/context.js.map +1 -1
  21. package/dist/domain/graph/builder/helpers.d.ts +17 -1
  22. package/dist/domain/graph/builder/helpers.d.ts.map +1 -1
  23. package/dist/domain/graph/builder/helpers.js +159 -5
  24. package/dist/domain/graph/builder/helpers.js.map +1 -1
  25. package/dist/domain/graph/builder/incremental.d.ts.map +1 -1
  26. package/dist/domain/graph/builder/incremental.js +147 -54
  27. package/dist/domain/graph/builder/incremental.js.map +1 -1
  28. package/dist/domain/graph/builder/stages/build-edges.d.ts +2 -0
  29. package/dist/domain/graph/builder/stages/build-edges.d.ts.map +1 -1
  30. package/dist/domain/graph/builder/stages/build-edges.js +932 -110
  31. package/dist/domain/graph/builder/stages/build-edges.js.map +1 -1
  32. package/dist/domain/graph/builder/stages/detect-changes.d.ts.map +1 -1
  33. package/dist/domain/graph/builder/stages/detect-changes.js +2 -1
  34. package/dist/domain/graph/builder/stages/detect-changes.js.map +1 -1
  35. package/dist/domain/graph/builder/stages/native-orchestrator.d.ts.map +1 -1
  36. package/dist/domain/graph/builder/stages/native-orchestrator.js +501 -14
  37. package/dist/domain/graph/builder/stages/native-orchestrator.js.map +1 -1
  38. package/dist/domain/graph/builder/stages/resolve-imports.d.ts +1 -0
  39. package/dist/domain/graph/builder/stages/resolve-imports.d.ts.map +1 -1
  40. package/dist/domain/graph/builder/stages/resolve-imports.js +9 -0
  41. package/dist/domain/graph/builder/stages/resolve-imports.js.map +1 -1
  42. package/dist/domain/graph/journal.js +1 -1
  43. package/dist/domain/graph/journal.js.map +1 -1
  44. package/dist/domain/graph/resolver/points-to.d.ts +53 -0
  45. package/dist/domain/graph/resolver/points-to.d.ts.map +1 -0
  46. package/dist/domain/graph/resolver/points-to.js +213 -0
  47. package/dist/domain/graph/resolver/points-to.js.map +1 -0
  48. package/dist/domain/graph/resolver/ts-resolver.d.ts +9 -0
  49. package/dist/domain/graph/resolver/ts-resolver.d.ts.map +1 -0
  50. package/dist/domain/graph/resolver/ts-resolver.js +476 -0
  51. package/dist/domain/graph/resolver/ts-resolver.js.map +1 -0
  52. package/dist/domain/graph/watcher.d.ts.map +1 -1
  53. package/dist/domain/graph/watcher.js +5 -2
  54. package/dist/domain/graph/watcher.js.map +1 -1
  55. package/dist/domain/parser.d.ts +10 -1
  56. package/dist/domain/parser.d.ts.map +1 -1
  57. package/dist/domain/parser.js +39 -7
  58. package/dist/domain/parser.js.map +1 -1
  59. package/dist/domain/wasm-worker-entry.js +25 -0
  60. package/dist/domain/wasm-worker-entry.js.map +1 -1
  61. package/dist/domain/wasm-worker-pool.d.ts.map +1 -1
  62. package/dist/domain/wasm-worker-pool.js +32 -0
  63. package/dist/domain/wasm-worker-pool.js.map +1 -1
  64. package/dist/domain/wasm-worker-protocol.d.ts +14 -1
  65. package/dist/domain/wasm-worker-protocol.d.ts.map +1 -1
  66. package/dist/extractors/c.js +3 -3
  67. package/dist/extractors/c.js.map +1 -1
  68. package/dist/extractors/clojure.js +1 -1
  69. package/dist/extractors/clojure.js.map +1 -1
  70. package/dist/extractors/cpp.js +3 -3
  71. package/dist/extractors/cpp.js.map +1 -1
  72. package/dist/extractors/csharp.d.ts.map +1 -1
  73. package/dist/extractors/csharp.js +37 -8
  74. package/dist/extractors/csharp.js.map +1 -1
  75. package/dist/extractors/cuda.js +3 -3
  76. package/dist/extractors/cuda.js.map +1 -1
  77. package/dist/extractors/elixir.js +6 -6
  78. package/dist/extractors/elixir.js.map +1 -1
  79. package/dist/extractors/fsharp.js +1 -1
  80. package/dist/extractors/fsharp.js.map +1 -1
  81. package/dist/extractors/go.js +5 -5
  82. package/dist/extractors/go.js.map +1 -1
  83. package/dist/extractors/haskell.js +1 -1
  84. package/dist/extractors/haskell.js.map +1 -1
  85. package/dist/extractors/java.js +2 -2
  86. package/dist/extractors/java.js.map +1 -1
  87. package/dist/extractors/javascript.d.ts +2 -0
  88. package/dist/extractors/javascript.d.ts.map +1 -1
  89. package/dist/extractors/javascript.js +1674 -64
  90. package/dist/extractors/javascript.js.map +1 -1
  91. package/dist/extractors/kotlin.js +5 -5
  92. package/dist/extractors/kotlin.js.map +1 -1
  93. package/dist/extractors/lua.js +1 -1
  94. package/dist/extractors/lua.js.map +1 -1
  95. package/dist/extractors/objc.js +3 -3
  96. package/dist/extractors/objc.js.map +1 -1
  97. package/dist/extractors/ocaml.js +1 -1
  98. package/dist/extractors/ocaml.js.map +1 -1
  99. package/dist/extractors/php.js +2 -2
  100. package/dist/extractors/php.js.map +1 -1
  101. package/dist/extractors/python.js +7 -7
  102. package/dist/extractors/python.js.map +1 -1
  103. package/dist/extractors/ruby.js +2 -2
  104. package/dist/extractors/ruby.js.map +1 -1
  105. package/dist/extractors/scala.js +1 -1
  106. package/dist/extractors/scala.js.map +1 -1
  107. package/dist/extractors/solidity.js +1 -1
  108. package/dist/extractors/solidity.js.map +1 -1
  109. package/dist/extractors/swift.js +4 -4
  110. package/dist/extractors/swift.js.map +1 -1
  111. package/dist/extractors/zig.js +4 -4
  112. package/dist/extractors/zig.js.map +1 -1
  113. package/dist/features/structure.d.ts.map +1 -1
  114. package/dist/features/structure.js +121 -16
  115. package/dist/features/structure.js.map +1 -1
  116. package/dist/infrastructure/config.d.ts +10 -0
  117. package/dist/infrastructure/config.d.ts.map +1 -1
  118. package/dist/infrastructure/config.js +15 -0
  119. package/dist/infrastructure/config.js.map +1 -1
  120. package/dist/infrastructure/native.d.ts +11 -0
  121. package/dist/infrastructure/native.d.ts.map +1 -1
  122. package/dist/infrastructure/native.js +78 -5
  123. package/dist/infrastructure/native.js.map +1 -1
  124. package/dist/presentation/queries-cli/overview.d.ts.map +1 -1
  125. package/dist/presentation/queries-cli/overview.js +5 -0
  126. package/dist/presentation/queries-cli/overview.js.map +1 -1
  127. package/dist/types.d.ts +184 -0
  128. package/dist/types.d.ts.map +1 -1
  129. package/grammars/tree-sitter-erlang.wasm +0 -0
  130. package/package.json +9 -9
  131. package/src/db/migrations.ts +7 -0
  132. package/src/domain/analysis/module-map.ts +29 -1
  133. package/src/domain/graph/builder/call-resolver.ts +351 -0
  134. package/src/domain/graph/builder/cha.ts +175 -0
  135. package/src/domain/graph/builder/context.ts +3 -0
  136. package/src/domain/graph/builder/helpers.ts +175 -5
  137. package/src/domain/graph/builder/incremental.ts +186 -66
  138. package/src/domain/graph/builder/stages/build-edges.ts +1146 -146
  139. package/src/domain/graph/builder/stages/detect-changes.ts +3 -1
  140. package/src/domain/graph/builder/stages/native-orchestrator.ts +583 -20
  141. package/src/domain/graph/builder/stages/resolve-imports.ts +14 -0
  142. package/src/domain/graph/journal.ts +1 -1
  143. package/src/domain/graph/resolver/points-to.ts +254 -0
  144. package/src/domain/graph/resolver/ts-resolver.ts +536 -0
  145. package/src/domain/graph/watcher.ts +4 -2
  146. package/src/domain/parser.ts +43 -5
  147. package/src/domain/wasm-worker-entry.ts +25 -0
  148. package/src/domain/wasm-worker-pool.ts +21 -0
  149. package/src/domain/wasm-worker-protocol.ts +14 -0
  150. package/src/extractors/c.ts +3 -3
  151. package/src/extractors/clojure.ts +1 -1
  152. package/src/extractors/cpp.ts +3 -3
  153. package/src/extractors/csharp.ts +33 -9
  154. package/src/extractors/cuda.ts +3 -3
  155. package/src/extractors/elixir.ts +6 -6
  156. package/src/extractors/fsharp.ts +1 -1
  157. package/src/extractors/go.ts +5 -5
  158. package/src/extractors/haskell.ts +1 -1
  159. package/src/extractors/java.ts +2 -2
  160. package/src/extractors/javascript.ts +1802 -66
  161. package/src/extractors/kotlin.ts +5 -5
  162. package/src/extractors/lua.ts +1 -1
  163. package/src/extractors/objc.ts +3 -3
  164. package/src/extractors/ocaml.ts +1 -1
  165. package/src/extractors/php.ts +2 -2
  166. package/src/extractors/python.ts +7 -7
  167. package/src/extractors/ruby.ts +2 -2
  168. package/src/extractors/scala.ts +1 -1
  169. package/src/extractors/solidity.ts +1 -1
  170. package/src/extractors/swift.ts +4 -4
  171. package/src/extractors/zig.ts +4 -4
  172. package/src/features/structure.ts +143 -23
  173. package/src/infrastructure/config.ts +15 -0
  174. package/src/infrastructure/native.ts +87 -5
  175. package/src/presentation/queries-cli/overview.ts +15 -1
  176. package/src/types.ts +194 -0
@@ -7,11 +7,19 @@
7
7
  import path from 'node:path';
8
8
  import { performance } from 'node:perf_hooks';
9
9
  import { getNodeId } from '../../../../db/index.js';
10
+ import { setTypeMapEntry } from '../../../../extractors/helpers.js';
11
+ import { PROPAGATION_HOP_PENALTY } from '../../../../extractors/javascript.js';
10
12
  import { debug } from '../../../../infrastructure/logger.js';
11
13
  import { loadNative } from '../../../../infrastructure/native.js';
12
14
  import { computeConfidence } from '../../resolve.js';
13
- import { BUILTIN_RECEIVERS, batchInsertEdges } from '../helpers.js';
14
- import { getResolved, isBarrelFile, resolveBarrelExport } from './resolve-imports.js';
15
+ import { buildPointsToMap, resolveViaPointsTo } from '../../resolver/points-to.js';
16
+ import { enrichTypeMapWithTsc } from '../../resolver/ts-resolver.js';
17
+ import { findCaller, isModuleScopedLanguage, resolveCallTargets, resolveReceiverEdge, } from '../call-resolver.js';
18
+ import { buildChaContext, resolveChaTargets, resolveThisDispatch } from '../cha.js';
19
+ import { BUILTIN_RECEIVERS, batchInsertEdges, runChaPostPass } from '../helpers.js';
20
+ import { getResolved, isBarrelFile, resolveBarrelExportCached } from './resolve-imports.js';
21
+ /** Phase 8.5: confidence penalty applied to CHA-dispatch edges. */
22
+ export const CHA_DISPATCH_PENALTY = 0.1;
15
23
  // ── Node lookup setup ───────────────────────────────────────────────────
16
24
  function makeGetNodeIdStmt(db) {
17
25
  return {
@@ -58,13 +66,13 @@ function emitTypeOnlySymbolEdges(ctx, imp, resolvedPath, fileNodeId, allEdgeRows
58
66
  const cleanName = name.replace(/^\*\s+as\s+/, '');
59
67
  let targetFile = resolvedPath;
60
68
  if (isBarrelFile(ctx, resolvedPath)) {
61
- const actual = resolveBarrelExport(ctx, resolvedPath, cleanName);
69
+ const actual = resolveBarrelExportCached(ctx, resolvedPath, cleanName);
62
70
  if (actual)
63
71
  targetFile = actual;
64
72
  }
65
73
  const candidates = ctx.nodesByNameAndFile.get(`${cleanName}|${targetFile}`);
66
74
  if (candidates && candidates.length > 0) {
67
- allEdgeRows.push([fileNodeId, candidates[0].id, 'imports-type', 1.0, 0]);
75
+ allEdgeRows.push([fileNodeId, candidates[0].id, 'imports-type', 1.0, 0, null]);
68
76
  }
69
77
  }
70
78
  }
@@ -78,7 +86,7 @@ function emitEdgesForImport(ctx, imp, fileNodeId, relPath, getNodeIdStmt, allEdg
78
86
  if (!targetRow)
79
87
  return;
80
88
  const edgeKind = importEdgeKind(imp);
81
- allEdgeRows.push([fileNodeId, targetRow.id, edgeKind, 1.0, 0]);
89
+ allEdgeRows.push([fileNodeId, targetRow.id, edgeKind, 1.0, 0, null]);
82
90
  if (imp.typeOnly) {
83
91
  emitTypeOnlySymbolEdges(ctx, imp, resolvedPath, fileNodeId, allEdgeRows);
84
92
  }
@@ -106,7 +114,7 @@ function buildBarrelEdges(ctx, imp, resolvedPath, fileNodeId, edgeKind, getNodeI
106
114
  const resolvedSources = new Set();
107
115
  for (const name of imp.names) {
108
116
  const cleanName = name.replace(/^\*\s+as\s+/, '');
109
- const actualSource = resolveBarrelExport(ctx, resolvedPath, cleanName);
117
+ const actualSource = resolveBarrelExportCached(ctx, resolvedPath, cleanName);
110
118
  if (actualSource && actualSource !== resolvedPath && !resolvedSources.has(actualSource)) {
111
119
  resolvedSources.add(actualSource);
112
120
  const actualRow = getNodeIdStmt.get(actualSource, 'file', actualSource, 0);
@@ -116,7 +124,7 @@ function buildBarrelEdges(ctx, imp, resolvedPath, fileNodeId, edgeKind, getNodeI
116
124
  : edgeKind === 'dynamic-imports'
117
125
  ? 'dynamic-imports'
118
126
  : 'imports';
119
- edgeRows.push([fileNodeId, actualRow.id, kind, 0.9, 0]);
127
+ edgeRows.push([fileNodeId, actualRow.id, kind, 0.9, 0, null]);
120
128
  }
121
129
  }
122
130
  }
@@ -231,7 +239,66 @@ function buildImportEdgesNative(ctx, getNodeIdStmt, allEdgeRows, native) {
231
239
  const symbolNodes = collectSymbolNodes(ctx);
232
240
  const nativeEdges = native.buildImportEdges(files, resolvedImports, fileReexports, registry.ids, barrelFiles, ctx.rootDir, symbolNodes);
233
241
  for (const e of nativeEdges) {
234
- allEdgeRows.push([e.sourceId, e.targetId, e.kind, e.confidence, e.dynamic]);
242
+ allEdgeRows.push([e.sourceId, e.targetId, e.kind, e.confidence, e.dynamic, null]);
243
+ }
244
+ }
245
+ // ── Phase 8.2: Cross-file return-type propagation ───────────────────────
246
+ /**
247
+ * Augment each file's typeMap with return types from imported functions.
248
+ *
249
+ * The per-file extractor already resolves same-file call assignments (intra-file
250
+ * propagation). This function handles the cross-file case: when a file imports a
251
+ * function from another file and assigns its return value to a variable, we look up
252
+ * the callee's return type in the source file's returnTypeMap and inject it.
253
+ *
254
+ * Called once before call-edge building so both the native and JS paths benefit.
255
+ */
256
+ function propagateReturnTypesAcrossFiles(fileSymbols, ctx, rootDir) {
257
+ // Index: filePath → per-file return-type map
258
+ const returnTypeIndex = new Map();
259
+ for (const [relPath, symbols] of fileSymbols) {
260
+ if (symbols.returnTypeMap?.size)
261
+ returnTypeIndex.set(relPath, symbols.returnTypeMap);
262
+ }
263
+ if (returnTypeIndex.size === 0)
264
+ return;
265
+ // Flat global map for qualified method lookups (TypeName.methodName → entry).
266
+ // Conflicts resolved by keeping the highest-confidence entry.
267
+ const globalReturnTypeMap = new Map();
268
+ for (const rtm of returnTypeIndex.values()) {
269
+ for (const [name, entry] of rtm) {
270
+ const existing = globalReturnTypeMap.get(name);
271
+ if (!existing || entry.confidence > existing.confidence)
272
+ globalReturnTypeMap.set(name, entry);
273
+ }
274
+ }
275
+ for (const [relPath, symbols] of fileSymbols) {
276
+ if (!symbols.callAssignments?.length)
277
+ continue;
278
+ // Phase 8.4 side-effect: buildImportedNamesMap now traces through barrel
279
+ // files (traceBarrel), so `importedFrom` resolves to the leaf definition
280
+ // file rather than the barrel. This means returnTypeIndex.get(importedFrom)
281
+ // now finds entries it previously missed, improving cross-file return-type
282
+ // propagation through re-export chains (Phase 8.2 improvement).
283
+ const importedNamesMap = buildImportedNamesMap(ctx, relPath, symbols, rootDir);
284
+ for (const ca of symbols.callAssignments) {
285
+ if (symbols.typeMap.has(ca.varName))
286
+ continue; // already resolved locally
287
+ let returnEntry;
288
+ if (ca.receiverTypeName) {
289
+ returnEntry = globalReturnTypeMap.get(`${ca.receiverTypeName}.${ca.calleeName}`);
290
+ }
291
+ else {
292
+ const importedFrom = importedNamesMap.get(ca.calleeName);
293
+ if (importedFrom)
294
+ returnEntry = returnTypeIndex.get(importedFrom)?.get(ca.calleeName);
295
+ }
296
+ if (returnEntry) {
297
+ const propagatedConf = returnEntry.confidence - PROPAGATION_HOP_PENALTY;
298
+ if (propagatedConf > 0)
299
+ setTypeMapEntry(symbols.typeMap, ca.varName, returnEntry.type, propagatedConf);
300
+ }
301
+ }
235
302
  }
236
303
  }
237
304
  // ── Call edges (native engine) ──────────────────────────────────────────
@@ -280,13 +347,432 @@ function buildCallEdgesNative(ctx, getNodeIdStmt, allEdgeRows, allNodes, native)
280
347
  importedNames,
281
348
  classes: symbols.classes,
282
349
  typeMap,
350
+ fnRefBindings: symbols.fnRefBindings?.length ? symbols.fnRefBindings : undefined,
283
351
  });
284
352
  }
285
353
  const nativeEdges = native.buildCallEdges(nativeFiles, allNodes, [
286
354
  ...BUILTIN_RECEIVERS,
287
355
  ]);
288
356
  for (const e of nativeEdges) {
289
- allEdgeRows.push([e.sourceId, e.targetId, e.kind, e.confidence, e.dynamic]);
357
+ allEdgeRows.push([
358
+ e.sourceId,
359
+ e.targetId,
360
+ e.kind,
361
+ e.confidence,
362
+ e.dynamic,
363
+ e.kind === 'calls' ? 'ts-native' : null,
364
+ ]);
365
+ }
366
+ }
367
+ /**
368
+ * Phase 8.3c pts post-pass for the native call-edge path.
369
+ *
370
+ * The native Rust engine builds call edges without knowledge of paramBindings,
371
+ * so `fn()` calls inside higher-order functions are not resolved to their
372
+ * concrete targets. This JS post-pass runs after the native edge pass and adds
373
+ * only the parameter-flow pts edges that the native engine missed.
374
+ *
375
+ * To avoid duplicating edges already emitted by the native engine, the current
376
+ * allEdgeRows snapshot is used to seed a seenByPair set before processing each
377
+ * file.
378
+ */
379
+ function buildParamFlowPtsPostPass(ctx, getNodeIdStmt, allEdgeRows, sharedLookup) {
380
+ // Only process files that actually have paramBindings (avoid useless work).
381
+ const filesWithParams = [...ctx.fileSymbols].filter(([, symbols]) => symbols.paramBindings && symbols.paramBindings.length > 0);
382
+ if (filesWithParams.length === 0)
383
+ return;
384
+ // Seed seenByPair from the existing rows so we don't duplicate native edges.
385
+ // This is O(|allEdgeRows|) once per post-pass, which is acceptable.
386
+ const seenByPair = new Set();
387
+ for (const [srcId, tgtId] of allEdgeRows) {
388
+ seenByPair.add(`${srcId}|${tgtId}`);
389
+ }
390
+ const { barrelOnlyFiles, rootDir } = ctx;
391
+ const lookup = sharedLookup ?? makeContextLookup(ctx, getNodeIdStmt);
392
+ for (const [relPath, symbols] of filesWithParams) {
393
+ if (barrelOnlyFiles.has(relPath))
394
+ continue;
395
+ const fileNodeRow = getNodeIdStmt.get(relPath, 'file', relPath, 0);
396
+ if (!fileNodeRow)
397
+ continue;
398
+ const importedNames = buildImportedNamesMap(ctx, relPath, symbols, rootDir);
399
+ const typeMap = symbols.typeMap || new Map();
400
+ const ptsMap = buildPointsToMapForFile(symbols, importedNames);
401
+ if (!ptsMap)
402
+ continue;
403
+ for (const call of symbols.calls) {
404
+ if (call.receiver || call.dynamic)
405
+ continue; // pts post-pass handles only param-flow (non-dynamic)
406
+ const caller = findCaller(lookup, call, symbols.definitions, relPath, fileNodeRow);
407
+ const scopedKey = caller.callerName != null ? `${caller.callerName}::${call.name}` : null;
408
+ if (!scopedKey || !ptsMap.has(scopedKey))
409
+ continue;
410
+ // Only resolve calls that had no direct targets (same guard as buildFileCallEdges).
411
+ const { targets } = resolveCallTargets(lookup, call, relPath, importedNames, typeMap);
412
+ if (targets.length > 0)
413
+ continue;
414
+ for (const alias of resolveViaPointsTo(scopedKey, ptsMap)) {
415
+ const { targets: aliasTargets, importedFrom: aliasFrom } = resolveCallTargets(lookup, { name: alias }, relPath, importedNames, typeMap);
416
+ for (const t of aliasTargets) {
417
+ const edgeKey = `${caller.id}|${t.id}`;
418
+ if (t.id !== caller.id && !seenByPair.has(edgeKey)) {
419
+ const conf = computeConfidence(relPath, t.file, aliasFrom ?? null) - PROPAGATION_HOP_PENALTY;
420
+ if (conf > 0) {
421
+ seenByPair.add(edgeKey);
422
+ allEdgeRows.push([caller.id, t.id, 'calls', conf, 0, 'points-to']);
423
+ }
424
+ }
425
+ }
426
+ }
427
+ }
428
+ }
429
+ }
430
+ /**
431
+ * bind/alias pts post-pass for the native call-edge path.
432
+ *
433
+ * The native Rust engine has no knowledge of JS-layer fnRefBindings (e.g.
434
+ * `const f = fn.bind(ctx)`), so calls to bind-created aliases are not resolved
435
+ * to their original function on the native path. This JS post-pass runs after
436
+ * the native edge pass and adds only the fnRefBindings-seeded pts edges that the
437
+ * native engine missed.
438
+ *
439
+ * Uses the same seenByPair dedup guard as buildParamFlowPtsPostPass to avoid
440
+ * duplicating edges already emitted by the native engine.
441
+ */
442
+ function buildFnRefBindingsPtsPostPass(ctx, getNodeIdStmt, allEdgeRows, sharedLookup) {
443
+ // Only process files that actually have fnRefBindings.
444
+ const filesWithBindings = [...ctx.fileSymbols].filter(([, symbols]) => symbols.fnRefBindings && symbols.fnRefBindings.length > 0);
445
+ if (filesWithBindings.length === 0)
446
+ return;
447
+ // Seed seenByPair from the existing rows so we don't duplicate native edges.
448
+ const seenByPair = new Set();
449
+ for (const [srcId, tgtId] of allEdgeRows) {
450
+ seenByPair.add(`${srcId}|${tgtId}`);
451
+ }
452
+ const { barrelOnlyFiles, rootDir } = ctx;
453
+ const lookup = sharedLookup ?? makeContextLookup(ctx, getNodeIdStmt);
454
+ for (const [relPath, symbols] of filesWithBindings) {
455
+ if (barrelOnlyFiles.has(relPath))
456
+ continue;
457
+ const fileNodeRow = getNodeIdStmt.get(relPath, 'file', relPath, 0);
458
+ if (!fileNodeRow)
459
+ continue;
460
+ const importedNames = buildImportedNamesMap(ctx, relPath, symbols, rootDir);
461
+ const typeMap = symbols.typeMap || new Map();
462
+ const ptsMap = buildPointsToMapForFile(symbols, importedNames);
463
+ if (!ptsMap)
464
+ continue;
465
+ // Only resolve calls whose name is an lhs in fnRefBindings — the same
466
+ // narrowed guard used in buildFileCallEdges case (c).
467
+ const fnRefBindingLhs = new Set(symbols.fnRefBindings.map((b) => b.lhs));
468
+ for (const call of symbols.calls) {
469
+ if (call.receiver || call.dynamic)
470
+ continue; // bind aliases are flat-keyed, never dynamic
471
+ if (!fnRefBindingLhs.has(call.name))
472
+ continue;
473
+ if (!ptsMap.has(call.name))
474
+ continue;
475
+ const caller = findCaller(lookup, call, symbols.definitions, relPath, fileNodeRow);
476
+ // Only resolve calls that had no direct targets (same guard as buildFileCallEdges).
477
+ const { targets } = resolveCallTargets(lookup, call, relPath, importedNames, typeMap);
478
+ if (targets.length > 0)
479
+ continue;
480
+ for (const alias of resolveViaPointsTo(call.name, ptsMap)) {
481
+ const { targets: aliasTargets, importedFrom: aliasFrom } = resolveCallTargets(lookup, { name: alias }, relPath, importedNames, typeMap);
482
+ for (const t of aliasTargets) {
483
+ const edgeKey = `${caller.id}|${t.id}`;
484
+ if (t.id !== caller.id && !seenByPair.has(edgeKey)) {
485
+ const conf = computeConfidence(relPath, t.file, aliasFrom ?? null) - PROPAGATION_HOP_PENALTY;
486
+ if (conf > 0) {
487
+ seenByPair.add(edgeKey);
488
+ allEdgeRows.push([caller.id, t.id, 'calls', conf, 0, 'points-to']);
489
+ }
490
+ }
491
+ }
492
+ }
493
+ }
494
+ }
495
+ }
496
+ /**
497
+ * this-rebinding post-pass for the native call-edge path.
498
+ *
499
+ * When `fn.call(namedCtx, ...)` or `fn.apply(namedCtx, ...)` is extracted by the
500
+ * WASM layer, `thisCallBindings` records `{ callee: 'fn', thisArg: 'namedCtx' }`.
501
+ * The native Rust engine has no knowledge of these bindings, so `this()` calls
502
+ * inside `fn` remain unresolved. This JS post-pass adds the missing edges by
503
+ * resolving `this()` calls inside each `fn` that has a thisCallBinding.
504
+ */
505
+ function buildThisCallBindingsPtsPostPass(ctx, getNodeIdStmt, allEdgeRows, sharedLookup) {
506
+ const filesWithBindings = [...ctx.fileSymbols].filter(([, symbols]) => symbols.thisCallBindings && symbols.thisCallBindings.length > 0);
507
+ if (filesWithBindings.length === 0)
508
+ return;
509
+ const seenByPair = new Set();
510
+ for (const [srcId, tgtId] of allEdgeRows) {
511
+ seenByPair.add(`${srcId}|${tgtId}`);
512
+ }
513
+ const { barrelOnlyFiles, rootDir } = ctx;
514
+ const lookup = sharedLookup ?? makeContextLookup(ctx, getNodeIdStmt);
515
+ for (const [relPath, symbols] of filesWithBindings) {
516
+ if (barrelOnlyFiles.has(relPath))
517
+ continue;
518
+ const fileNodeRow = getNodeIdStmt.get(relPath, 'file', relPath, 0);
519
+ if (!fileNodeRow)
520
+ continue;
521
+ const importedNames = buildImportedNamesMap(ctx, relPath, symbols, rootDir);
522
+ const typeMap = symbols.typeMap || new Map();
523
+ const ptsMap = buildPointsToMapForFile(symbols, importedNames);
524
+ if (!ptsMap)
525
+ continue;
526
+ // Only process calls named 'this' (callee-not-receiver usage)
527
+ for (const call of symbols.calls) {
528
+ if (call.name !== 'this' || call.receiver)
529
+ continue;
530
+ const caller = findCaller(lookup, call, symbols.definitions, relPath, fileNodeRow);
531
+ if (caller.callerName == null)
532
+ continue;
533
+ const scopedKey = `${caller.callerName}::this`;
534
+ if (!ptsMap.has(scopedKey))
535
+ continue;
536
+ for (const alias of resolveViaPointsTo(scopedKey, ptsMap)) {
537
+ const { targets: aliasTargets, importedFrom: aliasFrom } = resolveCallTargets(lookup, { name: alias }, relPath, importedNames, typeMap);
538
+ for (const t of aliasTargets) {
539
+ const edgeKey = `${caller.id}|${t.id}`;
540
+ if (t.id !== caller.id && !seenByPair.has(edgeKey)) {
541
+ const conf = computeConfidence(relPath, t.file, aliasFrom ?? null) - PROPAGATION_HOP_PENALTY;
542
+ if (conf > 0) {
543
+ seenByPair.add(edgeKey);
544
+ allEdgeRows.push([caller.id, t.id, 'calls', conf, 0, 'points-to']);
545
+ }
546
+ }
547
+ }
548
+ }
549
+ }
550
+ }
551
+ }
552
+ /**
553
+ * Phase 8.3f post-pass for the native call-edge path.
554
+ *
555
+ * The native Rust engine builds call edges without knowledge of
556
+ * objectRestParamBindings, so `rest.method()` calls inside functions with
557
+ * object-destructuring rest parameters are not resolved via the typeMap chain.
558
+ * The Rust engine already resolves same-file and directly-imported callees
559
+ * (via steps 1–2 of its resolution logic), so this post-pass only adds edges
560
+ * that require the typeMap-chain path:
561
+ * typeMap[restName] → argName → typeMap[argName.method] → target
562
+ *
563
+ * Mirrors the seeding in buildCallEdgesJS (Phase 8.3f) to ensure both engine
564
+ * paths produce identical results for receiver-typed rest-param calls.
565
+ */
566
+ function buildObjectRestParamPostPass(ctx, getNodeIdStmt, allEdgeRows, sharedLookup) {
567
+ const filesWithRestBindings = [...ctx.fileSymbols].filter(([, symbols]) => symbols.objectRestParamBindings &&
568
+ symbols.objectRestParamBindings.length > 0 &&
569
+ symbols.paramBindings &&
570
+ symbols.paramBindings.length > 0);
571
+ if (filesWithRestBindings.length === 0)
572
+ return;
573
+ const seenByPair = new Set();
574
+ for (const [srcId, tgtId] of allEdgeRows) {
575
+ seenByPair.add(`${srcId}|${tgtId}`);
576
+ }
577
+ const { barrelOnlyFiles, rootDir } = ctx;
578
+ const lookup = sharedLookup ?? makeContextLookup(ctx, getNodeIdStmt);
579
+ for (const [relPath, symbols] of filesWithRestBindings) {
580
+ if (barrelOnlyFiles.has(relPath))
581
+ continue;
582
+ const fileNodeRow = getNodeIdStmt.get(relPath, 'file', relPath, 0);
583
+ if (!fileNodeRow)
584
+ continue;
585
+ const importedNames = buildImportedNamesMap(ctx, relPath, symbols, rootDir);
586
+ const typeMap = new Map(symbols.typeMap instanceof Map ? symbols.typeMap : []);
587
+ // Seed typeMap[callee::restName] = { type: argName } for each matching pair.
588
+ // Mirrors the seeding in buildCallEdgesJS Phase 8.3f. Keys are scoped by
589
+ // callee so two functions with the same rest-param name (e.g. `...rest`) in
590
+ // the same file don't collide (#1358).
591
+ // When only one callee uses a given rest name, also seed the unscoped key
592
+ // as a null-callerName fallback so edges aren't silently dropped if
593
+ // findCaller can't identify the enclosing function (#1358).
594
+ const restNameCallees = new Map();
595
+ for (const orpb of symbols.objectRestParamBindings) {
596
+ if (!restNameCallees.has(orpb.restName))
597
+ restNameCallees.set(orpb.restName, new Set());
598
+ restNameCallees.get(orpb.restName).add(orpb.callee);
599
+ }
600
+ const restNames = new Set();
601
+ for (const orpb of symbols.objectRestParamBindings) {
602
+ for (const pb of symbols.paramBindings) {
603
+ if (pb.callee === orpb.callee && pb.argIndex === orpb.argIndex) {
604
+ const scopedKey = `${orpb.callee}::${orpb.restName}`;
605
+ if (!typeMap.has(scopedKey)) {
606
+ typeMap.set(scopedKey, { type: pb.argName, confidence: 0.65 });
607
+ if (restNameCallees.get(orpb.restName).size === 1 && !typeMap.has(orpb.restName)) {
608
+ typeMap.set(orpb.restName, { type: pb.argName, confidence: 0.65 });
609
+ }
610
+ }
611
+ // restNames tracks every rest-parameter name found, regardless of whether the
612
+ // scoped key was already in typeMap. This ensures the post-pass (below) processes
613
+ // all calls whose receiver matches a known rest binding — not just those whose
614
+ // typeMap entry was seeded in this iteration.
615
+ restNames.add(orpb.restName);
616
+ }
617
+ }
618
+ }
619
+ if (restNames.size === 0)
620
+ continue;
621
+ for (const call of symbols.calls) {
622
+ // Only process calls whose receiver is a known rest-binding name.
623
+ if (!call.receiver || !restNames.has(call.receiver))
624
+ continue;
625
+ const caller = findCaller(lookup, call, symbols.definitions, relPath, fileNodeRow);
626
+ // Resolve with the enriched typeMap. callerName is passed so
627
+ // resolveByMethodOrGlobal can look up the scoped key callee::restName (#1358).
628
+ // seenByPair deduplicates edges the native engine already emitted.
629
+ const { targets, importedFrom } = resolveCallTargets(lookup, call, relPath, importedNames, typeMap, caller.callerName);
630
+ for (const t of targets) {
631
+ const edgeKey = `${caller.id}|${t.id}`;
632
+ if (t.id !== caller.id && !seenByPair.has(edgeKey)) {
633
+ const conf = computeConfidence(relPath, t.file, importedFrom ?? null) - PROPAGATION_HOP_PENALTY;
634
+ if (conf > 0) {
635
+ seenByPair.add(edgeKey);
636
+ allEdgeRows.push([caller.id, t.id, 'calls', conf, 0, 'points-to']);
637
+ }
638
+ }
639
+ }
640
+ }
641
+ }
642
+ }
643
+ /**
644
+ * Object.defineProperty accessor post-pass for the native call-edge path.
645
+ *
646
+ * When a function is registered as a getter/setter via
647
+ * `Object.defineProperty(obj, "bar", { get: getter })`, calls to `this.X()`
648
+ * inside `getter` need to resolve against `obj` (because `this === obj` when
649
+ * the accessor is invoked). The native Rust engine has no knowledge of
650
+ * `definePropertyReceivers`, so this JS post-pass adds the missing edges.
651
+ */
652
+ function buildDefinePropertyPostPass(ctx, getNodeIdStmt, allEdgeRows, sharedLookup) {
653
+ const filesWithReceivers = [...ctx.fileSymbols].filter(([, symbols]) => symbols.definePropertyReceivers && symbols.definePropertyReceivers.size > 0);
654
+ if (filesWithReceivers.length === 0)
655
+ return;
656
+ const seenByPair = new Set();
657
+ for (const [srcId, tgtId] of allEdgeRows) {
658
+ seenByPair.add(`${srcId}|${tgtId}`);
659
+ }
660
+ const { barrelOnlyFiles, rootDir } = ctx;
661
+ const lookup = sharedLookup ?? makeContextLookup(ctx, getNodeIdStmt);
662
+ for (const [relPath, symbols] of filesWithReceivers) {
663
+ if (barrelOnlyFiles.has(relPath))
664
+ continue;
665
+ const fileNodeRow = getNodeIdStmt.get(relPath, 'file', relPath, 0);
666
+ if (!fileNodeRow)
667
+ continue;
668
+ const importedNames = buildImportedNamesMap(ctx, relPath, symbols, rootDir);
669
+ const typeMap = symbols.typeMap || new Map();
670
+ const definePropertyReceivers = symbols.definePropertyReceivers;
671
+ for (const call of symbols.calls) {
672
+ if (call.receiver !== 'this')
673
+ continue;
674
+ const caller = findCaller(lookup, call, symbols.definitions, relPath, fileNodeRow);
675
+ if (!caller.callerName)
676
+ continue;
677
+ const receiverVarName = definePropertyReceivers.get(caller.callerName);
678
+ if (!receiverVarName)
679
+ continue;
680
+ // Only add edges the native engine missed (no direct target already).
681
+ const { targets: directTargets } = resolveCallTargets(lookup, call, relPath, importedNames, typeMap, caller.callerName);
682
+ if (directTargets.length > 0)
683
+ continue;
684
+ // Resolve via receiver type
685
+ let targets = [];
686
+ const typeEntry = typeMap.get(receiverVarName);
687
+ const typeName = typeEntry
688
+ ? typeof typeEntry === 'string'
689
+ ? typeEntry
690
+ : typeEntry.type
691
+ : null;
692
+ if (typeName) {
693
+ const qualifiedName = `${typeName}.${call.name}`;
694
+ targets = lookup.byNameAndFile(qualifiedName, relPath);
695
+ }
696
+ // Same-file fallback for plain object-literal methods
697
+ if (targets.length === 0) {
698
+ targets = lookup.byNameAndFile(call.name, relPath);
699
+ }
700
+ for (const t of targets) {
701
+ const edgeKey = `${caller.id}|${t.id}`;
702
+ if (t.id !== caller.id && !seenByPair.has(edgeKey)) {
703
+ const conf = computeConfidence(relPath, t.file, null);
704
+ if (conf > 0) {
705
+ seenByPair.add(edgeKey);
706
+ allEdgeRows.push([caller.id, t.id, 'calls', conf, 0, 'ts-native']);
707
+ }
708
+ }
709
+ }
710
+ }
711
+ }
712
+ }
713
+ /**
714
+ * Phase 8.5: CHA + RTA post-pass for the native call-edge path.
715
+ *
716
+ * The native Rust engine has no knowledge of the CHA context, so `this.method()`
717
+ * calls and interface method dispatches are not expanded to their concrete
718
+ * implementations. This JS post-pass runs after the native edges (and the pts
719
+ * post-pass) and adds only the CHA-resolved edges that the native engine missed.
720
+ *
721
+ * Like buildParamFlowPtsPostPass, it seeds seenByPair from the current allEdgeRows
722
+ * snapshot to avoid duplicating edges the native engine already produced.
723
+ */
724
+ function buildChaPostPass(ctx, getNodeIdStmt, allEdgeRows, chaCtx) {
725
+ // Fast-exit when the CHA context is empty (no class hierarchy in the project)
726
+ if (chaCtx.implementors.size === 0 && chaCtx.parents.size === 0)
727
+ return;
728
+ // Seed only from 'calls' edges — import/extends/implements edges share (src,tgt) pairs
729
+ // with real call edges at the file-node level and would cause false dedup if included.
730
+ const seenByPair = new Set();
731
+ for (const row of allEdgeRows) {
732
+ if (row[2] === 'calls')
733
+ seenByPair.add(`${row[0]}|${row[1]}`);
734
+ }
735
+ const { fileSymbols, barrelOnlyFiles } = ctx;
736
+ const lookup = makeContextLookup(ctx, getNodeIdStmt);
737
+ for (const [relPath, symbols] of fileSymbols) {
738
+ if (barrelOnlyFiles.has(relPath))
739
+ continue;
740
+ const fileNodeRow = getNodeIdStmt.get(relPath, 'file', relPath, 0);
741
+ if (!fileNodeRow)
742
+ continue;
743
+ const typeMap = symbols.typeMap || new Map();
744
+ for (const call of symbols.calls) {
745
+ if (!call.receiver)
746
+ continue;
747
+ if (BUILTIN_RECEIVERS.has(call.receiver))
748
+ continue;
749
+ const caller = findCaller(lookup, call, symbols.definitions, relPath, fileNodeRow);
750
+ let chaTargets = [];
751
+ if (call.receiver === 'this' || call.receiver === 'self' || call.receiver === 'super') {
752
+ chaTargets = resolveThisDispatch(call.name, caller.callerName, call.receiver, chaCtx, lookup);
753
+ }
754
+ else {
755
+ const typeEntry = typeMap.get(call.receiver);
756
+ const typeName = typeEntry
757
+ ? typeof typeEntry === 'string'
758
+ ? typeEntry
759
+ : typeEntry.type
760
+ : null;
761
+ if (typeName) {
762
+ chaTargets = resolveChaTargets(typeName, call.name, chaCtx, lookup);
763
+ }
764
+ }
765
+ for (const t of chaTargets) {
766
+ const edgeKey = `${caller.id}|${t.id}`;
767
+ if (t.id !== caller.id && !seenByPair.has(edgeKey)) {
768
+ const conf = computeConfidence(relPath, t.file, null) - CHA_DISPATCH_PENALTY;
769
+ if (conf > 0) {
770
+ seenByPair.add(edgeKey);
771
+ allEdgeRows.push([caller.id, t.id, 'calls', conf, 0, 'cha']);
772
+ }
773
+ }
774
+ }
775
+ }
290
776
  }
291
777
  }
292
778
  function buildImportedNamesForNative(ctx, relPath, symbols, rootDir) {
@@ -300,7 +786,7 @@ function buildImportedNamesForNative(ctx, relPath, symbols, rootDir) {
300
786
  const cleanName = name.replace(/^\*\s+as\s+/, '');
301
787
  let targetFile = resolvedPath;
302
788
  if (isBarrelFile(ctx, resolvedPath)) {
303
- const actual = resolveBarrelExport(ctx, resolvedPath, cleanName);
789
+ const actual = resolveBarrelExportCached(ctx, resolvedPath, cleanName);
304
790
  if (actual)
305
791
  targetFile = actual;
306
792
  }
@@ -318,8 +804,9 @@ function buildImportedNamesForNative(ctx, relPath, symbols, rootDir) {
318
804
  return importedNames;
319
805
  }
320
806
  // ── Call edges (JS fallback) ────────────────────────────────────────────
321
- function buildCallEdgesJS(ctx, getNodeIdStmt, allEdgeRows) {
807
+ function buildCallEdgesJS(ctx, getNodeIdStmt, allEdgeRows, chaCtx) {
322
808
  const { fileSymbols, barrelOnlyFiles, rootDir } = ctx;
809
+ const lookup = makeContextLookup(ctx, getNodeIdStmt);
323
810
  for (const [relPath, symbols] of fileSymbols) {
324
811
  if (barrelOnlyFiles.has(relPath))
325
812
  continue;
@@ -327,9 +814,36 @@ function buildCallEdgesJS(ctx, getNodeIdStmt, allEdgeRows) {
327
814
  if (!fileNodeRow)
328
815
  continue;
329
816
  const importedNames = buildImportedNamesMap(ctx, relPath, symbols, rootDir);
330
- const typeMap = symbols.typeMap || new Map();
817
+ const typeMap = new Map(symbols.typeMap instanceof Map ? symbols.typeMap : []);
818
+ // Phase 8.3f: seed typeMap[callee::restName] = { type: argName } for each
819
+ // object-destructuring rest parameter binding × call-site argument binding.
820
+ // Keys are scoped so two functions with the same rest-param name in the same
821
+ // file don't collide (#1358). When only one callee uses a given rest name,
822
+ // also seed the unscoped key as a null-callerName fallback.
823
+ if (symbols.objectRestParamBindings?.length && symbols.paramBindings?.length) {
824
+ const restNameCallees = new Map();
825
+ for (const orpb of symbols.objectRestParamBindings) {
826
+ if (!restNameCallees.has(orpb.restName))
827
+ restNameCallees.set(orpb.restName, new Set());
828
+ restNameCallees.get(orpb.restName).add(orpb.callee);
829
+ }
830
+ for (const orpb of symbols.objectRestParamBindings) {
831
+ for (const pb of symbols.paramBindings) {
832
+ if (pb.callee === orpb.callee && pb.argIndex === orpb.argIndex) {
833
+ const scopedKey = `${orpb.callee}::${orpb.restName}`;
834
+ if (!typeMap.has(scopedKey)) {
835
+ typeMap.set(scopedKey, { type: pb.argName, confidence: 0.65 });
836
+ if (restNameCallees.get(orpb.restName).size === 1 && !typeMap.has(orpb.restName)) {
837
+ typeMap.set(orpb.restName, { type: pb.argName, confidence: 0.65 });
838
+ }
839
+ }
840
+ }
841
+ }
842
+ }
843
+ }
331
844
  const seenCallEdges = new Set();
332
- buildFileCallEdges(ctx, relPath, symbols, fileNodeRow, importedNames, seenCallEdges, getNodeIdStmt, allEdgeRows, typeMap);
845
+ const ptsMap = buildPointsToMapForFile(symbols, importedNames);
846
+ buildFileCallEdges(relPath, symbols, fileNodeRow, importedNames, seenCallEdges, lookup, allEdgeRows, typeMap, ptsMap, chaCtx);
333
847
  buildClassHierarchyEdges(ctx, relPath, symbols, allEdgeRows);
334
848
  }
335
849
  }
@@ -339,12 +853,23 @@ function buildImportedNamesMap(ctx, relPath, symbols, rootDir) {
339
853
  // (higher priority). Static imports represent direct bindings while dynamic
340
854
  // imports often use aliased destructuring (`{ foo: bar } = await import(…)`).
341
855
  // When both contribute the same name, the static binding is authoritative.
856
+ //
857
+ // Phase 8.4: trace through barrel files so that symbol names map to their
858
+ // actual definition file, not the re-exporting barrel. Mirrors the tracing
859
+ // already done in buildImportedNamesForNative (the native path).
860
+ const traceBarrel = (resolvedPath, cleanName) => {
861
+ if (!isBarrelFile(ctx, resolvedPath))
862
+ return resolvedPath;
863
+ const actual = resolveBarrelExportCached(ctx, resolvedPath, cleanName);
864
+ return actual ?? resolvedPath;
865
+ };
342
866
  for (const imp of symbols.imports) {
343
867
  if (!imp.dynamicImport)
344
868
  continue;
345
869
  const resolvedPath = getResolved(ctx, path.join(rootDir, relPath), imp.source);
346
870
  for (const name of imp.names) {
347
- importedNames.set(name.replace(/^\*\s+as\s+/, ''), resolvedPath);
871
+ const cleanName = name.replace(/^\*\s+as\s+/, '');
872
+ importedNames.set(cleanName, traceBarrel(resolvedPath, cleanName));
348
873
  }
349
874
  }
350
875
  for (const imp of symbols.imports) {
@@ -353,124 +878,333 @@ function buildImportedNamesMap(ctx, relPath, symbols, rootDir) {
353
878
  const resolvedPath = getResolved(ctx, path.join(rootDir, relPath), imp.source);
354
879
  for (const name of imp.names) {
355
880
  const cleanName = name.replace(/^\*\s+as\s+/, '');
356
- importedNames.set(cleanName, resolvedPath);
881
+ importedNames.set(cleanName, traceBarrel(resolvedPath, cleanName));
357
882
  }
358
883
  }
359
884
  return importedNames;
360
885
  }
361
- function findCaller(call, definitions, relPath, getNodeIdStmt, fileNodeRow) {
362
- let caller = null;
363
- let callerSpan = Infinity;
364
- for (const def of definitions) {
365
- if (def.line <= call.line) {
366
- const end = def.endLine || Infinity;
367
- if (call.line <= end) {
368
- const span = end - def.line;
369
- if (span < callerSpan) {
370
- const row = getNodeIdStmt.get(def.name, def.kind, relPath, def.line);
371
- if (row) {
372
- caller = row;
373
- callerSpan = span;
374
- }
375
- }
376
- }
377
- }
378
- }
379
- return caller || fileNodeRow;
886
+ function makeContextLookup(ctx, getNodeIdStmt) {
887
+ return {
888
+ byNameAndFile: (name, file) => ctx.nodesByNameAndFile.get(`${name}|${file}`) ?? [],
889
+ byName: (name) => ctx.nodesByName.get(name) ?? [],
890
+ isBarrel: (file) => isBarrelFile(ctx, file),
891
+ resolveBarrel: (barrelFile, symbolName) => resolveBarrelExportCached(ctx, barrelFile, symbolName),
892
+ nodeId: (name, kind, file, line) => getNodeIdStmt.get(name, kind, file, line),
893
+ };
380
894
  }
381
- function resolveCallTargets(ctx, call, relPath, importedNames, typeMap) {
382
- const importedFrom = importedNames.get(call.name);
383
- let targets;
384
- if (importedFrom) {
385
- targets = ctx.nodesByNameAndFile.get(`${call.name}|${importedFrom}`) || [];
386
- if (targets.length === 0 && isBarrelFile(ctx, importedFrom)) {
387
- const actualSource = resolveBarrelExport(ctx, importedFrom, call.name);
388
- if (actualSource) {
389
- targets = ctx.nodesByNameAndFile.get(`${call.name}|${actualSource}`) || [];
390
- }
391
- }
392
- }
393
- if (!targets || targets.length === 0) {
394
- targets = ctx.nodesByNameAndFile.get(`${call.name}|${relPath}`) || [];
395
- if (targets.length === 0) {
396
- targets = resolveByMethodOrGlobal(ctx, call, relPath, typeMap);
397
- }
398
- }
399
- if (targets.length > 1) {
400
- targets.sort((a, b) => {
401
- const confA = computeConfidence(relPath, a.file, importedFrom ?? null);
402
- const confB = computeConfidence(relPath, b.file, importedFrom ?? null);
403
- return confB - confA;
404
- });
895
+ /**
896
+ * Build a per-file points-to map for Phase 8.3 alias resolution.
897
+ * Returns null fast when the file has no function-reference bindings.
898
+ *
899
+ * Only callable definitions (function/method) are seeded as concrete targets.
900
+ * Class and interface names are intentionally excluded — aliasing a constructor
901
+ * (`const Svc = MyService`) is an uncommon pattern that would require tracking
902
+ * `new`-expression flows separately from the alias chain. That is left to Phase
903
+ * 8.2 call-assignment propagation, which already handles constructor assignments.
904
+ */
905
+ function buildPointsToMapForFile(symbols, importedNames) {
906
+ const hasThisCallBindings = !!symbols.thisCallBindings?.length;
907
+ if (!symbols.fnRefBindings?.length &&
908
+ !symbols.paramBindings?.length &&
909
+ !symbols.arrayElemBindings?.length &&
910
+ !symbols.spreadArgBindings?.length &&
911
+ !symbols.forOfBindings?.length &&
912
+ !symbols.arrayCallbackBindings?.length &&
913
+ !symbols.objectRestParamBindings?.length &&
914
+ !symbols.objectPropBindings?.length &&
915
+ !hasThisCallBindings)
916
+ return null;
917
+ const defNames = new Set(symbols.definitions
918
+ .filter((d) => d.kind === 'function' || d.kind === 'method')
919
+ .map((d) => d.name));
920
+ const definitionParams = buildDefinitionParamsMap(symbols.definitions);
921
+ // Convert thisCallBindings into scoped fnRefBindings: `fn::this → namedCtx`.
922
+ // The scoped key `fn::this` is looked up when `this()` calls are resolved inside
923
+ // function `fn` — caller.callerName='fn', call.name='this' → scopedPtsKey='fn::this'.
924
+ let allFnRefBindings = symbols.fnRefBindings ?? [];
925
+ if (hasThisCallBindings) {
926
+ const extra = (symbols.thisCallBindings ?? []).map((b) => ({
927
+ lhs: `${b.callee}::this`,
928
+ rhs: b.thisArg,
929
+ }));
930
+ allFnRefBindings = [...allFnRefBindings, ...extra];
405
931
  }
406
- return { targets, importedFrom };
932
+ return buildPointsToMap(allFnRefBindings, defNames, importedNames, symbols.paramBindings, definitionParams, symbols.arrayElemBindings, symbols.spreadArgBindings, symbols.forOfBindings, symbols.arrayCallbackBindings, symbols.objectRestParamBindings, symbols.objectPropBindings);
407
933
  }
408
- function resolveByMethodOrGlobal(ctx, call, relPath, typeMap) {
409
- // Type-aware resolution: translate variable receiver to its declared type
410
- if (call.receiver && typeMap) {
411
- const typeEntry = typeMap.get(call.receiver);
412
- const typeName = typeEntry
413
- ? typeof typeEntry === 'string'
414
- ? typeEntry
415
- : typeEntry.type
416
- : null;
417
- if (typeName) {
418
- const qualifiedName = `${typeName}.${call.name}`;
419
- const typed = (ctx.nodesByName.get(qualifiedName) || []).filter((n) => n.kind === 'method');
420
- if (typed.length > 0)
421
- return typed;
934
+ function buildDefinitionParamsMap(definitions) {
935
+ const map = new Map();
936
+ for (const def of definitions) {
937
+ if ((def.kind === 'function' || def.kind === 'method') && def.children) {
938
+ const params = def.children.filter((c) => c.kind === 'parameter').map((c) => c.name);
939
+ if (params.length > 0) {
940
+ if (map.has(def.name)) {
941
+ // Two definitions share the same name (e.g. overloads, same-named method and
942
+ // function, or conditional redeclaration). Keep the first entry — using the
943
+ // wrong parameter list would map argIndex to the wrong parameter name.
944
+ debug(`buildDefinitionParamsMap: duplicate def name "${def.name}" (kind=${def.kind}, line=${def.line}) — skipping; first entry kept`);
945
+ }
946
+ else {
947
+ map.set(def.name, params);
948
+ }
949
+ }
422
950
  }
423
951
  }
424
- if (!call.receiver ||
425
- call.receiver === 'this' ||
426
- call.receiver === 'self' ||
427
- call.receiver === 'super') {
428
- return (ctx.nodesByName.get(call.name) || []).filter((n) => computeConfidence(relPath, n.file, null) >= 0.5);
429
- }
430
- return [];
952
+ return map;
431
953
  }
432
- function buildFileCallEdges(ctx, relPath, symbols, fileNodeRow, importedNames, seenCallEdges, getNodeIdStmt, allEdgeRows, typeMap) {
954
+ function buildFileCallEdges(relPath, symbols, fileNodeRow, importedNames, seenCallEdges, lookup, allEdgeRows, typeMap, ptsMap, chaCtx) {
955
+ // Tracks edges that were inserted by the pts fallback (edgeKey → allEdgeRows index).
956
+ // Kept separate from seenCallEdges so that a subsequent direct-call edge for the same
957
+ // caller→target pair can upgrade the confidence in-place rather than being silently
958
+ // dropped by the dedup guard. Once upgraded, the key moves to seenCallEdges and is
959
+ // no longer tracked here.
960
+ const ptsEdgeRows = new Map();
961
+ // Pre-compute the set of names that appear as lhs in fnRefBindings so that
962
+ // case (c) of the pts gate below only fires for names that are genuine
963
+ // bind/alias entries, not for every locally-defined function or import that
964
+ // buildPointsToMap seeds with a self-pointing entry.
965
+ const fnRefBindingLhs = new Set(symbols.fnRefBindings?.map((b) => b.lhs) ?? []);
433
966
  for (const call of symbols.calls) {
434
967
  if (call.receiver && BUILTIN_RECEIVERS.has(call.receiver))
435
968
  continue;
436
- const caller = findCaller(call, symbols.definitions, relPath, getNodeIdStmt, fileNodeRow);
969
+ const caller = findCaller(lookup, call, symbols.definitions, relPath, fileNodeRow);
437
970
  const isDynamic = call.dynamic ? 1 : 0;
438
- const { targets, importedFrom } = resolveCallTargets(ctx, call, relPath, importedNames, typeMap);
971
+ let { targets, importedFrom } = resolveCallTargets(lookup, call, relPath, importedNames, typeMap, caller.callerName);
972
+ // Same-class `this.method()` fallback: when the call receiver is `this` and
973
+ // resolveCallTargets found nothing, derive the enclosing class name from the
974
+ // caller (e.g. `Logger.info` → class prefix `Logger`) and retry with the
975
+ // qualified method name `Logger._write`. This mirrors what the native Rust
976
+ // engine does implicitly via its class-scoped symbol table.
977
+ // NOTE: restricted to `this` only — `super.method()` targets a parent class,
978
+ // not the enclosing class, so qualifying with the child class name would
979
+ // produce a false edge when the child also defines a same-named method.
980
+ if (targets.length === 0 && call.receiver === 'this' && caller.callerName != null) {
981
+ const lastDot = caller.callerName.lastIndexOf('.');
982
+ if (lastDot > 0) {
983
+ const prevDot = caller.callerName.lastIndexOf('.', lastDot - 1);
984
+ const className = caller.callerName.slice(prevDot + 1, lastDot);
985
+ const qualifiedName = `${className}.${call.name}`;
986
+ const qualified = lookup
987
+ .byNameAndFile(qualifiedName, relPath)
988
+ .filter((n) => n.kind === 'method');
989
+ if (qualified.length > 0) {
990
+ targets = qualified;
991
+ }
992
+ }
993
+ }
994
+ // Same-class bare-call fallback: when a no-receiver call can't be resolved
995
+ // globally, try the caller's own class as a qualifier. Handles C# static
996
+ // sibling calls: `IsValidEmail()` inside `Validators.ValidateUser` resolves
997
+ // to `Validators.IsValidEmail`. Skipped for JS/TS where bare calls are
998
+ // module-scoped, not class-scoped.
999
+ if (targets.length === 0 &&
1000
+ !call.receiver &&
1001
+ caller.callerName != null &&
1002
+ !isModuleScopedLanguage(relPath)) {
1003
+ const lastDot = caller.callerName.lastIndexOf('.');
1004
+ if (lastDot > 0) {
1005
+ const prevDot = caller.callerName.lastIndexOf('.', lastDot - 1);
1006
+ const className = caller.callerName.slice(prevDot + 1, lastDot);
1007
+ const qualifiedName = `${className}.${call.name}`;
1008
+ const qualified = lookup
1009
+ .byNameAndFile(qualifiedName, relPath)
1010
+ .filter((n) => n.kind === 'method');
1011
+ if (qualified.length > 0) {
1012
+ targets = qualified;
1013
+ }
1014
+ }
1015
+ }
1016
+ // Object.defineProperty accessor fallback: when a function is registered as
1017
+ // a getter/setter via `Object.defineProperty(obj, "bar", { get: getter })`,
1018
+ // calls to `this.X()` inside `getter` resolve against `obj` (this === obj
1019
+ // when the accessor is invoked). If the same-class fallback above found
1020
+ // nothing, try treating `obj` as the receiver and look up `obj.X` in the
1021
+ // typeMap, or fall back to a same-file lookup of any definition named X
1022
+ // that belongs to the object literal or its type.
1023
+ if (targets.length === 0 &&
1024
+ call.receiver === 'this' &&
1025
+ caller.callerName != null &&
1026
+ symbols.definePropertyReceivers) {
1027
+ const receiverVarName = symbols.definePropertyReceivers.get(caller.callerName);
1028
+ if (receiverVarName) {
1029
+ // Try typeMap lookup for receiver.methodName
1030
+ const typeEntry = typeMap.get(receiverVarName);
1031
+ const typeName = typeEntry
1032
+ ? typeof typeEntry === 'string'
1033
+ ? typeEntry
1034
+ : typeEntry.type
1035
+ : null;
1036
+ if (typeName) {
1037
+ const qualifiedName = `${typeName}.${call.name}`;
1038
+ const qualified = lookup.byNameAndFile(qualifiedName, relPath);
1039
+ if (qualified.length > 0) {
1040
+ targets = [...qualified];
1041
+ }
1042
+ }
1043
+ // If still no targets, search for any definition named `call.name` in
1044
+ // the same file — handles plain object literals where the method isn't
1045
+ // qualified (e.g. `const obj = { baz() {} }` defines `baz` directly).
1046
+ // Note: this is intentionally broad — it matches any same-file definition
1047
+ // with the called name, not just members of the receiver object. This is
1048
+ // the same behaviour used by the native post-pass path (buildDefinePropertyPostPass).
1049
+ if (targets.length === 0) {
1050
+ const sameFile = lookup.byNameAndFile(call.name, relPath);
1051
+ if (sameFile.length > 0) {
1052
+ targets = [...sameFile];
1053
+ }
1054
+ }
1055
+ }
1056
+ }
439
1057
  for (const t of targets) {
440
1058
  const edgeKey = `${caller.id}|${t.id}`;
441
- if (t.id !== caller.id && !seenCallEdges.has(edgeKey)) {
442
- seenCallEdges.add(edgeKey);
1059
+ if (t.id !== caller.id) {
443
1060
  const confidence = computeConfidence(relPath, t.file, importedFrom ?? null);
444
- allEdgeRows.push([caller.id, t.id, 'calls', confidence, isDynamic]);
1061
+ if (seenCallEdges.has(edgeKey))
1062
+ continue;
1063
+ const ptsIdx = ptsEdgeRows.get(edgeKey);
1064
+ if (ptsIdx !== undefined) {
1065
+ // A pts-resolved edge already exists for this caller→target pair with a
1066
+ // penalised confidence. Upgrade it to the direct-call confidence in-place,
1067
+ // then promote to seenCallEdges so no further processing is needed.
1068
+ const ptsRow = allEdgeRows[ptsIdx];
1069
+ if (ptsRow) {
1070
+ ptsRow[3] = confidence;
1071
+ ptsRow[4] = isDynamic; // upgrade is_dynamic: direct call overrides the pts-alias dynamic flag
1072
+ ptsRow[5] = 'ts-native'; // promoted from pts to direct-call resolution
1073
+ }
1074
+ ptsEdgeRows.delete(edgeKey);
1075
+ seenCallEdges.add(edgeKey);
1076
+ }
1077
+ else {
1078
+ seenCallEdges.add(edgeKey);
1079
+ allEdgeRows.push([caller.id, t.id, 'calls', confidence, isDynamic, 'ts-native']);
1080
+ }
1081
+ }
1082
+ }
1083
+ // Phase 8.3 / 8.3c / bind: points-to fallback for unresolved calls.
1084
+ // Fires for three cases:
1085
+ // (a) dynamic=true: alias calls emitted by extractCallbackReferenceCalls.
1086
+ // Looks up `call.name` directly (alias entries are flat-keyed).
1087
+ // (b) non-dynamic: parameter variable calls (fn() where fn is a param).
1088
+ // Looks up the scoped key `callerName::call.name` to avoid spurious
1089
+ // edges from same-named parameters across different functions.
1090
+ // (c) non-dynamic: module-level alias bindings — `f = fn.bind(ctx)` or
1091
+ // `const f = handler` — where pts('f') was seeded by fnRefBindings.
1092
+ // Checked against fnRefBindingLhs (the pre-computed set of lhs names from
1093
+ // fnRefBindings) rather than the full ptsMap, so case (c) only fires for
1094
+ // genuine bind/alias entries and never for self-seeded local definitions.
1095
+ // Confidence is penalised by one hop to reflect the extra indirection.
1096
+ //
1097
+ // Note: pts edges are added to ptsEdgeRows (not seenCallEdges) so that a later
1098
+ // direct call to the same target in the same function body can upgrade confidence
1099
+ // rather than being silently dropped by the dedup guard.
1100
+ const scopedPtsKey = caller.callerName != null ? `${caller.callerName}::${call.name}` : null;
1101
+ // Module-level calls (callerName === null) use the '<module>' sentinel emitted by
1102
+ // extractSpreadForOfWalk for top-level for-of loops. Look it up as a fallback so
1103
+ // that `for (const f of arr) { f(); }` at module scope resolves correctly.
1104
+ const modulePtsKey = caller.callerName === null && ptsMap?.has(`<module>::${call.name}`)
1105
+ ? `<module>::${call.name}`
1106
+ : null;
1107
+ const flatPtsKey = !call.dynamic && fnRefBindingLhs.has(call.name) && ptsMap?.has(call.name) ? call.name : null;
1108
+ if (targets.length === 0 &&
1109
+ !call.receiver &&
1110
+ ptsMap &&
1111
+ (call.dynamic ||
1112
+ (scopedPtsKey != null && ptsMap.has(scopedPtsKey)) ||
1113
+ modulePtsKey != null ||
1114
+ flatPtsKey != null)) {
1115
+ const ptsLookupName = call.dynamic
1116
+ ? call.name
1117
+ : scopedPtsKey != null && ptsMap.has(scopedPtsKey)
1118
+ ? scopedPtsKey
1119
+ : modulePtsKey != null
1120
+ ? modulePtsKey
1121
+ : // flatPtsKey != null is guaranteed by the outer if condition: if neither
1122
+ // call.dynamic nor scopedPtsKey nor modulePtsKey matched, flatPtsKey must be non-null.
1123
+ flatPtsKey;
1124
+ for (const alias of resolveViaPointsTo(ptsLookupName, ptsMap)) {
1125
+ // Resolve the concrete alias target. Only `name` is needed here — receiver
1126
+ // and line are not relevant for alias resolution (we are looking up the
1127
+ // aliased function by name, not dispatching a method call).
1128
+ const { targets: aliasTargets, importedFrom: aliasFrom } = resolveCallTargets(lookup, { name: alias }, relPath, importedNames, typeMap);
1129
+ for (const t of aliasTargets) {
1130
+ const edgeKey = `${caller.id}|${t.id}`;
1131
+ if (t.id !== caller.id && !seenCallEdges.has(edgeKey) && !ptsEdgeRows.has(edgeKey)) {
1132
+ const conf = computeConfidence(relPath, t.file, aliasFrom ?? null) - PROPAGATION_HOP_PENALTY;
1133
+ if (conf > 0) {
1134
+ ptsEdgeRows.set(edgeKey, allEdgeRows.length);
1135
+ allEdgeRows.push([caller.id, t.id, 'calls', conf, isDynamic, 'points-to']);
1136
+ }
1137
+ }
1138
+ }
1139
+ }
1140
+ }
1141
+ // Phase 8.3f: pts fallback for receiver calls via object-rest param bindings.
1142
+ // Fires when `rest.prop()` is encountered and `rest` was seeded as `pts["rest.prop"]`
1143
+ // by the object-rest dispatch chain (ObjectRestParamBinding + paramBinding + ObjectPropBinding).
1144
+ if (targets.length === 0 &&
1145
+ call.receiver &&
1146
+ !BUILTIN_RECEIVERS.has(call.receiver) &&
1147
+ call.receiver !== 'this' &&
1148
+ call.receiver !== 'self' &&
1149
+ call.receiver !== 'super' &&
1150
+ ptsMap) {
1151
+ const receiverKey = `${call.receiver}.${call.name}`;
1152
+ if (ptsMap.has(receiverKey)) {
1153
+ for (const alias of resolveViaPointsTo(receiverKey, ptsMap)) {
1154
+ const { targets: aliasTargets, importedFrom: aliasFrom } = resolveCallTargets(lookup, { name: alias }, relPath, importedNames, typeMap);
1155
+ for (const t of aliasTargets) {
1156
+ const edgeKey = `${caller.id}|${t.id}`;
1157
+ if (t.id !== caller.id && !seenCallEdges.has(edgeKey) && !ptsEdgeRows.has(edgeKey)) {
1158
+ const conf = computeConfidence(relPath, t.file, aliasFrom ?? null) - PROPAGATION_HOP_PENALTY;
1159
+ if (conf > 0) {
1160
+ ptsEdgeRows.set(edgeKey, allEdgeRows.length);
1161
+ allEdgeRows.push([caller.id, t.id, 'calls', conf, isDynamic, 'points-to']);
1162
+ }
1163
+ }
1164
+ }
1165
+ }
445
1166
  }
446
1167
  }
447
- // Receiver edge
448
1168
  if (call.receiver &&
449
1169
  !BUILTIN_RECEIVERS.has(call.receiver) &&
450
1170
  call.receiver !== 'this' &&
451
1171
  call.receiver !== 'self' &&
452
1172
  call.receiver !== 'super') {
453
- buildReceiverEdge(ctx, call, caller, relPath, seenCallEdges, allEdgeRows, typeMap);
1173
+ const recv = resolveReceiverEdge(lookup, { name: call.name, receiver: call.receiver }, caller, relPath, typeMap, seenCallEdges);
1174
+ if (recv) {
1175
+ allEdgeRows.push([recv.callerId, recv.receiverId, 'receiver', recv.confidence, 0, null]);
1176
+ }
454
1177
  }
455
- }
456
- }
457
- function buildReceiverEdge(ctx, call, caller, relPath, seenCallEdges, allEdgeRows, typeMap) {
458
- const receiverKinds = new Set(['class', 'struct', 'interface', 'type', 'module']);
459
- const typeEntry = typeMap?.get(call.receiver);
460
- const typeName = typeEntry ? (typeof typeEntry === 'string' ? typeEntry : typeEntry.type) : null;
461
- const typeConfidence = typeEntry && typeof typeEntry === 'object' ? typeEntry.confidence : null;
462
- const effectiveReceiver = typeName || call.receiver;
463
- const samefile = ctx.nodesByNameAndFile.get(`${effectiveReceiver}|${relPath}`) || [];
464
- const candidates = samefile.length > 0 ? samefile : ctx.nodesByName.get(effectiveReceiver) || [];
465
- const receiverNodes = candidates.filter((n) => receiverKinds.has(n.kind));
466
- if (receiverNodes.length > 0 && caller) {
467
- const recvTarget = receiverNodes[0];
468
- const recvKey = `recv|${caller.id}|${recvTarget.id}`;
469
- if (!seenCallEdges.has(recvKey)) {
470
- seenCallEdges.add(recvKey);
471
- // Use type source confidence when available, otherwise 0.7 for untyped receiver
472
- const confidence = typeConfidence ?? (typeName ? 0.9 : 0.7);
473
- allEdgeRows.push([caller.id, recvTarget.id, 'receiver', confidence, 0]);
1178
+ // Phase 8.5: CHA + RTA dispatch expansion.
1179
+ // For `this`/`self`/`super` calls: resolve through the class hierarchy instead
1180
+ // of relying solely on global name matching.
1181
+ // For typed receiver calls: expand to all instantiated concrete implementations.
1182
+ if (chaCtx && call.receiver) {
1183
+ let chaTargets = [];
1184
+ if (call.receiver === 'this' || call.receiver === 'self' || call.receiver === 'super') {
1185
+ chaTargets = resolveThisDispatch(call.name, caller.callerName, call.receiver, chaCtx, lookup);
1186
+ }
1187
+ else if (!BUILTIN_RECEIVERS.has(call.receiver)) {
1188
+ const typeEntry = typeMap.get(call.receiver);
1189
+ const typeName = typeEntry
1190
+ ? typeof typeEntry === 'string'
1191
+ ? typeEntry
1192
+ : typeEntry.type
1193
+ : null;
1194
+ if (typeName) {
1195
+ chaTargets = resolveChaTargets(typeName, call.name, chaCtx, lookup);
1196
+ }
1197
+ }
1198
+ for (const t of chaTargets) {
1199
+ const edgeKey = `${caller.id}|${t.id}`;
1200
+ if (t.id !== caller.id && !seenCallEdges.has(edgeKey) && !ptsEdgeRows.has(edgeKey)) {
1201
+ const conf = computeConfidence(relPath, t.file, null) - CHA_DISPATCH_PENALTY;
1202
+ if (conf > 0) {
1203
+ seenCallEdges.add(edgeKey);
1204
+ allEdgeRows.push([caller.id, t.id, 'calls', conf, 0, 'cha']);
1205
+ }
1206
+ }
1207
+ }
474
1208
  }
475
1209
  }
476
1210
  }
@@ -485,7 +1219,7 @@ function buildClassHierarchyEdges(ctx, relPath, symbols, allEdgeRows) {
485
1219
  const targetRows = (ctx.nodesByName.get(cls.extends) || []).filter((n) => EXTENDS_TARGET_KINDS.has(n.kind));
486
1220
  if (sourceRow) {
487
1221
  for (const t of targetRows) {
488
- allEdgeRows.push([sourceRow.id, t.id, 'extends', 1.0, 0]);
1222
+ allEdgeRows.push([sourceRow.id, t.id, 'extends', 1.0, 0, null]);
489
1223
  }
490
1224
  }
491
1225
  }
@@ -494,12 +1228,46 @@ function buildClassHierarchyEdges(ctx, relPath, symbols, allEdgeRows) {
494
1228
  const targetRows = (ctx.nodesByName.get(cls.implements) || []).filter((n) => IMPLEMENTS_TARGET_KINDS.has(n.kind));
495
1229
  if (sourceRow) {
496
1230
  for (const t of targetRows) {
497
- allEdgeRows.push([sourceRow.id, t.id, 'implements', 1.0, 0]);
1231
+ allEdgeRows.push([sourceRow.id, t.id, 'implements', 1.0, 0, null]);
498
1232
  }
499
1233
  }
500
1234
  }
501
1235
  }
502
1236
  }
1237
+ // ── Native bulk-insert technique back-fill ──────────────────────────────
1238
+ /**
1239
+ * After native bulkInsertEdges (which does not write the technique column),
1240
+ * apply technique values from the in-memory row array back to the DB.
1241
+ *
1242
+ * Rows with an explicit technique get a targeted UPDATE by (source_id, target_id).
1243
+ * The catch-all 'ts-native' tag is scoped to only the source_ids present in this
1244
+ * batch — this prevents mis-tagging pre-migration NULL-technique edges from
1245
+ * unchanged files that were never purged and re-inserted.
1246
+ */
1247
+ function applyEdgeTechniquesAfterNativeInsert(db, rows) {
1248
+ const callRows = rows.filter((r) => r[2] === 'calls');
1249
+ if (callRows.length === 0)
1250
+ return;
1251
+ const taggedRows = callRows.filter((r) => r[5] != null);
1252
+ // Collect distinct source IDs for this batch so the catch-all UPDATE is scoped
1253
+ // to edges inserted in the current run, not the entire table.
1254
+ const sourceIds = [...new Set(callRows.map((r) => r[0]))];
1255
+ // Chunk to stay within SQLite's SQLITE_LIMIT_VARIABLE_NUMBER (999 on older builds).
1256
+ const CHUNK_SIZE = 500;
1257
+ const tx = db.transaction(() => {
1258
+ if (taggedRows.length > 0) {
1259
+ const stmt = db.prepare("UPDATE edges SET technique = ? WHERE kind = 'calls' AND source_id = ? AND target_id = ? AND technique IS NULL");
1260
+ for (const r of taggedRows)
1261
+ stmt.run(r[5], r[0], r[1]);
1262
+ }
1263
+ for (let i = 0; i < sourceIds.length; i += CHUNK_SIZE) {
1264
+ const chunk = sourceIds.slice(i, i + CHUNK_SIZE);
1265
+ const placeholders = chunk.map(() => '?').join(',');
1266
+ db.prepare(`UPDATE edges SET technique = 'ts-native' WHERE kind = 'calls' AND technique IS NULL AND source_id IN (${placeholders})`).run(...chunk);
1267
+ }
1268
+ });
1269
+ tx();
1270
+ }
503
1271
  // ── Reverse-dep edge reconnection (#932, #933) ─────────────────────────
504
1272
  /**
505
1273
  * Reconnect edges that were saved before changed-file purge.
@@ -523,6 +1291,7 @@ function reconnectReverseDepEdges(ctx) {
523
1291
  saved.edgeKind,
524
1292
  saved.confidence,
525
1293
  saved.dynamic,
1294
+ saved.technique,
526
1295
  ]);
527
1296
  }
528
1297
  else {
@@ -543,6 +1312,9 @@ function reconnectReverseDepEdges(ctx) {
543
1312
  if (!ok) {
544
1313
  batchInsertEdges(db, reconnectedRows);
545
1314
  }
1315
+ else {
1316
+ applyEdgeTechniquesAfterNativeInsert(db, reconnectedRows);
1317
+ }
546
1318
  }
547
1319
  else {
548
1320
  batchInsertEdges(db, reconnectedRows);
@@ -624,7 +1396,21 @@ export async function buildEdges(ctx) {
624
1396
  setupNodeLookups(ctx, allNodesBefore);
625
1397
  addLazyFallback(ctx, scopedLoad);
626
1398
  const t0 = performance.now();
1399
+ // Enrich typeMap for .ts/.tsx files using the TypeScript compiler API.
1400
+ // Runs before call-edge construction so the accurate types are available
1401
+ // for method-call resolution. Gated on config so users can opt out.
1402
+ if (ctx.config.build.typescriptResolver) {
1403
+ await enrichTypeMapWithTsc(ctx.rootDir, ctx.fileSymbols);
1404
+ }
627
1405
  const native = engineName === 'native' ? loadNative() : null;
1406
+ // Phase 8.2: Augment typeMaps with cross-file return-type propagation before
1407
+ // the transaction opens. This is pure in-memory mutation (no DB I/O) and must
1408
+ // run outside the transaction to avoid leaving ctx.fileSymbols in a partial
1409
+ // state if the transaction rolls back unexpectedly.
1410
+ propagateReturnTypesAcrossFiles(ctx.fileSymbols, ctx, ctx.rootDir);
1411
+ // Phase 8.5: Build CHA context after propagation so typeMap confidence values
1412
+ // (used for RTA seeding) reflect any cross-file propagated types.
1413
+ const chaCtx = buildChaContext(ctx.fileSymbols);
628
1414
  // Phase 1: Compute edges inside a better-sqlite3 transaction.
629
1415
  // Barrel-edge deletion lives here so that the JS path (which also inserts
630
1416
  // edges in this transaction) keeps deletion + insertion atomic.
@@ -668,9 +1454,36 @@ export async function buildEdges(ctx) {
668
1454
  (ctx.isFullBuild || ctx.fileSymbols.size > ctx.config.build.smallFilesThreshold);
669
1455
  if (useNativeCallEdges) {
670
1456
  buildCallEdgesNative(ctx, getNodeIdStmt, allEdgeRows, allNodesBefore, native);
1457
+ // Build the shared lookup once — both pts post-passes use it, avoiding
1458
+ // redundant construction of the same context closure.
1459
+ const sharedLookup = makeContextLookup(ctx, getNodeIdStmt);
1460
+ // Phase 8.3c post-pass: augment native call edges with parameter-flow pts
1461
+ // edges. The native Rust engine has no knowledge of paramBindings, so any
1462
+ // `fn()` call inside a higher-order function would be missed. This JS pass
1463
+ // runs on top of the native edges and adds only the pts-resolved edges that
1464
+ // the native engine could not produce.
1465
+ buildParamFlowPtsPostPass(ctx, getNodeIdStmt, allEdgeRows, sharedLookup);
1466
+ // bind/alias post-pass: augment native call edges with fnRefBindings-seeded
1467
+ // pts edges. The native Rust engine has no knowledge of JS fnRefBindings
1468
+ // (e.g. `const f = fn.bind(ctx)`), so calls to bind-created aliases are
1469
+ // not resolved to their original function on the native path.
1470
+ buildFnRefBindingsPtsPostPass(ctx, getNodeIdStmt, allEdgeRows, sharedLookup);
1471
+ // this-rebinding post-pass: resolve `this()` calls inside functions that
1472
+ // were invoked via `.call(namedCtx, ...)` / `.apply(namedCtx, ...)`.
1473
+ buildThisCallBindingsPtsPostPass(ctx, getNodeIdStmt, allEdgeRows, sharedLookup);
1474
+ // Phase 8.3f post-pass: augment native call edges with object rest-param
1475
+ // receiver resolution — typeMap[restName] → argName → typeMap[argName.method].
1476
+ buildObjectRestParamPostPass(ctx, getNodeIdStmt, allEdgeRows, sharedLookup);
1477
+ // Object.defineProperty accessor post-pass: resolve this-dispatch inside
1478
+ // getter/setter functions registered via Object.defineProperty.
1479
+ buildDefinePropertyPostPass(ctx, getNodeIdStmt, allEdgeRows, sharedLookup);
1480
+ // Phase 8.5 post-pass: augment native call edges with CHA-resolved dispatch.
1481
+ // The native Rust engine has no knowledge of the CHA context, so this/self
1482
+ // calls and interface dispatch are not expanded to concrete implementations.
1483
+ buildChaPostPass(ctx, getNodeIdStmt, allEdgeRows, chaCtx);
671
1484
  }
672
1485
  else {
673
- buildCallEdgesJS(ctx, getNodeIdStmt, allEdgeRows);
1486
+ buildCallEdgesJS(ctx, getNodeIdStmt, allEdgeRows, chaCtx);
674
1487
  }
675
1488
  // When using native edge insert, skip JS insert here — do it after tx commits.
676
1489
  // Otherwise insert edges within this transaction for atomicity.
@@ -696,6 +1509,9 @@ export async function buildEdges(ctx) {
696
1509
  debug('Native bulkInsertEdges failed — falling back to JS batchInsertEdges');
697
1510
  batchInsertEdges(ctx.db, allEdgeRows);
698
1511
  }
1512
+ else {
1513
+ applyEdgeTechniquesAfterNativeInsert(ctx.db, allEdgeRows);
1514
+ }
699
1515
  }
700
1516
  // Phase 3: Reconnect saved reverse-dep edges (#932, #933).
701
1517
  // When the WASM/JS path purged changed files, edges FROM reverse-dep files TO
@@ -705,6 +1521,12 @@ export async function buildEdges(ctx) {
705
1521
  if (ctx.savedReverseDepEdges.length > 0) {
706
1522
  reconnectReverseDepEdges(ctx);
707
1523
  }
1524
+ // Phase 4: CHA post-pass — expand virtual-dispatch edges for class hierarchies
1525
+ // and interface implementations. Runs after all call + hierarchy edges are
1526
+ // committed so the DB is consistent.
1527
+ // Note: the native orchestrator success path runs this independently in
1528
+ // tryNativeOrchestrator; this phase covers the WASM and native-fallback paths.
1529
+ runChaPostPass(db);
708
1530
  ctx.timing.edgesMs = performance.now() - t0;
709
1531
  }
710
1532
  //# sourceMappingURL=build-edges.js.map