@optave/codegraph 3.12.0 → 3.13.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 (144) hide show
  1. package/README.md +71 -35
  2. package/dist/cli/commands/audit.d.ts.map +1 -1
  3. package/dist/cli/commands/audit.js +2 -1
  4. package/dist/cli/commands/audit.js.map +1 -1
  5. package/dist/cli/commands/batch.d.ts.map +1 -1
  6. package/dist/cli/commands/batch.js +1 -0
  7. package/dist/cli/commands/batch.js.map +1 -1
  8. package/dist/cli/commands/build.d.ts.map +1 -1
  9. package/dist/cli/commands/build.js +6 -1
  10. package/dist/cli/commands/build.js.map +1 -1
  11. package/dist/cli/commands/config.d.ts +3 -0
  12. package/dist/cli/commands/config.d.ts.map +1 -0
  13. package/dist/cli/commands/config.js +272 -0
  14. package/dist/cli/commands/config.js.map +1 -0
  15. package/dist/cli/commands/triage.js +1 -1
  16. package/dist/cli/commands/triage.js.map +1 -1
  17. package/dist/cli/index.d.ts.map +1 -1
  18. package/dist/cli/index.js +10 -0
  19. package/dist/cli/index.js.map +1 -1
  20. package/dist/cli/shared/options.d.ts +2 -1
  21. package/dist/cli/shared/options.d.ts.map +1 -1
  22. package/dist/cli/shared/options.js +11 -1
  23. package/dist/cli/shared/options.js.map +1 -1
  24. package/dist/cli/types.d.ts +2 -0
  25. package/dist/cli/types.d.ts.map +1 -1
  26. package/dist/db/migrations.js +1 -1
  27. package/dist/db/migrations.js.map +1 -1
  28. package/dist/domain/graph/builder/call-resolver.d.ts +12 -8
  29. package/dist/domain/graph/builder/call-resolver.d.ts.map +1 -1
  30. package/dist/domain/graph/builder/call-resolver.js +93 -38
  31. package/dist/domain/graph/builder/call-resolver.js.map +1 -1
  32. package/dist/domain/graph/builder/cha.d.ts +9 -1
  33. package/dist/domain/graph/builder/cha.d.ts.map +1 -1
  34. package/dist/domain/graph/builder/cha.js +17 -2
  35. package/dist/domain/graph/builder/cha.js.map +1 -1
  36. package/dist/domain/graph/builder/helpers.d.ts +8 -0
  37. package/dist/domain/graph/builder/helpers.d.ts.map +1 -1
  38. package/dist/domain/graph/builder/helpers.js +22 -3
  39. package/dist/domain/graph/builder/helpers.js.map +1 -1
  40. package/dist/domain/graph/builder/incremental.d.ts.map +1 -1
  41. package/dist/domain/graph/builder/incremental.js +1 -1
  42. package/dist/domain/graph/builder/incremental.js.map +1 -1
  43. package/dist/domain/graph/builder/pipeline.d.ts.map +1 -1
  44. package/dist/domain/graph/builder/pipeline.js +37 -2
  45. package/dist/domain/graph/builder/pipeline.js.map +1 -1
  46. package/dist/domain/graph/builder/stages/build-edges.d.ts +0 -2
  47. package/dist/domain/graph/builder/stages/build-edges.d.ts.map +1 -1
  48. package/dist/domain/graph/builder/stages/build-edges.js +88 -318
  49. package/dist/domain/graph/builder/stages/build-edges.js.map +1 -1
  50. package/dist/domain/graph/builder/stages/detect-changes.js +1 -1
  51. package/dist/domain/graph/builder/stages/detect-changes.js.map +1 -1
  52. package/dist/domain/graph/builder/stages/finalize.d.ts.map +1 -1
  53. package/dist/domain/graph/builder/stages/finalize.js +4 -0
  54. package/dist/domain/graph/builder/stages/finalize.js.map +1 -1
  55. package/dist/domain/graph/builder/stages/native-orchestrator.d.ts.map +1 -1
  56. package/dist/domain/graph/builder/stages/native-orchestrator.js +341 -82
  57. package/dist/domain/graph/builder/stages/native-orchestrator.js.map +1 -1
  58. package/dist/domain/graph/builder/stages/resolve-imports.js +1 -1
  59. package/dist/domain/graph/builder/stages/resolve-imports.js.map +1 -1
  60. package/dist/domain/parser.d.ts +4 -5
  61. package/dist/domain/parser.d.ts.map +1 -1
  62. package/dist/domain/parser.js +46 -15
  63. package/dist/domain/parser.js.map +1 -1
  64. package/dist/domain/wasm-worker-entry.js +10 -2
  65. package/dist/domain/wasm-worker-entry.js.map +1 -1
  66. package/dist/domain/wasm-worker-pool.d.ts.map +1 -1
  67. package/dist/domain/wasm-worker-pool.js +2 -0
  68. package/dist/domain/wasm-worker-pool.js.map +1 -1
  69. package/dist/domain/wasm-worker-protocol.d.ts +1 -0
  70. package/dist/domain/wasm-worker-protocol.d.ts.map +1 -1
  71. package/dist/extractors/cpp.d.ts.map +1 -1
  72. package/dist/extractors/cpp.js +42 -1
  73. package/dist/extractors/cpp.js.map +1 -1
  74. package/dist/extractors/cuda.d.ts.map +1 -1
  75. package/dist/extractors/cuda.js +42 -1
  76. package/dist/extractors/cuda.js.map +1 -1
  77. package/dist/extractors/helpers.d.ts +11 -0
  78. package/dist/extractors/helpers.d.ts.map +1 -1
  79. package/dist/extractors/helpers.js +40 -0
  80. package/dist/extractors/helpers.js.map +1 -1
  81. package/dist/extractors/java.d.ts.map +1 -1
  82. package/dist/extractors/java.js +8 -7
  83. package/dist/extractors/java.js.map +1 -1
  84. package/dist/extractors/javascript.js +137 -6
  85. package/dist/extractors/javascript.js.map +1 -1
  86. package/dist/features/structure-query.d.ts +1 -1
  87. package/dist/features/structure-query.d.ts.map +1 -1
  88. package/dist/features/structure-query.js +6 -6
  89. package/dist/features/structure-query.js.map +1 -1
  90. package/dist/index.d.ts +1 -1
  91. package/dist/index.d.ts.map +1 -1
  92. package/dist/index.js +1 -1
  93. package/dist/index.js.map +1 -1
  94. package/dist/infrastructure/config.d.ts +77 -4
  95. package/dist/infrastructure/config.d.ts.map +1 -1
  96. package/dist/infrastructure/config.js +395 -21
  97. package/dist/infrastructure/config.js.map +1 -1
  98. package/dist/infrastructure/registry.d.ts +27 -0
  99. package/dist/infrastructure/registry.d.ts.map +1 -1
  100. package/dist/infrastructure/registry.js +59 -1
  101. package/dist/infrastructure/registry.js.map +1 -1
  102. package/dist/presentation/structure.d.ts +1 -1
  103. package/dist/presentation/structure.d.ts.map +1 -1
  104. package/dist/presentation/structure.js +2 -2
  105. package/dist/presentation/structure.js.map +1 -1
  106. package/dist/types.d.ts +37 -0
  107. package/dist/types.d.ts.map +1 -1
  108. package/grammars/tree-sitter-gleam.wasm +0 -0
  109. package/package.json +7 -8
  110. package/src/cli/commands/audit.ts +2 -1
  111. package/src/cli/commands/batch.ts +1 -0
  112. package/src/cli/commands/build.ts +6 -1
  113. package/src/cli/commands/config.ts +353 -0
  114. package/src/cli/commands/triage.ts +1 -1
  115. package/src/cli/index.ts +10 -0
  116. package/src/cli/shared/options.ts +11 -1
  117. package/src/cli/types.ts +2 -0
  118. package/src/db/migrations.ts +1 -1
  119. package/src/domain/graph/builder/call-resolver.ts +99 -41
  120. package/src/domain/graph/builder/cha.ts +18 -1
  121. package/src/domain/graph/builder/helpers.ts +24 -4
  122. package/src/domain/graph/builder/incremental.ts +1 -0
  123. package/src/domain/graph/builder/pipeline.ts +49 -2
  124. package/src/domain/graph/builder/stages/build-edges.ts +130 -399
  125. package/src/domain/graph/builder/stages/detect-changes.ts +1 -1
  126. package/src/domain/graph/builder/stages/finalize.ts +4 -0
  127. package/src/domain/graph/builder/stages/native-orchestrator.ts +396 -92
  128. package/src/domain/graph/builder/stages/resolve-imports.ts +1 -1
  129. package/src/domain/parser.ts +45 -14
  130. package/src/domain/wasm-worker-entry.ts +10 -2
  131. package/src/domain/wasm-worker-pool.ts +1 -0
  132. package/src/domain/wasm-worker-protocol.ts +1 -0
  133. package/src/extractors/cpp.ts +44 -1
  134. package/src/extractors/cuda.ts +44 -1
  135. package/src/extractors/helpers.ts +43 -0
  136. package/src/extractors/java.ts +8 -7
  137. package/src/extractors/javascript.ts +127 -6
  138. package/src/features/structure-query.ts +7 -7
  139. package/src/index.ts +5 -1
  140. package/src/infrastructure/config.ts +481 -22
  141. package/src/infrastructure/registry.ts +82 -1
  142. package/src/presentation/structure.ts +3 -3
  143. package/src/types.ts +41 -0
  144. package/grammars/tree-sitter-erlang.wasm +0 -0
@@ -16,10 +16,8 @@ import { buildPointsToMap, resolveViaPointsTo } from '../../resolver/points-to.j
16
16
  import { enrichTypeMapWithTsc } from '../../resolver/ts-resolver.js';
17
17
  import { findCaller, isModuleScopedLanguage, resolveCallTargets, resolveReceiverEdge, } from '../call-resolver.js';
18
18
  import { buildChaContext, resolveChaTargets, resolveThisDispatch } from '../cha.js';
19
- import { BUILTIN_RECEIVERS, batchInsertEdges, runChaPostPass } from '../helpers.js';
19
+ import { BUILTIN_RECEIVERS, batchInsertEdges, CHA_DISPATCH_PENALTY, CHA_TYPED_DISPATCH_CONFIDENCE, runChaPostPass, } from '../helpers.js';
20
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;
23
21
  // ── Node lookup setup ───────────────────────────────────────────────────
24
22
  function makeGetNodeIdStmt(db) {
25
23
  return {
@@ -337,17 +335,35 @@ function buildCallEdgesNative(ctx, getNodeIdStmt, allEdgeRows, allNodes, native)
337
335
  nativeFiles.push({
338
336
  file: relPath,
339
337
  fileNodeId: fileNodeRow.id,
340
- definitions: symbols.definitions.map((d) => ({
341
- name: d.name,
342
- kind: d.kind,
343
- line: d.line,
344
- endLine: d.endLine ?? null,
345
- })),
338
+ definitions: symbols.definitions.map((d) => {
339
+ const params = d.children?.filter((c) => c.kind === 'parameter').map((c) => c.name);
340
+ return {
341
+ name: d.name,
342
+ kind: d.kind,
343
+ line: d.line,
344
+ endLine: d.endLine ?? null,
345
+ params: params?.length ? params : undefined,
346
+ };
347
+ }),
346
348
  calls: symbols.calls,
347
349
  importedNames,
348
350
  classes: symbols.classes,
349
351
  typeMap,
350
352
  fnRefBindings: symbols.fnRefBindings?.length ? symbols.fnRefBindings : undefined,
353
+ paramBindings: symbols.paramBindings?.length ? symbols.paramBindings : undefined,
354
+ thisCallBindings: symbols.thisCallBindings?.length ? symbols.thisCallBindings : undefined,
355
+ arrayElemBindings: symbols.arrayElemBindings?.length ? symbols.arrayElemBindings : undefined,
356
+ spreadArgBindings: symbols.spreadArgBindings?.length ? symbols.spreadArgBindings : undefined,
357
+ forOfBindings: symbols.forOfBindings?.length ? symbols.forOfBindings : undefined,
358
+ arrayCallbackBindings: symbols.arrayCallbackBindings?.length
359
+ ? symbols.arrayCallbackBindings
360
+ : undefined,
361
+ objectRestParamBindings: symbols.objectRestParamBindings?.length
362
+ ? symbols.objectRestParamBindings
363
+ : undefined,
364
+ objectPropBindings: symbols.objectPropBindings?.length
365
+ ? symbols.objectPropBindings
366
+ : undefined,
351
367
  });
352
368
  }
353
369
  const nativeEdges = native.buildCallEdges(nativeFiles, allNodes, [
@@ -364,282 +380,6 @@ function buildCallEdgesNative(ctx, getNodeIdStmt, allEdgeRows, allNodes, native)
364
380
  ]);
365
381
  }
366
382
  }
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
383
  /**
644
384
  * Object.defineProperty accessor post-pass for the native call-edge path.
645
385
  *
@@ -715,11 +455,11 @@ function buildDefinePropertyPostPass(ctx, getNodeIdStmt, allEdgeRows, sharedLook
715
455
  *
716
456
  * The native Rust engine has no knowledge of the CHA context, so `this.method()`
717
457
  * 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.
458
+ * implementations. This JS post-pass runs after the native edges and adds only
459
+ * the CHA-resolved edges that the native engine missed.
720
460
  *
721
- * Like buildParamFlowPtsPostPass, it seeds seenByPair from the current allEdgeRows
722
- * snapshot to avoid duplicating edges the native engine already produced.
461
+ * Seeds seenByPair from the current allEdgeRows snapshot to avoid duplicating
462
+ * edges the native engine already produced.
723
463
  */
724
464
  function buildChaPostPass(ctx, getNodeIdStmt, allEdgeRows, chaCtx) {
725
465
  // Fast-exit when the CHA context is empty (no class hierarchy in the project)
@@ -748,8 +488,9 @@ function buildChaPostPass(ctx, getNodeIdStmt, allEdgeRows, chaCtx) {
748
488
  continue;
749
489
  const caller = findCaller(lookup, call, symbols.definitions, relPath, fileNodeRow);
750
490
  let chaTargets = [];
491
+ let isTypedReceiverDispatch = false;
751
492
  if (call.receiver === 'this' || call.receiver === 'self' || call.receiver === 'super') {
752
- chaTargets = resolveThisDispatch(call.name, caller.callerName, call.receiver, chaCtx, lookup);
493
+ chaTargets = resolveThisDispatch(call.name, caller.callerName, call.receiver, chaCtx, lookup, relPath);
753
494
  }
754
495
  else {
755
496
  const typeEntry = typeMap.get(call.receiver);
@@ -760,15 +501,25 @@ function buildChaPostPass(ctx, getNodeIdStmt, allEdgeRows, chaCtx) {
760
501
  : null;
761
502
  if (typeName) {
762
503
  chaTargets = resolveChaTargets(typeName, call.name, chaCtx, lookup);
504
+ isTypedReceiverDispatch = true;
763
505
  }
764
506
  }
765
507
  for (const t of chaTargets) {
766
508
  const edgeKey = `${caller.id}|${t.id}`;
767
509
  if (t.id !== caller.id && !seenByPair.has(edgeKey)) {
768
- const conf = computeConfidence(relPath, t.file, null) - CHA_DISPATCH_PENALTY;
510
+ // Typed-receiver (interface/CHA) dispatch: use CHA_TYPED_DISPATCH_CONFIDENCE
511
+ // — file proximity is not meaningful for virtual dispatch confidence.
512
+ // this/super dispatch keeps computeConfidence-based proximity scoring to
513
+ // match runPostNativeThisDispatch (native-orchestrator.ts).
514
+ const conf = isTypedReceiverDispatch
515
+ ? CHA_TYPED_DISPATCH_CONFIDENCE
516
+ : computeConfidence(relPath, t.file, null) - CHA_DISPATCH_PENALTY;
769
517
  if (conf > 0) {
770
518
  seenByPair.add(edgeKey);
771
- allEdgeRows.push([caller.id, t.id, 'calls', conf, 0, 'cha']);
519
+ // Tag super-dispatch edges distinctly so runChaPostPass can exclude them
520
+ // from further CHA expansion (super calls are not virtual dispatch).
521
+ const technique = call.receiver === 'super' ? 'super-dispatch' : 'cha';
522
+ allEdgeRows.push([caller.id, t.id, 'calls', conf, 0, technique]);
772
523
  }
773
524
  }
774
525
  }
@@ -1054,6 +805,15 @@ function buildFileCallEdges(relPath, symbols, fileNodeRow, importedNames, seenCa
1054
805
  }
1055
806
  }
1056
807
  }
808
+ // Sort targets by confidence descending before emitting edges.
809
+ // For multi-target calls with duplicate (source_id, target_id) pairs the
810
+ // stored confidence depends on which duplicate is processed last — sorting
811
+ // here guarantees the highest-confidence target wins on dedup, matching the
812
+ // native engine's sort_targets_by_confidence call in build_edges.rs.
813
+ if (targets.length > 1) {
814
+ targets = [...targets].sort((a, b) => computeConfidence(relPath, b.file, importedFrom ?? null) -
815
+ computeConfidence(relPath, a.file, importedFrom ?? null));
816
+ }
1057
817
  for (const t of targets) {
1058
818
  const edgeKey = `${caller.id}|${t.id}`;
1059
819
  if (t.id !== caller.id) {
@@ -1126,7 +886,11 @@ function buildFileCallEdges(relPath, symbols, fileNodeRow, importedNames, seenCa
1126
886
  // and line are not relevant for alias resolution (we are looking up the
1127
887
  // aliased function by name, not dispatching a method call).
1128
888
  const { targets: aliasTargets, importedFrom: aliasFrom } = resolveCallTargets(lookup, { name: alias }, relPath, importedNames, typeMap);
1129
- for (const t of aliasTargets) {
889
+ const sortedAliasTargets = aliasTargets.length > 1
890
+ ? [...aliasTargets].sort((a, b) => computeConfidence(relPath, b.file, aliasFrom ?? null) -
891
+ computeConfidence(relPath, a.file, aliasFrom ?? null))
892
+ : aliasTargets;
893
+ for (const t of sortedAliasTargets) {
1130
894
  const edgeKey = `${caller.id}|${t.id}`;
1131
895
  if (t.id !== caller.id && !seenCallEdges.has(edgeKey) && !ptsEdgeRows.has(edgeKey)) {
1132
896
  const conf = computeConfidence(relPath, t.file, aliasFrom ?? null) - PROPAGATION_HOP_PENALTY;
@@ -1152,7 +916,11 @@ function buildFileCallEdges(relPath, symbols, fileNodeRow, importedNames, seenCa
1152
916
  if (ptsMap.has(receiverKey)) {
1153
917
  for (const alias of resolveViaPointsTo(receiverKey, ptsMap)) {
1154
918
  const { targets: aliasTargets, importedFrom: aliasFrom } = resolveCallTargets(lookup, { name: alias }, relPath, importedNames, typeMap);
1155
- for (const t of aliasTargets) {
919
+ const sortedAliasTargets = aliasTargets.length > 1
920
+ ? [...aliasTargets].sort((a, b) => computeConfidence(relPath, b.file, aliasFrom ?? null) -
921
+ computeConfidence(relPath, a.file, aliasFrom ?? null))
922
+ : aliasTargets;
923
+ for (const t of sortedAliasTargets) {
1156
924
  const edgeKey = `${caller.id}|${t.id}`;
1157
925
  if (t.id !== caller.id && !seenCallEdges.has(edgeKey) && !ptsEdgeRows.has(edgeKey)) {
1158
926
  const conf = computeConfidence(relPath, t.file, aliasFrom ?? null) - PROPAGATION_HOP_PENALTY;
@@ -1170,7 +938,7 @@ function buildFileCallEdges(relPath, symbols, fileNodeRow, importedNames, seenCa
1170
938
  call.receiver !== 'this' &&
1171
939
  call.receiver !== 'self' &&
1172
940
  call.receiver !== 'super') {
1173
- const recv = resolveReceiverEdge(lookup, { name: call.name, receiver: call.receiver }, caller, relPath, typeMap, seenCallEdges);
941
+ const recv = resolveReceiverEdge(lookup, { name: call.name, receiver: call.receiver }, caller, relPath, typeMap, seenCallEdges, importedNames);
1174
942
  if (recv) {
1175
943
  allEdgeRows.push([recv.callerId, recv.receiverId, 'receiver', recv.confidence, 0, null]);
1176
944
  }
@@ -1181,8 +949,9 @@ function buildFileCallEdges(relPath, symbols, fileNodeRow, importedNames, seenCa
1181
949
  // For typed receiver calls: expand to all instantiated concrete implementations.
1182
950
  if (chaCtx && call.receiver) {
1183
951
  let chaTargets = [];
952
+ let isTypedReceiverDispatch = false;
1184
953
  if (call.receiver === 'this' || call.receiver === 'self' || call.receiver === 'super') {
1185
- chaTargets = resolveThisDispatch(call.name, caller.callerName, call.receiver, chaCtx, lookup);
954
+ chaTargets = resolveThisDispatch(call.name, caller.callerName, call.receiver, chaCtx, lookup, relPath);
1186
955
  }
1187
956
  else if (!BUILTIN_RECEIVERS.has(call.receiver)) {
1188
957
  const typeEntry = typeMap.get(call.receiver);
@@ -1193,12 +962,19 @@ function buildFileCallEdges(relPath, symbols, fileNodeRow, importedNames, seenCa
1193
962
  : null;
1194
963
  if (typeName) {
1195
964
  chaTargets = resolveChaTargets(typeName, call.name, chaCtx, lookup);
965
+ isTypedReceiverDispatch = true;
1196
966
  }
1197
967
  }
1198
968
  for (const t of chaTargets) {
1199
969
  const edgeKey = `${caller.id}|${t.id}`;
1200
970
  if (t.id !== caller.id && !seenCallEdges.has(edgeKey) && !ptsEdgeRows.has(edgeKey)) {
1201
- const conf = computeConfidence(relPath, t.file, null) - CHA_DISPATCH_PENALTY;
971
+ // Typed-receiver (interface/CHA) dispatch: use CHA_TYPED_DISPATCH_CONFIDENCE
972
+ // — file proximity is not meaningful for virtual dispatch confidence.
973
+ // this/super dispatch keeps computeConfidence-based proximity scoring to
974
+ // match runPostNativeThisDispatch (native-orchestrator.ts).
975
+ const conf = isTypedReceiverDispatch
976
+ ? CHA_TYPED_DISPATCH_CONFIDENCE
977
+ : computeConfidence(relPath, t.file, null) - CHA_DISPATCH_PENALTY;
1202
978
  if (conf > 0) {
1203
979
  seenCallEdges.add(edgeKey);
1204
980
  allEdgeRows.push([caller.id, t.id, 'calls', conf, 0, 'cha']);
@@ -1330,7 +1106,7 @@ function reconnectReverseDepEdges(ctx) {
1330
1106
  * their import targets. Falls back to loading ALL nodes for full builds or
1331
1107
  * larger incremental changes.
1332
1108
  */
1333
- const NODE_KIND_FILTER_SQL = `kind IN ('function','method','class','interface','struct','type','module','enum','trait','record','constant')`;
1109
+ const NODE_KIND_FILTER_SQL = `kind IN ('function','method','class','interface','struct','type','module','enum','trait','record','constant','variable')`;
1334
1110
  function loadNodes(ctx) {
1335
1111
  const { db, fileSymbols, isFullBuild, batchResolved } = ctx;
1336
1112
  const nodeKindFilter = NODE_KIND_FILTER_SQL;
@@ -1399,7 +1175,15 @@ export async function buildEdges(ctx) {
1399
1175
  // Enrich typeMap for .ts/.tsx files using the TypeScript compiler API.
1400
1176
  // Runs before call-edge construction so the accurate types are available
1401
1177
  // for method-call resolution. Gated on config so users can opt out.
1402
- if (ctx.config.build.typescriptResolver) {
1178
+ //
1179
+ // Skip for small incremental builds: TypeScript program creation requires
1180
+ // loading the entire tsconfig file list (~700ms startup on the codegraph
1181
+ // corpus), which dominates the 1-file rebuild time. Native engine bypasses
1182
+ // this entirely via the Rust orchestrator; WASM/JS engines need this gate
1183
+ // to match native's effective behaviour on tiny incremental changes.
1184
+ // Mirrors the smallFilesThreshold gates for nativeDb and native call-edges.
1185
+ const isSmallIncremental = !ctx.isFullBuild && ctx.fileSymbols.size <= ctx.config.build.smallFilesThreshold;
1186
+ if (ctx.config.build.typescriptResolver && !isSmallIncremental) {
1403
1187
  await enrichTypeMapWithTsc(ctx.rootDir, ctx.fileSymbols);
1404
1188
  }
1405
1189
  const native = engineName === 'native' ? loadNative() : null;
@@ -1454,26 +1238,12 @@ export async function buildEdges(ctx) {
1454
1238
  (ctx.isFullBuild || ctx.fileSymbols.size > ctx.config.build.smallFilesThreshold);
1455
1239
  if (useNativeCallEdges) {
1456
1240
  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.
1241
+ // The native engine receives all pts bindings (paramBindings,
1242
+ // fnRefBindings, thisCallBindings, objectRestParamBindings, …) through
1243
+ // NativeFileEntry and runs the same points-to solver as the JS path, so
1244
+ // no pts post-passes are needed here. Only capabilities that remain
1245
+ // JS-only run as post-passes below.
1459
1246
  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
1247
  // Object.defineProperty accessor post-pass: resolve this-dispatch inside
1478
1248
  // getter/setter functions registered via Object.defineProperty.
1479
1249
  buildDefinePropertyPostPass(ctx, getNodeIdStmt, allEdgeRows, sharedLookup);