@optave/codegraph 1.4.1 → 2.1.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 +127 -40
- package/package.json +10 -10
- package/src/builder.js +61 -8
- package/src/cli.js +147 -5
- package/src/config.js +1 -1
- package/src/constants.js +0 -2
- package/src/cycles.js +2 -2
- package/src/db.js +13 -0
- package/src/embedder.js +3 -3
- package/src/export.js +44 -9
- package/src/extractors/csharp.js +243 -0
- package/src/extractors/go.js +167 -0
- package/src/extractors/hcl.js +73 -0
- package/src/extractors/helpers.js +10 -0
- package/src/extractors/index.js +9 -0
- package/src/extractors/java.js +227 -0
- package/src/extractors/javascript.js +396 -0
- package/src/extractors/php.js +237 -0
- package/src/extractors/python.js +143 -0
- package/src/extractors/ruby.js +185 -0
- package/src/extractors/rust.js +215 -0
- package/src/index.js +22 -0
- package/src/mcp.js +141 -6
- package/src/parser.js +29 -1893
- package/src/queries.js +190 -4
- package/src/registry.js +162 -0
- package/src/resolve.js +4 -3
- package/src/structure.js +491 -0
- package/src/watcher.js +2 -2
package/src/cli.js
CHANGED
|
@@ -18,14 +18,25 @@ import {
|
|
|
18
18
|
impactAnalysis,
|
|
19
19
|
moduleMap,
|
|
20
20
|
queryName,
|
|
21
|
+
stats,
|
|
21
22
|
} from './queries.js';
|
|
23
|
+
import {
|
|
24
|
+
listRepos,
|
|
25
|
+
pruneRegistry,
|
|
26
|
+
REGISTRY_PATH,
|
|
27
|
+
registerRepo,
|
|
28
|
+
unregisterRepo,
|
|
29
|
+
} from './registry.js';
|
|
22
30
|
import { watchProject } from './watcher.js';
|
|
23
31
|
|
|
32
|
+
const __cliDir = path.dirname(new URL(import.meta.url).pathname.replace(/^\/([A-Z]:)/i, '$1'));
|
|
33
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(__cliDir, '..', 'package.json'), 'utf-8'));
|
|
34
|
+
|
|
24
35
|
const program = new Command();
|
|
25
36
|
program
|
|
26
37
|
.name('codegraph')
|
|
27
38
|
.description('Local code dependency graph tool')
|
|
28
|
-
.version(
|
|
39
|
+
.version(pkg.version)
|
|
29
40
|
.option('-v, --verbose', 'Enable verbose/debug output')
|
|
30
41
|
.option('--engine <engine>', 'Parser engine: native, wasm, or auto (default: auto)', 'auto')
|
|
31
42
|
.hook('preAction', (thisCommand) => {
|
|
@@ -71,6 +82,15 @@ program
|
|
|
71
82
|
moduleMap(opts.db, parseInt(opts.limit, 10), { json: opts.json });
|
|
72
83
|
});
|
|
73
84
|
|
|
85
|
+
program
|
|
86
|
+
.command('stats')
|
|
87
|
+
.description('Show graph health overview: nodes, edges, languages, cycles, hotspots, embeddings')
|
|
88
|
+
.option('-d, --db <path>', 'Path to graph.db')
|
|
89
|
+
.option('-j, --json', 'Output as JSON')
|
|
90
|
+
.action((opts) => {
|
|
91
|
+
stats(opts.db, { json: opts.json });
|
|
92
|
+
});
|
|
93
|
+
|
|
74
94
|
program
|
|
75
95
|
.command('deps <file>')
|
|
76
96
|
.description('Show what this file imports and what imports it')
|
|
@@ -186,9 +206,84 @@ program
|
|
|
186
206
|
.command('mcp')
|
|
187
207
|
.description('Start MCP (Model Context Protocol) server for AI assistant integration')
|
|
188
208
|
.option('-d, --db <path>', 'Path to graph.db')
|
|
209
|
+
.option('--multi-repo', 'Enable access to all registered repositories')
|
|
210
|
+
.option('--repos <names>', 'Comma-separated list of allowed repo names (restricts access)')
|
|
189
211
|
.action(async (opts) => {
|
|
190
212
|
const { startMCPServer } = await import('./mcp.js');
|
|
191
|
-
|
|
213
|
+
const mcpOpts = {};
|
|
214
|
+
mcpOpts.multiRepo = opts.multiRepo || !!opts.repos;
|
|
215
|
+
if (opts.repos) {
|
|
216
|
+
mcpOpts.allowedRepos = opts.repos.split(',').map((s) => s.trim());
|
|
217
|
+
}
|
|
218
|
+
await startMCPServer(opts.db, mcpOpts);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// ─── Registry commands ──────────────────────────────────────────────────
|
|
222
|
+
|
|
223
|
+
const registry = program.command('registry').description('Manage the multi-repo project registry');
|
|
224
|
+
|
|
225
|
+
registry
|
|
226
|
+
.command('list')
|
|
227
|
+
.description('List all registered repositories')
|
|
228
|
+
.option('-j, --json', 'Output as JSON')
|
|
229
|
+
.action((opts) => {
|
|
230
|
+
pruneRegistry();
|
|
231
|
+
const repos = listRepos();
|
|
232
|
+
if (opts.json) {
|
|
233
|
+
console.log(JSON.stringify(repos, null, 2));
|
|
234
|
+
} else if (repos.length === 0) {
|
|
235
|
+
console.log(`No repositories registered.\nRegistry: ${REGISTRY_PATH}`);
|
|
236
|
+
} else {
|
|
237
|
+
console.log(`Registered repositories (${REGISTRY_PATH}):\n`);
|
|
238
|
+
for (const r of repos) {
|
|
239
|
+
const dbExists = fs.existsSync(r.dbPath);
|
|
240
|
+
const status = dbExists ? '' : ' [DB missing]';
|
|
241
|
+
console.log(` ${r.name}${status}`);
|
|
242
|
+
console.log(` Path: ${r.path}`);
|
|
243
|
+
console.log(` DB: ${r.dbPath}`);
|
|
244
|
+
console.log();
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
registry
|
|
250
|
+
.command('add <dir>')
|
|
251
|
+
.description('Register a project directory')
|
|
252
|
+
.option('-n, --name <name>', 'Custom name (defaults to directory basename)')
|
|
253
|
+
.action((dir, opts) => {
|
|
254
|
+
const absDir = path.resolve(dir);
|
|
255
|
+
const { name, entry } = registerRepo(absDir, opts.name);
|
|
256
|
+
console.log(`Registered "${name}" → ${entry.path}`);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
registry
|
|
260
|
+
.command('remove <name>')
|
|
261
|
+
.description('Unregister a repository by name')
|
|
262
|
+
.action((name) => {
|
|
263
|
+
const removed = unregisterRepo(name);
|
|
264
|
+
if (removed) {
|
|
265
|
+
console.log(`Removed "${name}" from registry.`);
|
|
266
|
+
} else {
|
|
267
|
+
console.error(`Repository "${name}" not found in registry.`);
|
|
268
|
+
process.exit(1);
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
registry
|
|
273
|
+
.command('prune')
|
|
274
|
+
.description('Remove stale registry entries (missing directories or idle beyond TTL)')
|
|
275
|
+
.option('--ttl <days>', 'Days of inactivity before pruning (default: 30)', '30')
|
|
276
|
+
.action((opts) => {
|
|
277
|
+
const pruned = pruneRegistry(undefined, parseInt(opts.ttl, 10));
|
|
278
|
+
if (pruned.length === 0) {
|
|
279
|
+
console.log('No stale entries found.');
|
|
280
|
+
} else {
|
|
281
|
+
for (const entry of pruned) {
|
|
282
|
+
const tag = entry.reason === 'expired' ? 'expired' : 'missing';
|
|
283
|
+
console.log(`Pruned "${entry.name}" (${entry.path}) [${tag}]`);
|
|
284
|
+
}
|
|
285
|
+
console.log(`\nRemoved ${pruned.length} stale ${pruned.length === 1 ? 'entry' : 'entries'}.`);
|
|
286
|
+
}
|
|
192
287
|
});
|
|
193
288
|
|
|
194
289
|
// ─── Embedding commands ─────────────────────────────────────────────────
|
|
@@ -199,7 +294,7 @@ program
|
|
|
199
294
|
.action(() => {
|
|
200
295
|
console.log('\nAvailable embedding models:\n');
|
|
201
296
|
for (const [key, config] of Object.entries(MODELS)) {
|
|
202
|
-
const def = key === '
|
|
297
|
+
const def = key === 'jina-code' ? ' (default)' : '';
|
|
203
298
|
console.log(` ${key.padEnd(12)} ${String(config.dim).padStart(4)}d ${config.desc}${def}`);
|
|
204
299
|
}
|
|
205
300
|
console.log('\nUsage: codegraph embed --model <name>');
|
|
@@ -213,8 +308,8 @@ program
|
|
|
213
308
|
)
|
|
214
309
|
.option(
|
|
215
310
|
'-m, --model <name>',
|
|
216
|
-
'Embedding model: minilm
|
|
217
|
-
'
|
|
311
|
+
'Embedding model: minilm, jina-small, jina-base, jina-code (default), nomic, nomic-v1.5, bge-large. Run `codegraph models` for details',
|
|
312
|
+
'jina-code',
|
|
218
313
|
)
|
|
219
314
|
.action(async (dir, opts) => {
|
|
220
315
|
const root = path.resolve(dir || '.');
|
|
@@ -244,6 +339,53 @@ program
|
|
|
244
339
|
});
|
|
245
340
|
});
|
|
246
341
|
|
|
342
|
+
program
|
|
343
|
+
.command('structure [dir]')
|
|
344
|
+
.description(
|
|
345
|
+
'Show project directory structure with hierarchy, cohesion scores, and per-file metrics',
|
|
346
|
+
)
|
|
347
|
+
.option('-d, --db <path>', 'Path to graph.db')
|
|
348
|
+
.option('--depth <n>', 'Max directory depth')
|
|
349
|
+
.option('--sort <metric>', 'Sort by: cohesion | fan-in | fan-out | density | files', 'files')
|
|
350
|
+
.option('-j, --json', 'Output as JSON')
|
|
351
|
+
.action(async (dir, opts) => {
|
|
352
|
+
const { structureData, formatStructure } = await import('./structure.js');
|
|
353
|
+
const data = structureData(opts.db, {
|
|
354
|
+
directory: dir,
|
|
355
|
+
depth: opts.depth ? parseInt(opts.depth, 10) : undefined,
|
|
356
|
+
sort: opts.sort,
|
|
357
|
+
});
|
|
358
|
+
if (opts.json) {
|
|
359
|
+
console.log(JSON.stringify(data, null, 2));
|
|
360
|
+
} else {
|
|
361
|
+
console.log(formatStructure(data));
|
|
362
|
+
}
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
program
|
|
366
|
+
.command('hotspots')
|
|
367
|
+
.description(
|
|
368
|
+
'Find structural hotspots: files or directories with extreme fan-in, fan-out, or symbol density',
|
|
369
|
+
)
|
|
370
|
+
.option('-d, --db <path>', 'Path to graph.db')
|
|
371
|
+
.option('-n, --limit <number>', 'Number of results', '10')
|
|
372
|
+
.option('--metric <metric>', 'fan-in | fan-out | density | coupling', 'fan-in')
|
|
373
|
+
.option('--level <level>', 'file | directory', 'file')
|
|
374
|
+
.option('-j, --json', 'Output as JSON')
|
|
375
|
+
.action(async (opts) => {
|
|
376
|
+
const { hotspotsData, formatHotspots } = await import('./structure.js');
|
|
377
|
+
const data = hotspotsData(opts.db, {
|
|
378
|
+
metric: opts.metric,
|
|
379
|
+
level: opts.level,
|
|
380
|
+
limit: parseInt(opts.limit, 10),
|
|
381
|
+
});
|
|
382
|
+
if (opts.json) {
|
|
383
|
+
console.log(JSON.stringify(data, null, 2));
|
|
384
|
+
} else {
|
|
385
|
+
console.log(formatHotspots(data));
|
|
386
|
+
}
|
|
387
|
+
});
|
|
388
|
+
|
|
247
389
|
program
|
|
248
390
|
.command('watch [dir]')
|
|
249
391
|
.description('Watch project for file changes and incrementally update the graph')
|
package/src/config.js
CHANGED
|
@@ -19,7 +19,7 @@ export const DEFAULTS = {
|
|
|
19
19
|
defaultDepth: 3,
|
|
20
20
|
defaultLimit: 20,
|
|
21
21
|
},
|
|
22
|
-
embeddings: { model: '
|
|
22
|
+
embeddings: { model: 'jina-code', llmProvider: null },
|
|
23
23
|
llm: { provider: null, model: null, baseUrl: null, apiKey: null, apiKeyCommand: null },
|
|
24
24
|
search: { defaultMinScore: 0.2, rrfK: 60, topK: 15 },
|
|
25
25
|
ci: { failOnCycles: false, impactThreshold: null },
|
package/src/constants.js
CHANGED
|
@@ -20,8 +20,6 @@ export const IGNORE_DIRS = new Set([
|
|
|
20
20
|
'.env',
|
|
21
21
|
]);
|
|
22
22
|
|
|
23
|
-
// Re-export as an indirect binding to avoid TDZ in the circular
|
|
24
|
-
// parser.js ↔ constants.js import (no value read at evaluation time).
|
|
25
23
|
export { SUPPORTED_EXTENSIONS as EXTENSIONS };
|
|
26
24
|
|
|
27
25
|
export function shouldIgnore(dirName) {
|
package/src/cycles.js
CHANGED
|
@@ -31,8 +31,8 @@ export function findCycles(db, opts = {}) {
|
|
|
31
31
|
FROM edges e
|
|
32
32
|
JOIN nodes n1 ON e.source_id = n1.id
|
|
33
33
|
JOIN nodes n2 ON e.target_id = n2.id
|
|
34
|
-
WHERE n1.kind IN ('function', 'method', 'class')
|
|
35
|
-
AND n2.kind IN ('function', 'method', 'class')
|
|
34
|
+
WHERE n1.kind IN ('function', 'method', 'class', 'interface', 'type', 'struct', 'enum', 'trait', 'record', 'module')
|
|
35
|
+
AND n2.kind IN ('function', 'method', 'class', 'interface', 'type', 'struct', 'enum', 'trait', 'record', 'module')
|
|
36
36
|
AND e.kind = 'calls'
|
|
37
37
|
AND n1.id != n2.id
|
|
38
38
|
`)
|
package/src/db.js
CHANGED
|
@@ -33,6 +33,19 @@ export const MIGRATIONS = [
|
|
|
33
33
|
CREATE INDEX IF NOT EXISTS idx_edges_source ON edges(source_id);
|
|
34
34
|
CREATE INDEX IF NOT EXISTS idx_edges_target ON edges(target_id);
|
|
35
35
|
CREATE INDEX IF NOT EXISTS idx_edges_kind ON edges(kind);
|
|
36
|
+
CREATE TABLE IF NOT EXISTS node_metrics (
|
|
37
|
+
node_id INTEGER PRIMARY KEY,
|
|
38
|
+
line_count INTEGER,
|
|
39
|
+
symbol_count INTEGER,
|
|
40
|
+
import_count INTEGER,
|
|
41
|
+
export_count INTEGER,
|
|
42
|
+
fan_in INTEGER,
|
|
43
|
+
fan_out INTEGER,
|
|
44
|
+
cohesion REAL,
|
|
45
|
+
file_count INTEGER,
|
|
46
|
+
FOREIGN KEY(node_id) REFERENCES nodes(id)
|
|
47
|
+
);
|
|
48
|
+
CREATE INDEX IF NOT EXISTS idx_node_metrics_node ON node_metrics(node_id);
|
|
36
49
|
`,
|
|
37
50
|
},
|
|
38
51
|
{
|
package/src/embedder.js
CHANGED
|
@@ -55,7 +55,7 @@ export const MODELS = {
|
|
|
55
55
|
},
|
|
56
56
|
};
|
|
57
57
|
|
|
58
|
-
export const DEFAULT_MODEL = '
|
|
58
|
+
export const DEFAULT_MODEL = 'jina-code';
|
|
59
59
|
const BATCH_SIZE_MAP = {
|
|
60
60
|
minilm: 32,
|
|
61
61
|
'jina-small': 16,
|
|
@@ -173,10 +173,10 @@ function initEmbeddingsSchema(db) {
|
|
|
173
173
|
/**
|
|
174
174
|
* Build embeddings for all functions/methods/classes in the graph.
|
|
175
175
|
*/
|
|
176
|
-
export async function buildEmbeddings(rootDir, modelKey) {
|
|
176
|
+
export async function buildEmbeddings(rootDir, modelKey, customDbPath) {
|
|
177
177
|
// path already imported at top
|
|
178
178
|
// fs already imported at top
|
|
179
|
-
const dbPath = findDbPath(null);
|
|
179
|
+
const dbPath = customDbPath || findDbPath(null);
|
|
180
180
|
|
|
181
181
|
const db = new Database(dbPath);
|
|
182
182
|
initEmbeddingsSchema(db);
|
package/src/export.js
CHANGED
|
@@ -24,25 +24,60 @@ export function exportDOT(db, opts = {}) {
|
|
|
24
24
|
`)
|
|
25
25
|
.all();
|
|
26
26
|
|
|
27
|
+
// Try to use directory nodes from DB (built by structure analysis)
|
|
28
|
+
const hasDirectoryNodes =
|
|
29
|
+
db.prepare("SELECT COUNT(*) as c FROM nodes WHERE kind = 'directory'").get().c > 0;
|
|
30
|
+
|
|
27
31
|
const dirs = new Map();
|
|
28
32
|
const allFiles = new Set();
|
|
29
33
|
for (const { source, target } of edges) {
|
|
30
34
|
allFiles.add(source);
|
|
31
35
|
allFiles.add(target);
|
|
32
36
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
+
|
|
38
|
+
if (hasDirectoryNodes) {
|
|
39
|
+
// Use DB directory structure with cohesion labels
|
|
40
|
+
const dbDirs = db
|
|
41
|
+
.prepare(`
|
|
42
|
+
SELECT n.id, n.name, nm.cohesion
|
|
43
|
+
FROM nodes n
|
|
44
|
+
LEFT JOIN node_metrics nm ON n.id = nm.node_id
|
|
45
|
+
WHERE n.kind = 'directory'
|
|
46
|
+
`)
|
|
47
|
+
.all();
|
|
48
|
+
|
|
49
|
+
for (const d of dbDirs) {
|
|
50
|
+
const containedFiles = db
|
|
51
|
+
.prepare(`
|
|
52
|
+
SELECT n.name FROM edges e
|
|
53
|
+
JOIN nodes n ON e.target_id = n.id
|
|
54
|
+
WHERE e.source_id = ? AND e.kind = 'contains' AND n.kind = 'file'
|
|
55
|
+
`)
|
|
56
|
+
.all(d.id)
|
|
57
|
+
.map((r) => r.name)
|
|
58
|
+
.filter((f) => allFiles.has(f));
|
|
59
|
+
|
|
60
|
+
if (containedFiles.length > 0) {
|
|
61
|
+
dirs.set(d.name, { files: containedFiles, cohesion: d.cohesion });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
} else {
|
|
65
|
+
// Fallback: reconstruct from path.dirname()
|
|
66
|
+
for (const file of allFiles) {
|
|
67
|
+
const dir = path.dirname(file) || '.';
|
|
68
|
+
if (!dirs.has(dir)) dirs.set(dir, { files: [], cohesion: null });
|
|
69
|
+
dirs.get(dir).files.push(file);
|
|
70
|
+
}
|
|
37
71
|
}
|
|
38
72
|
|
|
39
73
|
let clusterIdx = 0;
|
|
40
|
-
for (const [dir,
|
|
74
|
+
for (const [dir, info] of [...dirs].sort((a, b) => a[0].localeCompare(b[0]))) {
|
|
41
75
|
lines.push(` subgraph cluster_${clusterIdx++} {`);
|
|
42
|
-
|
|
76
|
+
const cohLabel = info.cohesion !== null ? ` (cohesion: ${info.cohesion.toFixed(2)})` : '';
|
|
77
|
+
lines.push(` label="${dir}${cohLabel}";`);
|
|
43
78
|
lines.push(` style=dashed;`);
|
|
44
79
|
lines.push(` color="#999999";`);
|
|
45
|
-
for (const f of files) {
|
|
80
|
+
for (const f of info.files) {
|
|
46
81
|
const label = path.basename(f);
|
|
47
82
|
lines.push(` "${f}" [label="${label}"];`);
|
|
48
83
|
}
|
|
@@ -62,7 +97,7 @@ export function exportDOT(db, opts = {}) {
|
|
|
62
97
|
FROM edges e
|
|
63
98
|
JOIN nodes n1 ON e.source_id = n1.id
|
|
64
99
|
JOIN nodes n2 ON e.target_id = n2.id
|
|
65
|
-
WHERE n1.kind IN ('function', 'method', 'class') AND n2.kind IN ('function', 'method', 'class')
|
|
100
|
+
WHERE n1.kind IN ('function', 'method', 'class', 'interface', 'type', 'struct', 'enum', 'trait', 'record', 'module') AND n2.kind IN ('function', 'method', 'class', 'interface', 'type', 'struct', 'enum', 'trait', 'record', 'module')
|
|
66
101
|
AND e.kind = 'calls'
|
|
67
102
|
`)
|
|
68
103
|
.all();
|
|
@@ -111,7 +146,7 @@ export function exportMermaid(db, opts = {}) {
|
|
|
111
146
|
FROM edges e
|
|
112
147
|
JOIN nodes n1 ON e.source_id = n1.id
|
|
113
148
|
JOIN nodes n2 ON e.target_id = n2.id
|
|
114
|
-
WHERE n1.kind IN ('function', 'method', 'class') AND n2.kind IN ('function', 'method', 'class')
|
|
149
|
+
WHERE n1.kind IN ('function', 'method', 'class', 'interface', 'type', 'struct', 'enum', 'trait', 'record', 'module') AND n2.kind IN ('function', 'method', 'class', 'interface', 'type', 'struct', 'enum', 'trait', 'record', 'module')
|
|
115
150
|
AND e.kind = 'calls'
|
|
116
151
|
`)
|
|
117
152
|
.all();
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { findChild, nodeEndLine } from './helpers.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Extract symbols from C# files.
|
|
5
|
+
*/
|
|
6
|
+
export function extractCSharpSymbols(tree, _filePath) {
|
|
7
|
+
const definitions = [];
|
|
8
|
+
const calls = [];
|
|
9
|
+
const imports = [];
|
|
10
|
+
const classes = [];
|
|
11
|
+
const exports = [];
|
|
12
|
+
|
|
13
|
+
function findCSharpParentType(node) {
|
|
14
|
+
let current = node.parent;
|
|
15
|
+
while (current) {
|
|
16
|
+
if (
|
|
17
|
+
current.type === 'class_declaration' ||
|
|
18
|
+
current.type === 'struct_declaration' ||
|
|
19
|
+
current.type === 'interface_declaration' ||
|
|
20
|
+
current.type === 'enum_declaration' ||
|
|
21
|
+
current.type === 'record_declaration'
|
|
22
|
+
) {
|
|
23
|
+
const nameNode = current.childForFieldName('name');
|
|
24
|
+
return nameNode ? nameNode.text : null;
|
|
25
|
+
}
|
|
26
|
+
current = current.parent;
|
|
27
|
+
}
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function walkCSharpNode(node) {
|
|
32
|
+
switch (node.type) {
|
|
33
|
+
case 'class_declaration': {
|
|
34
|
+
const nameNode = node.childForFieldName('name');
|
|
35
|
+
if (nameNode) {
|
|
36
|
+
definitions.push({
|
|
37
|
+
name: nameNode.text,
|
|
38
|
+
kind: 'class',
|
|
39
|
+
line: node.startPosition.row + 1,
|
|
40
|
+
endLine: nodeEndLine(node),
|
|
41
|
+
});
|
|
42
|
+
extractCSharpBaseTypes(node, nameNode.text, classes);
|
|
43
|
+
}
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
case 'struct_declaration': {
|
|
48
|
+
const nameNode = node.childForFieldName('name');
|
|
49
|
+
if (nameNode) {
|
|
50
|
+
definitions.push({
|
|
51
|
+
name: nameNode.text,
|
|
52
|
+
kind: 'struct',
|
|
53
|
+
line: node.startPosition.row + 1,
|
|
54
|
+
endLine: nodeEndLine(node),
|
|
55
|
+
});
|
|
56
|
+
extractCSharpBaseTypes(node, nameNode.text, classes);
|
|
57
|
+
}
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
case 'record_declaration': {
|
|
62
|
+
const nameNode = node.childForFieldName('name');
|
|
63
|
+
if (nameNode) {
|
|
64
|
+
definitions.push({
|
|
65
|
+
name: nameNode.text,
|
|
66
|
+
kind: 'record',
|
|
67
|
+
line: node.startPosition.row + 1,
|
|
68
|
+
endLine: nodeEndLine(node),
|
|
69
|
+
});
|
|
70
|
+
extractCSharpBaseTypes(node, nameNode.text, classes);
|
|
71
|
+
}
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
case 'interface_declaration': {
|
|
76
|
+
const nameNode = node.childForFieldName('name');
|
|
77
|
+
if (nameNode) {
|
|
78
|
+
definitions.push({
|
|
79
|
+
name: nameNode.text,
|
|
80
|
+
kind: 'interface',
|
|
81
|
+
line: node.startPosition.row + 1,
|
|
82
|
+
endLine: nodeEndLine(node),
|
|
83
|
+
});
|
|
84
|
+
const body = node.childForFieldName('body');
|
|
85
|
+
if (body) {
|
|
86
|
+
for (let i = 0; i < body.childCount; i++) {
|
|
87
|
+
const child = body.child(i);
|
|
88
|
+
if (child && child.type === 'method_declaration') {
|
|
89
|
+
const methName = child.childForFieldName('name');
|
|
90
|
+
if (methName) {
|
|
91
|
+
definitions.push({
|
|
92
|
+
name: `${nameNode.text}.${methName.text}`,
|
|
93
|
+
kind: 'method',
|
|
94
|
+
line: child.startPosition.row + 1,
|
|
95
|
+
endLine: child.endPosition.row + 1,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
case 'enum_declaration': {
|
|
106
|
+
const nameNode = node.childForFieldName('name');
|
|
107
|
+
if (nameNode) {
|
|
108
|
+
definitions.push({
|
|
109
|
+
name: nameNode.text,
|
|
110
|
+
kind: 'enum',
|
|
111
|
+
line: node.startPosition.row + 1,
|
|
112
|
+
endLine: nodeEndLine(node),
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
case 'method_declaration': {
|
|
119
|
+
const nameNode = node.childForFieldName('name');
|
|
120
|
+
if (nameNode) {
|
|
121
|
+
const parentType = findCSharpParentType(node);
|
|
122
|
+
const fullName = parentType ? `${parentType}.${nameNode.text}` : nameNode.text;
|
|
123
|
+
definitions.push({
|
|
124
|
+
name: fullName,
|
|
125
|
+
kind: 'method',
|
|
126
|
+
line: node.startPosition.row + 1,
|
|
127
|
+
endLine: nodeEndLine(node),
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
case 'constructor_declaration': {
|
|
134
|
+
const nameNode = node.childForFieldName('name');
|
|
135
|
+
if (nameNode) {
|
|
136
|
+
const parentType = findCSharpParentType(node);
|
|
137
|
+
const fullName = parentType ? `${parentType}.${nameNode.text}` : nameNode.text;
|
|
138
|
+
definitions.push({
|
|
139
|
+
name: fullName,
|
|
140
|
+
kind: 'method',
|
|
141
|
+
line: node.startPosition.row + 1,
|
|
142
|
+
endLine: nodeEndLine(node),
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
case 'property_declaration': {
|
|
149
|
+
const nameNode = node.childForFieldName('name');
|
|
150
|
+
if (nameNode) {
|
|
151
|
+
const parentType = findCSharpParentType(node);
|
|
152
|
+
const fullName = parentType ? `${parentType}.${nameNode.text}` : nameNode.text;
|
|
153
|
+
definitions.push({
|
|
154
|
+
name: fullName,
|
|
155
|
+
kind: 'method',
|
|
156
|
+
line: node.startPosition.row + 1,
|
|
157
|
+
endLine: nodeEndLine(node),
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
case 'using_directive': {
|
|
164
|
+
// using System.Collections.Generic;
|
|
165
|
+
const nameNode =
|
|
166
|
+
node.childForFieldName('name') ||
|
|
167
|
+
findChild(node, 'qualified_name') ||
|
|
168
|
+
findChild(node, 'identifier');
|
|
169
|
+
if (nameNode) {
|
|
170
|
+
const fullPath = nameNode.text;
|
|
171
|
+
const lastName = fullPath.split('.').pop();
|
|
172
|
+
imports.push({
|
|
173
|
+
source: fullPath,
|
|
174
|
+
names: [lastName],
|
|
175
|
+
line: node.startPosition.row + 1,
|
|
176
|
+
csharpUsing: true,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
case 'invocation_expression': {
|
|
183
|
+
const fn = node.childForFieldName('function') || node.child(0);
|
|
184
|
+
if (fn) {
|
|
185
|
+
if (fn.type === 'identifier') {
|
|
186
|
+
calls.push({ name: fn.text, line: node.startPosition.row + 1 });
|
|
187
|
+
} else if (fn.type === 'member_access_expression') {
|
|
188
|
+
const name = fn.childForFieldName('name');
|
|
189
|
+
if (name) calls.push({ name: name.text, line: node.startPosition.row + 1 });
|
|
190
|
+
} else if (fn.type === 'generic_name' || fn.type === 'member_binding_expression') {
|
|
191
|
+
const name = fn.childForFieldName('name') || fn.child(0);
|
|
192
|
+
if (name) calls.push({ name: name.text, line: node.startPosition.row + 1 });
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
break;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
case 'object_creation_expression': {
|
|
199
|
+
const typeNode = node.childForFieldName('type');
|
|
200
|
+
if (typeNode) {
|
|
201
|
+
const typeName =
|
|
202
|
+
typeNode.type === 'generic_name'
|
|
203
|
+
? typeNode.childForFieldName('name')?.text || typeNode.child(0)?.text
|
|
204
|
+
: typeNode.text;
|
|
205
|
+
if (typeName) calls.push({ name: typeName, line: node.startPosition.row + 1 });
|
|
206
|
+
}
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
for (let i = 0; i < node.childCount; i++) walkCSharpNode(node.child(i));
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
walkCSharpNode(tree.rootNode);
|
|
215
|
+
return { definitions, calls, imports, classes, exports };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function extractCSharpBaseTypes(node, className, classes) {
|
|
219
|
+
const baseList = node.childForFieldName('bases');
|
|
220
|
+
if (!baseList) return;
|
|
221
|
+
for (let i = 0; i < baseList.childCount; i++) {
|
|
222
|
+
const child = baseList.child(i);
|
|
223
|
+
if (!child) continue;
|
|
224
|
+
if (child.type === 'identifier' || child.type === 'qualified_name') {
|
|
225
|
+
classes.push({ name: className, extends: child.text, line: node.startPosition.row + 1 });
|
|
226
|
+
} else if (child.type === 'generic_name') {
|
|
227
|
+
const name = child.childForFieldName('name') || child.child(0);
|
|
228
|
+
if (name)
|
|
229
|
+
classes.push({ name: className, extends: name.text, line: node.startPosition.row + 1 });
|
|
230
|
+
} else if (child.type === 'base_list') {
|
|
231
|
+
for (let j = 0; j < child.childCount; j++) {
|
|
232
|
+
const base = child.child(j);
|
|
233
|
+
if (base && (base.type === 'identifier' || base.type === 'qualified_name')) {
|
|
234
|
+
classes.push({ name: className, extends: base.text, line: node.startPosition.row + 1 });
|
|
235
|
+
} else if (base && base.type === 'generic_name') {
|
|
236
|
+
const name = base.childForFieldName('name') || base.child(0);
|
|
237
|
+
if (name)
|
|
238
|
+
classes.push({ name: className, extends: name.text, line: node.startPosition.row + 1 });
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|