@optave/codegraph 3.10.0 → 3.11.1

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 (312) hide show
  1. package/README.md +40 -33
  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/rules/index.d.ts.map +1 -1
  6. package/dist/ast-analysis/rules/index.js +77 -0
  7. package/dist/ast-analysis/rules/index.js.map +1 -1
  8. package/dist/ast-analysis/visitor-utils.d.ts +3 -0
  9. package/dist/ast-analysis/visitor-utils.d.ts.map +1 -1
  10. package/dist/ast-analysis/visitor-utils.js +83 -49
  11. package/dist/ast-analysis/visitor-utils.js.map +1 -1
  12. package/dist/ast-analysis/visitors/ast-store-visitor.d.ts.map +1 -1
  13. package/dist/ast-analysis/visitors/ast-store-visitor.js +78 -62
  14. package/dist/ast-analysis/visitors/ast-store-visitor.js.map +1 -1
  15. package/dist/ast-analysis/visitors/dataflow-visitor.d.ts.map +1 -1
  16. package/dist/ast-analysis/visitors/dataflow-visitor.js +61 -42
  17. package/dist/ast-analysis/visitors/dataflow-visitor.js.map +1 -1
  18. package/dist/cli/commands/audit.js +1 -1
  19. package/dist/cli/commands/audit.js.map +1 -1
  20. package/dist/cli/commands/build.d.ts.map +1 -1
  21. package/dist/cli/commands/build.js +2 -0
  22. package/dist/cli/commands/build.js.map +1 -1
  23. package/dist/cli/commands/check.js +1 -1
  24. package/dist/cli/commands/check.js.map +1 -1
  25. package/dist/cli/commands/children.js +1 -1
  26. package/dist/cli/commands/children.js.map +1 -1
  27. package/dist/cli/commands/diff-impact.js +1 -1
  28. package/dist/cli/commands/diff-impact.js.map +1 -1
  29. package/dist/cli/commands/embed.d.ts.map +1 -1
  30. package/dist/cli/commands/embed.js +49 -4
  31. package/dist/cli/commands/embed.js.map +1 -1
  32. package/dist/cli/commands/roles.js +1 -1
  33. package/dist/cli/commands/roles.js.map +1 -1
  34. package/dist/cli/commands/structure.js +1 -1
  35. package/dist/cli/commands/structure.js.map +1 -1
  36. package/dist/cli/shared/options.js +1 -1
  37. package/dist/cli/shared/options.js.map +1 -1
  38. package/dist/db/connection.d.ts.map +1 -1
  39. package/dist/db/connection.js +8 -0
  40. package/dist/db/connection.js.map +1 -1
  41. package/dist/domain/analysis/dependencies.d.ts.map +1 -1
  42. package/dist/domain/analysis/dependencies.js +106 -80
  43. package/dist/domain/analysis/dependencies.js.map +1 -1
  44. package/dist/domain/analysis/fn-impact.d.ts.map +1 -1
  45. package/dist/domain/analysis/fn-impact.js +77 -52
  46. package/dist/domain/analysis/fn-impact.js.map +1 -1
  47. package/dist/domain/analysis/module-map.d.ts.map +1 -1
  48. package/dist/domain/analysis/module-map.js +132 -121
  49. package/dist/domain/analysis/module-map.js.map +1 -1
  50. package/dist/domain/graph/builder/helpers.d.ts +4 -4
  51. package/dist/domain/graph/builder/helpers.d.ts.map +1 -1
  52. package/dist/domain/graph/builder/helpers.js +47 -33
  53. package/dist/domain/graph/builder/helpers.js.map +1 -1
  54. package/dist/domain/graph/builder/incremental.d.ts +6 -6
  55. package/dist/domain/graph/builder/incremental.d.ts.map +1 -1
  56. package/dist/domain/graph/builder/incremental.js +148 -99
  57. package/dist/domain/graph/builder/incremental.js.map +1 -1
  58. package/dist/domain/graph/builder/pipeline.d.ts +1 -0
  59. package/dist/domain/graph/builder/pipeline.d.ts.map +1 -1
  60. package/dist/domain/graph/builder/pipeline.js +23 -637
  61. package/dist/domain/graph/builder/pipeline.js.map +1 -1
  62. package/dist/domain/graph/builder/stages/build-edges.d.ts.map +1 -1
  63. package/dist/domain/graph/builder/stages/build-edges.js +141 -98
  64. package/dist/domain/graph/builder/stages/build-edges.js.map +1 -1
  65. package/dist/domain/graph/builder/stages/build-structure.d.ts.map +1 -1
  66. package/dist/domain/graph/builder/stages/build-structure.js +82 -65
  67. package/dist/domain/graph/builder/stages/build-structure.js.map +1 -1
  68. package/dist/domain/graph/builder/stages/detect-changes.d.ts.map +1 -1
  69. package/dist/domain/graph/builder/stages/detect-changes.js +84 -56
  70. package/dist/domain/graph/builder/stages/detect-changes.js.map +1 -1
  71. package/dist/domain/graph/builder/stages/finalize.d.ts.map +1 -1
  72. package/dist/domain/graph/builder/stages/finalize.js +60 -51
  73. package/dist/domain/graph/builder/stages/finalize.js.map +1 -1
  74. package/dist/domain/graph/builder/stages/insert-nodes.d.ts +8 -6
  75. package/dist/domain/graph/builder/stages/insert-nodes.d.ts.map +1 -1
  76. package/dist/domain/graph/builder/stages/insert-nodes.js +107 -122
  77. package/dist/domain/graph/builder/stages/insert-nodes.js.map +1 -1
  78. package/dist/domain/graph/builder/stages/native-db-lifecycle.d.ts +14 -0
  79. package/dist/domain/graph/builder/stages/native-db-lifecycle.d.ts.map +1 -0
  80. package/dist/domain/graph/builder/stages/native-db-lifecycle.js +77 -0
  81. package/dist/domain/graph/builder/stages/native-db-lifecycle.js.map +1 -0
  82. package/dist/domain/graph/builder/stages/native-orchestrator.d.ts +62 -0
  83. package/dist/domain/graph/builder/stages/native-orchestrator.d.ts.map +1 -0
  84. package/dist/domain/graph/builder/stages/native-orchestrator.js +747 -0
  85. package/dist/domain/graph/builder/stages/native-orchestrator.js.map +1 -0
  86. package/dist/domain/graph/builder/stages/resolve-imports.d.ts.map +1 -1
  87. package/dist/domain/graph/builder/stages/resolve-imports.js +73 -22
  88. package/dist/domain/graph/builder/stages/resolve-imports.js.map +1 -1
  89. package/dist/domain/graph/cycles.d.ts +6 -4
  90. package/dist/domain/graph/cycles.d.ts.map +1 -1
  91. package/dist/domain/graph/cycles.js +50 -55
  92. package/dist/domain/graph/cycles.js.map +1 -1
  93. package/dist/domain/graph/journal.d.ts.map +1 -1
  94. package/dist/domain/graph/journal.js +89 -70
  95. package/dist/domain/graph/journal.js.map +1 -1
  96. package/dist/domain/graph/watcher.d.ts.map +1 -1
  97. package/dist/domain/graph/watcher.js +28 -20
  98. package/dist/domain/graph/watcher.js.map +1 -1
  99. package/dist/domain/parser.d.ts +12 -23
  100. package/dist/domain/parser.d.ts.map +1 -1
  101. package/dist/domain/parser.js +153 -80
  102. package/dist/domain/parser.js.map +1 -1
  103. package/dist/domain/search/generator.d.ts +3 -1
  104. package/dist/domain/search/generator.d.ts.map +1 -1
  105. package/dist/domain/search/generator.js +68 -45
  106. package/dist/domain/search/generator.js.map +1 -1
  107. package/dist/domain/search/models.d.ts +18 -0
  108. package/dist/domain/search/models.d.ts.map +1 -1
  109. package/dist/domain/search/models.js +72 -4
  110. package/dist/domain/search/models.js.map +1 -1
  111. package/dist/domain/search/search/hybrid.d.ts.map +1 -1
  112. package/dist/domain/search/search/hybrid.js +49 -40
  113. package/dist/domain/search/search/hybrid.js.map +1 -1
  114. package/dist/domain/search/search/semantic.d.ts.map +1 -1
  115. package/dist/domain/search/search/semantic.js +69 -49
  116. package/dist/domain/search/search/semantic.js.map +1 -1
  117. package/dist/domain/wasm-worker-entry.js +209 -137
  118. package/dist/domain/wasm-worker-entry.js.map +1 -1
  119. package/dist/extractors/c.js +25 -6
  120. package/dist/extractors/c.js.map +1 -1
  121. package/dist/extractors/cpp.js +47 -6
  122. package/dist/extractors/cpp.js.map +1 -1
  123. package/dist/extractors/cuda.js +90 -14
  124. package/dist/extractors/cuda.js.map +1 -1
  125. package/dist/extractors/elixir.js +108 -4
  126. package/dist/extractors/elixir.js.map +1 -1
  127. package/dist/extractors/erlang.js +56 -20
  128. package/dist/extractors/erlang.js.map +1 -1
  129. package/dist/extractors/fsharp.d.ts +7 -0
  130. package/dist/extractors/fsharp.d.ts.map +1 -1
  131. package/dist/extractors/fsharp.js +94 -0
  132. package/dist/extractors/fsharp.js.map +1 -1
  133. package/dist/extractors/gleam.d.ts.map +1 -1
  134. package/dist/extractors/gleam.js +29 -33
  135. package/dist/extractors/gleam.js.map +1 -1
  136. package/dist/extractors/groovy.js +41 -1
  137. package/dist/extractors/groovy.js.map +1 -1
  138. package/dist/extractors/haskell.js +48 -4
  139. package/dist/extractors/haskell.js.map +1 -1
  140. package/dist/extractors/helpers.d.ts +79 -1
  141. package/dist/extractors/helpers.d.ts.map +1 -1
  142. package/dist/extractors/helpers.js +137 -0
  143. package/dist/extractors/helpers.js.map +1 -1
  144. package/dist/extractors/java.d.ts.map +1 -1
  145. package/dist/extractors/java.js +37 -49
  146. package/dist/extractors/java.js.map +1 -1
  147. package/dist/extractors/javascript.d.ts.map +1 -1
  148. package/dist/extractors/javascript.js +44 -44
  149. package/dist/extractors/javascript.js.map +1 -1
  150. package/dist/extractors/julia.js +198 -74
  151. package/dist/extractors/julia.js.map +1 -1
  152. package/dist/extractors/kotlin.js +4 -0
  153. package/dist/extractors/kotlin.js.map +1 -1
  154. package/dist/extractors/objc.js +184 -47
  155. package/dist/extractors/objc.js.map +1 -1
  156. package/dist/extractors/python.js +7 -4
  157. package/dist/extractors/python.js.map +1 -1
  158. package/dist/extractors/r.d.ts.map +1 -1
  159. package/dist/extractors/r.js +103 -87
  160. package/dist/extractors/r.js.map +1 -1
  161. package/dist/extractors/scala.d.ts.map +1 -1
  162. package/dist/extractors/scala.js +18 -32
  163. package/dist/extractors/scala.js.map +1 -1
  164. package/dist/extractors/solidity.d.ts.map +1 -1
  165. package/dist/extractors/solidity.js +55 -69
  166. package/dist/extractors/solidity.js.map +1 -1
  167. package/dist/extractors/verilog.js +80 -15
  168. package/dist/extractors/verilog.js.map +1 -1
  169. package/dist/features/boundaries.d.ts.map +1 -1
  170. package/dist/features/boundaries.js +49 -39
  171. package/dist/features/boundaries.js.map +1 -1
  172. package/dist/features/cfg.d.ts.map +1 -1
  173. package/dist/features/cfg.js +90 -63
  174. package/dist/features/cfg.js.map +1 -1
  175. package/dist/features/check.d.ts.map +1 -1
  176. package/dist/features/check.js +43 -34
  177. package/dist/features/check.js.map +1 -1
  178. package/dist/features/cochange.d.ts.map +1 -1
  179. package/dist/features/cochange.js +68 -56
  180. package/dist/features/cochange.js.map +1 -1
  181. package/dist/features/complexity.d.ts.map +1 -1
  182. package/dist/features/complexity.js +105 -75
  183. package/dist/features/complexity.js.map +1 -1
  184. package/dist/features/dataflow.d.ts.map +1 -1
  185. package/dist/features/dataflow.js +37 -29
  186. package/dist/features/dataflow.js.map +1 -1
  187. package/dist/features/flow.d.ts.map +1 -1
  188. package/dist/features/flow.js +31 -22
  189. package/dist/features/flow.js.map +1 -1
  190. package/dist/features/graph-enrichment.d.ts.map +1 -1
  191. package/dist/features/graph-enrichment.js +77 -70
  192. package/dist/features/graph-enrichment.js.map +1 -1
  193. package/dist/features/owners.d.ts +17 -26
  194. package/dist/features/owners.d.ts.map +1 -1
  195. package/dist/features/owners.js +120 -109
  196. package/dist/features/owners.js.map +1 -1
  197. package/dist/features/sequence.d.ts.map +1 -1
  198. package/dist/features/sequence.js +59 -54
  199. package/dist/features/sequence.js.map +1 -1
  200. package/dist/features/structure-query.d.ts.map +1 -1
  201. package/dist/features/structure-query.js +60 -60
  202. package/dist/features/structure-query.js.map +1 -1
  203. package/dist/features/structure.js +28 -36
  204. package/dist/features/structure.js.map +1 -1
  205. package/dist/graph/algorithms/leiden/optimiser.d.ts.map +1 -1
  206. package/dist/graph/algorithms/leiden/optimiser.js +100 -69
  207. package/dist/graph/algorithms/leiden/optimiser.js.map +1 -1
  208. package/dist/graph/classifiers/roles.d.ts.map +1 -1
  209. package/dist/graph/classifiers/roles.js +63 -59
  210. package/dist/graph/classifiers/roles.js.map +1 -1
  211. package/dist/infrastructure/config.d.ts +1 -1
  212. package/dist/infrastructure/config.d.ts.map +1 -1
  213. package/dist/infrastructure/config.js +1 -1
  214. package/dist/infrastructure/config.js.map +1 -1
  215. package/dist/mcp/tool-registry.d.ts.map +1 -1
  216. package/dist/mcp/tool-registry.js +4 -0
  217. package/dist/mcp/tool-registry.js.map +1 -1
  218. package/dist/mcp/tools/semantic-search.d.ts +1 -0
  219. package/dist/mcp/tools/semantic-search.d.ts.map +1 -1
  220. package/dist/mcp/tools/semantic-search.js +1 -0
  221. package/dist/mcp/tools/semantic-search.js.map +1 -1
  222. package/dist/presentation/cfg.d.ts.map +1 -1
  223. package/dist/presentation/cfg.js +44 -29
  224. package/dist/presentation/cfg.js.map +1 -1
  225. package/dist/presentation/flow.d.ts.map +1 -1
  226. package/dist/presentation/flow.js +58 -38
  227. package/dist/presentation/flow.js.map +1 -1
  228. package/dist/types.d.ts +16 -2
  229. package/dist/types.d.ts.map +1 -1
  230. package/grammars/tree-sitter-erlang.wasm +0 -0
  231. package/grammars/tree-sitter-fsharp.wasm +0 -0
  232. package/grammars/tree-sitter-fsharp_signature.wasm +0 -0
  233. package/grammars/tree-sitter-gleam.wasm +0 -0
  234. package/package.json +10 -10
  235. package/src/ast-analysis/engine.ts +145 -61
  236. package/src/ast-analysis/rules/index.ts +87 -0
  237. package/src/ast-analysis/visitor-utils.ts +86 -46
  238. package/src/ast-analysis/visitors/ast-store-visitor.ts +104 -69
  239. package/src/ast-analysis/visitors/dataflow-visitor.ts +86 -47
  240. package/src/cli/commands/audit.ts +1 -1
  241. package/src/cli/commands/build.ts +2 -0
  242. package/src/cli/commands/check.ts +1 -1
  243. package/src/cli/commands/children.ts +1 -1
  244. package/src/cli/commands/diff-impact.ts +1 -1
  245. package/src/cli/commands/embed.ts +54 -4
  246. package/src/cli/commands/roles.ts +1 -1
  247. package/src/cli/commands/structure.ts +1 -1
  248. package/src/cli/shared/options.ts +1 -1
  249. package/src/db/connection.ts +8 -0
  250. package/src/domain/analysis/dependencies.ts +166 -85
  251. package/src/domain/analysis/fn-impact.ts +120 -50
  252. package/src/domain/analysis/module-map.ts +175 -140
  253. package/src/domain/graph/builder/helpers.ts +85 -76
  254. package/src/domain/graph/builder/incremental.ts +223 -131
  255. package/src/domain/graph/builder/pipeline.ts +32 -785
  256. package/src/domain/graph/builder/stages/build-edges.ts +207 -142
  257. package/src/domain/graph/builder/stages/build-structure.ts +115 -82
  258. package/src/domain/graph/builder/stages/detect-changes.ts +107 -64
  259. package/src/domain/graph/builder/stages/finalize.ts +72 -70
  260. package/src/domain/graph/builder/stages/insert-nodes.ts +154 -120
  261. package/src/domain/graph/builder/stages/native-db-lifecycle.ts +74 -0
  262. package/src/domain/graph/builder/stages/native-orchestrator.ts +942 -0
  263. package/src/domain/graph/builder/stages/resolve-imports.ts +79 -25
  264. package/src/domain/graph/cycles.ts +51 -49
  265. package/src/domain/graph/journal.ts +84 -69
  266. package/src/domain/graph/watcher.ts +29 -25
  267. package/src/domain/parser.ts +170 -67
  268. package/src/domain/search/generator.ts +132 -74
  269. package/src/domain/search/models.ts +75 -4
  270. package/src/domain/search/search/hybrid.ts +53 -42
  271. package/src/domain/search/search/semantic.ts +105 -65
  272. package/src/domain/wasm-worker-entry.ts +243 -153
  273. package/src/extractors/c.ts +27 -8
  274. package/src/extractors/cpp.ts +50 -8
  275. package/src/extractors/cuda.ts +90 -16
  276. package/src/extractors/elixir.ts +103 -4
  277. package/src/extractors/erlang.ts +63 -20
  278. package/src/extractors/fsharp.ts +104 -0
  279. package/src/extractors/gleam.ts +40 -39
  280. package/src/extractors/groovy.ts +45 -1
  281. package/src/extractors/haskell.ts +45 -4
  282. package/src/extractors/helpers.ts +205 -1
  283. package/src/extractors/java.ts +42 -45
  284. package/src/extractors/javascript.ts +44 -43
  285. package/src/extractors/julia.ts +191 -77
  286. package/src/extractors/kotlin.ts +4 -0
  287. package/src/extractors/objc.ts +171 -47
  288. package/src/extractors/python.ts +5 -3
  289. package/src/extractors/r.ts +104 -82
  290. package/src/extractors/scala.ts +24 -36
  291. package/src/extractors/solidity.ts +59 -78
  292. package/src/extractors/verilog.ts +83 -15
  293. package/src/features/boundaries.ts +64 -46
  294. package/src/features/cfg.ts +145 -74
  295. package/src/features/check.ts +60 -43
  296. package/src/features/cochange.ts +95 -72
  297. package/src/features/complexity.ts +134 -79
  298. package/src/features/dataflow.ts +57 -34
  299. package/src/features/flow.ts +48 -24
  300. package/src/features/graph-enrichment.ts +105 -70
  301. package/src/features/owners.ts +186 -146
  302. package/src/features/sequence.ts +99 -69
  303. package/src/features/structure-query.ts +94 -79
  304. package/src/features/structure.ts +56 -56
  305. package/src/graph/algorithms/leiden/optimiser.ts +142 -87
  306. package/src/graph/classifiers/roles.ts +64 -54
  307. package/src/infrastructure/config.ts +1 -1
  308. package/src/mcp/tool-registry.ts +5 -0
  309. package/src/mcp/tools/semantic-search.ts +2 -0
  310. package/src/presentation/cfg.ts +48 -32
  311. package/src/presentation/flow.ts +100 -52
  312. package/src/types.ts +16 -1
@@ -306,11 +306,18 @@ const LANGUAGE_REGISTRY: LanguageRegistryEntry[] = [
306
306
  },
307
307
  {
308
308
  id: 'fsharp',
309
- extensions: ['.fs', '.fsx', '.fsi'],
309
+ extensions: ['.fs', '.fsx'],
310
310
  grammarFile: 'tree-sitter-fsharp.wasm',
311
311
  extractor: extractFSharpSymbols,
312
312
  required: false,
313
313
  },
314
+ {
315
+ id: 'fsharp-signature',
316
+ extensions: ['.fsi'],
317
+ grammarFile: 'tree-sitter-fsharp_signature.wasm',
318
+ extractor: extractFSharpSymbols,
319
+ required: false,
320
+ },
314
321
  {
315
322
  id: 'gleam',
316
323
  extensions: ['.gleam'],
@@ -566,6 +573,90 @@ interface SetupResult {
566
573
  dataflowVisitor: Visitor | null;
567
574
  }
568
575
 
576
+ /**
577
+ * Build the AST-store visitor for `langId`. Returns `null` when AST is
578
+ * disabled or the language has no AST type map. db-free — passes an empty
579
+ * nodeIdMap. The main thread re-resolves parent node IDs in
580
+ * `features/ast.ts::collectFileAstRows`.
581
+ */
582
+ function buildAstVisitor(
583
+ langId: string,
584
+ defs: ExtractorOutput['definitions'],
585
+ relPath: string,
586
+ enabled: boolean,
587
+ ): Visitor | null {
588
+ if (!enabled) return null;
589
+ const astTypeMap = AST_TYPE_MAPS.get(langId);
590
+ if (!astTypeMap) return null;
591
+ const stringConfig = AST_STRING_CONFIGS.get(langId);
592
+ return createAstStoreVisitor(
593
+ astTypeMap,
594
+ defs,
595
+ relPath,
596
+ new Map<string, number>(),
597
+ stringConfig,
598
+ astStopRecurseKinds(langId),
599
+ );
600
+ }
601
+
602
+ /**
603
+ * Build the complexity visitor when enabled, the language has complexity
604
+ * rules, and at least one definition still lacks a `complexity` payload.
605
+ * Side-effect: extends `walkerOpts` with nesting-node types and a
606
+ * `getFunctionName` resolver suitable for this language.
607
+ */
608
+ function buildComplexityVisitor(
609
+ langId: string,
610
+ defs: ExtractorOutput['definitions'],
611
+ enabled: boolean,
612
+ walkerOpts: WalkOptions,
613
+ ): Visitor | null {
614
+ if (!enabled) return null;
615
+ const cRules = COMPLEXITY_RULES.get(langId);
616
+ if (!cRules || !defs.some((d) => hasFuncBody(d) && !d.complexity)) return null;
617
+
618
+ const hRules = HALSTEAD_RULES.get(langId);
619
+ const visitor = createComplexityVisitor(cRules, hRules, { fileLevelWalk: true, langId });
620
+ for (const t of cRules.nestingNodes) walkerOpts.nestingNodeTypes?.add(t);
621
+ const dfRules = DATAFLOW_RULES.get(langId);
622
+ walkerOpts.getFunctionName = (node: TreeSitterNode): string | null => {
623
+ const nameNode = node.childForFieldName('name');
624
+ if (nameNode) return nameNode.text;
625
+ // dfRules shape varies per language; visitor-utils accepts any shape
626
+ if (dfRules) return getFuncName(node, dfRules as any);
627
+ return null;
628
+ };
629
+ return visitor;
630
+ }
631
+
632
+ /** Build the CFG visitor when enabled and at least one definition still lacks blocks. */
633
+ function buildCfgVisitor(
634
+ langId: string,
635
+ defs: ExtractorOutput['definitions'],
636
+ enabled: boolean,
637
+ ): Visitor | null {
638
+ if (!enabled) return null;
639
+ const cfgRulesForLang = CFG_RULES.get(langId);
640
+ if (!cfgRulesForLang) return null;
641
+ const needsCfg = defs.some(
642
+ (d) => hasFuncBody(d) && d.cfg !== null && !Array.isArray(d.cfg?.blocks),
643
+ );
644
+ if (!needsCfg) return null;
645
+ return createCfgVisitor(cfgRulesForLang);
646
+ }
647
+
648
+ /** Build the dataflow visitor when enabled and `symbols.dataflow` is not yet populated. */
649
+ function buildDataflowVisitor(
650
+ langId: string,
651
+ symbols: ExtractorOutput,
652
+ enabled: boolean,
653
+ ): Visitor | null {
654
+ if (!enabled) return null;
655
+ const dfRules = DATAFLOW_RULES.get(langId);
656
+ if (!dfRules || symbols.dataflow) return null;
657
+ return createDataflowVisitor(dfRules);
658
+ }
659
+
569
660
  function setupVisitorsLocal(
570
661
  symbols: ExtractorOutput,
571
662
  relPath: string,
@@ -573,82 +664,161 @@ function setupVisitorsLocal(
573
664
  opts: WorkerParseRequest['opts'],
574
665
  ): SetupResult {
575
666
  const defs = symbols.definitions || [];
576
- const visitors: Visitor[] = [];
577
667
  const walkerOpts: WalkOptions = {
578
668
  functionNodeTypes: new Set<string>(),
579
669
  nestingNodeTypes: new Set<string>(),
580
670
  getFunctionName: (_node: TreeSitterNode) => null,
581
671
  };
582
672
 
583
- // AST-store: db-free pass an empty nodeIdMap. The main thread re-resolves
584
- // parent node IDs in features/ast.ts::collectFileAstRows.
585
- let astVisitor: Visitor | null = null;
586
- if (opts.ast) {
587
- const astTypeMap = AST_TYPE_MAPS.get(langId);
588
- if (astTypeMap) {
589
- const stringConfig = AST_STRING_CONFIGS.get(langId);
590
- astVisitor = createAstStoreVisitor(
591
- astTypeMap,
592
- defs,
593
- relPath,
594
- new Map<string, number>(),
595
- stringConfig,
596
- astStopRecurseKinds(langId),
597
- );
598
- visitors.push(astVisitor);
599
- }
600
- }
673
+ const astVisitor = buildAstVisitor(langId, defs, relPath, !!opts.ast);
674
+ const complexityVisitor = buildComplexityVisitor(langId, defs, !!opts.complexity, walkerOpts);
675
+ const cfgVisitor = buildCfgVisitor(langId, defs, !!opts.cfg);
676
+ const dataflowVisitor = buildDataflowVisitor(langId, symbols, !!opts.dataflow);
601
677
 
602
- // Complexity
603
- let complexityVisitor: Visitor | null = null;
604
- if (opts.complexity) {
605
- const cRules = COMPLEXITY_RULES.get(langId);
606
- if (cRules && defs.some((d) => hasFuncBody(d) && !d.complexity)) {
607
- const hRules = HALSTEAD_RULES.get(langId);
608
- complexityVisitor = createComplexityVisitor(cRules, hRules, {
609
- fileLevelWalk: true,
610
- langId,
611
- });
612
- for (const t of cRules.nestingNodes) walkerOpts.nestingNodeTypes?.add(t);
613
- const dfRules = DATAFLOW_RULES.get(langId);
614
- walkerOpts.getFunctionName = (node: TreeSitterNode): string | null => {
615
- const nameNode = node.childForFieldName('name');
616
- if (nameNode) return nameNode.text;
617
- // dfRules shape varies per language; visitor-utils accepts any shape
618
- if (dfRules) return getFuncName(node, dfRules as any);
619
- return null;
620
- };
621
- visitors.push(complexityVisitor);
622
- }
623
- }
678
+ const visitors: Visitor[] = [];
679
+ if (astVisitor) visitors.push(astVisitor);
680
+ if (complexityVisitor) visitors.push(complexityVisitor);
681
+ if (cfgVisitor) visitors.push(cfgVisitor);
682
+ if (dataflowVisitor) visitors.push(dataflowVisitor);
624
683
 
625
- // CFG
626
- let cfgVisitor: Visitor | null = null;
627
- if (opts.cfg) {
628
- const cfgRulesForLang = CFG_RULES.get(langId);
629
- if (
630
- cfgRulesForLang &&
631
- defs.some((d) => hasFuncBody(d) && d.cfg !== null && !Array.isArray(d.cfg?.blocks))
632
- ) {
633
- cfgVisitor = createCfgVisitor(cfgRulesForLang);
634
- visitors.push(cfgVisitor);
635
- }
684
+ return { visitors, walkerOpts, astVisitor, complexityVisitor, cfgVisitor, dataflowVisitor };
685
+ }
686
+
687
+ // ── Main parse handler ──────────────────────────────────────────────────────
688
+
689
+ /**
690
+ * Run tree-sitter parse + extractor on `code`. Returns `null` when either
691
+ * step yields no usable output. Throws (for the caller to report back to the
692
+ * pool) only on a hard tree-sitter parse error.
693
+ */
694
+ function parseAndExtract(
695
+ parser: Parser,
696
+ entry: LanguageRegistryEntry,
697
+ filePath: string,
698
+ code: string,
699
+ ): { tree: Tree; symbols: ExtractorOutput } | null {
700
+ let tree: Tree | null;
701
+ try {
702
+ tree = parser.parse(code);
703
+ } catch (e: unknown) {
704
+ // Parse error — report back but keep worker alive.
705
+ throw new Error(`parse failed: ${(e as Error).message}`);
636
706
  }
707
+ if (!tree) return null;
637
708
 
638
- // Dataflow
639
- let dataflowVisitor: Visitor | null = null;
640
- if (opts.dataflow) {
641
- const dfRules = DATAFLOW_RULES.get(langId);
642
- if (dfRules && !symbols.dataflow) {
643
- dataflowVisitor = createDataflowVisitor(dfRules);
644
- visitors.push(dataflowVisitor);
645
- }
709
+ // Extractor — on failure, skip file (ok:true, null) to match parser.ts
710
+ // behavior where extractor issues don't crash the build. Dispose the tree
711
+ // before returning null so WASM linear memory doesn't accumulate in the worker.
712
+ let symbols: ExtractorOutput | null;
713
+ try {
714
+ const query = _queries.get(entry.id);
715
+ // tree-sitter's Tree/Query are structurally compatible with
716
+ // TreeSitterTree/TreeSitterQuery at runtime — same cast style as
717
+ // parser.ts::wasmExtractSymbols (parser.ts:789).
718
+ symbols = entry.extractor(tree as any, filePath, query as any) ?? null;
719
+ } catch {
720
+ disposeTree(tree);
721
+ return null;
646
722
  }
723
+ if (!symbols) {
724
+ disposeTree(tree);
725
+ return null;
726
+ }
727
+ return { tree, symbols };
728
+ }
647
729
 
648
- return { visitors, walkerOpts, astVisitor, complexityVisitor, cfgVisitor, dataflowVisitor };
730
+ /**
731
+ * Project the visitor `ast-store` rows into the wire-safe shape returned to
732
+ * the main thread. Strips `file` and `parentNodeId` — both are re-resolved in
733
+ * `features/ast.ts::collectFileAstRows`. Always returns an array (even empty)
734
+ * so `engine.ts::fileNeedsWasmTree` doesn't treat the file as un-walked and
735
+ * trigger a full ensureWasmTrees re-parse (#1036).
736
+ */
737
+ function projectAstNodes(results: WalkResults): SerializedExtractorOutput['astNodes'] {
738
+ const astRows = (results['ast-store'] || []) as Array<{
739
+ line: number;
740
+ kind: string;
741
+ name: string | null | undefined;
742
+ text: string | null;
743
+ receiver: string | null;
744
+ file?: string;
745
+ parentNodeId?: number | null;
746
+ }>;
747
+ return astRows.map((n) => ({
748
+ line: n.line,
749
+ kind: n.kind,
750
+ name: n.name ?? '',
751
+ text: n.text ?? undefined,
752
+ receiver: n.receiver ?? undefined,
753
+ }));
649
754
  }
650
755
 
651
- // ── Main parse handler ──────────────────────────────────────────────────────
756
+ /**
757
+ * Run the configured visitor walk over `tree.rootNode` and apply each
758
+ * visitor's results back onto `symbols`. Returns the serialized astNodes
759
+ * (or `undefined` when AST is disabled / no rows produced).
760
+ *
761
+ * Mirrors engine.ts:791-829. Runs BEFORE `tree.delete()` because
762
+ * storeComplexityResults / storeCfgResults read `funcNode` off live nodes.
763
+ */
764
+ function runVisitorWalk(
765
+ tree: Tree,
766
+ symbols: ExtractorOutput,
767
+ langId: string,
768
+ setup: SetupResult,
769
+ ): SerializedExtractorOutput['astNodes'] {
770
+ if (setup.visitors.length === 0) return undefined;
771
+ // rootNode shape matches TreeSitterNode at runtime — same cast as parser.ts:789.
772
+ const results = walkWithVisitors(tree.rootNode as any, setup.visitors, langId, setup.walkerOpts);
773
+ const defs = symbols.definitions || [];
774
+ let serializedAstNodes: SerializedExtractorOutput['astNodes'];
775
+ if (setup.astVisitor) serializedAstNodes = projectAstNodes(results);
776
+ if (setup.complexityVisitor) storeComplexityResults(results, defs, langId);
777
+ if (setup.cfgVisitor) storeCfgResults(results, defs);
778
+ if (setup.dataflowVisitor) symbols.dataflow = results.dataflow as DataflowResult;
779
+ return serializedAstNodes;
780
+ }
781
+
782
+ /**
783
+ * Pack the in-memory ExtractorOutput into the structured-clone-safe shape
784
+ * sent back across the worker boundary. Converts the typeMap into a tuple
785
+ * array and intentionally omits `_tree` (cannot cross the boundary).
786
+ */
787
+ function serializeExtractorOutput(
788
+ symbols: ExtractorOutput,
789
+ langId: LanguageId,
790
+ code: string,
791
+ astNodes: SerializedExtractorOutput['astNodes'],
792
+ ): SerializedExtractorOutput {
793
+ return {
794
+ definitions: symbols.definitions,
795
+ calls: symbols.calls,
796
+ imports: symbols.imports,
797
+ classes: symbols.classes,
798
+ exports: symbols.exports,
799
+ typeMap: Array.from(symbols.typeMap.entries()),
800
+ _langId: langId,
801
+ _lineCount: code.split('\n').length,
802
+ dataflow: symbols.dataflow,
803
+ astNodes,
804
+ };
805
+ }
806
+
807
+ /**
808
+ * Release WASM linear memory backing a tree. Best-effort — swallows errors so
809
+ * the worker keeps serving requests. Deferring this would let trees accumulate
810
+ * in the worker's WASM heap and defeat the point of isolating parse calls.
811
+ */
812
+ function disposeTree(tree: Tree | null): void {
813
+ if (!tree) return;
814
+ const deletable = tree as unknown as { delete?: () => void };
815
+ if (typeof deletable.delete !== 'function') return;
816
+ try {
817
+ deletable.delete();
818
+ } catch {
819
+ // best-effort cleanup — swallow; worker continues.
820
+ }
821
+ }
652
822
 
653
823
  async function handleParse(msg: WorkerParseRequest): Promise<SerializedExtractorOutput | null> {
654
824
  const ext = path.extname(msg.filePath).toLowerCase();
@@ -659,100 +829,20 @@ async function handleParse(msg: WorkerParseRequest): Promise<SerializedExtractor
659
829
  const parser = await loadLanguageLazy(entry);
660
830
  if (!parser) return null;
661
831
 
662
- let tree: Tree | null = null;
663
- try {
664
- try {
665
- tree = parser.parse(msg.code);
666
- } catch (e: unknown) {
667
- // Parse error — report back but keep worker alive.
668
- throw new Error(`parse failed: ${(e as Error).message}`);
669
- }
670
- if (!tree) return null;
671
-
672
- // Extractor — on failure, skip file (ok:true, null) to match parser.ts
673
- // behavior where extractor issues don't crash the build.
674
- let symbols: ExtractorOutput | null;
675
- try {
676
- const query = _queries.get(entry.id);
677
- // tree-sitter's Tree/Query are structurally compatible with
678
- // TreeSitterTree/TreeSitterQuery at runtime — same cast style as
679
- // parser.ts::wasmExtractSymbols (parser.ts:789).
680
- symbols = entry.extractor(tree as any, msg.filePath, query as any) ?? null;
681
- } catch {
682
- return null;
683
- }
684
- if (!symbols) return null;
685
-
686
- // Unified visitor walk — mirrors engine.ts:791-829. Runs BEFORE tree.delete()
687
- // because storeComplexityResults/storeCfgResults read funcNode off live nodes.
688
- const { visitors, walkerOpts, astVisitor, complexityVisitor, cfgVisitor, dataflowVisitor } =
689
- setupVisitorsLocal(symbols, msg.filePath, entry.id, msg.opts);
832
+ const parsed = parseAndExtract(parser, entry, msg.filePath, msg.code);
833
+ if (!parsed) return null;
834
+ const { tree, symbols } = parsed;
690
835
 
691
- // astNodes are kept in the serialized shape (without `file`/`parentNodeId`),
836
+ try {
837
+ const setup = setupVisitorsLocal(symbols, msg.filePath, entry.id, msg.opts);
838
+ // astNodes kept in the serialized shape (without `file`/`parentNodeId`),
692
839
  // not assigned back to symbols.astNodes — ExtractorOutput.astNodes is
693
840
  // ASTNodeRow[] (DB row shape with node_id), which is a different type.
694
- let serializedAstNodes: SerializedExtractorOutput['astNodes'];
695
-
696
- if (visitors.length > 0) {
697
- // rootNode shape matches TreeSitterNode at runtime — same cast as parser.ts:789.
698
- const results = walkWithVisitors(tree.rootNode as any, visitors, entry.id, walkerOpts);
699
-
700
- const defs = symbols.definitions || [];
701
- if (astVisitor) {
702
- const astRows = (results['ast-store'] || []) as Array<{
703
- line: number;
704
- kind: string;
705
- name: string | null | undefined;
706
- text: string | null;
707
- receiver: string | null;
708
- file?: string;
709
- parentNodeId?: number | null;
710
- }>;
711
- // Always set an array (even empty) — leaving astNodes undefined makes
712
- // engine.ts::fileNeedsWasmTree treat the file as un-walked and trigger
713
- // a full ensureWasmTrees re-parse of every WASM-parseable file (#1036).
714
- // Strip `file` and `parentNodeId` — main thread re-resolves both in
715
- // features/ast.ts::collectFileAstRows.
716
- serializedAstNodes = astRows.map((n) => ({
717
- line: n.line,
718
- kind: n.kind,
719
- name: n.name ?? '',
720
- text: n.text ?? undefined,
721
- receiver: n.receiver ?? undefined,
722
- }));
723
- }
724
-
725
- if (complexityVisitor) storeComplexityResults(results, defs, entry.id);
726
- if (cfgVisitor) storeCfgResults(results, defs);
727
- if (dataflowVisitor) symbols.dataflow = results.dataflow as DataflowResult;
728
- }
729
-
730
- // Serialize — convert Map<string, TypeMapEntry> to tuple array for the wire.
731
- const serialized: SerializedExtractorOutput = {
732
- definitions: symbols.definitions,
733
- calls: symbols.calls,
734
- imports: symbols.imports,
735
- classes: symbols.classes,
736
- exports: symbols.exports,
737
- typeMap: Array.from(symbols.typeMap.entries()),
738
- _langId: entry.id as LanguageId,
739
- _lineCount: msg.code.split('\n').length,
740
- dataflow: symbols.dataflow,
741
- astNodes: serializedAstNodes,
742
- };
743
- // _tree is deliberately not serialized — it cannot cross the worker boundary.
744
- return serialized;
841
+ const serializedAstNodes = runVisitorWalk(tree, symbols, entry.id, setup);
842
+ return serializeExtractorOutput(symbols, entry.id as LanguageId, msg.code, serializedAstNodes);
745
843
  } finally {
746
- // ALWAYS release WASM memory before responding. Deferring this would let
747
- // trees accumulate in the worker's WASM heap across requests and defeat
748
- // the point of isolating parse calls.
749
- if (tree && typeof (tree as unknown as { delete?: () => void }).delete === 'function') {
750
- try {
751
- (tree as unknown as { delete: () => void }).delete();
752
- } catch {
753
- // best-effort cleanup — swallow; worker continues.
754
- }
755
- }
844
+ // ALWAYS release WASM memory before responding (see disposeTree note).
845
+ disposeTree(tree);
756
846
  }
757
847
  }
758
848
 
@@ -159,6 +159,31 @@ function handleCCallExpression(node: TreeSitterNode, ctx: ExtractorOutput): void
159
159
 
160
160
  // ── Child extraction helpers ────────────────────────────────────────────────
161
161
 
162
+ const C_DECLARATOR_WRAPPERS = new Set([
163
+ 'pointer_declarator',
164
+ 'array_declarator',
165
+ 'parenthesized_declarator',
166
+ 'function_declarator',
167
+ ]);
168
+
169
+ /**
170
+ * Drill through pointer/array/parenthesized/function declarator wrappers to
171
+ * recover the bare identifier. Mirrors `unwrap_declarator` in the native C
172
+ * extractor so both engines agree on the name for parameters such as
173
+ * `void process(int callback(int))` (function-type parameter → `callback`) or
174
+ * `int *func(int)` (pointer-returning function → `func`).
175
+ */
176
+ function unwrapCDeclaratorName(node: TreeSitterNode): string {
177
+ let current: TreeSitterNode | null = node;
178
+ while (current && C_DECLARATOR_WRAPPERS.has(current.type)) {
179
+ current = current.childForFieldName('declarator');
180
+ }
181
+ if (current?.type === 'identifier' || current?.type === 'field_identifier') {
182
+ return current.text;
183
+ }
184
+ return current?.text ?? node.text;
185
+ }
186
+
162
187
  function extractCParameters(paramListNode: TreeSitterNode | null): SubDeclaration[] {
163
188
  const params: SubDeclaration[] = [];
164
189
  if (!paramListNode) return params;
@@ -167,10 +192,7 @@ function extractCParameters(paramListNode: TreeSitterNode | null): SubDeclaratio
167
192
  if (!param || param.type !== 'parameter_declaration') continue;
168
193
  const nameNode = param.childForFieldName('declarator');
169
194
  if (nameNode) {
170
- const name =
171
- nameNode.type === 'identifier'
172
- ? nameNode.text
173
- : (findChild(nameNode, 'identifier')?.text ?? nameNode.text);
195
+ const name = unwrapCDeclaratorName(nameNode);
174
196
  params.push({ name, kind: 'parameter', line: param.startPosition.row + 1 });
175
197
  }
176
198
  }
@@ -186,10 +208,7 @@ function extractStructFields(structNode: TreeSitterNode): SubDeclaration[] {
186
208
  if (!member || member.type !== 'field_declaration') continue;
187
209
  const nameNode = member.childForFieldName('declarator');
188
210
  if (nameNode) {
189
- const name =
190
- nameNode.type === 'identifier'
191
- ? nameNode.text
192
- : (findChild(nameNode, 'identifier')?.text ?? nameNode.text);
211
+ const name = unwrapCDeclaratorName(nameNode);
193
212
  fields.push({ name, kind: 'property', line: member.startPosition.row + 1 });
194
213
  }
195
214
  }
@@ -239,6 +239,54 @@ function findCppParentClass(node: TreeSitterNode): string | null {
239
239
  return null;
240
240
  }
241
241
 
242
+ const CPP_DECLARATOR_WRAPPERS = new Set([
243
+ 'pointer_declarator',
244
+ 'reference_declarator',
245
+ 'array_declarator',
246
+ 'parenthesized_declarator',
247
+ 'function_declarator',
248
+ ]);
249
+
250
+ /**
251
+ * Drill through pointer/reference/array/parenthesized/function declarator
252
+ * wrappers to recover the bare identifier. Mirrors `unwrap_cpp_declarator` in
253
+ * the native C++ extractor. tree-sitter-cpp's `reference_declarator` does not
254
+ * expose a `declarator` field, so the loop falls back to scanning children
255
+ * for the next nested declarator or identifier.
256
+ */
257
+ function unwrapCppDeclaratorName(node: TreeSitterNode): string {
258
+ let current: TreeSitterNode | null = node;
259
+ while (current && CPP_DECLARATOR_WRAPPERS.has(current.type)) {
260
+ const named = current.childForFieldName('declarator');
261
+ if (named) {
262
+ current = named;
263
+ continue;
264
+ }
265
+ const fallback = nextCppDeclaratorChild(current);
266
+ if (!fallback) break;
267
+ current = fallback;
268
+ }
269
+ if (current?.type === 'identifier' || current?.type === 'field_identifier') {
270
+ return current.text;
271
+ }
272
+ return current?.text ?? node.text;
273
+ }
274
+
275
+ function nextCppDeclaratorChild(node: TreeSitterNode): TreeSitterNode | null {
276
+ for (let i = 0; i < node.childCount; i++) {
277
+ const child = node.child(i);
278
+ if (!child) continue;
279
+ if (
280
+ child.type === 'identifier' ||
281
+ child.type === 'field_identifier' ||
282
+ CPP_DECLARATOR_WRAPPERS.has(child.type)
283
+ ) {
284
+ return child;
285
+ }
286
+ }
287
+ return null;
288
+ }
289
+
242
290
  function extractCppParameters(paramListNode: TreeSitterNode | null): SubDeclaration[] {
243
291
  const params: SubDeclaration[] = [];
244
292
  if (!paramListNode) return params;
@@ -247,10 +295,7 @@ function extractCppParameters(paramListNode: TreeSitterNode | null): SubDeclarat
247
295
  if (!param || param.type !== 'parameter_declaration') continue;
248
296
  const nameNode = param.childForFieldName('declarator');
249
297
  if (nameNode) {
250
- const name =
251
- nameNode.type === 'identifier'
252
- ? nameNode.text
253
- : (findChild(nameNode, 'identifier')?.text ?? nameNode.text);
298
+ const name = unwrapCppDeclaratorName(nameNode);
254
299
  params.push({ name, kind: 'parameter', line: param.startPosition.row + 1 });
255
300
  }
256
301
  }
@@ -267,10 +312,7 @@ function extractCppClassFields(classNode: TreeSitterNode): SubDeclaration[] {
267
312
  if (!member || member.type !== 'field_declaration') continue;
268
313
  const nameNode = member.childForFieldName('declarator');
269
314
  if (nameNode) {
270
- const name =
271
- nameNode.type === 'identifier'
272
- ? nameNode.text
273
- : (findChild(nameNode, 'identifier')?.text ?? nameNode.text);
315
+ const name = unwrapCppDeclaratorName(nameNode);
274
316
  fields.push({
275
317
  name,
276
318
  kind: 'property',