@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/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('1.3.0')
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
- await startMCPServer(opts.db);
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 === 'minilm' ? ' (default)' : '';
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 (default), jina-small, jina-base, jina-code, nomic, nomic-v1.5, bge-large. Run `codegraph models` for details',
217
- 'minilm',
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: 'minilm', llmProvider: null },
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 = 'minilm';
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
- for (const file of allFiles) {
34
- const dir = path.dirname(file) || '.';
35
- if (!dirs.has(dir)) dirs.set(dir, []);
36
- dirs.get(dir).push(file);
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, files] of [...dirs].sort()) {
74
+ for (const [dir, info] of [...dirs].sort((a, b) => a[0].localeCompare(b[0]))) {
41
75
  lines.push(` subgraph cluster_${clusterIdx++} {`);
42
- lines.push(` label="${dir}";`);
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
+ }