@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.
- package/README.md +13 -13
- package/dist/ast-analysis/rules/index.d.ts.map +1 -1
- package/dist/ast-analysis/rules/index.js +77 -0
- package/dist/ast-analysis/rules/index.js.map +1 -1
- package/dist/cli/commands/audit.js +1 -1
- package/dist/cli/commands/audit.js.map +1 -1
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/build.js +2 -0
- package/dist/cli/commands/build.js.map +1 -1
- package/dist/cli/commands/check.js +1 -1
- package/dist/cli/commands/check.js.map +1 -1
- package/dist/cli/commands/children.js +1 -1
- package/dist/cli/commands/children.js.map +1 -1
- package/dist/cli/commands/diff-impact.js +1 -1
- package/dist/cli/commands/diff-impact.js.map +1 -1
- package/dist/cli/commands/roles.js +1 -1
- package/dist/cli/commands/roles.js.map +1 -1
- package/dist/cli/commands/structure.js +1 -1
- package/dist/cli/commands/structure.js.map +1 -1
- package/dist/cli/shared/options.js +1 -1
- package/dist/cli/shared/options.js.map +1 -1
- package/dist/db/connection.d.ts.map +1 -1
- package/dist/db/connection.js +8 -0
- package/dist/db/connection.js.map +1 -1
- package/dist/domain/graph/builder/incremental.d.ts +0 -6
- package/dist/domain/graph/builder/incremental.d.ts.map +1 -1
- package/dist/domain/graph/builder/incremental.js +6 -23
- package/dist/domain/graph/builder/incremental.js.map +1 -1
- package/dist/domain/graph/builder/pipeline.d.ts +44 -0
- package/dist/domain/graph/builder/pipeline.d.ts.map +1 -1
- package/dist/domain/graph/builder/pipeline.js +181 -39
- package/dist/domain/graph/builder/pipeline.js.map +1 -1
- package/dist/domain/graph/builder/stages/build-edges.d.ts.map +1 -1
- package/dist/domain/graph/builder/stages/build-edges.js +8 -2
- package/dist/domain/graph/builder/stages/build-edges.js.map +1 -1
- package/dist/domain/graph/builder/stages/resolve-imports.d.ts.map +1 -1
- package/dist/domain/graph/builder/stages/resolve-imports.js +73 -22
- package/dist/domain/graph/builder/stages/resolve-imports.js.map +1 -1
- package/dist/domain/graph/watcher.d.ts.map +1 -1
- package/dist/domain/graph/watcher.js +23 -18
- package/dist/domain/graph/watcher.js.map +1 -1
- package/dist/domain/parser.d.ts.map +1 -1
- package/dist/domain/parser.js +27 -1
- package/dist/domain/parser.js.map +1 -1
- package/dist/domain/search/models.d.ts +16 -0
- package/dist/domain/search/models.d.ts.map +1 -1
- package/dist/domain/search/models.js +35 -1
- package/dist/domain/search/models.js.map +1 -1
- package/dist/domain/wasm-worker-entry.js +8 -1
- package/dist/domain/wasm-worker-entry.js.map +1 -1
- package/dist/extractors/c.js +25 -6
- package/dist/extractors/c.js.map +1 -1
- package/dist/extractors/cpp.js +47 -6
- package/dist/extractors/cpp.js.map +1 -1
- package/dist/extractors/cuda.js +90 -14
- package/dist/extractors/cuda.js.map +1 -1
- package/dist/extractors/elixir.js +83 -3
- package/dist/extractors/elixir.js.map +1 -1
- package/dist/extractors/erlang.js +56 -20
- package/dist/extractors/erlang.js.map +1 -1
- package/dist/extractors/fsharp.d.ts +7 -0
- package/dist/extractors/fsharp.d.ts.map +1 -1
- package/dist/extractors/fsharp.js +94 -0
- package/dist/extractors/fsharp.js.map +1 -1
- package/dist/extractors/gleam.js +6 -2
- package/dist/extractors/gleam.js.map +1 -1
- package/dist/extractors/groovy.js +41 -1
- package/dist/extractors/groovy.js.map +1 -1
- package/dist/extractors/haskell.js +48 -4
- package/dist/extractors/haskell.js.map +1 -1
- package/dist/extractors/julia.js +172 -41
- package/dist/extractors/julia.js.map +1 -1
- package/dist/extractors/kotlin.js +4 -0
- package/dist/extractors/kotlin.js.map +1 -1
- package/dist/extractors/objc.js +184 -47
- package/dist/extractors/objc.js.map +1 -1
- package/dist/extractors/python.js +7 -4
- package/dist/extractors/python.js.map +1 -1
- package/dist/extractors/r.js +93 -52
- package/dist/extractors/r.js.map +1 -1
- package/dist/extractors/scala.d.ts.map +1 -1
- package/dist/extractors/scala.js +18 -32
- package/dist/extractors/scala.js.map +1 -1
- package/dist/extractors/solidity.js +18 -9
- package/dist/extractors/solidity.js.map +1 -1
- package/dist/extractors/verilog.js +80 -15
- package/dist/extractors/verilog.js.map +1 -1
- package/dist/mcp/tool-registry.d.ts.map +1 -1
- package/dist/mcp/tool-registry.js +4 -0
- package/dist/mcp/tool-registry.js.map +1 -1
- package/dist/mcp/tools/semantic-search.d.ts +1 -0
- package/dist/mcp/tools/semantic-search.d.ts.map +1 -1
- package/dist/mcp/tools/semantic-search.js +1 -0
- package/dist/mcp/tools/semantic-search.js.map +1 -1
- package/dist/types.d.ts +15 -1
- package/dist/types.d.ts.map +1 -1
- package/grammars/tree-sitter-erlang.wasm +0 -0
- package/grammars/tree-sitter-fsharp.wasm +0 -0
- package/grammars/tree-sitter-fsharp_signature.wasm +0 -0
- package/grammars/tree-sitter-gleam.wasm +0 -0
- package/package.json +10 -10
- package/src/ast-analysis/rules/index.ts +87 -0
- package/src/cli/commands/audit.ts +1 -1
- package/src/cli/commands/build.ts +2 -0
- package/src/cli/commands/check.ts +1 -1
- package/src/cli/commands/children.ts +1 -1
- package/src/cli/commands/diff-impact.ts +1 -1
- package/src/cli/commands/roles.ts +1 -1
- package/src/cli/commands/structure.ts +1 -1
- package/src/cli/shared/options.ts +1 -1
- package/src/db/connection.ts +8 -0
- package/src/domain/graph/builder/incremental.ts +6 -41
- package/src/domain/graph/builder/pipeline.ts +222 -37
- package/src/domain/graph/builder/stages/build-edges.ts +9 -2
- package/src/domain/graph/builder/stages/resolve-imports.ts +79 -25
- package/src/domain/graph/watcher.ts +21 -23
- package/src/domain/parser.ts +27 -1
- package/src/domain/search/models.ts +36 -1
- package/src/domain/wasm-worker-entry.ts +8 -1
- package/src/extractors/c.ts +27 -8
- package/src/extractors/cpp.ts +50 -8
- package/src/extractors/cuda.ts +90 -16
- package/src/extractors/elixir.ts +75 -3
- package/src/extractors/erlang.ts +63 -20
- package/src/extractors/fsharp.ts +104 -0
- package/src/extractors/gleam.ts +7 -2
- package/src/extractors/groovy.ts +45 -1
- package/src/extractors/haskell.ts +45 -4
- package/src/extractors/julia.ts +164 -43
- package/src/extractors/kotlin.ts +4 -0
- package/src/extractors/objc.ts +171 -47
- package/src/extractors/python.ts +5 -3
- package/src/extractors/r.ts +88 -48
- package/src/extractors/scala.ts +24 -36
- package/src/extractors/solidity.ts +17 -8
- package/src/extractors/verilog.ts +83 -15
- package/src/mcp/tool-registry.ts +5 -0
- package/src/mcp/tools/semantic-search.ts +2 -0
- 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
|
-
|
|
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: (
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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) {
|
package/src/domain/parser.ts
CHANGED
|
@@ -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'
|
|
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'
|
|
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'],
|
package/src/extractors/c.ts
CHANGED
|
@@ -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
|
}
|
package/src/extractors/cpp.ts
CHANGED
|
@@ -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',
|
package/src/extractors/cuda.ts
CHANGED
|
@@ -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
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
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');
|
package/src/extractors/elixir.ts
CHANGED
|
@@ -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
|
-
|
|
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;
|