@optave/codegraph 3.5.0 → 3.7.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 (346) hide show
  1. package/README.md +47 -21
  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/visitors/ast-store-visitor.d.ts.map +1 -1
  6. package/dist/ast-analysis/visitors/ast-store-visitor.js +14 -1
  7. package/dist/ast-analysis/visitors/ast-store-visitor.js.map +1 -1
  8. package/dist/ast-analysis/visitors/complexity-visitor.d.ts.map +1 -1
  9. package/dist/ast-analysis/visitors/complexity-visitor.js +11 -13
  10. package/dist/ast-analysis/visitors/complexity-visitor.js.map +1 -1
  11. package/dist/db/connection.d.ts +12 -2
  12. package/dist/db/connection.d.ts.map +1 -1
  13. package/dist/db/connection.js +81 -53
  14. package/dist/db/connection.js.map +1 -1
  15. package/dist/db/index.d.ts +1 -1
  16. package/dist/db/index.d.ts.map +1 -1
  17. package/dist/db/index.js +1 -1
  18. package/dist/db/index.js.map +1 -1
  19. package/dist/db/migrations.d.ts.map +1 -1
  20. package/dist/db/migrations.js +38 -32
  21. package/dist/db/migrations.js.map +1 -1
  22. package/dist/domain/analysis/context.d.ts.map +1 -1
  23. package/dist/domain/analysis/context.js +51 -66
  24. package/dist/domain/analysis/context.js.map +1 -1
  25. package/dist/domain/analysis/dependencies.d.ts.map +1 -1
  26. package/dist/domain/analysis/dependencies.js +62 -70
  27. package/dist/domain/analysis/dependencies.js.map +1 -1
  28. package/dist/domain/analysis/diff-impact.d.ts +9 -7
  29. package/dist/domain/analysis/diff-impact.d.ts.map +1 -1
  30. package/dist/domain/analysis/exports.d.ts.map +1 -1
  31. package/dist/domain/analysis/exports.js +29 -33
  32. package/dist/domain/analysis/exports.js.map +1 -1
  33. package/dist/domain/analysis/fn-impact.d.ts +15 -17
  34. package/dist/domain/analysis/fn-impact.d.ts.map +1 -1
  35. package/dist/domain/analysis/fn-impact.js +35 -65
  36. package/dist/domain/analysis/fn-impact.js.map +1 -1
  37. package/dist/domain/analysis/module-map.d.ts.map +1 -1
  38. package/dist/domain/analysis/module-map.js +91 -6
  39. package/dist/domain/analysis/module-map.js.map +1 -1
  40. package/dist/domain/analysis/query-helpers.d.ts +20 -0
  41. package/dist/domain/analysis/query-helpers.d.ts.map +1 -0
  42. package/dist/domain/analysis/query-helpers.js +27 -0
  43. package/dist/domain/analysis/query-helpers.js.map +1 -0
  44. package/dist/domain/graph/builder/helpers.d.ts.map +1 -1
  45. package/dist/domain/graph/builder/helpers.js +15 -9
  46. package/dist/domain/graph/builder/helpers.js.map +1 -1
  47. package/dist/domain/graph/builder/incremental.d.ts.map +1 -1
  48. package/dist/domain/graph/builder/incremental.js +3 -2
  49. package/dist/domain/graph/builder/incremental.js.map +1 -1
  50. package/dist/domain/graph/builder/pipeline.d.ts.map +1 -1
  51. package/dist/domain/graph/builder/pipeline.js +69 -3
  52. package/dist/domain/graph/builder/pipeline.js.map +1 -1
  53. package/dist/domain/graph/builder/stages/build-edges.d.ts.map +1 -1
  54. package/dist/domain/graph/builder/stages/build-edges.js +7 -51
  55. package/dist/domain/graph/builder/stages/build-edges.js.map +1 -1
  56. package/dist/domain/graph/builder/stages/build-structure.d.ts.map +1 -1
  57. package/dist/domain/graph/builder/stages/build-structure.js +7 -5
  58. package/dist/domain/graph/builder/stages/build-structure.js.map +1 -1
  59. package/dist/domain/graph/builder/stages/collect-files.js +2 -2
  60. package/dist/domain/graph/builder/stages/collect-files.js.map +1 -1
  61. package/dist/domain/graph/builder/stages/detect-changes.d.ts.map +1 -1
  62. package/dist/domain/graph/builder/stages/detect-changes.js +2 -2
  63. package/dist/domain/graph/builder/stages/detect-changes.js.map +1 -1
  64. package/dist/domain/graph/builder/stages/finalize.d.ts.map +1 -1
  65. package/dist/domain/graph/builder/stages/finalize.js +124 -105
  66. package/dist/domain/graph/builder/stages/finalize.js.map +1 -1
  67. package/dist/domain/graph/builder/stages/insert-nodes.d.ts.map +1 -1
  68. package/dist/domain/graph/builder/stages/insert-nodes.js +28 -15
  69. package/dist/domain/graph/builder/stages/insert-nodes.js.map +1 -1
  70. package/dist/domain/graph/builder/stages/resolve-imports.d.ts.map +1 -1
  71. package/dist/domain/graph/builder/stages/resolve-imports.js +3 -2
  72. package/dist/domain/graph/builder/stages/resolve-imports.js.map +1 -1
  73. package/dist/domain/graph/resolve.d.ts +0 -4
  74. package/dist/domain/graph/resolve.d.ts.map +1 -1
  75. package/dist/domain/graph/resolve.js +32 -48
  76. package/dist/domain/graph/resolve.js.map +1 -1
  77. package/dist/domain/graph/watcher.d.ts.map +1 -1
  78. package/dist/domain/graph/watcher.js +12 -12
  79. package/dist/domain/graph/watcher.js.map +1 -1
  80. package/dist/domain/parser.d.ts +1 -1
  81. package/dist/domain/parser.d.ts.map +1 -1
  82. package/dist/domain/parser.js +206 -101
  83. package/dist/domain/parser.js.map +1 -1
  84. package/dist/domain/search/search/cli-formatter.d.ts.map +1 -1
  85. package/dist/domain/search/search/cli-formatter.js +88 -83
  86. package/dist/domain/search/search/cli-formatter.js.map +1 -1
  87. package/dist/extractors/bash.d.ts +6 -0
  88. package/dist/extractors/bash.d.ts.map +1 -0
  89. package/dist/extractors/bash.js +91 -0
  90. package/dist/extractors/bash.js.map +1 -0
  91. package/dist/extractors/c.d.ts +6 -0
  92. package/dist/extractors/c.d.ts.map +1 -0
  93. package/dist/extractors/c.js +204 -0
  94. package/dist/extractors/c.js.map +1 -0
  95. package/dist/extractors/cpp.d.ts +6 -0
  96. package/dist/extractors/cpp.d.ts.map +1 -0
  97. package/dist/extractors/cpp.js +283 -0
  98. package/dist/extractors/cpp.js.map +1 -0
  99. package/dist/extractors/csharp.d.ts.map +1 -1
  100. package/dist/extractors/csharp.js +42 -54
  101. package/dist/extractors/csharp.js.map +1 -1
  102. package/dist/extractors/dart.d.ts +6 -0
  103. package/dist/extractors/dart.d.ts.map +1 -0
  104. package/dist/extractors/dart.js +277 -0
  105. package/dist/extractors/dart.js.map +1 -0
  106. package/dist/extractors/elixir.d.ts +9 -0
  107. package/dist/extractors/elixir.d.ts.map +1 -0
  108. package/dist/extractors/elixir.js +223 -0
  109. package/dist/extractors/elixir.js.map +1 -0
  110. package/dist/extractors/go.d.ts.map +1 -1
  111. package/dist/extractors/go.js +126 -130
  112. package/dist/extractors/go.js.map +1 -1
  113. package/dist/extractors/haskell.d.ts +8 -0
  114. package/dist/extractors/haskell.d.ts.map +1 -0
  115. package/dist/extractors/haskell.js +217 -0
  116. package/dist/extractors/haskell.js.map +1 -0
  117. package/dist/extractors/hcl.js +6 -6
  118. package/dist/extractors/hcl.js.map +1 -1
  119. package/dist/extractors/helpers.d.ts +32 -1
  120. package/dist/extractors/helpers.d.ts.map +1 -1
  121. package/dist/extractors/helpers.js +74 -0
  122. package/dist/extractors/helpers.js.map +1 -1
  123. package/dist/extractors/index.d.ts +12 -0
  124. package/dist/extractors/index.d.ts.map +1 -1
  125. package/dist/extractors/index.js +12 -0
  126. package/dist/extractors/index.js.map +1 -1
  127. package/dist/extractors/java.d.ts.map +1 -1
  128. package/dist/extractors/java.js +32 -47
  129. package/dist/extractors/java.js.map +1 -1
  130. package/dist/extractors/javascript.d.ts.map +1 -1
  131. package/dist/extractors/javascript.js +306 -292
  132. package/dist/extractors/javascript.js.map +1 -1
  133. package/dist/extractors/kotlin.d.ts +6 -0
  134. package/dist/extractors/kotlin.d.ts.map +1 -0
  135. package/dist/extractors/kotlin.js +275 -0
  136. package/dist/extractors/kotlin.js.map +1 -0
  137. package/dist/extractors/lua.d.ts +6 -0
  138. package/dist/extractors/lua.d.ts.map +1 -0
  139. package/dist/extractors/lua.js +162 -0
  140. package/dist/extractors/lua.js.map +1 -0
  141. package/dist/extractors/ocaml.d.ts +6 -0
  142. package/dist/extractors/ocaml.d.ts.map +1 -0
  143. package/dist/extractors/ocaml.js +236 -0
  144. package/dist/extractors/ocaml.js.map +1 -0
  145. package/dist/extractors/php.d.ts.map +1 -1
  146. package/dist/extractors/php.js +39 -44
  147. package/dist/extractors/php.js.map +1 -1
  148. package/dist/extractors/python.d.ts.map +1 -1
  149. package/dist/extractors/python.js +75 -93
  150. package/dist/extractors/python.js.map +1 -1
  151. package/dist/extractors/ruby.js +6 -13
  152. package/dist/extractors/ruby.js.map +1 -1
  153. package/dist/extractors/rust.d.ts.map +1 -1
  154. package/dist/extractors/rust.js +58 -83
  155. package/dist/extractors/rust.js.map +1 -1
  156. package/dist/extractors/scala.d.ts +6 -0
  157. package/dist/extractors/scala.d.ts.map +1 -0
  158. package/dist/extractors/scala.js +269 -0
  159. package/dist/extractors/scala.js.map +1 -0
  160. package/dist/extractors/swift.d.ts +6 -0
  161. package/dist/extractors/swift.d.ts.map +1 -0
  162. package/dist/extractors/swift.js +275 -0
  163. package/dist/extractors/swift.js.map +1 -0
  164. package/dist/extractors/zig.d.ts +9 -0
  165. package/dist/extractors/zig.d.ts.map +1 -0
  166. package/dist/extractors/zig.js +276 -0
  167. package/dist/extractors/zig.js.map +1 -0
  168. package/dist/features/ast.d.ts +2 -0
  169. package/dist/features/ast.d.ts.map +1 -1
  170. package/dist/features/ast.js +9 -24
  171. package/dist/features/ast.js.map +1 -1
  172. package/dist/features/audit.d.ts.map +1 -1
  173. package/dist/features/audit.js +17 -21
  174. package/dist/features/audit.js.map +1 -1
  175. package/dist/features/branch-compare.d.ts.map +1 -1
  176. package/dist/features/branch-compare.js +47 -3
  177. package/dist/features/branch-compare.js.map +1 -1
  178. package/dist/features/cfg.d.ts +7 -1
  179. package/dist/features/cfg.d.ts.map +1 -1
  180. package/dist/features/cfg.js +72 -61
  181. package/dist/features/cfg.js.map +1 -1
  182. package/dist/features/check.d.ts.map +1 -1
  183. package/dist/features/check.js +79 -62
  184. package/dist/features/check.js.map +1 -1
  185. package/dist/features/complexity-query.d.ts.map +1 -1
  186. package/dist/features/complexity-query.js +142 -137
  187. package/dist/features/complexity-query.js.map +1 -1
  188. package/dist/features/complexity.d.ts +7 -1
  189. package/dist/features/complexity.d.ts.map +1 -1
  190. package/dist/features/complexity.js +62 -1
  191. package/dist/features/complexity.js.map +1 -1
  192. package/dist/features/dataflow.d.ts +7 -1
  193. package/dist/features/dataflow.d.ts.map +1 -1
  194. package/dist/features/dataflow.js +356 -188
  195. package/dist/features/dataflow.js.map +1 -1
  196. package/dist/features/graph-enrichment.d.ts.map +1 -1
  197. package/dist/features/graph-enrichment.js +117 -104
  198. package/dist/features/graph-enrichment.js.map +1 -1
  199. package/dist/features/sequence.d.ts.map +1 -1
  200. package/dist/features/sequence.js +25 -4
  201. package/dist/features/sequence.js.map +1 -1
  202. package/dist/features/structure-query.d.ts.map +1 -1
  203. package/dist/features/structure-query.js +29 -4
  204. package/dist/features/structure-query.js.map +1 -1
  205. package/dist/features/structure.d.ts.map +1 -1
  206. package/dist/features/structure.js +35 -15
  207. package/dist/features/structure.js.map +1 -1
  208. package/dist/graph/algorithms/leiden/adapter.d.ts.map +1 -1
  209. package/dist/graph/algorithms/leiden/adapter.js +88 -73
  210. package/dist/graph/algorithms/leiden/adapter.js.map +1 -1
  211. package/dist/graph/algorithms/leiden/index.js +43 -28
  212. package/dist/graph/algorithms/leiden/index.js.map +1 -1
  213. package/dist/graph/algorithms/leiden/optimiser.d.ts.map +1 -1
  214. package/dist/graph/algorithms/leiden/optimiser.js +90 -104
  215. package/dist/graph/algorithms/leiden/optimiser.js.map +1 -1
  216. package/dist/graph/algorithms/leiden/partition.d.ts.map +1 -1
  217. package/dist/graph/algorithms/leiden/partition.js +89 -106
  218. package/dist/graph/algorithms/leiden/partition.js.map +1 -1
  219. package/dist/graph/model.d.ts +2 -0
  220. package/dist/graph/model.d.ts.map +1 -1
  221. package/dist/graph/model.js +20 -8
  222. package/dist/graph/model.js.map +1 -1
  223. package/dist/infrastructure/config.d.ts +0 -8
  224. package/dist/infrastructure/config.d.ts.map +1 -1
  225. package/dist/infrastructure/config.js +73 -62
  226. package/dist/infrastructure/config.js.map +1 -1
  227. package/dist/infrastructure/registry.d.ts +0 -8
  228. package/dist/infrastructure/registry.d.ts.map +1 -1
  229. package/dist/infrastructure/registry.js +12 -14
  230. package/dist/infrastructure/registry.js.map +1 -1
  231. package/dist/mcp/server.d.ts.map +1 -1
  232. package/dist/mcp/server.js +45 -36
  233. package/dist/mcp/server.js.map +1 -1
  234. package/dist/presentation/audit.d.ts.map +1 -1
  235. package/dist/presentation/audit.js +61 -57
  236. package/dist/presentation/audit.js.map +1 -1
  237. package/dist/presentation/branch-compare.d.ts.map +1 -1
  238. package/dist/presentation/branch-compare.js +56 -38
  239. package/dist/presentation/branch-compare.js.map +1 -1
  240. package/dist/presentation/check.d.ts.map +1 -1
  241. package/dist/presentation/check.js +30 -32
  242. package/dist/presentation/check.js.map +1 -1
  243. package/dist/presentation/colors.d.ts.map +1 -1
  244. package/dist/presentation/colors.js +2 -0
  245. package/dist/presentation/colors.js.map +1 -1
  246. package/dist/presentation/complexity.d.ts.map +1 -1
  247. package/dist/presentation/complexity.js +25 -19
  248. package/dist/presentation/complexity.js.map +1 -1
  249. package/dist/presentation/queries-cli/exports.d.ts.map +1 -1
  250. package/dist/presentation/queries-cli/exports.js +15 -15
  251. package/dist/presentation/queries-cli/exports.js.map +1 -1
  252. package/dist/presentation/queries-cli/impact.d.ts.map +1 -1
  253. package/dist/presentation/queries-cli/impact.js +29 -19
  254. package/dist/presentation/queries-cli/impact.js.map +1 -1
  255. package/dist/types.d.ts +182 -7
  256. package/dist/types.d.ts.map +1 -1
  257. package/grammars/tree-sitter-bash.wasm +0 -0
  258. package/grammars/tree-sitter-c.wasm +0 -0
  259. package/grammars/tree-sitter-cpp.wasm +0 -0
  260. package/grammars/tree-sitter-dart.wasm +0 -0
  261. package/grammars/tree-sitter-elixir.wasm +0 -0
  262. package/grammars/tree-sitter-haskell.wasm +0 -0
  263. package/grammars/tree-sitter-kotlin.wasm +0 -0
  264. package/grammars/tree-sitter-lua.wasm +0 -0
  265. package/grammars/tree-sitter-ocaml.wasm +0 -0
  266. package/grammars/tree-sitter-scala.wasm +0 -0
  267. package/grammars/tree-sitter-swift.wasm +0 -0
  268. package/grammars/tree-sitter-zig.wasm +0 -0
  269. package/package.json +19 -7
  270. package/src/ast-analysis/engine.ts +147 -138
  271. package/src/ast-analysis/visitors/ast-store-visitor.ts +15 -2
  272. package/src/ast-analysis/visitors/complexity-visitor.ts +11 -11
  273. package/src/db/connection.ts +90 -59
  274. package/src/db/index.ts +1 -0
  275. package/src/db/migrations.ts +36 -32
  276. package/src/domain/analysis/context.ts +73 -75
  277. package/src/domain/analysis/dependencies.ts +78 -68
  278. package/src/domain/analysis/exports.ts +45 -34
  279. package/src/domain/analysis/fn-impact.ts +67 -64
  280. package/src/domain/analysis/module-map.ts +103 -8
  281. package/src/domain/analysis/query-helpers.ts +35 -0
  282. package/src/domain/graph/builder/helpers.ts +12 -6
  283. package/src/domain/graph/builder/incremental.ts +3 -2
  284. package/src/domain/graph/builder/pipeline.ts +71 -3
  285. package/src/domain/graph/builder/stages/build-edges.ts +10 -75
  286. package/src/domain/graph/builder/stages/build-structure.ts +9 -7
  287. package/src/domain/graph/builder/stages/collect-files.ts +2 -2
  288. package/src/domain/graph/builder/stages/detect-changes.ts +7 -2
  289. package/src/domain/graph/builder/stages/finalize.ts +159 -125
  290. package/src/domain/graph/builder/stages/insert-nodes.ts +32 -21
  291. package/src/domain/graph/builder/stages/resolve-imports.ts +3 -2
  292. package/src/domain/graph/resolve.ts +34 -46
  293. package/src/domain/graph/watcher.ts +12 -14
  294. package/src/domain/parser.ts +222 -97
  295. package/src/domain/search/search/cli-formatter.ts +121 -94
  296. package/src/extractors/bash.ts +97 -0
  297. package/src/extractors/c.ts +212 -0
  298. package/src/extractors/cpp.ts +298 -0
  299. package/src/extractors/csharp.ts +53 -56
  300. package/src/extractors/dart.ts +304 -0
  301. package/src/extractors/elixir.ts +251 -0
  302. package/src/extractors/go.ts +152 -134
  303. package/src/extractors/haskell.ts +235 -0
  304. package/src/extractors/hcl.ts +6 -6
  305. package/src/extractors/helpers.ts +93 -1
  306. package/src/extractors/index.ts +12 -0
  307. package/src/extractors/java.ts +43 -48
  308. package/src/extractors/javascript.ts +328 -281
  309. package/src/extractors/kotlin.ts +293 -0
  310. package/src/extractors/lua.ts +169 -0
  311. package/src/extractors/ocaml.ts +259 -0
  312. package/src/extractors/php.ts +46 -40
  313. package/src/extractors/python.ts +81 -104
  314. package/src/extractors/ruby.ts +6 -13
  315. package/src/extractors/rust.ts +65 -85
  316. package/src/extractors/scala.ts +285 -0
  317. package/src/extractors/swift.ts +293 -0
  318. package/src/extractors/zig.ts +294 -0
  319. package/src/features/ast.ts +10 -25
  320. package/src/features/audit.ts +24 -20
  321. package/src/features/branch-compare.ts +51 -4
  322. package/src/features/cfg.ts +113 -65
  323. package/src/features/check.ts +90 -74
  324. package/src/features/complexity-query.ts +181 -163
  325. package/src/features/complexity.ts +64 -1
  326. package/src/features/dataflow.ts +462 -217
  327. package/src/features/graph-enrichment.ts +161 -117
  328. package/src/features/sequence.ts +27 -4
  329. package/src/features/structure-query.ts +43 -4
  330. package/src/features/structure.ts +50 -22
  331. package/src/graph/algorithms/leiden/adapter.ts +126 -71
  332. package/src/graph/algorithms/leiden/index.ts +67 -28
  333. package/src/graph/algorithms/leiden/optimiser.ts +114 -105
  334. package/src/graph/algorithms/leiden/partition.ts +131 -98
  335. package/src/graph/model.ts +19 -7
  336. package/src/infrastructure/config.ts +60 -58
  337. package/src/infrastructure/registry.ts +17 -14
  338. package/src/mcp/server.ts +46 -37
  339. package/src/presentation/audit.ts +72 -67
  340. package/src/presentation/branch-compare.ts +54 -50
  341. package/src/presentation/check.ts +34 -34
  342. package/src/presentation/colors.ts +2 -0
  343. package/src/presentation/complexity.ts +39 -33
  344. package/src/presentation/queries-cli/exports.ts +17 -17
  345. package/src/presentation/queries-cli/impact.ts +30 -22
  346. package/src/types.ts +195 -7
@@ -0,0 +1,259 @@
1
+ import type {
2
+ Call,
3
+ ExtractorOutput,
4
+ SubDeclaration,
5
+ TreeSitterNode,
6
+ TreeSitterTree,
7
+ } from '../types.js';
8
+ import { findChild, nodeEndLine } from './helpers.js';
9
+
10
+ /**
11
+ * Extract symbols from OCaml files.
12
+ */
13
+ export function extractOCamlSymbols(tree: TreeSitterTree, _filePath: string): ExtractorOutput {
14
+ const ctx: ExtractorOutput = {
15
+ definitions: [],
16
+ calls: [],
17
+ imports: [],
18
+ classes: [],
19
+ exports: [],
20
+ typeMap: new Map(),
21
+ };
22
+
23
+ walkOCamlNode(tree.rootNode, ctx);
24
+ return ctx;
25
+ }
26
+
27
+ function walkOCamlNode(node: TreeSitterNode, ctx: ExtractorOutput): void {
28
+ switch (node.type) {
29
+ case 'value_definition':
30
+ handleOCamlValueDef(node, ctx);
31
+ break;
32
+ case 'let_binding':
33
+ // Only handle top-level let bindings not inside value_definition
34
+ if (node.parent?.type !== 'value_definition') {
35
+ handleOCamlLetBinding(node, ctx);
36
+ }
37
+ break;
38
+ case 'module_definition':
39
+ handleOCamlModuleDef(node, ctx);
40
+ break;
41
+ case 'type_definition':
42
+ handleOCamlTypeDef(node, ctx);
43
+ break;
44
+ case 'class_definition':
45
+ handleOCamlClassDef(node, ctx);
46
+ break;
47
+ case 'open_module':
48
+ handleOCamlOpen(node, ctx);
49
+ break;
50
+ case 'application_expression':
51
+ handleOCamlApplication(node, ctx);
52
+ break;
53
+ }
54
+
55
+ for (let i = 0; i < node.childCount; i++) {
56
+ const child = node.child(i);
57
+ if (child) walkOCamlNode(child, ctx);
58
+ }
59
+ }
60
+
61
+ function handleOCamlValueDef(node: TreeSitterNode, ctx: ExtractorOutput): void {
62
+ // value_definition contains one or more let_bindings
63
+ for (let i = 0; i < node.childCount; i++) {
64
+ const child = node.child(i);
65
+ if (child && child.type === 'let_binding') {
66
+ handleOCamlLetBinding(child, ctx);
67
+ }
68
+ }
69
+ }
70
+
71
+ function handleOCamlLetBinding(node: TreeSitterNode, ctx: ExtractorOutput): void {
72
+ // let_binding has a pattern (the name) and optionally a body
73
+ const pattern = node.childForFieldName('pattern');
74
+ if (!pattern) return;
75
+
76
+ // Check if this is a function (has parameter children)
77
+ const hasParams = hasOCamlParams(node);
78
+ const name = extractOCamlPatternName(pattern);
79
+ if (!name) return;
80
+
81
+ if (hasParams) {
82
+ const params = extractOCamlParams(node);
83
+ ctx.definitions.push({
84
+ name,
85
+ kind: 'function',
86
+ line: node.startPosition.row + 1,
87
+ endLine: nodeEndLine(node),
88
+ children: params.length > 0 ? params : undefined,
89
+ });
90
+ } else {
91
+ ctx.definitions.push({
92
+ name,
93
+ kind: 'variable',
94
+ line: node.startPosition.row + 1,
95
+ endLine: nodeEndLine(node),
96
+ });
97
+ }
98
+ }
99
+
100
+ function extractOCamlPatternName(pattern: TreeSitterNode): string | null {
101
+ if (pattern.type === 'value_name' || pattern.type === 'identifier') {
102
+ return pattern.text;
103
+ }
104
+ // Operator definitions like `let (+) a b = ...`
105
+ if (pattern.type === 'parenthesized_operator') {
106
+ return pattern.text;
107
+ }
108
+ const nameNode = findChild(pattern, 'value_name') || findChild(pattern, 'identifier');
109
+ return nameNode ? nameNode.text : null;
110
+ }
111
+
112
+ function hasOCamlParams(letBinding: TreeSitterNode): boolean {
113
+ for (let i = 0; i < letBinding.childCount; i++) {
114
+ const child = letBinding.child(i);
115
+ if (!child) continue;
116
+ if (child.type === 'parameter' || child.type === 'value_pattern') return true;
117
+ }
118
+ return false;
119
+ }
120
+
121
+ function extractOCamlParams(letBinding: TreeSitterNode): SubDeclaration[] {
122
+ const params: SubDeclaration[] = [];
123
+ for (let i = 0; i < letBinding.childCount; i++) {
124
+ const child = letBinding.child(i);
125
+ if (!child) continue;
126
+ if (child.type === 'parameter' || child.type === 'value_pattern') {
127
+ const name = extractOCamlPatternName(child);
128
+ if (name) {
129
+ params.push({ name, kind: 'parameter', line: child.startPosition.row + 1 });
130
+ }
131
+ }
132
+ }
133
+ return params;
134
+ }
135
+
136
+ function handleOCamlModuleDef(node: TreeSitterNode, ctx: ExtractorOutput): void {
137
+ const binding = findChild(node, 'module_binding');
138
+ if (!binding) return;
139
+
140
+ const nameNode =
141
+ binding.childForFieldName('name') ||
142
+ findChild(binding, 'module_name') ||
143
+ findChild(binding, 'identifier');
144
+ if (!nameNode) return;
145
+
146
+ ctx.definitions.push({
147
+ name: nameNode.text,
148
+ kind: 'module',
149
+ line: node.startPosition.row + 1,
150
+ endLine: nodeEndLine(node),
151
+ });
152
+ }
153
+
154
+ function handleOCamlTypeDef(node: TreeSitterNode, ctx: ExtractorOutput): void {
155
+ // type_definition contains one or more type_bindings
156
+ for (let i = 0; i < node.childCount; i++) {
157
+ const child = node.child(i);
158
+ if (!child || child.type !== 'type_binding') continue;
159
+
160
+ const nameNode =
161
+ child.childForFieldName('name') ||
162
+ findChild(child, 'type_constructor') ||
163
+ findChild(child, 'identifier');
164
+ if (!nameNode) continue;
165
+
166
+ const children: SubDeclaration[] = [];
167
+ extractOCamlTypeConstructors(child, children);
168
+
169
+ ctx.definitions.push({
170
+ name: nameNode.text,
171
+ kind: 'type',
172
+ line: child.startPosition.row + 1,
173
+ endLine: nodeEndLine(child),
174
+ children: children.length > 0 ? children : undefined,
175
+ });
176
+ }
177
+ }
178
+
179
+ function extractOCamlTypeConstructors(
180
+ typeBinding: TreeSitterNode,
181
+ children: SubDeclaration[],
182
+ ): void {
183
+ for (let i = 0; i < typeBinding.childCount; i++) {
184
+ const child = typeBinding.child(i);
185
+ if (!child) continue;
186
+ if (child.type === 'constructor_declaration') {
187
+ const nameNode = findChild(child, 'constructor_name') || findChild(child, 'identifier');
188
+ if (nameNode) {
189
+ children.push({ name: nameNode.text, kind: 'property', line: child.startPosition.row + 1 });
190
+ }
191
+ }
192
+ }
193
+ }
194
+
195
+ function handleOCamlClassDef(node: TreeSitterNode, ctx: ExtractorOutput): void {
196
+ const binding = findChild(node, 'class_binding');
197
+ if (!binding) return;
198
+
199
+ const nameNode = binding.childForFieldName('name') || findChild(binding, 'identifier');
200
+ if (!nameNode) return;
201
+
202
+ ctx.definitions.push({
203
+ name: nameNode.text,
204
+ kind: 'class',
205
+ line: node.startPosition.row + 1,
206
+ endLine: nodeEndLine(node),
207
+ });
208
+ }
209
+
210
+ function handleOCamlOpen(node: TreeSitterNode, ctx: ExtractorOutput): void {
211
+ // open_module contains a module_path
212
+ let moduleName: string | null = null;
213
+ for (let i = 0; i < node.childCount; i++) {
214
+ const child = node.child(i);
215
+ if (!child) continue;
216
+ if (
217
+ child.type === 'module_path' ||
218
+ child.type === 'module_name' ||
219
+ child.type === 'extended_module_path' ||
220
+ child.type === 'constructor_name'
221
+ ) {
222
+ moduleName = child.text;
223
+ break;
224
+ }
225
+ }
226
+ if (!moduleName) return;
227
+
228
+ ctx.imports.push({
229
+ source: moduleName,
230
+ names: [moduleName.split('.').pop() || moduleName],
231
+ line: node.startPosition.row + 1,
232
+ });
233
+ }
234
+
235
+ function handleOCamlApplication(node: TreeSitterNode, ctx: ExtractorOutput): void {
236
+ // application_expression: first child is the function, rest are arguments
237
+ const funcNode = node.child(0);
238
+ if (!funcNode) return;
239
+
240
+ if (
241
+ funcNode.type === 'value_path' ||
242
+ funcNode.type === 'value_name' ||
243
+ funcNode.type === 'identifier'
244
+ ) {
245
+ ctx.calls.push({ name: funcNode.text, line: node.startPosition.row + 1 });
246
+ } else if (funcNode.type === 'field_get_expression') {
247
+ // Module.function calls
248
+ const field =
249
+ funcNode.childForFieldName('field') ||
250
+ findChild(funcNode, 'value_name') ||
251
+ findChild(funcNode, 'identifier');
252
+ const record = funcNode.child(0);
253
+ if (field) {
254
+ const call: Call = { name: field.text, line: node.startPosition.row + 1 };
255
+ if (record && record !== field) call.receiver = record.text;
256
+ ctx.calls.push(call);
257
+ }
258
+ }
259
+ }
@@ -5,7 +5,14 @@ import type {
5
5
  TreeSitterNode,
6
6
  TreeSitterTree,
7
7
  } from '../types.js';
8
- import { extractModifierVisibility, findChild, MAX_WALK_DEPTH, nodeEndLine } from './helpers.js';
8
+ import {
9
+ extractBodyMembers,
10
+ extractModifierVisibility,
11
+ findChild,
12
+ lastPathSegment,
13
+ MAX_WALK_DEPTH,
14
+ nodeEndLine,
15
+ } from './helpers.js';
9
16
 
10
17
  function extractPhpParameters(fnNode: TreeSitterNode): SubDeclaration[] {
11
18
  const params: SubDeclaration[] = [];
@@ -25,6 +32,39 @@ function extractPhpParameters(fnNode: TreeSitterNode): SubDeclaration[] {
25
32
  return params;
26
33
  }
27
34
 
35
+ /** Extract property declarations from a PHP class member. */
36
+ function extractPhpProperties(member: TreeSitterNode, children: SubDeclaration[]): void {
37
+ for (let j = 0; j < member.childCount; j++) {
38
+ const el = member.child(j);
39
+ if (!el || el.type !== 'property_element') continue;
40
+ const varNode = findChild(el, 'variable_name');
41
+ if (varNode) {
42
+ children.push({
43
+ name: varNode.text,
44
+ kind: 'property',
45
+ line: member.startPosition.row + 1,
46
+ visibility: extractModifierVisibility(member),
47
+ });
48
+ }
49
+ }
50
+ }
51
+
52
+ /** Extract constant declarations from a PHP class member. */
53
+ function extractPhpConstants(member: TreeSitterNode, children: SubDeclaration[]): void {
54
+ for (let j = 0; j < member.childCount; j++) {
55
+ const el = member.child(j);
56
+ if (!el || el.type !== 'const_element') continue;
57
+ const nameNode = el.childForFieldName('name') || findChild(el, 'name');
58
+ if (nameNode) {
59
+ children.push({
60
+ name: nameNode.text,
61
+ kind: 'constant',
62
+ line: member.startPosition.row + 1,
63
+ });
64
+ }
65
+ }
66
+ }
67
+
28
68
  function extractPhpClassChildren(classNode: TreeSitterNode): SubDeclaration[] {
29
69
  const children: SubDeclaration[] = [];
30
70
  const body = classNode.childForFieldName('body') || findChild(classNode, 'declaration_list');
@@ -33,50 +73,16 @@ function extractPhpClassChildren(classNode: TreeSitterNode): SubDeclaration[] {
33
73
  const member = body.child(i);
34
74
  if (!member) continue;
35
75
  if (member.type === 'property_declaration') {
36
- for (let j = 0; j < member.childCount; j++) {
37
- const el = member.child(j);
38
- if (!el || el.type !== 'property_element') continue;
39
- const varNode = findChild(el, 'variable_name');
40
- if (varNode) {
41
- children.push({
42
- name: varNode.text,
43
- kind: 'property',
44
- line: member.startPosition.row + 1,
45
- visibility: extractModifierVisibility(member),
46
- });
47
- }
48
- }
76
+ extractPhpProperties(member, children);
49
77
  } else if (member.type === 'const_declaration') {
50
- for (let j = 0; j < member.childCount; j++) {
51
- const el = member.child(j);
52
- if (!el || el.type !== 'const_element') continue;
53
- const nameNode = el.childForFieldName('name') || findChild(el, 'name');
54
- if (nameNode) {
55
- children.push({
56
- name: nameNode.text,
57
- kind: 'constant',
58
- line: member.startPosition.row + 1,
59
- });
60
- }
61
- }
78
+ extractPhpConstants(member, children);
62
79
  }
63
80
  }
64
81
  return children;
65
82
  }
66
83
 
67
84
  function extractPhpEnumCases(enumNode: TreeSitterNode): SubDeclaration[] {
68
- const children: SubDeclaration[] = [];
69
- const body = enumNode.childForFieldName('body') || findChild(enumNode, 'enum_declaration_list');
70
- if (!body) return children;
71
- for (let i = 0; i < body.childCount; i++) {
72
- const member = body.child(i);
73
- if (!member || member.type !== 'enum_case') continue;
74
- const nameNode = member.childForFieldName('name');
75
- if (nameNode) {
76
- children.push({ name: nameNode.text, kind: 'constant', line: member.startPosition.row + 1 });
77
- }
78
- }
79
- return children;
85
+ return extractBodyMembers(enumNode, ['body', 'enum_declaration_list'], 'enum_case', 'constant');
80
86
  }
81
87
 
82
88
  /**
@@ -272,7 +278,7 @@ function handlePhpNamespaceUse(node: TreeSitterNode, ctx: ExtractorOutput): void
272
278
  const nameNode = findChild(child, 'qualified_name') || findChild(child, 'name');
273
279
  if (nameNode) {
274
280
  const fullPath = nameNode.text;
275
- const lastName = fullPath.split('\\').pop() ?? fullPath;
281
+ const lastName = lastPathSegment(fullPath, '\\');
276
282
  const alias = child.childForFieldName('alias');
277
283
  ctx.imports.push({
278
284
  source: fullPath,
@@ -284,7 +290,7 @@ function handlePhpNamespaceUse(node: TreeSitterNode, ctx: ExtractorOutput): void
284
290
  }
285
291
  if (child && (child.type === 'qualified_name' || child.type === 'name')) {
286
292
  const fullPath = child.text;
287
- const lastName = fullPath.split('\\').pop() ?? fullPath;
293
+ const lastName = lastPathSegment(fullPath, '\\');
288
294
  ctx.imports.push({
289
295
  source: fullPath,
290
296
  names: [lastName],
@@ -4,9 +4,15 @@ import type {
4
4
  SubDeclaration,
5
5
  TreeSitterNode,
6
6
  TreeSitterTree,
7
- TypeMapEntry,
8
7
  } from '../types.js';
9
- import { findChild, MAX_WALK_DEPTH, nodeEndLine, pythonVisibility } from './helpers.js';
8
+ import {
9
+ findChild,
10
+ findParentNode,
11
+ MAX_WALK_DEPTH,
12
+ nodeEndLine,
13
+ pythonVisibility,
14
+ setTypeMapEntry,
15
+ } from './helpers.js';
10
16
 
11
17
  /** Built-in globals that start with uppercase but are not user-defined types. */
12
18
  const BUILTIN_GLOBALS_PY: Set<string> = new Set([
@@ -268,6 +274,37 @@ function extractPythonParameters(fnNode: TreeSitterNode): SubDeclaration[] {
268
274
  return params;
269
275
  }
270
276
 
277
+ /** Extract class-level assignment properties from expression statements. */
278
+ function extractClassAssignment(
279
+ child: TreeSitterNode,
280
+ seen: Set<string>,
281
+ props: SubDeclaration[],
282
+ ): void {
283
+ const assignment = findChild(child, 'assignment');
284
+ if (!assignment) return;
285
+ const left = assignment.childForFieldName('left');
286
+ if (!left || left.type !== 'identifier' || seen.has(left.text)) return;
287
+ seen.add(left.text);
288
+ props.push({
289
+ name: left.text,
290
+ kind: 'property',
291
+ line: child.startPosition.row + 1,
292
+ visibility: pythonVisibility(left.text),
293
+ });
294
+ }
295
+
296
+ /** If node is an __init__ method, walk its body for self.x assignments. */
297
+ function extractInitProperties(
298
+ node: TreeSitterNode,
299
+ seen: Set<string>,
300
+ props: SubDeclaration[],
301
+ ): void {
302
+ const fnName = node.childForFieldName('name');
303
+ if (!fnName || fnName.text !== '__init__') return;
304
+ const initBody = node.childForFieldName('body') || findChild(node, 'block');
305
+ if (initBody) walkInitBody(initBody, seen, props);
306
+ }
307
+
271
308
  function extractPythonClassProperties(classNode: TreeSitterNode): SubDeclaration[] {
272
309
  const props: SubDeclaration[] = [];
273
310
  const seen = new Set<string>();
@@ -279,42 +316,14 @@ function extractPythonClassProperties(classNode: TreeSitterNode): SubDeclaration
279
316
  if (!child) continue;
280
317
 
281
318
  if (child.type === 'expression_statement') {
282
- const assignment = findChild(child, 'assignment');
283
- if (assignment) {
284
- const left = assignment.childForFieldName('left');
285
- if (left && left.type === 'identifier' && !seen.has(left.text)) {
286
- seen.add(left.text);
287
- props.push({
288
- name: left.text,
289
- kind: 'property',
290
- line: child.startPosition.row + 1,
291
- visibility: pythonVisibility(left.text),
292
- });
293
- }
294
- }
295
- }
296
-
297
- if (child.type === 'function_definition') {
298
- const fnName = child.childForFieldName('name');
299
- if (fnName && fnName.text === '__init__') {
300
- const initBody = child.childForFieldName('body') || findChild(child, 'block');
301
- if (initBody) {
302
- walkInitBody(initBody, seen, props);
303
- }
304
- }
305
- }
306
-
307
- if (child.type === 'decorated_definition') {
319
+ extractClassAssignment(child, seen, props);
320
+ } else if (child.type === 'function_definition') {
321
+ extractInitProperties(child, seen, props);
322
+ } else if (child.type === 'decorated_definition') {
308
323
  for (let j = 0; j < child.childCount; j++) {
309
324
  const inner = child.child(j);
310
325
  if (inner && inner.type === 'function_definition') {
311
- const fnName = inner.childForFieldName('name');
312
- if (fnName && fnName.text === '__init__') {
313
- const initBody = inner.childForFieldName('body') || findChild(inner, 'block');
314
- if (initBody) {
315
- walkInitBody(initBody, seen, props);
316
- }
317
- }
326
+ extractInitProperties(inner, seen, props);
318
327
  }
319
328
  }
320
329
  }
@@ -348,15 +357,37 @@ function extractPythonTypeMap(node: TreeSitterNode, ctx: ExtractorOutput): void
348
357
  extractPythonTypeMapDepth(node, ctx, 0);
349
358
  }
350
359
 
351
- function setIfHigherPy(
352
- typeMap: Map<string, TypeMapEntry>,
353
- name: string,
354
- type: string,
355
- confidence: number,
356
- ): void {
357
- const existing = typeMap.get(name);
358
- if (!existing || confidence > existing.confidence) {
359
- typeMap.set(name, { type, confidence });
360
+ /** Handle typed_parameter or typed_default_parameter for type map. */
361
+ function handlePyTypedParam(node: TreeSitterNode, ctx: ExtractorOutput): void {
362
+ const isDefault = node.type === 'typed_default_parameter';
363
+ const nameNode = isDefault ? node.childForFieldName('name') : node.child(0);
364
+ const typeNode = node.childForFieldName('type');
365
+ if (!nameNode || nameNode.type !== 'identifier' || !typeNode) return;
366
+ if (nameNode.text === 'self' || nameNode.text === 'cls') return;
367
+ const typeName = extractPythonTypeName(typeNode);
368
+ if (typeName && ctx.typeMap) setTypeMapEntry(ctx.typeMap, nameNode.text, typeName, 0.9);
369
+ }
370
+
371
+ /** Handle assignment for constructor/factory type inference. */
372
+ function handlePyAssignmentType(node: TreeSitterNode, ctx: ExtractorOutput): void {
373
+ const left = node.childForFieldName('left');
374
+ const right = node.childForFieldName('right');
375
+ if (!left || left.type !== 'identifier' || !right || right.type !== 'call') return;
376
+
377
+ const fn = right.childForFieldName('function');
378
+ if (!fn) return;
379
+ if (fn.type === 'identifier') {
380
+ const name = fn.text;
381
+ if (name[0] && name[0] !== name[0].toLowerCase()) {
382
+ if (ctx.typeMap) setTypeMapEntry(ctx.typeMap, left.text, name, 1.0);
383
+ }
384
+ } else if (fn.type === 'attribute') {
385
+ const obj = fn.childForFieldName('object');
386
+ if (!obj || obj.type !== 'identifier') return;
387
+ const objName = obj.text;
388
+ if (objName[0] && objName[0] !== objName[0].toLowerCase() && !BUILTIN_GLOBALS_PY.has(objName)) {
389
+ if (ctx.typeMap) setTypeMapEntry(ctx.typeMap, left.text, objName, 0.7);
390
+ }
360
391
  }
361
392
  }
362
393
 
@@ -367,57 +398,10 @@ function extractPythonTypeMapDepth(
367
398
  ): void {
368
399
  if (depth >= MAX_WALK_DEPTH) return;
369
400
 
370
- // typed_parameter: identifier : type (confidence 0.9)
371
- if (node.type === 'typed_parameter') {
372
- const nameNode = node.child(0);
373
- const typeNode = node.childForFieldName('type');
374
- if (nameNode && nameNode.type === 'identifier' && typeNode) {
375
- const typeName = extractPythonTypeName(typeNode);
376
- if (typeName && nameNode.text !== 'self' && nameNode.text !== 'cls') {
377
- if (ctx.typeMap) setIfHigherPy(ctx.typeMap, nameNode.text, typeName, 0.9);
378
- }
379
- }
380
- }
381
-
382
- // typed_default_parameter: name : type = default (confidence 0.9)
383
- if (node.type === 'typed_default_parameter') {
384
- const nameNode = node.childForFieldName('name');
385
- const typeNode = node.childForFieldName('type');
386
- if (nameNode && nameNode.type === 'identifier' && typeNode) {
387
- const typeName = extractPythonTypeName(typeNode);
388
- if (typeName && nameNode.text !== 'self' && nameNode.text !== 'cls') {
389
- if (ctx.typeMap) setIfHigherPy(ctx.typeMap, nameNode.text, typeName, 0.9);
390
- }
391
- }
392
- }
393
-
394
- // assignment: x = SomeClass(...) → constructor (confidence 1.0)
395
- // x = SomeClass.create(...) → factory (confidence 0.7)
396
- if (node.type === 'assignment') {
397
- const left = node.childForFieldName('left');
398
- const right = node.childForFieldName('right');
399
- if (left && left.type === 'identifier' && right && right.type === 'call') {
400
- const fn = right.childForFieldName('function');
401
- if (fn && fn.type === 'identifier') {
402
- const name = fn.text;
403
- if (name[0] && name[0] !== name[0].toLowerCase()) {
404
- if (ctx.typeMap) setIfHigherPy(ctx.typeMap, left.text, name, 1.0);
405
- }
406
- }
407
- if (fn && fn.type === 'attribute') {
408
- const obj = fn.childForFieldName('object');
409
- if (obj && obj.type === 'identifier') {
410
- const objName = obj.text;
411
- if (
412
- objName[0] &&
413
- objName[0] !== objName[0].toLowerCase() &&
414
- !BUILTIN_GLOBALS_PY.has(objName)
415
- ) {
416
- if (ctx.typeMap) setIfHigherPy(ctx.typeMap, left.text, objName, 0.7);
417
- }
418
- }
419
- }
420
- }
401
+ if (node.type === 'typed_parameter' || node.type === 'typed_default_parameter') {
402
+ handlePyTypedParam(node, ctx);
403
+ } else if (node.type === 'assignment') {
404
+ handlePyAssignmentType(node, ctx);
421
405
  }
422
406
 
423
407
  for (let i = 0; i < node.childCount; i++) {
@@ -441,14 +425,7 @@ function extractPythonTypeName(typeNode: TreeSitterNode): string | null {
441
425
  return null;
442
426
  }
443
427
 
428
+ const PY_CLASS_TYPES = ['class_definition'] as const;
444
429
  function findPythonParentClass(node: TreeSitterNode): string | null {
445
- let current = node.parent;
446
- while (current) {
447
- if (current.type === 'class_definition') {
448
- const nameNode = current.childForFieldName('name');
449
- return nameNode ? nameNode.text : null;
450
- }
451
- current = current.parent;
452
- }
453
- return null;
430
+ return findParentNode(node, PY_CLASS_TYPES);
454
431
  }
@@ -5,7 +5,7 @@ import type {
5
5
  TreeSitterNode,
6
6
  TreeSitterTree,
7
7
  } from '../types.js';
8
- import { findChild, nodeEndLine } from './helpers.js';
8
+ import { findChild, findParentNode, lastPathSegment, nodeEndLine, stripQuotes } from './helpers.js';
9
9
 
10
10
  /**
11
11
  * Extract symbols from Ruby files.
@@ -176,10 +176,10 @@ function handleRubyRequire(node: TreeSitterNode, ctx: ExtractorOutput): void {
176
176
  for (let i = 0; i < args.childCount; i++) {
177
177
  const arg = args.child(i);
178
178
  if (arg && (arg.type === 'string' || arg.type === 'string_content')) {
179
- const strContent = arg.text.replace(/^['"]|['"]$/g, '');
179
+ const strContent = stripQuotes(arg.text);
180
180
  ctx.imports.push({
181
181
  source: strContent,
182
- names: [strContent.split('/').pop() ?? strContent],
182
+ names: [lastPathSegment(strContent)],
183
183
  line: node.startPosition.row + 1,
184
184
  rubyRequire: true,
185
185
  });
@@ -190,7 +190,7 @@ function handleRubyRequire(node: TreeSitterNode, ctx: ExtractorOutput): void {
190
190
  if (content) {
191
191
  ctx.imports.push({
192
192
  source: content.text,
193
- names: [content.text.split('/').pop() ?? content.text],
193
+ names: [lastPathSegment(content.text)],
194
194
  line: node.startPosition.row + 1,
195
195
  rubyRequire: true,
196
196
  });
@@ -221,16 +221,9 @@ function handleRubyModuleInclusion(
221
221
  }
222
222
  }
223
223
 
224
+ const RUBY_PARENT_TYPES = ['class', 'module'] as const;
224
225
  function findRubyParentClass(node: TreeSitterNode): string | null {
225
- let current = node.parent;
226
- while (current) {
227
- if (current.type === 'class' || current.type === 'module') {
228
- const nameNode = current.childForFieldName('name');
229
- return nameNode ? nameNode.text : null;
230
- }
231
- current = current.parent;
232
- }
233
- return null;
226
+ return findParentNode(node, RUBY_PARENT_TYPES);
234
227
  }
235
228
 
236
229
  // ── Child extraction helpers ────────────────────────────────────────────────