@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.
Files changed (56) hide show
  1. package/README.md +20 -15
  2. package/dist/domain/parser.d.ts +1 -1
  3. package/dist/domain/parser.d.ts.map +1 -1
  4. package/dist/domain/parser.js +44 -2
  5. package/dist/domain/parser.js.map +1 -1
  6. package/dist/extractors/dart.d.ts +6 -0
  7. package/dist/extractors/dart.d.ts.map +1 -0
  8. package/dist/extractors/dart.js +277 -0
  9. package/dist/extractors/dart.js.map +1 -0
  10. package/dist/extractors/elixir.d.ts +9 -0
  11. package/dist/extractors/elixir.d.ts.map +1 -0
  12. package/dist/extractors/elixir.js +223 -0
  13. package/dist/extractors/elixir.js.map +1 -0
  14. package/dist/extractors/haskell.d.ts +8 -0
  15. package/dist/extractors/haskell.d.ts.map +1 -0
  16. package/dist/extractors/haskell.js +217 -0
  17. package/dist/extractors/haskell.js.map +1 -0
  18. package/dist/extractors/index.d.ts +6 -0
  19. package/dist/extractors/index.d.ts.map +1 -1
  20. package/dist/extractors/index.js +6 -0
  21. package/dist/extractors/index.js.map +1 -1
  22. package/dist/extractors/lua.d.ts +6 -0
  23. package/dist/extractors/lua.d.ts.map +1 -0
  24. package/dist/extractors/lua.js +162 -0
  25. package/dist/extractors/lua.js.map +1 -0
  26. package/dist/extractors/ocaml.d.ts +6 -0
  27. package/dist/extractors/ocaml.d.ts.map +1 -0
  28. package/dist/extractors/ocaml.js +236 -0
  29. package/dist/extractors/ocaml.js.map +1 -0
  30. package/dist/extractors/zig.d.ts +9 -0
  31. package/dist/extractors/zig.d.ts.map +1 -0
  32. package/dist/extractors/zig.js +276 -0
  33. package/dist/extractors/zig.js.map +1 -0
  34. package/dist/features/cfg.d.ts +1 -1
  35. package/dist/features/cfg.d.ts.map +1 -1
  36. package/dist/features/cfg.js +6 -51
  37. package/dist/features/cfg.js.map +1 -1
  38. package/dist/types.d.ts +1 -1
  39. package/dist/types.d.ts.map +1 -1
  40. package/grammars/tree-sitter-dart.wasm +0 -0
  41. package/grammars/tree-sitter-elixir.wasm +0 -0
  42. package/grammars/tree-sitter-haskell.wasm +0 -0
  43. package/grammars/tree-sitter-lua.wasm +0 -0
  44. package/grammars/tree-sitter-ocaml.wasm +0 -0
  45. package/grammars/tree-sitter-zig.wasm +0 -0
  46. package/package.json +13 -7
  47. package/src/domain/parser.ts +54 -0
  48. package/src/extractors/dart.ts +304 -0
  49. package/src/extractors/elixir.ts +251 -0
  50. package/src/extractors/haskell.ts +235 -0
  51. package/src/extractors/index.ts +6 -0
  52. package/src/extractors/lua.ts +169 -0
  53. package/src/extractors/ocaml.ts +259 -0
  54. package/src/extractors/zig.ts +294 -0
  55. package/src/features/cfg.ts +6 -51
  56. 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
+ }
@@ -369,7 +369,7 @@ export async function buildCFGData(
369
369
  db: BetterSqlite3Database,
370
370
  fileSymbols: Map<string, FileSymbols>,
371
371
  rootDir: string,
372
- engineOpts?: {
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
- // ── Native bulk-insert fast path ──────────────────────────────────────
383
- const nativeDb = engineOpts?.nativeDb;
384
- if (allNative && nativeDb?.bulkInsertCfg) {
385
- const entries: Array<Record<string, unknown>> = [];
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';