@optave/codegraph 3.6.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.
- package/README.md +20 -15
- package/dist/domain/parser.d.ts +1 -1
- package/dist/domain/parser.d.ts.map +1 -1
- package/dist/domain/parser.js +44 -2
- package/dist/domain/parser.js.map +1 -1
- package/dist/extractors/dart.d.ts +6 -0
- package/dist/extractors/dart.d.ts.map +1 -0
- package/dist/extractors/dart.js +277 -0
- package/dist/extractors/dart.js.map +1 -0
- package/dist/extractors/elixir.d.ts +9 -0
- package/dist/extractors/elixir.d.ts.map +1 -0
- package/dist/extractors/elixir.js +223 -0
- package/dist/extractors/elixir.js.map +1 -0
- package/dist/extractors/haskell.d.ts +8 -0
- package/dist/extractors/haskell.d.ts.map +1 -0
- package/dist/extractors/haskell.js +217 -0
- package/dist/extractors/haskell.js.map +1 -0
- package/dist/extractors/index.d.ts +6 -0
- package/dist/extractors/index.d.ts.map +1 -1
- package/dist/extractors/index.js +6 -0
- package/dist/extractors/index.js.map +1 -1
- package/dist/extractors/lua.d.ts +6 -0
- package/dist/extractors/lua.d.ts.map +1 -0
- package/dist/extractors/lua.js +162 -0
- package/dist/extractors/lua.js.map +1 -0
- package/dist/extractors/ocaml.d.ts +6 -0
- package/dist/extractors/ocaml.d.ts.map +1 -0
- package/dist/extractors/ocaml.js +236 -0
- package/dist/extractors/ocaml.js.map +1 -0
- package/dist/extractors/zig.d.ts +9 -0
- package/dist/extractors/zig.d.ts.map +1 -0
- package/dist/extractors/zig.js +276 -0
- package/dist/extractors/zig.js.map +1 -0
- package/dist/features/cfg.d.ts +1 -1
- package/dist/features/cfg.d.ts.map +1 -1
- package/dist/features/cfg.js +6 -51
- package/dist/features/cfg.js.map +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/grammars/tree-sitter-dart.wasm +0 -0
- package/grammars/tree-sitter-elixir.wasm +0 -0
- package/grammars/tree-sitter-haskell.wasm +0 -0
- package/grammars/tree-sitter-lua.wasm +0 -0
- package/grammars/tree-sitter-ocaml.wasm +0 -0
- package/grammars/tree-sitter-zig.wasm +0 -0
- package/package.json +13 -7
- package/src/domain/parser.ts +54 -0
- package/src/extractors/dart.ts +304 -0
- package/src/extractors/elixir.ts +251 -0
- package/src/extractors/haskell.ts +235 -0
- package/src/extractors/index.ts +6 -0
- package/src/extractors/lua.ts +169 -0
- package/src/extractors/ocaml.ts +259 -0
- package/src/extractors/zig.ts +294 -0
- package/src/features/cfg.ts +6 -51
- package/src/types.ts +7 -1
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import type { ExtractorOutput, SubDeclaration, TreeSitterNode, TreeSitterTree } from '../types.js';
|
|
2
|
+
import { findChild, nodeEndLine } from './helpers.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Extract symbols from Haskell files.
|
|
6
|
+
*
|
|
7
|
+
* Note: tree-sitter-haskell uses `type_synomym` (misspelled) for type aliases.
|
|
8
|
+
*/
|
|
9
|
+
export function extractHaskellSymbols(tree: TreeSitterTree, _filePath: string): ExtractorOutput {
|
|
10
|
+
const ctx: ExtractorOutput = {
|
|
11
|
+
definitions: [],
|
|
12
|
+
calls: [],
|
|
13
|
+
imports: [],
|
|
14
|
+
classes: [],
|
|
15
|
+
exports: [],
|
|
16
|
+
typeMap: new Map(),
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
walkHaskellNode(tree.rootNode, ctx);
|
|
20
|
+
return ctx;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function walkHaskellNode(node: TreeSitterNode, ctx: ExtractorOutput): void {
|
|
24
|
+
switch (node.type) {
|
|
25
|
+
case 'function':
|
|
26
|
+
handleHaskellFunction(node, ctx);
|
|
27
|
+
break;
|
|
28
|
+
case 'bind':
|
|
29
|
+
handleHaskellBind(node, ctx);
|
|
30
|
+
break;
|
|
31
|
+
case 'data_type':
|
|
32
|
+
handleHaskellDataType(node, ctx);
|
|
33
|
+
break;
|
|
34
|
+
case 'newtype':
|
|
35
|
+
handleHaskellNewtype(node, ctx);
|
|
36
|
+
break;
|
|
37
|
+
case 'type_synomym':
|
|
38
|
+
handleHaskellTypeSynonym(node, ctx);
|
|
39
|
+
break;
|
|
40
|
+
case 'class':
|
|
41
|
+
handleHaskellClass(node, ctx);
|
|
42
|
+
break;
|
|
43
|
+
case 'instance':
|
|
44
|
+
handleHaskellInstance(node, ctx);
|
|
45
|
+
break;
|
|
46
|
+
case 'import':
|
|
47
|
+
handleHaskellImport(node, ctx);
|
|
48
|
+
break;
|
|
49
|
+
case 'apply':
|
|
50
|
+
handleHaskellApply(node, ctx);
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
55
|
+
const child = node.child(i);
|
|
56
|
+
if (child) walkHaskellNode(child, ctx);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function handleHaskellFunction(node: TreeSitterNode, ctx: ExtractorOutput): void {
|
|
61
|
+
const nameNode = node.childForFieldName('name');
|
|
62
|
+
if (!nameNode) return;
|
|
63
|
+
|
|
64
|
+
const params = extractHaskellParams(node);
|
|
65
|
+
|
|
66
|
+
ctx.definitions.push({
|
|
67
|
+
name: nameNode.text,
|
|
68
|
+
kind: 'function',
|
|
69
|
+
line: node.startPosition.row + 1,
|
|
70
|
+
endLine: nodeEndLine(node),
|
|
71
|
+
children: params.length > 0 ? params : undefined,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function extractHaskellParams(funcNode: TreeSitterNode): SubDeclaration[] {
|
|
76
|
+
const params: SubDeclaration[] = [];
|
|
77
|
+
// Haskell function patterns are positional children
|
|
78
|
+
for (let i = 0; i < funcNode.childCount; i++) {
|
|
79
|
+
const child = funcNode.child(i);
|
|
80
|
+
if (!child) continue;
|
|
81
|
+
if (child.type === 'patterns' || child.type === 'parameter') {
|
|
82
|
+
for (let j = 0; j < child.childCount; j++) {
|
|
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
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (child.type === 'variable' && i > 0) {
|
|
90
|
+
// Pattern parameters after the function name
|
|
91
|
+
params.push({ name: child.text, kind: 'parameter', line: child.startPosition.row + 1 });
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return params;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function handleHaskellBind(node: TreeSitterNode, ctx: ExtractorOutput): void {
|
|
98
|
+
const nameNode = node.childForFieldName('name');
|
|
99
|
+
if (!nameNode) return;
|
|
100
|
+
|
|
101
|
+
ctx.definitions.push({
|
|
102
|
+
name: nameNode.text,
|
|
103
|
+
kind: 'variable',
|
|
104
|
+
line: node.startPosition.row + 1,
|
|
105
|
+
endLine: nodeEndLine(node),
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function handleHaskellDataType(node: TreeSitterNode, ctx: ExtractorOutput): void {
|
|
110
|
+
const nameNode = node.childForFieldName('name');
|
|
111
|
+
if (!nameNode) return;
|
|
112
|
+
const name = nameNode.text;
|
|
113
|
+
|
|
114
|
+
const children: SubDeclaration[] = [];
|
|
115
|
+
// Extract constructors
|
|
116
|
+
const constructors = node.childForFieldName('constructors');
|
|
117
|
+
if (constructors) {
|
|
118
|
+
for (let i = 0; i < constructors.childCount; i++) {
|
|
119
|
+
const ctor = constructors.child(i);
|
|
120
|
+
if (!ctor) continue;
|
|
121
|
+
if (ctor.type === 'data_constructor' || ctor.type === 'gadt_constructor') {
|
|
122
|
+
const ctorName = findChild(ctor, 'constructor') || findChild(ctor, 'constructor_operator');
|
|
123
|
+
if (ctorName) {
|
|
124
|
+
children.push({
|
|
125
|
+
name: ctorName.text,
|
|
126
|
+
kind: 'property',
|
|
127
|
+
line: ctor.startPosition.row + 1,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
ctx.definitions.push({
|
|
135
|
+
name,
|
|
136
|
+
kind: 'type',
|
|
137
|
+
line: node.startPosition.row + 1,
|
|
138
|
+
endLine: nodeEndLine(node),
|
|
139
|
+
children: children.length > 0 ? children : undefined,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function handleHaskellNewtype(node: TreeSitterNode, ctx: ExtractorOutput): void {
|
|
144
|
+
const nameNode = node.childForFieldName('name');
|
|
145
|
+
if (!nameNode) return;
|
|
146
|
+
|
|
147
|
+
ctx.definitions.push({
|
|
148
|
+
name: nameNode.text,
|
|
149
|
+
kind: 'type',
|
|
150
|
+
line: node.startPosition.row + 1,
|
|
151
|
+
endLine: nodeEndLine(node),
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function handleHaskellTypeSynonym(node: TreeSitterNode, ctx: ExtractorOutput): void {
|
|
156
|
+
const nameNode = node.childForFieldName('name');
|
|
157
|
+
if (!nameNode) return;
|
|
158
|
+
|
|
159
|
+
ctx.definitions.push({
|
|
160
|
+
name: nameNode.text,
|
|
161
|
+
kind: 'type',
|
|
162
|
+
line: node.startPosition.row + 1,
|
|
163
|
+
endLine: nodeEndLine(node),
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function handleHaskellClass(node: TreeSitterNode, ctx: ExtractorOutput): void {
|
|
168
|
+
const nameNode = node.childForFieldName('name');
|
|
169
|
+
if (!nameNode) return;
|
|
170
|
+
|
|
171
|
+
ctx.definitions.push({
|
|
172
|
+
name: nameNode.text,
|
|
173
|
+
kind: 'class',
|
|
174
|
+
line: node.startPosition.row + 1,
|
|
175
|
+
endLine: nodeEndLine(node),
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function handleHaskellInstance(node: TreeSitterNode, ctx: ExtractorOutput): void {
|
|
180
|
+
const nameNode = node.childForFieldName('name');
|
|
181
|
+
if (!nameNode) return;
|
|
182
|
+
|
|
183
|
+
ctx.definitions.push({
|
|
184
|
+
name: nameNode.text,
|
|
185
|
+
kind: 'class',
|
|
186
|
+
line: node.startPosition.row + 1,
|
|
187
|
+
endLine: nodeEndLine(node),
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function handleHaskellImport(node: TreeSitterNode, ctx: ExtractorOutput): void {
|
|
192
|
+
const moduleNode = node.childForFieldName('module');
|
|
193
|
+
if (!moduleNode) return;
|
|
194
|
+
|
|
195
|
+
const source = moduleNode.text;
|
|
196
|
+
const names: string[] = [];
|
|
197
|
+
|
|
198
|
+
const alias = node.childForFieldName('alias');
|
|
199
|
+
if (alias) names.push(alias.text);
|
|
200
|
+
|
|
201
|
+
const importList = node.childForFieldName('names');
|
|
202
|
+
if (importList) {
|
|
203
|
+
for (let i = 0; i < importList.childCount; i++) {
|
|
204
|
+
const item = importList.child(i);
|
|
205
|
+
if (
|
|
206
|
+
item &&
|
|
207
|
+
(item.type === 'variable' || item.type === 'constructor' || item.type === 'type')
|
|
208
|
+
) {
|
|
209
|
+
names.push(item.text);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
ctx.imports.push({
|
|
215
|
+
source,
|
|
216
|
+
names: names.length > 0 ? names : [source.split('.').pop() || source],
|
|
217
|
+
line: node.startPosition.row + 1,
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function handleHaskellApply(node: TreeSitterNode, ctx: ExtractorOutput): void {
|
|
222
|
+
const funcNode = node.childForFieldName('function');
|
|
223
|
+
if (!funcNode) return;
|
|
224
|
+
|
|
225
|
+
// Only record named function applications, not complex expressions
|
|
226
|
+
if (
|
|
227
|
+
funcNode.type === 'variable' ||
|
|
228
|
+
funcNode.type === 'constructor' ||
|
|
229
|
+
funcNode.type === 'identifier'
|
|
230
|
+
) {
|
|
231
|
+
ctx.calls.push({ name: funcNode.text, line: node.startPosition.row + 1 });
|
|
232
|
+
} else if (funcNode.type === 'qualified_variable' || funcNode.type === 'qualified_constructor') {
|
|
233
|
+
ctx.calls.push({ name: funcNode.text, line: node.startPosition.row + 1 });
|
|
234
|
+
}
|
|
235
|
+
}
|
package/src/extractors/index.ts
CHANGED
|
@@ -2,14 +2,20 @@ export { extractBashSymbols } from './bash.js';
|
|
|
2
2
|
export { extractCSymbols } from './c.js';
|
|
3
3
|
export { extractCppSymbols } from './cpp.js';
|
|
4
4
|
export { extractCSharpSymbols } from './csharp.js';
|
|
5
|
+
export { extractDartSymbols } from './dart.js';
|
|
6
|
+
export { extractElixirSymbols } from './elixir.js';
|
|
5
7
|
export { extractGoSymbols } from './go.js';
|
|
8
|
+
export { extractHaskellSymbols } from './haskell.js';
|
|
6
9
|
export { extractHCLSymbols } from './hcl.js';
|
|
7
10
|
export { extractJavaSymbols } from './java.js';
|
|
8
11
|
export { extractSymbols } from './javascript.js';
|
|
9
12
|
export { extractKotlinSymbols } from './kotlin.js';
|
|
13
|
+
export { extractLuaSymbols } from './lua.js';
|
|
14
|
+
export { extractOCamlSymbols } from './ocaml.js';
|
|
10
15
|
export { extractPHPSymbols } from './php.js';
|
|
11
16
|
export { extractPythonSymbols } from './python.js';
|
|
12
17
|
export { extractRubySymbols } from './ruby.js';
|
|
13
18
|
export { extractRustSymbols } from './rust.js';
|
|
14
19
|
export { extractScalaSymbols } from './scala.js';
|
|
15
20
|
export { extractSwiftSymbols } from './swift.js';
|
|
21
|
+
export { extractZigSymbols } from './zig.js';
|
|
@@ -0,0 +1,169 @@
|
|
|
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 Lua files.
|
|
12
|
+
*/
|
|
13
|
+
export function extractLuaSymbols(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
|
+
walkLuaNode(tree.rootNode, ctx);
|
|
24
|
+
return ctx;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function walkLuaNode(node: TreeSitterNode, ctx: ExtractorOutput): void {
|
|
28
|
+
switch (node.type) {
|
|
29
|
+
case 'function_declaration':
|
|
30
|
+
handleLuaFunctionDecl(node, ctx);
|
|
31
|
+
break;
|
|
32
|
+
case 'variable_declaration':
|
|
33
|
+
handleLuaVariableDecl(node, ctx);
|
|
34
|
+
break;
|
|
35
|
+
case 'function_call':
|
|
36
|
+
handleLuaFunctionCall(node, ctx);
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
41
|
+
const child = node.child(i);
|
|
42
|
+
if (child) walkLuaNode(child, ctx);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function handleLuaFunctionDecl(node: TreeSitterNode, ctx: ExtractorOutput): void {
|
|
47
|
+
const nameNode = node.childForFieldName('name');
|
|
48
|
+
if (!nameNode) return;
|
|
49
|
+
|
|
50
|
+
let name: string;
|
|
51
|
+
let kind: 'function' | 'method' = 'function';
|
|
52
|
+
|
|
53
|
+
if (nameNode.type === 'method_index_expression') {
|
|
54
|
+
const table = nameNode.childForFieldName('table');
|
|
55
|
+
const method = nameNode.childForFieldName('method');
|
|
56
|
+
if (table && method) {
|
|
57
|
+
name = `${table.text}.${method.text}`;
|
|
58
|
+
kind = 'method';
|
|
59
|
+
} else {
|
|
60
|
+
name = nameNode.text;
|
|
61
|
+
}
|
|
62
|
+
} else if (nameNode.type === 'dot_index_expression') {
|
|
63
|
+
const table = nameNode.childForFieldName('table');
|
|
64
|
+
const field = nameNode.childForFieldName('field');
|
|
65
|
+
if (table && field) {
|
|
66
|
+
name = `${table.text}.${field.text}`;
|
|
67
|
+
kind = 'method';
|
|
68
|
+
} else {
|
|
69
|
+
name = nameNode.text;
|
|
70
|
+
}
|
|
71
|
+
} else {
|
|
72
|
+
name = nameNode.text;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const params = extractLuaParams(node);
|
|
76
|
+
|
|
77
|
+
ctx.definitions.push({
|
|
78
|
+
name,
|
|
79
|
+
kind,
|
|
80
|
+
line: node.startPosition.row + 1,
|
|
81
|
+
endLine: nodeEndLine(node),
|
|
82
|
+
children: params.length > 0 ? params : undefined,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function extractLuaParams(funcNode: TreeSitterNode): SubDeclaration[] {
|
|
87
|
+
const params: SubDeclaration[] = [];
|
|
88
|
+
const paramList = funcNode.childForFieldName('parameters');
|
|
89
|
+
if (!paramList) return params;
|
|
90
|
+
|
|
91
|
+
for (let i = 0; i < paramList.childCount; i++) {
|
|
92
|
+
const param = paramList.child(i);
|
|
93
|
+
if (!param || param.type !== 'identifier') continue;
|
|
94
|
+
params.push({ name: param.text, kind: 'parameter', line: param.startPosition.row + 1 });
|
|
95
|
+
}
|
|
96
|
+
return params;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function handleLuaVariableDecl(node: TreeSitterNode, ctx: ExtractorOutput): void {
|
|
100
|
+
// Check for require calls in the assignment
|
|
101
|
+
const assignment = findChild(node, 'assignment_statement');
|
|
102
|
+
if (assignment) {
|
|
103
|
+
checkForRequire(assignment, ctx);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function checkForRequire(node: TreeSitterNode, ctx: ExtractorOutput): void {
|
|
108
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
109
|
+
const child = node.child(i);
|
|
110
|
+
if (!child) continue;
|
|
111
|
+
if (child.type === 'function_call') {
|
|
112
|
+
const nameNode = child.childForFieldName('name');
|
|
113
|
+
if (nameNode && nameNode.type === 'identifier' && nameNode.text === 'require') {
|
|
114
|
+
const args = child.childForFieldName('arguments');
|
|
115
|
+
if (args) {
|
|
116
|
+
const strArg = findChild(args, 'string');
|
|
117
|
+
if (strArg) {
|
|
118
|
+
const source = strArg.text.replace(/^['"]|['"]$/g, '');
|
|
119
|
+
ctx.imports.push({
|
|
120
|
+
source,
|
|
121
|
+
names: ['require'],
|
|
122
|
+
line: child.startPosition.row + 1,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function handleLuaFunctionCall(node: TreeSitterNode, ctx: ExtractorOutput): void {
|
|
132
|
+
const nameNode = node.childForFieldName('name');
|
|
133
|
+
if (!nameNode) return;
|
|
134
|
+
|
|
135
|
+
// Check for require() as import
|
|
136
|
+
if (nameNode.type === 'identifier' && nameNode.text === 'require') {
|
|
137
|
+
const args = node.childForFieldName('arguments');
|
|
138
|
+
if (args) {
|
|
139
|
+
const strArg = findChild(args, 'string');
|
|
140
|
+
if (strArg) {
|
|
141
|
+
const source = strArg.text.replace(/^['"]|['"]$/g, '');
|
|
142
|
+
ctx.imports.push({
|
|
143
|
+
source,
|
|
144
|
+
names: ['require'],
|
|
145
|
+
line: node.startPosition.row + 1,
|
|
146
|
+
});
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const call: Call = { name: '', line: node.startPosition.row + 1 };
|
|
153
|
+
|
|
154
|
+
if (nameNode.type === 'method_index_expression') {
|
|
155
|
+
const table = nameNode.childForFieldName('table');
|
|
156
|
+
const method = nameNode.childForFieldName('method');
|
|
157
|
+
if (method) call.name = method.text;
|
|
158
|
+
if (table) call.receiver = table.text;
|
|
159
|
+
} else if (nameNode.type === 'dot_index_expression') {
|
|
160
|
+
const table = nameNode.childForFieldName('table');
|
|
161
|
+
const field = nameNode.childForFieldName('field');
|
|
162
|
+
if (field) call.name = field.text;
|
|
163
|
+
if (table) call.receiver = table.text;
|
|
164
|
+
} else {
|
|
165
|
+
call.name = nameNode.text;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (call.name) ctx.calls.push(call);
|
|
169
|
+
}
|
|
@@ -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
|
+
}
|