@optave/codegraph 3.8.0 → 3.9.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 (310) hide show
  1. package/README.md +13 -8
  2. package/dist/ast-analysis/engine.d.ts.map +1 -1
  3. package/dist/ast-analysis/engine.js +137 -86
  4. package/dist/ast-analysis/engine.js.map +1 -1
  5. package/dist/ast-analysis/metrics.d.ts +0 -3
  6. package/dist/ast-analysis/metrics.d.ts.map +1 -1
  7. package/dist/ast-analysis/metrics.js +30 -13
  8. package/dist/ast-analysis/metrics.js.map +1 -1
  9. package/dist/ast-analysis/shared.d.ts.map +1 -1
  10. package/dist/ast-analysis/shared.js +24 -19
  11. package/dist/ast-analysis/shared.js.map +1 -1
  12. package/dist/ast-analysis/visitor-utils.d.ts.map +1 -1
  13. package/dist/ast-analysis/visitor-utils.js +55 -39
  14. package/dist/ast-analysis/visitor-utils.js.map +1 -1
  15. package/dist/ast-analysis/visitor.d.ts.map +1 -1
  16. package/dist/ast-analysis/visitor.js +91 -70
  17. package/dist/ast-analysis/visitor.js.map +1 -1
  18. package/dist/ast-analysis/visitors/ast-store-visitor.d.ts.map +1 -1
  19. package/dist/ast-analysis/visitors/ast-store-visitor.js +54 -58
  20. package/dist/ast-analysis/visitors/ast-store-visitor.js.map +1 -1
  21. package/dist/ast-analysis/visitors/complexity-visitor.d.ts.map +1 -1
  22. package/dist/ast-analysis/visitors/complexity-visitor.js +81 -39
  23. package/dist/ast-analysis/visitors/complexity-visitor.js.map +1 -1
  24. package/dist/ast-analysis/visitors/dataflow-visitor.d.ts.map +1 -1
  25. package/dist/ast-analysis/visitors/dataflow-visitor.js +57 -38
  26. package/dist/ast-analysis/visitors/dataflow-visitor.js.map +1 -1
  27. package/dist/cli/commands/branch-compare.d.ts.map +1 -1
  28. package/dist/cli/commands/branch-compare.js +4 -0
  29. package/dist/cli/commands/branch-compare.js.map +1 -1
  30. package/dist/cli/commands/diff-impact.d.ts.map +1 -1
  31. package/dist/cli/commands/diff-impact.js +2 -1
  32. package/dist/cli/commands/diff-impact.js.map +1 -1
  33. package/dist/cli/commands/info.d.ts.map +1 -1
  34. package/dist/cli/commands/info.js +3 -2
  35. package/dist/cli/commands/info.js.map +1 -1
  36. package/dist/cli/commands/watch.d.ts.map +1 -1
  37. package/dist/cli/commands/watch.js +16 -2
  38. package/dist/cli/commands/watch.js.map +1 -1
  39. package/dist/db/connection.d.ts.map +1 -1
  40. package/dist/db/connection.js +29 -26
  41. package/dist/db/connection.js.map +1 -1
  42. package/dist/db/query-builder.d.ts.map +1 -1
  43. package/dist/db/query-builder.js +16 -5
  44. package/dist/db/query-builder.js.map +1 -1
  45. package/dist/db/repository/base.d.ts +16 -0
  46. package/dist/db/repository/base.d.ts.map +1 -1
  47. package/dist/db/repository/base.js +31 -0
  48. package/dist/db/repository/base.js.map +1 -1
  49. package/dist/db/repository/native-repository.d.ts +7 -1
  50. package/dist/db/repository/native-repository.d.ts.map +1 -1
  51. package/dist/db/repository/native-repository.js +100 -1
  52. package/dist/db/repository/native-repository.js.map +1 -1
  53. package/dist/db/repository/nodes.d.ts.map +1 -1
  54. package/dist/db/repository/nodes.js +8 -4
  55. package/dist/db/repository/nodes.js.map +1 -1
  56. package/dist/db/repository/sqlite-repository.d.ts +4 -0
  57. package/dist/db/repository/sqlite-repository.d.ts.map +1 -1
  58. package/dist/db/repository/sqlite-repository.js +51 -0
  59. package/dist/db/repository/sqlite-repository.js.map +1 -1
  60. package/dist/domain/analysis/brief.d.ts.map +1 -1
  61. package/dist/domain/analysis/brief.js +13 -17
  62. package/dist/domain/analysis/brief.js.map +1 -1
  63. package/dist/domain/analysis/context.d.ts.map +1 -1
  64. package/dist/domain/analysis/context.js +14 -11
  65. package/dist/domain/analysis/context.js.map +1 -1
  66. package/dist/domain/analysis/dependencies.d.ts.map +1 -1
  67. package/dist/domain/analysis/dependencies.js +64 -59
  68. package/dist/domain/analysis/dependencies.js.map +1 -1
  69. package/dist/domain/analysis/fn-impact.d.ts +2 -7
  70. package/dist/domain/analysis/fn-impact.d.ts.map +1 -1
  71. package/dist/domain/analysis/fn-impact.js +33 -31
  72. package/dist/domain/analysis/fn-impact.js.map +1 -1
  73. package/dist/domain/analysis/implementations.d.ts.map +1 -1
  74. package/dist/domain/analysis/implementations.js +11 -19
  75. package/dist/domain/analysis/implementations.js.map +1 -1
  76. package/dist/domain/analysis/module-map.d.ts.map +1 -1
  77. package/dist/domain/analysis/module-map.js +55 -76
  78. package/dist/domain/analysis/module-map.js.map +1 -1
  79. package/dist/domain/analysis/query-helpers.d.ts +7 -0
  80. package/dist/domain/analysis/query-helpers.d.ts.map +1 -1
  81. package/dist/domain/analysis/query-helpers.js +15 -1
  82. package/dist/domain/analysis/query-helpers.js.map +1 -1
  83. package/dist/domain/graph/builder/pipeline.d.ts.map +1 -1
  84. package/dist/domain/graph/builder/pipeline.js +352 -107
  85. package/dist/domain/graph/builder/pipeline.js.map +1 -1
  86. package/dist/domain/graph/builder/stages/build-edges.d.ts.map +1 -1
  87. package/dist/domain/graph/builder/stages/build-edges.js +49 -18
  88. package/dist/domain/graph/builder/stages/build-edges.js.map +1 -1
  89. package/dist/domain/graph/builder/stages/detect-changes.js +2 -2
  90. package/dist/domain/graph/builder/stages/detect-changes.js.map +1 -1
  91. package/dist/domain/graph/builder/stages/finalize.js +2 -2
  92. package/dist/domain/graph/builder/stages/finalize.js.map +1 -1
  93. package/dist/domain/graph/builder/stages/insert-nodes.d.ts.map +1 -1
  94. package/dist/domain/graph/builder/stages/insert-nodes.js +32 -21
  95. package/dist/domain/graph/builder/stages/insert-nodes.js.map +1 -1
  96. package/dist/domain/graph/builder/stages/resolve-imports.d.ts.map +1 -1
  97. package/dist/domain/graph/builder/stages/resolve-imports.js +95 -84
  98. package/dist/domain/graph/builder/stages/resolve-imports.js.map +1 -1
  99. package/dist/domain/graph/cycles.d.ts +6 -0
  100. package/dist/domain/graph/cycles.d.ts.map +1 -1
  101. package/dist/domain/graph/cycles.js +114 -22
  102. package/dist/domain/graph/cycles.js.map +1 -1
  103. package/dist/domain/graph/resolve.js +1 -1
  104. package/dist/domain/graph/resolve.js.map +1 -1
  105. package/dist/domain/graph/watcher.d.ts +2 -0
  106. package/dist/domain/graph/watcher.d.ts.map +1 -1
  107. package/dist/domain/graph/watcher.js +170 -75
  108. package/dist/domain/graph/watcher.js.map +1 -1
  109. package/dist/domain/parser.d.ts +3 -4
  110. package/dist/domain/parser.d.ts.map +1 -1
  111. package/dist/domain/parser.js +141 -89
  112. package/dist/domain/parser.js.map +1 -1
  113. package/dist/domain/search/generator.js +1 -1
  114. package/dist/domain/search/generator.js.map +1 -1
  115. package/dist/domain/search/models.d.ts +4 -3
  116. package/dist/domain/search/models.d.ts.map +1 -1
  117. package/dist/domain/search/models.js +23 -8
  118. package/dist/domain/search/models.js.map +1 -1
  119. package/dist/domain/search/search/hybrid.d.ts.map +1 -1
  120. package/dist/domain/search/search/hybrid.js +29 -18
  121. package/dist/domain/search/search/hybrid.js.map +1 -1
  122. package/dist/extractors/go.js +36 -33
  123. package/dist/extractors/go.js.map +1 -1
  124. package/dist/extractors/helpers.d.ts.map +1 -1
  125. package/dist/extractors/helpers.js +40 -29
  126. package/dist/extractors/helpers.js.map +1 -1
  127. package/dist/extractors/java.js +58 -46
  128. package/dist/extractors/java.js.map +1 -1
  129. package/dist/extractors/javascript.js +65 -54
  130. package/dist/extractors/javascript.js.map +1 -1
  131. package/dist/extractors/kotlin.js +84 -78
  132. package/dist/extractors/kotlin.js.map +1 -1
  133. package/dist/extractors/python.js +29 -24
  134. package/dist/extractors/python.js.map +1 -1
  135. package/dist/extractors/rust.js +41 -32
  136. package/dist/extractors/rust.js.map +1 -1
  137. package/dist/extractors/solidity.js +58 -67
  138. package/dist/extractors/solidity.js.map +1 -1
  139. package/dist/extractors/swift.js +83 -81
  140. package/dist/extractors/swift.js.map +1 -1
  141. package/dist/extractors/zig.js +58 -60
  142. package/dist/extractors/zig.js.map +1 -1
  143. package/dist/features/ast.d.ts +16 -14
  144. package/dist/features/ast.d.ts.map +1 -1
  145. package/dist/features/ast.js +83 -81
  146. package/dist/features/ast.js.map +1 -1
  147. package/dist/features/audit.d.ts.map +1 -1
  148. package/dist/features/audit.js +8 -6
  149. package/dist/features/audit.js.map +1 -1
  150. package/dist/features/branch-compare.d.ts.map +1 -1
  151. package/dist/features/branch-compare.js +69 -72
  152. package/dist/features/branch-compare.js.map +1 -1
  153. package/dist/features/communities.d.ts.map +1 -1
  154. package/dist/features/communities.js +19 -7
  155. package/dist/features/communities.js.map +1 -1
  156. package/dist/features/complexity.d.ts.map +1 -1
  157. package/dist/features/complexity.js +120 -125
  158. package/dist/features/complexity.js.map +1 -1
  159. package/dist/features/dataflow.d.ts.map +1 -1
  160. package/dist/features/dataflow.js +136 -137
  161. package/dist/features/dataflow.js.map +1 -1
  162. package/dist/features/flow.d.ts.map +1 -1
  163. package/dist/features/flow.js +84 -79
  164. package/dist/features/flow.js.map +1 -1
  165. package/dist/features/structure-query.d.ts.map +1 -1
  166. package/dist/features/structure-query.js +69 -65
  167. package/dist/features/structure-query.js.map +1 -1
  168. package/dist/graph/algorithms/leiden/optimiser.d.ts.map +1 -1
  169. package/dist/graph/algorithms/leiden/optimiser.js +70 -55
  170. package/dist/graph/algorithms/leiden/optimiser.js.map +1 -1
  171. package/dist/graph/algorithms/leiden/partition.d.ts.map +1 -1
  172. package/dist/graph/algorithms/leiden/partition.js +288 -266
  173. package/dist/graph/algorithms/leiden/partition.js.map +1 -1
  174. package/dist/graph/model.d.ts.map +1 -1
  175. package/dist/graph/model.js +5 -1
  176. package/dist/graph/model.js.map +1 -1
  177. package/dist/infrastructure/config.d.ts.map +1 -1
  178. package/dist/infrastructure/config.js +6 -4
  179. package/dist/infrastructure/config.js.map +1 -1
  180. package/dist/infrastructure/suppress.d.ts +25 -0
  181. package/dist/infrastructure/suppress.d.ts.map +1 -0
  182. package/dist/infrastructure/suppress.js +43 -0
  183. package/dist/infrastructure/suppress.js.map +1 -0
  184. package/dist/mcp/server.d.ts.map +1 -1
  185. package/dist/mcp/server.js +29 -24
  186. package/dist/mcp/server.js.map +1 -1
  187. package/dist/presentation/dataflow.d.ts.map +1 -1
  188. package/dist/presentation/dataflow.js +47 -38
  189. package/dist/presentation/dataflow.js.map +1 -1
  190. package/dist/presentation/diff-impact-mermaid.d.ts.map +1 -1
  191. package/dist/presentation/diff-impact-mermaid.js +60 -51
  192. package/dist/presentation/diff-impact-mermaid.js.map +1 -1
  193. package/dist/presentation/queries-cli/exports.d.ts.map +1 -1
  194. package/dist/presentation/queries-cli/exports.js +20 -14
  195. package/dist/presentation/queries-cli/exports.js.map +1 -1
  196. package/dist/presentation/queries-cli/impact.d.ts.map +1 -1
  197. package/dist/presentation/queries-cli/impact.js +15 -13
  198. package/dist/presentation/queries-cli/impact.js.map +1 -1
  199. package/dist/presentation/queries-cli/inspect.d.ts.map +1 -1
  200. package/dist/presentation/queries-cli/inspect.js +101 -79
  201. package/dist/presentation/queries-cli/inspect.js.map +1 -1
  202. package/dist/presentation/queries-cli/overview.d.ts.map +1 -1
  203. package/dist/presentation/queries-cli/overview.js +25 -16
  204. package/dist/presentation/queries-cli/overview.js.map +1 -1
  205. package/dist/presentation/queries-cli/path.js +26 -20
  206. package/dist/presentation/queries-cli/path.js.map +1 -1
  207. package/dist/presentation/result-formatter.d.ts +10 -0
  208. package/dist/presentation/result-formatter.d.ts.map +1 -1
  209. package/dist/presentation/result-formatter.js +16 -1
  210. package/dist/presentation/result-formatter.js.map +1 -1
  211. package/dist/presentation/viewer.d.ts.map +1 -1
  212. package/dist/presentation/viewer.js +18 -12
  213. package/dist/presentation/viewer.js.map +1 -1
  214. package/dist/shared/errors.d.ts +5 -0
  215. package/dist/shared/errors.d.ts.map +1 -1
  216. package/dist/shared/errors.js +5 -0
  217. package/dist/shared/errors.js.map +1 -1
  218. package/dist/shared/hierarchy.d.ts +8 -2
  219. package/dist/shared/hierarchy.d.ts.map +1 -1
  220. package/dist/shared/hierarchy.js +42 -1
  221. package/dist/shared/hierarchy.js.map +1 -1
  222. package/dist/shared/normalize.d.ts +6 -1
  223. package/dist/shared/normalize.d.ts.map +1 -1
  224. package/dist/shared/normalize.js +20 -12
  225. package/dist/shared/normalize.js.map +1 -1
  226. package/dist/shared/paginate.d.ts +0 -9
  227. package/dist/shared/paginate.d.ts.map +1 -1
  228. package/dist/shared/paginate.js +0 -15
  229. package/dist/shared/paginate.js.map +1 -1
  230. package/dist/types.d.ts +12 -5
  231. package/dist/types.d.ts.map +1 -1
  232. package/grammars/tree-sitter-erlang.wasm +0 -0
  233. package/grammars/tree-sitter-gleam.wasm +0 -0
  234. package/package.json +9 -9
  235. package/src/ast-analysis/engine.ts +176 -104
  236. package/src/ast-analysis/metrics.ts +33 -11
  237. package/src/ast-analysis/shared.ts +33 -24
  238. package/src/ast-analysis/visitor-utils.ts +52 -32
  239. package/src/ast-analysis/visitor.ts +132 -71
  240. package/src/ast-analysis/visitors/ast-store-visitor.ts +53 -50
  241. package/src/ast-analysis/visitors/complexity-visitor.ts +89 -40
  242. package/src/ast-analysis/visitors/dataflow-visitor.ts +87 -43
  243. package/src/cli/commands/branch-compare.ts +4 -0
  244. package/src/cli/commands/diff-impact.ts +2 -1
  245. package/src/cli/commands/info.ts +3 -2
  246. package/src/cli/commands/watch.ts +16 -2
  247. package/src/db/connection.ts +29 -28
  248. package/src/db/query-builder.ts +15 -3
  249. package/src/db/repository/base.ts +34 -0
  250. package/src/db/repository/native-repository.ts +104 -1
  251. package/src/db/repository/nodes.ts +13 -8
  252. package/src/db/repository/sqlite-repository.ts +55 -0
  253. package/src/domain/analysis/brief.ts +15 -25
  254. package/src/domain/analysis/context.ts +17 -10
  255. package/src/domain/analysis/dependencies.ts +77 -81
  256. package/src/domain/analysis/fn-impact.ts +36 -43
  257. package/src/domain/analysis/implementations.ts +11 -17
  258. package/src/domain/analysis/module-map.ts +58 -92
  259. package/src/domain/analysis/query-helpers.ts +18 -1
  260. package/src/domain/graph/builder/pipeline.ts +409 -99
  261. package/src/domain/graph/builder/stages/build-edges.ts +45 -19
  262. package/src/domain/graph/builder/stages/detect-changes.ts +2 -2
  263. package/src/domain/graph/builder/stages/finalize.ts +2 -2
  264. package/src/domain/graph/builder/stages/insert-nodes.ts +59 -34
  265. package/src/domain/graph/builder/stages/resolve-imports.ts +122 -100
  266. package/src/domain/graph/cycles.ts +110 -23
  267. package/src/domain/graph/resolve.ts +1 -1
  268. package/src/domain/graph/watcher.ts +202 -96
  269. package/src/domain/parser.ts +143 -89
  270. package/src/domain/search/generator.ts +1 -1
  271. package/src/domain/search/models.ts +26 -7
  272. package/src/domain/search/search/hybrid.ts +69 -51
  273. package/src/extractors/go.ts +43 -33
  274. package/src/extractors/helpers.ts +37 -23
  275. package/src/extractors/java.ts +66 -47
  276. package/src/extractors/javascript.ts +66 -54
  277. package/src/extractors/kotlin.ts +84 -77
  278. package/src/extractors/python.ts +31 -25
  279. package/src/extractors/rust.ts +37 -29
  280. package/src/extractors/solidity.ts +57 -61
  281. package/src/extractors/swift.ts +81 -80
  282. package/src/extractors/zig.ts +58 -61
  283. package/src/features/ast.ts +130 -110
  284. package/src/features/audit.ts +8 -6
  285. package/src/features/branch-compare.ts +105 -79
  286. package/src/features/communities.ts +25 -10
  287. package/src/features/complexity.ts +171 -134
  288. package/src/features/dataflow.ts +165 -175
  289. package/src/features/flow.ts +129 -92
  290. package/src/features/structure-query.ts +79 -64
  291. package/src/graph/algorithms/leiden/optimiser.ts +99 -55
  292. package/src/graph/algorithms/leiden/partition.ts +359 -294
  293. package/src/graph/model.ts +6 -1
  294. package/src/infrastructure/config.ts +6 -4
  295. package/src/infrastructure/suppress.ts +47 -0
  296. package/src/mcp/server.ts +53 -37
  297. package/src/presentation/dataflow.ts +50 -44
  298. package/src/presentation/diff-impact-mermaid.ts +104 -62
  299. package/src/presentation/queries-cli/exports.ts +21 -13
  300. package/src/presentation/queries-cli/impact.ts +15 -13
  301. package/src/presentation/queries-cli/inspect.ts +100 -81
  302. package/src/presentation/queries-cli/overview.ts +26 -16
  303. package/src/presentation/queries-cli/path.ts +33 -25
  304. package/src/presentation/result-formatter.ts +19 -1
  305. package/src/presentation/viewer.ts +42 -14
  306. package/src/shared/errors.ts +6 -0
  307. package/src/shared/hierarchy.ts +50 -2
  308. package/src/shared/normalize.ts +31 -12
  309. package/src/shared/paginate.ts +0 -17
  310. package/src/types.ts +26 -5
@@ -21,6 +21,7 @@ import { performance } from 'node:perf_hooks';
21
21
  import { bulkNodeIdsByFile } from '../db/index.js';
22
22
  import { debug } from '../infrastructure/logger.js';
23
23
  import { loadNative } from '../infrastructure/native.js';
24
+ import { toErrorMessage } from '../shared/errors.js';
24
25
  import type {
25
26
  AnalysisOpts,
26
27
  AnalysisTiming,
@@ -102,12 +103,83 @@ async function getParserModule(): Promise<typeof import('../domain/parser.js')>
102
103
 
103
104
  // ─── Native standalone analysis ─────────────────────────────────────────
104
105
 
106
+ interface NativeAnalysisNeeds {
107
+ complexity: boolean;
108
+ cfg: boolean;
109
+ dataflow: boolean;
110
+ }
111
+
105
112
  /**
106
113
  * Try native Rust analysis for files missing complexity/CFG/dataflow data.
107
114
  * Reads source from disk, calls the native standalone functions, and stores
108
- * results directly on definitions/symbols. Returns the set of files that
109
- * were fully handled (no remaining gaps except possibly AST store).
115
+ * results directly on definitions/symbols.
110
116
  */
117
+
118
+ /** Determine which native analyses a file still needs. */
119
+ function detectNativeNeeds(
120
+ symbols: ExtractorOutput,
121
+ ext: string,
122
+ langId: string,
123
+ opts: { doComplexity: boolean; doCfg: boolean; doDataflow: boolean },
124
+ ): NativeAnalysisNeeds {
125
+ const defs = symbols.definitions || [];
126
+ const langSupportsComplexity = COMPLEXITY_EXTENSIONS.has(ext) || COMPLEXITY_RULES.has(langId);
127
+ const langSupportsCfg = CFG_EXTENSIONS.has(ext) || CFG_RULES.has(langId);
128
+ const langSupportsDataflow = DATAFLOW_EXTENSIONS.has(ext) || DATAFLOW_RULES.has(langId);
129
+
130
+ return {
131
+ complexity:
132
+ opts.doComplexity &&
133
+ langSupportsComplexity &&
134
+ defs.some((d) => hasFuncBody(d) && !d.complexity),
135
+ cfg:
136
+ opts.doCfg &&
137
+ langSupportsCfg &&
138
+ defs.some((d) => hasFuncBody(d) && d.cfg !== null && !Array.isArray(d.cfg?.blocks)),
139
+ dataflow: opts.doDataflow && !symbols.dataflow && langSupportsDataflow,
140
+ };
141
+ }
142
+
143
+ /** Run native analysis passes for a single file. */
144
+ function runNativeFileAnalysis(
145
+ native: NativeAddon,
146
+ source: string,
147
+ absPath: string,
148
+ relPath: string,
149
+ langId: string,
150
+ symbols: ExtractorOutput,
151
+ needs: NativeAnalysisNeeds,
152
+ ): void {
153
+ const defs = symbols.definitions || [];
154
+
155
+ if (needs.complexity && native.analyzeComplexity) {
156
+ try {
157
+ const results = native.analyzeComplexity(source, absPath, langId);
158
+ storeNativeComplexityResults(results, defs);
159
+ } catch (err: unknown) {
160
+ debug(`native analyzeComplexity failed for ${relPath}: ${toErrorMessage(err)}`);
161
+ }
162
+ }
163
+
164
+ if (needs.cfg && native.buildCfgAnalysis) {
165
+ try {
166
+ const results = native.buildCfgAnalysis(source, absPath, langId);
167
+ storeNativeCfgResults(results, defs);
168
+ } catch (err: unknown) {
169
+ debug(`native buildCfgAnalysis failed for ${relPath}: ${toErrorMessage(err)}`);
170
+ }
171
+ }
172
+
173
+ if (needs.dataflow && native.extractDataflowAnalysis) {
174
+ try {
175
+ const result = native.extractDataflowAnalysis(source, absPath, langId);
176
+ if (result) symbols.dataflow = result;
177
+ } catch (err: unknown) {
178
+ debug(`native extractDataflowAnalysis failed for ${relPath}: ${toErrorMessage(err)}`);
179
+ }
180
+ }
181
+ }
182
+
111
183
  function runNativeAnalysis(
112
184
  native: NativeAddon,
113
185
  fileSymbols: Map<string, ExtractorOutput>,
@@ -115,68 +187,31 @@ function runNativeAnalysis(
115
187
  opts: AnalysisOpts,
116
188
  extToLang: Map<string, string>,
117
189
  ): void {
118
- const doComplexity = opts.complexity !== false;
119
- const doCfg = opts.cfg !== false;
120
- const doDataflow = opts.dataflow !== false;
190
+ const optsFlags = {
191
+ doComplexity: opts.complexity !== false,
192
+ doCfg: opts.cfg !== false,
193
+ doDataflow: opts.dataflow !== false,
194
+ };
121
195
 
122
196
  for (const [relPath, symbols] of fileSymbols) {
123
- if (symbols._tree) continue; // already has WASM tree, skip native
197
+ if (symbols._tree) continue;
124
198
  const ext = path.extname(relPath).toLowerCase();
125
199
  const langId = symbols._langId || extToLang.get(ext);
126
200
  if (!langId) continue;
127
201
 
128
- const defs = symbols.definitions || [];
129
-
130
- const needsComplexity =
131
- doComplexity &&
132
- COMPLEXITY_EXTENSIONS.has(ext) &&
133
- defs.some((d) => hasFuncBody(d) && !d.complexity);
134
- const needsCfg =
135
- doCfg &&
136
- CFG_EXTENSIONS.has(ext) &&
137
- defs.some((d) => hasFuncBody(d) && d.cfg !== null && !Array.isArray(d.cfg?.blocks));
138
- const needsDataflow = doDataflow && !symbols.dataflow && DATAFLOW_EXTENSIONS.has(ext);
202
+ const needs = detectNativeNeeds(symbols, ext, langId, optsFlags);
203
+ if (!needs.complexity && !needs.cfg && !needs.dataflow) continue;
139
204
 
140
- if (!needsComplexity && !needsCfg && !needsDataflow) continue;
141
-
142
- // Read source from disk
143
205
  const absPath = path.join(rootDir, relPath);
144
206
  let source: string;
145
207
  try {
146
208
  source = fs.readFileSync(absPath, 'utf-8');
147
- } catch {
209
+ } catch (e) {
210
+ debug(`runNativeAnalysis: failed to read ${relPath}: ${toErrorMessage(e)}`);
148
211
  continue;
149
212
  }
150
213
 
151
- // Complexity
152
- if (needsComplexity && native.analyzeComplexity) {
153
- try {
154
- const results = native.analyzeComplexity(source, absPath);
155
- storeNativeComplexityResults(results, defs);
156
- } catch (err: unknown) {
157
- debug(`native analyzeComplexity failed for ${relPath}: ${(err as Error).message}`);
158
- }
159
- }
160
-
161
- // CFG
162
- if (needsCfg && native.buildCfgAnalysis) {
163
- try {
164
- const results = native.buildCfgAnalysis(source, absPath);
165
- storeNativeCfgResults(results, defs);
166
- } catch (err: unknown) {
167
- debug(`native buildCfgAnalysis failed for ${relPath}: ${(err as Error).message}`);
168
- }
169
- }
170
-
171
- // Dataflow
172
- if (needsDataflow && native.extractDataflowAnalysis) {
173
- try {
174
- const result = native.extractDataflowAnalysis(source, absPath);
175
- if (result) symbols.dataflow = result;
176
- } catch (err: unknown) {
177
- debug(`native extractDataflowAnalysis failed for ${relPath}: ${(err as Error).message}`);
178
- }
179
- }
214
+ runNativeFileAnalysis(native, source, absPath, relPath, langId, symbols, needs);
180
215
  }
181
216
  }
182
217
 
@@ -207,6 +242,12 @@ function storeNativeComplexityResults(
207
242
  maxNesting: c.maxNesting,
208
243
  halstead: c.halstead
209
244
  ? {
245
+ n1: c.halstead.n1,
246
+ n2: c.halstead.n2,
247
+ bigN1: c.halstead.bigN1,
248
+ bigN2: c.halstead.bigN2,
249
+ vocabulary: c.halstead.vocabulary,
250
+ length: c.halstead.length,
210
251
  volume: c.halstead.volume,
211
252
  difficulty: c.halstead.difficulty,
212
253
  effort: c.halstead.effort,
@@ -222,6 +263,25 @@ function storeNativeComplexityResults(
222
263
  }
223
264
  }
224
265
 
266
+ /** Override a definition's cyclomatic complexity with a CFG-derived value and recompute MI. */
267
+ function overrideCyclomaticFromCfg(def: Definition, cfgCyclomatic: number): void {
268
+ if (!def.complexity) return;
269
+ if (cfgCyclomatic <= 0) {
270
+ debug(`overrideCyclomaticFromCfg: skipping ${def.name} — cfgCyclomatic=${cfgCyclomatic}`);
271
+ return;
272
+ }
273
+ def.complexity.cyclomatic = cfgCyclomatic;
274
+ const { loc, halstead } = def.complexity;
275
+ const volume = halstead ? halstead.volume : 0;
276
+ const commentRatio = loc && loc.loc > 0 ? loc.commentLines / loc.loc : 0;
277
+ def.complexity.maintainabilityIndex = computeMaintainabilityIndex(
278
+ volume,
279
+ cfgCyclomatic,
280
+ loc?.sloc ?? 0,
281
+ commentRatio,
282
+ );
283
+ }
284
+
225
285
  /** Store native CFG results on definitions, matched by line number. */
226
286
  function storeNativeCfgResults(results: NativeFunctionCfgResult[], defs: Definition[]): void {
227
287
  const byLine = new Map<number, NativeFunctionCfgResult[]>();
@@ -248,19 +308,43 @@ function storeNativeCfgResults(results: NativeFunctionCfgResult[], defs: Definit
248
308
 
249
309
  // Override complexity cyclomatic with CFG-derived value
250
310
  const { edges, blocks } = match.cfg;
251
- if (def.complexity && edges && blocks) {
252
- const cfgCyclomatic = edges.length - blocks.length + 2;
253
- if (cfgCyclomatic > 0) {
254
- def.complexity.cyclomatic = cfgCyclomatic;
255
- const { loc, halstead } = def.complexity;
256
- const volume = halstead ? halstead.volume : 0;
257
- const commentRatio = loc && loc.loc > 0 ? loc.commentLines / loc.loc : 0;
258
- def.complexity.maintainabilityIndex = computeMaintainabilityIndex(
259
- volume,
260
- cfgCyclomatic,
261
- loc?.sloc ?? 0,
262
- commentRatio,
263
- );
311
+ if (edges && blocks) {
312
+ overrideCyclomaticFromCfg(def, edges.length - blocks.length + 2);
313
+ }
314
+ }
315
+ }
316
+ }
317
+
318
+ // ─── CFG cyclomatic reconciliation ──────────────────────────────────────
319
+
320
+ /**
321
+ * Apply CFG-derived cyclomatic override for definitions that already have both
322
+ * `complexity` and `cfg` with blocks/edges but whose cyclomatic was never
323
+ * overridden (e.g., native extractors provide both fields inline, so the
324
+ * normal override path in storeNativeCfgResults / storeCfgResults is skipped).
325
+ */
326
+ /** Type guard for cfg objects with blocks and edges arrays. */
327
+ function hasCfgBlocksAndEdges(cfg: unknown): cfg is { blocks: unknown[]; edges: unknown[] } {
328
+ return (
329
+ cfg != null &&
330
+ typeof cfg === 'object' &&
331
+ Array.isArray((cfg as { blocks?: unknown }).blocks) &&
332
+ Array.isArray((cfg as { edges?: unknown }).edges)
333
+ );
334
+ }
335
+
336
+ function reconcileCfgCyclomatic(fileSymbols: Map<string, ExtractorOutput>): void {
337
+ for (const [, symbols] of fileSymbols) {
338
+ const defs = symbols.definitions || [];
339
+ for (const def of defs) {
340
+ if (
341
+ (def.kind === 'function' || def.kind === 'method') &&
342
+ def.complexity &&
343
+ hasCfgBlocksAndEdges(def.cfg)
344
+ ) {
345
+ const cfgCyclomatic = Math.max(def.cfg.edges.length - def.cfg.blocks.length + 2, 1);
346
+ if (cfgCyclomatic !== def.complexity.cyclomatic) {
347
+ overrideCyclomaticFromCfg(def, cfgCyclomatic);
264
348
  }
265
349
  }
266
350
  }
@@ -287,34 +371,22 @@ async function ensureWasmTreesIfNeeded(
287
371
  const ext = path.extname(relPath).toLowerCase();
288
372
  const defs = symbols.definitions || [];
289
373
 
290
- // Only consider definitions with a real function body.
291
- // Interface/type property signatures are extracted as methods but correctly
292
- // lack complexity/CFG data from the native engine. Exclude them by:
293
- // 1. Single-line span (endLine === line) — type property on one line
294
- // 2. Dotted names (e.g. "Interface.prop") — child definitions of types
295
- const hasFuncBody = (d: {
296
- name: string;
297
- kind: string;
298
- line: number;
299
- endLine?: number | null;
300
- }) =>
301
- (d.kind === 'function' || d.kind === 'method') &&
302
- d.line > 0 &&
303
- d.endLine != null &&
304
- d.endLine > d.line &&
305
- !d.name.includes('.');
306
-
307
374
  // AST: need tree when native didn't provide non-call astNodes
308
- const needsAst = doAst && !Array.isArray(symbols.astNodes) && WALK_EXTENSIONS.has(ext);
375
+ const lid = symbols._langId || '';
376
+ const needsAst =
377
+ doAst &&
378
+ !Array.isArray(symbols.astNodes) &&
379
+ (WALK_EXTENSIONS.has(ext) || AST_TYPE_MAPS.has(lid));
309
380
  const needsComplexity =
310
381
  doComplexity &&
311
- COMPLEXITY_EXTENSIONS.has(ext) &&
382
+ (COMPLEXITY_EXTENSIONS.has(ext) || COMPLEXITY_RULES.has(lid)) &&
312
383
  defs.some((d) => hasFuncBody(d) && !d.complexity);
313
384
  const needsCfg =
314
385
  doCfg &&
315
- CFG_EXTENSIONS.has(ext) &&
386
+ (CFG_EXTENSIONS.has(ext) || CFG_RULES.has(lid)) &&
316
387
  defs.some((d) => hasFuncBody(d) && d.cfg !== null && !Array.isArray(d.cfg?.blocks));
317
- const needsDataflow = doDataflow && !symbols.dataflow && DATAFLOW_EXTENSIONS.has(ext);
388
+ const needsDataflow =
389
+ doDataflow && !symbols.dataflow && (DATAFLOW_EXTENSIONS.has(ext) || DATAFLOW_RULES.has(lid));
318
390
 
319
391
  if (needsAst || needsComplexity || needsCfg || needsDataflow) {
320
392
  needsWasmTrees = true;
@@ -327,7 +399,7 @@ async function ensureWasmTreesIfNeeded(
327
399
  const { ensureWasmTrees } = await getParserModule();
328
400
  await ensureWasmTrees(fileSymbols, rootDir);
329
401
  } catch (err: unknown) {
330
- debug(`ensureWasmTrees failed: ${(err as Error).message}`);
402
+ debug(`ensureWasmTrees failed: ${toErrorMessage(err)}`);
331
403
  }
332
404
  }
333
405
  }
@@ -396,9 +468,9 @@ function setupComplexityVisitorForFile(
396
468
  }
397
469
 
398
470
  /** Set up CFG visitor if any definitions need WASM CFG analysis. */
399
- function setupCfgVisitorForFile(defs: Definition[], langId: string, ext: string): Visitor | null {
471
+ function setupCfgVisitorForFile(defs: Definition[], langId: string): Visitor | null {
400
472
  const cfgRulesForLang = CFG_RULES.get(langId);
401
- if (!cfgRulesForLang || !CFG_EXTENSIONS.has(ext)) return null;
473
+ if (!cfgRulesForLang) return null;
402
474
 
403
475
  const needsWasmCfg = defs.some(
404
476
  (d) => hasFuncBody(d) && d.cfg !== null && !Array.isArray(d.cfg?.blocks),
@@ -432,12 +504,12 @@ function setupVisitors(
432
504
  opts.complexity !== false ? setupComplexityVisitorForFile(defs, langId, walkerOpts) : null;
433
505
  if (complexityVisitor) visitors.push(complexityVisitor);
434
506
 
435
- const cfgVisitor = opts.cfg !== false ? setupCfgVisitorForFile(defs, langId, ext) : null;
507
+ const cfgVisitor = opts.cfg !== false ? setupCfgVisitorForFile(defs, langId) : null;
436
508
  if (cfgVisitor) visitors.push(cfgVisitor);
437
509
 
438
510
  let dataflowVisitor: Visitor | null = null;
439
511
  const dfRules = DATAFLOW_RULES.get(langId);
440
- if (opts.dataflow !== false && dfRules && DATAFLOW_EXTENSIONS.has(ext) && !symbols.dataflow) {
512
+ if (opts.dataflow !== false && dfRules && !symbols.dataflow) {
441
513
  dataflowVisitor = createDataflowVisitor(dfRules);
442
514
  visitors.push(dataflowVisitor);
443
515
  }
@@ -510,17 +582,8 @@ function storeCfgResults(results: WalkResults, defs: Definition[]): void {
510
582
  def.cfg = { blocks: cfgResult.blocks, edges: cfgResult.edges };
511
583
 
512
584
  // Override complexity's cyclomatic with CFG-derived value (single source of truth)
513
- if (def.complexity && cfgResult.cyclomatic != null) {
514
- def.complexity.cyclomatic = cfgResult.cyclomatic;
515
- const { loc, halstead } = def.complexity;
516
- const volume = halstead ? halstead.volume : 0;
517
- const commentRatio = loc && loc.loc > 0 ? loc.commentLines / loc.loc : 0;
518
- def.complexity.maintainabilityIndex = computeMaintainabilityIndex(
519
- volume,
520
- cfgResult.cyclomatic,
521
- loc?.sloc ?? 0,
522
- commentRatio,
523
- );
585
+ if (cfgResult.cyclomatic != null) {
586
+ overrideCyclomaticFromCfg(def, cfgResult.cyclomatic);
524
587
  }
525
588
  }
526
589
  }
@@ -542,7 +605,7 @@ async function delegateToBuildFunctions(
542
605
  const { buildAstNodes } = await import('../features/ast.js');
543
606
  await buildAstNodes(db, fileSymbols as Map<string, any>, rootDir, engineOpts);
544
607
  } catch (err: unknown) {
545
- debug(`buildAstNodes failed: ${(err as Error).message}`);
608
+ debug(`buildAstNodes failed: ${toErrorMessage(err)}`);
546
609
  }
547
610
  timing.astMs = performance.now() - t0;
548
611
  }
@@ -553,7 +616,7 @@ async function delegateToBuildFunctions(
553
616
  const { buildComplexityMetrics } = await import('../features/complexity.js');
554
617
  await buildComplexityMetrics(db, fileSymbols as Map<string, any>, rootDir, engineOpts);
555
618
  } catch (err: unknown) {
556
- debug(`buildComplexityMetrics failed: ${(err as Error).message}`);
619
+ debug(`buildComplexityMetrics failed: ${toErrorMessage(err)}`);
557
620
  }
558
621
  timing.complexityMs = performance.now() - t0;
559
622
  }
@@ -564,7 +627,7 @@ async function delegateToBuildFunctions(
564
627
  const { buildCFGData } = await import('../features/cfg.js');
565
628
  await buildCFGData(db, fileSymbols, rootDir, engineOpts);
566
629
  } catch (err: unknown) {
567
- debug(`buildCFGData failed: ${(err as Error).message}`);
630
+ debug(`buildCFGData failed: ${toErrorMessage(err)}`);
568
631
  }
569
632
  timing.cfgMs = performance.now() - t0;
570
633
  }
@@ -575,7 +638,7 @@ async function delegateToBuildFunctions(
575
638
  const { buildDataflowEdges } = await import('../features/dataflow.js');
576
639
  await buildDataflowEdges(db, fileSymbols, rootDir, engineOpts);
577
640
  } catch (err: unknown) {
578
- debug(`buildDataflowEdges failed: ${(err as Error).message}`);
641
+ debug(`buildDataflowEdges failed: ${toErrorMessage(err)}`);
579
642
  }
580
643
  timing.dataflowMs = performance.now() - t0;
581
644
  }
@@ -644,6 +707,15 @@ export async function runAnalyses(
644
707
 
645
708
  timing._unifiedWalkMs = performance.now() - t0walk;
646
709
 
710
+ // Reconcile: apply CFG-derived cyclomatic override for any definitions that have
711
+ // both precomputed complexity and CFG data but whose cyclomatic was never overridden.
712
+ // This closes a parity gap where native extractors provide both fields inline but
713
+ // the override step (storeNativeCfgResults / storeCfgResults) is skipped because
714
+ // detectNativeNeeds sees both as already present.
715
+ if (doComplexity && doCfg) {
716
+ reconcileCfgCyclomatic(fileSymbols);
717
+ }
718
+
647
719
  // Delegate to buildXxx functions for DB writes + native fallback
648
720
  await delegateToBuildFunctions(db, fileSymbols, rootDir, opts, engineOpts, timing);
649
721
 
@@ -9,6 +9,16 @@ import type { HalsteadDerivedMetrics, LOCMetrics, TreeSitterNode } from '../type
9
9
 
10
10
  // ─── Halstead Derived Metrics ─────────────────────────────────────────────
11
11
 
12
+ /** Halstead delivered-bugs denominator (industry standard: V / 3000). */
13
+ const HALSTEAD_BUGS_DIVISOR = 3000;
14
+
15
+ /** Sum all values in a count map. */
16
+ function sumCounts(map: Map<string, number>): number {
17
+ let total = 0;
18
+ for (const c of map.values()) total += c;
19
+ return total;
20
+ }
21
+
12
22
  /**
13
23
  * Compute Halstead derived metrics from raw operator/operand counts.
14
24
  *
@@ -22,17 +32,15 @@ export function computeHalsteadDerived(
22
32
  ): HalsteadDerivedMetrics {
23
33
  const n1 = operators.size;
24
34
  const n2 = operands.size;
25
- let bigN1 = 0;
26
- for (const c of operators.values()) bigN1 += c;
27
- let bigN2 = 0;
28
- for (const c of operands.values()) bigN2 += c;
35
+ const bigN1 = sumCounts(operators);
36
+ const bigN2 = sumCounts(operands);
29
37
 
30
38
  const vocabulary = n1 + n2;
31
39
  const length = bigN1 + bigN2;
32
40
  const volume = vocabulary > 0 ? length * Math.log2(vocabulary) : 0;
33
41
  const difficulty = n2 > 0 ? (n1 / 2) * (bigN2 / n2) : 0;
34
42
  const effort = difficulty * volume;
35
- const bugs = volume / 3000;
43
+ const bugs = volume / HALSTEAD_BUGS_DIVISOR;
36
44
 
37
45
  return {
38
46
  n1,
@@ -97,10 +105,20 @@ export function computeLOCMetrics(functionNode: TreeSitterNode, language?: strin
97
105
  // ─── Maintainability Index ────────────────────────────────────────────────
98
106
 
99
107
  /**
100
- * Compute normalized Maintainability Index (0-100 scale).
101
- *
102
- * Original SEI formula: MI = 171 - 5.2*ln(V) - 0.23*G - 16.2*ln(LOC) + 50*sin(sqrt(2.4*CM))
108
+ * SEI Maintainability Index formula coefficients.
109
+ * Original: MI = 171 - 5.2*ln(V) - 0.23*G - 16.2*ln(LOC) + 50*sin(sqrt(2.4*CM))
103
110
  * Microsoft normalization: max(0, min(100, MI * 100/171))
111
+ */
112
+ const MI_BASE = 171;
113
+ const MI_VOLUME_COEFF = 5.2;
114
+ const MI_CYCLOMATIC_COEFF = 0.23;
115
+ const MI_LOC_COEFF = 16.2;
116
+ const MI_COMMENT_AMPLITUDE = 50;
117
+ const MI_COMMENT_SCALE = 2.4;
118
+ const MI_NORMALIZE_SCALE = 100;
119
+
120
+ /**
121
+ * Compute normalized Maintainability Index (0-100 scale).
104
122
  *
105
123
  * @param {number} volume - Halstead volume
106
124
  * @param {number} cyclomatic - Cyclomatic complexity
@@ -117,12 +135,16 @@ export function computeMaintainabilityIndex(
117
135
  const safeVolume = Math.max(volume, 1);
118
136
  const safeSLOC = Math.max(sloc, 1);
119
137
 
120
- let mi = 171 - 5.2 * Math.log(safeVolume) - 0.23 * cyclomatic - 16.2 * Math.log(safeSLOC);
138
+ let mi =
139
+ MI_BASE -
140
+ MI_VOLUME_COEFF * Math.log(safeVolume) -
141
+ MI_CYCLOMATIC_COEFF * cyclomatic -
142
+ MI_LOC_COEFF * Math.log(safeSLOC);
121
143
 
122
144
  if (commentRatio != null && commentRatio > 0) {
123
- mi += 50 * Math.sin(Math.sqrt(2.4 * commentRatio));
145
+ mi += MI_COMMENT_AMPLITUDE * Math.sin(Math.sqrt(MI_COMMENT_SCALE * commentRatio));
124
146
  }
125
147
 
126
- const normalized = Math.max(0, Math.min(100, (mi * 100) / 171));
148
+ const normalized = Math.max(0, Math.min(MI_NORMALIZE_SCALE, (mi * MI_NORMALIZE_SCALE) / MI_BASE));
127
149
  return +normalized.toFixed(1);
128
150
  }
@@ -150,6 +150,38 @@ export function makeDataflowRules(overrides: Partial<DataflowRulesConfig>): Data
150
150
 
151
151
  // ─── AST Helpers ──────────────────────────────────────────────────────────
152
152
 
153
+ /** Compute the span (row count) of a tree-sitter node. */
154
+ function nodeSpan(node: TreeSitterNode): number {
155
+ return node.endPosition.row - node.startPosition.row;
156
+ }
157
+
158
+ /**
159
+ * Recursively search for the narrowest function node at the target line.
160
+ */
161
+ function searchFunctionNode(
162
+ node: TreeSitterNode,
163
+ targetStart: number,
164
+ functionNodeTypes: Set<string>,
165
+ best: TreeSitterNode | null,
166
+ ): TreeSitterNode | null {
167
+ const nodeStart = node.startPosition.row;
168
+ const nodeEnd = node.endPosition.row;
169
+
170
+ // Prune branches outside range
171
+ if (nodeEnd < targetStart || nodeStart > targetStart + 1) return best;
172
+
173
+ if (functionNodeTypes.has(node.type) && nodeStart === targetStart) {
174
+ if (!best || nodeSpan(node) < nodeSpan(best)) {
175
+ best = node;
176
+ }
177
+ }
178
+
179
+ for (let i = 0; i < node.childCount; i++) {
180
+ best = searchFunctionNode(node.child(i)!, targetStart, functionNodeTypes, best);
181
+ }
182
+ return best;
183
+ }
184
+
153
185
  export function findFunctionNode(
154
186
  rootNode: TreeSitterNode,
155
187
  startLine: number,
@@ -158,30 +190,7 @@ export function findFunctionNode(
158
190
  ): TreeSitterNode | null {
159
191
  // tree-sitter lines are 0-indexed
160
192
  const targetStart = startLine - 1;
161
-
162
- let best: TreeSitterNode | null = null;
163
-
164
- function search(node: TreeSitterNode): void {
165
- const nodeStart = node.startPosition.row;
166
- const nodeEnd = node.endPosition.row;
167
-
168
- // Prune branches outside range
169
- if (nodeEnd < targetStart || nodeStart > targetStart + 1) return;
170
-
171
- if (rules.functionNodes.has(node.type) && nodeStart === targetStart) {
172
- // Found a function node at the right position — pick it
173
- if (!best || nodeEnd - nodeStart < best.endPosition.row - best.startPosition.row) {
174
- best = node;
175
- }
176
- }
177
-
178
- for (let i = 0; i < node.childCount; i++) {
179
- search(node.child(i)!);
180
- }
181
- }
182
-
183
- search(rootNode);
184
- return best;
193
+ return searchFunctionNode(rootNode, targetStart, rules.functionNodes, null);
185
194
  }
186
195
 
187
196
  // ─── Extension / Language Mapping ─────────────────────────────────────────
@@ -88,6 +88,41 @@ export function extractParams(
88
88
  return result;
89
89
  }
90
90
 
91
+ /** Extract names from a rest parameter (e.g. `...args`). */
92
+ function extractRestParamNames(node: TreeSitterNode, rules: LanguageRules): string[] {
93
+ const nameNode = node.childForFieldName('name');
94
+ if (nameNode) return [nameNode.text];
95
+ for (const child of node.namedChildren) {
96
+ if (child.type === rules.paramIdentifier) return [child.text];
97
+ }
98
+ return [];
99
+ }
100
+
101
+ /** Extract names from an object destructuring pattern (e.g. `{ a, b: c }`). */
102
+ function extractObjectDestructNames(node: TreeSitterNode, rules: LanguageRules): string[] {
103
+ const names: string[] = [];
104
+ for (const child of node.namedChildren) {
105
+ if (rules.shorthandPropPattern && child.type === rules.shorthandPropPattern) {
106
+ names.push(child.text);
107
+ } else if (rules.pairPatternType && child.type === rules.pairPatternType) {
108
+ const value = child.childForFieldName('value');
109
+ if (value) names.push(...extractParamNames(value, rules));
110
+ } else if (rules.restParamType && child.type === rules.restParamType) {
111
+ names.push(...extractParamNames(child, rules));
112
+ }
113
+ }
114
+ return names;
115
+ }
116
+
117
+ /** Extract names from an array destructuring pattern (e.g. `[a, b]`). */
118
+ function extractArrayDestructNames(node: TreeSitterNode, rules: LanguageRules): string[] {
119
+ const names: string[] = [];
120
+ for (const child of node.namedChildren) {
121
+ names.push(...extractParamNames(child, rules));
122
+ }
123
+ return names;
124
+ }
125
+
91
126
  /**
92
127
  * Extract parameter names from a single parameter node.
93
128
  */
@@ -113,35 +148,15 @@ export function extractParamNames(node: TreeSitterNode | null, rules: LanguageRu
113
148
  }
114
149
 
115
150
  if (rules.restParamType && t === rules.restParamType) {
116
- const nameNode = node.childForFieldName('name');
117
- if (nameNode) return [nameNode.text];
118
- for (const child of node.namedChildren) {
119
- if (child.type === rules.paramIdentifier) return [child.text];
120
- }
121
- return [];
151
+ return extractRestParamNames(node, rules);
122
152
  }
123
153
 
124
154
  if (rules.objectDestructType && t === rules.objectDestructType) {
125
- const names: string[] = [];
126
- for (const child of node.namedChildren) {
127
- if (rules.shorthandPropPattern && child.type === rules.shorthandPropPattern) {
128
- names.push(child.text);
129
- } else if (rules.pairPatternType && child.type === rules.pairPatternType) {
130
- const value = child.childForFieldName('value');
131
- if (value) names.push(...extractParamNames(value, rules));
132
- } else if (rules.restParamType && child.type === rules.restParamType) {
133
- names.push(...extractParamNames(child, rules));
134
- }
135
- }
136
- return names;
155
+ return extractObjectDestructNames(node, rules);
137
156
  }
138
157
 
139
158
  if (rules.arrayDestructType && t === rules.arrayDestructType) {
140
- const names: string[] = [];
141
- for (const child of node.namedChildren) {
142
- names.push(...extractParamNames(child, rules));
143
- }
144
- return names;
159
+ return extractArrayDestructNames(node, rules);
145
160
  }
146
161
 
147
162
  return [];
@@ -155,6 +170,19 @@ export function isIdent(nodeType: string, rules: LanguageRules): boolean {
155
170
  return rules.extraIdentifierTypes ? rules.extraIdentifierTypes.has(nodeType) : false;
156
171
  }
157
172
 
173
+ /** Resolve callee name from an optional chain node (e.g. `obj?.method()`). */
174
+ function resolveOptionalChainCallee(fn: TreeSitterNode, rules: LanguageRules): string | null {
175
+ const target = fn.namedChildren[0];
176
+ if (!target) return null;
177
+ if (target.type === rules.memberNode) {
178
+ const prop = target.childForFieldName(rules.memberPropertyField);
179
+ return prop ? prop.text : null;
180
+ }
181
+ if (target.type === 'identifier') return target.text;
182
+ const prop = fn.childForFieldName(rules.memberPropertyField);
183
+ return prop ? prop.text : null;
184
+ }
185
+
158
186
  /**
159
187
  * Resolve the name a call expression is calling using rules.
160
188
  */
@@ -170,15 +198,7 @@ export function resolveCalleeName(callNode: TreeSitterNode, rules: LanguageRules
170
198
  return prop ? prop.text : null;
171
199
  }
172
200
  if (rules.optionalChainNode && fn.type === rules.optionalChainNode) {
173
- const target = fn.namedChildren[0];
174
- if (!target) return null;
175
- if (target.type === rules.memberNode) {
176
- const prop = target.childForFieldName(rules.memberPropertyField);
177
- return prop ? prop.text : null;
178
- }
179
- if (target.type === 'identifier') return target.text;
180
- const prop = fn.childForFieldName(rules.memberPropertyField);
181
- return prop ? prop.text : null;
201
+ return resolveOptionalChainCallee(fn, rules);
182
202
  }
183
203
  return null;
184
204
  }