@optave/codegraph 3.9.6 → 3.11.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 (186) hide show
  1. package/README.md +26 -12
  2. package/dist/ast-analysis/engine.d.ts.map +1 -1
  3. package/dist/ast-analysis/engine.js +1 -1
  4. package/dist/ast-analysis/engine.js.map +1 -1
  5. package/dist/ast-analysis/rules/index.d.ts.map +1 -1
  6. package/dist/ast-analysis/rules/index.js +77 -0
  7. package/dist/ast-analysis/rules/index.js.map +1 -1
  8. package/dist/ast-analysis/visitors/ast-store-visitor.d.ts.map +1 -1
  9. package/dist/ast-analysis/visitors/ast-store-visitor.js +50 -8
  10. package/dist/ast-analysis/visitors/ast-store-visitor.js.map +1 -1
  11. package/dist/cli/commands/audit.js +1 -1
  12. package/dist/cli/commands/audit.js.map +1 -1
  13. package/dist/cli/commands/build.d.ts.map +1 -1
  14. package/dist/cli/commands/build.js +2 -0
  15. package/dist/cli/commands/build.js.map +1 -1
  16. package/dist/cli/commands/check.js +1 -1
  17. package/dist/cli/commands/check.js.map +1 -1
  18. package/dist/cli/commands/children.js +1 -1
  19. package/dist/cli/commands/children.js.map +1 -1
  20. package/dist/cli/commands/diff-impact.js +1 -1
  21. package/dist/cli/commands/diff-impact.js.map +1 -1
  22. package/dist/cli/commands/roles.js +1 -1
  23. package/dist/cli/commands/roles.js.map +1 -1
  24. package/dist/cli/commands/structure.js +1 -1
  25. package/dist/cli/commands/structure.js.map +1 -1
  26. package/dist/cli/shared/options.js +1 -1
  27. package/dist/cli/shared/options.js.map +1 -1
  28. package/dist/db/connection.d.ts.map +1 -1
  29. package/dist/db/connection.js +8 -0
  30. package/dist/db/connection.js.map +1 -1
  31. package/dist/domain/graph/builder/context.d.ts +10 -0
  32. package/dist/domain/graph/builder/context.d.ts.map +1 -1
  33. package/dist/domain/graph/builder/context.js +10 -0
  34. package/dist/domain/graph/builder/context.js.map +1 -1
  35. package/dist/domain/graph/builder/helpers.d.ts +7 -2
  36. package/dist/domain/graph/builder/helpers.d.ts.map +1 -1
  37. package/dist/domain/graph/builder/helpers.js +7 -2
  38. package/dist/domain/graph/builder/helpers.js.map +1 -1
  39. package/dist/domain/graph/builder/incremental.d.ts +0 -6
  40. package/dist/domain/graph/builder/incremental.d.ts.map +1 -1
  41. package/dist/domain/graph/builder/incremental.js +6 -23
  42. package/dist/domain/graph/builder/incremental.js.map +1 -1
  43. package/dist/domain/graph/builder/pipeline.d.ts +44 -0
  44. package/dist/domain/graph/builder/pipeline.d.ts.map +1 -1
  45. package/dist/domain/graph/builder/pipeline.js +348 -42
  46. package/dist/domain/graph/builder/pipeline.js.map +1 -1
  47. package/dist/domain/graph/builder/stages/build-edges.d.ts.map +1 -1
  48. package/dist/domain/graph/builder/stages/build-edges.js +8 -2
  49. package/dist/domain/graph/builder/stages/build-edges.js.map +1 -1
  50. package/dist/domain/graph/builder/stages/collect-files.d.ts.map +1 -1
  51. package/dist/domain/graph/builder/stages/collect-files.js +8 -0
  52. package/dist/domain/graph/builder/stages/collect-files.js.map +1 -1
  53. package/dist/domain/graph/builder/stages/detect-changes.d.ts +24 -0
  54. package/dist/domain/graph/builder/stages/detect-changes.d.ts.map +1 -1
  55. package/dist/domain/graph/builder/stages/detect-changes.js +117 -3
  56. package/dist/domain/graph/builder/stages/detect-changes.js.map +1 -1
  57. package/dist/domain/graph/builder/stages/finalize.d.ts.map +1 -1
  58. package/dist/domain/graph/builder/stages/finalize.js +9 -6
  59. package/dist/domain/graph/builder/stages/finalize.js.map +1 -1
  60. package/dist/domain/graph/builder/stages/insert-nodes.d.ts +30 -0
  61. package/dist/domain/graph/builder/stages/insert-nodes.d.ts.map +1 -1
  62. package/dist/domain/graph/builder/stages/insert-nodes.js +36 -13
  63. package/dist/domain/graph/builder/stages/insert-nodes.js.map +1 -1
  64. package/dist/domain/graph/builder/stages/resolve-imports.d.ts.map +1 -1
  65. package/dist/domain/graph/builder/stages/resolve-imports.js +73 -22
  66. package/dist/domain/graph/builder/stages/resolve-imports.js.map +1 -1
  67. package/dist/domain/graph/watcher.d.ts.map +1 -1
  68. package/dist/domain/graph/watcher.js +23 -18
  69. package/dist/domain/graph/watcher.js.map +1 -1
  70. package/dist/domain/parser.d.ts +14 -1
  71. package/dist/domain/parser.d.ts.map +1 -1
  72. package/dist/domain/parser.js +104 -11
  73. package/dist/domain/parser.js.map +1 -1
  74. package/dist/domain/search/models.d.ts +16 -0
  75. package/dist/domain/search/models.d.ts.map +1 -1
  76. package/dist/domain/search/models.js +36 -2
  77. package/dist/domain/search/models.js.map +1 -1
  78. package/dist/domain/wasm-worker-entry.js +20 -13
  79. package/dist/domain/wasm-worker-entry.js.map +1 -1
  80. package/dist/extractors/c.js +25 -6
  81. package/dist/extractors/c.js.map +1 -1
  82. package/dist/extractors/cpp.js +47 -6
  83. package/dist/extractors/cpp.js.map +1 -1
  84. package/dist/extractors/cuda.js +90 -14
  85. package/dist/extractors/cuda.js.map +1 -1
  86. package/dist/extractors/elixir.js +83 -3
  87. package/dist/extractors/elixir.js.map +1 -1
  88. package/dist/extractors/erlang.js +56 -20
  89. package/dist/extractors/erlang.js.map +1 -1
  90. package/dist/extractors/fsharp.d.ts +7 -0
  91. package/dist/extractors/fsharp.d.ts.map +1 -1
  92. package/dist/extractors/fsharp.js +94 -0
  93. package/dist/extractors/fsharp.js.map +1 -1
  94. package/dist/extractors/gleam.js +6 -2
  95. package/dist/extractors/gleam.js.map +1 -1
  96. package/dist/extractors/groovy.js +41 -1
  97. package/dist/extractors/groovy.js.map +1 -1
  98. package/dist/extractors/haskell.js +48 -4
  99. package/dist/extractors/haskell.js.map +1 -1
  100. package/dist/extractors/julia.js +172 -41
  101. package/dist/extractors/julia.js.map +1 -1
  102. package/dist/extractors/kotlin.js +4 -0
  103. package/dist/extractors/kotlin.js.map +1 -1
  104. package/dist/extractors/objc.js +184 -47
  105. package/dist/extractors/objc.js.map +1 -1
  106. package/dist/extractors/python.js +7 -4
  107. package/dist/extractors/python.js.map +1 -1
  108. package/dist/extractors/r.js +93 -52
  109. package/dist/extractors/r.js.map +1 -1
  110. package/dist/extractors/scala.d.ts.map +1 -1
  111. package/dist/extractors/scala.js +18 -32
  112. package/dist/extractors/scala.js.map +1 -1
  113. package/dist/extractors/solidity.js +18 -9
  114. package/dist/extractors/solidity.js.map +1 -1
  115. package/dist/extractors/verilog.js +80 -15
  116. package/dist/extractors/verilog.js.map +1 -1
  117. package/dist/infrastructure/config.d.ts +1 -0
  118. package/dist/infrastructure/config.d.ts.map +1 -1
  119. package/dist/infrastructure/config.js +1 -0
  120. package/dist/infrastructure/config.js.map +1 -1
  121. package/dist/mcp/server.d.ts.map +1 -1
  122. package/dist/mcp/server.js +14 -8
  123. package/dist/mcp/server.js.map +1 -1
  124. package/dist/mcp/tool-registry.d.ts +1 -1
  125. package/dist/mcp/tool-registry.d.ts.map +1 -1
  126. package/dist/mcp/tool-registry.js +23 -5
  127. package/dist/mcp/tool-registry.js.map +1 -1
  128. package/dist/mcp/tools/semantic-search.d.ts +1 -0
  129. package/dist/mcp/tools/semantic-search.d.ts.map +1 -1
  130. package/dist/mcp/tools/semantic-search.js +1 -0
  131. package/dist/mcp/tools/semantic-search.js.map +1 -1
  132. package/dist/types.d.ts +16 -1
  133. package/dist/types.d.ts.map +1 -1
  134. package/grammars/tree-sitter-erlang.wasm +0 -0
  135. package/grammars/tree-sitter-fsharp.wasm +0 -0
  136. package/grammars/tree-sitter-fsharp_signature.wasm +0 -0
  137. package/grammars/tree-sitter-gleam.wasm +0 -0
  138. package/package.json +11 -10
  139. package/src/ast-analysis/engine.ts +3 -1
  140. package/src/ast-analysis/rules/index.ts +87 -0
  141. package/src/ast-analysis/visitors/ast-store-visitor.ts +45 -9
  142. package/src/cli/commands/audit.ts +1 -1
  143. package/src/cli/commands/build.ts +2 -0
  144. package/src/cli/commands/check.ts +1 -1
  145. package/src/cli/commands/children.ts +1 -1
  146. package/src/cli/commands/diff-impact.ts +1 -1
  147. package/src/cli/commands/roles.ts +1 -1
  148. package/src/cli/commands/structure.ts +1 -1
  149. package/src/cli/shared/options.ts +1 -1
  150. package/src/db/connection.ts +8 -0
  151. package/src/domain/graph/builder/context.ts +10 -0
  152. package/src/domain/graph/builder/helpers.ts +8 -3
  153. package/src/domain/graph/builder/incremental.ts +6 -41
  154. package/src/domain/graph/builder/pipeline.ts +404 -41
  155. package/src/domain/graph/builder/stages/build-edges.ts +9 -2
  156. package/src/domain/graph/builder/stages/collect-files.ts +9 -0
  157. package/src/domain/graph/builder/stages/detect-changes.ts +130 -4
  158. package/src/domain/graph/builder/stages/finalize.ts +9 -6
  159. package/src/domain/graph/builder/stages/insert-nodes.ts +38 -14
  160. package/src/domain/graph/builder/stages/resolve-imports.ts +79 -25
  161. package/src/domain/graph/watcher.ts +21 -23
  162. package/src/domain/parser.ts +110 -10
  163. package/src/domain/search/models.ts +37 -2
  164. package/src/domain/wasm-worker-entry.ts +20 -13
  165. package/src/extractors/c.ts +27 -8
  166. package/src/extractors/cpp.ts +50 -8
  167. package/src/extractors/cuda.ts +90 -16
  168. package/src/extractors/elixir.ts +75 -3
  169. package/src/extractors/erlang.ts +63 -20
  170. package/src/extractors/fsharp.ts +104 -0
  171. package/src/extractors/gleam.ts +7 -2
  172. package/src/extractors/groovy.ts +45 -1
  173. package/src/extractors/haskell.ts +45 -4
  174. package/src/extractors/julia.ts +164 -43
  175. package/src/extractors/kotlin.ts +4 -0
  176. package/src/extractors/objc.ts +171 -47
  177. package/src/extractors/python.ts +5 -3
  178. package/src/extractors/r.ts +88 -48
  179. package/src/extractors/scala.ts +24 -36
  180. package/src/extractors/solidity.ts +17 -8
  181. package/src/extractors/verilog.ts +83 -15
  182. package/src/infrastructure/config.ts +1 -0
  183. package/src/mcp/server.ts +16 -9
  184. package/src/mcp/tool-registry.ts +28 -5
  185. package/src/mcp/tools/semantic-search.ts +2 -0
  186. package/src/types.ts +16 -0
@@ -70,7 +70,10 @@ function walkErlangNode(node: TreeSitterNode, ctx: ExtractorOutput): void {
70
70
 
71
71
  function handleModuleAttr(node: TreeSitterNode, ctx: ExtractorOutput): void {
72
72
  // module_attribute: - module ( atom ) .
73
- const nameNode = findChild(node, 'atom');
73
+ // Prefer the named `name` field exposed by tree-sitter-erlang so we don't
74
+ // accidentally pick up the `module` keyword if a future grammar exposes it
75
+ // as a named `atom` child.
76
+ const nameNode = node.childForFieldName('name') ?? findChild(node, 'atom');
74
77
  if (!nameNode) return;
75
78
 
76
79
  ctx.definitions.push({
@@ -83,7 +86,10 @@ function handleModuleAttr(node: TreeSitterNode, ctx: ExtractorOutput): void {
83
86
 
84
87
  function handleRecordDecl(node: TreeSitterNode, ctx: ExtractorOutput): void {
85
88
  // record_decl: - record ( atom , { record_field, ... } ) .
86
- const nameNode = findChild(node, 'atom');
89
+ // Prefer the named `name` field exposed by tree-sitter-erlang; fall back to
90
+ // the first atom child for grammar versions that don't expose it. Mirrors
91
+ // the Rust `handle_record_decl` defensive pattern.
92
+ const nameNode = node.childForFieldName('name') ?? findChild(node, 'atom');
87
93
  if (!nameNode) return;
88
94
 
89
95
  const children: SubDeclaration[] = [];
@@ -112,7 +118,14 @@ function handleRecordDecl(node: TreeSitterNode, ctx: ExtractorOutput): void {
112
118
  }
113
119
 
114
120
  function handleTypeAlias(node: TreeSitterNode, ctx: ExtractorOutput): void {
115
- const nameNode = findChild(node, 'atom');
121
+ // type_alias: -type name(...) :: ty.
122
+ // Name is typically wrapped in a `type_name` node containing an `atom`.
123
+ // Mirrors the Rust `handle_type_alias` fallback so the two engines agree
124
+ // even when the grammar nests the name inside `type_name`.
125
+ const directAtom = findChild(node, 'atom');
126
+ const typeNameNode = !directAtom ? findChild(node, 'type_name') : null;
127
+ const wrappedAtom = typeNameNode ? findChild(typeNameNode, 'atom') : null;
128
+ const nameNode = directAtom ?? wrappedAtom;
116
129
  if (!nameNode) return;
117
130
 
118
131
  ctx.definitions.push({
@@ -134,13 +147,22 @@ function handleFunDecl(node: TreeSitterNode, ctx: ExtractorOutput): void {
134
147
 
135
148
  function handleFunctionClause(node: TreeSitterNode, ctx: ExtractorOutput): void {
136
149
  // function_clause: atom expr_args clause_body
137
- const nameNode = findChild(node, 'atom');
150
+ const nameNode = node.childForFieldName('name') ?? findChild(node, 'atom');
138
151
  if (!nameNode) return;
139
152
 
140
- // Don't duplicate if we already have this function
141
- if (ctx.definitions.some((d) => d.name === nameNode.text && d.kind === 'function')) return;
142
-
143
153
  const params = extractErlangParams(node);
154
+ const arity = params.length;
155
+
156
+ // Don't duplicate if we already have this function at the same arity.
157
+ // Erlang overloads by arity, so `foo/1` and `foo/2` are distinct definitions.
158
+ if (
159
+ ctx.definitions.some(
160
+ (d) =>
161
+ d.name === nameNode.text && d.kind === 'function' && (d.children?.length ?? 0) === arity,
162
+ )
163
+ ) {
164
+ return;
165
+ }
144
166
 
145
167
  ctx.definitions.push({
146
168
  name: nameNode.text,
@@ -154,31 +176,42 @@ function handleFunctionClause(node: TreeSitterNode, ctx: ExtractorOutput): void
154
176
 
155
177
  function extractErlangParams(clauseNode: TreeSitterNode): SubDeclaration[] {
156
178
  const params: SubDeclaration[] = [];
157
- const argsNode = findChild(clauseNode, 'expr_args');
179
+ const argsNode = clauseNode.childForFieldName('args') ?? findChild(clauseNode, 'expr_args');
158
180
  if (!argsNode) return params;
159
181
 
160
- for (let i = 0; i < argsNode.childCount; i++) {
161
- const child = argsNode.child(i);
182
+ // Iterate named children so every argument pattern counts as one parameter,
183
+ // independent of whether it is a bare `var`/`atom` or a complex destructuring
184
+ // pattern (tuple, list, binary, etc.). Punctuation tokens are anonymous and
185
+ // therefore excluded automatically.
186
+ for (let i = 0; i < argsNode.namedChildCount; i++) {
187
+ const child = argsNode.namedChild(i);
162
188
  if (!child) continue;
163
- if (child.type === 'var') {
164
- params.push({ name: child.text, kind: 'parameter', line: child.startPosition.row + 1 });
165
- }
166
- if (child.type === 'atom') {
167
- params.push({ name: child.text, kind: 'parameter', line: child.startPosition.row + 1 });
168
- }
189
+ const label =
190
+ child.type === 'var' || child.type === 'atom'
191
+ ? child.text
192
+ : // Placeholder for complex patterns so arity is preserved.
193
+ `_${i}`;
194
+ params.push({ name: label, kind: 'parameter', line: child.startPosition.row + 1 });
169
195
  }
170
196
  return params;
171
197
  }
172
198
 
173
199
  function handleDefine(node: TreeSitterNode, ctx: ExtractorOutput): void {
174
200
  // pp_define: -define(NAME, value).
201
+ // For parametric macros, the grammar wraps the name in a `macro_lhs(name, args)`
202
+ // node. Inside `macro_lhs` the name comes first, followed by `(`, the argument
203
+ // `var` children, and `)`. We must therefore try `atom` (lowercase macros,
204
+ // e.g. `-define(foo(X), X+1)`) before `var` (uppercase macros, e.g.
205
+ // `-define(FOO(X), X+1)`) — otherwise `findChild(.., 'var')` skips the leading
206
+ // atom and lands on the first argument variable, mislabeling the definition.
207
+ // Mirrors the Rust `handle_define` so both engines agree.
175
208
  const nameNode =
176
209
  findChild(node, 'var') || findChild(node, 'atom') || findChild(node, 'macro_lhs');
177
210
  if (!nameNode) return;
178
211
 
179
212
  const name =
180
213
  nameNode.type === 'macro_lhs'
181
- ? (findChild(nameNode, 'var')?.text ?? nameNode.text)
214
+ ? (findChild(nameNode, 'atom')?.text ?? findChild(nameNode, 'var')?.text ?? nameNode.text)
182
215
  : nameNode.text;
183
216
 
184
217
  ctx.definitions.push({
@@ -194,9 +227,15 @@ function handleInclude(node: TreeSitterNode, ctx: ExtractorOutput): void {
194
227
  if (!strNode) return;
195
228
 
196
229
  const source = strNode.text.replace(/^"|"$/g, '');
230
+ // Preserve the distinction between local includes (`-include("foo.hrl")`)
231
+ // and OTP library includes (`-include_lib("kernel/include/file.hrl")`) so
232
+ // downstream consumers can apply the correct path-resolution strategy
233
+ // (local: relative to the source file; lib: relative to an OTP app root).
234
+ // Mirrors the Rust `handle_include` so both engines agree.
235
+ const kind = node.type === 'pp_include_lib' ? 'include_lib' : 'include';
197
236
  ctx.imports.push({
198
237
  source,
199
- names: ['include'],
238
+ names: [kind],
200
239
  line: node.startPosition.row + 1,
201
240
  });
202
241
  }
@@ -224,8 +263,12 @@ function handleImportAttr(node: TreeSitterNode, ctx: ExtractorOutput): void {
224
263
  }
225
264
 
226
265
  function handleCall(node: TreeSitterNode, ctx: ExtractorOutput): void {
227
- // call: first child is function ref (atom or remote), then expr_args
228
- const funcNode = node.child(0);
266
+ // call: first named child is function ref (atom or remote), then expr_args.
267
+ // Using `namedChild(0)` rather than `child(0)` skips anonymous tokens
268
+ // (punctuation, keywords) so a future grammar revision that inserts a
269
+ // leading anonymous node won't silently drop the call. Mirrors the Rust
270
+ // `handle_call` so both engines emit the same set of calls.
271
+ const funcNode = node.namedChild(0);
229
272
  if (!funcNode) return;
230
273
 
231
274
  if (funcNode.type === 'atom' || funcNode.type === 'identifier') {
@@ -10,6 +10,13 @@ import { findChild, nodeEndLine } from './helpers.js';
10
10
  /**
11
11
  * Extract symbols from F# files.
12
12
  *
13
+ * Grammar source: `tree-sitter-fsharp` v0.3.0 installed via a pinned GitHub
14
+ * tarball in `package.json` because the ionide/tree-sitter-fsharp project has
15
+ * no v0.3.0 release published to the npm registry. The cargo crate the native
16
+ * engine uses is also v0.3.0; both engines must stay aligned. Upgrading
17
+ * requires a manual edit of the tarball URL in `package.json` and
18
+ * `package-lock.json` — `npm update` will not bump this entry.
19
+ *
13
20
  * tree-sitter-fsharp grammar notes:
14
21
  * - named_module: top-level module declaration
15
22
  * - function_declaration_left: LHS of `let name params = ...`
@@ -42,6 +49,14 @@ function walkFSharpNode(
42
49
  case 'named_module':
43
50
  nextModule = handleNamedModule(node, ctx);
44
51
  break;
52
+ case 'module_defn':
53
+ // Nested signature module (`module Foo = ...`) in `.fsi` files,
54
+ // emitted by both the WASM (npm ionide tarball v0.3.0) and cargo
55
+ // v0.3.0 tree-sitter-fsharp signature grammars. Accumulate the
56
+ // dotted module path so nested `val` declarations are qualified
57
+ // as `Outer.Inner.foo` in parity with the native engine.
58
+ nextModule = handleModuleDefn(node, ctx, currentModule);
59
+ break;
45
60
  case 'function_declaration_left':
46
61
  handleFunctionDecl(node, ctx, currentModule);
47
62
  break;
@@ -57,6 +72,9 @@ function walkFSharpNode(
57
72
  case 'dot_expression':
58
73
  handleDotExpression(node, ctx);
59
74
  break;
75
+ case 'value_definition':
76
+ handleValueDefinition(node, ctx, currentModule);
77
+ break;
60
78
  }
61
79
 
62
80
  for (let i = 0; i < node.childCount; i++) {
@@ -79,6 +97,27 @@ function handleNamedModule(node: TreeSitterNode, ctx: ExtractorOutput): string |
79
97
  return nameNode.text;
80
98
  }
81
99
 
100
+ function handleModuleDefn(
101
+ node: TreeSitterNode,
102
+ ctx: ExtractorOutput,
103
+ currentModule: string | null,
104
+ ): string | null {
105
+ // `module_defn` (cargo 0.3.0 signature grammar) wraps `module Foo = ...`
106
+ // sections inside an outer `namespace` or another module. The name is a
107
+ // direct `identifier` child.
108
+ const nameNode = findChild(node, 'identifier');
109
+ if (!nameNode) return currentModule;
110
+
111
+ const qualified = currentModule ? `${currentModule}.${nameNode.text}` : nameNode.text;
112
+ ctx.definitions.push({
113
+ name: qualified,
114
+ kind: 'module',
115
+ line: node.startPosition.row + 1,
116
+ endLine: nodeEndLine(node),
117
+ });
118
+ return qualified;
119
+ }
120
+
82
121
  function handleFunctionDecl(
83
122
  node: TreeSitterNode,
84
123
  ctx: ExtractorOutput,
@@ -251,3 +290,68 @@ function handleDotExpression(node: TreeSitterNode, ctx: ExtractorOutput): void {
251
290
  ctx.calls.push(call);
252
291
  }
253
292
  }
293
+
294
+ // Handle `val name : type` declarations in `.fsi` signature files.
295
+ // The signature grammar reuses `value_definition` for `val` bindings,
296
+ // distinguished from the source grammar's `let` bindings by the first
297
+ // child being the literal `val` keyword. Source-file `value_definition`
298
+ // nodes (which start with `let`) are intentionally ignored to preserve
299
+ // `.fs` extractor parity.
300
+ function handleValueDefinition(
301
+ node: TreeSitterNode,
302
+ ctx: ExtractorOutput,
303
+ currentModule: string | null,
304
+ ): void {
305
+ const first = node.child(0);
306
+ if (!first || first.type !== 'val') return;
307
+
308
+ const declLeft = findChild(node, 'value_declaration_left');
309
+ if (!declLeft) return;
310
+
311
+ const pattern = findChild(declLeft, 'identifier_pattern');
312
+ if (!pattern) return;
313
+
314
+ const ident =
315
+ findChild(findChild(pattern, 'long_identifier_or_op') ?? pattern, 'identifier') ??
316
+ findChild(pattern, 'identifier');
317
+ if (!ident) return;
318
+
319
+ // The npm and cargo tree-sitter-fsharp 0.3.0 grammars — though sharing a
320
+ // version tag — emit type signatures with different node shapes:
321
+ // • WASM (npm 0.3.0 ionide tarball): `function_type` is the explicit
322
+ // function-type kind, present as a direct child of `value_definition`
323
+ // for `a -> b` types; plain values (e.g. `val pi : float`) appear as
324
+ // `simple_type`.
325
+ // • Native (cargo 0.3.0): every type signature is wrapped in
326
+ // `curried_spec`. A function type contains one or more `arguments_spec`
327
+ // children; a plain value wraps a single `simple_type`.
328
+ // Classify as a function whenever `function_type` appears OR a
329
+ // `curried_spec` contains an `arguments_spec` child, so both engines stay
330
+ // in parity until the grammars converge.
331
+ let hasFunctionType = false;
332
+ for (let i = 0; i < node.childCount; i++) {
333
+ const c = node.child(i);
334
+ if (!c) continue;
335
+ if (c.type === 'function_type') {
336
+ hasFunctionType = true;
337
+ break;
338
+ }
339
+ if (c.type === 'curried_spec') {
340
+ for (let j = 0; j < c.childCount; j++) {
341
+ if (c.child(j)?.type === 'arguments_spec') {
342
+ hasFunctionType = true;
343
+ break;
344
+ }
345
+ }
346
+ if (hasFunctionType) break;
347
+ }
348
+ }
349
+
350
+ const name = currentModule ? `${currentModule}.${ident.text}` : ident.text;
351
+ ctx.definitions.push({
352
+ name,
353
+ kind: hasFunctionType ? 'function' : 'variable',
354
+ line: node.startPosition.row + 1,
355
+ endLine: nodeEndLine(node),
356
+ });
357
+ }
@@ -85,12 +85,15 @@ function handleExternalFunction(node: TreeSitterNode, ctx: ExtractorOutput): voi
85
85
  const nameNode = node.childForFieldName('name') || findChild(node, 'identifier');
86
86
  if (!nameNode) return;
87
87
 
88
+ const params = extractParams(node);
89
+
88
90
  ctx.definitions.push({
89
91
  name: nameNode.text,
90
92
  kind: 'function',
91
93
  line: node.startPosition.row + 1,
92
94
  endLine: nodeEndLine(node),
93
95
  visibility: isPublic(node) ? 'public' : 'private',
96
+ children: params.length > 0 ? params : undefined,
94
97
  });
95
98
  }
96
99
 
@@ -198,14 +201,16 @@ function handleImport(node: TreeSitterNode, ctx: ExtractorOutput): void {
198
201
  }
199
202
 
200
203
  function handleCall(node: TreeSitterNode, ctx: ExtractorOutput): void {
201
- const funcNode = node.childForFieldName('function') || node.child(0);
204
+ const funcNode = node.childForFieldName('function') || node.namedChild(0);
202
205
  if (!funcNode) return;
203
206
 
204
207
  if (funcNode.type === 'identifier' || funcNode.type === 'variable') {
205
208
  ctx.calls.push({ name: funcNode.text, line: node.startPosition.row + 1 });
206
209
  } else if (funcNode.type === 'field_access' || funcNode.type === 'module_select') {
207
210
  const field = funcNode.childForFieldName('field') || funcNode.childForFieldName('label');
208
- const record = funcNode.child(0);
211
+ // Prefer the `record` field; fall back to first named child to skip
212
+ // anonymous punctuation tokens (the `.` between record and field).
213
+ const record = funcNode.childForFieldName('record') || funcNode.namedChild(0);
209
214
  if (field) {
210
215
  const call: Call = { name: field.text, line: node.startPosition.row + 1 };
211
216
  if (record && record !== field) call.receiver = record.text;
@@ -68,6 +68,7 @@ function walkGroovyNode(node: TreeSitterNode, ctx: ExtractorOutput): void {
68
68
  case 'method_invocation':
69
69
  case 'call_expression':
70
70
  case 'function_call':
71
+ case 'juxt_function_call':
71
72
  handleGroovyCallExpr(node, ctx);
72
73
  break;
73
74
  case 'object_creation_expression':
@@ -140,14 +141,57 @@ function handleGroovyClassDecl(node: TreeSitterNode, ctx: ExtractorOutput): void
140
141
  function handleGroovyInterfaceDecl(node: TreeSitterNode, ctx: ExtractorOutput): void {
141
142
  const nameNode = node.childForFieldName('name');
142
143
  if (!nameNode) return;
144
+ const ifaceName = nameNode.text;
143
145
 
144
146
  ctx.definitions.push({
145
- name: nameNode.text,
147
+ name: ifaceName,
146
148
  kind: 'interface',
147
149
  line: node.startPosition.row + 1,
148
150
  endLine: nodeEndLine(node),
149
151
  visibility: extractModifierVisibility(node),
150
152
  });
153
+
154
+ // `interface X extends Y, Z` — tree-sitter-groovy 0.1.x exposes parent
155
+ // interfaces via an unnamed `extends_interfaces` child (not a field), which
156
+ // wraps a `type_list` of `_type` nodes. Mirrors the Rust extractor.
157
+ for (let i = 0; i < node.childCount; i++) {
158
+ const child = node.child(i);
159
+ if (child && child.type === 'extends_interfaces') {
160
+ collectGroovyParentInterfaces(child, ifaceName, ctx);
161
+ break;
162
+ }
163
+ }
164
+ }
165
+
166
+ function collectGroovyParentInterfaces(
167
+ parent: TreeSitterNode,
168
+ name: string,
169
+ ctx: ExtractorOutput,
170
+ ): void {
171
+ // Use the current node's start line at each recursion level — matches the
172
+ // Rust `collect_interfaces` helper, which re-evaluates `start_line(interfaces)`
173
+ // for whatever node (`extends_interfaces` → `type_list`) is being processed.
174
+ const line = parent.startPosition.row + 1;
175
+ for (let i = 0; i < parent.childCount; i++) {
176
+ const child = parent.child(i);
177
+ if (!child) continue;
178
+ switch (child.type) {
179
+ case 'type_identifier':
180
+ case 'identifier':
181
+ case 'scoped_type_identifier': {
182
+ ctx.classes.push({ name, implements: child.text, line });
183
+ break;
184
+ }
185
+ case 'generic_type': {
186
+ const inner = child.child(0)?.text;
187
+ if (inner) ctx.classes.push({ name, implements: inner, line });
188
+ break;
189
+ }
190
+ case 'type_list':
191
+ collectGroovyParentInterfaces(child, name, ctx);
192
+ break;
193
+ }
194
+ }
151
195
  }
152
196
 
153
197
  function handleGroovyEnumDecl(node: TreeSitterNode, ctx: ExtractorOutput): void {
@@ -81,19 +81,60 @@ function extractHaskellParams(funcNode: TreeSitterNode): SubDeclaration[] {
81
81
  if (child.type === 'patterns' || child.type === 'parameter') {
82
82
  for (let j = 0; j < child.childCount; j++) {
83
83
  const pat = child.child(j);
84
- if (pat && (pat.type === 'variable' || pat.type === 'identifier')) {
85
- params.push({ name: pat.text, kind: 'parameter', line: pat.startPosition.row + 1 });
86
- }
84
+ if (pat) collectHaskellPatternBindings(pat, params);
87
85
  }
88
86
  }
89
87
  if (child.type === 'variable' && i > 0) {
90
- // Pattern parameters after the function name
88
+ // Pattern parameters after the function name (no enclosing `patterns` node)
91
89
  params.push({ name: child.text, kind: 'parameter', line: child.startPosition.row + 1 });
92
90
  }
93
91
  }
94
92
  return params;
95
93
  }
96
94
 
95
+ // Walk a pattern node and emit each bound variable (and `_` for wildcards) as a parameter.
96
+ // Container patterns — parens, constructor application, infix (cons), tuple, list, as, strict,
97
+ // irrefutable, qualified — are transparent: descend into their children. `record` is special:
98
+ // only the right-hand-side of each `field_pattern` is bound (the field name is not).
99
+ // Literals, bare constructors, and operators do not bind.
100
+ function collectHaskellPatternBindings(node: TreeSitterNode, out: SubDeclaration[]): void {
101
+ switch (node.type) {
102
+ case 'variable':
103
+ case 'identifier':
104
+ out.push({ name: node.text, kind: 'parameter', line: node.startPosition.row + 1 });
105
+ return;
106
+ case 'wildcard':
107
+ out.push({ name: '_', kind: 'parameter', line: node.startPosition.row + 1 });
108
+ return;
109
+ case 'parens':
110
+ case 'apply':
111
+ case 'infix':
112
+ case 'tuple':
113
+ case 'list':
114
+ case 'strict':
115
+ case 'irrefutable':
116
+ case 'as':
117
+ case 'qualified':
118
+ for (let i = 0; i < node.childCount; i++) {
119
+ const c = node.child(i);
120
+ if (c) collectHaskellPatternBindings(c, out);
121
+ }
122
+ return;
123
+ case 'record':
124
+ for (let i = 0; i < node.childCount; i++) {
125
+ const fp = node.child(i);
126
+ if (!fp || fp.type !== 'field_pattern') continue;
127
+ for (let j = 0; j < fp.childCount; j++) {
128
+ const g = fp.child(j);
129
+ if (g && g.type !== 'field_name') collectHaskellPatternBindings(g, out);
130
+ }
131
+ }
132
+ return;
133
+ default:
134
+ return;
135
+ }
136
+ }
137
+
97
138
  function handleHaskellBind(node: TreeSitterNode, ctx: ExtractorOutput): void {
98
139
  const nameNode = node.childForFieldName('name');
99
140
  if (!nameNode) return;