@optave/codegraph 3.4.1 → 3.6.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 (349) hide show
  1. package/README.md +50 -28
  2. package/dist/ast-analysis/engine.d.ts.map +1 -1
  3. package/dist/ast-analysis/engine.js +119 -127
  4. package/dist/ast-analysis/engine.js.map +1 -1
  5. package/dist/ast-analysis/rules/javascript.d.ts.map +1 -1
  6. package/dist/ast-analysis/rules/javascript.js +1 -0
  7. package/dist/ast-analysis/rules/javascript.js.map +1 -1
  8. package/dist/ast-analysis/visitors/ast-store-visitor.d.ts.map +1 -1
  9. package/dist/ast-analysis/visitors/ast-store-visitor.js +116 -35
  10. package/dist/ast-analysis/visitors/ast-store-visitor.js.map +1 -1
  11. package/dist/ast-analysis/visitors/complexity-visitor.d.ts.map +1 -1
  12. package/dist/ast-analysis/visitors/complexity-visitor.js +11 -13
  13. package/dist/ast-analysis/visitors/complexity-visitor.js.map +1 -1
  14. package/dist/db/better-sqlite3.d.ts +3 -0
  15. package/dist/db/better-sqlite3.d.ts.map +1 -0
  16. package/dist/db/better-sqlite3.js +19 -0
  17. package/dist/db/better-sqlite3.js.map +1 -0
  18. package/dist/db/connection.d.ts +25 -4
  19. package/dist/db/connection.d.ts.map +1 -1
  20. package/dist/db/connection.js +125 -23
  21. package/dist/db/connection.js.map +1 -1
  22. package/dist/db/index.d.ts +2 -2
  23. package/dist/db/index.d.ts.map +1 -1
  24. package/dist/db/index.js +1 -1
  25. package/dist/db/index.js.map +1 -1
  26. package/dist/db/migrations.d.ts.map +1 -1
  27. package/dist/db/migrations.js +40 -32
  28. package/dist/db/migrations.js.map +1 -1
  29. package/dist/db/query-builder.d.ts +5 -5
  30. package/dist/db/query-builder.d.ts.map +1 -1
  31. package/dist/db/query-builder.js +20 -4
  32. package/dist/db/query-builder.js.map +1 -1
  33. package/dist/db/repository/index.d.ts +1 -0
  34. package/dist/db/repository/index.d.ts.map +1 -1
  35. package/dist/db/repository/index.js +1 -0
  36. package/dist/db/repository/index.js.map +1 -1
  37. package/dist/db/repository/native-repository.d.ts +58 -0
  38. package/dist/db/repository/native-repository.d.ts.map +1 -0
  39. package/dist/db/repository/native-repository.js +261 -0
  40. package/dist/db/repository/native-repository.js.map +1 -0
  41. package/dist/db/repository/nodes.d.ts +4 -4
  42. package/dist/db/repository/nodes.d.ts.map +1 -1
  43. package/dist/db/repository/nodes.js +6 -6
  44. package/dist/db/repository/nodes.js.map +1 -1
  45. package/dist/domain/analysis/context.d.ts.map +1 -1
  46. package/dist/domain/analysis/context.js +51 -66
  47. package/dist/domain/analysis/context.js.map +1 -1
  48. package/dist/domain/analysis/dependencies.d.ts.map +1 -1
  49. package/dist/domain/analysis/dependencies.js +62 -70
  50. package/dist/domain/analysis/dependencies.js.map +1 -1
  51. package/dist/domain/analysis/diff-impact.d.ts +9 -7
  52. package/dist/domain/analysis/diff-impact.d.ts.map +1 -1
  53. package/dist/domain/analysis/exports.d.ts.map +1 -1
  54. package/dist/domain/analysis/exports.js +29 -33
  55. package/dist/domain/analysis/exports.js.map +1 -1
  56. package/dist/domain/analysis/fn-impact.d.ts +15 -17
  57. package/dist/domain/analysis/fn-impact.d.ts.map +1 -1
  58. package/dist/domain/analysis/fn-impact.js +35 -65
  59. package/dist/domain/analysis/fn-impact.js.map +1 -1
  60. package/dist/domain/analysis/module-map.d.ts.map +1 -1
  61. package/dist/domain/analysis/module-map.js +91 -6
  62. package/dist/domain/analysis/module-map.js.map +1 -1
  63. package/dist/domain/analysis/query-helpers.d.ts +20 -0
  64. package/dist/domain/analysis/query-helpers.d.ts.map +1 -0
  65. package/dist/domain/analysis/query-helpers.js +27 -0
  66. package/dist/domain/analysis/query-helpers.js.map +1 -0
  67. package/dist/domain/graph/builder/context.d.ts +2 -1
  68. package/dist/domain/graph/builder/context.d.ts.map +1 -1
  69. package/dist/domain/graph/builder/context.js +1 -0
  70. package/dist/domain/graph/builder/context.js.map +1 -1
  71. package/dist/domain/graph/builder/helpers.d.ts.map +1 -1
  72. package/dist/domain/graph/builder/helpers.js +15 -9
  73. package/dist/domain/graph/builder/helpers.js.map +1 -1
  74. package/dist/domain/graph/builder/incremental.d.ts.map +1 -1
  75. package/dist/domain/graph/builder/incremental.js +3 -2
  76. package/dist/domain/graph/builder/incremental.js.map +1 -1
  77. package/dist/domain/graph/builder/pipeline.d.ts.map +1 -1
  78. package/dist/domain/graph/builder/pipeline.js +95 -7
  79. package/dist/domain/graph/builder/pipeline.js.map +1 -1
  80. package/dist/domain/graph/builder/stages/build-edges.d.ts.map +1 -1
  81. package/dist/domain/graph/builder/stages/build-edges.js +101 -57
  82. package/dist/domain/graph/builder/stages/build-edges.js.map +1 -1
  83. package/dist/domain/graph/builder/stages/build-structure.d.ts.map +1 -1
  84. package/dist/domain/graph/builder/stages/build-structure.js +33 -3
  85. package/dist/domain/graph/builder/stages/build-structure.js.map +1 -1
  86. package/dist/domain/graph/builder/stages/collect-files.d.ts.map +1 -1
  87. package/dist/domain/graph/builder/stages/collect-files.js +70 -6
  88. package/dist/domain/graph/builder/stages/collect-files.js.map +1 -1
  89. package/dist/domain/graph/builder/stages/detect-changes.d.ts.map +1 -1
  90. package/dist/domain/graph/builder/stages/detect-changes.js +36 -14
  91. package/dist/domain/graph/builder/stages/detect-changes.js.map +1 -1
  92. package/dist/domain/graph/builder/stages/finalize.d.ts.map +1 -1
  93. package/dist/domain/graph/builder/stages/finalize.js +130 -88
  94. package/dist/domain/graph/builder/stages/finalize.js.map +1 -1
  95. package/dist/domain/graph/builder/stages/insert-nodes.d.ts.map +1 -1
  96. package/dist/domain/graph/builder/stages/insert-nodes.js +124 -16
  97. package/dist/domain/graph/builder/stages/insert-nodes.js.map +1 -1
  98. package/dist/domain/graph/builder/stages/resolve-imports.d.ts.map +1 -1
  99. package/dist/domain/graph/builder/stages/resolve-imports.js +3 -2
  100. package/dist/domain/graph/builder/stages/resolve-imports.js.map +1 -1
  101. package/dist/domain/graph/resolve.d.ts +0 -4
  102. package/dist/domain/graph/resolve.d.ts.map +1 -1
  103. package/dist/domain/graph/resolve.js +32 -48
  104. package/dist/domain/graph/resolve.js.map +1 -1
  105. package/dist/domain/graph/watcher.d.ts.map +1 -1
  106. package/dist/domain/graph/watcher.js +12 -12
  107. package/dist/domain/graph/watcher.js.map +1 -1
  108. package/dist/domain/parser.d.ts +1 -1
  109. package/dist/domain/parser.d.ts.map +1 -1
  110. package/dist/domain/parser.js +165 -101
  111. package/dist/domain/parser.js.map +1 -1
  112. package/dist/domain/search/search/cli-formatter.d.ts.map +1 -1
  113. package/dist/domain/search/search/cli-formatter.js +88 -83
  114. package/dist/domain/search/search/cli-formatter.js.map +1 -1
  115. package/dist/extractors/bash.d.ts +6 -0
  116. package/dist/extractors/bash.d.ts.map +1 -0
  117. package/dist/extractors/bash.js +91 -0
  118. package/dist/extractors/bash.js.map +1 -0
  119. package/dist/extractors/c.d.ts +6 -0
  120. package/dist/extractors/c.d.ts.map +1 -0
  121. package/dist/extractors/c.js +204 -0
  122. package/dist/extractors/c.js.map +1 -0
  123. package/dist/extractors/cpp.d.ts +6 -0
  124. package/dist/extractors/cpp.d.ts.map +1 -0
  125. package/dist/extractors/cpp.js +283 -0
  126. package/dist/extractors/cpp.js.map +1 -0
  127. package/dist/extractors/csharp.d.ts.map +1 -1
  128. package/dist/extractors/csharp.js +42 -54
  129. package/dist/extractors/csharp.js.map +1 -1
  130. package/dist/extractors/go.d.ts.map +1 -1
  131. package/dist/extractors/go.js +126 -130
  132. package/dist/extractors/go.js.map +1 -1
  133. package/dist/extractors/hcl.js +6 -6
  134. package/dist/extractors/hcl.js.map +1 -1
  135. package/dist/extractors/helpers.d.ts +32 -1
  136. package/dist/extractors/helpers.d.ts.map +1 -1
  137. package/dist/extractors/helpers.js +74 -0
  138. package/dist/extractors/helpers.js.map +1 -1
  139. package/dist/extractors/index.d.ts +6 -0
  140. package/dist/extractors/index.d.ts.map +1 -1
  141. package/dist/extractors/index.js +6 -0
  142. package/dist/extractors/index.js.map +1 -1
  143. package/dist/extractors/java.d.ts.map +1 -1
  144. package/dist/extractors/java.js +32 -47
  145. package/dist/extractors/java.js.map +1 -1
  146. package/dist/extractors/javascript.d.ts.map +1 -1
  147. package/dist/extractors/javascript.js +359 -330
  148. package/dist/extractors/javascript.js.map +1 -1
  149. package/dist/extractors/kotlin.d.ts +6 -0
  150. package/dist/extractors/kotlin.d.ts.map +1 -0
  151. package/dist/extractors/kotlin.js +275 -0
  152. package/dist/extractors/kotlin.js.map +1 -0
  153. package/dist/extractors/php.d.ts.map +1 -1
  154. package/dist/extractors/php.js +39 -44
  155. package/dist/extractors/php.js.map +1 -1
  156. package/dist/extractors/python.d.ts.map +1 -1
  157. package/dist/extractors/python.js +75 -93
  158. package/dist/extractors/python.js.map +1 -1
  159. package/dist/extractors/ruby.js +6 -13
  160. package/dist/extractors/ruby.js.map +1 -1
  161. package/dist/extractors/rust.d.ts.map +1 -1
  162. package/dist/extractors/rust.js +58 -82
  163. package/dist/extractors/rust.js.map +1 -1
  164. package/dist/extractors/scala.d.ts +6 -0
  165. package/dist/extractors/scala.d.ts.map +1 -0
  166. package/dist/extractors/scala.js +269 -0
  167. package/dist/extractors/scala.js.map +1 -0
  168. package/dist/extractors/swift.d.ts +6 -0
  169. package/dist/extractors/swift.d.ts.map +1 -0
  170. package/dist/extractors/swift.js +275 -0
  171. package/dist/extractors/swift.js.map +1 -0
  172. package/dist/features/ast.d.ts +16 -1
  173. package/dist/features/ast.d.ts.map +1 -1
  174. package/dist/features/ast.js +45 -23
  175. package/dist/features/ast.js.map +1 -1
  176. package/dist/features/audit.d.ts.map +1 -1
  177. package/dist/features/audit.js +17 -21
  178. package/dist/features/audit.js.map +1 -1
  179. package/dist/features/branch-compare.d.ts.map +1 -1
  180. package/dist/features/branch-compare.js +50 -4
  181. package/dist/features/branch-compare.js.map +1 -1
  182. package/dist/features/cfg.d.ts +7 -1
  183. package/dist/features/cfg.d.ts.map +1 -1
  184. package/dist/features/cfg.js +118 -62
  185. package/dist/features/cfg.js.map +1 -1
  186. package/dist/features/check.d.ts.map +1 -1
  187. package/dist/features/check.js +79 -62
  188. package/dist/features/check.js.map +1 -1
  189. package/dist/features/complexity-query.d.ts.map +1 -1
  190. package/dist/features/complexity-query.js +142 -137
  191. package/dist/features/complexity-query.js.map +1 -1
  192. package/dist/features/complexity.d.ts +7 -1
  193. package/dist/features/complexity.d.ts.map +1 -1
  194. package/dist/features/complexity.js +62 -1
  195. package/dist/features/complexity.js.map +1 -1
  196. package/dist/features/dataflow.d.ts +7 -1
  197. package/dist/features/dataflow.d.ts.map +1 -1
  198. package/dist/features/dataflow.js +356 -188
  199. package/dist/features/dataflow.js.map +1 -1
  200. package/dist/features/graph-enrichment.d.ts.map +1 -1
  201. package/dist/features/graph-enrichment.js +117 -104
  202. package/dist/features/graph-enrichment.js.map +1 -1
  203. package/dist/features/sequence.d.ts.map +1 -1
  204. package/dist/features/sequence.js +25 -4
  205. package/dist/features/sequence.js.map +1 -1
  206. package/dist/features/snapshot.d.ts.map +1 -1
  207. package/dist/features/snapshot.js +2 -1
  208. package/dist/features/snapshot.js.map +1 -1
  209. package/dist/features/structure-query.d.ts.map +1 -1
  210. package/dist/features/structure-query.js +29 -4
  211. package/dist/features/structure-query.js.map +1 -1
  212. package/dist/features/structure.d.ts.map +1 -1
  213. package/dist/features/structure.js +35 -15
  214. package/dist/features/structure.js.map +1 -1
  215. package/dist/graph/algorithms/leiden/adapter.d.ts.map +1 -1
  216. package/dist/graph/algorithms/leiden/adapter.js +88 -73
  217. package/dist/graph/algorithms/leiden/adapter.js.map +1 -1
  218. package/dist/graph/algorithms/leiden/index.js +43 -28
  219. package/dist/graph/algorithms/leiden/index.js.map +1 -1
  220. package/dist/graph/algorithms/leiden/optimiser.d.ts.map +1 -1
  221. package/dist/graph/algorithms/leiden/optimiser.js +90 -104
  222. package/dist/graph/algorithms/leiden/optimiser.js.map +1 -1
  223. package/dist/graph/algorithms/leiden/partition.d.ts.map +1 -1
  224. package/dist/graph/algorithms/leiden/partition.js +89 -106
  225. package/dist/graph/algorithms/leiden/partition.js.map +1 -1
  226. package/dist/graph/model.d.ts +2 -0
  227. package/dist/graph/model.d.ts.map +1 -1
  228. package/dist/graph/model.js +20 -8
  229. package/dist/graph/model.js.map +1 -1
  230. package/dist/infrastructure/config.d.ts +0 -8
  231. package/dist/infrastructure/config.d.ts.map +1 -1
  232. package/dist/infrastructure/config.js +73 -62
  233. package/dist/infrastructure/config.js.map +1 -1
  234. package/dist/infrastructure/registry.d.ts +0 -8
  235. package/dist/infrastructure/registry.d.ts.map +1 -1
  236. package/dist/infrastructure/registry.js +12 -14
  237. package/dist/infrastructure/registry.js.map +1 -1
  238. package/dist/mcp/server.d.ts.map +1 -1
  239. package/dist/mcp/server.js +47 -45
  240. package/dist/mcp/server.js.map +1 -1
  241. package/dist/presentation/audit.d.ts.map +1 -1
  242. package/dist/presentation/audit.js +61 -57
  243. package/dist/presentation/audit.js.map +1 -1
  244. package/dist/presentation/branch-compare.d.ts.map +1 -1
  245. package/dist/presentation/branch-compare.js +56 -38
  246. package/dist/presentation/branch-compare.js.map +1 -1
  247. package/dist/presentation/check.d.ts.map +1 -1
  248. package/dist/presentation/check.js +30 -32
  249. package/dist/presentation/check.js.map +1 -1
  250. package/dist/presentation/colors.d.ts.map +1 -1
  251. package/dist/presentation/colors.js +2 -0
  252. package/dist/presentation/colors.js.map +1 -1
  253. package/dist/presentation/complexity.d.ts.map +1 -1
  254. package/dist/presentation/complexity.js +25 -19
  255. package/dist/presentation/complexity.js.map +1 -1
  256. package/dist/presentation/queries-cli/exports.d.ts.map +1 -1
  257. package/dist/presentation/queries-cli/exports.js +15 -15
  258. package/dist/presentation/queries-cli/exports.js.map +1 -1
  259. package/dist/presentation/queries-cli/impact.d.ts.map +1 -1
  260. package/dist/presentation/queries-cli/impact.js +29 -19
  261. package/dist/presentation/queries-cli/impact.js.map +1 -1
  262. package/dist/types.d.ts +406 -3
  263. package/dist/types.d.ts.map +1 -1
  264. package/grammars/tree-sitter-bash.wasm +0 -0
  265. package/grammars/tree-sitter-c.wasm +0 -0
  266. package/grammars/tree-sitter-cpp.wasm +0 -0
  267. package/grammars/tree-sitter-kotlin.wasm +0 -0
  268. package/grammars/tree-sitter-scala.wasm +0 -0
  269. package/grammars/tree-sitter-swift.wasm +0 -0
  270. package/package.json +67 -11
  271. package/src/ast-analysis/engine.ts +147 -138
  272. package/src/ast-analysis/rules/javascript.ts +1 -0
  273. package/src/ast-analysis/visitors/ast-store-visitor.ts +116 -34
  274. package/src/ast-analysis/visitors/complexity-visitor.ts +11 -11
  275. package/src/db/better-sqlite3.ts +20 -0
  276. package/src/db/connection.ts +148 -26
  277. package/src/db/index.ts +4 -1
  278. package/src/db/migrations.ts +38 -32
  279. package/src/db/query-builder.ts +30 -5
  280. package/src/db/repository/index.ts +1 -0
  281. package/src/db/repository/native-repository.ts +361 -0
  282. package/src/db/repository/nodes.ts +7 -3
  283. package/src/domain/analysis/context.ts +73 -75
  284. package/src/domain/analysis/dependencies.ts +78 -68
  285. package/src/domain/analysis/exports.ts +45 -34
  286. package/src/domain/analysis/fn-impact.ts +67 -64
  287. package/src/domain/analysis/module-map.ts +103 -8
  288. package/src/domain/analysis/query-helpers.ts +35 -0
  289. package/src/domain/graph/builder/context.ts +2 -0
  290. package/src/domain/graph/builder/helpers.ts +12 -6
  291. package/src/domain/graph/builder/incremental.ts +3 -2
  292. package/src/domain/graph/builder/pipeline.ts +98 -6
  293. package/src/domain/graph/builder/stages/build-edges.ts +116 -83
  294. package/src/domain/graph/builder/stages/build-structure.ts +46 -8
  295. package/src/domain/graph/builder/stages/collect-files.ts +83 -6
  296. package/src/domain/graph/builder/stages/detect-changes.ts +44 -21
  297. package/src/domain/graph/builder/stages/finalize.ts +172 -109
  298. package/src/domain/graph/builder/stages/insert-nodes.ts +147 -17
  299. package/src/domain/graph/builder/stages/resolve-imports.ts +3 -2
  300. package/src/domain/graph/resolve.ts +34 -46
  301. package/src/domain/graph/watcher.ts +12 -14
  302. package/src/domain/parser.ts +169 -97
  303. package/src/domain/search/search/cli-formatter.ts +121 -94
  304. package/src/extractors/bash.ts +97 -0
  305. package/src/extractors/c.ts +212 -0
  306. package/src/extractors/cpp.ts +298 -0
  307. package/src/extractors/csharp.ts +53 -56
  308. package/src/extractors/go.ts +152 -134
  309. package/src/extractors/hcl.ts +6 -6
  310. package/src/extractors/helpers.ts +93 -1
  311. package/src/extractors/index.ts +6 -0
  312. package/src/extractors/java.ts +43 -48
  313. package/src/extractors/javascript.ts +382 -317
  314. package/src/extractors/kotlin.ts +293 -0
  315. package/src/extractors/php.ts +46 -40
  316. package/src/extractors/python.ts +81 -104
  317. package/src/extractors/ruby.ts +6 -13
  318. package/src/extractors/rust.ts +65 -84
  319. package/src/extractors/scala.ts +285 -0
  320. package/src/extractors/swift.ts +293 -0
  321. package/src/features/ast.ts +74 -24
  322. package/src/features/audit.ts +24 -20
  323. package/src/features/branch-compare.ts +54 -5
  324. package/src/features/cfg.ts +158 -65
  325. package/src/features/check.ts +90 -74
  326. package/src/features/complexity-query.ts +181 -163
  327. package/src/features/complexity.ts +64 -1
  328. package/src/features/dataflow.ts +462 -217
  329. package/src/features/graph-enrichment.ts +161 -117
  330. package/src/features/sequence.ts +27 -4
  331. package/src/features/snapshot.ts +2 -1
  332. package/src/features/structure-query.ts +43 -4
  333. package/src/features/structure.ts +50 -22
  334. package/src/graph/algorithms/leiden/adapter.ts +126 -71
  335. package/src/graph/algorithms/leiden/index.ts +67 -28
  336. package/src/graph/algorithms/leiden/optimiser.ts +114 -105
  337. package/src/graph/algorithms/leiden/partition.ts +131 -98
  338. package/src/graph/model.ts +19 -7
  339. package/src/infrastructure/config.ts +60 -58
  340. package/src/infrastructure/registry.ts +17 -14
  341. package/src/mcp/server.ts +48 -47
  342. package/src/presentation/audit.ts +72 -67
  343. package/src/presentation/branch-compare.ts +54 -50
  344. package/src/presentation/check.ts +34 -34
  345. package/src/presentation/colors.ts +2 -0
  346. package/src/presentation/complexity.ts +39 -33
  347. package/src/presentation/queries-cli/exports.ts +17 -17
  348. package/src/presentation/queries-cli/impact.ts +30 -22
  349. package/src/types.ts +458 -3
@@ -102,11 +102,12 @@ async function ensureWasmTreesIfNeeded(
102
102
  opts: AnalysisOpts,
103
103
  rootDir: string,
104
104
  ): Promise<void> {
105
+ const doAst = opts.ast !== false;
105
106
  const doComplexity = opts.complexity !== false;
106
107
  const doCfg = opts.cfg !== false;
107
108
  const doDataflow = opts.dataflow !== false;
108
109
 
109
- if (!doComplexity && !doCfg && !doDataflow) return;
110
+ if (!doAst && !doComplexity && !doCfg && !doDataflow) return;
110
111
 
111
112
  let needsWasmTrees = false;
112
113
  for (const [relPath, symbols] of fileSymbols) {
@@ -131,6 +132,8 @@ async function ensureWasmTreesIfNeeded(
131
132
  d.endLine > d.line &&
132
133
  !d.name.includes('.');
133
134
 
135
+ // AST: need tree when native didn't provide non-call astNodes
136
+ const needsAst = doAst && !Array.isArray(symbols.astNodes) && WALK_EXTENSIONS.has(ext);
134
137
  const needsComplexity =
135
138
  doComplexity &&
136
139
  COMPLEXITY_EXTENSIONS.has(ext) &&
@@ -141,7 +144,7 @@ async function ensureWasmTreesIfNeeded(
141
144
  defs.some((d) => hasFuncBody(d) && d.cfg !== null && !Array.isArray(d.cfg?.blocks));
142
145
  const needsDataflow = doDataflow && !symbols.dataflow && DATAFLOW_EXTENSIONS.has(ext);
143
146
 
144
- if (needsComplexity || needsCfg || needsDataflow) {
147
+ if (needsAst || needsComplexity || needsCfg || needsDataflow) {
145
148
  needsWasmTrees = true;
146
149
  break;
147
150
  }
@@ -159,6 +162,80 @@ async function ensureWasmTreesIfNeeded(
159
162
 
160
163
  // ─── Per-file visitor setup ─────────────────────────────────────────────
161
164
 
165
+ /** Check if a definition has a real function body (not a type signature). */
166
+ function hasFuncBody(d: {
167
+ name: string;
168
+ kind: string;
169
+ line: number;
170
+ endLine?: number | null;
171
+ }): boolean {
172
+ return (
173
+ (d.kind === 'function' || d.kind === 'method') &&
174
+ d.line > 0 &&
175
+ d.endLine != null &&
176
+ d.endLine > d.line &&
177
+ !d.name.includes('.')
178
+ );
179
+ }
180
+
181
+ /** Set up AST-store visitor if applicable. */
182
+ function setupAstVisitor(
183
+ db: BetterSqlite3Database,
184
+ relPath: string,
185
+ symbols: ExtractorOutput,
186
+ langId: string,
187
+ ext: string,
188
+ ): Visitor | null {
189
+ const astTypeMap = AST_TYPE_MAPS.get(langId);
190
+ if (!astTypeMap || !WALK_EXTENSIONS.has(ext) || Array.isArray(symbols.astNodes)) return null;
191
+ const nodeIdMap = new Map<string, number>();
192
+ for (const row of bulkNodeIdsByFile(db, relPath)) {
193
+ nodeIdMap.set(`${row.name}|${row.kind}|${row.line}`, row.id);
194
+ }
195
+ return createAstStoreVisitor(astTypeMap, symbols.definitions || [], relPath, nodeIdMap);
196
+ }
197
+
198
+ /** Set up complexity visitor if any definitions need WASM complexity analysis. */
199
+ function setupComplexityVisitorForFile(
200
+ defs: Definition[],
201
+ langId: string,
202
+ walkerOpts: WalkOptions,
203
+ ): Visitor | null {
204
+ const cRules = COMPLEXITY_RULES.get(langId);
205
+ if (!cRules) return null;
206
+
207
+ const hRules = HALSTEAD_RULES.get(langId);
208
+ const needsWasmComplexity = defs.some((d) => hasFuncBody(d) && !d.complexity);
209
+ if (!needsWasmComplexity) return null;
210
+
211
+ const visitor = createComplexityVisitor(cRules, hRules, { fileLevelWalk: true, langId });
212
+
213
+ for (const t of cRules.nestingNodes) walkerOpts.nestingNodeTypes?.add(t);
214
+
215
+ const dfRules = DATAFLOW_RULES.get(langId);
216
+ walkerOpts.getFunctionName = (node: TreeSitterNode): string | null => {
217
+ const nameNode = node.childForFieldName('name');
218
+ if (nameNode) return nameNode.text;
219
+ if (dfRules) return getFuncName(node, dfRules as any);
220
+ return null;
221
+ };
222
+
223
+ return visitor;
224
+ }
225
+
226
+ /** Set up CFG visitor if any definitions need WASM CFG analysis. */
227
+ function setupCfgVisitorForFile(defs: Definition[], langId: string, ext: string): Visitor | null {
228
+ const cfgRulesForLang = CFG_RULES.get(langId);
229
+ if (!cfgRulesForLang || !CFG_EXTENSIONS.has(ext)) return null;
230
+
231
+ const needsWasmCfg = defs.some(
232
+ (d) => hasFuncBody(d) && d.cfg !== null && !Array.isArray(d.cfg?.blocks),
233
+ );
234
+ if (!needsWasmCfg) return null;
235
+
236
+ return createCfgVisitor(cfgRulesForLang);
237
+ }
238
+
162
239
  function setupVisitors(
163
240
  db: BetterSqlite3Database,
164
241
  relPath: string,
@@ -168,10 +245,6 @@ function setupVisitors(
168
245
  ): SetupResult {
169
246
  const ext = path.extname(relPath).toLowerCase();
170
247
  const defs = symbols.definitions || [];
171
- const doAst = opts.ast !== false;
172
- const doComplexity = opts.complexity !== false;
173
- const doCfg = opts.cfg !== false;
174
- const doDataflow = opts.dataflow !== false;
175
248
 
176
249
  const visitors: Visitor[] = [];
177
250
  const walkerOpts: WalkOptions = {
@@ -180,75 +253,19 @@ function setupVisitors(
180
253
  getFunctionName: (_node: TreeSitterNode) => null,
181
254
  };
182
255
 
183
- // AST-store visitor
184
- let astVisitor: Visitor | null = null;
185
- const astTypeMap = AST_TYPE_MAPS.get(langId);
186
- if (doAst && astTypeMap && WALK_EXTENSIONS.has(ext) && !Array.isArray(symbols.astNodes)) {
187
- const nodeIdMap = new Map<string, number>();
188
- for (const row of bulkNodeIdsByFile(db, relPath)) {
189
- nodeIdMap.set(`${row.name}|${row.kind}|${row.line}`, row.id);
190
- }
191
- astVisitor = createAstStoreVisitor(astTypeMap, defs, relPath, nodeIdMap);
192
- visitors.push(astVisitor);
193
- }
256
+ const astVisitor = opts.ast !== false ? setupAstVisitor(db, relPath, symbols, langId, ext) : null;
257
+ if (astVisitor) visitors.push(astVisitor);
194
258
 
195
- // Complexity visitor (file-level mode)
196
- let complexityVisitor: Visitor | null = null;
197
- const cRules = COMPLEXITY_RULES.get(langId);
198
- const hRules = HALSTEAD_RULES.get(langId);
199
- if (doComplexity && cRules) {
200
- // Only trigger WASM complexity for definitions with real function bodies.
201
- // Interface/type property signatures (dotted names, single-line span)
202
- // correctly lack native complexity data and should not trigger a fallback.
203
- const needsWasmComplexity = defs.some(
204
- (d) =>
205
- (d.kind === 'function' || d.kind === 'method') &&
206
- d.line > 0 &&
207
- d.endLine != null &&
208
- d.endLine > d.line &&
209
- !d.name.includes('.') &&
210
- !d.complexity,
211
- );
212
- if (needsWasmComplexity) {
213
- complexityVisitor = createComplexityVisitor(cRules, hRules, { fileLevelWalk: true, langId });
214
- visitors.push(complexityVisitor);
215
-
216
- for (const t of cRules.nestingNodes) walkerOpts.nestingNodeTypes?.add(t);
217
-
218
- const dfRules = DATAFLOW_RULES.get(langId);
219
- walkerOpts.getFunctionName = (node: TreeSitterNode): string | null => {
220
- const nameNode = node.childForFieldName('name');
221
- if (nameNode) return nameNode.text;
222
- if (dfRules) return getFuncName(node, dfRules as any);
223
- return null;
224
- };
225
- }
226
- }
259
+ const complexityVisitor =
260
+ opts.complexity !== false ? setupComplexityVisitorForFile(defs, langId, walkerOpts) : null;
261
+ if (complexityVisitor) visitors.push(complexityVisitor);
227
262
 
228
- // CFG visitor
229
- let cfgVisitor: Visitor | null = null;
230
- const cfgRulesForLang = CFG_RULES.get(langId);
231
- if (doCfg && cfgRulesForLang && CFG_EXTENSIONS.has(ext)) {
232
- const needsWasmCfg = defs.some(
233
- (d) =>
234
- (d.kind === 'function' || d.kind === 'method') &&
235
- d.line > 0 &&
236
- d.endLine != null &&
237
- d.endLine > d.line &&
238
- !d.name.includes('.') &&
239
- d.cfg !== null &&
240
- !Array.isArray(d.cfg?.blocks),
241
- );
242
- if (needsWasmCfg) {
243
- cfgVisitor = createCfgVisitor(cfgRulesForLang);
244
- visitors.push(cfgVisitor);
245
- }
246
- }
263
+ const cfgVisitor = opts.cfg !== false ? setupCfgVisitorForFile(defs, langId, ext) : null;
264
+ if (cfgVisitor) visitors.push(cfgVisitor);
247
265
 
248
- // Dataflow visitor
249
266
  let dataflowVisitor: Visitor | null = null;
250
267
  const dfRules = DATAFLOW_RULES.get(langId);
251
- if (doDataflow && dfRules && DATAFLOW_EXTENSIONS.has(ext) && !symbols.dataflow) {
268
+ if (opts.dataflow !== false && dfRules && DATAFLOW_EXTENSIONS.has(ext) && !symbols.dataflow) {
252
269
  dataflowVisitor = createDataflowVisitor(dfRules);
253
270
  visitors.push(dataflowVisitor);
254
271
  }
@@ -258,88 +275,80 @@ function setupVisitors(
258
275
 
259
276
  // ─── Result storage helpers ─────────────────────────────────────────────
260
277
 
261
- function storeComplexityResults(results: WalkResults, defs: Definition[], langId: string): void {
262
- const complexityResults = (results.complexity || []) as ComplexityFuncResult[];
263
- const resultByLine = new Map<number, ComplexityFuncResult[]>();
264
- for (const r of complexityResults) {
265
- if (r.funcNode) {
266
- const line = r.funcNode.startPosition.row + 1;
267
- if (!resultByLine.has(line)) resultByLine.set(line, []);
268
- resultByLine.get(line)?.push(r);
269
- }
278
+ /** Index per-function results by start line for O(1) lookup. */
279
+ function indexByLine<T extends { funcNode: TreeSitterNode }>(results: T[]): Map<number, T[]> {
280
+ const byLine = new Map<number, T[]>();
281
+ for (const r of results) {
282
+ if (!r.funcNode) continue;
283
+ const line = r.funcNode.startPosition.row + 1;
284
+ if (!byLine.has(line)) byLine.set(line, []);
285
+ byLine.get(line)?.push(r);
270
286
  }
287
+ return byLine;
288
+ }
289
+
290
+ /** Find the best matching result for a definition by line + name. */
291
+ function matchResultToDef<T extends { funcNode: TreeSitterNode }>(
292
+ candidates: T[] | undefined,
293
+ defName: string,
294
+ ): T | undefined {
295
+ if (!candidates) return undefined;
296
+ if (candidates.length === 1) return candidates[0];
297
+ return (
298
+ candidates.find((r) => {
299
+ const n = r.funcNode.childForFieldName('name');
300
+ return n && n.text === defName;
301
+ }) ?? candidates[0]
302
+ );
303
+ }
304
+
305
+ function storeComplexityResults(results: WalkResults, defs: Definition[], langId: string): void {
306
+ const byLine = indexByLine((results.complexity || []) as ComplexityFuncResult[]);
271
307
  for (const def of defs) {
272
308
  if ((def.kind === 'function' || def.kind === 'method') && def.line && !def.complexity) {
273
- const candidates = resultByLine.get(def.line);
274
- const funcResult = !candidates
275
- ? undefined
276
- : candidates.length === 1
277
- ? candidates[0]
278
- : (candidates.find((r) => {
279
- const n = r.funcNode.childForFieldName('name');
280
- return n && n.text === def.name;
281
- }) ?? candidates[0]);
282
- if (funcResult) {
283
- const { metrics } = funcResult;
284
- const loc = computeLOCMetrics(funcResult.funcNode, langId);
285
- const volume = metrics.halstead ? metrics.halstead.volume : 0;
286
- const commentRatio = loc.loc > 0 ? loc.commentLines / loc.loc : 0;
287
- const mi = computeMaintainabilityIndex(volume, metrics.cyclomatic, loc.sloc, commentRatio);
288
-
289
- def.complexity = {
290
- cognitive: metrics.cognitive,
291
- cyclomatic: metrics.cyclomatic,
292
- maxNesting: metrics.maxNesting,
293
- halstead: metrics.halstead,
294
- loc,
295
- maintainabilityIndex: mi,
296
- };
297
- }
309
+ const funcResult = matchResultToDef(byLine.get(def.line), def.name);
310
+ if (!funcResult) continue;
311
+ const { metrics } = funcResult;
312
+ const loc = computeLOCMetrics(funcResult.funcNode, langId);
313
+ const volume = metrics.halstead ? metrics.halstead.volume : 0;
314
+ const commentRatio = loc.loc > 0 ? loc.commentLines / loc.loc : 0;
315
+ const mi = computeMaintainabilityIndex(volume, metrics.cyclomatic, loc.sloc, commentRatio);
316
+ def.complexity = {
317
+ cognitive: metrics.cognitive,
318
+ cyclomatic: metrics.cyclomatic,
319
+ maxNesting: metrics.maxNesting,
320
+ halstead: metrics.halstead,
321
+ loc,
322
+ maintainabilityIndex: mi,
323
+ };
298
324
  }
299
325
  }
300
326
  }
301
327
 
302
328
  function storeCfgResults(results: WalkResults, defs: Definition[]): void {
303
- const cfgResults = (results.cfg || []) as CfgFuncResult[];
304
- const cfgByLine = new Map<number, CfgFuncResult[]>();
305
- for (const r of cfgResults) {
306
- if (r.funcNode) {
307
- const line = r.funcNode.startPosition.row + 1;
308
- if (!cfgByLine.has(line)) cfgByLine.set(line, []);
309
- cfgByLine.get(line)?.push(r);
310
- }
311
- }
329
+ const byLine = indexByLine((results.cfg || []) as CfgFuncResult[]);
312
330
  for (const def of defs) {
313
331
  if (
314
332
  (def.kind === 'function' || def.kind === 'method') &&
315
333
  def.line &&
316
334
  !def.cfg?.blocks?.length
317
335
  ) {
318
- const candidates = cfgByLine.get(def.line);
319
- const cfgResult = !candidates
320
- ? undefined
321
- : candidates.length === 1
322
- ? candidates[0]
323
- : (candidates.find((r) => {
324
- const n = r.funcNode.childForFieldName('name');
325
- return n && n.text === def.name;
326
- }) ?? candidates[0]);
327
- if (cfgResult) {
328
- def.cfg = { blocks: cfgResult.blocks, edges: cfgResult.edges };
329
-
330
- // Override complexity's cyclomatic with CFG-derived value (single source of truth)
331
- if (def.complexity && cfgResult.cyclomatic != null) {
332
- def.complexity.cyclomatic = cfgResult.cyclomatic;
333
- const { loc, halstead } = def.complexity;
334
- const volume = halstead ? halstead.volume : 0;
335
- const commentRatio = loc && loc.loc > 0 ? loc.commentLines / loc.loc : 0;
336
- def.complexity.maintainabilityIndex = computeMaintainabilityIndex(
337
- volume,
338
- cfgResult.cyclomatic,
339
- loc?.sloc ?? 0,
340
- commentRatio,
341
- );
342
- }
336
+ const cfgResult = matchResultToDef(byLine.get(def.line), def.name);
337
+ if (!cfgResult) continue;
338
+ def.cfg = { blocks: cfgResult.blocks, edges: cfgResult.edges };
339
+
340
+ // Override complexity's cyclomatic with CFG-derived value (single source of truth)
341
+ if (def.complexity && cfgResult.cyclomatic != null) {
342
+ def.complexity.cyclomatic = cfgResult.cyclomatic;
343
+ const { loc, halstead } = def.complexity;
344
+ const volume = halstead ? halstead.volume : 0;
345
+ const commentRatio = loc && loc.loc > 0 ? loc.commentLines / loc.loc : 0;
346
+ def.complexity.maintainabilityIndex = computeMaintainabilityIndex(
347
+ volume,
348
+ cfgResult.cyclomatic,
349
+ loc?.sloc ?? 0,
350
+ commentRatio,
351
+ );
343
352
  }
344
353
  }
345
354
  }
@@ -237,6 +237,7 @@ export const dataflow: DataflowRulesConfig = makeDataflowRules({
237
237
  // ─── AST Node Types ───────────────────────────────────────────────────────
238
238
 
239
239
  export const astTypes: Record<string, string> | null = {
240
+ call_expression: 'call',
240
241
  new_expression: 'new',
241
242
  throw_statement: 'throw',
242
243
  await_expression: 'await',
@@ -14,7 +14,7 @@ interface AstStoreRow {
14
14
  kind: string;
15
15
  name: string | null | undefined;
16
16
  text: string | null;
17
- receiver: null;
17
+ receiver: string | null;
18
18
  parentNodeId: number | null;
19
19
  }
20
20
 
@@ -44,6 +44,22 @@ function extractExpressionText(node: TreeSitterNode): string | null {
44
44
  return truncate(node.text);
45
45
  }
46
46
 
47
+ function extractCallName(node: TreeSitterNode): string {
48
+ for (const field of ['function', 'method', 'name']) {
49
+ const fn = node.childForFieldName(field);
50
+ if (fn) return fn.text;
51
+ }
52
+ return node.text?.split('(')[0] || '?';
53
+ }
54
+
55
+ /** Extract receiver for call expressions (e.g. "obj" in "obj.method()"). */
56
+ function extractCallReceiver(node: TreeSitterNode): string | null {
57
+ const fn = node.childForFieldName('function');
58
+ if (!fn || fn.type !== 'member_expression') return null;
59
+ const obj = fn.childForFieldName('object');
60
+ return obj ? obj.text : null;
61
+ }
62
+
47
63
  function extractName(kind: string, node: TreeSitterNode): string | null {
48
64
  if (kind === 'throw') {
49
65
  for (let i = 0; i < node.childCount; i++) {
@@ -102,49 +118,115 @@ export function createAstStoreVisitor(
102
118
  return nodeIdMap.get(`${parentDef.name}|${parentDef.kind}|${parentDef.line}`) || null;
103
119
  }
104
120
 
121
+ /** Recursively walk a subtree collecting AST nodes — used for arguments-only traversal. */
122
+ function walkSubtree(node: TreeSitterNode | null): void {
123
+ if (!node) return;
124
+ if (matched.has(node.id)) return;
125
+
126
+ const kind = astTypeMap[node.type];
127
+ if (kind === 'call') {
128
+ // Capture this call and recurse only into its arguments
129
+ collectNode(node, kind);
130
+ walkCallArguments(node);
131
+ return;
132
+ }
133
+ if (kind) {
134
+ collectNode(node, kind);
135
+ if (kind !== 'string' && kind !== 'regex') return; // skipChildren for non-leaf kinds
136
+ }
137
+ for (let i = 0; i < node.childCount; i++) {
138
+ walkSubtree(node.child(i));
139
+ }
140
+ }
141
+
142
+ /**
143
+ * Recurse into only the arguments of a call node — mirrors the native engine's
144
+ * strategy that prevents double-counting nested calls in the function field
145
+ * (e.g. chained calls like `a().b()`).
146
+ */
147
+ function walkCallArguments(callNode: TreeSitterNode): void {
148
+ // Try field-based lookup first, fall back to kind-based matching
149
+ const argsNode =
150
+ callNode.childForFieldName('arguments') ??
151
+ findChildByKind(callNode, ['arguments', 'argument_list', 'method_arguments']);
152
+ if (!argsNode) return;
153
+ for (let i = 0; i < argsNode.childCount; i++) {
154
+ walkSubtree(argsNode.child(i));
155
+ }
156
+ }
157
+
158
+ function findChildByKind(node: TreeSitterNode, kinds: string[]): TreeSitterNode | null {
159
+ for (let i = 0; i < node.childCount; i++) {
160
+ const child = node.child(i);
161
+ if (child && kinds.includes(child.type)) return child;
162
+ }
163
+ return null;
164
+ }
165
+
166
+ function collectNode(node: TreeSitterNode, kind: string): void {
167
+ if (matched.has(node.id)) return;
168
+
169
+ const line = node.startPosition.row + 1;
170
+ let name: string | null | undefined;
171
+ let text: string | null = null;
172
+ let receiver: string | null = null;
173
+
174
+ if (kind === 'call') {
175
+ name = extractCallName(node);
176
+ text = truncate(node.text);
177
+ receiver = extractCallReceiver(node);
178
+ } else if (kind === 'new') {
179
+ name = extractNewName(node);
180
+ text = truncate(node.text);
181
+ } else if (kind === 'throw') {
182
+ name = extractName('throw', node);
183
+ text = extractExpressionText(node);
184
+ } else if (kind === 'await') {
185
+ name = extractName('await', node);
186
+ text = extractExpressionText(node);
187
+ } else if (kind === 'string') {
188
+ const content = node.text?.replace(/^['"`]|['"`]$/g, '') || '';
189
+ if (content.length < 2) return;
190
+ name = truncate(content, 100);
191
+ text = truncate(node.text);
192
+ } else if (kind === 'regex') {
193
+ name = node.text || '?';
194
+ text = truncate(node.text);
195
+ }
196
+
197
+ rows.push({
198
+ file: relPath,
199
+ line,
200
+ kind,
201
+ name,
202
+ text,
203
+ receiver,
204
+ parentNodeId: resolveParentNodeId(line),
205
+ });
206
+
207
+ matched.add(node.id);
208
+ }
209
+
105
210
  return {
106
211
  name: 'ast-store',
107
212
 
108
213
  enterNode(node: TreeSitterNode, _context: VisitorContext): EnterNodeResult | undefined {
214
+ // Guard: skip re-collection but do NOT skipChildren — node.id (memory address)
215
+ // can be reused by tree-sitter, so a collision would incorrectly suppress an
216
+ // unrelated subtree. The parent call's skipChildren handles the intended case.
109
217
  if (matched.has(node.id)) return;
110
218
 
111
219
  const kind = astTypeMap[node.type];
112
220
  if (!kind) return;
113
221
 
114
- const line = node.startPosition.row + 1;
115
- let name: string | null | undefined;
116
- let text: string | null = null;
117
-
118
- if (kind === 'new') {
119
- name = extractNewName(node);
120
- text = truncate(node.text);
121
- } else if (kind === 'throw') {
122
- name = extractName('throw', node);
123
- text = extractExpressionText(node);
124
- } else if (kind === 'await') {
125
- name = extractName('await', node);
126
- text = extractExpressionText(node);
127
- } else if (kind === 'string') {
128
- const content = node.text?.replace(/^['"`]|['"`]$/g, '') || '';
129
- if (content.length < 2) return;
130
- name = truncate(content, 100);
131
- text = truncate(node.text);
132
- } else if (kind === 'regex') {
133
- name = node.text || '?';
134
- text = truncate(node.text);
135
- }
222
+ collectNode(node, kind);
136
223
 
137
- rows.push({
138
- file: relPath,
139
- line,
140
- kind,
141
- name,
142
- text,
143
- receiver: null,
144
- parentNodeId: resolveParentNodeId(line),
145
- });
146
-
147
- matched.add(node.id);
224
+ if (kind === 'call') {
225
+ // Mirror native: skip full subtree, recurse only into arguments.
226
+ // Prevents double-counting chained calls like service.getUser().getName().
227
+ walkCallArguments(node);
228
+ return { skipChildren: true };
229
+ }
148
230
 
149
231
  if (kind !== 'string' && kind !== 'regex') {
150
232
  return { skipChildren: true };
@@ -87,6 +87,16 @@ function classifyBranchNode(
87
87
  }
88
88
  }
89
89
 
90
+ function classifyLogicalOp(node: TreeSitterNode, cRules: AnyRules, acc: ComplexityAcc): void {
91
+ const op = node.child(1)?.type;
92
+ if (!op || !cRules.logicalOperators.has(op)) return;
93
+ acc.cyclomatic++;
94
+ const parent = node.parent;
95
+ const sameSequence =
96
+ parent != null && parent.type === cRules.logicalNodeType && parent.child(1)?.type === op;
97
+ if (!sameSequence) acc.cognitive++;
98
+ }
99
+
90
100
  function classifyPlainElse(
91
101
  node: TreeSitterNode,
92
102
  type: string,
@@ -215,17 +225,7 @@ export function createComplexityVisitor(
215
225
  if (nestingLevel > acc.maxNesting) acc.maxNesting = nestingLevel;
216
226
 
217
227
  if (type === cRules.logicalNodeType) {
218
- const op = node.child(1)?.type;
219
- if (op && cRules.logicalOperators.has(op)) {
220
- acc.cyclomatic++;
221
- const parent = node.parent;
222
- let sameSequence = false;
223
- if (parent && parent.type === cRules.logicalNodeType) {
224
- const parentOp = parent.child(1)?.type;
225
- if (parentOp === op) sameSequence = true;
226
- }
227
- if (!sameSequence) acc.cognitive++;
228
- }
228
+ classifyLogicalOp(node, cRules, acc);
229
229
  }
230
230
 
231
231
  if (type === cRules.optionalChainType) acc.cyclomatic++;
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Lazy-loaded better-sqlite3 constructor.
3
+ *
4
+ * Centralises the `createRequire` + cache pattern so every call site that
5
+ * needs a JS-side SQLite handle can `import { getDatabase } from '…/db/better-sqlite3.js'`
6
+ * instead of duplicating the boilerplate. The native engine path (NativeDatabase /
7
+ * rusqlite) never touches this module.
8
+ */
9
+ import { createRequire } from 'node:module';
10
+
11
+ const _require = createRequire(import.meta.url);
12
+ let _Database: any;
13
+
14
+ /** Return the `better-sqlite3` Database constructor, loading it on first call. */
15
+ export function getDatabase(): new (...args: any[]) => any {
16
+ if (!_Database) {
17
+ _Database = _require('better-sqlite3');
18
+ }
19
+ return _Database;
20
+ }