@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,294 @@
|
|
|
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 Zig files.
|
|
12
|
+
*
|
|
13
|
+
* Zig's structs/enums/unions are anonymous — their names come from the
|
|
14
|
+
* enclosing `variable_declaration` (e.g. `const Foo = struct { ... };`).
|
|
15
|
+
*/
|
|
16
|
+
export function extractZigSymbols(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
|
+
walkZigNode(tree.rootNode, ctx);
|
|
27
|
+
return ctx;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function walkZigNode(node: TreeSitterNode, ctx: ExtractorOutput): void {
|
|
31
|
+
switch (node.type) {
|
|
32
|
+
case 'function_declaration':
|
|
33
|
+
handleZigFunction(node, ctx);
|
|
34
|
+
break;
|
|
35
|
+
case 'variable_declaration':
|
|
36
|
+
handleZigVariable(node, ctx);
|
|
37
|
+
break;
|
|
38
|
+
case 'call_expression':
|
|
39
|
+
handleZigCallExpression(node, ctx);
|
|
40
|
+
break;
|
|
41
|
+
case 'builtin_function':
|
|
42
|
+
handleZigBuiltin(node, ctx);
|
|
43
|
+
break;
|
|
44
|
+
case 'test_declaration':
|
|
45
|
+
handleZigTest(node, ctx);
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
50
|
+
const child = node.child(i);
|
|
51
|
+
if (child) walkZigNode(child, ctx);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function isInsideZigContainer(node: TreeSitterNode): boolean {
|
|
56
|
+
let current = node.parent;
|
|
57
|
+
while (current) {
|
|
58
|
+
if (current.type === 'struct_declaration' || current.type === 'union_declaration') return true;
|
|
59
|
+
current = current.parent;
|
|
60
|
+
}
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function handleZigFunction(node: TreeSitterNode, ctx: ExtractorOutput): void {
|
|
65
|
+
if (isInsideZigContainer(node)) return; // already emitted by extractZigContainerMethods
|
|
66
|
+
|
|
67
|
+
const nameNode = node.childForFieldName('name');
|
|
68
|
+
if (!nameNode) return;
|
|
69
|
+
|
|
70
|
+
const params = extractZigParams(node);
|
|
71
|
+
|
|
72
|
+
ctx.definitions.push({
|
|
73
|
+
name: nameNode.text,
|
|
74
|
+
kind: 'function',
|
|
75
|
+
line: node.startPosition.row + 1,
|
|
76
|
+
endLine: nodeEndLine(node),
|
|
77
|
+
children: params.length > 0 ? params : undefined,
|
|
78
|
+
visibility: isZigPub(node) ? 'public' : 'private',
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function extractZigParams(funcNode: TreeSitterNode): SubDeclaration[] {
|
|
83
|
+
const params: SubDeclaration[] = [];
|
|
84
|
+
const paramList = funcNode.childForFieldName('parameters');
|
|
85
|
+
if (!paramList) return params;
|
|
86
|
+
|
|
87
|
+
for (let i = 0; i < paramList.childCount; i++) {
|
|
88
|
+
const param = paramList.child(i);
|
|
89
|
+
if (!param || param.type !== 'parameter') continue;
|
|
90
|
+
const nameNode = findChild(param, 'identifier');
|
|
91
|
+
if (nameNode) {
|
|
92
|
+
params.push({ name: nameNode.text, kind: 'parameter', line: param.startPosition.row + 1 });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return params;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function handleZigVariable(node: TreeSitterNode, ctx: ExtractorOutput): void {
|
|
99
|
+
const nameNode = findChild(node, 'identifier');
|
|
100
|
+
if (!nameNode) return;
|
|
101
|
+
const name = nameNode.text;
|
|
102
|
+
|
|
103
|
+
// Check if this is a struct/enum/union definition
|
|
104
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
105
|
+
const child = node.child(i);
|
|
106
|
+
if (!child) continue;
|
|
107
|
+
|
|
108
|
+
if (child.type === 'struct_declaration') {
|
|
109
|
+
const members = extractZigContainerFields(child);
|
|
110
|
+
ctx.definitions.push({
|
|
111
|
+
name,
|
|
112
|
+
kind: 'struct',
|
|
113
|
+
line: node.startPosition.row + 1,
|
|
114
|
+
endLine: nodeEndLine(node),
|
|
115
|
+
children: members.length > 0 ? members : undefined,
|
|
116
|
+
visibility: isZigPub(node) ? 'public' : undefined,
|
|
117
|
+
});
|
|
118
|
+
extractZigContainerMethods(child, name, ctx);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (child.type === 'enum_declaration') {
|
|
122
|
+
ctx.definitions.push({
|
|
123
|
+
name,
|
|
124
|
+
kind: 'enum',
|
|
125
|
+
line: node.startPosition.row + 1,
|
|
126
|
+
endLine: nodeEndLine(node),
|
|
127
|
+
visibility: isZigPub(node) ? 'public' : undefined,
|
|
128
|
+
});
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
if (child.type === 'union_declaration') {
|
|
132
|
+
ctx.definitions.push({
|
|
133
|
+
name,
|
|
134
|
+
kind: 'struct',
|
|
135
|
+
line: node.startPosition.row + 1,
|
|
136
|
+
endLine: nodeEndLine(node),
|
|
137
|
+
visibility: isZigPub(node) ? 'public' : undefined,
|
|
138
|
+
});
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Check for @import
|
|
144
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
145
|
+
const child = node.child(i);
|
|
146
|
+
if (!child) continue;
|
|
147
|
+
if (child.type === 'builtin_function') {
|
|
148
|
+
const builtinId = findChild(child, 'builtin_identifier');
|
|
149
|
+
if (builtinId?.text === '@import') {
|
|
150
|
+
const args = findChild(child, 'arguments');
|
|
151
|
+
if (args) {
|
|
152
|
+
const strArg = findChild(args, 'string_literal') || findChild(args, 'string');
|
|
153
|
+
if (strArg) {
|
|
154
|
+
const source = strArg.text.replace(/^"|"$/g, '');
|
|
155
|
+
ctx.imports.push({
|
|
156
|
+
source,
|
|
157
|
+
names: [name],
|
|
158
|
+
line: node.startPosition.row + 1,
|
|
159
|
+
});
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Regular constant/variable
|
|
168
|
+
const isConst = hasChildText(node, 'const');
|
|
169
|
+
ctx.definitions.push({
|
|
170
|
+
name,
|
|
171
|
+
kind: isConst ? 'constant' : 'variable',
|
|
172
|
+
line: node.startPosition.row + 1,
|
|
173
|
+
endLine: nodeEndLine(node),
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function extractZigContainerFields(container: TreeSitterNode): SubDeclaration[] {
|
|
178
|
+
const fields: SubDeclaration[] = [];
|
|
179
|
+
for (let i = 0; i < container.childCount; i++) {
|
|
180
|
+
const child = container.child(i);
|
|
181
|
+
if (!child || child.type !== 'container_field') continue;
|
|
182
|
+
const nameNode = child.childForFieldName('name') || findChild(child, 'identifier');
|
|
183
|
+
if (nameNode) {
|
|
184
|
+
fields.push({ name: nameNode.text, kind: 'property', line: child.startPosition.row + 1 });
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return fields;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function extractZigContainerMethods(
|
|
191
|
+
container: TreeSitterNode,
|
|
192
|
+
parentName: string,
|
|
193
|
+
ctx: ExtractorOutput,
|
|
194
|
+
): void {
|
|
195
|
+
for (let i = 0; i < container.childCount; i++) {
|
|
196
|
+
const child = container.child(i);
|
|
197
|
+
if (!child || child.type !== 'function_declaration') continue;
|
|
198
|
+
const nameNode = child.childForFieldName('name');
|
|
199
|
+
if (nameNode) {
|
|
200
|
+
ctx.definitions.push({
|
|
201
|
+
name: `${parentName}.${nameNode.text}`,
|
|
202
|
+
kind: 'method',
|
|
203
|
+
line: child.startPosition.row + 1,
|
|
204
|
+
endLine: nodeEndLine(child),
|
|
205
|
+
visibility: isZigPub(child) ? 'public' : 'private',
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function handleZigCallExpression(node: TreeSitterNode, ctx: ExtractorOutput): void {
|
|
212
|
+
const funcNode = node.childForFieldName('function');
|
|
213
|
+
if (!funcNode) return;
|
|
214
|
+
|
|
215
|
+
const call: Call = { name: '', line: node.startPosition.row + 1 };
|
|
216
|
+
|
|
217
|
+
if (funcNode.type === 'field_expression' || funcNode.type === 'field_access') {
|
|
218
|
+
const field = funcNode.childForFieldName('field') || funcNode.childForFieldName('member');
|
|
219
|
+
const value = funcNode.childForFieldName('value') || funcNode.child(0);
|
|
220
|
+
if (field) call.name = field.text;
|
|
221
|
+
if (value) call.receiver = value.text;
|
|
222
|
+
} else {
|
|
223
|
+
call.name = funcNode.text;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (call.name) ctx.calls.push(call);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function handleZigBuiltin(node: TreeSitterNode, ctx: ExtractorOutput): void {
|
|
230
|
+
const builtinId = findChild(node, 'builtin_identifier');
|
|
231
|
+
if (!builtinId) return;
|
|
232
|
+
|
|
233
|
+
// Treat @import as import (when standalone, not in variable_declaration)
|
|
234
|
+
if (builtinId.text === '@import' && node.parent?.type !== 'variable_declaration') {
|
|
235
|
+
const args = findChild(node, 'arguments');
|
|
236
|
+
if (args) {
|
|
237
|
+
const strArg = findChild(args, 'string_literal') || findChild(args, 'string');
|
|
238
|
+
if (strArg) {
|
|
239
|
+
const source = strArg.text.replace(/^"|"$/g, '');
|
|
240
|
+
ctx.imports.push({
|
|
241
|
+
source,
|
|
242
|
+
names: ['@import'],
|
|
243
|
+
line: node.startPosition.row + 1,
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Other builtins are calls
|
|
251
|
+
ctx.calls.push({ name: builtinId.text, line: node.startPosition.row + 1 });
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function handleZigTest(node: TreeSitterNode, ctx: ExtractorOutput): void {
|
|
255
|
+
let name = 'test';
|
|
256
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
257
|
+
const child = node.child(i);
|
|
258
|
+
if (!child) continue;
|
|
259
|
+
if (child.type === 'string_literal' || child.type === 'string') {
|
|
260
|
+
// Extract the string content child if available, otherwise strip quotes
|
|
261
|
+
const content = findChild(child, 'string_content');
|
|
262
|
+
name = content ? content.text : child.text.replace(/^"|"$/g, '');
|
|
263
|
+
break;
|
|
264
|
+
}
|
|
265
|
+
if (child.type === 'identifier') {
|
|
266
|
+
name = child.text;
|
|
267
|
+
break;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
ctx.definitions.push({
|
|
272
|
+
name,
|
|
273
|
+
kind: 'function',
|
|
274
|
+
line: node.startPosition.row + 1,
|
|
275
|
+
endLine: nodeEndLine(node),
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function isZigPub(node: TreeSitterNode): boolean {
|
|
280
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
281
|
+
const child = node.child(i);
|
|
282
|
+
if (child && child.type === 'pub') return true;
|
|
283
|
+
if (child && child.text === 'pub') return true;
|
|
284
|
+
}
|
|
285
|
+
return false;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function hasChildText(node: TreeSitterNode, text: string): boolean {
|
|
289
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
290
|
+
const child = node.child(i);
|
|
291
|
+
if (child && child.text === text) return true;
|
|
292
|
+
}
|
|
293
|
+
return false;
|
|
294
|
+
}
|
package/src/features/cfg.ts
CHANGED
|
@@ -369,7 +369,7 @@ export async function buildCFGData(
|
|
|
369
369
|
db: BetterSqlite3Database,
|
|
370
370
|
fileSymbols: Map<string, FileSymbols>,
|
|
371
371
|
rootDir: string,
|
|
372
|
-
|
|
372
|
+
_engineOpts?: {
|
|
373
373
|
nativeDb?: { bulkInsertCfg?(entries: Array<Record<string, unknown>>): number };
|
|
374
374
|
suspendJsDb?: () => void;
|
|
375
375
|
resumeJsDb?: () => void;
|
|
@@ -379,56 +379,11 @@ export async function buildCFGData(
|
|
|
379
379
|
// skip WASM parser init, tree parsing, and JS visitor entirely — just persist.
|
|
380
380
|
const allNative = allCfgNative(fileSymbols);
|
|
381
381
|
|
|
382
|
-
//
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
for (const [relPath, symbols] of fileSymbols) {
|
|
388
|
-
const ext = path.extname(relPath).toLowerCase();
|
|
389
|
-
if (!CFG_EXTENSIONS.has(ext)) continue;
|
|
390
|
-
|
|
391
|
-
for (const def of symbols.definitions) {
|
|
392
|
-
if (def.kind !== 'function' && def.kind !== 'method') continue;
|
|
393
|
-
if (!def.line) continue;
|
|
394
|
-
|
|
395
|
-
const nodeId = getFunctionNodeId(db, def.name, relPath, def.line);
|
|
396
|
-
if (!nodeId) continue;
|
|
397
|
-
|
|
398
|
-
deleteCfgForNode(db, nodeId);
|
|
399
|
-
if (!def.cfg?.blocks?.length) continue;
|
|
400
|
-
|
|
401
|
-
const cfg = def.cfg as unknown as { blocks: CfgBuildBlock[]; edges: CfgBuildEdge[] };
|
|
402
|
-
entries.push({
|
|
403
|
-
nodeId,
|
|
404
|
-
blocks: cfg.blocks.map((b) => ({
|
|
405
|
-
index: b.index,
|
|
406
|
-
blockType: b.type,
|
|
407
|
-
startLine: b.startLine ?? null,
|
|
408
|
-
endLine: b.endLine ?? null,
|
|
409
|
-
label: b.label ?? null,
|
|
410
|
-
})),
|
|
411
|
-
edges: cfg.edges.map((e) => ({
|
|
412
|
-
sourceIndex: e.sourceIndex,
|
|
413
|
-
targetIndex: e.targetIndex,
|
|
414
|
-
kind: e.kind,
|
|
415
|
-
})),
|
|
416
|
-
});
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
if (entries.length > 0) {
|
|
421
|
-
let inserted: number;
|
|
422
|
-
try {
|
|
423
|
-
engineOpts?.suspendJsDb?.();
|
|
424
|
-
inserted = nativeDb.bulkInsertCfg(entries);
|
|
425
|
-
} finally {
|
|
426
|
-
engineOpts?.resumeJsDb?.();
|
|
427
|
-
}
|
|
428
|
-
info(`CFG (native bulk): ${inserted} blocks across ${entries.length} functions`);
|
|
429
|
-
}
|
|
430
|
-
return;
|
|
431
|
-
}
|
|
382
|
+
// NOTE: nativeDb.bulkInsertCfg is intentionally NOT used here.
|
|
383
|
+
// The CFG path requires delete-before-insert (deleteCfgForNode) which creates
|
|
384
|
+
// a dual-connection WAL conflict when deletes go through JS (better-sqlite3)
|
|
385
|
+
// and inserts go through native (rusqlite). The JS-only persistNativeFileCfg
|
|
386
|
+
// path below handles both on a single connection safely.
|
|
432
387
|
|
|
433
388
|
const extToLang = buildExtToLangMap();
|
|
434
389
|
let parsers: unknown = null;
|
package/src/types.ts
CHANGED
|
@@ -90,7 +90,13 @@ export type LanguageId =
|
|
|
90
90
|
| 'kotlin'
|
|
91
91
|
| 'swift'
|
|
92
92
|
| 'scala'
|
|
93
|
-
| 'bash'
|
|
93
|
+
| 'bash'
|
|
94
|
+
| 'elixir'
|
|
95
|
+
| 'lua'
|
|
96
|
+
| 'dart'
|
|
97
|
+
| 'zig'
|
|
98
|
+
| 'haskell'
|
|
99
|
+
| 'ocaml';
|
|
94
100
|
|
|
95
101
|
/** Engine mode selector. */
|
|
96
102
|
export type EngineMode = 'native' | 'wasm' | 'auto';
|