@optave/codegraph 3.7.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 (148) hide show
  1. package/README.md +25 -14
  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 +88 -4
  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/erlang.d.ts +14 -0
  44. package/dist/extractors/erlang.d.ts.map +1 -0
  45. package/dist/extractors/erlang.js +239 -0
  46. package/dist/extractors/erlang.js.map +1 -0
  47. package/dist/extractors/fsharp.d.ts +13 -0
  48. package/dist/extractors/fsharp.d.ts.map +1 -0
  49. package/dist/extractors/fsharp.js +218 -0
  50. package/dist/extractors/fsharp.js.map +1 -0
  51. package/dist/extractors/gleam.d.ts +14 -0
  52. package/dist/extractors/gleam.d.ts.map +1 -0
  53. package/dist/extractors/gleam.js +229 -0
  54. package/dist/extractors/gleam.js.map +1 -0
  55. package/dist/extractors/groovy.d.ts +10 -0
  56. package/dist/extractors/groovy.d.ts.map +1 -0
  57. package/dist/extractors/groovy.js +304 -0
  58. package/dist/extractors/groovy.js.map +1 -0
  59. package/dist/extractors/index.d.ts +11 -0
  60. package/dist/extractors/index.d.ts.map +1 -1
  61. package/dist/extractors/index.js +11 -0
  62. package/dist/extractors/index.js.map +1 -1
  63. package/dist/extractors/julia.d.ts +16 -0
  64. package/dist/extractors/julia.d.ts.map +1 -0
  65. package/dist/extractors/julia.js +287 -0
  66. package/dist/extractors/julia.js.map +1 -0
  67. package/dist/extractors/objc.d.ts +9 -0
  68. package/dist/extractors/objc.d.ts.map +1 -0
  69. package/dist/extractors/objc.js +406 -0
  70. package/dist/extractors/objc.js.map +1 -0
  71. package/dist/extractors/ocaml.js +74 -0
  72. package/dist/extractors/ocaml.js.map +1 -1
  73. package/dist/extractors/r.d.ts +13 -0
  74. package/dist/extractors/r.d.ts.map +1 -0
  75. package/dist/extractors/r.js +251 -0
  76. package/dist/extractors/r.js.map +1 -0
  77. package/dist/extractors/solidity.d.ts +9 -0
  78. package/dist/extractors/solidity.d.ts.map +1 -0
  79. package/dist/extractors/solidity.js +374 -0
  80. package/dist/extractors/solidity.js.map +1 -0
  81. package/dist/extractors/verilog.d.ts +9 -0
  82. package/dist/extractors/verilog.d.ts.map +1 -0
  83. package/dist/extractors/verilog.js +286 -0
  84. package/dist/extractors/verilog.js.map +1 -0
  85. package/dist/features/ast.d.ts.map +1 -1
  86. package/dist/features/ast.js +1 -2
  87. package/dist/features/ast.js.map +1 -1
  88. package/dist/graph/algorithms/bfs.d.ts +2 -0
  89. package/dist/graph/algorithms/bfs.d.ts.map +1 -1
  90. package/dist/graph/algorithms/bfs.js +27 -0
  91. package/dist/graph/algorithms/bfs.js.map +1 -1
  92. package/dist/graph/algorithms/centrality.d.ts +2 -0
  93. package/dist/graph/algorithms/centrality.d.ts.map +1 -1
  94. package/dist/graph/algorithms/centrality.js +28 -0
  95. package/dist/graph/algorithms/centrality.js.map +1 -1
  96. package/dist/graph/algorithms/louvain.d.ts +3 -4
  97. package/dist/graph/algorithms/louvain.d.ts.map +1 -1
  98. package/dist/graph/algorithms/louvain.js +29 -0
  99. package/dist/graph/algorithms/louvain.js.map +1 -1
  100. package/dist/graph/algorithms/shortest-path.d.ts +2 -0
  101. package/dist/graph/algorithms/shortest-path.d.ts.map +1 -1
  102. package/dist/graph/algorithms/shortest-path.js +18 -1
  103. package/dist/graph/algorithms/shortest-path.js.map +1 -1
  104. package/dist/types.d.ts +122 -2
  105. package/dist/types.d.ts.map +1 -1
  106. package/grammars/tree-sitter-clojure.wasm +0 -0
  107. package/grammars/tree-sitter-cuda.wasm +0 -0
  108. package/grammars/tree-sitter-erlang.wasm +0 -0
  109. package/grammars/tree-sitter-fsharp.wasm +0 -0
  110. package/grammars/tree-sitter-gleam.wasm +0 -0
  111. package/grammars/tree-sitter-groovy.wasm +0 -0
  112. package/grammars/tree-sitter-julia.wasm +0 -0
  113. package/grammars/tree-sitter-objc.wasm +0 -0
  114. package/grammars/tree-sitter-ocaml_interface.wasm +0 -0
  115. package/grammars/tree-sitter-r.wasm +0 -0
  116. package/grammars/tree-sitter-solidity.wasm +0 -0
  117. package/grammars/tree-sitter-verilog.wasm +0 -0
  118. package/package.json +18 -7
  119. package/src/ast-analysis/engine.ts +183 -1
  120. package/src/ast-analysis/rules/javascript.ts +0 -1
  121. package/src/ast-analysis/visitors/ast-store-visitor.ts +2 -75
  122. package/src/cli/commands/ast.ts +2 -2
  123. package/src/domain/graph/builder/pipeline.ts +142 -6
  124. package/src/domain/graph/builder/stages/build-edges.ts +158 -1
  125. package/src/domain/graph/builder/stages/collect-files.ts +18 -7
  126. package/src/domain/graph/builder/stages/detect-changes.ts +109 -55
  127. package/src/domain/graph/builder/stages/finalize.ts +39 -9
  128. package/src/domain/graph/builder/stages/insert-nodes.ts +18 -7
  129. package/src/domain/parser.ts +108 -2
  130. package/src/extractors/clojure.ts +273 -0
  131. package/src/extractors/cuda.ts +316 -0
  132. package/src/extractors/erlang.ts +252 -0
  133. package/src/extractors/fsharp.ts +253 -0
  134. package/src/extractors/gleam.ts +246 -0
  135. package/src/extractors/groovy.ts +332 -0
  136. package/src/extractors/index.ts +11 -0
  137. package/src/extractors/julia.ts +318 -0
  138. package/src/extractors/objc.ts +431 -0
  139. package/src/extractors/ocaml.ts +78 -0
  140. package/src/extractors/r.ts +253 -0
  141. package/src/extractors/solidity.ts +398 -0
  142. package/src/extractors/verilog.ts +315 -0
  143. package/src/features/ast.ts +1 -2
  144. package/src/graph/algorithms/bfs.ts +34 -0
  145. package/src/graph/algorithms/centrality.ts +30 -0
  146. package/src/graph/algorithms/louvain.ts +31 -4
  147. package/src/graph/algorithms/shortest-path.ts +20 -1
  148. package/src/types.ts +117 -2
@@ -0,0 +1,316 @@
1
+ import type {
2
+ Call,
3
+ ExtractorOutput,
4
+ SubDeclaration,
5
+ TreeSitterNode,
6
+ TreeSitterTree,
7
+ } from '../types.js';
8
+ import { extractModifierVisibility, findChild, nodeEndLine } from './helpers.js';
9
+
10
+ /**
11
+ * Extract symbols from CUDA files.
12
+ *
13
+ * CUDA is a C++ superset. The tree-sitter-cuda grammar extends C++ with
14
+ * __global__, __device__, __host__, __shared__ qualifiers and kernel
15
+ * launch syntax (<<<...>>>). We reuse C++ handler patterns and add
16
+ * CUDA-specific qualifier detection.
17
+ */
18
+ export function extractCudaSymbols(tree: TreeSitterTree, _filePath: string): ExtractorOutput {
19
+ const ctx: ExtractorOutput = {
20
+ definitions: [],
21
+ calls: [],
22
+ imports: [],
23
+ classes: [],
24
+ exports: [],
25
+ typeMap: new Map(),
26
+ };
27
+
28
+ walkCudaNode(tree.rootNode, ctx);
29
+ return ctx;
30
+ }
31
+
32
+ const CUDA_QUALIFIERS = new Set([
33
+ '__global__',
34
+ '__device__',
35
+ '__host__',
36
+ '__shared__',
37
+ '__constant__',
38
+ ]);
39
+
40
+ function walkCudaNode(node: TreeSitterNode, ctx: ExtractorOutput): void {
41
+ switch (node.type) {
42
+ case 'function_definition':
43
+ handleCudaFunctionDef(node, ctx);
44
+ break;
45
+ case 'class_specifier':
46
+ handleCudaClassSpecifier(node, ctx);
47
+ break;
48
+ case 'struct_specifier':
49
+ handleCudaStructSpecifier(node, ctx);
50
+ break;
51
+ case 'enum_specifier':
52
+ handleCudaEnumSpecifier(node, ctx);
53
+ break;
54
+ case 'namespace_definition':
55
+ handleCudaNamespaceDef(node, ctx);
56
+ break;
57
+ case 'type_definition':
58
+ handleCudaTypedef(node, ctx);
59
+ break;
60
+ case 'preproc_include':
61
+ handleCudaInclude(node, ctx);
62
+ break;
63
+ case 'call_expression':
64
+ handleCudaCallExpression(node, ctx);
65
+ break;
66
+ }
67
+
68
+ for (let i = 0; i < node.childCount; i++) {
69
+ const child = node.child(i);
70
+ if (child) walkCudaNode(child, ctx);
71
+ }
72
+ }
73
+
74
+ // ── Handlers ───────────────────────────────────────────────────────────────
75
+
76
+ function handleCudaFunctionDef(node: TreeSitterNode, ctx: ExtractorOutput): void {
77
+ const declarator = node.childForFieldName('declarator');
78
+ if (!declarator) return;
79
+ const funcDeclarator =
80
+ declarator.type === 'function_declarator'
81
+ ? declarator
82
+ : findChild(declarator, 'function_declarator');
83
+ if (!funcDeclarator) return;
84
+ const nameNode = funcDeclarator.childForFieldName('declarator');
85
+ if (!nameNode) return;
86
+ const name = nameNode.text;
87
+
88
+ const parentClass = findCudaParentClass(node);
89
+ const fullName = parentClass ? `${parentClass}.${name}` : name;
90
+ const kind = parentClass ? 'method' : 'function';
91
+
92
+ const params = extractCudaParameters(funcDeclarator.childForFieldName('parameters'));
93
+ const decorators = extractCudaQualifiers(node);
94
+
95
+ ctx.definitions.push({
96
+ name: fullName,
97
+ kind,
98
+ line: node.startPosition.row + 1,
99
+ endLine: nodeEndLine(node),
100
+ children: params.length > 0 ? params : undefined,
101
+ visibility: parentClass ? extractModifierVisibility(node) : undefined,
102
+ decorators: decorators.length > 0 ? decorators : undefined,
103
+ });
104
+ }
105
+
106
+ function handleCudaClassSpecifier(node: TreeSitterNode, ctx: ExtractorOutput): void {
107
+ const nameNode = node.childForFieldName('name');
108
+ if (!nameNode) return;
109
+ const children = extractCudaClassFields(node);
110
+ ctx.definitions.push({
111
+ name: nameNode.text,
112
+ kind: 'class',
113
+ line: node.startPosition.row + 1,
114
+ endLine: nodeEndLine(node),
115
+ children: children.length > 0 ? children : undefined,
116
+ });
117
+
118
+ const baseClause = findChild(node, 'base_class_clause');
119
+ if (baseClause) {
120
+ for (let i = 0; i < baseClause.childCount; i++) {
121
+ const child = baseClause.child(i);
122
+ if (child && (child.type === 'type_identifier' || child.type === 'qualified_identifier')) {
123
+ ctx.classes.push({
124
+ name: nameNode.text,
125
+ extends: child.text,
126
+ line: node.startPosition.row + 1,
127
+ });
128
+ }
129
+ }
130
+ }
131
+ }
132
+
133
+ function handleCudaStructSpecifier(node: TreeSitterNode, ctx: ExtractorOutput): void {
134
+ const nameNode = node.childForFieldName('name');
135
+ if (!nameNode) return;
136
+ const children = extractCudaClassFields(node);
137
+ ctx.definitions.push({
138
+ name: nameNode.text,
139
+ kind: 'struct',
140
+ line: node.startPosition.row + 1,
141
+ endLine: nodeEndLine(node),
142
+ children: children.length > 0 ? children : undefined,
143
+ });
144
+ }
145
+
146
+ function handleCudaEnumSpecifier(node: TreeSitterNode, ctx: ExtractorOutput): void {
147
+ const nameNode = node.childForFieldName('name');
148
+ if (!nameNode) return;
149
+ const children = extractCudaEnumEntries(node);
150
+ ctx.definitions.push({
151
+ name: nameNode.text,
152
+ kind: 'enum',
153
+ line: node.startPosition.row + 1,
154
+ endLine: nodeEndLine(node),
155
+ children: children.length > 0 ? children : undefined,
156
+ });
157
+ }
158
+
159
+ function handleCudaNamespaceDef(node: TreeSitterNode, ctx: ExtractorOutput): void {
160
+ const nameNode = node.childForFieldName('name');
161
+ if (!nameNode) return;
162
+ ctx.definitions.push({
163
+ name: nameNode.text,
164
+ kind: 'namespace',
165
+ line: node.startPosition.row + 1,
166
+ endLine: nodeEndLine(node),
167
+ });
168
+ }
169
+
170
+ function handleCudaTypedef(node: TreeSitterNode, ctx: ExtractorOutput): void {
171
+ let name: string | undefined;
172
+ for (let i = node.childCount - 1; i >= 0; i--) {
173
+ const child = node.child(i);
174
+ if (
175
+ child &&
176
+ (child.type === 'type_identifier' ||
177
+ child.type === 'identifier' ||
178
+ child.type === 'primitive_type')
179
+ ) {
180
+ name = child.text;
181
+ break;
182
+ }
183
+ }
184
+ if (!name) return;
185
+ ctx.definitions.push({
186
+ name,
187
+ kind: 'type',
188
+ line: node.startPosition.row + 1,
189
+ endLine: nodeEndLine(node),
190
+ });
191
+ }
192
+
193
+ function handleCudaInclude(node: TreeSitterNode, ctx: ExtractorOutput): void {
194
+ const pathNode = node.childForFieldName('path');
195
+ if (!pathNode) return;
196
+ const raw = pathNode.text;
197
+ const source = raw.replace(/^["<]|[">]$/g, '');
198
+ const lastName = source.split('/').pop() ?? source;
199
+ ctx.imports.push({
200
+ source,
201
+ names: [lastName],
202
+ line: node.startPosition.row + 1,
203
+ cInclude: true,
204
+ });
205
+ }
206
+
207
+ function handleCudaCallExpression(node: TreeSitterNode, ctx: ExtractorOutput): void {
208
+ const funcNode = node.childForFieldName('function');
209
+ if (!funcNode) return;
210
+ const call: Call = { name: '', line: node.startPosition.row + 1 };
211
+ if (funcNode.type === 'field_expression') {
212
+ const field = funcNode.childForFieldName('field');
213
+ const argument = funcNode.childForFieldName('argument');
214
+ if (field) call.name = field.text;
215
+ if (argument) call.receiver = argument.text;
216
+ } else {
217
+ call.name = funcNode.text;
218
+ }
219
+ if (call.name) ctx.calls.push(call);
220
+ }
221
+
222
+ // ── Helpers ────────────────────────────────────────────────────────────────
223
+
224
+ function extractCudaQualifiers(node: TreeSitterNode): string[] {
225
+ const qualifiers: string[] = [];
226
+ for (let i = 0; i < node.childCount; i++) {
227
+ const child = node.child(i);
228
+ if (!child) continue;
229
+ // Check direct text match for bare qualifier tokens, or look inside
230
+ // storage_class_specifier / attribute_specifier wrapper nodes.
231
+ // Use `else if` to avoid pushing the same qualifier twice when
232
+ // wrapper-node text also matches CUDA_QUALIFIERS directly.
233
+ if (child.type === 'storage_class_specifier' || child.type === 'attribute_specifier') {
234
+ if (CUDA_QUALIFIERS.has(child.text)) qualifiers.push(child.text);
235
+ } else if (CUDA_QUALIFIERS.has(child.text)) {
236
+ qualifiers.push(child.text);
237
+ }
238
+ }
239
+ return qualifiers;
240
+ }
241
+
242
+ function findCudaParentClass(node: TreeSitterNode): string | null {
243
+ let current = node.parent;
244
+ while (current) {
245
+ if (current.type === 'field_declaration_list') {
246
+ const classNode = current.parent;
247
+ if (
248
+ classNode &&
249
+ (classNode.type === 'class_specifier' || classNode.type === 'struct_specifier')
250
+ ) {
251
+ const nameNode = classNode.childForFieldName('name');
252
+ return nameNode ? nameNode.text : null;
253
+ }
254
+ }
255
+ current = current.parent;
256
+ }
257
+ return null;
258
+ }
259
+
260
+ function extractCudaParameters(paramListNode: TreeSitterNode | null): SubDeclaration[] {
261
+ const params: SubDeclaration[] = [];
262
+ if (!paramListNode) return params;
263
+ for (let i = 0; i < paramListNode.childCount; i++) {
264
+ const param = paramListNode.child(i);
265
+ if (!param || param.type !== 'parameter_declaration') continue;
266
+ const nameNode = param.childForFieldName('declarator');
267
+ if (nameNode) {
268
+ const name =
269
+ nameNode.type === 'identifier'
270
+ ? nameNode.text
271
+ : (findChild(nameNode, 'identifier')?.text ?? nameNode.text);
272
+ params.push({ name, kind: 'parameter', line: param.startPosition.row + 1 });
273
+ }
274
+ }
275
+ return params;
276
+ }
277
+
278
+ function extractCudaClassFields(classNode: TreeSitterNode): SubDeclaration[] {
279
+ const fields: SubDeclaration[] = [];
280
+ const body =
281
+ classNode.childForFieldName('body') || findChild(classNode, 'field_declaration_list');
282
+ if (!body) return fields;
283
+ for (let i = 0; i < body.childCount; i++) {
284
+ const member = body.child(i);
285
+ if (!member || member.type !== 'field_declaration') continue;
286
+ const nameNode = member.childForFieldName('declarator');
287
+ if (nameNode) {
288
+ const name =
289
+ nameNode.type === 'identifier'
290
+ ? nameNode.text
291
+ : (findChild(nameNode, 'identifier')?.text ?? nameNode.text);
292
+ fields.push({
293
+ name,
294
+ kind: 'property',
295
+ line: member.startPosition.row + 1,
296
+ visibility: extractModifierVisibility(member),
297
+ });
298
+ }
299
+ }
300
+ return fields;
301
+ }
302
+
303
+ function extractCudaEnumEntries(enumNode: TreeSitterNode): SubDeclaration[] {
304
+ const entries: SubDeclaration[] = [];
305
+ const body = findChild(enumNode, 'enumerator_list');
306
+ if (!body) return entries;
307
+ for (let i = 0; i < body.childCount; i++) {
308
+ const member = body.child(i);
309
+ if (!member || member.type !== 'enumerator') continue;
310
+ const nameNode = member.childForFieldName('name');
311
+ if (nameNode) {
312
+ entries.push({ name: nameNode.text, kind: 'constant', line: member.startPosition.row + 1 });
313
+ }
314
+ }
315
+ return entries;
316
+ }
@@ -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
+ }