@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,7 +7,7 @@ import { createHash } from 'node:crypto';
7
7
  import fs from 'node:fs';
8
8
  import path from 'node:path';
9
9
  import { purgeFilesData } from '../../../db/index.js';
10
- import { warn } from '../../../infrastructure/logger.js';
10
+ import { debug, warn } from '../../../infrastructure/logger.js';
11
11
  import { EXTENSIONS, IGNORE_DIRS, normalizePath } from '../../../shared/constants.js';
12
12
  import { compileGlobs, matchesAny } from '../../../shared/globs.js';
13
13
  import type {
@@ -313,9 +313,9 @@ function getEdgeStmt(db: BetterSqlite3Database, chunkSize: number): SqliteStatem
313
313
  }
314
314
  let stmt = cache.get(chunkSize);
315
315
  if (!stmt) {
316
- const ph = '(?,?,?,?,?)';
316
+ const ph = '(?,?,?,?,?,?)';
317
317
  stmt = db.prepare(
318
- 'INSERT INTO edges (source_id,target_id,kind,confidence,dynamic) VALUES ' +
318
+ 'INSERT INTO edges (source_id,target_id,kind,confidence,dynamic,technique) VALUES ' +
319
319
  Array.from({ length: chunkSize }, () => ph).join(','),
320
320
  );
321
321
  cache.set(chunkSize, stmt);
@@ -344,7 +344,7 @@ export function batchInsertNodes(db: BetterSqlite3Database, rows: unknown[][]):
344
344
 
345
345
  /**
346
346
  * Batch-insert edge rows via multi-value INSERT statements.
347
- * Each row: [source_id, target_id, kind, confidence, dynamic]
347
+ * Each row: [source_id, target_id, kind, confidence, dynamic, technique]
348
348
  */
349
349
  export function batchInsertEdges(db: BetterSqlite3Database, rows: unknown[][]): void {
350
350
  if (!rows.length) return;
@@ -355,8 +355,178 @@ export function batchInsertEdges(db: BetterSqlite3Database, rows: unknown[][]):
355
355
  const vals: unknown[] = [];
356
356
  for (let j = i; j < end; j++) {
357
357
  const r = rows[j] as unknown[];
358
- vals.push(r[0], r[1], r[2], r[3], r[4]);
358
+ vals.push(r[0], r[1], r[2], r[3], r[4], r[5] ?? null);
359
359
  }
360
360
  stmt.run(...vals);
361
361
  }
362
362
  }
363
+
364
+ /**
365
+ * CHA (Class Hierarchy Analysis) post-pass.
366
+ *
367
+ * Expands virtual-dispatch call edges for class hierarchies and interface
368
+ * implementations already present in the DB:
369
+ *
370
+ * 1. Build implementors map: parent/interface → [child/implementing class] from
371
+ * `extends` and `implements` edges.
372
+ * 2. Collect RTA evidence: class nodes that appear as `calls` targets (new X()).
373
+ * 3. Find all `calls` edges to qualified method nodes (name contains '.').
374
+ * 4. For each such call, expand to concrete overrides via the implementors map,
375
+ * filtered by RTA when evidence exists.
376
+ *
377
+ * Used by both the native orchestrator post-pass and the WASM build-edges pass.
378
+ */
379
+ export function runChaPostPass(db: BetterSqlite3Database): number {
380
+ const hasHierarchy = db
381
+ .prepare(`SELECT 1 FROM edges WHERE kind IN ('extends', 'implements') LIMIT 1`)
382
+ .get();
383
+ if (!hasHierarchy) return 0;
384
+
385
+ const hierarchyRows = db
386
+ .prepare(
387
+ `SELECT src.name AS child_name, tgt.name AS parent_name
388
+ FROM edges e
389
+ JOIN nodes src ON e.source_id = src.id
390
+ JOIN nodes tgt ON e.target_id = tgt.id
391
+ WHERE e.kind IN ('extends', 'implements')`,
392
+ )
393
+ .all() as Array<{ child_name: string; parent_name: string }>;
394
+
395
+ const implementorSets = new Map<string, Set<string>>();
396
+ for (const row of hierarchyRows) {
397
+ let set = implementorSets.get(row.parent_name);
398
+ if (!set) {
399
+ set = new Set<string>();
400
+ implementorSets.set(row.parent_name, set);
401
+ }
402
+ set.add(row.child_name);
403
+ }
404
+ if (implementorSets.size === 0) return 0;
405
+ // Convert to arrays for iteration compatibility with the rest of the function
406
+ const implementors = new Map([...implementorSets.entries()].map(([k, v]) => [k, [...v]]));
407
+
408
+ // RTA: collect class names instantiated via constructor calls (`new X()`).
409
+ let rtaRows = db
410
+ .prepare(
411
+ `SELECT DISTINCT tgt.name
412
+ FROM edges e
413
+ JOIN nodes tgt ON e.target_id = tgt.id
414
+ WHERE e.kind = 'calls' AND tgt.kind = 'class'`,
415
+ )
416
+ .all() as Array<{ name: string }>;
417
+ if (rtaRows.length === 0) {
418
+ // Fallback: some languages (e.g. TypeScript via WASM) record constructor calls as
419
+ // 'function' or 'constructor' kind rather than 'class'. Restrict to names that are
420
+ // actually known class names to avoid treating unrelated function calls like `logger()`
421
+ // as class-instantiation evidence.
422
+ // Include both parent/interface names AND implementor (child) names so that
423
+ // `new UserRepository()` (a child class) is correctly detected as RTA evidence.
424
+ const knownClassNames = [
425
+ ...new Set([
426
+ ...implementorSets.keys(),
427
+ ...[...implementorSets.values()].flatMap((s) => [...s]),
428
+ ]),
429
+ ];
430
+ if (knownClassNames.length > 0) {
431
+ // Chunk to stay within SQLite SQLITE_MAX_VARIABLE_NUMBER (999 in many builds).
432
+ const CHUNK = 999;
433
+ for (let i = 0; i < knownClassNames.length; i += CHUNK) {
434
+ const chunk = knownClassNames.slice(i, i + CHUNK);
435
+ const placeholders = chunk.map(() => '?').join(',');
436
+ const chunkRows = db
437
+ .prepare(
438
+ `SELECT DISTINCT tgt.name
439
+ FROM edges e
440
+ JOIN nodes tgt ON e.target_id = tgt.id
441
+ WHERE e.kind = 'calls' AND tgt.kind IN ('constructor', 'function')
442
+ AND tgt.name IN (${placeholders})`,
443
+ )
444
+ .all(...chunk) as Array<{ name: string }>;
445
+ rtaRows = rtaRows.concat(chunkRows);
446
+ }
447
+ }
448
+ }
449
+ const instantiated = new Set(rtaRows.map((r) => r.name));
450
+ const noRtaEvidence = instantiated.size === 0;
451
+ if (noRtaEvidence) {
452
+ debug('runChaPostPass: no constructor-call evidence — proceeding without RTA filter');
453
+ }
454
+
455
+ const callToMethods = db
456
+ .prepare(
457
+ `SELECT e.source_id, tgt.name AS method_name
458
+ FROM edges e
459
+ JOIN nodes tgt ON e.target_id = tgt.id
460
+ WHERE e.kind = 'calls' AND tgt.kind = 'method'
461
+ AND INSTR(tgt.name, '.') > 0`,
462
+ )
463
+ .all() as Array<{ source_id: number; method_name: string }>;
464
+
465
+ const seen = new Set<string>();
466
+ // Scope deduplication to only the source_ids we are about to expand, avoiding
467
+ // a full-table scan. CHA only inserts edges FROM callers that already call a
468
+ // qualified method (the source_ids in callToMethods), so we only need to
469
+ // check existing edges for those specific callers.
470
+ const callerIds = [...new Set(callToMethods.map((r) => r.source_id))];
471
+ if (callerIds.length > 0) {
472
+ // Chunk to stay within SQLite SQLITE_MAX_VARIABLE_NUMBER (999 in many builds).
473
+ const CHUNK = 999;
474
+ for (let i = 0; i < callerIds.length; i += CHUNK) {
475
+ const chunk = callerIds.slice(i, i + CHUNK);
476
+ const placeholders = chunk.map(() => '?').join(',');
477
+ const existingPairs = db
478
+ .prepare(
479
+ `SELECT source_id, target_id FROM edges WHERE kind = 'calls' AND source_id IN (${placeholders})`,
480
+ )
481
+ .all(...chunk) as Array<{ source_id: number; target_id: number }>;
482
+ for (const e of existingPairs) seen.add(`${e.source_id}|${e.target_id}`);
483
+ }
484
+ }
485
+
486
+ // No LIMIT: multiple files can define the same qualified name in a monorepo.
487
+ const findMethodStmt = db.prepare(`SELECT id FROM nodes WHERE name = ? AND kind = 'method'`);
488
+ const newEdges: Array<[number, number, string, number, number, string]> = [];
489
+
490
+ for (const { source_id, method_name } of callToMethods) {
491
+ const dotIdx = method_name.indexOf('.');
492
+ if (dotIdx === -1) continue;
493
+ const typeName = method_name.slice(0, dotIdx);
494
+ const methodSuffix = method_name.slice(dotIdx + 1);
495
+
496
+ // BFS over the implementors map — handles multi-level hierarchies where
497
+ // abstract/non-instantiated classes sit between the call-site type and
498
+ // the concrete leaf implementations (matches runPostNativeCha, issue #1311).
499
+ const bfsQueue: string[] = [typeName];
500
+ const bfsVisited = new Set<string>([typeName]);
501
+ while (bfsQueue.length > 0) {
502
+ const current = bfsQueue.shift()!;
503
+ const children = implementors.get(current);
504
+ if (!children?.length) continue;
505
+
506
+ for (const cls of children) {
507
+ if (bfsVisited.has(cls)) continue;
508
+ bfsVisited.add(cls);
509
+
510
+ if (noRtaEvidence || instantiated.has(cls)) {
511
+ const qualifiedName = `${cls}.${methodSuffix}`;
512
+ const methodNodes = findMethodStmt.all(qualifiedName) as Array<{ id: number }>;
513
+ for (const methodNode of methodNodes) {
514
+ const key = `${source_id}|${methodNode.id}`;
515
+ if (seen.has(key)) continue;
516
+ seen.add(key);
517
+ newEdges.push([source_id, methodNode.id, 'calls', 0.8, 0, 'cha']);
518
+ }
519
+ }
520
+
521
+ // Always traverse children — non-instantiated classes may have instantiated subclasses.
522
+ bfsQueue.push(cls);
523
+ }
524
+ }
525
+ }
526
+
527
+ if (newEdges.length > 0) {
528
+ db.transaction(() => batchInsertEdges(db, newEdges))();
529
+ debug(`runChaPostPass: inserted ${newEdges.length} CHA dispatch edge(s)`);
530
+ }
531
+ return newEdges.length;
532
+ }
@@ -21,6 +21,12 @@ import type {
21
21
  } from '../../../types.js';
22
22
  import { parseFileIncremental } from '../../parser.js';
23
23
  import { computeConfidence, resolveImportPath } from '../resolve.js';
24
+ import {
25
+ type CallNodeLookup,
26
+ findCaller,
27
+ resolveCallTargets,
28
+ resolveReceiverEdge,
29
+ } from './call-resolver.js';
24
30
  import { BUILTIN_RECEIVERS, readFileSafe } from './helpers.js';
25
31
 
26
32
  // ── Local types ─────────────────────────────────────────────────────────
@@ -185,8 +191,9 @@ function rebuildReverseDepEdges(
185
191
  aliases,
186
192
  skipBarrel ? null : db,
187
193
  );
188
- const importedNames = buildImportedNamesMap(symbols, rootDir, depRelPath, aliases);
189
- edgesAdded += buildCallEdges(stmts, depRelPath, symbols, fileNodeRow, importedNames);
194
+ const importedNames = buildImportedNamesMap(symbols, rootDir, depRelPath, aliases, db);
195
+ edgesAdded += buildCallEdges(db, stmts, depRelPath, symbols, fileNodeRow, importedNames);
196
+ edgesAdded += buildClassHierarchyEdges(stmts, depRelPath, symbols);
190
197
  return edgesAdded;
191
198
  }
192
199
 
@@ -353,7 +360,13 @@ function emitEdgesForImport(
353
360
  const targetRow = stmts.getNodeId.get(resolvedPath, 'file', resolvedPath, 0);
354
361
  if (!targetRow) return 0;
355
362
 
356
- const edgeKind = imp.reexport ? 'reexports' : imp.typeOnly ? 'imports-type' : 'imports';
363
+ const edgeKind = imp.reexport
364
+ ? 'reexports'
365
+ : imp.typeOnly
366
+ ? 'imports-type'
367
+ : imp.dynamicImport
368
+ ? 'dynamic-imports'
369
+ : 'imports';
357
370
  stmts.insertEdge.run(fileNodeId, targetRow.id, edgeKind, 1.0, 0);
358
371
  let edgesAdded = 1;
359
372
 
@@ -387,6 +400,7 @@ function buildImportedNamesMap(
387
400
  rootDir: string,
388
401
  relPath: string,
389
402
  aliases: PathAliases,
403
+ db: BetterSqlite3Database,
390
404
  ): Map<string, string> {
391
405
  const importedNames = new Map<string, string>();
392
406
  for (const imp of symbols.imports) {
@@ -397,78 +411,79 @@ function buildImportedNamesMap(
397
411
  aliases,
398
412
  );
399
413
  for (const name of imp.names) {
400
- importedNames.set(name.replace(/^\*\s+as\s+/, ''), resolvedPath);
414
+ const cleanName = name.replace(/^\*\s+as\s+/, '');
415
+ // Mirror full-build's `buildImportedNamesMap`: follow barrel re-exports so
416
+ // `importedNames` maps to the *defining* file, not the barrel. This ensures
417
+ // `computeConfidence` gets `importedFrom === targetFile` and returns 1.0
418
+ // instead of the cross-directory fallback (0.3).
419
+ let targetFile = resolvedPath;
420
+ if (isBarrelFile(db, resolvedPath)) {
421
+ const actual = resolveBarrelTarget(db, resolvedPath, cleanName);
422
+ if (actual) targetFile = actual;
423
+ }
424
+ importedNames.set(cleanName, targetFile);
401
425
  }
402
426
  }
403
427
  return importedNames;
404
428
  }
405
429
 
406
- // ── Call edge building ──────────────────────────────────────────────────
430
+ // ── Class hierarchy edges ───────────────────────────────────────────────
407
431
 
408
- function findCaller(
409
- call: ExtractorOutput['calls'][number],
410
- definitions: ExtractorOutput['definitions'],
411
- relPath: string,
412
- stmts: IncrementalStmts,
413
- ): { id: number } | null {
414
- let caller: { id: number } | null = null;
415
- let callerSpan = Infinity;
416
- for (const def of definitions) {
417
- if (def.line <= call.line) {
418
- const end = def.endLine || Infinity;
419
- if (call.line <= end) {
420
- const span = end - def.line;
421
- if (span < callerSpan) {
422
- const row = stmts.getNodeId.get(def.name, def.kind, relPath, def.line);
423
- if (row) {
424
- caller = row;
425
- callerSpan = span;
426
- }
427
- }
428
- }
429
- }
430
- }
431
- return caller;
432
- }
432
+ type NodeWithKind = { id: number; kind: string; file: string };
433
+
434
+ const HIERARCHY_SOURCE_KINDS = new Set(['class', 'struct', 'record', 'enum']);
435
+ const EXTENDS_TARGET_KINDS = new Set(['class', 'struct', 'trait', 'record']);
436
+ const IMPLEMENTS_TARGET_KINDS = new Set(['interface', 'trait', 'class']);
433
437
 
434
- function resolveCallTargets(
438
+ function buildClassHierarchyEdges(
435
439
  stmts: IncrementalStmts,
436
- call: ExtractorOutput['calls'][number],
437
440
  relPath: string,
438
- importedNames: Map<string, string>,
439
- typeMap: Map<string, unknown>,
440
- ): { targets: Array<{ id: number; file: string }>; importedFrom: string | undefined } {
441
- const importedFrom = importedNames.get(call.name);
442
- let targets: Array<{ id: number; file: string }> | undefined;
443
- if (importedFrom) {
444
- targets = stmts.findNodeInFile.all(call.name, importedFrom) as Array<{
445
- id: number;
446
- file: string;
447
- }>;
448
- }
449
- if (!targets || targets.length === 0) {
450
- targets = stmts.findNodeInFile.all(call.name, relPath) as Array<{ id: number; file: string }>;
451
- if (targets.length === 0) {
452
- targets = stmts.findNodeByName.all(call.name) as Array<{ id: number; file: string }>;
441
+ symbols: ExtractorOutput,
442
+ ): number {
443
+ let edgesAdded = 0;
444
+ for (const cls of symbols.classes) {
445
+ const sourceRow = (stmts.findNodeInFile.all(cls.name, relPath) as NodeWithKind[]).find((n) =>
446
+ HIERARCHY_SOURCE_KINDS.has(n.kind),
447
+ );
448
+ if (!sourceRow) continue;
449
+
450
+ if (cls.extends) {
451
+ for (const t of (stmts.findNodeByName.all(cls.extends) as NodeWithKind[]).filter((n) =>
452
+ EXTENDS_TARGET_KINDS.has(n.kind),
453
+ )) {
454
+ stmts.insertEdge.run(sourceRow.id, t.id, 'extends', 1.0, 0);
455
+ edgesAdded++;
456
+ }
453
457
  }
454
- }
455
- // Type-aware resolution: translate variable receiver to declared type
456
- if ((!targets || targets.length === 0) && call.receiver && typeMap) {
457
- const typeEntry = typeMap.get(call.receiver);
458
- const typeName = typeEntry
459
- ? typeof typeEntry === 'string'
460
- ? typeEntry
461
- : (typeEntry as { type?: string }).type
462
- : null;
463
- if (typeName) {
464
- const qualified = `${typeName}.${call.name}`;
465
- targets = stmts.findNodeByName.all(qualified) as Array<{ id: number; file: string }>;
458
+ if (cls.implements) {
459
+ for (const t of (stmts.findNodeByName.all(cls.implements) as NodeWithKind[]).filter((n) =>
460
+ IMPLEMENTS_TARGET_KINDS.has(n.kind),
461
+ )) {
462
+ stmts.insertEdge.run(sourceRow.id, t.id, 'implements', 1.0, 0);
463
+ edgesAdded++;
464
+ }
466
465
  }
467
466
  }
468
- return { targets: targets ?? [], importedFrom };
467
+ return edgesAdded;
468
+ }
469
+
470
+ // ── Call edge building ──────────────────────────────────────────────────
471
+
472
+ function makeIncrementalLookup(db: BetterSqlite3Database, stmts: IncrementalStmts): CallNodeLookup {
473
+ return {
474
+ byNameAndFile: (name, file) =>
475
+ stmts.findNodeInFile.all(name, file) as Array<{ id: number; file: string; kind?: string }>,
476
+ byName: (name) =>
477
+ stmts.findNodeByName.all(name) as Array<{ id: number; file: string; kind?: string }>,
478
+ isBarrel: (file) => isBarrelFile(db, file),
479
+ resolveBarrel: (barrelFile, symbolName) => resolveBarrelTarget(db, barrelFile, symbolName),
480
+ nodeId: (name, kind, file, line) =>
481
+ stmts.getNodeId.get(name, kind, file, line) as { id: number } | undefined,
482
+ };
469
483
  }
470
484
 
471
485
  function buildCallEdges(
486
+ db: BetterSqlite3Database,
472
487
  stmts: IncrementalStmts,
473
488
  relPath: string,
474
489
  symbols: ExtractorOutput,
@@ -487,26 +502,130 @@ function buildCallEdges(
487
502
  ]),
488
503
  )
489
504
  : new Map();
505
+
506
+ // Phase 8.3f: seed typeMap[callee::restName] = { type: argName } from
507
+ // objectRestParamBindings × paramBindings, mirroring buildObjectRestParamPostPass.
508
+ // Scoped keys prevent same-name rest-param collisions when two functions in
509
+ // the same file both use `...rest` (#1358). The unscoped key is also seeded
510
+ // when only one callee uses a given rest name, preserving resolution when
511
+ // callerName is null (findCaller couldn't identify the enclosing function).
512
+ if (symbols.objectRestParamBindings?.length && symbols.paramBindings?.length) {
513
+ const restNameCallees = new Map<string, Set<string>>();
514
+ for (const orpb of symbols.objectRestParamBindings) {
515
+ if (!restNameCallees.has(orpb.restName)) restNameCallees.set(orpb.restName, new Set());
516
+ restNameCallees.get(orpb.restName)!.add(orpb.callee);
517
+ }
518
+ for (const orpb of symbols.objectRestParamBindings) {
519
+ for (const pb of symbols.paramBindings) {
520
+ if (pb.callee === orpb.callee && pb.argIndex === orpb.argIndex) {
521
+ const scopedKey = `${orpb.callee}::${orpb.restName}`;
522
+ if (!typeMap.has(scopedKey)) {
523
+ typeMap.set(scopedKey, { type: pb.argName, confidence: 0.65 });
524
+ if (restNameCallees.get(orpb.restName)!.size === 1 && !typeMap.has(orpb.restName)) {
525
+ typeMap.set(orpb.restName, { type: pb.argName, confidence: 0.65 });
526
+ }
527
+ }
528
+ }
529
+ }
530
+ }
531
+ }
532
+
533
+ const seenCallEdges = new Set<string>();
534
+ const lookup = makeIncrementalLookup(db, stmts);
490
535
  let edgesAdded = 0;
536
+
491
537
  for (const call of symbols.calls) {
492
538
  if (call.receiver && BUILTIN_RECEIVERS.has(call.receiver)) continue;
493
539
 
494
- const caller = findCaller(call, symbols.definitions, relPath, stmts) || fileNodeRow;
495
- const { targets, importedFrom } = resolveCallTargets(
496
- stmts,
540
+ const caller = findCaller(lookup, call, symbols.definitions, relPath, fileNodeRow);
541
+ const { targets: initialTargets, importedFrom } = resolveCallTargets(
542
+ lookup,
497
543
  call,
498
544
  relPath,
499
545
  importedNames,
500
546
  typeMap,
547
+ caller.callerName,
501
548
  );
549
+ let targets = initialTargets;
550
+
551
+ if (targets.length === 0 && call.receiver === 'this' && caller.callerName != null) {
552
+ const dotIdx = caller.callerName.indexOf('.');
553
+ if (dotIdx > 0) {
554
+ const className = caller.callerName.slice(0, dotIdx);
555
+ const qualifiedName = `${className}.${call.name}`;
556
+ const qualified = lookup
557
+ .byNameAndFile(qualifiedName, relPath)
558
+ .filter((n) => n.kind === 'method');
559
+ if (qualified.length > 0) {
560
+ targets = qualified;
561
+ }
562
+ }
563
+ }
564
+
565
+ if (
566
+ targets.length === 0 &&
567
+ call.receiver === 'this' &&
568
+ caller.callerName != null &&
569
+ symbols.definePropertyReceivers
570
+ ) {
571
+ const receiverVarName = symbols.definePropertyReceivers.get(caller.callerName);
572
+ if (receiverVarName) {
573
+ const typeEntry = typeMap.get(receiverVarName);
574
+ const typeName = typeEntry
575
+ ? typeof typeEntry === 'string'
576
+ ? typeEntry
577
+ : (typeEntry as { type?: string }).type
578
+ : null;
579
+ if (typeName) {
580
+ const qualifiedName = `${typeName}.${call.name}`;
581
+ const qualified = lookup.byNameAndFile(qualifiedName, relPath);
582
+ if (qualified.length > 0) {
583
+ targets = [...qualified];
584
+ }
585
+ }
586
+ if (targets.length === 0) {
587
+ // Narrow to function/method kinds only to avoid matching unrelated
588
+ // variables or classes that share a name in the same file.
589
+ const sameFile = lookup
590
+ .byNameAndFile(call.name, relPath)
591
+ .filter((n) => n.kind === 'function' || n.kind === 'method');
592
+ if (sameFile.length > 0) {
593
+ targets = [...sameFile];
594
+ }
595
+ }
596
+ }
597
+ }
502
598
 
503
599
  for (const t of targets) {
504
- if (t.id !== caller.id) {
600
+ const edgeKey = `${caller.id}|${t.id}`;
601
+ if (t.id !== caller.id && !seenCallEdges.has(edgeKey)) {
602
+ seenCallEdges.add(edgeKey);
505
603
  const confidence = computeConfidence(relPath, t.file, importedFrom ?? null);
506
604
  stmts.insertEdge.run(caller.id, t.id, 'calls', confidence, call.dynamic ? 1 : 0);
507
605
  edgesAdded++;
508
606
  }
509
607
  }
608
+
609
+ if (
610
+ call.receiver &&
611
+ !BUILTIN_RECEIVERS.has(call.receiver) &&
612
+ call.receiver !== 'this' &&
613
+ call.receiver !== 'self' &&
614
+ call.receiver !== 'super'
615
+ ) {
616
+ const recv = resolveReceiverEdge(
617
+ lookup,
618
+ { name: call.name, receiver: call.receiver },
619
+ caller,
620
+ relPath,
621
+ typeMap,
622
+ seenCallEdges,
623
+ );
624
+ if (recv) {
625
+ stmts.insertEdge.run(recv.callerId, recv.receiverId, 'receiver', recv.confidence, 0);
626
+ edgesAdded++;
627
+ }
628
+ }
510
629
  }
511
630
  return edgesAdded;
512
631
  }
@@ -549,8 +668,9 @@ function rebuildEdgesForTargetFile(
549
668
  let edgesAdded = buildContainmentEdges(db, stmts, relPath, symbols);
550
669
  edgesAdded += rebuildDirContainment(db, stmts, relPath);
551
670
  edgesAdded += buildImportEdges(stmts, relPath, symbols, rootDir, fileNodeRow.id, aliases, db);
552
- const importedNames = buildImportedNamesMap(symbols, rootDir, relPath, aliases);
553
- edgesAdded += buildCallEdges(stmts, relPath, symbols, fileNodeRow, importedNames);
671
+ const importedNames = buildImportedNamesMap(symbols, rootDir, relPath, aliases, db);
672
+ edgesAdded += buildCallEdges(db, stmts, relPath, symbols, fileNodeRow, importedNames);
673
+ edgesAdded += buildClassHierarchyEdges(stmts, relPath, symbols);
554
674
  return edgesAdded;
555
675
  }
556
676