@optave/codegraph 3.6.0 → 3.8.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 +32 -16
  2. package/dist/ast-analysis/engine.d.ts.map +1 -1
  3. package/dist/ast-analysis/engine.js +158 -1
  4. package/dist/ast-analysis/engine.js.map +1 -1
  5. package/dist/ast-analysis/rules/javascript.d.ts.map +1 -1
  6. package/dist/ast-analysis/rules/javascript.js +0 -1
  7. package/dist/ast-analysis/rules/javascript.js.map +1 -1
  8. package/dist/ast-analysis/visitors/ast-store-visitor.d.ts.map +1 -1
  9. package/dist/ast-analysis/visitors/ast-store-visitor.js +2 -75
  10. package/dist/ast-analysis/visitors/ast-store-visitor.js.map +1 -1
  11. package/dist/cli/commands/ast.js +2 -2
  12. package/dist/cli/commands/ast.js.map +1 -1
  13. package/dist/domain/graph/builder/pipeline.d.ts.map +1 -1
  14. package/dist/domain/graph/builder/pipeline.js +128 -6
  15. package/dist/domain/graph/builder/pipeline.js.map +1 -1
  16. package/dist/domain/graph/builder/stages/build-edges.d.ts.map +1 -1
  17. package/dist/domain/graph/builder/stages/build-edges.js +101 -1
  18. package/dist/domain/graph/builder/stages/build-edges.js.map +1 -1
  19. package/dist/domain/graph/builder/stages/collect-files.d.ts.map +1 -1
  20. package/dist/domain/graph/builder/stages/collect-files.js +17 -5
  21. package/dist/domain/graph/builder/stages/collect-files.js.map +1 -1
  22. package/dist/domain/graph/builder/stages/detect-changes.d.ts.map +1 -1
  23. package/dist/domain/graph/builder/stages/detect-changes.js +98 -50
  24. package/dist/domain/graph/builder/stages/detect-changes.js.map +1 -1
  25. package/dist/domain/graph/builder/stages/finalize.d.ts.map +1 -1
  26. package/dist/domain/graph/builder/stages/finalize.js +32 -5
  27. package/dist/domain/graph/builder/stages/finalize.js.map +1 -1
  28. package/dist/domain/graph/builder/stages/insert-nodes.d.ts.map +1 -1
  29. package/dist/domain/graph/builder/stages/insert-nodes.js +20 -7
  30. package/dist/domain/graph/builder/stages/insert-nodes.js.map +1 -1
  31. package/dist/domain/parser.d.ts +1 -1
  32. package/dist/domain/parser.d.ts.map +1 -1
  33. package/dist/domain/parser.js +129 -3
  34. package/dist/domain/parser.js.map +1 -1
  35. package/dist/extractors/clojure.d.ts +12 -0
  36. package/dist/extractors/clojure.d.ts.map +1 -0
  37. package/dist/extractors/clojure.js +245 -0
  38. package/dist/extractors/clojure.js.map +1 -0
  39. package/dist/extractors/cuda.d.ts +11 -0
  40. package/dist/extractors/cuda.d.ts.map +1 -0
  41. package/dist/extractors/cuda.js +302 -0
  42. package/dist/extractors/cuda.js.map +1 -0
  43. package/dist/extractors/dart.d.ts +6 -0
  44. package/dist/extractors/dart.d.ts.map +1 -0
  45. package/dist/extractors/dart.js +277 -0
  46. package/dist/extractors/dart.js.map +1 -0
  47. package/dist/extractors/elixir.d.ts +9 -0
  48. package/dist/extractors/elixir.d.ts.map +1 -0
  49. package/dist/extractors/elixir.js +223 -0
  50. package/dist/extractors/elixir.js.map +1 -0
  51. package/dist/extractors/erlang.d.ts +14 -0
  52. package/dist/extractors/erlang.d.ts.map +1 -0
  53. package/dist/extractors/erlang.js +239 -0
  54. package/dist/extractors/erlang.js.map +1 -0
  55. package/dist/extractors/fsharp.d.ts +13 -0
  56. package/dist/extractors/fsharp.d.ts.map +1 -0
  57. package/dist/extractors/fsharp.js +218 -0
  58. package/dist/extractors/fsharp.js.map +1 -0
  59. package/dist/extractors/gleam.d.ts +14 -0
  60. package/dist/extractors/gleam.d.ts.map +1 -0
  61. package/dist/extractors/gleam.js +229 -0
  62. package/dist/extractors/gleam.js.map +1 -0
  63. package/dist/extractors/groovy.d.ts +10 -0
  64. package/dist/extractors/groovy.d.ts.map +1 -0
  65. package/dist/extractors/groovy.js +304 -0
  66. package/dist/extractors/groovy.js.map +1 -0
  67. package/dist/extractors/haskell.d.ts +8 -0
  68. package/dist/extractors/haskell.d.ts.map +1 -0
  69. package/dist/extractors/haskell.js +217 -0
  70. package/dist/extractors/haskell.js.map +1 -0
  71. package/dist/extractors/index.d.ts +17 -0
  72. package/dist/extractors/index.d.ts.map +1 -1
  73. package/dist/extractors/index.js +17 -0
  74. package/dist/extractors/index.js.map +1 -1
  75. package/dist/extractors/julia.d.ts +16 -0
  76. package/dist/extractors/julia.d.ts.map +1 -0
  77. package/dist/extractors/julia.js +287 -0
  78. package/dist/extractors/julia.js.map +1 -0
  79. package/dist/extractors/lua.d.ts +6 -0
  80. package/dist/extractors/lua.d.ts.map +1 -0
  81. package/dist/extractors/lua.js +162 -0
  82. package/dist/extractors/lua.js.map +1 -0
  83. package/dist/extractors/objc.d.ts +9 -0
  84. package/dist/extractors/objc.d.ts.map +1 -0
  85. package/dist/extractors/objc.js +406 -0
  86. package/dist/extractors/objc.js.map +1 -0
  87. package/dist/extractors/ocaml.d.ts +6 -0
  88. package/dist/extractors/ocaml.d.ts.map +1 -0
  89. package/dist/extractors/ocaml.js +310 -0
  90. package/dist/extractors/ocaml.js.map +1 -0
  91. package/dist/extractors/r.d.ts +13 -0
  92. package/dist/extractors/r.d.ts.map +1 -0
  93. package/dist/extractors/r.js +251 -0
  94. package/dist/extractors/r.js.map +1 -0
  95. package/dist/extractors/solidity.d.ts +9 -0
  96. package/dist/extractors/solidity.d.ts.map +1 -0
  97. package/dist/extractors/solidity.js +374 -0
  98. package/dist/extractors/solidity.js.map +1 -0
  99. package/dist/extractors/verilog.d.ts +9 -0
  100. package/dist/extractors/verilog.d.ts.map +1 -0
  101. package/dist/extractors/verilog.js +286 -0
  102. package/dist/extractors/verilog.js.map +1 -0
  103. package/dist/extractors/zig.d.ts +9 -0
  104. package/dist/extractors/zig.d.ts.map +1 -0
  105. package/dist/extractors/zig.js +276 -0
  106. package/dist/extractors/zig.js.map +1 -0
  107. package/dist/features/ast.d.ts.map +1 -1
  108. package/dist/features/ast.js +1 -2
  109. package/dist/features/ast.js.map +1 -1
  110. package/dist/features/cfg.d.ts +1 -1
  111. package/dist/features/cfg.d.ts.map +1 -1
  112. package/dist/features/cfg.js +6 -51
  113. package/dist/features/cfg.js.map +1 -1
  114. package/dist/graph/algorithms/bfs.d.ts +2 -0
  115. package/dist/graph/algorithms/bfs.d.ts.map +1 -1
  116. package/dist/graph/algorithms/bfs.js +27 -0
  117. package/dist/graph/algorithms/bfs.js.map +1 -1
  118. package/dist/graph/algorithms/centrality.d.ts +2 -0
  119. package/dist/graph/algorithms/centrality.d.ts.map +1 -1
  120. package/dist/graph/algorithms/centrality.js +28 -0
  121. package/dist/graph/algorithms/centrality.js.map +1 -1
  122. package/dist/graph/algorithms/louvain.d.ts +3 -4
  123. package/dist/graph/algorithms/louvain.d.ts.map +1 -1
  124. package/dist/graph/algorithms/louvain.js +29 -0
  125. package/dist/graph/algorithms/louvain.js.map +1 -1
  126. package/dist/graph/algorithms/shortest-path.d.ts +2 -0
  127. package/dist/graph/algorithms/shortest-path.d.ts.map +1 -1
  128. package/dist/graph/algorithms/shortest-path.js +18 -1
  129. package/dist/graph/algorithms/shortest-path.js.map +1 -1
  130. package/dist/types.d.ts +122 -2
  131. package/dist/types.d.ts.map +1 -1
  132. package/grammars/tree-sitter-clojure.wasm +0 -0
  133. package/grammars/tree-sitter-cuda.wasm +0 -0
  134. package/grammars/tree-sitter-dart.wasm +0 -0
  135. package/grammars/tree-sitter-elixir.wasm +0 -0
  136. package/grammars/tree-sitter-erlang.wasm +0 -0
  137. package/grammars/tree-sitter-fsharp.wasm +0 -0
  138. package/grammars/tree-sitter-gleam.wasm +0 -0
  139. package/grammars/tree-sitter-groovy.wasm +0 -0
  140. package/grammars/tree-sitter-haskell.wasm +0 -0
  141. package/grammars/tree-sitter-julia.wasm +0 -0
  142. package/grammars/tree-sitter-lua.wasm +0 -0
  143. package/grammars/tree-sitter-objc.wasm +0 -0
  144. package/grammars/tree-sitter-ocaml.wasm +0 -0
  145. package/grammars/tree-sitter-ocaml_interface.wasm +0 -0
  146. package/grammars/tree-sitter-r.wasm +0 -0
  147. package/grammars/tree-sitter-solidity.wasm +0 -0
  148. package/grammars/tree-sitter-verilog.wasm +0 -0
  149. package/grammars/tree-sitter-zig.wasm +0 -0
  150. package/package.json +24 -7
  151. package/src/ast-analysis/engine.ts +183 -1
  152. package/src/ast-analysis/rules/javascript.ts +0 -1
  153. package/src/ast-analysis/visitors/ast-store-visitor.ts +2 -75
  154. package/src/cli/commands/ast.ts +2 -2
  155. package/src/domain/graph/builder/pipeline.ts +142 -6
  156. package/src/domain/graph/builder/stages/build-edges.ts +158 -1
  157. package/src/domain/graph/builder/stages/collect-files.ts +18 -7
  158. package/src/domain/graph/builder/stages/detect-changes.ts +109 -55
  159. package/src/domain/graph/builder/stages/finalize.ts +39 -9
  160. package/src/domain/graph/builder/stages/insert-nodes.ts +18 -7
  161. package/src/domain/parser.ts +161 -1
  162. package/src/extractors/clojure.ts +273 -0
  163. package/src/extractors/cuda.ts +316 -0
  164. package/src/extractors/dart.ts +304 -0
  165. package/src/extractors/elixir.ts +251 -0
  166. package/src/extractors/erlang.ts +252 -0
  167. package/src/extractors/fsharp.ts +253 -0
  168. package/src/extractors/gleam.ts +246 -0
  169. package/src/extractors/groovy.ts +332 -0
  170. package/src/extractors/haskell.ts +235 -0
  171. package/src/extractors/index.ts +17 -0
  172. package/src/extractors/julia.ts +318 -0
  173. package/src/extractors/lua.ts +169 -0
  174. package/src/extractors/objc.ts +431 -0
  175. package/src/extractors/ocaml.ts +337 -0
  176. package/src/extractors/r.ts +253 -0
  177. package/src/extractors/solidity.ts +398 -0
  178. package/src/extractors/verilog.ts +315 -0
  179. package/src/extractors/zig.ts +294 -0
  180. package/src/features/ast.ts +1 -2
  181. package/src/features/cfg.ts +6 -51
  182. package/src/graph/algorithms/bfs.ts +34 -0
  183. package/src/graph/algorithms/centrality.ts +30 -0
  184. package/src/graph/algorithms/louvain.ts +31 -4
  185. package/src/graph/algorithms/shortest-path.ts +20 -1
  186. package/src/types.ts +123 -2
@@ -0,0 +1,251 @@
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 Elixir files.
12
+ *
13
+ * Elixir's tree-sitter grammar represents most constructs as generic `call` nodes.
14
+ * We distinguish modules, functions, imports etc. by the call target's identifier text.
15
+ */
16
+ export function extractElixirSymbols(tree: TreeSitterTree, _filePath: string): ExtractorOutput {
17
+ const ctx: ExtractorOutput = {
18
+ definitions: [],
19
+ calls: [],
20
+ imports: [],
21
+ classes: [],
22
+ exports: [],
23
+ typeMap: new Map(),
24
+ };
25
+
26
+ walkElixirNode(tree.rootNode, ctx, null);
27
+ return ctx;
28
+ }
29
+
30
+ function walkElixirNode(
31
+ node: TreeSitterNode,
32
+ ctx: ExtractorOutput,
33
+ currentModule: string | null,
34
+ ): void {
35
+ let nextModule = currentModule;
36
+
37
+ if (node.type === 'call') {
38
+ const target = node.childForFieldName('target');
39
+ if (target?.type === 'identifier' && target.text === 'defmodule') {
40
+ const args = findChild(node, 'arguments');
41
+ const aliasNode = args && findChild(args, 'alias');
42
+ if (aliasNode) nextModule = aliasNode.text;
43
+ }
44
+ handleElixirCall(node, ctx, nextModule);
45
+ }
46
+
47
+ for (let i = 0; i < node.childCount; i++) {
48
+ const child = node.child(i);
49
+ if (child) walkElixirNode(child, ctx, nextModule);
50
+ }
51
+ }
52
+
53
+ function handleElixirCall(
54
+ node: TreeSitterNode,
55
+ ctx: ExtractorOutput,
56
+ currentModule: string | null,
57
+ ): void {
58
+ const target = node.childForFieldName('target');
59
+ if (!target) return;
60
+
61
+ if (target.type === 'identifier') {
62
+ const keyword = target.text;
63
+ switch (keyword) {
64
+ case 'defmodule':
65
+ handleDefmodule(node, ctx);
66
+ return;
67
+ case 'def':
68
+ case 'defp':
69
+ handleDefFunction(node, ctx, currentModule, keyword === 'defp' ? 'private' : 'public');
70
+ return;
71
+ case 'defprotocol':
72
+ handleDefprotocol(node, ctx);
73
+ return;
74
+ case 'defimpl':
75
+ handleDefimpl(node, ctx);
76
+ return;
77
+ case 'import':
78
+ case 'use':
79
+ case 'require':
80
+ case 'alias':
81
+ handleElixirImport(node, ctx, keyword);
82
+ return;
83
+ default:
84
+ // Regular function call
85
+ ctx.calls.push({ name: keyword, line: node.startPosition.row + 1 });
86
+ return;
87
+ }
88
+ }
89
+
90
+ if (target.type === 'dot') {
91
+ handleDotCall(node, target, ctx);
92
+ }
93
+ }
94
+
95
+ function handleDefmodule(node: TreeSitterNode, ctx: ExtractorOutput): void {
96
+ const args = findChild(node, 'arguments');
97
+ if (!args) return;
98
+ const aliasNode = findChild(args, 'alias');
99
+ if (!aliasNode) return;
100
+ const name = aliasNode.text;
101
+
102
+ const children: SubDeclaration[] = [];
103
+ const doBlock = findChild(node, 'do_block');
104
+ if (doBlock) {
105
+ collectModuleMembers(doBlock, ctx, name, children);
106
+ }
107
+
108
+ ctx.definitions.push({
109
+ name,
110
+ kind: 'module',
111
+ line: node.startPosition.row + 1,
112
+ endLine: nodeEndLine(node),
113
+ children: children.length > 0 ? children : undefined,
114
+ });
115
+ }
116
+
117
+ function collectModuleMembers(
118
+ doBlock: TreeSitterNode,
119
+ _ctx: ExtractorOutput,
120
+ _moduleName: string,
121
+ children: SubDeclaration[],
122
+ ): void {
123
+ for (let i = 0; i < doBlock.childCount; i++) {
124
+ const child = doBlock.child(i);
125
+ if (!child || child.type !== 'call') continue;
126
+ const target = child.childForFieldName('target');
127
+ if (!target || target.type !== 'identifier') continue;
128
+
129
+ if (target.text === 'def' || target.text === 'defp') {
130
+ const fnName = extractFunctionName(child);
131
+ if (fnName) {
132
+ children.push({
133
+ name: fnName,
134
+ kind: 'property',
135
+ line: child.startPosition.row + 1,
136
+ });
137
+ }
138
+ }
139
+ }
140
+ }
141
+
142
+ function handleDefFunction(
143
+ node: TreeSitterNode,
144
+ ctx: ExtractorOutput,
145
+ currentModule: string | null,
146
+ visibility: 'public' | 'private',
147
+ ): void {
148
+ const fnName = extractFunctionName(node);
149
+ if (!fnName) return;
150
+
151
+ const fullName = currentModule ? `${currentModule}.${fnName}` : fnName;
152
+ const params = extractElixirParams(node);
153
+
154
+ ctx.definitions.push({
155
+ name: fullName,
156
+ kind: 'function',
157
+ line: node.startPosition.row + 1,
158
+ endLine: nodeEndLine(node),
159
+ visibility,
160
+ children: params.length > 0 ? params : undefined,
161
+ });
162
+ }
163
+
164
+ function extractFunctionName(defCallNode: TreeSitterNode): string | null {
165
+ const args = findChild(defCallNode, 'arguments');
166
+ if (!args) return null;
167
+
168
+ for (let i = 0; i < args.childCount; i++) {
169
+ const child = args.child(i);
170
+ if (!child) continue;
171
+ if (child.type === 'call') {
172
+ const target = child.childForFieldName('target');
173
+ if (target?.type === 'identifier') return target.text;
174
+ }
175
+ if (child.type === 'identifier') return child.text;
176
+ }
177
+ return null;
178
+ }
179
+
180
+ function extractElixirParams(defCallNode: TreeSitterNode): SubDeclaration[] {
181
+ const params: SubDeclaration[] = [];
182
+ const args = findChild(defCallNode, 'arguments');
183
+ if (!args) return params;
184
+
185
+ for (let i = 0; i < args.childCount; i++) {
186
+ const child = args.child(i);
187
+ if (!child || child.type !== 'call') continue;
188
+ const innerArgs = findChild(child, 'arguments');
189
+ if (!innerArgs) continue;
190
+ for (let j = 0; j < innerArgs.childCount; j++) {
191
+ const param = innerArgs.child(j);
192
+ if (!param) continue;
193
+ if (param.type === 'identifier') {
194
+ params.push({ name: param.text, kind: 'parameter', line: param.startPosition.row + 1 });
195
+ }
196
+ }
197
+ }
198
+ return params;
199
+ }
200
+
201
+ function handleDefprotocol(node: TreeSitterNode, ctx: ExtractorOutput): void {
202
+ const args = findChild(node, 'arguments');
203
+ if (!args) return;
204
+ const aliasNode = findChild(args, 'alias');
205
+ if (!aliasNode) return;
206
+
207
+ ctx.definitions.push({
208
+ name: aliasNode.text,
209
+ kind: 'interface',
210
+ line: node.startPosition.row + 1,
211
+ endLine: nodeEndLine(node),
212
+ });
213
+ }
214
+
215
+ function handleDefimpl(node: TreeSitterNode, ctx: ExtractorOutput): void {
216
+ const args = findChild(node, 'arguments');
217
+ if (!args) return;
218
+ const aliasNode = findChild(args, 'alias');
219
+ if (!aliasNode) return;
220
+
221
+ ctx.definitions.push({
222
+ name: aliasNode.text,
223
+ kind: 'class',
224
+ line: node.startPosition.row + 1,
225
+ endLine: nodeEndLine(node),
226
+ });
227
+ }
228
+
229
+ function handleElixirImport(node: TreeSitterNode, ctx: ExtractorOutput, keyword: string): void {
230
+ const args = findChild(node, 'arguments');
231
+ if (!args) return;
232
+ const aliasNode = findChild(args, 'alias');
233
+ if (!aliasNode) return;
234
+
235
+ ctx.imports.push({
236
+ source: aliasNode.text,
237
+ names: [keyword],
238
+ line: node.startPosition.row + 1,
239
+ });
240
+ }
241
+
242
+ function handleDotCall(node: TreeSitterNode, dotNode: TreeSitterNode, ctx: ExtractorOutput): void {
243
+ const call: Call = { name: '', line: node.startPosition.row + 1 };
244
+ const right = findChild(dotNode, 'identifier');
245
+ const left = findChild(dotNode, 'alias');
246
+
247
+ if (right) call.name = right.text;
248
+ if (left) call.receiver = left.text;
249
+
250
+ if (call.name) ctx.calls.push(call);
251
+ }
@@ -0,0 +1,252 @@
1
+ import type { ExtractorOutput, SubDeclaration, TreeSitterNode, TreeSitterTree } from '../types.js';
2
+ import { findChild, nodeEndLine } from './helpers.js';
3
+
4
+ /**
5
+ * Extract symbols from Erlang files.
6
+ *
7
+ * tree-sitter-erlang (WhatsApp) grammar notes:
8
+ * - module_attribute: -module(name).
9
+ * - record_decl: -record(name, {fields}).
10
+ * - fun_decl: contains function_clause children
11
+ * - function_clause: atom expr_args clause_body
12
+ * - call: function calls, with remote child for module:func
13
+ * - expr_args: parenthesized argument lists
14
+ */
15
+ export function extractErlangSymbols(tree: TreeSitterTree, _filePath: string): ExtractorOutput {
16
+ const ctx: ExtractorOutput = {
17
+ definitions: [],
18
+ calls: [],
19
+ imports: [],
20
+ classes: [],
21
+ exports: [],
22
+ typeMap: new Map(),
23
+ };
24
+
25
+ walkErlangNode(tree.rootNode, ctx);
26
+ return ctx;
27
+ }
28
+
29
+ function walkErlangNode(node: TreeSitterNode, ctx: ExtractorOutput): void {
30
+ switch (node.type) {
31
+ case 'module_attribute':
32
+ handleModuleAttr(node, ctx);
33
+ break;
34
+ case 'record_decl':
35
+ handleRecordDecl(node, ctx);
36
+ break;
37
+ case 'type_alias':
38
+ case 'opaque':
39
+ handleTypeAlias(node, ctx);
40
+ break;
41
+ case 'fun_decl':
42
+ handleFunDecl(node, ctx);
43
+ break;
44
+ case 'function_clause':
45
+ // Only handle if not inside fun_decl (fun_decl handles its own clauses)
46
+ if (node.parent?.type !== 'fun_decl') {
47
+ handleFunctionClause(node, ctx);
48
+ }
49
+ break;
50
+ case 'pp_define':
51
+ handleDefine(node, ctx);
52
+ break;
53
+ case 'pp_include':
54
+ case 'pp_include_lib':
55
+ handleInclude(node, ctx);
56
+ break;
57
+ case 'import_attribute':
58
+ handleImportAttr(node, ctx);
59
+ break;
60
+ case 'call':
61
+ handleCall(node, ctx);
62
+ break;
63
+ }
64
+
65
+ for (let i = 0; i < node.childCount; i++) {
66
+ const child = node.child(i);
67
+ if (child) walkErlangNode(child, ctx);
68
+ }
69
+ }
70
+
71
+ function handleModuleAttr(node: TreeSitterNode, ctx: ExtractorOutput): void {
72
+ // module_attribute: - module ( atom ) .
73
+ const nameNode = findChild(node, 'atom');
74
+ if (!nameNode) return;
75
+
76
+ ctx.definitions.push({
77
+ name: nameNode.text,
78
+ kind: 'module',
79
+ line: node.startPosition.row + 1,
80
+ endLine: nodeEndLine(node),
81
+ });
82
+ }
83
+
84
+ function handleRecordDecl(node: TreeSitterNode, ctx: ExtractorOutput): void {
85
+ // record_decl: - record ( atom , { record_field, ... } ) .
86
+ const nameNode = findChild(node, 'atom');
87
+ if (!nameNode) return;
88
+
89
+ const children: SubDeclaration[] = [];
90
+ for (let i = 0; i < node.childCount; i++) {
91
+ const child = node.child(i);
92
+ if (!child) continue;
93
+ if (child.type === 'record_field' || child.type === 'typed_record_field') {
94
+ const fieldName = findChild(child, 'atom');
95
+ if (fieldName) {
96
+ children.push({
97
+ name: fieldName.text,
98
+ kind: 'property',
99
+ line: child.startPosition.row + 1,
100
+ });
101
+ }
102
+ }
103
+ }
104
+
105
+ ctx.definitions.push({
106
+ name: nameNode.text,
107
+ kind: 'record',
108
+ line: node.startPosition.row + 1,
109
+ endLine: nodeEndLine(node),
110
+ children: children.length > 0 ? children : undefined,
111
+ });
112
+ }
113
+
114
+ function handleTypeAlias(node: TreeSitterNode, ctx: ExtractorOutput): void {
115
+ const nameNode = findChild(node, 'atom');
116
+ if (!nameNode) return;
117
+
118
+ ctx.definitions.push({
119
+ name: nameNode.text,
120
+ kind: 'type',
121
+ line: node.startPosition.row + 1,
122
+ endLine: nodeEndLine(node),
123
+ });
124
+ }
125
+
126
+ function handleFunDecl(node: TreeSitterNode, ctx: ExtractorOutput): void {
127
+ // fun_decl contains one or more function_clause children + dots
128
+ // Extract from the first function_clause
129
+ const clause = findChild(node, 'function_clause');
130
+ if (!clause) return;
131
+
132
+ handleFunctionClause(clause, ctx);
133
+ }
134
+
135
+ function handleFunctionClause(node: TreeSitterNode, ctx: ExtractorOutput): void {
136
+ // function_clause: atom expr_args clause_body
137
+ const nameNode = findChild(node, 'atom');
138
+ if (!nameNode) return;
139
+
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
+ const params = extractErlangParams(node);
144
+
145
+ ctx.definitions.push({
146
+ name: nameNode.text,
147
+ kind: 'function',
148
+ line: node.startPosition.row + 1,
149
+ endLine: nodeEndLine(node.parent?.type === 'fun_decl' ? node.parent : node),
150
+ children: params.length > 0 ? params : undefined,
151
+ visibility: 'public',
152
+ });
153
+ }
154
+
155
+ function extractErlangParams(clauseNode: TreeSitterNode): SubDeclaration[] {
156
+ const params: SubDeclaration[] = [];
157
+ const argsNode = findChild(clauseNode, 'expr_args');
158
+ if (!argsNode) return params;
159
+
160
+ for (let i = 0; i < argsNode.childCount; i++) {
161
+ const child = argsNode.child(i);
162
+ 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
+ }
169
+ }
170
+ return params;
171
+ }
172
+
173
+ function handleDefine(node: TreeSitterNode, ctx: ExtractorOutput): void {
174
+ // pp_define: -define(NAME, value).
175
+ const nameNode =
176
+ findChild(node, 'var') || findChild(node, 'atom') || findChild(node, 'macro_lhs');
177
+ if (!nameNode) return;
178
+
179
+ const name =
180
+ nameNode.type === 'macro_lhs'
181
+ ? (findChild(nameNode, 'var')?.text ?? nameNode.text)
182
+ : nameNode.text;
183
+
184
+ ctx.definitions.push({
185
+ name,
186
+ kind: 'variable',
187
+ line: node.startPosition.row + 1,
188
+ endLine: nodeEndLine(node),
189
+ });
190
+ }
191
+
192
+ function handleInclude(node: TreeSitterNode, ctx: ExtractorOutput): void {
193
+ const strNode = findChild(node, 'string');
194
+ if (!strNode) return;
195
+
196
+ const source = strNode.text.replace(/^"|"$/g, '');
197
+ ctx.imports.push({
198
+ source,
199
+ names: ['include'],
200
+ line: node.startPosition.row + 1,
201
+ });
202
+ }
203
+
204
+ function handleImportAttr(node: TreeSitterNode, ctx: ExtractorOutput): void {
205
+ const moduleNode = findChild(node, 'atom');
206
+ if (!moduleNode) return;
207
+
208
+ const names: string[] = [];
209
+ // Find exported function names
210
+ for (let i = 0; i < node.childCount; i++) {
211
+ const child = node.child(i);
212
+ if (!child) continue;
213
+ if (child.type === 'fa') {
214
+ const fnName = findChild(child, 'atom');
215
+ if (fnName) names.push(fnName.text);
216
+ }
217
+ }
218
+
219
+ ctx.imports.push({
220
+ source: moduleNode.text,
221
+ names: names.length > 0 ? names : [moduleNode.text],
222
+ line: node.startPosition.row + 1,
223
+ });
224
+ }
225
+
226
+ 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);
229
+ if (!funcNode) return;
230
+
231
+ if (funcNode.type === 'atom' || funcNode.type === 'identifier') {
232
+ ctx.calls.push({ name: funcNode.text, line: node.startPosition.row + 1 });
233
+ } else if (funcNode.type === 'remote') {
234
+ // module:function — remote has atom : atom children
235
+ const atoms: string[] = [];
236
+ for (let i = 0; i < funcNode.childCount; i++) {
237
+ const child = funcNode.child(i);
238
+ if (child && (child.type === 'atom' || child.type === 'var')) {
239
+ atoms.push(child.text);
240
+ }
241
+ }
242
+ if (atoms.length >= 2) {
243
+ ctx.calls.push({
244
+ name: atoms[atoms.length - 1]!,
245
+ receiver: atoms.slice(0, -1).join(':'),
246
+ line: node.startPosition.row + 1,
247
+ });
248
+ } else if (atoms.length === 1) {
249
+ ctx.calls.push({ name: atoms[0]!, line: node.startPosition.row + 1 });
250
+ }
251
+ }
252
+ }