@optave/codegraph 2.6.0 → 3.0.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 +109 -52
- package/package.json +5 -5
- package/src/ast.js +392 -0
- package/src/batch.js +93 -3
- package/src/builder.js +314 -95
- package/src/cfg.js +1451 -0
- package/src/change-journal.js +130 -0
- package/src/cli.js +411 -139
- package/src/complexity.js +8 -8
- package/src/dataflow.js +1187 -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 +142 -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/index.js +51 -3
- package/src/mcp.js +403 -222
- package/src/paginate.js +3 -3
- package/src/parser.js +8 -0
- package/src/queries.js +362 -36
- package/src/structure.js +4 -1
- package/src/viewer.js +948 -0
- package/src/watcher.js +36 -1
package/src/paginate.js
CHANGED
|
@@ -9,17 +9,16 @@
|
|
|
9
9
|
export const MCP_DEFAULTS = {
|
|
10
10
|
// Existing
|
|
11
11
|
list_functions: 100,
|
|
12
|
-
|
|
12
|
+
query: 10,
|
|
13
13
|
where: 50,
|
|
14
14
|
node_roles: 100,
|
|
15
|
-
list_entry_points: 100,
|
|
16
15
|
export_graph: 500,
|
|
17
16
|
// Smaller defaults for rich/nested results
|
|
18
|
-
fn_deps: 10,
|
|
19
17
|
fn_impact: 5,
|
|
20
18
|
context: 5,
|
|
21
19
|
explain: 10,
|
|
22
20
|
file_deps: 20,
|
|
21
|
+
file_exports: 20,
|
|
23
22
|
diff_impact: 30,
|
|
24
23
|
impact_analysis: 20,
|
|
25
24
|
semantic_search: 20,
|
|
@@ -31,6 +30,7 @@ export const MCP_DEFAULTS = {
|
|
|
31
30
|
communities: 20,
|
|
32
31
|
structure: 30,
|
|
33
32
|
triage: 20,
|
|
33
|
+
ast_query: 50,
|
|
34
34
|
};
|
|
35
35
|
|
|
36
36
|
/** Hard cap to prevent abuse via MCP. */
|
package/src/parser.js
CHANGED
|
@@ -142,6 +142,14 @@ function normalizeNativeSymbols(result) {
|
|
|
142
142
|
maintainabilityIndex: d.complexity.maintainabilityIndex ?? null,
|
|
143
143
|
}
|
|
144
144
|
: null,
|
|
145
|
+
children: d.children?.length
|
|
146
|
+
? d.children.map((c) => ({
|
|
147
|
+
name: c.name,
|
|
148
|
+
kind: c.kind,
|
|
149
|
+
line: c.line,
|
|
150
|
+
endLine: c.endLine ?? c.end_line ?? null,
|
|
151
|
+
}))
|
|
152
|
+
: undefined,
|
|
145
153
|
})),
|
|
146
154
|
calls: (result.calls || []).map((c) => ({
|
|
147
155
|
name: c.name,
|
package/src/queries.js
CHANGED
|
@@ -59,7 +59,9 @@ export const FALSE_POSITIVE_NAMES = new Set([
|
|
|
59
59
|
export const FALSE_POSITIVE_CALLER_THRESHOLD = 20;
|
|
60
60
|
|
|
61
61
|
const FUNCTION_KINDS = ['function', 'method', 'class'];
|
|
62
|
-
|
|
62
|
+
|
|
63
|
+
// Original 10 kinds — used as default query scope
|
|
64
|
+
export const CORE_SYMBOL_KINDS = [
|
|
63
65
|
'function',
|
|
64
66
|
'method',
|
|
65
67
|
'class',
|
|
@@ -72,6 +74,39 @@ export const ALL_SYMBOL_KINDS = [
|
|
|
72
74
|
'module',
|
|
73
75
|
];
|
|
74
76
|
|
|
77
|
+
// Sub-declaration kinds (Phase 1)
|
|
78
|
+
export const EXTENDED_SYMBOL_KINDS = [
|
|
79
|
+
'parameter',
|
|
80
|
+
'property',
|
|
81
|
+
'constant',
|
|
82
|
+
// Phase 2 (reserved, not yet extracted):
|
|
83
|
+
// 'constructor', 'namespace', 'decorator', 'getter', 'setter',
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
// Full set for --kind validation and MCP enum
|
|
87
|
+
export const EVERY_SYMBOL_KIND = [...CORE_SYMBOL_KINDS, ...EXTENDED_SYMBOL_KINDS];
|
|
88
|
+
|
|
89
|
+
// Backward compat: ALL_SYMBOL_KINDS stays as the core 10
|
|
90
|
+
export const ALL_SYMBOL_KINDS = CORE_SYMBOL_KINDS;
|
|
91
|
+
|
|
92
|
+
// ── Edge kind constants ─────────────────────────────────────────────
|
|
93
|
+
// Core edge kinds — coupling and dependency relationships
|
|
94
|
+
export const CORE_EDGE_KINDS = [
|
|
95
|
+
'imports',
|
|
96
|
+
'imports-type',
|
|
97
|
+
'reexports',
|
|
98
|
+
'calls',
|
|
99
|
+
'extends',
|
|
100
|
+
'implements',
|
|
101
|
+
'contains',
|
|
102
|
+
];
|
|
103
|
+
|
|
104
|
+
// Structural edge kinds — parent/child and type relationships
|
|
105
|
+
export const STRUCTURAL_EDGE_KINDS = ['parameter_of', 'receiver'];
|
|
106
|
+
|
|
107
|
+
// Full set for MCP enum and validation
|
|
108
|
+
export const EVERY_EDGE_KIND = [...CORE_EDGE_KINDS, ...STRUCTURAL_EDGE_KINDS];
|
|
109
|
+
|
|
75
110
|
export const VALID_ROLES = ['entry', 'core', 'utility', 'adapter', 'dead', 'leaf'];
|
|
76
111
|
|
|
77
112
|
/**
|
|
@@ -190,6 +225,12 @@ export function kindIcon(kind) {
|
|
|
190
225
|
return 'I';
|
|
191
226
|
case 'type':
|
|
192
227
|
return 'T';
|
|
228
|
+
case 'parameter':
|
|
229
|
+
return 'p';
|
|
230
|
+
case 'property':
|
|
231
|
+
return '.';
|
|
232
|
+
case 'constant':
|
|
233
|
+
return 'C';
|
|
193
234
|
default:
|
|
194
235
|
return '-';
|
|
195
236
|
}
|
|
@@ -207,6 +248,7 @@ export function queryNameData(name, customDbPath, opts = {}) {
|
|
|
207
248
|
return { query: name, results: [] };
|
|
208
249
|
}
|
|
209
250
|
|
|
251
|
+
const hc = new Map();
|
|
210
252
|
const results = nodes.map((node) => {
|
|
211
253
|
let callees = db
|
|
212
254
|
.prepare(`
|
|
@@ -230,10 +272,7 @@ export function queryNameData(name, customDbPath, opts = {}) {
|
|
|
230
272
|
}
|
|
231
273
|
|
|
232
274
|
return {
|
|
233
|
-
|
|
234
|
-
kind: node.kind,
|
|
235
|
-
file: node.file,
|
|
236
|
-
line: node.line,
|
|
275
|
+
...normalizeSymbol(node, db, hc),
|
|
237
276
|
callees: callees.map((c) => ({
|
|
238
277
|
name: c.name,
|
|
239
278
|
kind: c.kind,
|
|
@@ -327,12 +366,12 @@ export function moduleMapData(customDbPath, limit = 20, opts = {}) {
|
|
|
327
366
|
const nodes = db
|
|
328
367
|
.prepare(`
|
|
329
368
|
SELECT n.*,
|
|
330
|
-
(SELECT COUNT(*) FROM edges WHERE source_id = n.id AND kind
|
|
331
|
-
(SELECT COUNT(*) FROM edges WHERE target_id = n.id AND kind
|
|
369
|
+
(SELECT COUNT(*) FROM edges WHERE source_id = n.id AND kind NOT IN ('contains', 'parameter_of', 'receiver')) as out_edges,
|
|
370
|
+
(SELECT COUNT(*) FROM edges WHERE target_id = n.id AND kind NOT IN ('contains', 'parameter_of', 'receiver')) as in_edges
|
|
332
371
|
FROM nodes n
|
|
333
372
|
WHERE n.kind = 'file'
|
|
334
373
|
${testFilter}
|
|
335
|
-
ORDER BY (SELECT COUNT(*) FROM edges WHERE target_id = n.id AND kind
|
|
374
|
+
ORDER BY (SELECT COUNT(*) FROM edges WHERE target_id = n.id AND kind NOT IN ('contains', 'parameter_of', 'receiver')) DESC
|
|
336
375
|
LIMIT ?
|
|
337
376
|
`)
|
|
338
377
|
.all(limit);
|
|
@@ -402,6 +441,7 @@ export function fnDepsData(name, customDbPath, opts = {}) {
|
|
|
402
441
|
const db = openReadonlyOrFail(customDbPath);
|
|
403
442
|
const depth = opts.depth || 3;
|
|
404
443
|
const noTests = opts.noTests || false;
|
|
444
|
+
const hc = new Map();
|
|
405
445
|
|
|
406
446
|
const nodes = findMatchingNodes(db, name, { noTests, file: opts.file, kind: opts.kind });
|
|
407
447
|
if (nodes.length === 0) {
|
|
@@ -493,10 +533,7 @@ export function fnDepsData(name, customDbPath, opts = {}) {
|
|
|
493
533
|
}
|
|
494
534
|
|
|
495
535
|
return {
|
|
496
|
-
|
|
497
|
-
kind: node.kind,
|
|
498
|
-
file: node.file,
|
|
499
|
-
line: node.line,
|
|
536
|
+
...normalizeSymbol(node, db, hc),
|
|
500
537
|
callees: filteredCallees.map((c) => ({
|
|
501
538
|
name: c.name,
|
|
502
539
|
kind: c.kind,
|
|
@@ -523,6 +560,7 @@ export function fnImpactData(name, customDbPath, opts = {}) {
|
|
|
523
560
|
const db = openReadonlyOrFail(customDbPath);
|
|
524
561
|
const maxDepth = opts.depth || 5;
|
|
525
562
|
const noTests = opts.noTests || false;
|
|
563
|
+
const hc = new Map();
|
|
526
564
|
|
|
527
565
|
const nodes = findMatchingNodes(db, name, { noTests, file: opts.file, kind: opts.kind });
|
|
528
566
|
if (nodes.length === 0) {
|
|
@@ -559,10 +597,7 @@ export function fnImpactData(name, customDbPath, opts = {}) {
|
|
|
559
597
|
}
|
|
560
598
|
|
|
561
599
|
return {
|
|
562
|
-
|
|
563
|
-
kind: node.kind,
|
|
564
|
-
file: node.file,
|
|
565
|
-
line: node.line,
|
|
600
|
+
...normalizeSymbol(node, db, hc),
|
|
566
601
|
levels,
|
|
567
602
|
totalDependents: visited.size - 1,
|
|
568
603
|
};
|
|
@@ -1194,14 +1229,16 @@ export function listFunctionsData(customDbPath, opts = {}) {
|
|
|
1194
1229
|
|
|
1195
1230
|
let rows = db
|
|
1196
1231
|
.prepare(
|
|
1197
|
-
`SELECT name, kind, file, line, role FROM nodes WHERE ${conditions.join(' AND ')} ORDER BY file, line`,
|
|
1232
|
+
`SELECT name, kind, file, line, end_line, role FROM nodes WHERE ${conditions.join(' AND ')} ORDER BY file, line`,
|
|
1198
1233
|
)
|
|
1199
1234
|
.all(...params);
|
|
1200
1235
|
|
|
1201
1236
|
if (noTests) rows = rows.filter((r) => !isTestFile(r.file));
|
|
1202
1237
|
|
|
1238
|
+
const hc = new Map();
|
|
1239
|
+
const functions = rows.map((r) => normalizeSymbol(r, db, hc));
|
|
1203
1240
|
db.close();
|
|
1204
|
-
const base = { count:
|
|
1241
|
+
const base = { count: functions.length, functions };
|
|
1205
1242
|
return paginateResult(base, 'functions', { limit: opts.limit, offset: opts.offset });
|
|
1206
1243
|
}
|
|
1207
1244
|
|
|
@@ -1234,11 +1271,18 @@ export function* iterListFunctions(customDbPath, opts = {}) {
|
|
|
1234
1271
|
}
|
|
1235
1272
|
|
|
1236
1273
|
const stmt = db.prepare(
|
|
1237
|
-
`SELECT name, kind, file, line, role FROM nodes WHERE ${conditions.join(' AND ')} ORDER BY file, line`,
|
|
1274
|
+
`SELECT name, kind, file, line, end_line, role FROM nodes WHERE ${conditions.join(' AND ')} ORDER BY file, line`,
|
|
1238
1275
|
);
|
|
1239
1276
|
for (const row of stmt.iterate(...params)) {
|
|
1240
1277
|
if (noTests && isTestFile(row.file)) continue;
|
|
1241
|
-
yield {
|
|
1278
|
+
yield {
|
|
1279
|
+
name: row.name,
|
|
1280
|
+
kind: row.kind,
|
|
1281
|
+
file: row.file,
|
|
1282
|
+
line: row.line,
|
|
1283
|
+
endLine: row.end_line ?? null,
|
|
1284
|
+
role: row.role ?? null,
|
|
1285
|
+
};
|
|
1242
1286
|
}
|
|
1243
1287
|
} finally {
|
|
1244
1288
|
db.close();
|
|
@@ -1252,7 +1296,7 @@ export function* iterListFunctions(customDbPath, opts = {}) {
|
|
|
1252
1296
|
* @param {boolean} [opts.noTests]
|
|
1253
1297
|
* @param {string} [opts.role]
|
|
1254
1298
|
* @param {string} [opts.file]
|
|
1255
|
-
* @yields {{ name: string, kind: string, file: string, line: number, role: string }}
|
|
1299
|
+
* @yields {{ name: string, kind: string, file: string, line: number, endLine: number|null, role: string }}
|
|
1256
1300
|
*/
|
|
1257
1301
|
export function* iterRoles(customDbPath, opts = {}) {
|
|
1258
1302
|
const db = openReadonlyOrFail(customDbPath);
|
|
@@ -1271,11 +1315,18 @@ export function* iterRoles(customDbPath, opts = {}) {
|
|
|
1271
1315
|
}
|
|
1272
1316
|
|
|
1273
1317
|
const stmt = db.prepare(
|
|
1274
|
-
`SELECT name, kind, file, line, role FROM nodes WHERE ${conditions.join(' AND ')} ORDER BY role, file, line`,
|
|
1318
|
+
`SELECT name, kind, file, line, end_line, role FROM nodes WHERE ${conditions.join(' AND ')} ORDER BY role, file, line`,
|
|
1275
1319
|
);
|
|
1276
1320
|
for (const row of stmt.iterate(...params)) {
|
|
1277
1321
|
if (noTests && isTestFile(row.file)) continue;
|
|
1278
|
-
yield {
|
|
1322
|
+
yield {
|
|
1323
|
+
name: row.name,
|
|
1324
|
+
kind: row.kind,
|
|
1325
|
+
file: row.file,
|
|
1326
|
+
line: row.line,
|
|
1327
|
+
endLine: row.end_line ?? null,
|
|
1328
|
+
role: row.role ?? null,
|
|
1329
|
+
};
|
|
1279
1330
|
}
|
|
1280
1331
|
} finally {
|
|
1281
1332
|
db.close();
|
|
@@ -2214,6 +2265,17 @@ export function contextData(name, customDbPath, opts = {}) {
|
|
|
2214
2265
|
/* table may not exist */
|
|
2215
2266
|
}
|
|
2216
2267
|
|
|
2268
|
+
// Children (parameters, properties, constants)
|
|
2269
|
+
let nodeChildren = [];
|
|
2270
|
+
try {
|
|
2271
|
+
nodeChildren = db
|
|
2272
|
+
.prepare('SELECT name, kind, line, end_line FROM nodes WHERE parent_id = ? ORDER BY line')
|
|
2273
|
+
.all(node.id)
|
|
2274
|
+
.map((c) => ({ name: c.name, kind: c.kind, line: c.line, endLine: c.end_line || null }));
|
|
2275
|
+
} catch {
|
|
2276
|
+
/* parent_id column may not exist */
|
|
2277
|
+
}
|
|
2278
|
+
|
|
2217
2279
|
return {
|
|
2218
2280
|
name: node.name,
|
|
2219
2281
|
kind: node.kind,
|
|
@@ -2224,6 +2286,7 @@ export function contextData(name, customDbPath, opts = {}) {
|
|
|
2224
2286
|
source,
|
|
2225
2287
|
signature,
|
|
2226
2288
|
complexity: complexityMetrics,
|
|
2289
|
+
children: nodeChildren.length > 0 ? nodeChildren : undefined,
|
|
2227
2290
|
callees,
|
|
2228
2291
|
callers,
|
|
2229
2292
|
relatedTests,
|
|
@@ -2263,6 +2326,15 @@ export function context(name, customDbPath, opts = {}) {
|
|
|
2263
2326
|
console.log();
|
|
2264
2327
|
}
|
|
2265
2328
|
|
|
2329
|
+
// Children
|
|
2330
|
+
if (r.children && r.children.length > 0) {
|
|
2331
|
+
console.log(`## Children (${r.children.length})`);
|
|
2332
|
+
for (const c of r.children) {
|
|
2333
|
+
console.log(` ${kindIcon(c.kind)} ${c.name} :${c.line}`);
|
|
2334
|
+
}
|
|
2335
|
+
console.log();
|
|
2336
|
+
}
|
|
2337
|
+
|
|
2266
2338
|
// Complexity
|
|
2267
2339
|
if (r.complexity) {
|
|
2268
2340
|
const cx = r.complexity;
|
|
@@ -2335,6 +2407,69 @@ export function context(name, customDbPath, opts = {}) {
|
|
|
2335
2407
|
}
|
|
2336
2408
|
}
|
|
2337
2409
|
|
|
2410
|
+
// ─── childrenData ───────────────────────────────────────────────────────
|
|
2411
|
+
|
|
2412
|
+
export function childrenData(name, customDbPath, opts = {}) {
|
|
2413
|
+
const db = openReadonlyOrFail(customDbPath);
|
|
2414
|
+
const noTests = opts.noTests || false;
|
|
2415
|
+
|
|
2416
|
+
const nodes = findMatchingNodes(db, name, { noTests, file: opts.file, kind: opts.kind });
|
|
2417
|
+
if (nodes.length === 0) {
|
|
2418
|
+
db.close();
|
|
2419
|
+
return { name, results: [] };
|
|
2420
|
+
}
|
|
2421
|
+
|
|
2422
|
+
const results = nodes.map((node) => {
|
|
2423
|
+
let children;
|
|
2424
|
+
try {
|
|
2425
|
+
children = db
|
|
2426
|
+
.prepare('SELECT name, kind, line, end_line FROM nodes WHERE parent_id = ? ORDER BY line')
|
|
2427
|
+
.all(node.id);
|
|
2428
|
+
} catch {
|
|
2429
|
+
children = [];
|
|
2430
|
+
}
|
|
2431
|
+
if (noTests) children = children.filter((c) => !isTestFile(c.file || node.file));
|
|
2432
|
+
return {
|
|
2433
|
+
name: node.name,
|
|
2434
|
+
kind: node.kind,
|
|
2435
|
+
file: node.file,
|
|
2436
|
+
line: node.line,
|
|
2437
|
+
children: children.map((c) => ({
|
|
2438
|
+
name: c.name,
|
|
2439
|
+
kind: c.kind,
|
|
2440
|
+
line: c.line,
|
|
2441
|
+
endLine: c.end_line || null,
|
|
2442
|
+
})),
|
|
2443
|
+
};
|
|
2444
|
+
});
|
|
2445
|
+
|
|
2446
|
+
db.close();
|
|
2447
|
+
const base = { name, results };
|
|
2448
|
+
return paginateResult(base, 'results', { limit: opts.limit, offset: opts.offset });
|
|
2449
|
+
}
|
|
2450
|
+
|
|
2451
|
+
export function children(name, customDbPath, opts = {}) {
|
|
2452
|
+
const data = childrenData(name, customDbPath, opts);
|
|
2453
|
+
if (opts.json) {
|
|
2454
|
+
console.log(JSON.stringify(data, null, 2));
|
|
2455
|
+
return;
|
|
2456
|
+
}
|
|
2457
|
+
if (data.results.length === 0) {
|
|
2458
|
+
console.log(`No symbol matching "${name}"`);
|
|
2459
|
+
return;
|
|
2460
|
+
}
|
|
2461
|
+
for (const r of data.results) {
|
|
2462
|
+
console.log(`\n${kindIcon(r.kind)} ${r.name} ${r.file}:${r.line}`);
|
|
2463
|
+
if (r.children.length === 0) {
|
|
2464
|
+
console.log(' (no children)');
|
|
2465
|
+
} else {
|
|
2466
|
+
for (const c of r.children) {
|
|
2467
|
+
console.log(` ${kindIcon(c.kind)} ${c.name} :${c.line}`);
|
|
2468
|
+
}
|
|
2469
|
+
}
|
|
2470
|
+
}
|
|
2471
|
+
}
|
|
2472
|
+
|
|
2338
2473
|
// ─── explainData ────────────────────────────────────────────────────────
|
|
2339
2474
|
|
|
2340
2475
|
function isFileLikeTarget(target) {
|
|
@@ -2457,6 +2592,7 @@ function explainFunctionImpl(db, target, noTests, getFileLines) {
|
|
|
2457
2592
|
if (noTests) nodes = nodes.filter((n) => !isTestFile(n.file));
|
|
2458
2593
|
if (nodes.length === 0) return [];
|
|
2459
2594
|
|
|
2595
|
+
const hc = new Map();
|
|
2460
2596
|
return nodes.slice(0, 10).map((node) => {
|
|
2461
2597
|
const fileLines = getFileLines(node.file);
|
|
2462
2598
|
const lineCount = node.end_line ? node.end_line - node.line + 1 : null;
|
|
@@ -2514,12 +2650,7 @@ function explainFunctionImpl(db, target, noTests, getFileLines) {
|
|
|
2514
2650
|
}
|
|
2515
2651
|
|
|
2516
2652
|
return {
|
|
2517
|
-
|
|
2518
|
-
kind: node.kind,
|
|
2519
|
-
file: node.file,
|
|
2520
|
-
line: node.line,
|
|
2521
|
-
role: node.role || null,
|
|
2522
|
-
endLine: node.end_line || null,
|
|
2653
|
+
...normalizeSymbol(node, db, hc),
|
|
2523
2654
|
lineCount,
|
|
2524
2655
|
summary,
|
|
2525
2656
|
signature,
|
|
@@ -2732,6 +2863,41 @@ export function explain(target, customDbPath, opts = {}) {
|
|
|
2732
2863
|
|
|
2733
2864
|
// ─── whereData ──────────────────────────────────────────────────────────
|
|
2734
2865
|
|
|
2866
|
+
function getFileHash(db, file) {
|
|
2867
|
+
const row = db.prepare('SELECT hash FROM file_hashes WHERE file = ?').get(file);
|
|
2868
|
+
return row ? row.hash : null;
|
|
2869
|
+
}
|
|
2870
|
+
|
|
2871
|
+
/**
|
|
2872
|
+
* Normalize a raw DB/query row into the stable 7-field symbol shape.
|
|
2873
|
+
* @param {object} row - Raw row (from SELECT * or explicit columns)
|
|
2874
|
+
* @param {object} [db] - Open DB handle; when null, fileHash will be null
|
|
2875
|
+
* @param {Map} [hashCache] - Optional per-file cache to avoid repeated getFileHash calls
|
|
2876
|
+
* @returns {{ name: string, kind: string, file: string, line: number, endLine: number|null, role: string|null, fileHash: string|null }}
|
|
2877
|
+
*/
|
|
2878
|
+
export function normalizeSymbol(row, db, hashCache) {
|
|
2879
|
+
let fileHash = null;
|
|
2880
|
+
if (db) {
|
|
2881
|
+
if (hashCache) {
|
|
2882
|
+
if (!hashCache.has(row.file)) {
|
|
2883
|
+
hashCache.set(row.file, getFileHash(db, row.file));
|
|
2884
|
+
}
|
|
2885
|
+
fileHash = hashCache.get(row.file);
|
|
2886
|
+
} else {
|
|
2887
|
+
fileHash = getFileHash(db, row.file);
|
|
2888
|
+
}
|
|
2889
|
+
}
|
|
2890
|
+
return {
|
|
2891
|
+
name: row.name,
|
|
2892
|
+
kind: row.kind,
|
|
2893
|
+
file: row.file,
|
|
2894
|
+
line: row.line,
|
|
2895
|
+
endLine: row.end_line ?? row.endLine ?? null,
|
|
2896
|
+
role: row.role ?? null,
|
|
2897
|
+
fileHash,
|
|
2898
|
+
};
|
|
2899
|
+
}
|
|
2900
|
+
|
|
2735
2901
|
function whereSymbolImpl(db, target, noTests) {
|
|
2736
2902
|
const placeholders = ALL_SYMBOL_KINDS.map(() => '?').join(', ');
|
|
2737
2903
|
let nodes = db
|
|
@@ -2741,6 +2907,7 @@ function whereSymbolImpl(db, target, noTests) {
|
|
|
2741
2907
|
.all(`%${target}%`, ...ALL_SYMBOL_KINDS);
|
|
2742
2908
|
if (noTests) nodes = nodes.filter((n) => !isTestFile(n.file));
|
|
2743
2909
|
|
|
2910
|
+
const hc = new Map();
|
|
2744
2911
|
return nodes.map((node) => {
|
|
2745
2912
|
const crossFileCallers = db
|
|
2746
2913
|
.prepare(
|
|
@@ -2759,11 +2926,7 @@ function whereSymbolImpl(db, target, noTests) {
|
|
|
2759
2926
|
if (noTests) uses = uses.filter((u) => !isTestFile(u.file));
|
|
2760
2927
|
|
|
2761
2928
|
return {
|
|
2762
|
-
|
|
2763
|
-
kind: node.kind,
|
|
2764
|
-
file: node.file,
|
|
2765
|
-
line: node.line,
|
|
2766
|
-
role: node.role || null,
|
|
2929
|
+
...normalizeSymbol(node, db, hc),
|
|
2767
2930
|
exported,
|
|
2768
2931
|
uses: uses.map((u) => ({ name: u.name, file: u.file, line: u.line })),
|
|
2769
2932
|
};
|
|
@@ -2813,6 +2976,7 @@ function whereFileImpl(db, target) {
|
|
|
2813
2976
|
|
|
2814
2977
|
return {
|
|
2815
2978
|
file: fn.file,
|
|
2979
|
+
fileHash: getFileHash(db, fn.file),
|
|
2816
2980
|
symbols: symbols.map((s) => ({ name: s.name, kind: s.kind, line: s.line })),
|
|
2817
2981
|
imports,
|
|
2818
2982
|
importedBy,
|
|
@@ -2908,7 +3072,7 @@ export function rolesData(customDbPath, opts = {}) {
|
|
|
2908
3072
|
|
|
2909
3073
|
let rows = db
|
|
2910
3074
|
.prepare(
|
|
2911
|
-
`SELECT name, kind, file, line, role FROM nodes WHERE ${conditions.join(' AND ')} ORDER BY role, file, line`,
|
|
3075
|
+
`SELECT name, kind, file, line, end_line, role FROM nodes WHERE ${conditions.join(' AND ')} ORDER BY role, file, line`,
|
|
2912
3076
|
)
|
|
2913
3077
|
.all(...params);
|
|
2914
3078
|
|
|
@@ -2919,8 +3083,10 @@ export function rolesData(customDbPath, opts = {}) {
|
|
|
2919
3083
|
summary[r.role] = (summary[r.role] || 0) + 1;
|
|
2920
3084
|
}
|
|
2921
3085
|
|
|
3086
|
+
const hc = new Map();
|
|
3087
|
+
const symbols = rows.map((r) => normalizeSymbol(r, db, hc));
|
|
2922
3088
|
db.close();
|
|
2923
|
-
const base = { count:
|
|
3089
|
+
const base = { count: symbols.length, summary, symbols };
|
|
2924
3090
|
return paginateResult(base, 'symbols', { limit: opts.limit, offset: opts.offset });
|
|
2925
3091
|
}
|
|
2926
3092
|
|
|
@@ -2966,6 +3132,166 @@ export function roles(customDbPath, opts = {}) {
|
|
|
2966
3132
|
}
|
|
2967
3133
|
}
|
|
2968
3134
|
|
|
3135
|
+
// ─── exportsData ─────────────────────────────────────────────────────
|
|
3136
|
+
|
|
3137
|
+
function exportsFileImpl(db, target, noTests, getFileLines) {
|
|
3138
|
+
const fileNodes = db
|
|
3139
|
+
.prepare(`SELECT * FROM nodes WHERE file LIKE ? AND kind = 'file'`)
|
|
3140
|
+
.all(`%${target}%`);
|
|
3141
|
+
if (fileNodes.length === 0) return [];
|
|
3142
|
+
|
|
3143
|
+
return fileNodes.map((fn) => {
|
|
3144
|
+
const symbols = db
|
|
3145
|
+
.prepare(`SELECT * FROM nodes WHERE file = ? AND kind != 'file' ORDER BY line`)
|
|
3146
|
+
.all(fn.file);
|
|
3147
|
+
|
|
3148
|
+
// IDs of symbols that have incoming calls from other files (exported)
|
|
3149
|
+
const exportedIds = new Set(
|
|
3150
|
+
db
|
|
3151
|
+
.prepare(
|
|
3152
|
+
`SELECT DISTINCT e.target_id FROM edges e
|
|
3153
|
+
JOIN nodes caller ON e.source_id = caller.id
|
|
3154
|
+
JOIN nodes target ON e.target_id = target.id
|
|
3155
|
+
WHERE target.file = ? AND caller.file != ? AND e.kind = 'calls'`,
|
|
3156
|
+
)
|
|
3157
|
+
.all(fn.file, fn.file)
|
|
3158
|
+
.map((r) => r.target_id),
|
|
3159
|
+
);
|
|
3160
|
+
|
|
3161
|
+
const exported = symbols.filter((s) => exportedIds.has(s.id));
|
|
3162
|
+
const internalCount = symbols.length - exported.length;
|
|
3163
|
+
|
|
3164
|
+
const results = exported.map((s) => {
|
|
3165
|
+
const fileLines = getFileLines(fn.file);
|
|
3166
|
+
|
|
3167
|
+
let consumers = db
|
|
3168
|
+
.prepare(
|
|
3169
|
+
`SELECT n.name, n.file, n.line FROM edges e JOIN nodes n ON e.source_id = n.id
|
|
3170
|
+
WHERE e.target_id = ? AND e.kind = 'calls'`,
|
|
3171
|
+
)
|
|
3172
|
+
.all(s.id);
|
|
3173
|
+
if (noTests) consumers = consumers.filter((c) => !isTestFile(c.file));
|
|
3174
|
+
|
|
3175
|
+
return {
|
|
3176
|
+
name: s.name,
|
|
3177
|
+
kind: s.kind,
|
|
3178
|
+
line: s.line,
|
|
3179
|
+
endLine: s.end_line ?? null,
|
|
3180
|
+
role: s.role || null,
|
|
3181
|
+
signature: fileLines ? extractSignature(fileLines, s.line) : null,
|
|
3182
|
+
summary: fileLines ? extractSummary(fileLines, s.line) : null,
|
|
3183
|
+
consumers: consumers.map((c) => ({ name: c.name, file: c.file, line: c.line })),
|
|
3184
|
+
consumerCount: consumers.length,
|
|
3185
|
+
};
|
|
3186
|
+
});
|
|
3187
|
+
|
|
3188
|
+
// Files that re-export this file (barrel → this file)
|
|
3189
|
+
const reexports = db
|
|
3190
|
+
.prepare(
|
|
3191
|
+
`SELECT DISTINCT n.file FROM edges e JOIN nodes n ON e.source_id = n.id
|
|
3192
|
+
WHERE e.target_id = ? AND e.kind = 'reexports'`,
|
|
3193
|
+
)
|
|
3194
|
+
.all(fn.id)
|
|
3195
|
+
.map((r) => ({ file: r.file }));
|
|
3196
|
+
|
|
3197
|
+
return {
|
|
3198
|
+
file: fn.file,
|
|
3199
|
+
results,
|
|
3200
|
+
reexports,
|
|
3201
|
+
totalExported: exported.length,
|
|
3202
|
+
totalInternal: internalCount,
|
|
3203
|
+
};
|
|
3204
|
+
});
|
|
3205
|
+
}
|
|
3206
|
+
|
|
3207
|
+
export function exportsData(file, customDbPath, opts = {}) {
|
|
3208
|
+
const db = openReadonlyOrFail(customDbPath);
|
|
3209
|
+
const noTests = opts.noTests || false;
|
|
3210
|
+
|
|
3211
|
+
const dbFilePath = findDbPath(customDbPath);
|
|
3212
|
+
const repoRoot = path.resolve(path.dirname(dbFilePath), '..');
|
|
3213
|
+
|
|
3214
|
+
const fileCache = new Map();
|
|
3215
|
+
function getFileLines(file) {
|
|
3216
|
+
if (fileCache.has(file)) return fileCache.get(file);
|
|
3217
|
+
try {
|
|
3218
|
+
const absPath = safePath(repoRoot, file);
|
|
3219
|
+
if (!absPath) {
|
|
3220
|
+
fileCache.set(file, null);
|
|
3221
|
+
return null;
|
|
3222
|
+
}
|
|
3223
|
+
const lines = fs.readFileSync(absPath, 'utf-8').split('\n');
|
|
3224
|
+
fileCache.set(file, lines);
|
|
3225
|
+
return lines;
|
|
3226
|
+
} catch {
|
|
3227
|
+
fileCache.set(file, null);
|
|
3228
|
+
return null;
|
|
3229
|
+
}
|
|
3230
|
+
}
|
|
3231
|
+
|
|
3232
|
+
const fileResults = exportsFileImpl(db, file, noTests, getFileLines);
|
|
3233
|
+
db.close();
|
|
3234
|
+
|
|
3235
|
+
if (fileResults.length === 0) {
|
|
3236
|
+
return paginateResult(
|
|
3237
|
+
{ file, results: [], reexports: [], totalExported: 0, totalInternal: 0 },
|
|
3238
|
+
'results',
|
|
3239
|
+
{ limit: opts.limit, offset: opts.offset },
|
|
3240
|
+
);
|
|
3241
|
+
}
|
|
3242
|
+
|
|
3243
|
+
// For single-file match return flat; for multi-match return first (like explainData)
|
|
3244
|
+
const first = fileResults[0];
|
|
3245
|
+
const base = {
|
|
3246
|
+
file: first.file,
|
|
3247
|
+
results: first.results,
|
|
3248
|
+
reexports: first.reexports,
|
|
3249
|
+
totalExported: first.totalExported,
|
|
3250
|
+
totalInternal: first.totalInternal,
|
|
3251
|
+
};
|
|
3252
|
+
return paginateResult(base, 'results', { limit: opts.limit, offset: opts.offset });
|
|
3253
|
+
}
|
|
3254
|
+
|
|
3255
|
+
export function fileExports(file, customDbPath, opts = {}) {
|
|
3256
|
+
const data = exportsData(file, customDbPath, opts);
|
|
3257
|
+
if (opts.ndjson) {
|
|
3258
|
+
printNdjson(data, 'results');
|
|
3259
|
+
return;
|
|
3260
|
+
}
|
|
3261
|
+
if (opts.json) {
|
|
3262
|
+
console.log(JSON.stringify(data, null, 2));
|
|
3263
|
+
return;
|
|
3264
|
+
}
|
|
3265
|
+
|
|
3266
|
+
if (data.results.length === 0) {
|
|
3267
|
+
console.log(`No exported symbols found for "${file}". Run "codegraph build" first.`);
|
|
3268
|
+
return;
|
|
3269
|
+
}
|
|
3270
|
+
|
|
3271
|
+
console.log(
|
|
3272
|
+
`\n# ${data.file} — ${data.totalExported} exported, ${data.totalInternal} internal\n`,
|
|
3273
|
+
);
|
|
3274
|
+
|
|
3275
|
+
for (const sym of data.results) {
|
|
3276
|
+
const icon = kindIcon(sym.kind);
|
|
3277
|
+
const sig = sym.signature?.params ? `(${sym.signature.params})` : '';
|
|
3278
|
+
const role = sym.role ? ` [${sym.role}]` : '';
|
|
3279
|
+
console.log(` ${icon} ${sym.name}${sig}${role} :${sym.line}`);
|
|
3280
|
+
if (sym.consumers.length === 0) {
|
|
3281
|
+
console.log(' (no consumers)');
|
|
3282
|
+
} else {
|
|
3283
|
+
for (const c of sym.consumers) {
|
|
3284
|
+
console.log(` <- ${c.name} (${c.file}:${c.line})`);
|
|
3285
|
+
}
|
|
3286
|
+
}
|
|
3287
|
+
}
|
|
3288
|
+
|
|
3289
|
+
if (data.reexports.length > 0) {
|
|
3290
|
+
console.log(`\n Re-exports: ${data.reexports.map((r) => r.file).join(', ')}`);
|
|
3291
|
+
}
|
|
3292
|
+
console.log();
|
|
3293
|
+
}
|
|
3294
|
+
|
|
2969
3295
|
export function fnImpact(name, customDbPath, opts = {}) {
|
|
2970
3296
|
const data = fnImpactData(name, customDbPath, opts);
|
|
2971
3297
|
if (opts.ndjson) {
|
package/src/structure.js
CHANGED
|
@@ -34,8 +34,11 @@ export function buildStructure(db, fileSymbols, _rootDir, lineCountMap, director
|
|
|
34
34
|
`);
|
|
35
35
|
|
|
36
36
|
// Clean previous directory nodes/edges (idempotent rebuild)
|
|
37
|
+
// Scope contains-edge delete to directory-sourced edges only,
|
|
38
|
+
// preserving symbol-level contains edges (file→def, class→method, etc.)
|
|
37
39
|
db.exec(`
|
|
38
|
-
DELETE FROM edges WHERE kind = 'contains'
|
|
40
|
+
DELETE FROM edges WHERE kind = 'contains'
|
|
41
|
+
AND source_id IN (SELECT id FROM nodes WHERE kind = 'directory');
|
|
39
42
|
DELETE FROM node_metrics;
|
|
40
43
|
DELETE FROM nodes WHERE kind = 'directory';
|
|
41
44
|
`);
|