@optave/codegraph 2.6.0 → 3.0.1
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 +111 -54
- package/package.json +5 -5
- package/src/ast.js +418 -0
- package/src/batch.js +93 -3
- package/src/builder.js +371 -103
- package/src/cfg.js +1452 -0
- package/src/change-journal.js +130 -0
- package/src/cli.js +415 -139
- package/src/complexity.js +8 -8
- package/src/dataflow.js +1190 -0
- package/src/db.js +96 -0
- package/src/embedder.js +16 -16
- package/src/export.js +305 -0
- package/src/extractors/csharp.js +64 -1
- package/src/extractors/go.js +66 -1
- package/src/extractors/hcl.js +22 -0
- package/src/extractors/java.js +61 -1
- package/src/extractors/javascript.js +193 -0
- package/src/extractors/php.js +79 -0
- package/src/extractors/python.js +134 -0
- package/src/extractors/ruby.js +89 -0
- package/src/extractors/rust.js +71 -1
- package/src/flow.js +5 -2
- package/src/index.js +52 -4
- package/src/mcp.js +403 -222
- package/src/paginate.js +3 -3
- package/src/parser.js +24 -0
- package/src/queries.js +362 -36
- package/src/structure.js +64 -8
- package/src/viewer.js +948 -0
- package/src/watcher.js +36 -1
package/src/db.js
CHANGED
|
@@ -144,6 +144,87 @@ export const MIGRATIONS = [
|
|
|
144
144
|
CREATE INDEX IF NOT EXISTS idx_fc_mi ON function_complexity(maintainability_index ASC);
|
|
145
145
|
`,
|
|
146
146
|
},
|
|
147
|
+
{
|
|
148
|
+
version: 10,
|
|
149
|
+
up: `
|
|
150
|
+
CREATE TABLE IF NOT EXISTS dataflow (
|
|
151
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
152
|
+
source_id INTEGER NOT NULL,
|
|
153
|
+
target_id INTEGER NOT NULL,
|
|
154
|
+
kind TEXT NOT NULL,
|
|
155
|
+
param_index INTEGER,
|
|
156
|
+
expression TEXT,
|
|
157
|
+
line INTEGER,
|
|
158
|
+
confidence REAL DEFAULT 1.0,
|
|
159
|
+
FOREIGN KEY(source_id) REFERENCES nodes(id),
|
|
160
|
+
FOREIGN KEY(target_id) REFERENCES nodes(id)
|
|
161
|
+
);
|
|
162
|
+
CREATE INDEX IF NOT EXISTS idx_dataflow_source ON dataflow(source_id);
|
|
163
|
+
CREATE INDEX IF NOT EXISTS idx_dataflow_target ON dataflow(target_id);
|
|
164
|
+
CREATE INDEX IF NOT EXISTS idx_dataflow_kind ON dataflow(kind);
|
|
165
|
+
CREATE INDEX IF NOT EXISTS idx_dataflow_source_kind ON dataflow(source_id, kind);
|
|
166
|
+
`,
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
version: 11,
|
|
170
|
+
up: `
|
|
171
|
+
ALTER TABLE nodes ADD COLUMN parent_id INTEGER REFERENCES nodes(id);
|
|
172
|
+
CREATE INDEX IF NOT EXISTS idx_nodes_parent ON nodes(parent_id);
|
|
173
|
+
CREATE INDEX IF NOT EXISTS idx_nodes_kind_parent ON nodes(kind, parent_id);
|
|
174
|
+
`,
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
version: 12,
|
|
178
|
+
up: `
|
|
179
|
+
CREATE TABLE IF NOT EXISTS cfg_blocks (
|
|
180
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
181
|
+
function_node_id INTEGER NOT NULL,
|
|
182
|
+
block_index INTEGER NOT NULL,
|
|
183
|
+
block_type TEXT NOT NULL,
|
|
184
|
+
start_line INTEGER,
|
|
185
|
+
end_line INTEGER,
|
|
186
|
+
label TEXT,
|
|
187
|
+
FOREIGN KEY(function_node_id) REFERENCES nodes(id),
|
|
188
|
+
UNIQUE(function_node_id, block_index)
|
|
189
|
+
);
|
|
190
|
+
CREATE INDEX IF NOT EXISTS idx_cfg_blocks_fn ON cfg_blocks(function_node_id);
|
|
191
|
+
|
|
192
|
+
CREATE TABLE IF NOT EXISTS cfg_edges (
|
|
193
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
194
|
+
function_node_id INTEGER NOT NULL,
|
|
195
|
+
source_block_id INTEGER NOT NULL,
|
|
196
|
+
target_block_id INTEGER NOT NULL,
|
|
197
|
+
kind TEXT NOT NULL,
|
|
198
|
+
FOREIGN KEY(function_node_id) REFERENCES nodes(id),
|
|
199
|
+
FOREIGN KEY(source_block_id) REFERENCES cfg_blocks(id),
|
|
200
|
+
FOREIGN KEY(target_block_id) REFERENCES cfg_blocks(id)
|
|
201
|
+
);
|
|
202
|
+
CREATE INDEX IF NOT EXISTS idx_cfg_edges_fn ON cfg_edges(function_node_id);
|
|
203
|
+
CREATE INDEX IF NOT EXISTS idx_cfg_edges_src ON cfg_edges(source_block_id);
|
|
204
|
+
CREATE INDEX IF NOT EXISTS idx_cfg_edges_tgt ON cfg_edges(target_block_id);
|
|
205
|
+
`,
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
version: 13,
|
|
209
|
+
up: `
|
|
210
|
+
CREATE TABLE IF NOT EXISTS ast_nodes (
|
|
211
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
212
|
+
file TEXT NOT NULL,
|
|
213
|
+
line INTEGER NOT NULL,
|
|
214
|
+
kind TEXT NOT NULL,
|
|
215
|
+
name TEXT NOT NULL,
|
|
216
|
+
text TEXT,
|
|
217
|
+
receiver TEXT,
|
|
218
|
+
parent_node_id INTEGER,
|
|
219
|
+
FOREIGN KEY(parent_node_id) REFERENCES nodes(id)
|
|
220
|
+
);
|
|
221
|
+
CREATE INDEX IF NOT EXISTS idx_ast_kind ON ast_nodes(kind);
|
|
222
|
+
CREATE INDEX IF NOT EXISTS idx_ast_name ON ast_nodes(name);
|
|
223
|
+
CREATE INDEX IF NOT EXISTS idx_ast_file ON ast_nodes(file);
|
|
224
|
+
CREATE INDEX IF NOT EXISTS idx_ast_parent ON ast_nodes(parent_node_id);
|
|
225
|
+
CREATE INDEX IF NOT EXISTS idx_ast_kind_name ON ast_nodes(kind, name);
|
|
226
|
+
`,
|
|
227
|
+
},
|
|
147
228
|
];
|
|
148
229
|
|
|
149
230
|
export function getBuildMeta(db, key) {
|
|
@@ -265,6 +346,21 @@ export function initSchema(db) {
|
|
|
265
346
|
} catch {
|
|
266
347
|
/* already exists */
|
|
267
348
|
}
|
|
349
|
+
try {
|
|
350
|
+
db.exec('ALTER TABLE nodes ADD COLUMN parent_id INTEGER REFERENCES nodes(id)');
|
|
351
|
+
} catch {
|
|
352
|
+
/* already exists */
|
|
353
|
+
}
|
|
354
|
+
try {
|
|
355
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_nodes_parent ON nodes(parent_id)');
|
|
356
|
+
} catch {
|
|
357
|
+
/* already exists */
|
|
358
|
+
}
|
|
359
|
+
try {
|
|
360
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_nodes_kind_parent ON nodes(kind, parent_id)');
|
|
361
|
+
} catch {
|
|
362
|
+
/* already exists */
|
|
363
|
+
}
|
|
268
364
|
}
|
|
269
365
|
|
|
270
366
|
export function findDbPath(customPath) {
|
package/src/embedder.js
CHANGED
|
@@ -4,6 +4,7 @@ import path from 'node:path';
|
|
|
4
4
|
import { createInterface } from 'node:readline';
|
|
5
5
|
import { closeDb, findDbPath, openDb, openReadonlyOrFail } from './db.js';
|
|
6
6
|
import { info, warn } from './logger.js';
|
|
7
|
+
import { normalizeSymbol } from './queries.js';
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Split an identifier into readable words.
|
|
@@ -582,7 +583,7 @@ function _prepareSearch(customDbPath, opts = {}) {
|
|
|
582
583
|
const noTests = opts.noTests || false;
|
|
583
584
|
const TEST_PATTERN = /\.(test|spec)\.|__test__|__tests__|\.stories\./;
|
|
584
585
|
let sql = `
|
|
585
|
-
SELECT e.node_id, e.vector, e.text_preview, n.name, n.kind, n.file, n.line
|
|
586
|
+
SELECT e.node_id, e.vector, e.text_preview, n.name, n.kind, n.file, n.line, n.end_line, n.role
|
|
586
587
|
FROM embeddings e
|
|
587
588
|
JOIN nodes n ON e.node_id = n.id
|
|
588
589
|
`;
|
|
@@ -638,6 +639,7 @@ export async function searchData(query, customDbPath, opts = {}) {
|
|
|
638
639
|
return null;
|
|
639
640
|
}
|
|
640
641
|
|
|
642
|
+
const hc = new Map();
|
|
641
643
|
const results = [];
|
|
642
644
|
for (const row of rows) {
|
|
643
645
|
const vec = new Float32Array(new Uint8Array(row.vector).buffer);
|
|
@@ -645,10 +647,7 @@ export async function searchData(query, customDbPath, opts = {}) {
|
|
|
645
647
|
|
|
646
648
|
if (sim >= minScore) {
|
|
647
649
|
results.push({
|
|
648
|
-
|
|
649
|
-
kind: row.kind,
|
|
650
|
-
file: row.file,
|
|
651
|
-
line: row.line,
|
|
650
|
+
...normalizeSymbol(row, db, hc),
|
|
652
651
|
similarity: sim,
|
|
653
652
|
});
|
|
654
653
|
}
|
|
@@ -734,14 +733,12 @@ export async function multiSearchData(queries, customDbPath, opts = {}) {
|
|
|
734
733
|
}
|
|
735
734
|
|
|
736
735
|
// Build results sorted by RRF score
|
|
736
|
+
const hc = new Map();
|
|
737
737
|
const results = [];
|
|
738
738
|
for (const [rowIndex, entry] of fusionMap) {
|
|
739
739
|
const row = rows[rowIndex];
|
|
740
740
|
results.push({
|
|
741
|
-
|
|
742
|
-
kind: row.kind,
|
|
743
|
-
file: row.file,
|
|
744
|
-
line: row.line,
|
|
741
|
+
...normalizeSymbol(row, db, hc),
|
|
745
742
|
rrf: entry.rrfScore,
|
|
746
743
|
queryScores: entry.queryScores,
|
|
747
744
|
});
|
|
@@ -804,7 +801,7 @@ export function ftsSearchData(query, customDbPath, opts = {}) {
|
|
|
804
801
|
|
|
805
802
|
let sql = `
|
|
806
803
|
SELECT f.rowid AS node_id, rank AS bm25_score,
|
|
807
|
-
n.name, n.kind, n.file, n.line
|
|
804
|
+
n.name, n.kind, n.file, n.line, n.end_line, n.role
|
|
808
805
|
FROM fts_index f
|
|
809
806
|
JOIN nodes n ON f.rowid = n.id
|
|
810
807
|
WHERE fts_index MATCH ?
|
|
@@ -841,16 +838,13 @@ export function ftsSearchData(query, customDbPath, opts = {}) {
|
|
|
841
838
|
rows = rows.filter((row) => !TEST_PATTERN.test(row.file));
|
|
842
839
|
}
|
|
843
840
|
|
|
844
|
-
|
|
845
|
-
|
|
841
|
+
const hc = new Map();
|
|
846
842
|
const results = rows.slice(0, limit).map((row) => ({
|
|
847
|
-
|
|
848
|
-
kind: row.kind,
|
|
849
|
-
file: row.file,
|
|
850
|
-
line: row.line,
|
|
843
|
+
...normalizeSymbol(row, db, hc),
|
|
851
844
|
bm25Score: -row.bm25_score, // FTS5 rank is negative; negate for display
|
|
852
845
|
}));
|
|
853
846
|
|
|
847
|
+
db.close();
|
|
854
848
|
return { results };
|
|
855
849
|
}
|
|
856
850
|
|
|
@@ -924,6 +918,9 @@ export async function hybridSearchData(query, customDbPath, opts = {}) {
|
|
|
924
918
|
kind: item.kind,
|
|
925
919
|
file: item.file,
|
|
926
920
|
line: item.line,
|
|
921
|
+
endLine: item.endLine ?? null,
|
|
922
|
+
role: item.role ?? null,
|
|
923
|
+
fileHash: item.fileHash ?? null,
|
|
927
924
|
rrfScore: 0,
|
|
928
925
|
bm25Score: null,
|
|
929
926
|
bm25Rank: null,
|
|
@@ -955,6 +952,9 @@ export async function hybridSearchData(query, customDbPath, opts = {}) {
|
|
|
955
952
|
kind: e.kind,
|
|
956
953
|
file: e.file,
|
|
957
954
|
line: e.line,
|
|
955
|
+
endLine: e.endLine,
|
|
956
|
+
role: e.role,
|
|
957
|
+
fileHash: e.fileHash,
|
|
958
958
|
rrf: e.rrfScore,
|
|
959
959
|
bm25Score: e.bm25Score,
|
|
960
960
|
bm25Rank: e.bm25Rank,
|
package/src/export.js
CHANGED
|
@@ -4,6 +4,25 @@ import { isTestFile } from './queries.js';
|
|
|
4
4
|
|
|
5
5
|
const DEFAULT_MIN_CONFIDENCE = 0.5;
|
|
6
6
|
|
|
7
|
+
/** Escape special XML characters. */
|
|
8
|
+
function escapeXml(s) {
|
|
9
|
+
return String(s)
|
|
10
|
+
.replace(/&/g, '&')
|
|
11
|
+
.replace(/</g, '<')
|
|
12
|
+
.replace(/>/g, '>')
|
|
13
|
+
.replace(/"/g, '"')
|
|
14
|
+
.replace(/'/g, ''');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** RFC 4180 CSV field escaping — quote fields containing commas, quotes, or newlines. */
|
|
18
|
+
function escapeCsv(s) {
|
|
19
|
+
const str = String(s);
|
|
20
|
+
if (str.includes(',') || str.includes('"') || str.includes('\n') || str.includes('\r')) {
|
|
21
|
+
return `"${str.replace(/"/g, '""')}"`;
|
|
22
|
+
}
|
|
23
|
+
return str;
|
|
24
|
+
}
|
|
25
|
+
|
|
7
26
|
/**
|
|
8
27
|
* Export the dependency graph in DOT (Graphviz) format.
|
|
9
28
|
*/
|
|
@@ -374,3 +393,289 @@ export function exportJSON(db, opts = {}) {
|
|
|
374
393
|
const base = { nodes, edges };
|
|
375
394
|
return paginateResult(base, 'edges', { limit: opts.limit, offset: opts.offset });
|
|
376
395
|
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Export the dependency graph in GraphML (XML) format.
|
|
399
|
+
*/
|
|
400
|
+
export function exportGraphML(db, opts = {}) {
|
|
401
|
+
const fileLevel = opts.fileLevel !== false;
|
|
402
|
+
const noTests = opts.noTests || false;
|
|
403
|
+
const minConf = opts.minConfidence ?? DEFAULT_MIN_CONFIDENCE;
|
|
404
|
+
const edgeLimit = opts.limit;
|
|
405
|
+
|
|
406
|
+
const lines = [
|
|
407
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
408
|
+
'<graphml xmlns="http://graphml.graphstruct.net/graphml">',
|
|
409
|
+
];
|
|
410
|
+
|
|
411
|
+
if (fileLevel) {
|
|
412
|
+
lines.push(' <key id="d0" for="node" attr.name="name" attr.type="string"/>');
|
|
413
|
+
lines.push(' <key id="d1" for="node" attr.name="file" attr.type="string"/>');
|
|
414
|
+
lines.push(' <key id="d2" for="edge" attr.name="kind" attr.type="string"/>');
|
|
415
|
+
lines.push(' <graph id="codegraph" edgedefault="directed">');
|
|
416
|
+
|
|
417
|
+
let edges = db
|
|
418
|
+
.prepare(`
|
|
419
|
+
SELECT DISTINCT n1.file AS source, n2.file AS target
|
|
420
|
+
FROM edges e
|
|
421
|
+
JOIN nodes n1 ON e.source_id = n1.id
|
|
422
|
+
JOIN nodes n2 ON e.target_id = n2.id
|
|
423
|
+
WHERE n1.file != n2.file AND e.kind IN ('imports', 'imports-type', 'calls')
|
|
424
|
+
AND e.confidence >= ?
|
|
425
|
+
`)
|
|
426
|
+
.all(minConf);
|
|
427
|
+
if (noTests) edges = edges.filter((e) => !isTestFile(e.source) && !isTestFile(e.target));
|
|
428
|
+
if (edgeLimit && edges.length > edgeLimit) edges = edges.slice(0, edgeLimit);
|
|
429
|
+
|
|
430
|
+
const files = new Set();
|
|
431
|
+
for (const { source, target } of edges) {
|
|
432
|
+
files.add(source);
|
|
433
|
+
files.add(target);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const fileIds = new Map();
|
|
437
|
+
let nIdx = 0;
|
|
438
|
+
for (const f of files) {
|
|
439
|
+
const id = `n${nIdx++}`;
|
|
440
|
+
fileIds.set(f, id);
|
|
441
|
+
lines.push(` <node id="${id}">`);
|
|
442
|
+
lines.push(` <data key="d0">${escapeXml(path.basename(f))}</data>`);
|
|
443
|
+
lines.push(` <data key="d1">${escapeXml(f)}</data>`);
|
|
444
|
+
lines.push(' </node>');
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
let eIdx = 0;
|
|
448
|
+
for (const { source, target } of edges) {
|
|
449
|
+
lines.push(
|
|
450
|
+
` <edge id="e${eIdx++}" source="${fileIds.get(source)}" target="${fileIds.get(target)}">`,
|
|
451
|
+
);
|
|
452
|
+
lines.push(' <data key="d2">imports</data>');
|
|
453
|
+
lines.push(' </edge>');
|
|
454
|
+
}
|
|
455
|
+
} else {
|
|
456
|
+
lines.push(' <key id="d0" for="node" attr.name="name" attr.type="string"/>');
|
|
457
|
+
lines.push(' <key id="d1" for="node" attr.name="kind" attr.type="string"/>');
|
|
458
|
+
lines.push(' <key id="d2" for="node" attr.name="file" attr.type="string"/>');
|
|
459
|
+
lines.push(' <key id="d3" for="node" attr.name="line" attr.type="int"/>');
|
|
460
|
+
lines.push(' <key id="d4" for="node" attr.name="role" attr.type="string"/>');
|
|
461
|
+
lines.push(' <key id="d5" for="edge" attr.name="kind" attr.type="string"/>');
|
|
462
|
+
lines.push(' <key id="d6" for="edge" attr.name="confidence" attr.type="double"/>');
|
|
463
|
+
lines.push(' <graph id="codegraph" edgedefault="directed">');
|
|
464
|
+
|
|
465
|
+
let edges = db
|
|
466
|
+
.prepare(`
|
|
467
|
+
SELECT n1.id AS source_id, n1.name AS source_name, n1.kind AS source_kind,
|
|
468
|
+
n1.file AS source_file, n1.line AS source_line, n1.role AS source_role,
|
|
469
|
+
n2.id AS target_id, n2.name AS target_name, n2.kind AS target_kind,
|
|
470
|
+
n2.file AS target_file, n2.line AS target_line, n2.role AS target_role,
|
|
471
|
+
e.kind AS edge_kind, e.confidence
|
|
472
|
+
FROM edges e
|
|
473
|
+
JOIN nodes n1 ON e.source_id = n1.id
|
|
474
|
+
JOIN nodes n2 ON e.target_id = n2.id
|
|
475
|
+
WHERE n1.kind IN ('function', 'method', 'class', 'interface', 'type', 'struct', 'enum', 'trait', 'record', 'module')
|
|
476
|
+
AND n2.kind IN ('function', 'method', 'class', 'interface', 'type', 'struct', 'enum', 'trait', 'record', 'module')
|
|
477
|
+
AND e.kind = 'calls'
|
|
478
|
+
AND e.confidence >= ?
|
|
479
|
+
`)
|
|
480
|
+
.all(minConf);
|
|
481
|
+
if (noTests)
|
|
482
|
+
edges = edges.filter((e) => !isTestFile(e.source_file) && !isTestFile(e.target_file));
|
|
483
|
+
if (edgeLimit && edges.length > edgeLimit) edges = edges.slice(0, edgeLimit);
|
|
484
|
+
|
|
485
|
+
const emittedNodes = new Set();
|
|
486
|
+
function emitNode(id, name, kind, file, line, role) {
|
|
487
|
+
if (emittedNodes.has(id)) return;
|
|
488
|
+
emittedNodes.add(id);
|
|
489
|
+
lines.push(` <node id="n${id}">`);
|
|
490
|
+
lines.push(` <data key="d0">${escapeXml(name)}</data>`);
|
|
491
|
+
lines.push(` <data key="d1">${escapeXml(kind)}</data>`);
|
|
492
|
+
lines.push(` <data key="d2">${escapeXml(file)}</data>`);
|
|
493
|
+
lines.push(` <data key="d3">${line}</data>`);
|
|
494
|
+
if (role) lines.push(` <data key="d4">${escapeXml(role)}</data>`);
|
|
495
|
+
lines.push(' </node>');
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
let eIdx = 0;
|
|
499
|
+
for (const e of edges) {
|
|
500
|
+
emitNode(
|
|
501
|
+
e.source_id,
|
|
502
|
+
e.source_name,
|
|
503
|
+
e.source_kind,
|
|
504
|
+
e.source_file,
|
|
505
|
+
e.source_line,
|
|
506
|
+
e.source_role,
|
|
507
|
+
);
|
|
508
|
+
emitNode(
|
|
509
|
+
e.target_id,
|
|
510
|
+
e.target_name,
|
|
511
|
+
e.target_kind,
|
|
512
|
+
e.target_file,
|
|
513
|
+
e.target_line,
|
|
514
|
+
e.target_role,
|
|
515
|
+
);
|
|
516
|
+
lines.push(` <edge id="e${eIdx++}" source="n${e.source_id}" target="n${e.target_id}">`);
|
|
517
|
+
lines.push(` <data key="d5">${escapeXml(e.edge_kind)}</data>`);
|
|
518
|
+
lines.push(` <data key="d6">${e.confidence}</data>`);
|
|
519
|
+
lines.push(' </edge>');
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
lines.push(' </graph>');
|
|
524
|
+
lines.push('</graphml>');
|
|
525
|
+
return lines.join('\n');
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* Export the dependency graph in TinkerPop GraphSON v3 format.
|
|
530
|
+
*/
|
|
531
|
+
export function exportGraphSON(db, opts = {}) {
|
|
532
|
+
const noTests = opts.noTests || false;
|
|
533
|
+
const minConf = opts.minConfidence ?? DEFAULT_MIN_CONFIDENCE;
|
|
534
|
+
|
|
535
|
+
let nodes = db
|
|
536
|
+
.prepare(`
|
|
537
|
+
SELECT id, name, kind, file, line, role FROM nodes
|
|
538
|
+
WHERE kind IN ('function', 'method', 'class', 'interface', 'type', 'struct', 'enum', 'trait', 'record', 'module', 'file')
|
|
539
|
+
`)
|
|
540
|
+
.all();
|
|
541
|
+
if (noTests) nodes = nodes.filter((n) => !isTestFile(n.file));
|
|
542
|
+
|
|
543
|
+
let edges = db
|
|
544
|
+
.prepare(`
|
|
545
|
+
SELECT e.rowid AS id, n1.id AS outV, n2.id AS inV, e.kind, e.confidence
|
|
546
|
+
FROM edges e
|
|
547
|
+
JOIN nodes n1 ON e.source_id = n1.id
|
|
548
|
+
JOIN nodes n2 ON e.target_id = n2.id
|
|
549
|
+
WHERE e.confidence >= ?
|
|
550
|
+
`)
|
|
551
|
+
.all(minConf);
|
|
552
|
+
if (noTests) {
|
|
553
|
+
const nodeIds = new Set(nodes.map((n) => n.id));
|
|
554
|
+
edges = edges.filter((e) => nodeIds.has(e.outV) && nodeIds.has(e.inV));
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
const vertices = nodes.map((n) => ({
|
|
558
|
+
id: n.id,
|
|
559
|
+
label: n.kind,
|
|
560
|
+
properties: {
|
|
561
|
+
name: [{ id: 0, value: n.name }],
|
|
562
|
+
file: [{ id: 0, value: n.file }],
|
|
563
|
+
...(n.line != null ? { line: [{ id: 0, value: n.line }] } : {}),
|
|
564
|
+
...(n.role ? { role: [{ id: 0, value: n.role }] } : {}),
|
|
565
|
+
},
|
|
566
|
+
}));
|
|
567
|
+
|
|
568
|
+
const gEdges = edges.map((e) => ({
|
|
569
|
+
id: e.id,
|
|
570
|
+
label: e.kind,
|
|
571
|
+
inV: e.inV,
|
|
572
|
+
outV: e.outV,
|
|
573
|
+
properties: {
|
|
574
|
+
confidence: e.confidence,
|
|
575
|
+
},
|
|
576
|
+
}));
|
|
577
|
+
|
|
578
|
+
const base = { vertices, edges: gEdges };
|
|
579
|
+
return paginateResult(base, 'edges', { limit: opts.limit, offset: opts.offset });
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* Export the dependency graph as Neo4j bulk-import CSV files.
|
|
584
|
+
* Returns { nodes: string, relationships: string }.
|
|
585
|
+
*/
|
|
586
|
+
export function exportNeo4jCSV(db, opts = {}) {
|
|
587
|
+
const fileLevel = opts.fileLevel !== false;
|
|
588
|
+
const noTests = opts.noTests || false;
|
|
589
|
+
const minConf = opts.minConfidence ?? DEFAULT_MIN_CONFIDENCE;
|
|
590
|
+
const edgeLimit = opts.limit;
|
|
591
|
+
|
|
592
|
+
if (fileLevel) {
|
|
593
|
+
let edges = db
|
|
594
|
+
.prepare(`
|
|
595
|
+
SELECT DISTINCT n1.file AS source, n2.file AS target, e.kind, e.confidence
|
|
596
|
+
FROM edges e
|
|
597
|
+
JOIN nodes n1 ON e.source_id = n1.id
|
|
598
|
+
JOIN nodes n2 ON e.target_id = n2.id
|
|
599
|
+
WHERE n1.file != n2.file AND e.kind IN ('imports', 'imports-type', 'calls')
|
|
600
|
+
AND e.confidence >= ?
|
|
601
|
+
`)
|
|
602
|
+
.all(minConf);
|
|
603
|
+
if (noTests) edges = edges.filter((e) => !isTestFile(e.source) && !isTestFile(e.target));
|
|
604
|
+
if (edgeLimit && edges.length > edgeLimit) edges = edges.slice(0, edgeLimit);
|
|
605
|
+
|
|
606
|
+
const files = new Map();
|
|
607
|
+
let idx = 0;
|
|
608
|
+
for (const { source, target } of edges) {
|
|
609
|
+
if (!files.has(source)) files.set(source, idx++);
|
|
610
|
+
if (!files.has(target)) files.set(target, idx++);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
const nodeLines = ['nodeId:ID,name,file:string,:LABEL'];
|
|
614
|
+
for (const [file, id] of files) {
|
|
615
|
+
nodeLines.push(`${id},${escapeCsv(path.basename(file))},${escapeCsv(file)},File`);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
const relLines = [':START_ID,:END_ID,:TYPE,confidence:float'];
|
|
619
|
+
for (const e of edges) {
|
|
620
|
+
const edgeType = e.kind.toUpperCase().replace(/-/g, '_');
|
|
621
|
+
relLines.push(`${files.get(e.source)},${files.get(e.target)},${edgeType},${e.confidence}`);
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
return { nodes: nodeLines.join('\n'), relationships: relLines.join('\n') };
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
let edges = db
|
|
628
|
+
.prepare(`
|
|
629
|
+
SELECT n1.id AS source_id, n1.name AS source_name, n1.kind AS source_kind,
|
|
630
|
+
n1.file AS source_file, n1.line AS source_line, n1.role AS source_role,
|
|
631
|
+
n2.id AS target_id, n2.name AS target_name, n2.kind AS target_kind,
|
|
632
|
+
n2.file AS target_file, n2.line AS target_line, n2.role AS target_role,
|
|
633
|
+
e.kind AS edge_kind, e.confidence
|
|
634
|
+
FROM edges e
|
|
635
|
+
JOIN nodes n1 ON e.source_id = n1.id
|
|
636
|
+
JOIN nodes n2 ON e.target_id = n2.id
|
|
637
|
+
WHERE n1.kind IN ('function', 'method', 'class', 'interface', 'type', 'struct', 'enum', 'trait', 'record', 'module')
|
|
638
|
+
AND n2.kind IN ('function', 'method', 'class', 'interface', 'type', 'struct', 'enum', 'trait', 'record', 'module')
|
|
639
|
+
AND e.kind = 'calls'
|
|
640
|
+
AND e.confidence >= ?
|
|
641
|
+
`)
|
|
642
|
+
.all(minConf);
|
|
643
|
+
if (noTests)
|
|
644
|
+
edges = edges.filter((e) => !isTestFile(e.source_file) && !isTestFile(e.target_file));
|
|
645
|
+
if (edgeLimit && edges.length > edgeLimit) edges = edges.slice(0, edgeLimit);
|
|
646
|
+
|
|
647
|
+
const emitted = new Set();
|
|
648
|
+
const nodeLines = ['nodeId:ID,name,kind,file:string,line:int,role,:LABEL'];
|
|
649
|
+
function emitNode(id, name, kind, file, line, role) {
|
|
650
|
+
if (emitted.has(id)) return;
|
|
651
|
+
emitted.add(id);
|
|
652
|
+
const label = kind.charAt(0).toUpperCase() + kind.slice(1);
|
|
653
|
+
nodeLines.push(
|
|
654
|
+
`${id},${escapeCsv(name)},${escapeCsv(kind)},${escapeCsv(file)},${line},${escapeCsv(role || '')},${label}`,
|
|
655
|
+
);
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
const relLines = [':START_ID,:END_ID,:TYPE,confidence:float'];
|
|
659
|
+
for (const e of edges) {
|
|
660
|
+
emitNode(
|
|
661
|
+
e.source_id,
|
|
662
|
+
e.source_name,
|
|
663
|
+
e.source_kind,
|
|
664
|
+
e.source_file,
|
|
665
|
+
e.source_line,
|
|
666
|
+
e.source_role,
|
|
667
|
+
);
|
|
668
|
+
emitNode(
|
|
669
|
+
e.target_id,
|
|
670
|
+
e.target_name,
|
|
671
|
+
e.target_kind,
|
|
672
|
+
e.target_file,
|
|
673
|
+
e.target_line,
|
|
674
|
+
e.target_role,
|
|
675
|
+
);
|
|
676
|
+
const edgeType = e.edge_kind.toUpperCase().replace(/-/g, '_');
|
|
677
|
+
relLines.push(`${e.source_id},${e.target_id},${edgeType},${e.confidence}`);
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
return { nodes: nodeLines.join('\n'), relationships: relLines.join('\n') };
|
|
681
|
+
}
|
package/src/extractors/csharp.js
CHANGED
|
@@ -33,11 +33,13 @@ export function extractCSharpSymbols(tree, _filePath) {
|
|
|
33
33
|
case 'class_declaration': {
|
|
34
34
|
const nameNode = node.childForFieldName('name');
|
|
35
35
|
if (nameNode) {
|
|
36
|
+
const classChildren = extractCSharpClassFields(node);
|
|
36
37
|
definitions.push({
|
|
37
38
|
name: nameNode.text,
|
|
38
39
|
kind: 'class',
|
|
39
40
|
line: node.startPosition.row + 1,
|
|
40
41
|
endLine: nodeEndLine(node),
|
|
42
|
+
children: classChildren.length > 0 ? classChildren : undefined,
|
|
41
43
|
});
|
|
42
44
|
extractCSharpBaseTypes(node, nameNode.text, classes);
|
|
43
45
|
}
|
|
@@ -47,11 +49,13 @@ export function extractCSharpSymbols(tree, _filePath) {
|
|
|
47
49
|
case 'struct_declaration': {
|
|
48
50
|
const nameNode = node.childForFieldName('name');
|
|
49
51
|
if (nameNode) {
|
|
52
|
+
const structChildren = extractCSharpClassFields(node);
|
|
50
53
|
definitions.push({
|
|
51
54
|
name: nameNode.text,
|
|
52
55
|
kind: 'struct',
|
|
53
56
|
line: node.startPosition.row + 1,
|
|
54
57
|
endLine: nodeEndLine(node),
|
|
58
|
+
children: structChildren.length > 0 ? structChildren : undefined,
|
|
55
59
|
});
|
|
56
60
|
extractCSharpBaseTypes(node, nameNode.text, classes);
|
|
57
61
|
}
|
|
@@ -105,11 +109,13 @@ export function extractCSharpSymbols(tree, _filePath) {
|
|
|
105
109
|
case 'enum_declaration': {
|
|
106
110
|
const nameNode = node.childForFieldName('name');
|
|
107
111
|
if (nameNode) {
|
|
112
|
+
const enumChildren = extractCSharpEnumMembers(node);
|
|
108
113
|
definitions.push({
|
|
109
114
|
name: nameNode.text,
|
|
110
115
|
kind: 'enum',
|
|
111
116
|
line: node.startPosition.row + 1,
|
|
112
117
|
endLine: nodeEndLine(node),
|
|
118
|
+
children: enumChildren.length > 0 ? enumChildren : undefined,
|
|
113
119
|
});
|
|
114
120
|
}
|
|
115
121
|
break;
|
|
@@ -120,11 +126,13 @@ export function extractCSharpSymbols(tree, _filePath) {
|
|
|
120
126
|
if (nameNode) {
|
|
121
127
|
const parentType = findCSharpParentType(node);
|
|
122
128
|
const fullName = parentType ? `${parentType}.${nameNode.text}` : nameNode.text;
|
|
129
|
+
const params = extractCSharpParameters(node.childForFieldName('parameters'));
|
|
123
130
|
definitions.push({
|
|
124
131
|
name: fullName,
|
|
125
132
|
kind: 'method',
|
|
126
133
|
line: node.startPosition.row + 1,
|
|
127
134
|
endLine: nodeEndLine(node),
|
|
135
|
+
children: params.length > 0 ? params : undefined,
|
|
128
136
|
});
|
|
129
137
|
}
|
|
130
138
|
break;
|
|
@@ -135,11 +143,13 @@ export function extractCSharpSymbols(tree, _filePath) {
|
|
|
135
143
|
if (nameNode) {
|
|
136
144
|
const parentType = findCSharpParentType(node);
|
|
137
145
|
const fullName = parentType ? `${parentType}.${nameNode.text}` : nameNode.text;
|
|
146
|
+
const params = extractCSharpParameters(node.childForFieldName('parameters'));
|
|
138
147
|
definitions.push({
|
|
139
148
|
name: fullName,
|
|
140
149
|
kind: 'method',
|
|
141
150
|
line: node.startPosition.row + 1,
|
|
142
151
|
endLine: nodeEndLine(node),
|
|
152
|
+
children: params.length > 0 ? params : undefined,
|
|
143
153
|
});
|
|
144
154
|
}
|
|
145
155
|
break;
|
|
@@ -152,7 +162,7 @@ export function extractCSharpSymbols(tree, _filePath) {
|
|
|
152
162
|
const fullName = parentType ? `${parentType}.${nameNode.text}` : nameNode.text;
|
|
153
163
|
definitions.push({
|
|
154
164
|
name: fullName,
|
|
155
|
-
kind: '
|
|
165
|
+
kind: 'property',
|
|
156
166
|
line: node.startPosition.row + 1,
|
|
157
167
|
endLine: nodeEndLine(node),
|
|
158
168
|
});
|
|
@@ -220,6 +230,59 @@ export function extractCSharpSymbols(tree, _filePath) {
|
|
|
220
230
|
return { definitions, calls, imports, classes, exports };
|
|
221
231
|
}
|
|
222
232
|
|
|
233
|
+
// ── Child extraction helpers ────────────────────────────────────────────────
|
|
234
|
+
|
|
235
|
+
function extractCSharpParameters(paramListNode) {
|
|
236
|
+
const params = [];
|
|
237
|
+
if (!paramListNode) return params;
|
|
238
|
+
for (let i = 0; i < paramListNode.childCount; i++) {
|
|
239
|
+
const param = paramListNode.child(i);
|
|
240
|
+
if (!param || param.type !== 'parameter') continue;
|
|
241
|
+
const nameNode = param.childForFieldName('name');
|
|
242
|
+
if (nameNode) {
|
|
243
|
+
params.push({ name: nameNode.text, kind: 'parameter', line: param.startPosition.row + 1 });
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
return params;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function extractCSharpClassFields(classNode) {
|
|
250
|
+
const fields = [];
|
|
251
|
+
const body = classNode.childForFieldName('body') || findChild(classNode, 'declaration_list');
|
|
252
|
+
if (!body) return fields;
|
|
253
|
+
for (let i = 0; i < body.childCount; i++) {
|
|
254
|
+
const member = body.child(i);
|
|
255
|
+
if (!member || member.type !== 'field_declaration') continue;
|
|
256
|
+
const varDecl = findChild(member, 'variable_declaration');
|
|
257
|
+
if (!varDecl) continue;
|
|
258
|
+
for (let j = 0; j < varDecl.childCount; j++) {
|
|
259
|
+
const child = varDecl.child(j);
|
|
260
|
+
if (!child || child.type !== 'variable_declarator') continue;
|
|
261
|
+
const nameNode = child.childForFieldName('name');
|
|
262
|
+
if (nameNode) {
|
|
263
|
+
fields.push({ name: nameNode.text, kind: 'property', line: member.startPosition.row + 1 });
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return fields;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function extractCSharpEnumMembers(enumNode) {
|
|
271
|
+
const constants = [];
|
|
272
|
+
const body =
|
|
273
|
+
enumNode.childForFieldName('body') || findChild(enumNode, 'enum_member_declaration_list');
|
|
274
|
+
if (!body) return constants;
|
|
275
|
+
for (let i = 0; i < body.childCount; i++) {
|
|
276
|
+
const member = body.child(i);
|
|
277
|
+
if (!member || member.type !== 'enum_member_declaration') continue;
|
|
278
|
+
const nameNode = member.childForFieldName('name');
|
|
279
|
+
if (nameNode) {
|
|
280
|
+
constants.push({ name: nameNode.text, kind: 'constant', line: member.startPosition.row + 1 });
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
return constants;
|
|
284
|
+
}
|
|
285
|
+
|
|
223
286
|
function extractCSharpBaseTypes(node, className, classes) {
|
|
224
287
|
const baseList = node.childForFieldName('bases');
|
|
225
288
|
if (!baseList) return;
|