@optave/codegraph 3.10.0 → 3.11.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 (139) hide show
  1. package/README.md +13 -13
  2. package/dist/ast-analysis/rules/index.d.ts.map +1 -1
  3. package/dist/ast-analysis/rules/index.js +77 -0
  4. package/dist/ast-analysis/rules/index.js.map +1 -1
  5. package/dist/cli/commands/audit.js +1 -1
  6. package/dist/cli/commands/audit.js.map +1 -1
  7. package/dist/cli/commands/build.d.ts.map +1 -1
  8. package/dist/cli/commands/build.js +2 -0
  9. package/dist/cli/commands/build.js.map +1 -1
  10. package/dist/cli/commands/check.js +1 -1
  11. package/dist/cli/commands/check.js.map +1 -1
  12. package/dist/cli/commands/children.js +1 -1
  13. package/dist/cli/commands/children.js.map +1 -1
  14. package/dist/cli/commands/diff-impact.js +1 -1
  15. package/dist/cli/commands/diff-impact.js.map +1 -1
  16. package/dist/cli/commands/roles.js +1 -1
  17. package/dist/cli/commands/roles.js.map +1 -1
  18. package/dist/cli/commands/structure.js +1 -1
  19. package/dist/cli/commands/structure.js.map +1 -1
  20. package/dist/cli/shared/options.js +1 -1
  21. package/dist/cli/shared/options.js.map +1 -1
  22. package/dist/db/connection.d.ts.map +1 -1
  23. package/dist/db/connection.js +8 -0
  24. package/dist/db/connection.js.map +1 -1
  25. package/dist/domain/graph/builder/incremental.d.ts +0 -6
  26. package/dist/domain/graph/builder/incremental.d.ts.map +1 -1
  27. package/dist/domain/graph/builder/incremental.js +6 -23
  28. package/dist/domain/graph/builder/incremental.js.map +1 -1
  29. package/dist/domain/graph/builder/pipeline.d.ts +44 -0
  30. package/dist/domain/graph/builder/pipeline.d.ts.map +1 -1
  31. package/dist/domain/graph/builder/pipeline.js +181 -39
  32. package/dist/domain/graph/builder/pipeline.js.map +1 -1
  33. package/dist/domain/graph/builder/stages/build-edges.d.ts.map +1 -1
  34. package/dist/domain/graph/builder/stages/build-edges.js +8 -2
  35. package/dist/domain/graph/builder/stages/build-edges.js.map +1 -1
  36. package/dist/domain/graph/builder/stages/resolve-imports.d.ts.map +1 -1
  37. package/dist/domain/graph/builder/stages/resolve-imports.js +73 -22
  38. package/dist/domain/graph/builder/stages/resolve-imports.js.map +1 -1
  39. package/dist/domain/graph/watcher.d.ts.map +1 -1
  40. package/dist/domain/graph/watcher.js +23 -18
  41. package/dist/domain/graph/watcher.js.map +1 -1
  42. package/dist/domain/parser.d.ts.map +1 -1
  43. package/dist/domain/parser.js +27 -1
  44. package/dist/domain/parser.js.map +1 -1
  45. package/dist/domain/search/models.d.ts +16 -0
  46. package/dist/domain/search/models.d.ts.map +1 -1
  47. package/dist/domain/search/models.js +35 -1
  48. package/dist/domain/search/models.js.map +1 -1
  49. package/dist/domain/wasm-worker-entry.js +8 -1
  50. package/dist/domain/wasm-worker-entry.js.map +1 -1
  51. package/dist/extractors/c.js +25 -6
  52. package/dist/extractors/c.js.map +1 -1
  53. package/dist/extractors/cpp.js +47 -6
  54. package/dist/extractors/cpp.js.map +1 -1
  55. package/dist/extractors/cuda.js +90 -14
  56. package/dist/extractors/cuda.js.map +1 -1
  57. package/dist/extractors/elixir.js +83 -3
  58. package/dist/extractors/elixir.js.map +1 -1
  59. package/dist/extractors/erlang.js +56 -20
  60. package/dist/extractors/erlang.js.map +1 -1
  61. package/dist/extractors/fsharp.d.ts +7 -0
  62. package/dist/extractors/fsharp.d.ts.map +1 -1
  63. package/dist/extractors/fsharp.js +94 -0
  64. package/dist/extractors/fsharp.js.map +1 -1
  65. package/dist/extractors/gleam.js +6 -2
  66. package/dist/extractors/gleam.js.map +1 -1
  67. package/dist/extractors/groovy.js +41 -1
  68. package/dist/extractors/groovy.js.map +1 -1
  69. package/dist/extractors/haskell.js +48 -4
  70. package/dist/extractors/haskell.js.map +1 -1
  71. package/dist/extractors/julia.js +172 -41
  72. package/dist/extractors/julia.js.map +1 -1
  73. package/dist/extractors/kotlin.js +4 -0
  74. package/dist/extractors/kotlin.js.map +1 -1
  75. package/dist/extractors/objc.js +184 -47
  76. package/dist/extractors/objc.js.map +1 -1
  77. package/dist/extractors/python.js +7 -4
  78. package/dist/extractors/python.js.map +1 -1
  79. package/dist/extractors/r.js +93 -52
  80. package/dist/extractors/r.js.map +1 -1
  81. package/dist/extractors/scala.d.ts.map +1 -1
  82. package/dist/extractors/scala.js +18 -32
  83. package/dist/extractors/scala.js.map +1 -1
  84. package/dist/extractors/solidity.js +18 -9
  85. package/dist/extractors/solidity.js.map +1 -1
  86. package/dist/extractors/verilog.js +80 -15
  87. package/dist/extractors/verilog.js.map +1 -1
  88. package/dist/mcp/tool-registry.d.ts.map +1 -1
  89. package/dist/mcp/tool-registry.js +4 -0
  90. package/dist/mcp/tool-registry.js.map +1 -1
  91. package/dist/mcp/tools/semantic-search.d.ts +1 -0
  92. package/dist/mcp/tools/semantic-search.d.ts.map +1 -1
  93. package/dist/mcp/tools/semantic-search.js +1 -0
  94. package/dist/mcp/tools/semantic-search.js.map +1 -1
  95. package/dist/types.d.ts +15 -1
  96. package/dist/types.d.ts.map +1 -1
  97. package/grammars/tree-sitter-erlang.wasm +0 -0
  98. package/grammars/tree-sitter-fsharp.wasm +0 -0
  99. package/grammars/tree-sitter-fsharp_signature.wasm +0 -0
  100. package/grammars/tree-sitter-gleam.wasm +0 -0
  101. package/package.json +10 -10
  102. package/src/ast-analysis/rules/index.ts +87 -0
  103. package/src/cli/commands/audit.ts +1 -1
  104. package/src/cli/commands/build.ts +2 -0
  105. package/src/cli/commands/check.ts +1 -1
  106. package/src/cli/commands/children.ts +1 -1
  107. package/src/cli/commands/diff-impact.ts +1 -1
  108. package/src/cli/commands/roles.ts +1 -1
  109. package/src/cli/commands/structure.ts +1 -1
  110. package/src/cli/shared/options.ts +1 -1
  111. package/src/db/connection.ts +8 -0
  112. package/src/domain/graph/builder/incremental.ts +6 -41
  113. package/src/domain/graph/builder/pipeline.ts +222 -37
  114. package/src/domain/graph/builder/stages/build-edges.ts +9 -2
  115. package/src/domain/graph/builder/stages/resolve-imports.ts +79 -25
  116. package/src/domain/graph/watcher.ts +21 -23
  117. package/src/domain/parser.ts +27 -1
  118. package/src/domain/search/models.ts +36 -1
  119. package/src/domain/wasm-worker-entry.ts +8 -1
  120. package/src/extractors/c.ts +27 -8
  121. package/src/extractors/cpp.ts +50 -8
  122. package/src/extractors/cuda.ts +90 -16
  123. package/src/extractors/elixir.ts +75 -3
  124. package/src/extractors/erlang.ts +63 -20
  125. package/src/extractors/fsharp.ts +104 -0
  126. package/src/extractors/gleam.ts +7 -2
  127. package/src/extractors/groovy.ts +45 -1
  128. package/src/extractors/haskell.ts +45 -4
  129. package/src/extractors/julia.ts +164 -43
  130. package/src/extractors/kotlin.ts +4 -0
  131. package/src/extractors/objc.ts +171 -47
  132. package/src/extractors/python.ts +5 -3
  133. package/src/extractors/r.ts +88 -48
  134. package/src/extractors/scala.ts +24 -36
  135. package/src/extractors/solidity.ts +17 -8
  136. package/src/extractors/verilog.ts +83 -15
  137. package/src/mcp/tool-registry.ts +5 -0
  138. package/src/mcp/tools/semantic-search.ts +2 -0
  139. package/src/types.ts +15 -0
@@ -1,7 +1,7 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { closeDb, getNodeId as getNodeIdQuery, initSchema, openDb } from '../../db/index.js';
4
- import { debug, info } from '../../infrastructure/logger.js';
4
+ import { debug, info, warn } from '../../infrastructure/logger.js';
5
5
  import { isSupportedFile, normalizePath, shouldIgnore } from '../../shared/constants.js';
6
6
  import { DbError } from '../../shared/errors.js';
7
7
  import { createParseTreeCache, getActiveEngine } from '../parser.js';
@@ -16,12 +16,13 @@ function shouldIgnorePath(filePath: string): boolean {
16
16
 
17
17
  /** Prepare all SQL statements needed by the watcher's incremental rebuild. */
18
18
  function prepareWatcherStatements(db: ReturnType<typeof openDb>): IncrementalStmts {
19
- const stmts = {
19
+ return {
20
20
  insertNode: db.prepare(
21
21
  'INSERT OR IGNORE INTO nodes (name, kind, file, line, end_line) VALUES (?, ?, ?, ?, ?)',
22
22
  ),
23
23
  getNodeId: {
24
- get: (name: string, kind: string, file: string, line: number) => {
24
+ get: (...params: unknown[]) => {
25
+ const [name, kind, file, line] = params as [string, string, string, number];
25
26
  const id = getNodeIdQuery(db, name, kind, file, line);
26
27
  return id != null ? { id } : undefined;
27
28
  },
@@ -29,10 +30,7 @@ function prepareWatcherStatements(db: ReturnType<typeof openDb>): IncrementalStm
29
30
  insertEdge: db.prepare(
30
31
  'INSERT INTO edges (source_id, target_id, kind, confidence, dynamic) VALUES (?, ?, ?, ?, ?)',
31
32
  ),
32
- deleteNodes: db.prepare('DELETE FROM nodes WHERE file = ?'),
33
- deleteEdgesForFile: null as { run: (f: string) => void } | null,
34
33
  countNodes: db.prepare('SELECT COUNT(*) as c FROM nodes WHERE file = ?'),
35
- countEdgesForFile: null as { get: (f: string) => { c: number } | undefined } | null,
36
34
  findNodeInFile: db.prepare(
37
35
  "SELECT id, file FROM nodes WHERE name = ? AND kind IN ('function', 'method', 'class', 'interface', 'type', 'struct', 'enum', 'trait', 'record', 'module', 'constant') AND file = ?",
38
36
  ),
@@ -41,19 +39,6 @@ function prepareWatcherStatements(db: ReturnType<typeof openDb>): IncrementalStm
41
39
  ),
42
40
  listSymbols: db.prepare("SELECT name, kind, line FROM nodes WHERE file = ? AND kind != 'file'"),
43
41
  };
44
-
45
- const origDeleteEdges = db.prepare(
46
- `DELETE FROM edges WHERE source_id IN (SELECT id FROM nodes WHERE file = @f) OR target_id IN (SELECT id FROM nodes WHERE file = @f)`,
47
- );
48
- const origCountEdges = db.prepare(
49
- `SELECT COUNT(*) as c FROM edges WHERE source_id IN (SELECT id FROM nodes WHERE file = @f) OR target_id IN (SELECT id FROM nodes WHERE file = @f)`,
50
- );
51
- stmts.deleteEdgesForFile = { run: (f: string) => origDeleteEdges.run({ f }) };
52
- stmts.countEdgesForFile = {
53
- get: (f: string) => origCountEdges.get({ f }) as { c: number } | undefined,
54
- };
55
-
56
- return stmts as IncrementalStmts;
57
42
  }
58
43
 
59
44
  /** Rebuild result shape from rebuildFile. */
@@ -80,10 +65,23 @@ async function processPendingFiles(
80
65
  ): Promise<void> {
81
66
  const results: RebuildResult[] = [];
82
67
  for (const filePath of files) {
83
- const result = (await rebuildFile(db, rootDir, filePath, stmts, engineOpts, cache, {
84
- diffSymbols: diffSymbols as (old: unknown[], new_: unknown[]) => unknown,
85
- })) as RebuildResult | null;
86
- if (result) results.push(result);
68
+ // Per-file try/catch so one bad rebuild doesn't crash the watcher loop.
69
+ // The watcher is a long-running session any SQLite error, parse failure,
70
+ // or filesystem race must be reported and skipped, not propagated. Issue #1176.
71
+ try {
72
+ const result = (await rebuildFile(db, rootDir, filePath, stmts, engineOpts, cache, {
73
+ diffSymbols: diffSymbols as (old: unknown[], new_: unknown[]) => unknown,
74
+ })) as RebuildResult | null;
75
+ if (result) results.push(result);
76
+ } catch (err: unknown) {
77
+ const relPath = normalizePath(path.relative(rootDir, filePath));
78
+ // Narrow with `instanceof` instead of casting: a non-Error throw (a plain
79
+ // string, `null`, or any value a third-party dependency throws) would log
80
+ // `(err as Error).message` as `undefined`. See Greptile review on #1182.
81
+ const message = err instanceof Error ? err.message : String(err);
82
+ warn(`Failed to rebuild ${relPath}: ${message} — skipping`);
83
+ debug(err instanceof Error ? (err.stack ?? message) : String(err));
84
+ }
87
85
  }
88
86
 
89
87
  if (results.length > 0) {
@@ -457,6 +457,8 @@ export const NATIVE_SUPPORTED_EXTENSIONS: ReadonlySet<string> = new Set([
457
457
  '.cc',
458
458
  '.cxx',
459
459
  '.hpp',
460
+ '.cu',
461
+ '.cuh',
460
462
  '.kt',
461
463
  '.kts',
462
464
  '.swift',
@@ -471,6 +473,23 @@ export const NATIVE_SUPPORTED_EXTENSIONS: ReadonlySet<string> = new Set([
471
473
  '.hs',
472
474
  '.ml',
473
475
  '.mli',
476
+ '.fs',
477
+ '.fsx',
478
+ '.fsi',
479
+ '.m',
480
+ '.gleam',
481
+ '.jl',
482
+ '.clj',
483
+ '.cljs',
484
+ '.cljc',
485
+ '.erl',
486
+ '.hrl',
487
+ '.groovy',
488
+ '.gvy',
489
+ '.r',
490
+ '.sol',
491
+ '.v',
492
+ '.sv',
474
493
  ]);
475
494
 
476
495
  /**
@@ -812,11 +831,18 @@ export const LANGUAGE_REGISTRY: LanguageRegistryEntry[] = [
812
831
  },
813
832
  {
814
833
  id: 'fsharp',
815
- extensions: ['.fs', '.fsx', '.fsi'],
834
+ extensions: ['.fs', '.fsx'],
816
835
  grammarFile: 'tree-sitter-fsharp.wasm',
817
836
  extractor: extractFSharpSymbols,
818
837
  required: false,
819
838
  },
839
+ {
840
+ id: 'fsharp-signature',
841
+ extensions: ['.fsi'],
842
+ grammarFile: 'tree-sitter-fsharp_signature.wasm',
843
+ extractor: extractFSharpSymbols,
844
+ required: false,
845
+ },
820
846
  {
821
847
  id: 'gleam',
822
848
  extensions: ['.gleam'],
@@ -1,8 +1,40 @@
1
1
  import { execFileSync } from 'node:child_process';
2
+ import { createRequire } from 'node:module';
3
+ import path from 'node:path';
2
4
  import { createInterface } from 'node:readline';
3
5
  import { info } from '../../infrastructure/logger.js';
4
6
  import { ConfigError, EngineError } from '../../shared/errors.js';
5
7
 
8
+ const _require = createRequire(import.meta.url);
9
+
10
+ /**
11
+ * Resolve the directory where `npm install` should run so the installed
12
+ * package ends up reachable by `await import(pkg)` from inside this module.
13
+ *
14
+ * Without a `cwd`, `execFileSync('npm', ['install', ...])` operates on
15
+ * `process.cwd()` — when the user runs codegraph against a repo that is *not*
16
+ * the directory where codegraph itself is installed, npm installs into the
17
+ * wrong `node_modules`, the dynamic import still fails, and the user gets
18
+ * `ENGINE_UNAVAILABLE: ... installed but failed to load`.
19
+ *
20
+ * Pin cwd to the directory that contains @optave/codegraph's `node_modules`
21
+ * so the install lands where Node's resolution algorithm will find it.
22
+ *
23
+ * @internal Exported for unit tests; not part of the public barrel.
24
+ */
25
+ export function resolveNpmInstallCwd(): string | undefined {
26
+ try {
27
+ const pkgJsonPath = _require.resolve('@optave/codegraph/package.json');
28
+ // pkgJsonPath = <host>/node_modules/@optave/codegraph/package.json
29
+ // dirname x4: package.json → codegraph → @optave → node_modules → <host>
30
+ return path.dirname(path.dirname(path.dirname(path.dirname(pkgJsonPath))));
31
+ } catch {
32
+ // Source-of-truth checkout (no @optave/codegraph in node_modules) — fall back
33
+ // to process.cwd() so legacy behavior survives in tests.
34
+ return undefined;
35
+ }
36
+ }
37
+
6
38
  export interface ModelConfig {
7
39
  name: string;
8
40
  dim: number;
@@ -104,12 +136,14 @@ export function getModelConfig(modelKey?: string): ModelConfig {
104
136
  * @internal Not part of the public barrel.
105
137
  */
106
138
  export function promptInstall(packageName: string): Promise<boolean> {
139
+ const installCwd = resolveNpmInstallCwd();
107
140
  if (!process.stdin.isTTY) {
108
141
  info(`Installing ${packageName} (optional dependency for semantic search)…`);
109
142
  try {
110
143
  execFileSync(NPM_BIN, ['install', '--no-save', packageName], {
111
144
  stdio: 'inherit',
112
145
  timeout: 300_000,
146
+ cwd: installCwd,
113
147
  });
114
148
  return Promise.resolve(true);
115
149
  } catch (err) {
@@ -128,9 +162,10 @@ export function promptInstall(packageName: string): Promise<boolean> {
128
162
  rl.close();
129
163
  if (answer.trim().toLowerCase() !== 'y') return resolve(false);
130
164
  try {
131
- execFileSync(NPM_BIN, ['install', packageName], {
165
+ execFileSync(NPM_BIN, ['install', '--no-save', packageName], {
132
166
  stdio: 'inherit',
133
167
  timeout: 300_000,
168
+ cwd: installCwd,
134
169
  });
135
170
  resolve(true);
136
171
  } catch (err) {
@@ -306,11 +306,18 @@ const LANGUAGE_REGISTRY: LanguageRegistryEntry[] = [
306
306
  },
307
307
  {
308
308
  id: 'fsharp',
309
- extensions: ['.fs', '.fsx', '.fsi'],
309
+ extensions: ['.fs', '.fsx'],
310
310
  grammarFile: 'tree-sitter-fsharp.wasm',
311
311
  extractor: extractFSharpSymbols,
312
312
  required: false,
313
313
  },
314
+ {
315
+ id: 'fsharp-signature',
316
+ extensions: ['.fsi'],
317
+ grammarFile: 'tree-sitter-fsharp_signature.wasm',
318
+ extractor: extractFSharpSymbols,
319
+ required: false,
320
+ },
314
321
  {
315
322
  id: 'gleam',
316
323
  extensions: ['.gleam'],
@@ -159,6 +159,31 @@ function handleCCallExpression(node: TreeSitterNode, ctx: ExtractorOutput): void
159
159
 
160
160
  // ── Child extraction helpers ────────────────────────────────────────────────
161
161
 
162
+ const C_DECLARATOR_WRAPPERS = new Set([
163
+ 'pointer_declarator',
164
+ 'array_declarator',
165
+ 'parenthesized_declarator',
166
+ 'function_declarator',
167
+ ]);
168
+
169
+ /**
170
+ * Drill through pointer/array/parenthesized/function declarator wrappers to
171
+ * recover the bare identifier. Mirrors `unwrap_declarator` in the native C
172
+ * extractor so both engines agree on the name for parameters such as
173
+ * `void process(int callback(int))` (function-type parameter → `callback`) or
174
+ * `int *func(int)` (pointer-returning function → `func`).
175
+ */
176
+ function unwrapCDeclaratorName(node: TreeSitterNode): string {
177
+ let current: TreeSitterNode | null = node;
178
+ while (current && C_DECLARATOR_WRAPPERS.has(current.type)) {
179
+ current = current.childForFieldName('declarator');
180
+ }
181
+ if (current?.type === 'identifier' || current?.type === 'field_identifier') {
182
+ return current.text;
183
+ }
184
+ return current?.text ?? node.text;
185
+ }
186
+
162
187
  function extractCParameters(paramListNode: TreeSitterNode | null): SubDeclaration[] {
163
188
  const params: SubDeclaration[] = [];
164
189
  if (!paramListNode) return params;
@@ -167,10 +192,7 @@ function extractCParameters(paramListNode: TreeSitterNode | null): SubDeclaratio
167
192
  if (!param || param.type !== 'parameter_declaration') continue;
168
193
  const nameNode = param.childForFieldName('declarator');
169
194
  if (nameNode) {
170
- const name =
171
- nameNode.type === 'identifier'
172
- ? nameNode.text
173
- : (findChild(nameNode, 'identifier')?.text ?? nameNode.text);
195
+ const name = unwrapCDeclaratorName(nameNode);
174
196
  params.push({ name, kind: 'parameter', line: param.startPosition.row + 1 });
175
197
  }
176
198
  }
@@ -186,10 +208,7 @@ function extractStructFields(structNode: TreeSitterNode): SubDeclaration[] {
186
208
  if (!member || member.type !== 'field_declaration') continue;
187
209
  const nameNode = member.childForFieldName('declarator');
188
210
  if (nameNode) {
189
- const name =
190
- nameNode.type === 'identifier'
191
- ? nameNode.text
192
- : (findChild(nameNode, 'identifier')?.text ?? nameNode.text);
211
+ const name = unwrapCDeclaratorName(nameNode);
193
212
  fields.push({ name, kind: 'property', line: member.startPosition.row + 1 });
194
213
  }
195
214
  }
@@ -239,6 +239,54 @@ function findCppParentClass(node: TreeSitterNode): string | null {
239
239
  return null;
240
240
  }
241
241
 
242
+ const CPP_DECLARATOR_WRAPPERS = new Set([
243
+ 'pointer_declarator',
244
+ 'reference_declarator',
245
+ 'array_declarator',
246
+ 'parenthesized_declarator',
247
+ 'function_declarator',
248
+ ]);
249
+
250
+ /**
251
+ * Drill through pointer/reference/array/parenthesized/function declarator
252
+ * wrappers to recover the bare identifier. Mirrors `unwrap_cpp_declarator` in
253
+ * the native C++ extractor. tree-sitter-cpp's `reference_declarator` does not
254
+ * expose a `declarator` field, so the loop falls back to scanning children
255
+ * for the next nested declarator or identifier.
256
+ */
257
+ function unwrapCppDeclaratorName(node: TreeSitterNode): string {
258
+ let current: TreeSitterNode | null = node;
259
+ while (current && CPP_DECLARATOR_WRAPPERS.has(current.type)) {
260
+ const named = current.childForFieldName('declarator');
261
+ if (named) {
262
+ current = named;
263
+ continue;
264
+ }
265
+ const fallback = nextCppDeclaratorChild(current);
266
+ if (!fallback) break;
267
+ current = fallback;
268
+ }
269
+ if (current?.type === 'identifier' || current?.type === 'field_identifier') {
270
+ return current.text;
271
+ }
272
+ return current?.text ?? node.text;
273
+ }
274
+
275
+ function nextCppDeclaratorChild(node: TreeSitterNode): TreeSitterNode | null {
276
+ for (let i = 0; i < node.childCount; i++) {
277
+ const child = node.child(i);
278
+ if (!child) continue;
279
+ if (
280
+ child.type === 'identifier' ||
281
+ child.type === 'field_identifier' ||
282
+ CPP_DECLARATOR_WRAPPERS.has(child.type)
283
+ ) {
284
+ return child;
285
+ }
286
+ }
287
+ return null;
288
+ }
289
+
242
290
  function extractCppParameters(paramListNode: TreeSitterNode | null): SubDeclaration[] {
243
291
  const params: SubDeclaration[] = [];
244
292
  if (!paramListNode) return params;
@@ -247,10 +295,7 @@ function extractCppParameters(paramListNode: TreeSitterNode | null): SubDeclarat
247
295
  if (!param || param.type !== 'parameter_declaration') continue;
248
296
  const nameNode = param.childForFieldName('declarator');
249
297
  if (nameNode) {
250
- const name =
251
- nameNode.type === 'identifier'
252
- ? nameNode.text
253
- : (findChild(nameNode, 'identifier')?.text ?? nameNode.text);
298
+ const name = unwrapCppDeclaratorName(nameNode);
254
299
  params.push({ name, kind: 'parameter', line: param.startPosition.row + 1 });
255
300
  }
256
301
  }
@@ -267,10 +312,7 @@ function extractCppClassFields(classNode: TreeSitterNode): SubDeclaration[] {
267
312
  if (!member || member.type !== 'field_declaration') continue;
268
313
  const nameNode = member.childForFieldName('declarator');
269
314
  if (nameNode) {
270
- const name =
271
- nameNode.type === 'identifier'
272
- ? nameNode.text
273
- : (findChild(nameNode, 'identifier')?.text ?? nameNode.text);
315
+ const name = unwrapCppDeclaratorName(nameNode);
274
316
  fields.push({
275
317
  name,
276
318
  kind: 'property',
@@ -265,10 +265,10 @@ function extractCudaParameters(paramListNode: TreeSitterNode | null): SubDeclara
265
265
  if (!param || param.type !== 'parameter_declaration') continue;
266
266
  const nameNode = param.childForFieldName('declarator');
267
267
  if (nameNode) {
268
- const name =
269
- nameNode.type === 'identifier'
270
- ? nameNode.text
271
- : (findChild(nameNode, 'identifier')?.text ?? nameNode.text);
268
+ // Reuse the field-name drill helper so function-type parameters like
269
+ // `void process(int callback(int))` yield the bare name `callback`
270
+ // instead of the raw declarator text, matching the native unwrap path.
271
+ const name = extractCudaFieldName(nameNode);
272
272
  params.push({ name, kind: 'parameter', line: param.startPosition.row + 1 });
273
273
  }
274
274
  }
@@ -284,22 +284,96 @@ function extractCudaClassFields(classNode: TreeSitterNode): SubDeclaration[] {
284
284
  const member = body.child(i);
285
285
  if (!member || member.type !== 'field_declaration') continue;
286
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
- }
287
+ if (!nameNode) continue;
288
+ // Skip method declarations — a `field_declaration` whose declarator
289
+ // (after unwrapping pointer/reference/array) is a `function_declarator`
290
+ // is a method signature in a header, not a data field. Native and WASM
291
+ // previously diverged on how to format these (native stripped the `*`
292
+ // from pointer-return types, WASM kept it), and both produced
293
+ // method-signature-shaped "property" entries that are not real fields.
294
+ if (isCudaMethodDeclarator(nameNode)) continue;
295
+ const name = extractCudaFieldName(nameNode);
296
+ fields.push({
297
+ name,
298
+ kind: 'property',
299
+ line: member.startPosition.row + 1,
300
+ visibility: extractModifierVisibility(member),
301
+ });
299
302
  }
300
303
  return fields;
301
304
  }
302
305
 
306
+ const CUDA_DECLARATOR_WRAPPERS = new Set([
307
+ 'pointer_declarator',
308
+ 'reference_declarator',
309
+ 'array_declarator',
310
+ 'parenthesized_declarator',
311
+ ]);
312
+
313
+ function isCudaMethodDeclarator(node: TreeSitterNode): boolean {
314
+ let current: TreeSitterNode | null = node;
315
+ while (current && CUDA_DECLARATOR_WRAPPERS.has(current.type)) {
316
+ current = current.childForFieldName('declarator');
317
+ }
318
+ if (current?.type !== 'function_declarator') return false;
319
+ // A `function_declarator` whose inner declarator is a `parenthesized_declarator`
320
+ // is a function-pointer (or function-reference) field — e.g. `void (*cb)(int)`
321
+ // parses as function_declarator > parenthesized_declarator > pointer_declarator >
322
+ // field_identifier. Those are real data fields, not method declarations.
323
+ const inner = current.childForFieldName('declarator');
324
+ return inner?.type !== 'parenthesized_declarator';
325
+ }
326
+
327
+ /**
328
+ * Resolve the identifier of a declarator by walking through any combination of
329
+ * pointer/reference/array/parenthesized wrappers and `function_declarator`
330
+ * nodes. Used by both class-field extraction (where `function_declarator`
331
+ * indicates a function-pointer field after method declarations have been
332
+ * filtered out) and parameter extraction (where `function_declarator` wraps a
333
+ * bare function-type parameter name like `callback` in
334
+ * `void process(int callback(int))`).
335
+ */
336
+ function extractCudaFieldName(decl: TreeSitterNode): string {
337
+ let current: TreeSitterNode | null = decl;
338
+ while (current) {
339
+ if (current.type === 'identifier' || current.type === 'field_identifier') {
340
+ return current.text;
341
+ }
342
+ if (CUDA_DECLARATOR_WRAPPERS.has(current.type) || current.type === 'function_declarator') {
343
+ const next = innerCudaDeclarator(current);
344
+ if (!next) break;
345
+ current = next;
346
+ continue;
347
+ }
348
+ break;
349
+ }
350
+ return decl.text;
351
+ }
352
+
353
+ /**
354
+ * Find the inner declarator of a wrapper node. Most C++ declarator wrappers
355
+ * expose it via the `declarator` field, but some (e.g. `parenthesized_declarator`
356
+ * and `reference_declarator` in tree-sitter-cuda) have unnamed children — so
357
+ * fall back to scanning children for a declarator-shaped node.
358
+ */
359
+ function innerCudaDeclarator(node: TreeSitterNode): TreeSitterNode | null {
360
+ const named = node.childForFieldName('declarator');
361
+ if (named) return named;
362
+ for (let i = 0; i < node.childCount; i++) {
363
+ const child = node.child(i);
364
+ if (!child) continue;
365
+ if (
366
+ child.type === 'identifier' ||
367
+ child.type === 'field_identifier' ||
368
+ child.type === 'function_declarator' ||
369
+ CUDA_DECLARATOR_WRAPPERS.has(child.type)
370
+ ) {
371
+ return child;
372
+ }
373
+ }
374
+ return null;
375
+ }
376
+
303
377
  function extractCudaEnumEntries(enumNode: TreeSitterNode): SubDeclaration[] {
304
378
  const entries: SubDeclaration[] = [];
305
379
  const body = findChild(enumNode, 'enumerator_list');
@@ -190,14 +190,86 @@ function extractElixirParams(defCallNode: TreeSitterNode): SubDeclaration[] {
190
190
  for (let j = 0; j < innerArgs.childCount; j++) {
191
191
  const param = innerArgs.child(j);
192
192
  if (!param) continue;
193
- if (param.type === 'identifier') {
194
- params.push({ name: param.text, kind: 'parameter', line: param.startPosition.row + 1 });
195
- }
193
+ collectElixirParamIdentifiers(param, params);
196
194
  }
197
195
  }
198
196
  return params;
199
197
  }
200
198
 
199
+ /**
200
+ * Recursively walk a parameter pattern and emit each bound identifier as a
201
+ * `parameter` child. Handles bare identifiers, default-value `a \\ default`,
202
+ * list-cons `[head | tail]`, list `[a, b, c]`, tuple `{x, y}`, and
203
+ * map / struct destructuring (`%{k: v}`, `%Foo{k: v}`).
204
+ */
205
+ function collectElixirParamIdentifiers(node: TreeSitterNode, out: SubDeclaration[]): void {
206
+ switch (node.type) {
207
+ case 'identifier':
208
+ out.push({ name: node.text, kind: 'parameter', line: node.startPosition.row + 1 });
209
+ return;
210
+ case 'binary_operator': {
211
+ // `name \\ default` (default-value) binds the left operand only.
212
+ // `head | tail` (list-cons, appears inside a `list` pattern) binds both operands.
213
+ const op = node.child(1);
214
+ if (!op) return;
215
+ if (op.type === '\\\\') {
216
+ const left = node.child(0);
217
+ if (left) collectElixirParamIdentifiers(left, out);
218
+ return;
219
+ }
220
+ if (op.type === '|') {
221
+ const left = node.child(0);
222
+ const right = node.child(2);
223
+ if (left) collectElixirParamIdentifiers(left, out);
224
+ if (right) collectElixirParamIdentifiers(right, out);
225
+ return;
226
+ }
227
+ return;
228
+ }
229
+ case 'list':
230
+ // `[a, b, c]` or `[head | tail]` — walk children, skipping punctuation. The
231
+ // `|` cons case is handled by the `binary_operator` arm when we recurse.
232
+ for (let i = 0; i < node.childCount; i++) {
233
+ const c = node.child(i);
234
+ if (!c || c.type === '[' || c.type === ']' || c.type === ',') continue;
235
+ collectElixirParamIdentifiers(c, out);
236
+ }
237
+ return;
238
+ case 'tuple':
239
+ for (let i = 0; i < node.childCount; i++) {
240
+ const c = node.child(i);
241
+ if (!c || c.type === '{' || c.type === '}' || c.type === ',') continue;
242
+ collectElixirParamIdentifiers(c, out);
243
+ }
244
+ return;
245
+ case 'map':
246
+ // `%{k: v}` or `%Foo{k: v}` — walk map_content > keywords > pair and emit each
247
+ // pair's value side (the bound name). The struct alias (`Foo`) is a type, not a
248
+ // bound identifier, so the leading `struct` child is intentionally skipped.
249
+ for (let i = 0; i < node.childCount; i++) {
250
+ const c = node.child(i);
251
+ if (c && c.type === 'map_content') collectElixirMapBindings(c, out);
252
+ }
253
+ return;
254
+ }
255
+ }
256
+
257
+ function collectElixirMapBindings(content: TreeSitterNode, out: SubDeclaration[]): void {
258
+ for (let i = 0; i < content.childCount; i++) {
259
+ const kws = content.child(i);
260
+ if (!kws || kws.type !== 'keywords') continue;
261
+ for (let j = 0; j < kws.childCount; j++) {
262
+ const pair = kws.child(j);
263
+ if (!pair || pair.type !== 'pair') continue;
264
+ for (let k = 0; k < pair.childCount; k++) {
265
+ const part = pair.child(k);
266
+ if (!part || part.type === 'keyword') continue;
267
+ collectElixirParamIdentifiers(part, out);
268
+ }
269
+ }
270
+ }
271
+ }
272
+
201
273
  function handleDefprotocol(node: TreeSitterNode, ctx: ExtractorOutput): void {
202
274
  const args = findChild(node, 'arguments');
203
275
  if (!args) return;