@optave/codegraph 3.11.0 → 3.11.2

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 (230) hide show
  1. package/README.md +38 -31
  2. package/dist/ast-analysis/engine.d.ts.map +1 -1
  3. package/dist/ast-analysis/engine.js +91 -60
  4. package/dist/ast-analysis/engine.js.map +1 -1
  5. package/dist/ast-analysis/visitor-utils.d.ts +3 -0
  6. package/dist/ast-analysis/visitor-utils.d.ts.map +1 -1
  7. package/dist/ast-analysis/visitor-utils.js +83 -49
  8. package/dist/ast-analysis/visitor-utils.js.map +1 -1
  9. package/dist/ast-analysis/visitors/ast-store-visitor.d.ts.map +1 -1
  10. package/dist/ast-analysis/visitors/ast-store-visitor.js +78 -62
  11. package/dist/ast-analysis/visitors/ast-store-visitor.js.map +1 -1
  12. package/dist/ast-analysis/visitors/dataflow-visitor.d.ts.map +1 -1
  13. package/dist/ast-analysis/visitors/dataflow-visitor.js +61 -42
  14. package/dist/ast-analysis/visitors/dataflow-visitor.js.map +1 -1
  15. package/dist/cli/commands/embed.d.ts.map +1 -1
  16. package/dist/cli/commands/embed.js +49 -4
  17. package/dist/cli/commands/embed.js.map +1 -1
  18. package/dist/domain/analysis/dependencies.d.ts.map +1 -1
  19. package/dist/domain/analysis/dependencies.js +106 -80
  20. package/dist/domain/analysis/dependencies.js.map +1 -1
  21. package/dist/domain/analysis/fn-impact.d.ts.map +1 -1
  22. package/dist/domain/analysis/fn-impact.js +77 -52
  23. package/dist/domain/analysis/fn-impact.js.map +1 -1
  24. package/dist/domain/analysis/module-map.d.ts.map +1 -1
  25. package/dist/domain/analysis/module-map.js +132 -121
  26. package/dist/domain/analysis/module-map.js.map +1 -1
  27. package/dist/domain/graph/builder/call-resolver.d.ts +71 -0
  28. package/dist/domain/graph/builder/call-resolver.d.ts.map +1 -0
  29. package/dist/domain/graph/builder/call-resolver.js +130 -0
  30. package/dist/domain/graph/builder/call-resolver.js.map +1 -0
  31. package/dist/domain/graph/builder/helpers.d.ts +4 -4
  32. package/dist/domain/graph/builder/helpers.d.ts.map +1 -1
  33. package/dist/domain/graph/builder/helpers.js +47 -33
  34. package/dist/domain/graph/builder/helpers.js.map +1 -1
  35. package/dist/domain/graph/builder/incremental.d.ts +6 -0
  36. package/dist/domain/graph/builder/incremental.d.ts.map +1 -1
  37. package/dist/domain/graph/builder/incremental.js +214 -127
  38. package/dist/domain/graph/builder/incremental.js.map +1 -1
  39. package/dist/domain/graph/builder/pipeline.d.ts +1 -44
  40. package/dist/domain/graph/builder/pipeline.d.ts.map +1 -1
  41. package/dist/domain/graph/builder/pipeline.js +10 -766
  42. package/dist/domain/graph/builder/pipeline.js.map +1 -1
  43. package/dist/domain/graph/builder/stages/build-edges.d.ts.map +1 -1
  44. package/dist/domain/graph/builder/stages/build-edges.js +151 -192
  45. package/dist/domain/graph/builder/stages/build-edges.js.map +1 -1
  46. package/dist/domain/graph/builder/stages/build-structure.d.ts.map +1 -1
  47. package/dist/domain/graph/builder/stages/build-structure.js +82 -65
  48. package/dist/domain/graph/builder/stages/build-structure.js.map +1 -1
  49. package/dist/domain/graph/builder/stages/detect-changes.d.ts.map +1 -1
  50. package/dist/domain/graph/builder/stages/detect-changes.js +84 -56
  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 +60 -51
  54. package/dist/domain/graph/builder/stages/finalize.js.map +1 -1
  55. package/dist/domain/graph/builder/stages/insert-nodes.d.ts +8 -6
  56. package/dist/domain/graph/builder/stages/insert-nodes.d.ts.map +1 -1
  57. package/dist/domain/graph/builder/stages/insert-nodes.js +107 -122
  58. package/dist/domain/graph/builder/stages/insert-nodes.js.map +1 -1
  59. package/dist/domain/graph/builder/stages/native-db-lifecycle.d.ts +14 -0
  60. package/dist/domain/graph/builder/stages/native-db-lifecycle.d.ts.map +1 -0
  61. package/dist/domain/graph/builder/stages/native-db-lifecycle.js +77 -0
  62. package/dist/domain/graph/builder/stages/native-db-lifecycle.js.map +1 -0
  63. package/dist/domain/graph/builder/stages/native-orchestrator.d.ts +62 -0
  64. package/dist/domain/graph/builder/stages/native-orchestrator.d.ts.map +1 -0
  65. package/dist/domain/graph/builder/stages/native-orchestrator.js +747 -0
  66. package/dist/domain/graph/builder/stages/native-orchestrator.js.map +1 -0
  67. package/dist/domain/graph/cycles.d.ts +6 -4
  68. package/dist/domain/graph/cycles.d.ts.map +1 -1
  69. package/dist/domain/graph/cycles.js +50 -55
  70. package/dist/domain/graph/cycles.js.map +1 -1
  71. package/dist/domain/graph/journal.d.ts.map +1 -1
  72. package/dist/domain/graph/journal.js +89 -70
  73. package/dist/domain/graph/journal.js.map +1 -1
  74. package/dist/domain/graph/watcher.d.ts.map +1 -1
  75. package/dist/domain/graph/watcher.js +10 -4
  76. package/dist/domain/graph/watcher.js.map +1 -1
  77. package/dist/domain/parser.d.ts +12 -23
  78. package/dist/domain/parser.d.ts.map +1 -1
  79. package/dist/domain/parser.js +126 -79
  80. package/dist/domain/parser.js.map +1 -1
  81. package/dist/domain/search/generator.d.ts +3 -1
  82. package/dist/domain/search/generator.d.ts.map +1 -1
  83. package/dist/domain/search/generator.js +68 -45
  84. package/dist/domain/search/generator.js.map +1 -1
  85. package/dist/domain/search/models.d.ts +2 -0
  86. package/dist/domain/search/models.d.ts.map +1 -1
  87. package/dist/domain/search/models.js +37 -3
  88. package/dist/domain/search/models.js.map +1 -1
  89. package/dist/domain/search/search/hybrid.d.ts.map +1 -1
  90. package/dist/domain/search/search/hybrid.js +49 -40
  91. package/dist/domain/search/search/hybrid.js.map +1 -1
  92. package/dist/domain/search/search/semantic.d.ts.map +1 -1
  93. package/dist/domain/search/search/semantic.js +69 -49
  94. package/dist/domain/search/search/semantic.js.map +1 -1
  95. package/dist/domain/wasm-worker-entry.js +201 -136
  96. package/dist/domain/wasm-worker-entry.js.map +1 -1
  97. package/dist/extractors/elixir.js +95 -71
  98. package/dist/extractors/elixir.js.map +1 -1
  99. package/dist/extractors/gleam.d.ts.map +1 -1
  100. package/dist/extractors/gleam.js +23 -31
  101. package/dist/extractors/gleam.js.map +1 -1
  102. package/dist/extractors/helpers.d.ts +79 -1
  103. package/dist/extractors/helpers.d.ts.map +1 -1
  104. package/dist/extractors/helpers.js +137 -0
  105. package/dist/extractors/helpers.js.map +1 -1
  106. package/dist/extractors/java.d.ts.map +1 -1
  107. package/dist/extractors/java.js +37 -49
  108. package/dist/extractors/java.js.map +1 -1
  109. package/dist/extractors/javascript.d.ts.map +1 -1
  110. package/dist/extractors/javascript.js +44 -44
  111. package/dist/extractors/javascript.js.map +1 -1
  112. package/dist/extractors/julia.js +27 -34
  113. package/dist/extractors/julia.js.map +1 -1
  114. package/dist/extractors/r.d.ts.map +1 -1
  115. package/dist/extractors/r.js +33 -58
  116. package/dist/extractors/r.js.map +1 -1
  117. package/dist/extractors/solidity.d.ts.map +1 -1
  118. package/dist/extractors/solidity.js +38 -61
  119. package/dist/extractors/solidity.js.map +1 -1
  120. package/dist/features/boundaries.d.ts.map +1 -1
  121. package/dist/features/boundaries.js +49 -39
  122. package/dist/features/boundaries.js.map +1 -1
  123. package/dist/features/cfg.d.ts.map +1 -1
  124. package/dist/features/cfg.js +90 -63
  125. package/dist/features/cfg.js.map +1 -1
  126. package/dist/features/check.d.ts.map +1 -1
  127. package/dist/features/check.js +43 -34
  128. package/dist/features/check.js.map +1 -1
  129. package/dist/features/cochange.d.ts.map +1 -1
  130. package/dist/features/cochange.js +68 -56
  131. package/dist/features/cochange.js.map +1 -1
  132. package/dist/features/complexity.d.ts.map +1 -1
  133. package/dist/features/complexity.js +105 -75
  134. package/dist/features/complexity.js.map +1 -1
  135. package/dist/features/dataflow.d.ts.map +1 -1
  136. package/dist/features/dataflow.js +37 -29
  137. package/dist/features/dataflow.js.map +1 -1
  138. package/dist/features/flow.d.ts.map +1 -1
  139. package/dist/features/flow.js +31 -22
  140. package/dist/features/flow.js.map +1 -1
  141. package/dist/features/graph-enrichment.d.ts.map +1 -1
  142. package/dist/features/graph-enrichment.js +77 -70
  143. package/dist/features/graph-enrichment.js.map +1 -1
  144. package/dist/features/owners.d.ts +17 -26
  145. package/dist/features/owners.d.ts.map +1 -1
  146. package/dist/features/owners.js +120 -109
  147. package/dist/features/owners.js.map +1 -1
  148. package/dist/features/sequence.d.ts.map +1 -1
  149. package/dist/features/sequence.js +59 -54
  150. package/dist/features/sequence.js.map +1 -1
  151. package/dist/features/structure-query.d.ts.map +1 -1
  152. package/dist/features/structure-query.js +60 -60
  153. package/dist/features/structure-query.js.map +1 -1
  154. package/dist/features/structure.d.ts.map +1 -1
  155. package/dist/features/structure.js +149 -52
  156. package/dist/features/structure.js.map +1 -1
  157. package/dist/graph/algorithms/leiden/optimiser.d.ts.map +1 -1
  158. package/dist/graph/algorithms/leiden/optimiser.js +100 -69
  159. package/dist/graph/algorithms/leiden/optimiser.js.map +1 -1
  160. package/dist/graph/classifiers/roles.d.ts.map +1 -1
  161. package/dist/graph/classifiers/roles.js +63 -59
  162. package/dist/graph/classifiers/roles.js.map +1 -1
  163. package/dist/infrastructure/config.d.ts +1 -1
  164. package/dist/infrastructure/config.d.ts.map +1 -1
  165. package/dist/infrastructure/config.js +1 -1
  166. package/dist/infrastructure/config.js.map +1 -1
  167. package/dist/presentation/cfg.d.ts.map +1 -1
  168. package/dist/presentation/cfg.js +44 -29
  169. package/dist/presentation/cfg.js.map +1 -1
  170. package/dist/presentation/flow.d.ts.map +1 -1
  171. package/dist/presentation/flow.js +58 -38
  172. package/dist/presentation/flow.js.map +1 -1
  173. package/dist/types.d.ts +1 -1
  174. package/dist/types.d.ts.map +1 -1
  175. package/grammars/tree-sitter-erlang.wasm +0 -0
  176. package/package.json +9 -9
  177. package/src/ast-analysis/engine.ts +145 -61
  178. package/src/ast-analysis/visitor-utils.ts +86 -46
  179. package/src/ast-analysis/visitors/ast-store-visitor.ts +104 -69
  180. package/src/ast-analysis/visitors/dataflow-visitor.ts +86 -47
  181. package/src/cli/commands/embed.ts +54 -4
  182. package/src/domain/analysis/dependencies.ts +166 -85
  183. package/src/domain/analysis/fn-impact.ts +120 -50
  184. package/src/domain/analysis/module-map.ts +175 -140
  185. package/src/domain/graph/builder/call-resolver.ts +181 -0
  186. package/src/domain/graph/builder/helpers.ts +85 -76
  187. package/src/domain/graph/builder/incremental.ts +321 -152
  188. package/src/domain/graph/builder/pipeline.ts +19 -957
  189. package/src/domain/graph/builder/stages/build-edges.ts +229 -275
  190. package/src/domain/graph/builder/stages/build-structure.ts +115 -82
  191. package/src/domain/graph/builder/stages/detect-changes.ts +107 -64
  192. package/src/domain/graph/builder/stages/finalize.ts +72 -70
  193. package/src/domain/graph/builder/stages/insert-nodes.ts +154 -120
  194. package/src/domain/graph/builder/stages/native-db-lifecycle.ts +74 -0
  195. package/src/domain/graph/builder/stages/native-orchestrator.ts +942 -0
  196. package/src/domain/graph/cycles.ts +51 -49
  197. package/src/domain/graph/journal.ts +84 -69
  198. package/src/domain/graph/watcher.ts +12 -4
  199. package/src/domain/parser.ts +143 -66
  200. package/src/domain/search/generator.ts +132 -74
  201. package/src/domain/search/models.ts +39 -3
  202. package/src/domain/search/search/hybrid.ts +53 -42
  203. package/src/domain/search/search/semantic.ts +105 -65
  204. package/src/domain/wasm-worker-entry.ts +235 -152
  205. package/src/extractors/elixir.ts +91 -64
  206. package/src/extractors/gleam.ts +33 -37
  207. package/src/extractors/helpers.ts +205 -1
  208. package/src/extractors/java.ts +42 -45
  209. package/src/extractors/javascript.ts +44 -43
  210. package/src/extractors/julia.ts +28 -35
  211. package/src/extractors/r.ts +38 -56
  212. package/src/extractors/solidity.ts +43 -71
  213. package/src/features/boundaries.ts +64 -46
  214. package/src/features/cfg.ts +145 -74
  215. package/src/features/check.ts +60 -43
  216. package/src/features/cochange.ts +95 -72
  217. package/src/features/complexity.ts +134 -79
  218. package/src/features/dataflow.ts +57 -34
  219. package/src/features/flow.ts +48 -24
  220. package/src/features/graph-enrichment.ts +105 -70
  221. package/src/features/owners.ts +186 -146
  222. package/src/features/sequence.ts +99 -69
  223. package/src/features/structure-query.ts +94 -79
  224. package/src/features/structure.ts +199 -79
  225. package/src/graph/algorithms/leiden/optimiser.ts +142 -87
  226. package/src/graph/classifiers/roles.ts +64 -54
  227. package/src/infrastructure/config.ts +1 -1
  228. package/src/presentation/cfg.ts +48 -32
  229. package/src/presentation/flow.ts +100 -52
  230. package/src/types.ts +1 -1
@@ -20,6 +20,12 @@ import type {
20
20
  TypeMapEntry,
21
21
  } from '../../../../types.js';
22
22
  import { computeConfidence } from '../../resolve.js';
23
+ import {
24
+ type CallNodeLookup,
25
+ findCaller,
26
+ resolveCallTargets,
27
+ resolveReceiverEdge,
28
+ } from '../call-resolver.js';
23
29
  import type { PipelineContext } from '../context.js';
24
30
  import { BUILTIN_RECEIVERS, batchInsertEdges } from '../helpers.js';
25
31
 
@@ -89,12 +95,74 @@ function setupNodeLookups(ctx: PipelineContext, allNodes: QueryNodeRow[]): void
89
95
 
90
96
  // ── Import edges ────────────────────────────────────────────────────────
91
97
 
98
+ /** Pick the edge kind for an import statement based on its modifiers. */
99
+ function importEdgeKind(imp: Import): string {
100
+ if (imp.reexport) return 'reexports';
101
+ if (imp.typeOnly) return 'imports-type';
102
+ if (imp.dynamicImport) return 'dynamic-imports';
103
+ return 'imports';
104
+ }
105
+
106
+ /**
107
+ * For a `import type` statement, emit symbol-level `imports-type` edges so
108
+ * the target symbols get fan-in credit and aren't classified as dead code.
109
+ */
110
+ function emitTypeOnlySymbolEdges(
111
+ ctx: PipelineContext,
112
+ imp: Import,
113
+ resolvedPath: string,
114
+ fileNodeId: number,
115
+ allEdgeRows: EdgeRowTuple[],
116
+ ): void {
117
+ if (!ctx.nodesByNameAndFile) return;
118
+ for (const name of imp.names) {
119
+ const cleanName = name.replace(/^\*\s+as\s+/, '');
120
+ let targetFile = resolvedPath;
121
+ if (isBarrelFile(ctx, resolvedPath)) {
122
+ const actual = resolveBarrelExport(ctx, resolvedPath, cleanName);
123
+ if (actual) targetFile = actual;
124
+ }
125
+ const candidates = ctx.nodesByNameAndFile.get(`${cleanName}|${targetFile}`);
126
+ if (candidates && candidates.length > 0) {
127
+ allEdgeRows.push([fileNodeId, candidates[0]!.id, 'imports-type', 1.0, 0]);
128
+ }
129
+ }
130
+ }
131
+
132
+ /**
133
+ * Process a single import statement and emit all resulting edges (file→file,
134
+ * type-only symbol-level, and barrel re-export targets).
135
+ */
136
+ function emitEdgesForImport(
137
+ ctx: PipelineContext,
138
+ imp: Import,
139
+ fileNodeId: number,
140
+ relPath: string,
141
+ getNodeIdStmt: NodeIdStmt,
142
+ allEdgeRows: EdgeRowTuple[],
143
+ ): void {
144
+ const resolvedPath = getResolved(ctx, path.join(ctx.rootDir, relPath), imp.source);
145
+ const targetRow = getNodeIdStmt.get(resolvedPath, 'file', resolvedPath, 0);
146
+ if (!targetRow) return;
147
+
148
+ const edgeKind = importEdgeKind(imp);
149
+ allEdgeRows.push([fileNodeId, targetRow.id, edgeKind, 1.0, 0]);
150
+
151
+ if (imp.typeOnly) {
152
+ emitTypeOnlySymbolEdges(ctx, imp, resolvedPath, fileNodeId, allEdgeRows);
153
+ }
154
+
155
+ if (!imp.reexport && isBarrelFile(ctx, resolvedPath)) {
156
+ buildBarrelEdges(ctx, imp, resolvedPath, fileNodeId, edgeKind, getNodeIdStmt, allEdgeRows);
157
+ }
158
+ }
159
+
92
160
  function buildImportEdges(
93
161
  ctx: PipelineContext,
94
162
  getNodeIdStmt: NodeIdStmt,
95
163
  allEdgeRows: EdgeRowTuple[],
96
164
  ): void {
97
- const { fileSymbols, barrelOnlyFiles, rootDir } = ctx;
165
+ const { fileSymbols, barrelOnlyFiles } = ctx;
98
166
 
99
167
  for (const [relPath, symbols] of fileSymbols) {
100
168
  const isBarrelOnly = barrelOnlyFiles.has(relPath);
@@ -105,40 +173,7 @@ function buildImportEdges(
105
173
  for (const imp of symbols.imports) {
106
174
  // Barrel-only files: only emit reexport edges, skip regular imports
107
175
  if (isBarrelOnly && !imp.reexport) continue;
108
-
109
- const resolvedPath = getResolved(ctx, path.join(rootDir, relPath), imp.source);
110
- const targetRow = getNodeIdStmt.get(resolvedPath, 'file', resolvedPath, 0);
111
- if (!targetRow) continue;
112
-
113
- const edgeKind = imp.reexport
114
- ? 'reexports'
115
- : imp.typeOnly
116
- ? 'imports-type'
117
- : imp.dynamicImport
118
- ? 'dynamic-imports'
119
- : 'imports';
120
- allEdgeRows.push([fileNodeId, targetRow.id, edgeKind, 1.0, 0]);
121
-
122
- // Type-only imports: create symbol-level edges so the target symbols
123
- // get fan-in credit and aren't falsely classified as dead code.
124
- if (imp.typeOnly && ctx.nodesByNameAndFile) {
125
- for (const name of imp.names) {
126
- const cleanName = name.replace(/^\*\s+as\s+/, '');
127
- let targetFile = resolvedPath;
128
- if (isBarrelFile(ctx, resolvedPath)) {
129
- const actual = resolveBarrelExport(ctx, resolvedPath, cleanName);
130
- if (actual) targetFile = actual;
131
- }
132
- const candidates = ctx.nodesByNameAndFile.get(`${cleanName}|${targetFile}`);
133
- if (candidates && candidates.length > 0) {
134
- allEdgeRows.push([fileNodeId, candidates[0]!.id, 'imports-type', 1.0, 0]);
135
- }
136
- }
137
- }
138
-
139
- if (!imp.reexport && isBarrelFile(ctx, resolvedPath)) {
140
- buildBarrelEdges(ctx, imp, resolvedPath, fileNodeId, edgeKind, getNodeIdStmt, allEdgeRows);
141
- }
176
+ emitEdgesForImport(ctx, imp, fileNodeId, relPath, getNodeIdStmt, allEdgeRows);
142
177
  }
143
178
  }
144
179
  }
@@ -174,83 +209,98 @@ function buildBarrelEdges(
174
209
 
175
210
  // ── Import edges (native engine) ────────────────────────────────────────
176
211
 
177
- function buildImportEdgesNative(
178
- ctx: PipelineContext,
179
- getNodeIdStmt: NodeIdStmt,
180
- allEdgeRows: EdgeRowTuple[],
181
- native: NativeAddon,
182
- ): void {
183
- const { fileSymbols, barrelOnlyFiles, rootDir } = ctx;
212
+ /** Native FFI input shape for a single import statement. */
213
+ interface NativeImportInfo {
214
+ source: string;
215
+ names: string[];
216
+ reexport: boolean;
217
+ typeOnly: boolean;
218
+ dynamicImport: boolean;
219
+ wildcardReexport: boolean;
220
+ }
184
221
 
185
- // 1. Build per-file input data
186
- const files: Array<{
187
- file: string;
188
- fileNodeId: number;
189
- isBarrelOnly: boolean;
190
- imports: Array<{
191
- source: string;
192
- names: string[];
193
- reexport: boolean;
194
- typeOnly: boolean;
195
- dynamicImport: boolean;
196
- wildcardReexport: boolean;
197
- }>;
198
- definitionNames: string[];
199
- }> = [];
200
-
201
- // Collect all file node IDs we'll need (sources + targets)
202
- const fileNodeIds: Array<{ file: string; nodeId: number }> = [];
203
- const seenNodeFiles = new Set<string>();
204
-
205
- const addFileNodeId = (relPath: string): { id: number } | undefined => {
206
- if (seenNodeFiles.has(relPath)) return fileNodeRowCache.get(relPath);
207
- const row = getNodeIdStmt.get(relPath, 'file', relPath, 0);
208
- if (row) {
209
- seenNodeFiles.add(relPath);
210
- fileNodeIds.push({ file: relPath, nodeId: row.id });
211
- fileNodeRowCache.set(relPath, row);
212
- }
213
- return row;
222
+ /** Native FFI input shape for a single file. */
223
+ interface NativeFileInput {
224
+ file: string;
225
+ fileNodeId: number;
226
+ isBarrelOnly: boolean;
227
+ imports: NativeImportInfo[];
228
+ definitionNames: string[];
229
+ }
230
+
231
+ /** Native FFI input shape for re-exports of a single file. */
232
+ interface NativeReexportInput {
233
+ file: string;
234
+ reexports: Array<{ source: string; names: string[]; wildcardReexport: boolean }>;
235
+ }
236
+
237
+ /** Lazily-resolving cache of file-node rows for the native input arrays. */
238
+ interface FileNodeIdRegistry {
239
+ ids: Array<{ file: string; nodeId: number }>;
240
+ add(relPath: string): { id: number } | undefined;
241
+ }
242
+
243
+ function createFileNodeIdRegistry(getNodeIdStmt: NodeIdStmt): FileNodeIdRegistry {
244
+ const ids: Array<{ file: string; nodeId: number }> = [];
245
+ const seen = new Set<string>();
246
+ const cache = new Map<string, { id: number }>();
247
+ return {
248
+ ids,
249
+ add(relPath: string) {
250
+ if (seen.has(relPath)) return cache.get(relPath);
251
+ const row = getNodeIdStmt.get(relPath, 'file', relPath, 0);
252
+ if (row) {
253
+ seen.add(relPath);
254
+ ids.push({ file: relPath, nodeId: row.id });
255
+ cache.set(relPath, row);
256
+ }
257
+ return row;
258
+ },
214
259
  };
215
- const fileNodeRowCache = new Map<string, { id: number }>();
260
+ }
216
261
 
217
- // 2. Pre-resolve all imports and build resolved imports array.
218
- // Keys use forward-slash-normalized rootDir + "/" + relPath to match the Rust
219
- // lookup format (format!("{}/{}", root_dir.replace('\\', "/"), file)).
220
- // On Windows, rootDir has backslashes but Rust normalizes them — the JS side
221
- // must do the same or every resolve key lookup misses (#750).
222
- const resolvedImports: Array<{ key: string; resolvedPath: string }> = [];
262
+ function toNativeImportInfo(imp: Import): NativeImportInfo {
263
+ return {
264
+ source: imp.source,
265
+ names: imp.names,
266
+ reexport: !!imp.reexport,
267
+ typeOnly: !!imp.typeOnly,
268
+ dynamicImport: !!imp.dynamicImport,
269
+ wildcardReexport: !!imp.wildcardReexport,
270
+ };
271
+ }
272
+
273
+ /**
274
+ * Pre-resolve every import for the given files, registering each resolved
275
+ * target with the registry so the native side has full node-id coverage.
276
+ *
277
+ * Resolved-import keys use forward-slash-normalized rootDir + "/" + relPath to
278
+ * match the Rust lookup format. On Windows, rootDir has backslashes but Rust
279
+ * normalizes them — the JS side must do the same or every key lookup misses
280
+ * (#750).
281
+ */
282
+ function buildNativeFileInputs(
283
+ ctx: PipelineContext,
284
+ registry: FileNodeIdRegistry,
285
+ ): {
286
+ files: NativeFileInput[];
287
+ resolvedImports: Array<{ key: string; resolvedPath: string }>;
288
+ } {
289
+ const { fileSymbols, barrelOnlyFiles, rootDir } = ctx;
223
290
  const fwdRootDir = rootDir.replace(/\\/g, '/');
291
+ const files: NativeFileInput[] = [];
292
+ const resolvedImports: Array<{ key: string; resolvedPath: string }> = [];
224
293
 
225
294
  for (const [relPath, symbols] of fileSymbols) {
226
- const fileNodeRow = addFileNodeId(relPath);
295
+ const fileNodeRow = registry.add(relPath);
227
296
  if (!fileNodeRow) continue;
228
297
 
229
- const importInfos: Array<{
230
- source: string;
231
- names: string[];
232
- reexport: boolean;
233
- typeOnly: boolean;
234
- dynamicImport: boolean;
235
- wildcardReexport: boolean;
236
- }> = [];
237
-
298
+ const importInfos: NativeImportInfo[] = [];
238
299
  for (const imp of symbols.imports) {
239
- // Pre-resolve and register target file node
240
300
  const resolvedPath = getResolved(ctx, path.join(rootDir, relPath), imp.source);
241
- addFileNodeId(resolvedPath);
242
-
243
- // Key matches Rust's format!("{}/{}", root_dir.replace('\\', "/"), file_input.file)
301
+ registry.add(resolvedPath);
244
302
  resolvedImports.push({ key: `${fwdRootDir}/${relPath}|${imp.source}`, resolvedPath });
245
-
246
- importInfos.push({
247
- source: imp.source,
248
- names: imp.names,
249
- reexport: !!imp.reexport,
250
- typeOnly: !!imp.typeOnly,
251
- dynamicImport: !!imp.dynamicImport,
252
- wildcardReexport: !!imp.wildcardReexport,
253
- });
303
+ importInfos.push(toNativeImportInfo(imp));
254
304
  }
255
305
 
256
306
  files.push({
@@ -261,61 +311,75 @@ function buildImportEdgesNative(
261
311
  definitionNames: symbols.definitions.map((d) => d.name),
262
312
  });
263
313
  }
314
+ return { files, resolvedImports };
315
+ }
264
316
 
265
- // 4. Flatten reexportMap
266
- const fileReexports: Array<{
267
- file: string;
268
- reexports: Array<{
269
- source: string;
270
- names: string[];
271
- wildcardReexport: boolean;
272
- }>;
273
- }> = [];
274
- if (ctx.reexportMap) {
275
- for (const [file, entries] of ctx.reexportMap) {
276
- const reexports = (
277
- entries as Array<{ source: string; names: string[]; wildcardReexport: boolean }>
278
- ).map((re) => ({
279
- source: re.source,
280
- names: re.names,
281
- wildcardReexport: !!re.wildcardReexport,
282
- }));
283
- fileReexports.push({ file, reexports });
317
+ /** Flatten `ctx.reexportMap` into the array shape the native side expects. */
318
+ function buildNativeReexports(
319
+ ctx: PipelineContext,
320
+ registry: FileNodeIdRegistry,
321
+ ): NativeReexportInput[] {
322
+ const fileReexports: NativeReexportInput[] = [];
323
+ if (!ctx.reexportMap) return fileReexports;
324
+
325
+ for (const [file, entries] of ctx.reexportMap) {
326
+ const reexports = (
327
+ entries as Array<{ source: string; names: string[]; wildcardReexport: boolean }>
328
+ ).map((re) => ({
329
+ source: re.source,
330
+ names: re.names,
331
+ wildcardReexport: !!re.wildcardReexport,
332
+ }));
333
+ fileReexports.push({ file, reexports });
284
334
 
285
- // Register reexport target files for node ID lookup
286
- for (const re of reexports) {
287
- addFileNodeId(re.source);
288
- }
335
+ for (const re of reexports) {
336
+ registry.add(re.source);
289
337
  }
290
338
  }
339
+ return fileReexports;
340
+ }
291
341
 
292
- // 5. Compute barrel file list
342
+ function collectBarrelFiles(ctx: PipelineContext): string[] {
293
343
  const barrelFiles: string[] = [];
294
- for (const [relPath] of fileSymbols) {
295
- if (isBarrelFile(ctx, relPath)) {
296
- barrelFiles.push(relPath);
297
- }
344
+ for (const [relPath] of ctx.fileSymbols) {
345
+ if (isBarrelFile(ctx, relPath)) barrelFiles.push(relPath);
298
346
  }
347
+ return barrelFiles;
348
+ }
299
349
 
300
- // 6. Build symbol node entries for type-only import resolution
350
+ function collectSymbolNodes(
351
+ ctx: PipelineContext,
352
+ ): Array<{ name: string; file: string; nodeId: number }> {
301
353
  const symbolNodes: Array<{ name: string; file: string; nodeId: number }> = [];
302
- if (ctx.nodesByNameAndFile) {
303
- for (const [key, nodes] of ctx.nodesByNameAndFile) {
304
- if (nodes.length > 0) {
305
- const [name, file] = key.split('|');
306
- symbolNodes.push({ name: name!, file: file!, nodeId: nodes[0]!.id });
307
- }
308
- }
354
+ if (!ctx.nodesByNameAndFile) return symbolNodes;
355
+ for (const [key, nodes] of ctx.nodesByNameAndFile) {
356
+ if (nodes.length === 0) continue;
357
+ const [name, file] = key.split('|');
358
+ symbolNodes.push({ name: name!, file: file!, nodeId: nodes[0]!.id });
309
359
  }
360
+ return symbolNodes;
361
+ }
362
+
363
+ function buildImportEdgesNative(
364
+ ctx: PipelineContext,
365
+ getNodeIdStmt: NodeIdStmt,
366
+ allEdgeRows: EdgeRowTuple[],
367
+ native: NativeAddon,
368
+ ): void {
369
+ const registry = createFileNodeIdRegistry(getNodeIdStmt);
370
+
371
+ const { files, resolvedImports } = buildNativeFileInputs(ctx, registry);
372
+ const fileReexports = buildNativeReexports(ctx, registry);
373
+ const barrelFiles = collectBarrelFiles(ctx);
374
+ const symbolNodes = collectSymbolNodes(ctx);
310
375
 
311
- // 7. Call native
312
376
  const nativeEdges = native.buildImportEdges!(
313
377
  files,
314
378
  resolvedImports,
315
379
  fileReexports,
316
- fileNodeIds,
380
+ registry.ids,
317
381
  barrelFiles,
318
- rootDir,
382
+ ctx.rootDir,
319
383
  symbolNodes,
320
384
  ) as NativeEdge[];
321
385
 
@@ -428,6 +492,7 @@ function buildCallEdgesJS(
428
492
  allEdgeRows: EdgeRowTuple[],
429
493
  ): void {
430
494
  const { fileSymbols, barrelOnlyFiles, rootDir } = ctx;
495
+ const lookup = makeContextLookup(ctx, getNodeIdStmt);
431
496
 
432
497
  for (const [relPath, symbols] of fileSymbols) {
433
498
  if (barrelOnlyFiles.has(relPath)) continue;
@@ -439,13 +504,12 @@ function buildCallEdgesJS(
439
504
  const seenCallEdges = new Set<string>();
440
505
 
441
506
  buildFileCallEdges(
442
- ctx,
443
507
  relPath,
444
508
  symbols,
445
509
  fileNodeRow,
446
510
  importedNames,
447
511
  seenCallEdges,
448
- getNodeIdStmt,
512
+ lookup,
449
513
  allEdgeRows,
450
514
  typeMap,
451
515
  );
@@ -482,127 +546,37 @@ function buildImportedNamesMap(
482
546
  return importedNames;
483
547
  }
484
548
 
485
- function findCaller(
486
- call: Call,
487
- definitions: ReadonlyArray<{ name: string; kind: string; line: number; endLine?: number | null }>,
488
- relPath: string,
489
- getNodeIdStmt: NodeIdStmt,
490
- fileNodeRow: { id: number },
491
- ): { id: number } {
492
- let caller: { id: number } | null = null;
493
- let callerSpan = Infinity;
494
- for (const def of definitions) {
495
- if (def.line <= call.line) {
496
- const end = def.endLine || Infinity;
497
- if (call.line <= end) {
498
- const span = end - def.line;
499
- if (span < callerSpan) {
500
- const row = getNodeIdStmt.get(def.name, def.kind, relPath, def.line);
501
- if (row) {
502
- caller = row;
503
- callerSpan = span;
504
- }
505
- }
506
- }
507
- }
508
- }
509
- return caller || fileNodeRow;
510
- }
511
-
512
- function resolveCallTargets(
513
- ctx: PipelineContext,
514
- call: Call,
515
- relPath: string,
516
- importedNames: Map<string, string>,
517
- typeMap: Map<string, TypeMapEntry | string>,
518
- ): { targets: NodeRow[]; importedFrom: string | undefined } {
519
- const importedFrom = importedNames.get(call.name);
520
- let targets: NodeRow[] | undefined;
521
-
522
- if (importedFrom) {
523
- targets = ctx.nodesByNameAndFile.get(`${call.name}|${importedFrom}`) || [];
524
- if (targets.length === 0 && isBarrelFile(ctx, importedFrom)) {
525
- const actualSource = resolveBarrelExport(ctx, importedFrom, call.name);
526
- if (actualSource) {
527
- targets = ctx.nodesByNameAndFile.get(`${call.name}|${actualSource}`) || [];
528
- }
529
- }
530
- }
531
-
532
- if (!targets || targets.length === 0) {
533
- targets = ctx.nodesByNameAndFile.get(`${call.name}|${relPath}`) || [];
534
- if (targets.length === 0) {
535
- targets = resolveByMethodOrGlobal(ctx, call, relPath, typeMap);
536
- }
537
- }
538
-
539
- if (targets.length > 1) {
540
- targets.sort((a, b) => {
541
- const confA = computeConfidence(relPath, a.file, importedFrom ?? null);
542
- const confB = computeConfidence(relPath, b.file, importedFrom ?? null);
543
- return confB - confA;
544
- });
545
- }
546
-
547
- return { targets, importedFrom };
548
- }
549
-
550
- function resolveByMethodOrGlobal(
551
- ctx: PipelineContext,
552
- call: Call,
553
- relPath: string,
554
- typeMap: Map<string, TypeMapEntry | string>,
555
- ): NodeRow[] {
556
- // Type-aware resolution: translate variable receiver to its declared type
557
- if (call.receiver && typeMap) {
558
- const typeEntry = typeMap.get(call.receiver);
559
- const typeName = typeEntry
560
- ? typeof typeEntry === 'string'
561
- ? typeEntry
562
- : typeEntry.type
563
- : null;
564
- if (typeName) {
565
- const qualifiedName = `${typeName}.${call.name}`;
566
- const typed = (ctx.nodesByName.get(qualifiedName) || []).filter((n) => n.kind === 'method');
567
- if (typed.length > 0) return typed;
568
- }
569
- }
570
-
571
- if (
572
- !call.receiver ||
573
- call.receiver === 'this' ||
574
- call.receiver === 'self' ||
575
- call.receiver === 'super'
576
- ) {
577
- return (ctx.nodesByName.get(call.name) || []).filter(
578
- (n) => computeConfidence(relPath, n.file, null) >= 0.5,
579
- );
580
- }
581
- return [];
549
+ function makeContextLookup(ctx: PipelineContext, getNodeIdStmt: NodeIdStmt): CallNodeLookup {
550
+ return {
551
+ byNameAndFile: (name, file) => ctx.nodesByNameAndFile.get(`${name}|${file}`) ?? [],
552
+ byName: (name) => ctx.nodesByName.get(name) ?? [],
553
+ isBarrel: (file) => isBarrelFile(ctx, file),
554
+ resolveBarrel: (barrelFile, symbolName) => resolveBarrelExport(ctx, barrelFile, symbolName),
555
+ nodeId: (name, kind, file, line) => getNodeIdStmt.get(name, kind, file, line),
556
+ };
582
557
  }
583
558
 
584
559
  function buildFileCallEdges(
585
- ctx: PipelineContext,
586
560
  relPath: string,
587
561
  symbols: ExtractorOutput,
588
562
  fileNodeRow: { id: number },
589
563
  importedNames: Map<string, string>,
590
564
  seenCallEdges: Set<string>,
591
- getNodeIdStmt: NodeIdStmt,
565
+ lookup: CallNodeLookup,
592
566
  allEdgeRows: EdgeRowTuple[],
593
567
  typeMap: Map<string, TypeMapEntry | string>,
594
568
  ): void {
595
569
  for (const call of symbols.calls) {
596
570
  if (call.receiver && BUILTIN_RECEIVERS.has(call.receiver)) continue;
597
571
 
598
- const caller = findCaller(call, symbols.definitions, relPath, getNodeIdStmt, fileNodeRow);
572
+ const caller = findCaller(lookup, call, symbols.definitions, relPath, fileNodeRow);
599
573
  const isDynamic: number = call.dynamic ? 1 : 0;
600
574
  const { targets, importedFrom } = resolveCallTargets(
601
- ctx,
575
+ lookup,
602
576
  call,
603
577
  relPath,
604
578
  importedNames,
605
- typeMap,
579
+ typeMap as Map<string, unknown>,
606
580
  );
607
581
 
608
582
  for (const t of targets) {
@@ -614,7 +588,6 @@ function buildFileCallEdges(
614
588
  }
615
589
  }
616
590
 
617
- // Receiver edge
618
591
  if (
619
592
  call.receiver &&
620
593
  !BUILTIN_RECEIVERS.has(call.receiver) &&
@@ -622,36 +595,17 @@ function buildFileCallEdges(
622
595
  call.receiver !== 'self' &&
623
596
  call.receiver !== 'super'
624
597
  ) {
625
- buildReceiverEdge(ctx, call, caller, relPath, seenCallEdges, allEdgeRows, typeMap);
626
- }
627
- }
628
- }
629
-
630
- function buildReceiverEdge(
631
- ctx: PipelineContext,
632
- call: Call,
633
- caller: { id: number },
634
- relPath: string,
635
- seenCallEdges: Set<string>,
636
- allEdgeRows: EdgeRowTuple[],
637
- typeMap: Map<string, TypeMapEntry | string>,
638
- ): void {
639
- const receiverKinds = new Set(['class', 'struct', 'interface', 'type', 'module']);
640
- const typeEntry = typeMap?.get(call.receiver!);
641
- const typeName = typeEntry ? (typeof typeEntry === 'string' ? typeEntry : typeEntry.type) : null;
642
- const typeConfidence = typeEntry && typeof typeEntry === 'object' ? typeEntry.confidence : null;
643
- const effectiveReceiver = typeName || call.receiver!;
644
- const samefile = ctx.nodesByNameAndFile.get(`${effectiveReceiver}|${relPath}`) || [];
645
- const candidates = samefile.length > 0 ? samefile : ctx.nodesByName.get(effectiveReceiver) || [];
646
- const receiverNodes = candidates.filter((n) => receiverKinds.has(n.kind));
647
- if (receiverNodes.length > 0 && caller) {
648
- const recvTarget = receiverNodes[0]!;
649
- const recvKey = `recv|${caller.id}|${recvTarget.id}`;
650
- if (!seenCallEdges.has(recvKey)) {
651
- seenCallEdges.add(recvKey);
652
- // Use type source confidence when available, otherwise 0.7 for untyped receiver
653
- const confidence = typeConfidence ?? (typeName ? 0.9 : 0.7);
654
- allEdgeRows.push([caller.id, recvTarget.id, 'receiver', confidence, 0]);
598
+ const recv = resolveReceiverEdge(
599
+ lookup,
600
+ { name: call.name, receiver: call.receiver },
601
+ caller,
602
+ relPath,
603
+ typeMap as Map<string, unknown>,
604
+ seenCallEdges,
605
+ );
606
+ if (recv) {
607
+ allEdgeRows.push([recv.callerId, recv.receiverId, 'receiver', recv.confidence, 0]);
608
+ }
655
609
  }
656
610
  }
657
611
  }