@optave/codegraph 3.0.4 → 3.1.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 +59 -52
- package/grammars/tree-sitter-go.wasm +0 -0
- package/package.json +9 -10
- package/src/ast-analysis/rules/csharp.js +201 -0
- package/src/ast-analysis/rules/go.js +182 -0
- package/src/ast-analysis/rules/index.js +82 -0
- package/src/ast-analysis/rules/java.js +175 -0
- package/src/ast-analysis/rules/javascript.js +246 -0
- package/src/ast-analysis/rules/php.js +219 -0
- package/src/ast-analysis/rules/python.js +196 -0
- package/src/ast-analysis/rules/ruby.js +204 -0
- package/src/ast-analysis/rules/rust.js +173 -0
- package/src/ast-analysis/shared.js +223 -0
- package/src/ast.js +15 -28
- package/src/audit.js +4 -5
- package/src/boundaries.js +1 -1
- package/src/branch-compare.js +84 -79
- package/src/builder.js +274 -159
- package/src/cfg.js +111 -341
- package/src/check.js +3 -3
- package/src/cli.js +122 -167
- package/src/cochange.js +1 -1
- package/src/communities.js +13 -16
- package/src/complexity.js +196 -1239
- package/src/cycles.js +1 -1
- package/src/dataflow.js +274 -697
- package/src/db/connection.js +88 -0
- package/src/db/migrations.js +312 -0
- package/src/db/query-builder.js +280 -0
- package/src/db/repository.js +134 -0
- package/src/db.js +19 -392
- package/src/embedder.js +145 -141
- package/src/export.js +1 -1
- package/src/flow.js +160 -228
- package/src/index.js +36 -2
- package/src/kinds.js +49 -0
- package/src/manifesto.js +3 -8
- package/src/mcp.js +97 -20
- package/src/owners.js +132 -132
- package/src/parser.js +58 -131
- package/src/queries-cli.js +866 -0
- package/src/queries.js +1356 -2261
- package/src/resolve.js +11 -2
- package/src/result-formatter.js +21 -0
- package/src/sequence.js +364 -0
- package/src/structure.js +200 -199
- package/src/test-filter.js +7 -0
- package/src/triage.js +120 -162
- package/src/viewer.js +1 -1
package/src/queries.js
CHANGED
|
@@ -5,11 +5,23 @@ import { evaluateBoundaries } from './boundaries.js';
|
|
|
5
5
|
import { coChangeForFiles } from './cochange.js';
|
|
6
6
|
import { loadConfig } from './config.js';
|
|
7
7
|
import { findCycles } from './cycles.js';
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
findDbPath,
|
|
10
|
+
findNodesWithFanIn,
|
|
11
|
+
iterateFunctionNodes,
|
|
12
|
+
listFunctionNodes,
|
|
13
|
+
openReadonlyOrFail,
|
|
14
|
+
testFilterSQL,
|
|
15
|
+
} from './db.js';
|
|
16
|
+
import { ALL_SYMBOL_KINDS } from './kinds.js';
|
|
9
17
|
import { debug } from './logger.js';
|
|
10
18
|
import { ownersForFiles } from './owners.js';
|
|
11
|
-
import { paginateResult
|
|
19
|
+
import { paginateResult } from './paginate.js';
|
|
12
20
|
import { LANGUAGE_REGISTRY } from './parser.js';
|
|
21
|
+
import { isTestFile } from './test-filter.js';
|
|
22
|
+
|
|
23
|
+
// Re-export from dedicated module for backward compat
|
|
24
|
+
export { isTestFile, TEST_PATTERN } from './test-filter.js';
|
|
13
25
|
|
|
14
26
|
/**
|
|
15
27
|
* Resolve a file path relative to repoRoot, rejecting traversal outside the repo.
|
|
@@ -21,11 +33,6 @@ function safePath(repoRoot, file) {
|
|
|
21
33
|
return resolved;
|
|
22
34
|
}
|
|
23
35
|
|
|
24
|
-
const TEST_PATTERN = /\.(test|spec)\.|__test__|__tests__|\.stories\./;
|
|
25
|
-
export function isTestFile(filePath) {
|
|
26
|
-
return TEST_PATTERN.test(filePath);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
36
|
export const FALSE_POSITIVE_NAMES = new Set([
|
|
30
37
|
'run',
|
|
31
38
|
'get',
|
|
@@ -60,54 +67,17 @@ export const FALSE_POSITIVE_CALLER_THRESHOLD = 20;
|
|
|
60
67
|
|
|
61
68
|
const FUNCTION_KINDS = ['function', 'method', 'class'];
|
|
62
69
|
|
|
63
|
-
//
|
|
64
|
-
export
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
'module',
|
|
75
|
-
];
|
|
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
|
-
|
|
110
|
-
export const VALID_ROLES = ['entry', 'core', 'utility', 'adapter', 'dead', 'leaf'];
|
|
70
|
+
// Re-export kind/edge constants from kinds.js (canonical source)
|
|
71
|
+
export {
|
|
72
|
+
ALL_SYMBOL_KINDS,
|
|
73
|
+
CORE_EDGE_KINDS,
|
|
74
|
+
CORE_SYMBOL_KINDS,
|
|
75
|
+
EVERY_EDGE_KIND,
|
|
76
|
+
EVERY_SYMBOL_KIND,
|
|
77
|
+
EXTENDED_SYMBOL_KINDS,
|
|
78
|
+
STRUCTURAL_EDGE_KINDS,
|
|
79
|
+
VALID_ROLES,
|
|
80
|
+
} from './kinds.js';
|
|
111
81
|
|
|
112
82
|
/**
|
|
113
83
|
* Get all ancestor class names for a given class using extends edges.
|
|
@@ -163,27 +133,10 @@ function resolveMethodViaHierarchy(db, methodName) {
|
|
|
163
133
|
* Find nodes matching a name query, ranked by relevance.
|
|
164
134
|
* Scoring: exact=100, prefix=60, word-boundary=40, substring=10, plus fan-in tiebreaker.
|
|
165
135
|
*/
|
|
166
|
-
function findMatchingNodes(db, name, opts = {}) {
|
|
167
|
-
const kinds = opts.kind ? [opts.kind] : FUNCTION_KINDS;
|
|
168
|
-
const placeholders = kinds.map(() => '?').join(', ');
|
|
169
|
-
const params = [`%${name}%`, ...kinds];
|
|
170
|
-
|
|
171
|
-
let fileCondition = '';
|
|
172
|
-
if (opts.file) {
|
|
173
|
-
fileCondition = ' AND n.file LIKE ?';
|
|
174
|
-
params.push(`%${opts.file}%`);
|
|
175
|
-
}
|
|
136
|
+
export function findMatchingNodes(db, name, opts = {}) {
|
|
137
|
+
const kinds = opts.kind ? [opts.kind] : opts.kinds?.length ? opts.kinds : FUNCTION_KINDS;
|
|
176
138
|
|
|
177
|
-
const rows = db
|
|
178
|
-
.prepare(`
|
|
179
|
-
SELECT n.*, COALESCE(fi.cnt, 0) AS fan_in
|
|
180
|
-
FROM nodes n
|
|
181
|
-
LEFT JOIN (
|
|
182
|
-
SELECT target_id, COUNT(*) AS cnt FROM edges WHERE kind = 'calls' GROUP BY target_id
|
|
183
|
-
) fi ON fi.target_id = n.id
|
|
184
|
-
WHERE n.name LIKE ? AND n.kind IN (${placeholders})${fileCondition}
|
|
185
|
-
`)
|
|
186
|
-
.all(...params);
|
|
139
|
+
const rows = findNodesWithFanIn(db, `%${name}%`, { kinds, file: opts.file });
|
|
187
140
|
|
|
188
141
|
const nodes = opts.noTests ? rows.filter((n) => !isTestFile(n.file)) : rows;
|
|
189
142
|
|
|
@@ -240,621 +193,570 @@ export function kindIcon(kind) {
|
|
|
240
193
|
|
|
241
194
|
export function queryNameData(name, customDbPath, opts = {}) {
|
|
242
195
|
const db = openReadonlyOrFail(customDbPath);
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
196
|
+
try {
|
|
197
|
+
const noTests = opts.noTests || false;
|
|
198
|
+
let nodes = db.prepare(`SELECT * FROM nodes WHERE name LIKE ?`).all(`%${name}%`);
|
|
199
|
+
if (noTests) nodes = nodes.filter((n) => !isTestFile(n.file));
|
|
200
|
+
if (nodes.length === 0) {
|
|
201
|
+
return { query: name, results: [] };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const hc = new Map();
|
|
205
|
+
const results = nodes.map((node) => {
|
|
206
|
+
let callees = db
|
|
207
|
+
.prepare(`
|
|
208
|
+
SELECT n.name, n.kind, n.file, n.line, e.kind as edge_kind
|
|
209
|
+
FROM edges e JOIN nodes n ON e.target_id = n.id
|
|
210
|
+
WHERE e.source_id = ?
|
|
211
|
+
`)
|
|
212
|
+
.all(node.id);
|
|
260
213
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
214
|
+
let callers = db
|
|
215
|
+
.prepare(`
|
|
216
|
+
SELECT n.name, n.kind, n.file, n.line, e.kind as edge_kind
|
|
217
|
+
FROM edges e JOIN nodes n ON e.source_id = n.id
|
|
218
|
+
WHERE e.target_id = ?
|
|
219
|
+
`)
|
|
220
|
+
.all(node.id);
|
|
268
221
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
222
|
+
if (noTests) {
|
|
223
|
+
callees = callees.filter((c) => !isTestFile(c.file));
|
|
224
|
+
callers = callers.filter((c) => !isTestFile(c.file));
|
|
225
|
+
}
|
|
273
226
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
227
|
+
return {
|
|
228
|
+
...normalizeSymbol(node, db, hc),
|
|
229
|
+
callees: callees.map((c) => ({
|
|
230
|
+
name: c.name,
|
|
231
|
+
kind: c.kind,
|
|
232
|
+
file: c.file,
|
|
233
|
+
line: c.line,
|
|
234
|
+
edgeKind: c.edge_kind,
|
|
235
|
+
})),
|
|
236
|
+
callers: callers.map((c) => ({
|
|
237
|
+
name: c.name,
|
|
238
|
+
kind: c.kind,
|
|
239
|
+
file: c.file,
|
|
240
|
+
line: c.line,
|
|
241
|
+
edgeKind: c.edge_kind,
|
|
242
|
+
})),
|
|
243
|
+
};
|
|
244
|
+
});
|
|
292
245
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
246
|
+
const base = { query: name, results };
|
|
247
|
+
return paginateResult(base, 'results', { limit: opts.limit, offset: opts.offset });
|
|
248
|
+
} finally {
|
|
249
|
+
db.close();
|
|
250
|
+
}
|
|
296
251
|
}
|
|
297
252
|
|
|
298
253
|
export function impactAnalysisData(file, customDbPath, opts = {}) {
|
|
299
254
|
const db = openReadonlyOrFail(customDbPath);
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
255
|
+
try {
|
|
256
|
+
const noTests = opts.noTests || false;
|
|
257
|
+
const fileNodes = db
|
|
258
|
+
.prepare(`SELECT * FROM nodes WHERE file LIKE ? AND kind = 'file'`)
|
|
259
|
+
.all(`%${file}%`);
|
|
260
|
+
if (fileNodes.length === 0) {
|
|
261
|
+
return { file, sources: [], levels: {}, totalDependents: 0 };
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const visited = new Set();
|
|
265
|
+
const queue = [];
|
|
266
|
+
const levels = new Map();
|
|
267
|
+
|
|
268
|
+
for (const fn of fileNodes) {
|
|
269
|
+
visited.add(fn.id);
|
|
270
|
+
queue.push(fn.id);
|
|
271
|
+
levels.set(fn.id, 0);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
while (queue.length > 0) {
|
|
275
|
+
const current = queue.shift();
|
|
276
|
+
const level = levels.get(current);
|
|
277
|
+
const dependents = db
|
|
278
|
+
.prepare(`
|
|
279
|
+
SELECT n.* FROM edges e JOIN nodes n ON e.source_id = n.id
|
|
280
|
+
WHERE e.target_id = ? AND e.kind IN ('imports', 'imports-type')
|
|
281
|
+
`)
|
|
282
|
+
.all(current);
|
|
283
|
+
for (const dep of dependents) {
|
|
284
|
+
if (!visited.has(dep.id) && (!noTests || !isTestFile(dep.file))) {
|
|
285
|
+
visited.add(dep.id);
|
|
286
|
+
queue.push(dep.id);
|
|
287
|
+
levels.set(dep.id, level + 1);
|
|
288
|
+
}
|
|
333
289
|
}
|
|
334
290
|
}
|
|
335
|
-
}
|
|
336
291
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
292
|
+
const byLevel = {};
|
|
293
|
+
for (const [id, level] of levels) {
|
|
294
|
+
if (level === 0) continue;
|
|
295
|
+
if (!byLevel[level]) byLevel[level] = [];
|
|
296
|
+
const node = db.prepare('SELECT * FROM nodes WHERE id = ?').get(id);
|
|
297
|
+
if (node) byLevel[level].push({ file: node.file });
|
|
298
|
+
}
|
|
344
299
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
}
|
|
300
|
+
return {
|
|
301
|
+
file,
|
|
302
|
+
sources: fileNodes.map((f) => f.file),
|
|
303
|
+
levels: byLevel,
|
|
304
|
+
totalDependents: visited.size - fileNodes.length,
|
|
305
|
+
};
|
|
306
|
+
} finally {
|
|
307
|
+
db.close();
|
|
308
|
+
}
|
|
352
309
|
}
|
|
353
310
|
|
|
354
311
|
export function moduleMapData(customDbPath, limit = 20, opts = {}) {
|
|
355
312
|
const db = openReadonlyOrFail(customDbPath);
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
const testFilter = noTests
|
|
359
|
-
? `AND n.file NOT LIKE '%.test.%'
|
|
360
|
-
AND n.file NOT LIKE '%.spec.%'
|
|
361
|
-
AND n.file NOT LIKE '%__test__%'
|
|
362
|
-
AND n.file NOT LIKE '%__tests__%'
|
|
363
|
-
AND n.file NOT LIKE '%.stories.%'`
|
|
364
|
-
: '';
|
|
365
|
-
|
|
366
|
-
const nodes = db
|
|
367
|
-
.prepare(`
|
|
368
|
-
SELECT n.*,
|
|
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
|
|
371
|
-
FROM nodes n
|
|
372
|
-
WHERE n.kind = 'file'
|
|
373
|
-
${testFilter}
|
|
374
|
-
ORDER BY (SELECT COUNT(*) FROM edges WHERE target_id = n.id AND kind NOT IN ('contains', 'parameter_of', 'receiver')) DESC
|
|
375
|
-
LIMIT ?
|
|
376
|
-
`)
|
|
377
|
-
.all(limit);
|
|
378
|
-
|
|
379
|
-
const topNodes = nodes.map((n) => ({
|
|
380
|
-
file: n.file,
|
|
381
|
-
dir: path.dirname(n.file) || '.',
|
|
382
|
-
inEdges: n.in_edges,
|
|
383
|
-
outEdges: n.out_edges,
|
|
384
|
-
coupling: n.in_edges + n.out_edges,
|
|
385
|
-
}));
|
|
386
|
-
|
|
387
|
-
const totalNodes = db.prepare('SELECT COUNT(*) as c FROM nodes').get().c;
|
|
388
|
-
const totalEdges = db.prepare('SELECT COUNT(*) as c FROM edges').get().c;
|
|
389
|
-
const totalFiles = db.prepare("SELECT COUNT(*) as c FROM nodes WHERE kind = 'file'").get().c;
|
|
390
|
-
|
|
391
|
-
db.close();
|
|
392
|
-
return { limit, topNodes, stats: { totalFiles, totalNodes, totalEdges } };
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
export function fileDepsData(file, customDbPath, opts = {}) {
|
|
396
|
-
const db = openReadonlyOrFail(customDbPath);
|
|
397
|
-
const noTests = opts.noTests || false;
|
|
398
|
-
const fileNodes = db
|
|
399
|
-
.prepare(`SELECT * FROM nodes WHERE file LIKE ? AND kind = 'file'`)
|
|
400
|
-
.all(`%${file}%`);
|
|
401
|
-
if (fileNodes.length === 0) {
|
|
402
|
-
db.close();
|
|
403
|
-
return { file, results: [] };
|
|
404
|
-
}
|
|
313
|
+
try {
|
|
314
|
+
const noTests = opts.noTests || false;
|
|
405
315
|
|
|
406
|
-
|
|
407
|
-
let importsTo = db
|
|
408
|
-
.prepare(`
|
|
409
|
-
SELECT n.file, e.kind as edge_kind FROM edges e JOIN nodes n ON e.target_id = n.id
|
|
410
|
-
WHERE e.source_id = ? AND e.kind IN ('imports', 'imports-type')
|
|
411
|
-
`)
|
|
412
|
-
.all(fn.id);
|
|
413
|
-
if (noTests) importsTo = importsTo.filter((i) => !isTestFile(i.file));
|
|
316
|
+
const testFilter = testFilterSQL('n.file', noTests);
|
|
414
317
|
|
|
415
|
-
|
|
318
|
+
const nodes = db
|
|
416
319
|
.prepare(`
|
|
417
|
-
SELECT n
|
|
418
|
-
|
|
320
|
+
SELECT n.*,
|
|
321
|
+
(SELECT COUNT(*) FROM edges WHERE source_id = n.id AND kind NOT IN ('contains', 'parameter_of', 'receiver')) as out_edges,
|
|
322
|
+
(SELECT COUNT(*) FROM edges WHERE target_id = n.id AND kind NOT IN ('contains', 'parameter_of', 'receiver')) as in_edges
|
|
323
|
+
FROM nodes n
|
|
324
|
+
WHERE n.kind = 'file'
|
|
325
|
+
${testFilter}
|
|
326
|
+
ORDER BY (SELECT COUNT(*) FROM edges WHERE target_id = n.id AND kind NOT IN ('contains', 'parameter_of', 'receiver')) DESC
|
|
327
|
+
LIMIT ?
|
|
419
328
|
`)
|
|
420
|
-
.all(
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
.
|
|
425
|
-
.
|
|
329
|
+
.all(limit);
|
|
330
|
+
|
|
331
|
+
const topNodes = nodes.map((n) => ({
|
|
332
|
+
file: n.file,
|
|
333
|
+
dir: path.dirname(n.file) || '.',
|
|
334
|
+
inEdges: n.in_edges,
|
|
335
|
+
outEdges: n.out_edges,
|
|
336
|
+
coupling: n.in_edges + n.out_edges,
|
|
337
|
+
}));
|
|
426
338
|
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
importedBy: importedBy.map((i) => ({ file: i.file })),
|
|
431
|
-
definitions: defs.map((d) => ({ name: d.name, kind: d.kind, line: d.line })),
|
|
432
|
-
};
|
|
433
|
-
});
|
|
339
|
+
const totalNodes = db.prepare('SELECT COUNT(*) as c FROM nodes').get().c;
|
|
340
|
+
const totalEdges = db.prepare('SELECT COUNT(*) as c FROM edges').get().c;
|
|
341
|
+
const totalFiles = db.prepare("SELECT COUNT(*) as c FROM nodes WHERE kind = 'file'").get().c;
|
|
434
342
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
343
|
+
return { limit, topNodes, stats: { totalFiles, totalNodes, totalEdges } };
|
|
344
|
+
} finally {
|
|
345
|
+
db.close();
|
|
346
|
+
}
|
|
438
347
|
}
|
|
439
348
|
|
|
440
|
-
export function
|
|
349
|
+
export function fileDepsData(file, customDbPath, opts = {}) {
|
|
441
350
|
const db = openReadonlyOrFail(customDbPath);
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
351
|
+
try {
|
|
352
|
+
const noTests = opts.noTests || false;
|
|
353
|
+
const fileNodes = db
|
|
354
|
+
.prepare(`SELECT * FROM nodes WHERE file LIKE ? AND kind = 'file'`)
|
|
355
|
+
.all(`%${file}%`);
|
|
356
|
+
if (fileNodes.length === 0) {
|
|
357
|
+
return { file, results: [] };
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const results = fileNodes.map((fn) => {
|
|
361
|
+
let importsTo = db
|
|
362
|
+
.prepare(`
|
|
363
|
+
SELECT n.file, e.kind as edge_kind FROM edges e JOIN nodes n ON e.target_id = n.id
|
|
364
|
+
WHERE e.source_id = ? AND e.kind IN ('imports', 'imports-type')
|
|
365
|
+
`)
|
|
366
|
+
.all(fn.id);
|
|
367
|
+
if (noTests) importsTo = importsTo.filter((i) => !isTestFile(i.file));
|
|
368
|
+
|
|
369
|
+
let importedBy = db
|
|
370
|
+
.prepare(`
|
|
371
|
+
SELECT n.file, e.kind as edge_kind FROM edges e JOIN nodes n ON e.source_id = n.id
|
|
372
|
+
WHERE e.target_id = ? AND e.kind IN ('imports', 'imports-type')
|
|
373
|
+
`)
|
|
374
|
+
.all(fn.id);
|
|
375
|
+
if (noTests) importedBy = importedBy.filter((i) => !isTestFile(i.file));
|
|
376
|
+
|
|
377
|
+
const defs = db
|
|
378
|
+
.prepare(`SELECT * FROM nodes WHERE file = ? AND kind != 'file' ORDER BY line`)
|
|
379
|
+
.all(fn.file);
|
|
380
|
+
|
|
381
|
+
return {
|
|
382
|
+
file: fn.file,
|
|
383
|
+
imports: importsTo.map((i) => ({ file: i.file, typeOnly: i.edge_kind === 'imports-type' })),
|
|
384
|
+
importedBy: importedBy.map((i) => ({ file: i.file })),
|
|
385
|
+
definitions: defs.map((d) => ({ name: d.name, kind: d.kind, line: d.line })),
|
|
386
|
+
};
|
|
387
|
+
});
|
|
445
388
|
|
|
446
|
-
|
|
447
|
-
|
|
389
|
+
const base = { file, results };
|
|
390
|
+
return paginateResult(base, 'results', { limit: opts.limit, offset: opts.offset });
|
|
391
|
+
} finally {
|
|
448
392
|
db.close();
|
|
449
|
-
return { name, results: [] };
|
|
450
393
|
}
|
|
394
|
+
}
|
|
451
395
|
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
`)
|
|
459
|
-
.all(node.id);
|
|
460
|
-
const filteredCallees = noTests ? callees.filter((c) => !isTestFile(c.file)) : callees;
|
|
461
|
-
|
|
462
|
-
let callers = db
|
|
463
|
-
.prepare(`
|
|
464
|
-
SELECT n.name, n.kind, n.file, n.line, e.kind as edge_kind
|
|
465
|
-
FROM edges e JOIN nodes n ON e.source_id = n.id
|
|
466
|
-
WHERE e.target_id = ? AND e.kind = 'calls'
|
|
467
|
-
`)
|
|
468
|
-
.all(node.id);
|
|
396
|
+
export function fnDepsData(name, customDbPath, opts = {}) {
|
|
397
|
+
const db = openReadonlyOrFail(customDbPath);
|
|
398
|
+
try {
|
|
399
|
+
const depth = opts.depth || 3;
|
|
400
|
+
const noTests = opts.noTests || false;
|
|
401
|
+
const hc = new Map();
|
|
469
402
|
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
for (const rm of relatedMethods) {
|
|
474
|
-
if (rm.id === node.id) continue;
|
|
475
|
-
const extraCallers = db
|
|
476
|
-
.prepare(`
|
|
477
|
-
SELECT n.name, n.kind, n.file, n.line, e.kind as edge_kind
|
|
478
|
-
FROM edges e JOIN nodes n ON e.source_id = n.id
|
|
479
|
-
WHERE e.target_id = ? AND e.kind = 'calls'
|
|
480
|
-
`)
|
|
481
|
-
.all(rm.id);
|
|
482
|
-
callers.push(...extraCallers.map((c) => ({ ...c, viaHierarchy: rm.name })));
|
|
483
|
-
}
|
|
403
|
+
const nodes = findMatchingNodes(db, name, { noTests, file: opts.file, kind: opts.kind });
|
|
404
|
+
if (nodes.length === 0) {
|
|
405
|
+
return { name, results: [] };
|
|
484
406
|
}
|
|
485
|
-
if (noTests) callers = callers.filter((c) => !isTestFile(c.file));
|
|
486
407
|
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
.
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
.
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
408
|
+
const results = nodes.map((node) => {
|
|
409
|
+
const callees = db
|
|
410
|
+
.prepare(`
|
|
411
|
+
SELECT n.name, n.kind, n.file, n.line, e.kind as edge_kind
|
|
412
|
+
FROM edges e JOIN nodes n ON e.target_id = n.id
|
|
413
|
+
WHERE e.source_id = ? AND e.kind = 'calls'
|
|
414
|
+
`)
|
|
415
|
+
.all(node.id);
|
|
416
|
+
const filteredCallees = noTests ? callees.filter((c) => !isTestFile(c.file)) : callees;
|
|
417
|
+
|
|
418
|
+
let callers = db
|
|
419
|
+
.prepare(`
|
|
420
|
+
SELECT n.name, n.kind, n.file, n.line, e.kind as edge_kind
|
|
421
|
+
FROM edges e JOIN nodes n ON e.source_id = n.id
|
|
422
|
+
WHERE e.target_id = ? AND e.kind = 'calls'
|
|
423
|
+
`)
|
|
424
|
+
.all(node.id);
|
|
425
|
+
|
|
426
|
+
if (node.kind === 'method' && node.name.includes('.')) {
|
|
427
|
+
const methodName = node.name.split('.').pop();
|
|
428
|
+
const relatedMethods = resolveMethodViaHierarchy(db, methodName);
|
|
429
|
+
for (const rm of relatedMethods) {
|
|
430
|
+
if (rm.id === node.id) continue;
|
|
431
|
+
const extraCallers = db
|
|
506
432
|
.prepare(`
|
|
507
|
-
SELECT n.name, n.kind, n.file, n.line
|
|
433
|
+
SELECT n.name, n.kind, n.file, n.line, e.kind as edge_kind
|
|
508
434
|
FROM edges e JOIN nodes n ON e.source_id = n.id
|
|
509
435
|
WHERE e.target_id = ? AND e.kind = 'calls'
|
|
510
436
|
`)
|
|
511
|
-
.all(
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
437
|
+
.all(rm.id);
|
|
438
|
+
callers.push(...extraCallers.map((c) => ({ ...c, viaHierarchy: rm.name })));
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
if (noTests) callers = callers.filter((c) => !isTestFile(c.file));
|
|
442
|
+
|
|
443
|
+
// Transitive callers
|
|
444
|
+
const transitiveCallers = {};
|
|
445
|
+
if (depth > 1) {
|
|
446
|
+
const visited = new Set([node.id]);
|
|
447
|
+
let frontier = callers
|
|
448
|
+
.map((c) => {
|
|
449
|
+
const row = db
|
|
515
450
|
.prepare('SELECT id FROM nodes WHERE name = ? AND kind = ? AND file = ? AND line = ?')
|
|
516
|
-
.get(
|
|
517
|
-
|
|
518
|
-
|
|
451
|
+
.get(c.name, c.kind, c.file, c.line);
|
|
452
|
+
return row ? { ...c, id: row.id } : null;
|
|
453
|
+
})
|
|
454
|
+
.filter(Boolean);
|
|
455
|
+
|
|
456
|
+
for (let d = 2; d <= depth; d++) {
|
|
457
|
+
const nextFrontier = [];
|
|
458
|
+
for (const f of frontier) {
|
|
459
|
+
if (visited.has(f.id)) continue;
|
|
460
|
+
visited.add(f.id);
|
|
461
|
+
const upstream = db
|
|
462
|
+
.prepare(`
|
|
463
|
+
SELECT n.name, n.kind, n.file, n.line
|
|
464
|
+
FROM edges e JOIN nodes n ON e.source_id = n.id
|
|
465
|
+
WHERE e.target_id = ? AND e.kind = 'calls'
|
|
466
|
+
`)
|
|
467
|
+
.all(f.id);
|
|
468
|
+
for (const u of upstream) {
|
|
469
|
+
if (noTests && isTestFile(u.file)) continue;
|
|
470
|
+
const uid = db
|
|
471
|
+
.prepare(
|
|
472
|
+
'SELECT id FROM nodes WHERE name = ? AND kind = ? AND file = ? AND line = ?',
|
|
473
|
+
)
|
|
474
|
+
.get(u.name, u.kind, u.file, u.line)?.id;
|
|
475
|
+
if (uid && !visited.has(uid)) {
|
|
476
|
+
nextFrontier.push({ ...u, id: uid });
|
|
477
|
+
}
|
|
519
478
|
}
|
|
520
479
|
}
|
|
480
|
+
if (nextFrontier.length > 0) {
|
|
481
|
+
transitiveCallers[d] = nextFrontier.map((n) => ({
|
|
482
|
+
name: n.name,
|
|
483
|
+
kind: n.kind,
|
|
484
|
+
file: n.file,
|
|
485
|
+
line: n.line,
|
|
486
|
+
}));
|
|
487
|
+
}
|
|
488
|
+
frontier = nextFrontier;
|
|
489
|
+
if (frontier.length === 0) break;
|
|
521
490
|
}
|
|
522
|
-
if (nextFrontier.length > 0) {
|
|
523
|
-
transitiveCallers[d] = nextFrontier.map((n) => ({
|
|
524
|
-
name: n.name,
|
|
525
|
-
kind: n.kind,
|
|
526
|
-
file: n.file,
|
|
527
|
-
line: n.line,
|
|
528
|
-
}));
|
|
529
|
-
}
|
|
530
|
-
frontier = nextFrontier;
|
|
531
|
-
if (frontier.length === 0) break;
|
|
532
491
|
}
|
|
533
|
-
}
|
|
534
492
|
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
493
|
+
return {
|
|
494
|
+
...normalizeSymbol(node, db, hc),
|
|
495
|
+
callees: filteredCallees.map((c) => ({
|
|
496
|
+
name: c.name,
|
|
497
|
+
kind: c.kind,
|
|
498
|
+
file: c.file,
|
|
499
|
+
line: c.line,
|
|
500
|
+
})),
|
|
501
|
+
callers: callers.map((c) => ({
|
|
502
|
+
name: c.name,
|
|
503
|
+
kind: c.kind,
|
|
504
|
+
file: c.file,
|
|
505
|
+
line: c.line,
|
|
506
|
+
viaHierarchy: c.viaHierarchy || undefined,
|
|
507
|
+
})),
|
|
508
|
+
transitiveCallers,
|
|
509
|
+
};
|
|
510
|
+
});
|
|
553
511
|
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
512
|
+
const base = { name, results };
|
|
513
|
+
return paginateResult(base, 'results', { limit: opts.limit, offset: opts.offset });
|
|
514
|
+
} finally {
|
|
515
|
+
db.close();
|
|
516
|
+
}
|
|
557
517
|
}
|
|
558
518
|
|
|
559
519
|
export function fnImpactData(name, customDbPath, opts = {}) {
|
|
560
520
|
const db = openReadonlyOrFail(customDbPath);
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
521
|
+
try {
|
|
522
|
+
const maxDepth = opts.depth || 5;
|
|
523
|
+
const noTests = opts.noTests || false;
|
|
524
|
+
const hc = new Map();
|
|
564
525
|
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
}
|
|
526
|
+
const nodes = findMatchingNodes(db, name, { noTests, file: opts.file, kind: opts.kind });
|
|
527
|
+
if (nodes.length === 0) {
|
|
528
|
+
return { name, results: [] };
|
|
529
|
+
}
|
|
570
530
|
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
531
|
+
const results = nodes.map((node) => {
|
|
532
|
+
const visited = new Set([node.id]);
|
|
533
|
+
const levels = {};
|
|
534
|
+
let frontier = [node.id];
|
|
535
|
+
|
|
536
|
+
for (let d = 1; d <= maxDepth; d++) {
|
|
537
|
+
const nextFrontier = [];
|
|
538
|
+
for (const fid of frontier) {
|
|
539
|
+
const callers = db
|
|
540
|
+
.prepare(`
|
|
541
|
+
SELECT DISTINCT n.id, n.name, n.kind, n.file, n.line
|
|
542
|
+
FROM edges e JOIN nodes n ON e.source_id = n.id
|
|
543
|
+
WHERE e.target_id = ? AND e.kind = 'calls'
|
|
544
|
+
`)
|
|
545
|
+
.all(fid);
|
|
546
|
+
for (const c of callers) {
|
|
547
|
+
if (!visited.has(c.id) && (!noTests || !isTestFile(c.file))) {
|
|
548
|
+
visited.add(c.id);
|
|
549
|
+
nextFrontier.push(c.id);
|
|
550
|
+
if (!levels[d]) levels[d] = [];
|
|
551
|
+
levels[d].push({ name: c.name, kind: c.kind, file: c.file, line: c.line });
|
|
552
|
+
}
|
|
592
553
|
}
|
|
593
554
|
}
|
|
555
|
+
frontier = nextFrontier;
|
|
556
|
+
if (frontier.length === 0) break;
|
|
594
557
|
}
|
|
595
|
-
frontier = nextFrontier;
|
|
596
|
-
if (frontier.length === 0) break;
|
|
597
|
-
}
|
|
598
558
|
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
559
|
+
return {
|
|
560
|
+
...normalizeSymbol(node, db, hc),
|
|
561
|
+
levels,
|
|
562
|
+
totalDependents: visited.size - 1,
|
|
563
|
+
};
|
|
564
|
+
});
|
|
605
565
|
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
566
|
+
const base = { name, results };
|
|
567
|
+
return paginateResult(base, 'results', { limit: opts.limit, offset: opts.offset });
|
|
568
|
+
} finally {
|
|
569
|
+
db.close();
|
|
570
|
+
}
|
|
609
571
|
}
|
|
610
572
|
|
|
611
573
|
export function pathData(from, to, customDbPath, opts = {}) {
|
|
612
574
|
const db = openReadonlyOrFail(customDbPath);
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
const fromNodes = findMatchingNodes(db, from, {
|
|
619
|
-
noTests,
|
|
620
|
-
file: opts.fromFile,
|
|
621
|
-
kind: opts.kind,
|
|
622
|
-
});
|
|
623
|
-
if (fromNodes.length === 0) {
|
|
624
|
-
db.close();
|
|
625
|
-
return {
|
|
626
|
-
from,
|
|
627
|
-
to,
|
|
628
|
-
found: false,
|
|
629
|
-
error: `No symbol matching "${from}"`,
|
|
630
|
-
fromCandidates: [],
|
|
631
|
-
toCandidates: [],
|
|
632
|
-
};
|
|
633
|
-
}
|
|
575
|
+
try {
|
|
576
|
+
const noTests = opts.noTests || false;
|
|
577
|
+
const maxDepth = opts.maxDepth || 10;
|
|
578
|
+
const edgeKinds = opts.edgeKinds || ['calls'];
|
|
579
|
+
const reverse = opts.reverse || false;
|
|
634
580
|
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
581
|
+
const fromNodes = findMatchingNodes(db, from, {
|
|
582
|
+
noTests,
|
|
583
|
+
file: opts.fromFile,
|
|
584
|
+
kind: opts.kind,
|
|
585
|
+
});
|
|
586
|
+
if (fromNodes.length === 0) {
|
|
587
|
+
return {
|
|
588
|
+
from,
|
|
589
|
+
to,
|
|
590
|
+
found: false,
|
|
591
|
+
error: `No symbol matching "${from}"`,
|
|
592
|
+
fromCandidates: [],
|
|
593
|
+
toCandidates: [],
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
const toNodes = findMatchingNodes(db, to, {
|
|
598
|
+
noTests,
|
|
599
|
+
file: opts.toFile,
|
|
600
|
+
kind: opts.kind,
|
|
601
|
+
});
|
|
602
|
+
if (toNodes.length === 0) {
|
|
603
|
+
return {
|
|
604
|
+
from,
|
|
605
|
+
to,
|
|
606
|
+
found: false,
|
|
607
|
+
error: `No symbol matching "${to}"`,
|
|
608
|
+
fromCandidates: fromNodes
|
|
609
|
+
.slice(0, 5)
|
|
610
|
+
.map((n) => ({ name: n.name, kind: n.kind, file: n.file, line: n.line })),
|
|
611
|
+
toCandidates: [],
|
|
612
|
+
};
|
|
613
|
+
}
|
|
653
614
|
|
|
654
|
-
|
|
655
|
-
|
|
615
|
+
const sourceNode = fromNodes[0];
|
|
616
|
+
const targetNode = toNodes[0];
|
|
656
617
|
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
618
|
+
const fromCandidates = fromNodes
|
|
619
|
+
.slice(0, 5)
|
|
620
|
+
.map((n) => ({ name: n.name, kind: n.kind, file: n.file, line: n.line }));
|
|
621
|
+
const toCandidates = toNodes
|
|
622
|
+
.slice(0, 5)
|
|
623
|
+
.map((n) => ({ name: n.name, kind: n.kind, file: n.file, line: n.line }));
|
|
663
624
|
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
}
|
|
688
|
-
}
|
|
625
|
+
// Self-path
|
|
626
|
+
if (sourceNode.id === targetNode.id) {
|
|
627
|
+
return {
|
|
628
|
+
from,
|
|
629
|
+
to,
|
|
630
|
+
fromCandidates,
|
|
631
|
+
toCandidates,
|
|
632
|
+
found: true,
|
|
633
|
+
hops: 0,
|
|
634
|
+
path: [
|
|
635
|
+
{
|
|
636
|
+
name: sourceNode.name,
|
|
637
|
+
kind: sourceNode.kind,
|
|
638
|
+
file: sourceNode.file,
|
|
639
|
+
line: sourceNode.line,
|
|
640
|
+
edgeKind: null,
|
|
641
|
+
},
|
|
642
|
+
],
|
|
643
|
+
alternateCount: 0,
|
|
644
|
+
edgeKinds,
|
|
645
|
+
reverse,
|
|
646
|
+
maxDepth,
|
|
647
|
+
};
|
|
648
|
+
}
|
|
689
649
|
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
650
|
+
// Build edge kind filter
|
|
651
|
+
const kindPlaceholders = edgeKinds.map(() => '?').join(', ');
|
|
652
|
+
|
|
653
|
+
// BFS — direction depends on `reverse` flag
|
|
654
|
+
// Forward: source_id → target_id (A calls... calls B)
|
|
655
|
+
// Reverse: target_id → source_id (B is called by... called by A)
|
|
656
|
+
const neighborQuery = reverse
|
|
657
|
+
? `SELECT n.id, n.name, n.kind, n.file, n.line, e.kind AS edge_kind
|
|
658
|
+
FROM edges e JOIN nodes n ON e.source_id = n.id
|
|
659
|
+
WHERE e.target_id = ? AND e.kind IN (${kindPlaceholders})`
|
|
660
|
+
: `SELECT n.id, n.name, n.kind, n.file, n.line, e.kind AS edge_kind
|
|
661
|
+
FROM edges e JOIN nodes n ON e.target_id = n.id
|
|
662
|
+
WHERE e.source_id = ? AND e.kind IN (${kindPlaceholders})`;
|
|
663
|
+
const neighborStmt = db.prepare(neighborQuery);
|
|
664
|
+
|
|
665
|
+
const visited = new Set([sourceNode.id]);
|
|
666
|
+
// parent map: nodeId → { parentId, edgeKind }
|
|
667
|
+
const parent = new Map();
|
|
668
|
+
let queue = [sourceNode.id];
|
|
669
|
+
let found = false;
|
|
670
|
+
let alternateCount = 0;
|
|
671
|
+
let foundDepth = -1;
|
|
672
|
+
|
|
673
|
+
for (let depth = 1; depth <= maxDepth; depth++) {
|
|
674
|
+
const nextQueue = [];
|
|
675
|
+
for (const currentId of queue) {
|
|
676
|
+
const neighbors = neighborStmt.all(currentId, ...edgeKinds);
|
|
677
|
+
for (const n of neighbors) {
|
|
678
|
+
if (noTests && isTestFile(n.file)) continue;
|
|
679
|
+
if (n.id === targetNode.id) {
|
|
680
|
+
if (!found) {
|
|
681
|
+
found = true;
|
|
682
|
+
foundDepth = depth;
|
|
683
|
+
parent.set(n.id, { parentId: currentId, edgeKind: n.edge_kind });
|
|
684
|
+
}
|
|
685
|
+
alternateCount++;
|
|
686
|
+
continue;
|
|
687
|
+
}
|
|
688
|
+
if (!visited.has(n.id)) {
|
|
689
|
+
visited.add(n.id);
|
|
723
690
|
parent.set(n.id, { parentId: currentId, edgeKind: n.edge_kind });
|
|
691
|
+
nextQueue.push(n.id);
|
|
724
692
|
}
|
|
725
|
-
alternateCount++;
|
|
726
|
-
continue;
|
|
727
|
-
}
|
|
728
|
-
if (!visited.has(n.id)) {
|
|
729
|
-
visited.add(n.id);
|
|
730
|
-
parent.set(n.id, { parentId: currentId, edgeKind: n.edge_kind });
|
|
731
|
-
nextQueue.push(n.id);
|
|
732
693
|
}
|
|
733
694
|
}
|
|
695
|
+
if (found) break;
|
|
696
|
+
queue = nextQueue;
|
|
697
|
+
if (queue.length === 0) break;
|
|
734
698
|
}
|
|
735
|
-
if (found) break;
|
|
736
|
-
queue = nextQueue;
|
|
737
|
-
if (queue.length === 0) break;
|
|
738
|
-
}
|
|
739
699
|
|
|
740
|
-
|
|
741
|
-
|
|
700
|
+
if (!found) {
|
|
701
|
+
return {
|
|
702
|
+
from,
|
|
703
|
+
to,
|
|
704
|
+
fromCandidates,
|
|
705
|
+
toCandidates,
|
|
706
|
+
found: false,
|
|
707
|
+
hops: null,
|
|
708
|
+
path: [],
|
|
709
|
+
alternateCount: 0,
|
|
710
|
+
edgeKinds,
|
|
711
|
+
reverse,
|
|
712
|
+
maxDepth,
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
// alternateCount includes the one we kept; subtract 1 for "alternates"
|
|
717
|
+
alternateCount = Math.max(0, alternateCount - 1);
|
|
718
|
+
|
|
719
|
+
// Reconstruct path from target back to source
|
|
720
|
+
const pathIds = [targetNode.id];
|
|
721
|
+
let cur = targetNode.id;
|
|
722
|
+
while (cur !== sourceNode.id) {
|
|
723
|
+
const p = parent.get(cur);
|
|
724
|
+
pathIds.push(p.parentId);
|
|
725
|
+
cur = p.parentId;
|
|
726
|
+
}
|
|
727
|
+
pathIds.reverse();
|
|
728
|
+
|
|
729
|
+
// Build path with node info
|
|
730
|
+
const nodeCache = new Map();
|
|
731
|
+
const getNode = (id) => {
|
|
732
|
+
if (nodeCache.has(id)) return nodeCache.get(id);
|
|
733
|
+
const row = db.prepare('SELECT name, kind, file, line FROM nodes WHERE id = ?').get(id);
|
|
734
|
+
nodeCache.set(id, row);
|
|
735
|
+
return row;
|
|
736
|
+
};
|
|
737
|
+
|
|
738
|
+
const resultPath = pathIds.map((id, idx) => {
|
|
739
|
+
const node = getNode(id);
|
|
740
|
+
const edgeKind = idx === 0 ? null : parent.get(id).edgeKind;
|
|
741
|
+
return { name: node.name, kind: node.kind, file: node.file, line: node.line, edgeKind };
|
|
742
|
+
});
|
|
743
|
+
|
|
742
744
|
return {
|
|
743
745
|
from,
|
|
744
746
|
to,
|
|
745
747
|
fromCandidates,
|
|
746
748
|
toCandidates,
|
|
747
|
-
found:
|
|
748
|
-
hops:
|
|
749
|
-
path:
|
|
750
|
-
alternateCount
|
|
749
|
+
found: true,
|
|
750
|
+
hops: foundDepth,
|
|
751
|
+
path: resultPath,
|
|
752
|
+
alternateCount,
|
|
751
753
|
edgeKinds,
|
|
752
754
|
reverse,
|
|
753
755
|
maxDepth,
|
|
754
756
|
};
|
|
757
|
+
} finally {
|
|
758
|
+
db.close();
|
|
755
759
|
}
|
|
756
|
-
|
|
757
|
-
// alternateCount includes the one we kept; subtract 1 for "alternates"
|
|
758
|
-
alternateCount = Math.max(0, alternateCount - 1);
|
|
759
|
-
|
|
760
|
-
// Reconstruct path from target back to source
|
|
761
|
-
const pathIds = [targetNode.id];
|
|
762
|
-
let cur = targetNode.id;
|
|
763
|
-
while (cur !== sourceNode.id) {
|
|
764
|
-
const p = parent.get(cur);
|
|
765
|
-
pathIds.push(p.parentId);
|
|
766
|
-
cur = p.parentId;
|
|
767
|
-
}
|
|
768
|
-
pathIds.reverse();
|
|
769
|
-
|
|
770
|
-
// Build path with node info
|
|
771
|
-
const nodeCache = new Map();
|
|
772
|
-
const getNode = (id) => {
|
|
773
|
-
if (nodeCache.has(id)) return nodeCache.get(id);
|
|
774
|
-
const row = db.prepare('SELECT name, kind, file, line FROM nodes WHERE id = ?').get(id);
|
|
775
|
-
nodeCache.set(id, row);
|
|
776
|
-
return row;
|
|
777
|
-
};
|
|
778
|
-
|
|
779
|
-
const resultPath = pathIds.map((id, idx) => {
|
|
780
|
-
const node = getNode(id);
|
|
781
|
-
const edgeKind = idx === 0 ? null : parent.get(id).edgeKind;
|
|
782
|
-
return { name: node.name, kind: node.kind, file: node.file, line: node.line, edgeKind };
|
|
783
|
-
});
|
|
784
|
-
|
|
785
|
-
db.close();
|
|
786
|
-
return {
|
|
787
|
-
from,
|
|
788
|
-
to,
|
|
789
|
-
fromCandidates,
|
|
790
|
-
toCandidates,
|
|
791
|
-
found: true,
|
|
792
|
-
hops: foundDepth,
|
|
793
|
-
path: resultPath,
|
|
794
|
-
alternateCount,
|
|
795
|
-
edgeKinds,
|
|
796
|
-
reverse,
|
|
797
|
-
maxDepth,
|
|
798
|
-
};
|
|
799
|
-
}
|
|
800
|
-
|
|
801
|
-
export function symbolPath(from, to, customDbPath, opts = {}) {
|
|
802
|
-
const data = pathData(from, to, customDbPath, opts);
|
|
803
|
-
if (opts.json) {
|
|
804
|
-
console.log(JSON.stringify(data, null, 2));
|
|
805
|
-
return;
|
|
806
|
-
}
|
|
807
|
-
|
|
808
|
-
if (data.error) {
|
|
809
|
-
console.log(data.error);
|
|
810
|
-
return;
|
|
811
|
-
}
|
|
812
|
-
|
|
813
|
-
if (!data.found) {
|
|
814
|
-
const dir = data.reverse ? 'reverse ' : '';
|
|
815
|
-
console.log(`No ${dir}path from "${from}" to "${to}" within ${data.maxDepth} hops.`);
|
|
816
|
-
if (data.fromCandidates.length > 1) {
|
|
817
|
-
console.log(
|
|
818
|
-
`\n "${from}" matched ${data.fromCandidates.length} symbols — using top match: ${data.fromCandidates[0].name} (${data.fromCandidates[0].file}:${data.fromCandidates[0].line})`,
|
|
819
|
-
);
|
|
820
|
-
}
|
|
821
|
-
if (data.toCandidates.length > 1) {
|
|
822
|
-
console.log(
|
|
823
|
-
` "${to}" matched ${data.toCandidates.length} symbols — using top match: ${data.toCandidates[0].name} (${data.toCandidates[0].file}:${data.toCandidates[0].line})`,
|
|
824
|
-
);
|
|
825
|
-
}
|
|
826
|
-
return;
|
|
827
|
-
}
|
|
828
|
-
|
|
829
|
-
if (data.hops === 0) {
|
|
830
|
-
console.log(`\n"${from}" and "${to}" resolve to the same symbol (0 hops):`);
|
|
831
|
-
const n = data.path[0];
|
|
832
|
-
console.log(` ${kindIcon(n.kind)} ${n.name} (${n.kind}) -- ${n.file}:${n.line}\n`);
|
|
833
|
-
return;
|
|
834
|
-
}
|
|
835
|
-
|
|
836
|
-
const dir = data.reverse ? ' (reverse)' : '';
|
|
837
|
-
console.log(
|
|
838
|
-
`\nPath from ${from} to ${to} (${data.hops} ${data.hops === 1 ? 'hop' : 'hops'})${dir}:\n`,
|
|
839
|
-
);
|
|
840
|
-
for (let i = 0; i < data.path.length; i++) {
|
|
841
|
-
const n = data.path[i];
|
|
842
|
-
const indent = ' '.repeat(i + 1);
|
|
843
|
-
if (i === 0) {
|
|
844
|
-
console.log(`${indent}${kindIcon(n.kind)} ${n.name} (${n.kind}) -- ${n.file}:${n.line}`);
|
|
845
|
-
} else {
|
|
846
|
-
console.log(
|
|
847
|
-
`${indent}--[${n.edgeKind}]--> ${kindIcon(n.kind)} ${n.name} (${n.kind}) -- ${n.file}:${n.line}`,
|
|
848
|
-
);
|
|
849
|
-
}
|
|
850
|
-
}
|
|
851
|
-
|
|
852
|
-
if (data.alternateCount > 0) {
|
|
853
|
-
console.log(
|
|
854
|
-
`\n (${data.alternateCount} alternate shortest ${data.alternateCount === 1 ? 'path' : 'paths'} at same depth)`,
|
|
855
|
-
);
|
|
856
|
-
}
|
|
857
|
-
console.log();
|
|
858
760
|
}
|
|
859
761
|
|
|
860
762
|
/**
|
|
@@ -863,236 +765,235 @@ export function symbolPath(from, to, customDbPath, opts = {}) {
|
|
|
863
765
|
*/
|
|
864
766
|
export function diffImpactData(customDbPath, opts = {}) {
|
|
865
767
|
const db = openReadonlyOrFail(customDbPath);
|
|
866
|
-
const noTests = opts.noTests || false;
|
|
867
|
-
const maxDepth = opts.depth || 3;
|
|
868
|
-
|
|
869
|
-
const dbPath = findDbPath(customDbPath);
|
|
870
|
-
const repoRoot = path.resolve(path.dirname(dbPath), '..');
|
|
871
|
-
|
|
872
|
-
// Verify we're in a git repository before running git diff
|
|
873
|
-
let checkDir = repoRoot;
|
|
874
|
-
let isGitRepo = false;
|
|
875
|
-
while (checkDir) {
|
|
876
|
-
if (fs.existsSync(path.join(checkDir, '.git'))) {
|
|
877
|
-
isGitRepo = true;
|
|
878
|
-
break;
|
|
879
|
-
}
|
|
880
|
-
const parent = path.dirname(checkDir);
|
|
881
|
-
if (parent === checkDir) break;
|
|
882
|
-
checkDir = parent;
|
|
883
|
-
}
|
|
884
|
-
if (!isGitRepo) {
|
|
885
|
-
db.close();
|
|
886
|
-
return { error: `Not a git repository: ${repoRoot}` };
|
|
887
|
-
}
|
|
888
|
-
|
|
889
|
-
let diffOutput;
|
|
890
768
|
try {
|
|
891
|
-
const
|
|
892
|
-
|
|
893
|
-
: ['diff', opts.ref || 'HEAD', '--unified=0', '--no-color'];
|
|
894
|
-
diffOutput = execFileSync('git', args, {
|
|
895
|
-
cwd: repoRoot,
|
|
896
|
-
encoding: 'utf-8',
|
|
897
|
-
maxBuffer: 10 * 1024 * 1024,
|
|
898
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
899
|
-
});
|
|
900
|
-
} catch (e) {
|
|
901
|
-
db.close();
|
|
902
|
-
return { error: `Failed to run git diff: ${e.message}` };
|
|
903
|
-
}
|
|
769
|
+
const noTests = opts.noTests || false;
|
|
770
|
+
const maxDepth = opts.depth || 3;
|
|
904
771
|
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
return {
|
|
908
|
-
changedFiles: 0,
|
|
909
|
-
newFiles: [],
|
|
910
|
-
affectedFunctions: [],
|
|
911
|
-
affectedFiles: [],
|
|
912
|
-
summary: null,
|
|
913
|
-
};
|
|
914
|
-
}
|
|
772
|
+
const dbPath = findDbPath(customDbPath);
|
|
773
|
+
const repoRoot = path.resolve(path.dirname(dbPath), '..');
|
|
915
774
|
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
if (fileMatch) {
|
|
931
|
-
currentFile = fileMatch[1];
|
|
932
|
-
if (!changedRanges.has(currentFile)) changedRanges.set(currentFile, []);
|
|
933
|
-
if (prevIsDevNull) newFiles.add(currentFile);
|
|
934
|
-
prevIsDevNull = false;
|
|
935
|
-
continue;
|
|
936
|
-
}
|
|
937
|
-
const hunkMatch = line.match(/^@@ .+ \+(\d+)(?:,(\d+))? @@/);
|
|
938
|
-
if (hunkMatch && currentFile) {
|
|
939
|
-
const start = parseInt(hunkMatch[1], 10);
|
|
940
|
-
const count = parseInt(hunkMatch[2] || '1', 10);
|
|
941
|
-
changedRanges.get(currentFile).push({ start, end: start + count - 1 });
|
|
775
|
+
// Verify we're in a git repository before running git diff
|
|
776
|
+
let checkDir = repoRoot;
|
|
777
|
+
let isGitRepo = false;
|
|
778
|
+
while (checkDir) {
|
|
779
|
+
if (fs.existsSync(path.join(checkDir, '.git'))) {
|
|
780
|
+
isGitRepo = true;
|
|
781
|
+
break;
|
|
782
|
+
}
|
|
783
|
+
const parent = path.dirname(checkDir);
|
|
784
|
+
if (parent === checkDir) break;
|
|
785
|
+
checkDir = parent;
|
|
786
|
+
}
|
|
787
|
+
if (!isGitRepo) {
|
|
788
|
+
return { error: `Not a git repository: ${repoRoot}` };
|
|
942
789
|
}
|
|
943
|
-
}
|
|
944
790
|
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
791
|
+
let diffOutput;
|
|
792
|
+
try {
|
|
793
|
+
const args = opts.staged
|
|
794
|
+
? ['diff', '--cached', '--unified=0', '--no-color']
|
|
795
|
+
: ['diff', opts.ref || 'HEAD', '--unified=0', '--no-color'];
|
|
796
|
+
diffOutput = execFileSync('git', args, {
|
|
797
|
+
cwd: repoRoot,
|
|
798
|
+
encoding: 'utf-8',
|
|
799
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
800
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
801
|
+
});
|
|
802
|
+
} catch (e) {
|
|
803
|
+
return { error: `Failed to run git diff: ${e.message}` };
|
|
804
|
+
}
|
|
955
805
|
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
806
|
+
if (!diffOutput.trim()) {
|
|
807
|
+
return {
|
|
808
|
+
changedFiles: 0,
|
|
809
|
+
newFiles: [],
|
|
810
|
+
affectedFunctions: [],
|
|
811
|
+
affectedFiles: [],
|
|
812
|
+
summary: null,
|
|
813
|
+
};
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
const changedRanges = new Map();
|
|
817
|
+
const newFiles = new Set();
|
|
818
|
+
let currentFile = null;
|
|
819
|
+
let prevIsDevNull = false;
|
|
820
|
+
for (const line of diffOutput.split('\n')) {
|
|
821
|
+
if (line.startsWith('--- /dev/null')) {
|
|
822
|
+
prevIsDevNull = true;
|
|
823
|
+
continue;
|
|
824
|
+
}
|
|
825
|
+
if (line.startsWith('--- ')) {
|
|
826
|
+
prevIsDevNull = false;
|
|
827
|
+
continue;
|
|
828
|
+
}
|
|
829
|
+
const fileMatch = line.match(/^\+\+\+ b\/(.+)/);
|
|
830
|
+
if (fileMatch) {
|
|
831
|
+
currentFile = fileMatch[1];
|
|
832
|
+
if (!changedRanges.has(currentFile)) changedRanges.set(currentFile, []);
|
|
833
|
+
if (prevIsDevNull) newFiles.add(currentFile);
|
|
834
|
+
prevIsDevNull = false;
|
|
835
|
+
continue;
|
|
836
|
+
}
|
|
837
|
+
const hunkMatch = line.match(/^@@ .+ \+(\d+)(?:,(\d+))? @@/);
|
|
838
|
+
if (hunkMatch && currentFile) {
|
|
839
|
+
const start = parseInt(hunkMatch[1], 10);
|
|
840
|
+
const count = parseInt(hunkMatch[2] || '1', 10);
|
|
841
|
+
changedRanges.get(currentFile).push({ start, end: start + count - 1 });
|
|
972
842
|
}
|
|
973
843
|
}
|
|
974
|
-
}
|
|
975
844
|
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
FROM
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
idToKey.set(c.id, callerKey);
|
|
1002
|
-
if (!levels[d]) levels[d] = [];
|
|
1003
|
-
levels[d].push({ name: c.name, kind: c.kind, file: c.file, line: c.line });
|
|
1004
|
-
edges.push({ from: idToKey.get(fid), to: callerKey });
|
|
1005
|
-
totalCallers++;
|
|
845
|
+
if (changedRanges.size === 0) {
|
|
846
|
+
return {
|
|
847
|
+
changedFiles: 0,
|
|
848
|
+
newFiles: [],
|
|
849
|
+
affectedFunctions: [],
|
|
850
|
+
affectedFiles: [],
|
|
851
|
+
summary: null,
|
|
852
|
+
};
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
const affectedFunctions = [];
|
|
856
|
+
for (const [file, ranges] of changedRanges) {
|
|
857
|
+
if (noTests && isTestFile(file)) continue;
|
|
858
|
+
const defs = db
|
|
859
|
+
.prepare(
|
|
860
|
+
`SELECT * FROM nodes WHERE file = ? AND kind IN ('function', 'method', 'class') ORDER BY line`,
|
|
861
|
+
)
|
|
862
|
+
.all(file);
|
|
863
|
+
for (let i = 0; i < defs.length; i++) {
|
|
864
|
+
const def = defs[i];
|
|
865
|
+
const endLine = def.end_line || (defs[i + 1] ? defs[i + 1].line - 1 : 999999);
|
|
866
|
+
for (const range of ranges) {
|
|
867
|
+
if (range.start <= endLine && range.end >= def.line) {
|
|
868
|
+
affectedFunctions.push(def);
|
|
869
|
+
break;
|
|
1006
870
|
}
|
|
1007
871
|
}
|
|
1008
872
|
}
|
|
1009
|
-
frontier = nextFrontier;
|
|
1010
|
-
if (frontier.length === 0) break;
|
|
1011
873
|
}
|
|
1012
|
-
return {
|
|
1013
|
-
name: fn.name,
|
|
1014
|
-
kind: fn.kind,
|
|
1015
|
-
file: fn.file,
|
|
1016
|
-
line: fn.line,
|
|
1017
|
-
transitiveCallers: totalCallers,
|
|
1018
|
-
levels,
|
|
1019
|
-
edges,
|
|
1020
|
-
};
|
|
1021
|
-
});
|
|
1022
874
|
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
875
|
+
const allAffected = new Set();
|
|
876
|
+
const functionResults = affectedFunctions.map((fn) => {
|
|
877
|
+
const visited = new Set([fn.id]);
|
|
878
|
+
let frontier = [fn.id];
|
|
879
|
+
let totalCallers = 0;
|
|
880
|
+
const levels = {};
|
|
881
|
+
const edges = [];
|
|
882
|
+
const idToKey = new Map();
|
|
883
|
+
idToKey.set(fn.id, `${fn.file}::${fn.name}:${fn.line}`);
|
|
884
|
+
for (let d = 1; d <= maxDepth; d++) {
|
|
885
|
+
const nextFrontier = [];
|
|
886
|
+
for (const fid of frontier) {
|
|
887
|
+
const callers = db
|
|
888
|
+
.prepare(`
|
|
889
|
+
SELECT DISTINCT n.id, n.name, n.kind, n.file, n.line
|
|
890
|
+
FROM edges e JOIN nodes n ON e.source_id = n.id
|
|
891
|
+
WHERE e.target_id = ? AND e.kind = 'calls'
|
|
892
|
+
`)
|
|
893
|
+
.all(fid);
|
|
894
|
+
for (const c of callers) {
|
|
895
|
+
if (!visited.has(c.id) && (!noTests || !isTestFile(c.file))) {
|
|
896
|
+
visited.add(c.id);
|
|
897
|
+
nextFrontier.push(c.id);
|
|
898
|
+
allAffected.add(`${c.file}:${c.name}`);
|
|
899
|
+
const callerKey = `${c.file}::${c.name}:${c.line}`;
|
|
900
|
+
idToKey.set(c.id, callerKey);
|
|
901
|
+
if (!levels[d]) levels[d] = [];
|
|
902
|
+
levels[d].push({ name: c.name, kind: c.kind, file: c.file, line: c.line });
|
|
903
|
+
edges.push({ from: idToKey.get(fid), to: callerKey });
|
|
904
|
+
totalCallers++;
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
frontier = nextFrontier;
|
|
909
|
+
if (frontier.length === 0) break;
|
|
910
|
+
}
|
|
911
|
+
return {
|
|
912
|
+
name: fn.name,
|
|
913
|
+
kind: fn.kind,
|
|
914
|
+
file: fn.file,
|
|
915
|
+
line: fn.line,
|
|
916
|
+
transitiveCallers: totalCallers,
|
|
917
|
+
levels,
|
|
918
|
+
edges,
|
|
919
|
+
};
|
|
1035
920
|
});
|
|
1036
|
-
// Exclude files already found via static analysis
|
|
1037
|
-
historicallyCoupled = coResults.filter((r) => !affectedFiles.has(r.file));
|
|
1038
|
-
} catch {
|
|
1039
|
-
/* co_changes table doesn't exist — skip silently */
|
|
1040
|
-
}
|
|
1041
921
|
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
try {
|
|
1045
|
-
const allFilePaths = [...new Set([...changedRanges.keys(), ...affectedFiles])];
|
|
1046
|
-
const ownerResult = ownersForFiles(allFilePaths, repoRoot);
|
|
1047
|
-
if (ownerResult.affectedOwners.length > 0) {
|
|
1048
|
-
ownership = {
|
|
1049
|
-
owners: Object.fromEntries(ownerResult.owners),
|
|
1050
|
-
affectedOwners: ownerResult.affectedOwners,
|
|
1051
|
-
suggestedReviewers: ownerResult.suggestedReviewers,
|
|
1052
|
-
};
|
|
1053
|
-
}
|
|
1054
|
-
} catch {
|
|
1055
|
-
/* CODEOWNERS missing or unreadable — skip silently */
|
|
1056
|
-
}
|
|
922
|
+
const affectedFiles = new Set();
|
|
923
|
+
for (const key of allAffected) affectedFiles.add(key.split(':')[0]);
|
|
1057
924
|
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
scopeFiles: [...changedRanges.keys()],
|
|
925
|
+
// Look up historically coupled files from co-change data
|
|
926
|
+
let historicallyCoupled = [];
|
|
927
|
+
try {
|
|
928
|
+
db.prepare('SELECT 1 FROM co_changes LIMIT 1').get();
|
|
929
|
+
const changedFilesList = [...changedRanges.keys()];
|
|
930
|
+
const coResults = coChangeForFiles(changedFilesList, db, {
|
|
931
|
+
minJaccard: 0.3,
|
|
932
|
+
limit: 20,
|
|
1067
933
|
noTests,
|
|
1068
934
|
});
|
|
1069
|
-
|
|
1070
|
-
|
|
935
|
+
// Exclude files already found via static analysis
|
|
936
|
+
historicallyCoupled = coResults.filter((r) => !affectedFiles.has(r.file));
|
|
937
|
+
} catch {
|
|
938
|
+
/* co_changes table doesn't exist — skip silently */
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
// Look up CODEOWNERS for changed + affected files
|
|
942
|
+
let ownership = null;
|
|
943
|
+
try {
|
|
944
|
+
const allFilePaths = [...new Set([...changedRanges.keys(), ...affectedFiles])];
|
|
945
|
+
const ownerResult = ownersForFiles(allFilePaths, repoRoot);
|
|
946
|
+
if (ownerResult.affectedOwners.length > 0) {
|
|
947
|
+
ownership = {
|
|
948
|
+
owners: Object.fromEntries(ownerResult.owners),
|
|
949
|
+
affectedOwners: ownerResult.affectedOwners,
|
|
950
|
+
suggestedReviewers: ownerResult.suggestedReviewers,
|
|
951
|
+
};
|
|
952
|
+
}
|
|
953
|
+
} catch {
|
|
954
|
+
/* CODEOWNERS missing or unreadable — skip silently */
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
// Check boundary violations scoped to changed files
|
|
958
|
+
let boundaryViolations = [];
|
|
959
|
+
let boundaryViolationCount = 0;
|
|
960
|
+
try {
|
|
961
|
+
const config = loadConfig(repoRoot);
|
|
962
|
+
const boundaryConfig = config.manifesto?.boundaries;
|
|
963
|
+
if (boundaryConfig) {
|
|
964
|
+
const result = evaluateBoundaries(db, boundaryConfig, {
|
|
965
|
+
scopeFiles: [...changedRanges.keys()],
|
|
966
|
+
noTests,
|
|
967
|
+
});
|
|
968
|
+
boundaryViolations = result.violations;
|
|
969
|
+
boundaryViolationCount = result.violationCount;
|
|
970
|
+
}
|
|
971
|
+
} catch {
|
|
972
|
+
/* boundary check failed — skip silently */
|
|
1071
973
|
}
|
|
1072
|
-
} catch {
|
|
1073
|
-
/* boundary check failed — skip silently */
|
|
1074
|
-
}
|
|
1075
974
|
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
boundaryViolations,
|
|
1085
|
-
boundaryViolationCount,
|
|
1086
|
-
summary: {
|
|
1087
|
-
functionsChanged: affectedFunctions.length,
|
|
1088
|
-
callersAffected: allAffected.size,
|
|
1089
|
-
filesAffected: affectedFiles.size,
|
|
1090
|
-
historicallyCoupledCount: historicallyCoupled.length,
|
|
1091
|
-
ownersAffected: ownership ? ownership.affectedOwners.length : 0,
|
|
975
|
+
const base = {
|
|
976
|
+
changedFiles: changedRanges.size,
|
|
977
|
+
newFiles: [...newFiles],
|
|
978
|
+
affectedFunctions: functionResults,
|
|
979
|
+
affectedFiles: [...affectedFiles],
|
|
980
|
+
historicallyCoupled,
|
|
981
|
+
ownership,
|
|
982
|
+
boundaryViolations,
|
|
1092
983
|
boundaryViolationCount,
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
984
|
+
summary: {
|
|
985
|
+
functionsChanged: affectedFunctions.length,
|
|
986
|
+
callersAffected: allAffected.size,
|
|
987
|
+
filesAffected: affectedFiles.size,
|
|
988
|
+
historicallyCoupledCount: historicallyCoupled.length,
|
|
989
|
+
ownersAffected: ownership ? ownership.affectedOwners.length : 0,
|
|
990
|
+
boundaryViolationCount,
|
|
991
|
+
},
|
|
992
|
+
};
|
|
993
|
+
return paginateResult(base, 'affectedFunctions', { limit: opts.limit, offset: opts.offset });
|
|
994
|
+
} finally {
|
|
995
|
+
db.close();
|
|
996
|
+
}
|
|
1096
997
|
}
|
|
1097
998
|
|
|
1098
999
|
export function diffImpactMermaid(customDbPath, opts = {}) {
|
|
@@ -1211,35 +1112,20 @@ export function diffImpactMermaid(customDbPath, opts = {}) {
|
|
|
1211
1112
|
|
|
1212
1113
|
export function listFunctionsData(customDbPath, opts = {}) {
|
|
1213
1114
|
const db = openReadonlyOrFail(customDbPath);
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
const placeholders = kinds.map(() => '?').join(', ');
|
|
1217
|
-
|
|
1218
|
-
const conditions = [`kind IN (${placeholders})`];
|
|
1219
|
-
const params = [...kinds];
|
|
1220
|
-
|
|
1221
|
-
if (opts.file) {
|
|
1222
|
-
conditions.push('file LIKE ?');
|
|
1223
|
-
params.push(`%${opts.file}%`);
|
|
1224
|
-
}
|
|
1225
|
-
if (opts.pattern) {
|
|
1226
|
-
conditions.push('name LIKE ?');
|
|
1227
|
-
params.push(`%${opts.pattern}%`);
|
|
1228
|
-
}
|
|
1115
|
+
try {
|
|
1116
|
+
const noTests = opts.noTests || false;
|
|
1229
1117
|
|
|
1230
|
-
|
|
1231
|
-
.prepare(
|
|
1232
|
-
`SELECT name, kind, file, line, end_line, role FROM nodes WHERE ${conditions.join(' AND ')} ORDER BY file, line`,
|
|
1233
|
-
)
|
|
1234
|
-
.all(...params);
|
|
1118
|
+
let rows = listFunctionNodes(db, { file: opts.file, pattern: opts.pattern });
|
|
1235
1119
|
|
|
1236
|
-
|
|
1120
|
+
if (noTests) rows = rows.filter((r) => !isTestFile(r.file));
|
|
1237
1121
|
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1122
|
+
const hc = new Map();
|
|
1123
|
+
const functions = rows.map((r) => normalizeSymbol(r, db, hc));
|
|
1124
|
+
const base = { count: functions.length, functions };
|
|
1125
|
+
return paginateResult(base, 'functions', { limit: opts.limit, offset: opts.offset });
|
|
1126
|
+
} finally {
|
|
1127
|
+
db.close();
|
|
1128
|
+
}
|
|
1243
1129
|
}
|
|
1244
1130
|
|
|
1245
1131
|
/**
|
|
@@ -1255,25 +1141,8 @@ export function* iterListFunctions(customDbPath, opts = {}) {
|
|
|
1255
1141
|
const db = openReadonlyOrFail(customDbPath);
|
|
1256
1142
|
try {
|
|
1257
1143
|
const noTests = opts.noTests || false;
|
|
1258
|
-
const kinds = ['function', 'method', 'class'];
|
|
1259
|
-
const placeholders = kinds.map(() => '?').join(', ');
|
|
1260
|
-
|
|
1261
|
-
const conditions = [`kind IN (${placeholders})`];
|
|
1262
|
-
const params = [...kinds];
|
|
1263
1144
|
|
|
1264
|
-
|
|
1265
|
-
conditions.push('file LIKE ?');
|
|
1266
|
-
params.push(`%${opts.file}%`);
|
|
1267
|
-
}
|
|
1268
|
-
if (opts.pattern) {
|
|
1269
|
-
conditions.push('name LIKE ?');
|
|
1270
|
-
params.push(`%${opts.pattern}%`);
|
|
1271
|
-
}
|
|
1272
|
-
|
|
1273
|
-
const stmt = db.prepare(
|
|
1274
|
-
`SELECT name, kind, file, line, end_line, role FROM nodes WHERE ${conditions.join(' AND ')} ORDER BY file, line`,
|
|
1275
|
-
);
|
|
1276
|
-
for (const row of stmt.iterate(...params)) {
|
|
1145
|
+
for (const row of iterateFunctionNodes(db, { file: opts.file, pattern: opts.pattern })) {
|
|
1277
1146
|
if (noTests && isTestFile(row.file)) continue;
|
|
1278
1147
|
yield {
|
|
1279
1148
|
name: row.name,
|
|
@@ -1383,569 +1252,247 @@ export function* iterWhere(target, customDbPath, opts = {}) {
|
|
|
1383
1252
|
|
|
1384
1253
|
export function statsData(customDbPath, opts = {}) {
|
|
1385
1254
|
const db = openReadonlyOrFail(customDbPath);
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1255
|
+
try {
|
|
1256
|
+
const noTests = opts.noTests || false;
|
|
1257
|
+
|
|
1258
|
+
// Build set of test file IDs for filtering nodes and edges
|
|
1259
|
+
let testFileIds = null;
|
|
1260
|
+
if (noTests) {
|
|
1261
|
+
const allFileNodes = db.prepare("SELECT id, file FROM nodes WHERE kind = 'file'").all();
|
|
1262
|
+
testFileIds = new Set();
|
|
1263
|
+
const testFiles = new Set();
|
|
1264
|
+
for (const n of allFileNodes) {
|
|
1265
|
+
if (isTestFile(n.file)) {
|
|
1266
|
+
testFileIds.add(n.id);
|
|
1267
|
+
testFiles.add(n.file);
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
// Also collect non-file node IDs that belong to test files
|
|
1271
|
+
const allNodes = db.prepare('SELECT id, file FROM nodes').all();
|
|
1272
|
+
for (const n of allNodes) {
|
|
1273
|
+
if (testFiles.has(n.file)) testFileIds.add(n.id);
|
|
1398
1274
|
}
|
|
1399
1275
|
}
|
|
1400
|
-
// Also collect non-file node IDs that belong to test files
|
|
1401
|
-
const allNodes = db.prepare('SELECT id, file FROM nodes').all();
|
|
1402
|
-
for (const n of allNodes) {
|
|
1403
|
-
if (testFiles.has(n.file)) testFileIds.add(n.id);
|
|
1404
|
-
}
|
|
1405
|
-
}
|
|
1406
1276
|
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
}
|
|
1418
|
-
const nodesByKind = {};
|
|
1419
|
-
let totalNodes = 0;
|
|
1420
|
-
for (const r of nodeRows) {
|
|
1421
|
-
nodesByKind[r.kind] = r.c;
|
|
1422
|
-
totalNodes += r.c;
|
|
1423
|
-
}
|
|
1424
|
-
|
|
1425
|
-
// Edge breakdown by kind
|
|
1426
|
-
let edgeRows;
|
|
1427
|
-
if (noTests) {
|
|
1428
|
-
const allEdges = db.prepare('SELECT source_id, target_id, kind FROM edges').all();
|
|
1429
|
-
const filtered = allEdges.filter(
|
|
1430
|
-
(e) => !testFileIds.has(e.source_id) && !testFileIds.has(e.target_id),
|
|
1431
|
-
);
|
|
1432
|
-
const counts = {};
|
|
1433
|
-
for (const e of filtered) counts[e.kind] = (counts[e.kind] || 0) + 1;
|
|
1434
|
-
edgeRows = Object.entries(counts).map(([kind, c]) => ({ kind, c }));
|
|
1435
|
-
} else {
|
|
1436
|
-
edgeRows = db.prepare('SELECT kind, COUNT(*) as c FROM edges GROUP BY kind').all();
|
|
1437
|
-
}
|
|
1438
|
-
const edgesByKind = {};
|
|
1439
|
-
let totalEdges = 0;
|
|
1440
|
-
for (const r of edgeRows) {
|
|
1441
|
-
edgesByKind[r.kind] = r.c;
|
|
1442
|
-
totalEdges += r.c;
|
|
1443
|
-
}
|
|
1444
|
-
|
|
1445
|
-
// File/language distribution — map extensions via LANGUAGE_REGISTRY
|
|
1446
|
-
const extToLang = new Map();
|
|
1447
|
-
for (const entry of LANGUAGE_REGISTRY) {
|
|
1448
|
-
for (const ext of entry.extensions) {
|
|
1449
|
-
extToLang.set(ext, entry.id);
|
|
1277
|
+
// Node breakdown by kind
|
|
1278
|
+
let nodeRows;
|
|
1279
|
+
if (noTests) {
|
|
1280
|
+
const allNodes = db.prepare('SELECT id, kind, file FROM nodes').all();
|
|
1281
|
+
const filtered = allNodes.filter((n) => !testFileIds.has(n.id));
|
|
1282
|
+
const counts = {};
|
|
1283
|
+
for (const n of filtered) counts[n.kind] = (counts[n.kind] || 0) + 1;
|
|
1284
|
+
nodeRows = Object.entries(counts).map(([kind, c]) => ({ kind, c }));
|
|
1285
|
+
} else {
|
|
1286
|
+
nodeRows = db.prepare('SELECT kind, COUNT(*) as c FROM nodes GROUP BY kind').all();
|
|
1450
1287
|
}
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
const ext = path.extname(row.file).toLowerCase();
|
|
1457
|
-
const lang = extToLang.get(ext) || 'other';
|
|
1458
|
-
byLanguage[lang] = (byLanguage[lang] || 0) + 1;
|
|
1459
|
-
}
|
|
1460
|
-
const langCount = Object.keys(byLanguage).length;
|
|
1461
|
-
|
|
1462
|
-
// Cycles
|
|
1463
|
-
const fileCycles = findCycles(db, { fileLevel: true, noTests });
|
|
1464
|
-
const fnCycles = findCycles(db, { fileLevel: false, noTests });
|
|
1465
|
-
|
|
1466
|
-
// Top 5 coupling hotspots (fan-in + fan-out, file nodes)
|
|
1467
|
-
const testFilter = noTests
|
|
1468
|
-
? `AND n.file NOT LIKE '%.test.%'
|
|
1469
|
-
AND n.file NOT LIKE '%.spec.%'
|
|
1470
|
-
AND n.file NOT LIKE '%__test__%'
|
|
1471
|
-
AND n.file NOT LIKE '%__tests__%'
|
|
1472
|
-
AND n.file NOT LIKE '%.stories.%'`
|
|
1473
|
-
: '';
|
|
1474
|
-
const hotspotRows = db
|
|
1475
|
-
.prepare(`
|
|
1476
|
-
SELECT n.file,
|
|
1477
|
-
(SELECT COUNT(*) FROM edges WHERE target_id = n.id) as fan_in,
|
|
1478
|
-
(SELECT COUNT(*) FROM edges WHERE source_id = n.id) as fan_out
|
|
1479
|
-
FROM nodes n
|
|
1480
|
-
WHERE n.kind = 'file' ${testFilter}
|
|
1481
|
-
ORDER BY (SELECT COUNT(*) FROM edges WHERE target_id = n.id)
|
|
1482
|
-
+ (SELECT COUNT(*) FROM edges WHERE source_id = n.id) DESC
|
|
1483
|
-
`)
|
|
1484
|
-
.all();
|
|
1485
|
-
const filteredHotspots = noTests ? hotspotRows.filter((r) => !isTestFile(r.file)) : hotspotRows;
|
|
1486
|
-
const hotspots = filteredHotspots.slice(0, 5).map((r) => ({
|
|
1487
|
-
file: r.file,
|
|
1488
|
-
fanIn: r.fan_in,
|
|
1489
|
-
fanOut: r.fan_out,
|
|
1490
|
-
}));
|
|
1491
|
-
|
|
1492
|
-
// Embeddings metadata
|
|
1493
|
-
let embeddings = null;
|
|
1494
|
-
try {
|
|
1495
|
-
const count = db.prepare('SELECT COUNT(*) as c FROM embeddings').get();
|
|
1496
|
-
if (count && count.c > 0) {
|
|
1497
|
-
const meta = {};
|
|
1498
|
-
const metaRows = db.prepare('SELECT key, value FROM embedding_meta').all();
|
|
1499
|
-
for (const r of metaRows) meta[r.key] = r.value;
|
|
1500
|
-
embeddings = {
|
|
1501
|
-
count: count.c,
|
|
1502
|
-
model: meta.model || null,
|
|
1503
|
-
dim: meta.dim ? parseInt(meta.dim, 10) : null,
|
|
1504
|
-
builtAt: meta.built_at || null,
|
|
1505
|
-
};
|
|
1288
|
+
const nodesByKind = {};
|
|
1289
|
+
let totalNodes = 0;
|
|
1290
|
+
for (const r of nodeRows) {
|
|
1291
|
+
nodesByKind[r.kind] = r.c;
|
|
1292
|
+
totalNodes += r.c;
|
|
1506
1293
|
}
|
|
1507
|
-
} catch {
|
|
1508
|
-
/* embeddings table may not exist */
|
|
1509
|
-
}
|
|
1510
|
-
|
|
1511
|
-
// Graph quality metrics
|
|
1512
|
-
const qualityTestFilter = testFilter.replace(/n\.file/g, 'file');
|
|
1513
|
-
const totalCallable = db
|
|
1514
|
-
.prepare(
|
|
1515
|
-
`SELECT COUNT(*) as c FROM nodes WHERE kind IN ('function', 'method') ${qualityTestFilter}`,
|
|
1516
|
-
)
|
|
1517
|
-
.get().c;
|
|
1518
|
-
const callableWithCallers = db
|
|
1519
|
-
.prepare(`
|
|
1520
|
-
SELECT COUNT(DISTINCT e.target_id) as c FROM edges e
|
|
1521
|
-
JOIN nodes n ON e.target_id = n.id
|
|
1522
|
-
WHERE e.kind = 'calls' AND n.kind IN ('function', 'method') ${testFilter}
|
|
1523
|
-
`)
|
|
1524
|
-
.get().c;
|
|
1525
|
-
const callerCoverage = totalCallable > 0 ? callableWithCallers / totalCallable : 0;
|
|
1526
|
-
|
|
1527
|
-
const totalCallEdges = db.prepare("SELECT COUNT(*) as c FROM edges WHERE kind = 'calls'").get().c;
|
|
1528
|
-
const highConfCallEdges = db
|
|
1529
|
-
.prepare("SELECT COUNT(*) as c FROM edges WHERE kind = 'calls' AND confidence >= 0.7")
|
|
1530
|
-
.get().c;
|
|
1531
|
-
const callConfidence = totalCallEdges > 0 ? highConfCallEdges / totalCallEdges : 0;
|
|
1532
|
-
|
|
1533
|
-
// False-positive warnings: generic names with > threshold callers
|
|
1534
|
-
const fpRows = db
|
|
1535
|
-
.prepare(`
|
|
1536
|
-
SELECT n.name, n.file, n.line, COUNT(e.source_id) as caller_count
|
|
1537
|
-
FROM nodes n
|
|
1538
|
-
LEFT JOIN edges e ON n.id = e.target_id AND e.kind = 'calls'
|
|
1539
|
-
WHERE n.kind IN ('function', 'method')
|
|
1540
|
-
GROUP BY n.id
|
|
1541
|
-
HAVING caller_count > ?
|
|
1542
|
-
ORDER BY caller_count DESC
|
|
1543
|
-
`)
|
|
1544
|
-
.all(FALSE_POSITIVE_CALLER_THRESHOLD);
|
|
1545
|
-
const falsePositiveWarnings = fpRows
|
|
1546
|
-
.filter((r) =>
|
|
1547
|
-
FALSE_POSITIVE_NAMES.has(r.name.includes('.') ? r.name.split('.').pop() : r.name),
|
|
1548
|
-
)
|
|
1549
|
-
.map((r) => ({ name: r.name, file: r.file, line: r.line, callerCount: r.caller_count }));
|
|
1550
|
-
|
|
1551
|
-
// Edges from suspicious nodes
|
|
1552
|
-
let fpEdgeCount = 0;
|
|
1553
|
-
for (const fp of falsePositiveWarnings) fpEdgeCount += fp.callerCount;
|
|
1554
|
-
const falsePositiveRatio = totalCallEdges > 0 ? fpEdgeCount / totalCallEdges : 0;
|
|
1555
|
-
|
|
1556
|
-
const score = Math.round(
|
|
1557
|
-
callerCoverage * 40 + callConfidence * 40 + (1 - falsePositiveRatio) * 20,
|
|
1558
|
-
);
|
|
1559
|
-
|
|
1560
|
-
const quality = {
|
|
1561
|
-
score,
|
|
1562
|
-
callerCoverage: {
|
|
1563
|
-
ratio: callerCoverage,
|
|
1564
|
-
covered: callableWithCallers,
|
|
1565
|
-
total: totalCallable,
|
|
1566
|
-
},
|
|
1567
|
-
callConfidence: {
|
|
1568
|
-
ratio: callConfidence,
|
|
1569
|
-
highConf: highConfCallEdges,
|
|
1570
|
-
total: totalCallEdges,
|
|
1571
|
-
},
|
|
1572
|
-
falsePositiveWarnings,
|
|
1573
|
-
};
|
|
1574
|
-
|
|
1575
|
-
// Role distribution
|
|
1576
|
-
let roleRows;
|
|
1577
|
-
if (noTests) {
|
|
1578
|
-
const allRoleNodes = db.prepare('SELECT role, file FROM nodes WHERE role IS NOT NULL').all();
|
|
1579
|
-
const filtered = allRoleNodes.filter((n) => !isTestFile(n.file));
|
|
1580
|
-
const counts = {};
|
|
1581
|
-
for (const n of filtered) counts[n.role] = (counts[n.role] || 0) + 1;
|
|
1582
|
-
roleRows = Object.entries(counts).map(([role, c]) => ({ role, c }));
|
|
1583
|
-
} else {
|
|
1584
|
-
roleRows = db
|
|
1585
|
-
.prepare('SELECT role, COUNT(*) as c FROM nodes WHERE role IS NOT NULL GROUP BY role')
|
|
1586
|
-
.all();
|
|
1587
|
-
}
|
|
1588
|
-
const roles = {};
|
|
1589
|
-
for (const r of roleRows) roles[r.role] = r.c;
|
|
1590
|
-
|
|
1591
|
-
// Complexity summary
|
|
1592
|
-
let complexity = null;
|
|
1593
|
-
try {
|
|
1594
|
-
const cRows = db
|
|
1595
|
-
.prepare(
|
|
1596
|
-
`SELECT fc.cognitive, fc.cyclomatic, fc.max_nesting, fc.maintainability_index
|
|
1597
|
-
FROM function_complexity fc JOIN nodes n ON fc.node_id = n.id
|
|
1598
|
-
WHERE n.kind IN ('function','method') ${testFilter}`,
|
|
1599
|
-
)
|
|
1600
|
-
.all();
|
|
1601
|
-
if (cRows.length > 0) {
|
|
1602
|
-
const miValues = cRows.map((r) => r.maintainability_index || 0);
|
|
1603
|
-
complexity = {
|
|
1604
|
-
analyzed: cRows.length,
|
|
1605
|
-
avgCognitive: +(cRows.reduce((s, r) => s + r.cognitive, 0) / cRows.length).toFixed(1),
|
|
1606
|
-
avgCyclomatic: +(cRows.reduce((s, r) => s + r.cyclomatic, 0) / cRows.length).toFixed(1),
|
|
1607
|
-
maxCognitive: Math.max(...cRows.map((r) => r.cognitive)),
|
|
1608
|
-
maxCyclomatic: Math.max(...cRows.map((r) => r.cyclomatic)),
|
|
1609
|
-
avgMI: +(miValues.reduce((s, v) => s + v, 0) / miValues.length).toFixed(1),
|
|
1610
|
-
minMI: +Math.min(...miValues).toFixed(1),
|
|
1611
|
-
};
|
|
1612
|
-
}
|
|
1613
|
-
} catch {
|
|
1614
|
-
/* table may not exist in older DBs */
|
|
1615
|
-
}
|
|
1616
|
-
|
|
1617
|
-
db.close();
|
|
1618
|
-
return {
|
|
1619
|
-
nodes: { total: totalNodes, byKind: nodesByKind },
|
|
1620
|
-
edges: { total: totalEdges, byKind: edgesByKind },
|
|
1621
|
-
files: { total: fileNodes.length, languages: langCount, byLanguage },
|
|
1622
|
-
cycles: { fileLevel: fileCycles.length, functionLevel: fnCycles.length },
|
|
1623
|
-
hotspots,
|
|
1624
|
-
embeddings,
|
|
1625
|
-
quality,
|
|
1626
|
-
roles,
|
|
1627
|
-
complexity,
|
|
1628
|
-
};
|
|
1629
|
-
}
|
|
1630
|
-
|
|
1631
|
-
export async function stats(customDbPath, opts = {}) {
|
|
1632
|
-
const data = statsData(customDbPath, { noTests: opts.noTests });
|
|
1633
|
-
|
|
1634
|
-
// Community detection summary (async import for lazy-loading)
|
|
1635
|
-
try {
|
|
1636
|
-
const { communitySummaryForStats } = await import('./communities.js');
|
|
1637
|
-
data.communities = communitySummaryForStats(customDbPath, { noTests: opts.noTests });
|
|
1638
|
-
} catch {
|
|
1639
|
-
/* graphology may not be available */
|
|
1640
|
-
}
|
|
1641
|
-
|
|
1642
|
-
if (opts.json) {
|
|
1643
|
-
console.log(JSON.stringify(data, null, 2));
|
|
1644
|
-
return;
|
|
1645
|
-
}
|
|
1646
1294
|
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
const kindParts = kindEntries.map(([k, v]) => `${k} ${v}`);
|
|
1654
|
-
// Print in rows of 3
|
|
1655
|
-
for (let i = 0; i < kindParts.length; i += 3) {
|
|
1656
|
-
const row = kindParts
|
|
1657
|
-
.slice(i, i + 3)
|
|
1658
|
-
.map((p) => p.padEnd(18))
|
|
1659
|
-
.join('');
|
|
1660
|
-
console.log(` ${row}`);
|
|
1661
|
-
}
|
|
1662
|
-
|
|
1663
|
-
// Edges
|
|
1664
|
-
console.log(`\nEdges: ${data.edges.total} total`);
|
|
1665
|
-
const edgeEntries = Object.entries(data.edges.byKind).sort((a, b) => b[1] - a[1]);
|
|
1666
|
-
const edgeParts = edgeEntries.map(([k, v]) => `${k} ${v}`);
|
|
1667
|
-
for (let i = 0; i < edgeParts.length; i += 3) {
|
|
1668
|
-
const row = edgeParts
|
|
1669
|
-
.slice(i, i + 3)
|
|
1670
|
-
.map((p) => p.padEnd(18))
|
|
1671
|
-
.join('');
|
|
1672
|
-
console.log(` ${row}`);
|
|
1673
|
-
}
|
|
1674
|
-
|
|
1675
|
-
// Files
|
|
1676
|
-
console.log(`\nFiles: ${data.files.total} (${data.files.languages} languages)`);
|
|
1677
|
-
const langEntries = Object.entries(data.files.byLanguage).sort((a, b) => b[1] - a[1]);
|
|
1678
|
-
const langParts = langEntries.map(([k, v]) => `${k} ${v}`);
|
|
1679
|
-
for (let i = 0; i < langParts.length; i += 3) {
|
|
1680
|
-
const row = langParts
|
|
1681
|
-
.slice(i, i + 3)
|
|
1682
|
-
.map((p) => p.padEnd(18))
|
|
1683
|
-
.join('');
|
|
1684
|
-
console.log(` ${row}`);
|
|
1685
|
-
}
|
|
1686
|
-
|
|
1687
|
-
// Cycles
|
|
1688
|
-
console.log(
|
|
1689
|
-
`\nCycles: ${data.cycles.fileLevel} file-level, ${data.cycles.functionLevel} function-level`,
|
|
1690
|
-
);
|
|
1691
|
-
|
|
1692
|
-
// Hotspots
|
|
1693
|
-
if (data.hotspots.length > 0) {
|
|
1694
|
-
console.log(`\nTop ${data.hotspots.length} coupling hotspots:`);
|
|
1695
|
-
for (let i = 0; i < data.hotspots.length; i++) {
|
|
1696
|
-
const h = data.hotspots[i];
|
|
1697
|
-
console.log(
|
|
1698
|
-
` ${String(i + 1).padStart(2)}. ${h.file.padEnd(35)} fan-in: ${String(h.fanIn).padStart(3)} fan-out: ${String(h.fanOut).padStart(3)}`,
|
|
1295
|
+
// Edge breakdown by kind
|
|
1296
|
+
let edgeRows;
|
|
1297
|
+
if (noTests) {
|
|
1298
|
+
const allEdges = db.prepare('SELECT source_id, target_id, kind FROM edges').all();
|
|
1299
|
+
const filtered = allEdges.filter(
|
|
1300
|
+
(e) => !testFileIds.has(e.source_id) && !testFileIds.has(e.target_id),
|
|
1699
1301
|
);
|
|
1302
|
+
const counts = {};
|
|
1303
|
+
for (const e of filtered) counts[e.kind] = (counts[e.kind] || 0) + 1;
|
|
1304
|
+
edgeRows = Object.entries(counts).map(([kind, c]) => ({ kind, c }));
|
|
1305
|
+
} else {
|
|
1306
|
+
edgeRows = db.prepare('SELECT kind, COUNT(*) as c FROM edges GROUP BY kind').all();
|
|
1307
|
+
}
|
|
1308
|
+
const edgesByKind = {};
|
|
1309
|
+
let totalEdges = 0;
|
|
1310
|
+
for (const r of edgeRows) {
|
|
1311
|
+
edgesByKind[r.kind] = r.c;
|
|
1312
|
+
totalEdges += r.c;
|
|
1700
1313
|
}
|
|
1701
|
-
}
|
|
1702
|
-
|
|
1703
|
-
// Embeddings
|
|
1704
|
-
if (data.embeddings) {
|
|
1705
|
-
const e = data.embeddings;
|
|
1706
|
-
console.log(
|
|
1707
|
-
`\nEmbeddings: ${e.count} vectors (${e.model || 'unknown'}, ${e.dim || '?'}d) built ${e.builtAt || 'unknown'}`,
|
|
1708
|
-
);
|
|
1709
|
-
} else {
|
|
1710
|
-
console.log('\nEmbeddings: not built');
|
|
1711
|
-
}
|
|
1712
1314
|
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
const
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
console.log(`\nGraph Quality: ${q.score}/100`);
|
|
1719
|
-
console.log(
|
|
1720
|
-
` Caller coverage: ${(cc.ratio * 100).toFixed(1)}% (${cc.covered}/${cc.total} functions have >=1 caller)`,
|
|
1721
|
-
);
|
|
1722
|
-
console.log(
|
|
1723
|
-
` Call confidence: ${(cf.ratio * 100).toFixed(1)}% (${cf.highConf}/${cf.total} call edges are high-confidence)`,
|
|
1724
|
-
);
|
|
1725
|
-
if (q.falsePositiveWarnings.length > 0) {
|
|
1726
|
-
console.log(' False-positive warnings:');
|
|
1727
|
-
for (const fp of q.falsePositiveWarnings) {
|
|
1728
|
-
console.log(` ! ${fp.name} (${fp.callerCount} callers) -- ${fp.file}:${fp.line}`);
|
|
1315
|
+
// File/language distribution — map extensions via LANGUAGE_REGISTRY
|
|
1316
|
+
const extToLang = new Map();
|
|
1317
|
+
for (const entry of LANGUAGE_REGISTRY) {
|
|
1318
|
+
for (const ext of entry.extensions) {
|
|
1319
|
+
extToLang.set(ext, entry.id);
|
|
1729
1320
|
}
|
|
1730
1321
|
}
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
.sort((a, b) => b[1] - a[1])
|
|
1739
|
-
.map(([k, v]) => `${k} ${v}`);
|
|
1740
|
-
for (let i = 0; i < roleParts.length; i += 3) {
|
|
1741
|
-
const row = roleParts
|
|
1742
|
-
.slice(i, i + 3)
|
|
1743
|
-
.map((p) => p.padEnd(18))
|
|
1744
|
-
.join('');
|
|
1745
|
-
console.log(` ${row}`);
|
|
1322
|
+
let fileNodes = db.prepare("SELECT file FROM nodes WHERE kind = 'file'").all();
|
|
1323
|
+
if (noTests) fileNodes = fileNodes.filter((n) => !isTestFile(n.file));
|
|
1324
|
+
const byLanguage = {};
|
|
1325
|
+
for (const row of fileNodes) {
|
|
1326
|
+
const ext = path.extname(row.file).toLowerCase();
|
|
1327
|
+
const lang = extToLang.get(ext) || 'other';
|
|
1328
|
+
byLanguage[lang] = (byLanguage[lang] || 0) + 1;
|
|
1746
1329
|
}
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
// Complexity
|
|
1750
|
-
if (data.complexity) {
|
|
1751
|
-
const cx = data.complexity;
|
|
1752
|
-
const miPart = cx.avgMI != null ? ` | avg MI: ${cx.avgMI} | min MI: ${cx.minMI}` : '';
|
|
1753
|
-
console.log(
|
|
1754
|
-
`\nComplexity: ${cx.analyzed} functions | avg cognitive: ${cx.avgCognitive} | avg cyclomatic: ${cx.avgCyclomatic} | max cognitive: ${cx.maxCognitive}${miPart}`,
|
|
1755
|
-
);
|
|
1756
|
-
}
|
|
1757
|
-
|
|
1758
|
-
// Communities
|
|
1759
|
-
if (data.communities) {
|
|
1760
|
-
const cm = data.communities;
|
|
1761
|
-
console.log(
|
|
1762
|
-
`\nCommunities: ${cm.communityCount} detected | modularity: ${cm.modularity} | drift: ${cm.driftScore}%`,
|
|
1763
|
-
);
|
|
1764
|
-
}
|
|
1765
|
-
|
|
1766
|
-
console.log();
|
|
1767
|
-
}
|
|
1330
|
+
const langCount = Object.keys(byLanguage).length;
|
|
1768
1331
|
|
|
1769
|
-
//
|
|
1332
|
+
// Cycles
|
|
1333
|
+
const fileCycles = findCycles(db, { fileLevel: true, noTests });
|
|
1334
|
+
const fnCycles = findCycles(db, { fileLevel: false, noTests });
|
|
1770
1335
|
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
console.log(`\nResults for "${name}":\n`);
|
|
1791
|
-
for (const r of data.results) {
|
|
1792
|
-
console.log(` ${kindIcon(r.kind)} ${r.name} (${r.kind}) -- ${r.file}:${r.line}`);
|
|
1793
|
-
if (r.callees.length > 0) {
|
|
1794
|
-
console.log(` -> calls/uses:`);
|
|
1795
|
-
for (const c of r.callees.slice(0, 15))
|
|
1796
|
-
console.log(` -> ${c.name} (${c.edgeKind}) ${c.file}:${c.line}`);
|
|
1797
|
-
if (r.callees.length > 15) console.log(` ... and ${r.callees.length - 15} more`);
|
|
1798
|
-
}
|
|
1799
|
-
if (r.callers.length > 0) {
|
|
1800
|
-
console.log(` <- called by:`);
|
|
1801
|
-
for (const c of r.callers.slice(0, 15))
|
|
1802
|
-
console.log(` <- ${c.name} (${c.edgeKind}) ${c.file}:${c.line}`);
|
|
1803
|
-
if (r.callers.length > 15) console.log(` ... and ${r.callers.length - 15} more`);
|
|
1804
|
-
}
|
|
1805
|
-
console.log();
|
|
1806
|
-
}
|
|
1807
|
-
}
|
|
1808
|
-
|
|
1809
|
-
export function impactAnalysis(file, customDbPath, opts = {}) {
|
|
1810
|
-
const data = impactAnalysisData(file, customDbPath, opts);
|
|
1811
|
-
if (opts.ndjson) {
|
|
1812
|
-
printNdjson(data, 'sources');
|
|
1813
|
-
return;
|
|
1814
|
-
}
|
|
1815
|
-
if (opts.json) {
|
|
1816
|
-
console.log(JSON.stringify(data, null, 2));
|
|
1817
|
-
return;
|
|
1818
|
-
}
|
|
1819
|
-
if (data.sources.length === 0) {
|
|
1820
|
-
console.log(`No file matching "${file}" in graph`);
|
|
1821
|
-
return;
|
|
1822
|
-
}
|
|
1336
|
+
// Top 5 coupling hotspots (fan-in + fan-out, file nodes)
|
|
1337
|
+
const testFilter = testFilterSQL('n.file', noTests);
|
|
1338
|
+
const hotspotRows = db
|
|
1339
|
+
.prepare(`
|
|
1340
|
+
SELECT n.file,
|
|
1341
|
+
(SELECT COUNT(*) FROM edges WHERE target_id = n.id) as fan_in,
|
|
1342
|
+
(SELECT COUNT(*) FROM edges WHERE source_id = n.id) as fan_out
|
|
1343
|
+
FROM nodes n
|
|
1344
|
+
WHERE n.kind = 'file' ${testFilter}
|
|
1345
|
+
ORDER BY (SELECT COUNT(*) FROM edges WHERE target_id = n.id)
|
|
1346
|
+
+ (SELECT COUNT(*) FROM edges WHERE source_id = n.id) DESC
|
|
1347
|
+
`)
|
|
1348
|
+
.all();
|
|
1349
|
+
const filteredHotspots = noTests ? hotspotRows.filter((r) => !isTestFile(r.file)) : hotspotRows;
|
|
1350
|
+
const hotspots = filteredHotspots.slice(0, 5).map((r) => ({
|
|
1351
|
+
file: r.file,
|
|
1352
|
+
fanIn: r.fan_in,
|
|
1353
|
+
fanOut: r.fan_out,
|
|
1354
|
+
}));
|
|
1823
1355
|
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1356
|
+
// Embeddings metadata
|
|
1357
|
+
let embeddings = null;
|
|
1358
|
+
try {
|
|
1359
|
+
const count = db.prepare('SELECT COUNT(*) as c FROM embeddings').get();
|
|
1360
|
+
if (count && count.c > 0) {
|
|
1361
|
+
const meta = {};
|
|
1362
|
+
const metaRows = db.prepare('SELECT key, value FROM embedding_meta').all();
|
|
1363
|
+
for (const r of metaRows) meta[r.key] = r.value;
|
|
1364
|
+
embeddings = {
|
|
1365
|
+
count: count.c,
|
|
1366
|
+
model: meta.model || null,
|
|
1367
|
+
dim: meta.dim ? parseInt(meta.dim, 10) : null,
|
|
1368
|
+
builtAt: meta.built_at || null,
|
|
1369
|
+
};
|
|
1370
|
+
}
|
|
1371
|
+
} catch {
|
|
1372
|
+
/* embeddings table may not exist */
|
|
1839
1373
|
}
|
|
1840
|
-
}
|
|
1841
|
-
console.log(`\n Total: ${data.totalDependents} files transitively depend on "${file}"\n`);
|
|
1842
|
-
}
|
|
1843
1374
|
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1375
|
+
// Graph quality metrics
|
|
1376
|
+
const qualityTestFilter = testFilter.replace(/n\.file/g, 'file');
|
|
1377
|
+
const totalCallable = db
|
|
1378
|
+
.prepare(
|
|
1379
|
+
`SELECT COUNT(*) as c FROM nodes WHERE kind IN ('function', 'method') ${qualityTestFilter}`,
|
|
1380
|
+
)
|
|
1381
|
+
.get().c;
|
|
1382
|
+
const callableWithCallers = db
|
|
1383
|
+
.prepare(`
|
|
1384
|
+
SELECT COUNT(DISTINCT e.target_id) as c FROM edges e
|
|
1385
|
+
JOIN nodes n ON e.target_id = n.id
|
|
1386
|
+
WHERE e.kind = 'calls' AND n.kind IN ('function', 'method') ${testFilter}
|
|
1387
|
+
`)
|
|
1388
|
+
.get().c;
|
|
1389
|
+
const callerCoverage = totalCallable > 0 ? callableWithCallers / totalCallable : 0;
|
|
1390
|
+
|
|
1391
|
+
const totalCallEdges = db
|
|
1392
|
+
.prepare("SELECT COUNT(*) as c FROM edges WHERE kind = 'calls'")
|
|
1393
|
+
.get().c;
|
|
1394
|
+
const highConfCallEdges = db
|
|
1395
|
+
.prepare("SELECT COUNT(*) as c FROM edges WHERE kind = 'calls' AND confidence >= 0.7")
|
|
1396
|
+
.get().c;
|
|
1397
|
+
const callConfidence = totalCallEdges > 0 ? highConfCallEdges / totalCallEdges : 0;
|
|
1398
|
+
|
|
1399
|
+
// False-positive warnings: generic names with > threshold callers
|
|
1400
|
+
const fpRows = db
|
|
1401
|
+
.prepare(`
|
|
1402
|
+
SELECT n.name, n.file, n.line, COUNT(e.source_id) as caller_count
|
|
1403
|
+
FROM nodes n
|
|
1404
|
+
LEFT JOIN edges e ON n.id = e.target_id AND e.kind = 'calls'
|
|
1405
|
+
WHERE n.kind IN ('function', 'method')
|
|
1406
|
+
GROUP BY n.id
|
|
1407
|
+
HAVING caller_count > ?
|
|
1408
|
+
ORDER BY caller_count DESC
|
|
1409
|
+
`)
|
|
1410
|
+
.all(FALSE_POSITIVE_CALLER_THRESHOLD);
|
|
1411
|
+
const falsePositiveWarnings = fpRows
|
|
1412
|
+
.filter((r) =>
|
|
1413
|
+
FALSE_POSITIVE_NAMES.has(r.name.includes('.') ? r.name.split('.').pop() : r.name),
|
|
1414
|
+
)
|
|
1415
|
+
.map((r) => ({ name: r.name, file: r.file, line: r.line, callerCount: r.caller_count }));
|
|
1850
1416
|
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
dirs.get(n.dir).push(n);
|
|
1856
|
-
}
|
|
1857
|
-
for (const [dir, files] of [...dirs].sort()) {
|
|
1858
|
-
console.log(` [${dir}/]`);
|
|
1859
|
-
for (const f of files) {
|
|
1860
|
-
const coupling = f.inEdges + f.outEdges;
|
|
1861
|
-
const bar = '#'.repeat(Math.min(coupling, 40));
|
|
1862
|
-
console.log(
|
|
1863
|
-
` ${path.basename(f.file).padEnd(35)} <-${String(f.inEdges).padStart(3)} ->${String(f.outEdges).padStart(3)} =${String(coupling).padStart(3)} ${bar}`,
|
|
1864
|
-
);
|
|
1865
|
-
}
|
|
1866
|
-
}
|
|
1867
|
-
console.log(
|
|
1868
|
-
`\n Total: ${data.stats.totalFiles} files, ${data.stats.totalNodes} symbols, ${data.stats.totalEdges} edges\n`,
|
|
1869
|
-
);
|
|
1870
|
-
}
|
|
1417
|
+
// Edges from suspicious nodes
|
|
1418
|
+
let fpEdgeCount = 0;
|
|
1419
|
+
for (const fp of falsePositiveWarnings) fpEdgeCount += fp.callerCount;
|
|
1420
|
+
const falsePositiveRatio = totalCallEdges > 0 ? fpEdgeCount / totalCallEdges : 0;
|
|
1871
1421
|
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
printNdjson(data, 'results');
|
|
1876
|
-
return;
|
|
1877
|
-
}
|
|
1878
|
-
if (opts.json) {
|
|
1879
|
-
console.log(JSON.stringify(data, null, 2));
|
|
1880
|
-
return;
|
|
1881
|
-
}
|
|
1882
|
-
if (data.results.length === 0) {
|
|
1883
|
-
console.log(`No file matching "${file}" in graph`);
|
|
1884
|
-
return;
|
|
1885
|
-
}
|
|
1422
|
+
const score = Math.round(
|
|
1423
|
+
callerCoverage * 40 + callConfidence * 40 + (1 - falsePositiveRatio) * 20,
|
|
1424
|
+
);
|
|
1886
1425
|
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
}
|
|
1902
|
-
console.log();
|
|
1903
|
-
}
|
|
1904
|
-
}
|
|
1426
|
+
const quality = {
|
|
1427
|
+
score,
|
|
1428
|
+
callerCoverage: {
|
|
1429
|
+
ratio: callerCoverage,
|
|
1430
|
+
covered: callableWithCallers,
|
|
1431
|
+
total: totalCallable,
|
|
1432
|
+
},
|
|
1433
|
+
callConfidence: {
|
|
1434
|
+
ratio: callConfidence,
|
|
1435
|
+
highConf: highConfCallEdges,
|
|
1436
|
+
total: totalCallEdges,
|
|
1437
|
+
},
|
|
1438
|
+
falsePositiveWarnings,
|
|
1439
|
+
};
|
|
1905
1440
|
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1441
|
+
// Role distribution
|
|
1442
|
+
let roleRows;
|
|
1443
|
+
if (noTests) {
|
|
1444
|
+
const allRoleNodes = db.prepare('SELECT role, file FROM nodes WHERE role IS NOT NULL').all();
|
|
1445
|
+
const filtered = allRoleNodes.filter((n) => !isTestFile(n.file));
|
|
1446
|
+
const counts = {};
|
|
1447
|
+
for (const n of filtered) counts[n.role] = (counts[n.role] || 0) + 1;
|
|
1448
|
+
roleRows = Object.entries(counts).map(([role, c]) => ({ role, c }));
|
|
1449
|
+
} else {
|
|
1450
|
+
roleRows = db
|
|
1451
|
+
.prepare('SELECT role, COUNT(*) as c FROM nodes WHERE role IS NOT NULL GROUP BY role')
|
|
1452
|
+
.all();
|
|
1453
|
+
}
|
|
1454
|
+
const roles = {};
|
|
1455
|
+
for (const r of roleRows) roles[r.role] = r.c;
|
|
1920
1456
|
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1457
|
+
// Complexity summary
|
|
1458
|
+
let complexity = null;
|
|
1459
|
+
try {
|
|
1460
|
+
const cRows = db
|
|
1461
|
+
.prepare(
|
|
1462
|
+
`SELECT fc.cognitive, fc.cyclomatic, fc.max_nesting, fc.maintainability_index
|
|
1463
|
+
FROM function_complexity fc JOIN nodes n ON fc.node_id = n.id
|
|
1464
|
+
WHERE n.kind IN ('function','method') ${testFilter}`,
|
|
1465
|
+
)
|
|
1466
|
+
.all();
|
|
1467
|
+
if (cRows.length > 0) {
|
|
1468
|
+
const miValues = cRows.map((r) => r.maintainability_index || 0);
|
|
1469
|
+
complexity = {
|
|
1470
|
+
analyzed: cRows.length,
|
|
1471
|
+
avgCognitive: +(cRows.reduce((s, r) => s + r.cognitive, 0) / cRows.length).toFixed(1),
|
|
1472
|
+
avgCyclomatic: +(cRows.reduce((s, r) => s + r.cyclomatic, 0) / cRows.length).toFixed(1),
|
|
1473
|
+
maxCognitive: Math.max(...cRows.map((r) => r.cognitive)),
|
|
1474
|
+
maxCyclomatic: Math.max(...cRows.map((r) => r.cyclomatic)),
|
|
1475
|
+
avgMI: +(miValues.reduce((s, v) => s + v, 0) / miValues.length).toFixed(1),
|
|
1476
|
+
minMI: +Math.min(...miValues).toFixed(1),
|
|
1477
|
+
};
|
|
1933
1478
|
}
|
|
1479
|
+
} catch {
|
|
1480
|
+
/* table may not exist in older DBs */
|
|
1934
1481
|
}
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1482
|
+
|
|
1483
|
+
return {
|
|
1484
|
+
nodes: { total: totalNodes, byKind: nodesByKind },
|
|
1485
|
+
edges: { total: totalEdges, byKind: edgesByKind },
|
|
1486
|
+
files: { total: fileNodes.length, languages: langCount, byLanguage },
|
|
1487
|
+
cycles: { fileLevel: fileCycles.length, functionLevel: fnCycles.length },
|
|
1488
|
+
hotspots,
|
|
1489
|
+
embeddings,
|
|
1490
|
+
quality,
|
|
1491
|
+
roles,
|
|
1492
|
+
complexity,
|
|
1493
|
+
};
|
|
1494
|
+
} finally {
|
|
1495
|
+
db.close();
|
|
1949
1496
|
}
|
|
1950
1497
|
}
|
|
1951
1498
|
|
|
@@ -2063,347 +1610,242 @@ function extractSignature(fileLines, line) {
|
|
|
2063
1610
|
|
|
2064
1611
|
export function contextData(name, customDbPath, opts = {}) {
|
|
2065
1612
|
const db = openReadonlyOrFail(customDbPath);
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
1613
|
+
try {
|
|
1614
|
+
const depth = opts.depth || 0;
|
|
1615
|
+
const noSource = opts.noSource || false;
|
|
1616
|
+
const noTests = opts.noTests || false;
|
|
1617
|
+
const includeTests = opts.includeTests || false;
|
|
2070
1618
|
|
|
2071
|
-
|
|
2072
|
-
|
|
1619
|
+
const dbPath = findDbPath(customDbPath);
|
|
1620
|
+
const repoRoot = path.resolve(path.dirname(dbPath), '..');
|
|
2073
1621
|
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
}
|
|
1622
|
+
const nodes = findMatchingNodes(db, name, { noTests, file: opts.file, kind: opts.kind });
|
|
1623
|
+
if (nodes.length === 0) {
|
|
1624
|
+
return { name, results: [] };
|
|
1625
|
+
}
|
|
2079
1626
|
|
|
2080
|
-
|
|
1627
|
+
// No hardcoded slice — pagination handles bounding via limit/offset
|
|
2081
1628
|
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
1629
|
+
// File-lines cache to avoid re-reading the same file
|
|
1630
|
+
const fileCache = new Map();
|
|
1631
|
+
function getFileLines(file) {
|
|
1632
|
+
if (fileCache.has(file)) return fileCache.get(file);
|
|
1633
|
+
try {
|
|
1634
|
+
const absPath = safePath(repoRoot, file);
|
|
1635
|
+
if (!absPath) {
|
|
1636
|
+
fileCache.set(file, null);
|
|
1637
|
+
return null;
|
|
1638
|
+
}
|
|
1639
|
+
const lines = fs.readFileSync(absPath, 'utf-8').split('\n');
|
|
1640
|
+
fileCache.set(file, lines);
|
|
1641
|
+
return lines;
|
|
1642
|
+
} catch (e) {
|
|
1643
|
+
debug(`getFileLines failed for ${file}: ${e.message}`);
|
|
2089
1644
|
fileCache.set(file, null);
|
|
2090
1645
|
return null;
|
|
2091
1646
|
}
|
|
2092
|
-
const lines = fs.readFileSync(absPath, 'utf-8').split('\n');
|
|
2093
|
-
fileCache.set(file, lines);
|
|
2094
|
-
return lines;
|
|
2095
|
-
} catch (e) {
|
|
2096
|
-
debug(`getFileLines failed for ${file}: ${e.message}`);
|
|
2097
|
-
fileCache.set(file, null);
|
|
2098
|
-
return null;
|
|
2099
1647
|
}
|
|
2100
|
-
}
|
|
2101
1648
|
|
|
2102
|
-
|
|
2103
|
-
|
|
1649
|
+
const results = nodes.map((node) => {
|
|
1650
|
+
const fileLines = getFileLines(node.file);
|
|
2104
1651
|
|
|
2105
|
-
|
|
2106
|
-
|
|
1652
|
+
// Source
|
|
1653
|
+
const source = noSource
|
|
1654
|
+
? null
|
|
1655
|
+
: readSourceRange(repoRoot, node.file, node.line, node.end_line);
|
|
2107
1656
|
|
|
2108
|
-
|
|
2109
|
-
|
|
1657
|
+
// Signature
|
|
1658
|
+
const signature = fileLines ? extractSignature(fileLines, node.line) : null;
|
|
2110
1659
|
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
1660
|
+
// Callees
|
|
1661
|
+
const calleeRows = db
|
|
1662
|
+
.prepare(
|
|
1663
|
+
`SELECT n.id, n.name, n.kind, n.file, n.line, n.end_line
|
|
1664
|
+
FROM edges e JOIN nodes n ON e.target_id = n.id
|
|
1665
|
+
WHERE e.source_id = ? AND e.kind = 'calls'`,
|
|
1666
|
+
)
|
|
1667
|
+
.all(node.id);
|
|
1668
|
+
const filteredCallees = noTests ? calleeRows.filter((c) => !isTestFile(c.file)) : calleeRows;
|
|
1669
|
+
|
|
1670
|
+
const callees = filteredCallees.map((c) => {
|
|
1671
|
+
const cLines = getFileLines(c.file);
|
|
1672
|
+
const summary = cLines ? extractSummary(cLines, c.line) : null;
|
|
1673
|
+
let calleeSource = null;
|
|
1674
|
+
if (depth >= 1) {
|
|
1675
|
+
calleeSource = readSourceRange(repoRoot, c.file, c.line, c.end_line);
|
|
1676
|
+
}
|
|
1677
|
+
return {
|
|
1678
|
+
name: c.name,
|
|
1679
|
+
kind: c.kind,
|
|
1680
|
+
file: c.file,
|
|
1681
|
+
line: c.line,
|
|
1682
|
+
endLine: c.end_line || null,
|
|
1683
|
+
summary,
|
|
1684
|
+
source: calleeSource,
|
|
1685
|
+
};
|
|
1686
|
+
});
|
|
2138
1687
|
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
1688
|
+
// Deep callee expansion via BFS (depth > 1, capped at 5)
|
|
1689
|
+
if (depth > 1) {
|
|
1690
|
+
const visited = new Set(filteredCallees.map((c) => c.id));
|
|
1691
|
+
visited.add(node.id);
|
|
1692
|
+
let frontier = filteredCallees.map((c) => c.id);
|
|
1693
|
+
const maxDepth = Math.min(depth, 5);
|
|
1694
|
+
for (let d = 2; d <= maxDepth; d++) {
|
|
1695
|
+
const nextFrontier = [];
|
|
1696
|
+
for (const fid of frontier) {
|
|
1697
|
+
const deeper = db
|
|
1698
|
+
.prepare(
|
|
1699
|
+
`SELECT n.id, n.name, n.kind, n.file, n.line, n.end_line
|
|
1700
|
+
FROM edges e JOIN nodes n ON e.target_id = n.id
|
|
1701
|
+
WHERE e.source_id = ? AND e.kind = 'calls'`,
|
|
1702
|
+
)
|
|
1703
|
+
.all(fid);
|
|
1704
|
+
for (const c of deeper) {
|
|
1705
|
+
if (!visited.has(c.id) && (!noTests || !isTestFile(c.file))) {
|
|
1706
|
+
visited.add(c.id);
|
|
1707
|
+
nextFrontier.push(c.id);
|
|
1708
|
+
const cLines = getFileLines(c.file);
|
|
1709
|
+
callees.push({
|
|
1710
|
+
name: c.name,
|
|
1711
|
+
kind: c.kind,
|
|
1712
|
+
file: c.file,
|
|
1713
|
+
line: c.line,
|
|
1714
|
+
endLine: c.end_line || null,
|
|
1715
|
+
summary: cLines ? extractSummary(cLines, c.line) : null,
|
|
1716
|
+
source: readSourceRange(repoRoot, c.file, c.line, c.end_line),
|
|
1717
|
+
});
|
|
1718
|
+
}
|
|
2169
1719
|
}
|
|
2170
1720
|
}
|
|
1721
|
+
frontier = nextFrontier;
|
|
1722
|
+
if (frontier.length === 0) break;
|
|
2171
1723
|
}
|
|
2172
|
-
frontier = nextFrontier;
|
|
2173
|
-
if (frontier.length === 0) break;
|
|
2174
1724
|
}
|
|
2175
|
-
}
|
|
2176
1725
|
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
.prepare(
|
|
2180
|
-
`SELECT n.name, n.kind, n.file, n.line
|
|
2181
|
-
FROM edges e JOIN nodes n ON e.source_id = n.id
|
|
2182
|
-
WHERE e.target_id = ? AND e.kind = 'calls'`,
|
|
2183
|
-
)
|
|
2184
|
-
.all(node.id);
|
|
2185
|
-
|
|
2186
|
-
// Method hierarchy resolution
|
|
2187
|
-
if (node.kind === 'method' && node.name.includes('.')) {
|
|
2188
|
-
const methodName = node.name.split('.').pop();
|
|
2189
|
-
const relatedMethods = resolveMethodViaHierarchy(db, methodName);
|
|
2190
|
-
for (const rm of relatedMethods) {
|
|
2191
|
-
if (rm.id === node.id) continue;
|
|
2192
|
-
const extraCallers = db
|
|
2193
|
-
.prepare(
|
|
2194
|
-
`SELECT n.name, n.kind, n.file, n.line
|
|
2195
|
-
FROM edges e JOIN nodes n ON e.source_id = n.id
|
|
2196
|
-
WHERE e.target_id = ? AND e.kind = 'calls'`,
|
|
2197
|
-
)
|
|
2198
|
-
.all(rm.id);
|
|
2199
|
-
callerRows.push(...extraCallers.map((c) => ({ ...c, viaHierarchy: rm.name })));
|
|
2200
|
-
}
|
|
2201
|
-
}
|
|
2202
|
-
if (noTests) callerRows = callerRows.filter((c) => !isTestFile(c.file));
|
|
2203
|
-
|
|
2204
|
-
const callers = callerRows.map((c) => ({
|
|
2205
|
-
name: c.name,
|
|
2206
|
-
kind: c.kind,
|
|
2207
|
-
file: c.file,
|
|
2208
|
-
line: c.line,
|
|
2209
|
-
viaHierarchy: c.viaHierarchy || undefined,
|
|
2210
|
-
}));
|
|
2211
|
-
|
|
2212
|
-
// Related tests: callers that live in test files
|
|
2213
|
-
const testCallerRows = db
|
|
2214
|
-
.prepare(
|
|
2215
|
-
`SELECT n.name, n.kind, n.file, n.line
|
|
2216
|
-
FROM edges e JOIN nodes n ON e.source_id = n.id
|
|
2217
|
-
WHERE e.target_id = ? AND e.kind = 'calls'`,
|
|
2218
|
-
)
|
|
2219
|
-
.all(node.id);
|
|
2220
|
-
const testCallers = testCallerRows.filter((c) => isTestFile(c.file));
|
|
2221
|
-
|
|
2222
|
-
const testsByFile = new Map();
|
|
2223
|
-
for (const tc of testCallers) {
|
|
2224
|
-
if (!testsByFile.has(tc.file)) testsByFile.set(tc.file, []);
|
|
2225
|
-
testsByFile.get(tc.file).push(tc);
|
|
2226
|
-
}
|
|
2227
|
-
|
|
2228
|
-
const relatedTests = [];
|
|
2229
|
-
for (const [file] of testsByFile) {
|
|
2230
|
-
const tLines = getFileLines(file);
|
|
2231
|
-
const testNames = [];
|
|
2232
|
-
if (tLines) {
|
|
2233
|
-
for (const tl of tLines) {
|
|
2234
|
-
const tm = tl.match(/(?:it|test|describe)\s*\(\s*['"`]([^'"`]+)['"`]/);
|
|
2235
|
-
if (tm) testNames.push(tm[1]);
|
|
2236
|
-
}
|
|
2237
|
-
}
|
|
2238
|
-
const testSource = includeTests && tLines ? tLines.join('\n') : undefined;
|
|
2239
|
-
relatedTests.push({
|
|
2240
|
-
file,
|
|
2241
|
-
testCount: testNames.length,
|
|
2242
|
-
testNames,
|
|
2243
|
-
source: testSource,
|
|
2244
|
-
});
|
|
2245
|
-
}
|
|
2246
|
-
|
|
2247
|
-
// Complexity metrics
|
|
2248
|
-
let complexityMetrics = null;
|
|
2249
|
-
try {
|
|
2250
|
-
const cRow = db
|
|
1726
|
+
// Callers
|
|
1727
|
+
let callerRows = db
|
|
2251
1728
|
.prepare(
|
|
2252
|
-
|
|
1729
|
+
`SELECT n.name, n.kind, n.file, n.line
|
|
1730
|
+
FROM edges e JOIN nodes n ON e.source_id = n.id
|
|
1731
|
+
WHERE e.target_id = ? AND e.kind = 'calls'`,
|
|
2253
1732
|
)
|
|
2254
|
-
.
|
|
2255
|
-
if (cRow) {
|
|
2256
|
-
complexityMetrics = {
|
|
2257
|
-
cognitive: cRow.cognitive,
|
|
2258
|
-
cyclomatic: cRow.cyclomatic,
|
|
2259
|
-
maxNesting: cRow.max_nesting,
|
|
2260
|
-
maintainabilityIndex: cRow.maintainability_index || 0,
|
|
2261
|
-
halsteadVolume: cRow.halstead_volume || 0,
|
|
2262
|
-
};
|
|
2263
|
-
}
|
|
2264
|
-
} catch {
|
|
2265
|
-
/* table may not exist */
|
|
2266
|
-
}
|
|
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
|
-
|
|
2279
|
-
return {
|
|
2280
|
-
name: node.name,
|
|
2281
|
-
kind: node.kind,
|
|
2282
|
-
file: node.file,
|
|
2283
|
-
line: node.line,
|
|
2284
|
-
role: node.role || null,
|
|
2285
|
-
endLine: node.end_line || null,
|
|
2286
|
-
source,
|
|
2287
|
-
signature,
|
|
2288
|
-
complexity: complexityMetrics,
|
|
2289
|
-
children: nodeChildren.length > 0 ? nodeChildren : undefined,
|
|
2290
|
-
callees,
|
|
2291
|
-
callers,
|
|
2292
|
-
relatedTests,
|
|
2293
|
-
};
|
|
2294
|
-
});
|
|
2295
|
-
|
|
2296
|
-
db.close();
|
|
2297
|
-
const base = { name, results };
|
|
2298
|
-
return paginateResult(base, 'results', { limit: opts.limit, offset: opts.offset });
|
|
2299
|
-
}
|
|
1733
|
+
.all(node.id);
|
|
2300
1734
|
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
1735
|
+
// Method hierarchy resolution
|
|
1736
|
+
if (node.kind === 'method' && node.name.includes('.')) {
|
|
1737
|
+
const methodName = node.name.split('.').pop();
|
|
1738
|
+
const relatedMethods = resolveMethodViaHierarchy(db, methodName);
|
|
1739
|
+
for (const rm of relatedMethods) {
|
|
1740
|
+
if (rm.id === node.id) continue;
|
|
1741
|
+
const extraCallers = db
|
|
1742
|
+
.prepare(
|
|
1743
|
+
`SELECT n.name, n.kind, n.file, n.line
|
|
1744
|
+
FROM edges e JOIN nodes n ON e.source_id = n.id
|
|
1745
|
+
WHERE e.target_id = ? AND e.kind = 'calls'`,
|
|
1746
|
+
)
|
|
1747
|
+
.all(rm.id);
|
|
1748
|
+
callerRows.push(...extraCallers.map((c) => ({ ...c, viaHierarchy: rm.name })));
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
if (noTests) callerRows = callerRows.filter((c) => !isTestFile(c.file));
|
|
2315
1752
|
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
1753
|
+
const callers = callerRows.map((c) => ({
|
|
1754
|
+
name: c.name,
|
|
1755
|
+
kind: c.kind,
|
|
1756
|
+
file: c.file,
|
|
1757
|
+
line: c.line,
|
|
1758
|
+
viaHierarchy: c.viaHierarchy || undefined,
|
|
1759
|
+
}));
|
|
2320
1760
|
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
1761
|
+
// Related tests: callers that live in test files
|
|
1762
|
+
const testCallerRows = db
|
|
1763
|
+
.prepare(
|
|
1764
|
+
`SELECT n.name, n.kind, n.file, n.line
|
|
1765
|
+
FROM edges e JOIN nodes n ON e.source_id = n.id
|
|
1766
|
+
WHERE e.target_id = ? AND e.kind = 'calls'`,
|
|
1767
|
+
)
|
|
1768
|
+
.all(node.id);
|
|
1769
|
+
const testCallers = testCallerRows.filter((c) => isTestFile(c.file));
|
|
2328
1770
|
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
console.log(` ${kindIcon(c.kind)} ${c.name} :${c.line}`);
|
|
1771
|
+
const testsByFile = new Map();
|
|
1772
|
+
for (const tc of testCallers) {
|
|
1773
|
+
if (!testsByFile.has(tc.file)) testsByFile.set(tc.file, []);
|
|
1774
|
+
testsByFile.get(tc.file).push(tc);
|
|
2334
1775
|
}
|
|
2335
|
-
console.log();
|
|
2336
|
-
}
|
|
2337
1776
|
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
console.log();
|
|
2347
|
-
}
|
|
2348
|
-
|
|
2349
|
-
// Source
|
|
2350
|
-
if (r.source) {
|
|
2351
|
-
console.log('## Source');
|
|
2352
|
-
for (const line of r.source.split('\n')) {
|
|
2353
|
-
console.log(` ${line}`);
|
|
2354
|
-
}
|
|
2355
|
-
console.log();
|
|
2356
|
-
}
|
|
2357
|
-
|
|
2358
|
-
// Callees
|
|
2359
|
-
if (r.callees.length > 0) {
|
|
2360
|
-
console.log(`## Direct Dependencies (${r.callees.length})`);
|
|
2361
|
-
for (const c of r.callees) {
|
|
2362
|
-
const summary = c.summary ? ` — ${c.summary}` : '';
|
|
2363
|
-
console.log(` ${kindIcon(c.kind)} ${c.name} ${c.file}:${c.line}${summary}`);
|
|
2364
|
-
if (c.source) {
|
|
2365
|
-
for (const line of c.source.split('\n').slice(0, 10)) {
|
|
2366
|
-
console.log(` | ${line}`);
|
|
1777
|
+
const relatedTests = [];
|
|
1778
|
+
for (const [file] of testsByFile) {
|
|
1779
|
+
const tLines = getFileLines(file);
|
|
1780
|
+
const testNames = [];
|
|
1781
|
+
if (tLines) {
|
|
1782
|
+
for (const tl of tLines) {
|
|
1783
|
+
const tm = tl.match(/(?:it|test|describe)\s*\(\s*['"`]([^'"`]+)['"`]/);
|
|
1784
|
+
if (tm) testNames.push(tm[1]);
|
|
2367
1785
|
}
|
|
2368
1786
|
}
|
|
1787
|
+
const testSource = includeTests && tLines ? tLines.join('\n') : undefined;
|
|
1788
|
+
relatedTests.push({
|
|
1789
|
+
file,
|
|
1790
|
+
testCount: testNames.length,
|
|
1791
|
+
testNames,
|
|
1792
|
+
source: testSource,
|
|
1793
|
+
});
|
|
2369
1794
|
}
|
|
2370
|
-
console.log();
|
|
2371
|
-
}
|
|
2372
1795
|
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
1796
|
+
// Complexity metrics
|
|
1797
|
+
let complexityMetrics = null;
|
|
1798
|
+
try {
|
|
1799
|
+
const cRow = db
|
|
1800
|
+
.prepare(
|
|
1801
|
+
'SELECT cognitive, cyclomatic, max_nesting, maintainability_index, halstead_volume FROM function_complexity WHERE node_id = ?',
|
|
1802
|
+
)
|
|
1803
|
+
.get(node.id);
|
|
1804
|
+
if (cRow) {
|
|
1805
|
+
complexityMetrics = {
|
|
1806
|
+
cognitive: cRow.cognitive,
|
|
1807
|
+
cyclomatic: cRow.cyclomatic,
|
|
1808
|
+
maxNesting: cRow.max_nesting,
|
|
1809
|
+
maintainabilityIndex: cRow.maintainability_index || 0,
|
|
1810
|
+
halsteadVolume: cRow.halstead_volume || 0,
|
|
1811
|
+
};
|
|
1812
|
+
}
|
|
1813
|
+
} catch {
|
|
1814
|
+
/* table may not exist */
|
|
2379
1815
|
}
|
|
2380
|
-
console.log();
|
|
2381
|
-
}
|
|
2382
1816
|
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
console.log(' Source:');
|
|
2393
|
-
for (const line of t.source.split('\n').slice(0, 20)) {
|
|
2394
|
-
console.log(` | ${line}`);
|
|
2395
|
-
}
|
|
2396
|
-
}
|
|
1817
|
+
// Children (parameters, properties, constants)
|
|
1818
|
+
let nodeChildren = [];
|
|
1819
|
+
try {
|
|
1820
|
+
nodeChildren = db
|
|
1821
|
+
.prepare('SELECT name, kind, line, end_line FROM nodes WHERE parent_id = ? ORDER BY line')
|
|
1822
|
+
.all(node.id)
|
|
1823
|
+
.map((c) => ({ name: c.name, kind: c.kind, line: c.line, endLine: c.end_line || null }));
|
|
1824
|
+
} catch {
|
|
1825
|
+
/* parent_id column may not exist */
|
|
2397
1826
|
}
|
|
2398
|
-
console.log();
|
|
2399
|
-
}
|
|
2400
1827
|
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
1828
|
+
return {
|
|
1829
|
+
name: node.name,
|
|
1830
|
+
kind: node.kind,
|
|
1831
|
+
file: node.file,
|
|
1832
|
+
line: node.line,
|
|
1833
|
+
role: node.role || null,
|
|
1834
|
+
endLine: node.end_line || null,
|
|
1835
|
+
source,
|
|
1836
|
+
signature,
|
|
1837
|
+
complexity: complexityMetrics,
|
|
1838
|
+
children: nodeChildren.length > 0 ? nodeChildren : undefined,
|
|
1839
|
+
callees,
|
|
1840
|
+
callers,
|
|
1841
|
+
relatedTests,
|
|
1842
|
+
};
|
|
1843
|
+
});
|
|
1844
|
+
|
|
1845
|
+
const base = { name, results };
|
|
1846
|
+
return paginateResult(base, 'results', { limit: opts.limit, offset: opts.offset });
|
|
1847
|
+
} finally {
|
|
1848
|
+
db.close();
|
|
2407
1849
|
}
|
|
2408
1850
|
}
|
|
2409
1851
|
|
|
@@ -2411,62 +1853,42 @@ export function context(name, customDbPath, opts = {}) {
|
|
|
2411
1853
|
|
|
2412
1854
|
export function childrenData(name, customDbPath, opts = {}) {
|
|
2413
1855
|
const db = openReadonlyOrFail(customDbPath);
|
|
2414
|
-
|
|
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
|
-
}
|
|
1856
|
+
try {
|
|
1857
|
+
const noTests = opts.noTests || false;
|
|
2421
1858
|
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
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 = [];
|
|
1859
|
+
const nodes = findMatchingNodes(db, name, { noTests, file: opts.file, kind: opts.kind });
|
|
1860
|
+
if (nodes.length === 0) {
|
|
1861
|
+
return { name, results: [] };
|
|
2430
1862
|
}
|
|
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
1863
|
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
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}`);
|
|
1864
|
+
const results = nodes.map((node) => {
|
|
1865
|
+
let children;
|
|
1866
|
+
try {
|
|
1867
|
+
children = db
|
|
1868
|
+
.prepare('SELECT name, kind, line, end_line FROM nodes WHERE parent_id = ? ORDER BY line')
|
|
1869
|
+
.all(node.id);
|
|
1870
|
+
} catch {
|
|
1871
|
+
children = [];
|
|
2468
1872
|
}
|
|
2469
|
-
|
|
1873
|
+
if (noTests) children = children.filter((c) => !isTestFile(c.file || node.file));
|
|
1874
|
+
return {
|
|
1875
|
+
name: node.name,
|
|
1876
|
+
kind: node.kind,
|
|
1877
|
+
file: node.file,
|
|
1878
|
+
line: node.line,
|
|
1879
|
+
children: children.map((c) => ({
|
|
1880
|
+
name: c.name,
|
|
1881
|
+
kind: c.kind,
|
|
1882
|
+
line: c.line,
|
|
1883
|
+
endLine: c.end_line || null,
|
|
1884
|
+
})),
|
|
1885
|
+
};
|
|
1886
|
+
});
|
|
1887
|
+
|
|
1888
|
+
const base = { name, results };
|
|
1889
|
+
return paginateResult(base, 'results', { limit: opts.limit, offset: opts.offset });
|
|
1890
|
+
} finally {
|
|
1891
|
+
db.close();
|
|
2470
1892
|
}
|
|
2471
1893
|
}
|
|
2472
1894
|
|
|
@@ -2664,200 +2086,73 @@ function explainFunctionImpl(db, target, noTests, getFileLines) {
|
|
|
2664
2086
|
|
|
2665
2087
|
export function explainData(target, customDbPath, opts = {}) {
|
|
2666
2088
|
const db = openReadonlyOrFail(customDbPath);
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2089
|
+
try {
|
|
2090
|
+
const noTests = opts.noTests || false;
|
|
2091
|
+
const depth = opts.depth || 0;
|
|
2092
|
+
const kind = isFileLikeTarget(target) ? 'file' : 'function';
|
|
2093
|
+
|
|
2094
|
+
const dbPath = findDbPath(customDbPath);
|
|
2095
|
+
const repoRoot = path.resolve(path.dirname(dbPath), '..');
|
|
2096
|
+
|
|
2097
|
+
const fileCache = new Map();
|
|
2098
|
+
function getFileLines(file) {
|
|
2099
|
+
if (fileCache.has(file)) return fileCache.get(file);
|
|
2100
|
+
try {
|
|
2101
|
+
const absPath = safePath(repoRoot, file);
|
|
2102
|
+
if (!absPath) {
|
|
2103
|
+
fileCache.set(file, null);
|
|
2104
|
+
return null;
|
|
2105
|
+
}
|
|
2106
|
+
const lines = fs.readFileSync(absPath, 'utf-8').split('\n');
|
|
2107
|
+
fileCache.set(file, lines);
|
|
2108
|
+
return lines;
|
|
2109
|
+
} catch (e) {
|
|
2110
|
+
debug(`getFileLines failed for ${file}: ${e.message}`);
|
|
2680
2111
|
fileCache.set(file, null);
|
|
2681
2112
|
return null;
|
|
2682
2113
|
}
|
|
2683
|
-
const lines = fs.readFileSync(absPath, 'utf-8').split('\n');
|
|
2684
|
-
fileCache.set(file, lines);
|
|
2685
|
-
return lines;
|
|
2686
|
-
} catch (e) {
|
|
2687
|
-
debug(`getFileLines failed for ${file}: ${e.message}`);
|
|
2688
|
-
fileCache.set(file, null);
|
|
2689
|
-
return null;
|
|
2690
2114
|
}
|
|
2691
|
-
}
|
|
2692
2115
|
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2116
|
+
const results =
|
|
2117
|
+
kind === 'file'
|
|
2118
|
+
? explainFileImpl(db, target, getFileLines)
|
|
2119
|
+
: explainFunctionImpl(db, target, noTests, getFileLines);
|
|
2120
|
+
|
|
2121
|
+
// Recursive dependency explanation for function targets
|
|
2122
|
+
if (kind === 'function' && depth > 0 && results.length > 0) {
|
|
2123
|
+
const visited = new Set(results.map((r) => `${r.name}:${r.file}:${r.line}`));
|
|
2124
|
+
|
|
2125
|
+
function explainCallees(parentResults, currentDepth) {
|
|
2126
|
+
if (currentDepth <= 0) return;
|
|
2127
|
+
for (const r of parentResults) {
|
|
2128
|
+
const newCallees = [];
|
|
2129
|
+
for (const callee of r.callees) {
|
|
2130
|
+
const key = `${callee.name}:${callee.file}:${callee.line}`;
|
|
2131
|
+
if (visited.has(key)) continue;
|
|
2132
|
+
visited.add(key);
|
|
2133
|
+
const calleeResults = explainFunctionImpl(db, callee.name, noTests, getFileLines);
|
|
2134
|
+
const exact = calleeResults.find(
|
|
2135
|
+
(cr) => cr.file === callee.file && cr.line === callee.line,
|
|
2136
|
+
);
|
|
2137
|
+
if (exact) {
|
|
2138
|
+
exact._depth = (r._depth || 0) + 1;
|
|
2139
|
+
newCallees.push(exact);
|
|
2140
|
+
}
|
|
2141
|
+
}
|
|
2142
|
+
if (newCallees.length > 0) {
|
|
2143
|
+
r.depDetails = newCallees;
|
|
2144
|
+
explainCallees(newCallees, currentDepth - 1);
|
|
2717
2145
|
}
|
|
2718
2146
|
}
|
|
2719
|
-
if (newCallees.length > 0) {
|
|
2720
|
-
r.depDetails = newCallees;
|
|
2721
|
-
explainCallees(newCallees, currentDepth - 1);
|
|
2722
|
-
}
|
|
2723
|
-
}
|
|
2724
|
-
}
|
|
2725
|
-
|
|
2726
|
-
explainCallees(results, depth);
|
|
2727
|
-
}
|
|
2728
|
-
|
|
2729
|
-
db.close();
|
|
2730
|
-
const base = { target, kind, results };
|
|
2731
|
-
return paginateResult(base, 'results', { limit: opts.limit, offset: opts.offset });
|
|
2732
|
-
}
|
|
2733
|
-
|
|
2734
|
-
export function explain(target, customDbPath, opts = {}) {
|
|
2735
|
-
const data = explainData(target, customDbPath, opts);
|
|
2736
|
-
if (opts.ndjson) {
|
|
2737
|
-
printNdjson(data, 'results');
|
|
2738
|
-
return;
|
|
2739
|
-
}
|
|
2740
|
-
if (opts.json) {
|
|
2741
|
-
console.log(JSON.stringify(data, null, 2));
|
|
2742
|
-
return;
|
|
2743
|
-
}
|
|
2744
|
-
if (data.results.length === 0) {
|
|
2745
|
-
console.log(`No ${data.kind === 'file' ? 'file' : 'function/symbol'} matching "${target}"`);
|
|
2746
|
-
return;
|
|
2747
|
-
}
|
|
2748
|
-
|
|
2749
|
-
if (data.kind === 'file') {
|
|
2750
|
-
for (const r of data.results) {
|
|
2751
|
-
const publicCount = r.publicApi.length;
|
|
2752
|
-
const internalCount = r.internal.length;
|
|
2753
|
-
const lineInfo = r.lineCount ? `${r.lineCount} lines, ` : '';
|
|
2754
|
-
console.log(`\n# ${r.file}`);
|
|
2755
|
-
console.log(
|
|
2756
|
-
` ${lineInfo}${r.symbolCount} symbols (${publicCount} exported, ${internalCount} internal)`,
|
|
2757
|
-
);
|
|
2758
|
-
|
|
2759
|
-
if (r.imports.length > 0) {
|
|
2760
|
-
console.log(` Imports: ${r.imports.map((i) => i.file).join(', ')}`);
|
|
2761
|
-
}
|
|
2762
|
-
if (r.importedBy.length > 0) {
|
|
2763
|
-
console.log(` Imported by: ${r.importedBy.map((i) => i.file).join(', ')}`);
|
|
2764
|
-
}
|
|
2765
|
-
|
|
2766
|
-
if (r.publicApi.length > 0) {
|
|
2767
|
-
console.log(`\n## Exported`);
|
|
2768
|
-
for (const s of r.publicApi) {
|
|
2769
|
-
const sig = s.signature?.params != null ? `(${s.signature.params})` : '';
|
|
2770
|
-
const roleTag = s.role ? ` [${s.role}]` : '';
|
|
2771
|
-
const summary = s.summary ? ` -- ${s.summary}` : '';
|
|
2772
|
-
console.log(` ${kindIcon(s.kind)} ${s.name}${sig}${roleTag} :${s.line}${summary}`);
|
|
2773
|
-
}
|
|
2774
|
-
}
|
|
2775
|
-
|
|
2776
|
-
if (r.internal.length > 0) {
|
|
2777
|
-
console.log(`\n## Internal`);
|
|
2778
|
-
for (const s of r.internal) {
|
|
2779
|
-
const sig = s.signature?.params != null ? `(${s.signature.params})` : '';
|
|
2780
|
-
const roleTag = s.role ? ` [${s.role}]` : '';
|
|
2781
|
-
const summary = s.summary ? ` -- ${s.summary}` : '';
|
|
2782
|
-
console.log(` ${kindIcon(s.kind)} ${s.name}${sig}${roleTag} :${s.line}${summary}`);
|
|
2783
|
-
}
|
|
2784
|
-
}
|
|
2785
|
-
|
|
2786
|
-
if (r.dataFlow.length > 0) {
|
|
2787
|
-
console.log(`\n## Data Flow`);
|
|
2788
|
-
for (const df of r.dataFlow) {
|
|
2789
|
-
console.log(` ${df.caller} -> ${df.callees.join(', ')}`);
|
|
2790
|
-
}
|
|
2791
|
-
}
|
|
2792
|
-
console.log();
|
|
2793
|
-
}
|
|
2794
|
-
} else {
|
|
2795
|
-
function printFunctionExplain(r, indent = '') {
|
|
2796
|
-
const lineRange = r.endLine ? `${r.line}-${r.endLine}` : `${r.line}`;
|
|
2797
|
-
const lineInfo = r.lineCount ? `${r.lineCount} lines` : '';
|
|
2798
|
-
const summaryPart = r.summary ? ` | ${r.summary}` : '';
|
|
2799
|
-
const roleTag = r.role ? ` [${r.role}]` : '';
|
|
2800
|
-
const depthLevel = r._depth || 0;
|
|
2801
|
-
const heading = depthLevel === 0 ? '#' : '##'.padEnd(depthLevel + 2, '#');
|
|
2802
|
-
console.log(`\n${indent}${heading} ${r.name} (${r.kind})${roleTag} ${r.file}:${lineRange}`);
|
|
2803
|
-
if (lineInfo || r.summary) {
|
|
2804
|
-
console.log(`${indent} ${lineInfo}${summaryPart}`);
|
|
2805
|
-
}
|
|
2806
|
-
if (r.signature) {
|
|
2807
|
-
if (r.signature.params != null)
|
|
2808
|
-
console.log(`${indent} Parameters: (${r.signature.params})`);
|
|
2809
|
-
if (r.signature.returnType) console.log(`${indent} Returns: ${r.signature.returnType}`);
|
|
2810
|
-
}
|
|
2811
|
-
|
|
2812
|
-
if (r.complexity) {
|
|
2813
|
-
const cx = r.complexity;
|
|
2814
|
-
const miPart = cx.maintainabilityIndex ? ` MI=${cx.maintainabilityIndex}` : '';
|
|
2815
|
-
console.log(
|
|
2816
|
-
`${indent} Complexity: cognitive=${cx.cognitive} cyclomatic=${cx.cyclomatic} nesting=${cx.maxNesting}${miPart}`,
|
|
2817
|
-
);
|
|
2818
|
-
}
|
|
2819
|
-
|
|
2820
|
-
if (r.callees.length > 0) {
|
|
2821
|
-
console.log(`\n${indent} Calls (${r.callees.length}):`);
|
|
2822
|
-
for (const c of r.callees) {
|
|
2823
|
-
console.log(`${indent} ${kindIcon(c.kind)} ${c.name} ${c.file}:${c.line}`);
|
|
2824
|
-
}
|
|
2825
|
-
}
|
|
2826
|
-
|
|
2827
|
-
if (r.callers.length > 0) {
|
|
2828
|
-
console.log(`\n${indent} Called by (${r.callers.length}):`);
|
|
2829
|
-
for (const c of r.callers) {
|
|
2830
|
-
console.log(`${indent} ${kindIcon(c.kind)} ${c.name} ${c.file}:${c.line}`);
|
|
2831
|
-
}
|
|
2832
|
-
}
|
|
2833
|
-
|
|
2834
|
-
if (r.relatedTests.length > 0) {
|
|
2835
|
-
const label = r.relatedTests.length === 1 ? 'file' : 'files';
|
|
2836
|
-
console.log(`\n${indent} Tests (${r.relatedTests.length} ${label}):`);
|
|
2837
|
-
for (const t of r.relatedTests) {
|
|
2838
|
-
console.log(`${indent} ${t.file}`);
|
|
2839
|
-
}
|
|
2840
|
-
}
|
|
2841
|
-
|
|
2842
|
-
if (r.callees.length === 0 && r.callers.length === 0) {
|
|
2843
|
-
console.log(
|
|
2844
|
-
`${indent} (no call edges found -- may be invoked dynamically or via re-exports)`,
|
|
2845
|
-
);
|
|
2846
2147
|
}
|
|
2847
2148
|
|
|
2848
|
-
|
|
2849
|
-
if (r.depDetails && r.depDetails.length > 0) {
|
|
2850
|
-
console.log(`\n${indent} --- Dependencies (depth ${depthLevel + 1}) ---`);
|
|
2851
|
-
for (const dep of r.depDetails) {
|
|
2852
|
-
printFunctionExplain(dep, `${indent} `);
|
|
2853
|
-
}
|
|
2854
|
-
}
|
|
2855
|
-
console.log();
|
|
2149
|
+
explainCallees(results, depth);
|
|
2856
2150
|
}
|
|
2857
2151
|
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2152
|
+
const base = { target, kind, results };
|
|
2153
|
+
return paginateResult(base, 'results', { limit: opts.limit, offset: opts.offset });
|
|
2154
|
+
} finally {
|
|
2155
|
+
db.close();
|
|
2861
2156
|
}
|
|
2862
2157
|
}
|
|
2863
2158
|
|
|
@@ -2987,178 +2282,107 @@ function whereFileImpl(db, target) {
|
|
|
2987
2282
|
|
|
2988
2283
|
export function whereData(target, customDbPath, opts = {}) {
|
|
2989
2284
|
const db = openReadonlyOrFail(customDbPath);
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
const results = fileMode ? whereFileImpl(db, target) : whereSymbolImpl(db, target, noTests);
|
|
2994
|
-
|
|
2995
|
-
db.close();
|
|
2996
|
-
const base = { target, mode: fileMode ? 'file' : 'symbol', results };
|
|
2997
|
-
return paginateResult(base, 'results', { limit: opts.limit, offset: opts.offset });
|
|
2998
|
-
}
|
|
2999
|
-
|
|
3000
|
-
export function where(target, customDbPath, opts = {}) {
|
|
3001
|
-
const data = whereData(target, customDbPath, opts);
|
|
3002
|
-
if (opts.ndjson) {
|
|
3003
|
-
printNdjson(data, 'results');
|
|
3004
|
-
return;
|
|
3005
|
-
}
|
|
3006
|
-
if (opts.json) {
|
|
3007
|
-
console.log(JSON.stringify(data, null, 2));
|
|
3008
|
-
return;
|
|
3009
|
-
}
|
|
2285
|
+
try {
|
|
2286
|
+
const noTests = opts.noTests || false;
|
|
2287
|
+
const fileMode = opts.file || false;
|
|
3010
2288
|
|
|
3011
|
-
|
|
3012
|
-
console.log(
|
|
3013
|
-
data.mode === 'file'
|
|
3014
|
-
? `No file matching "${target}" in graph`
|
|
3015
|
-
: `No symbol matching "${target}" in graph`,
|
|
3016
|
-
);
|
|
3017
|
-
return;
|
|
3018
|
-
}
|
|
2289
|
+
const results = fileMode ? whereFileImpl(db, target) : whereSymbolImpl(db, target, noTests);
|
|
3019
2290
|
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
console.log(`\n${kindIcon(r.kind)} ${r.name}${roleTag} ${r.file}:${r.line}${tag}`);
|
|
3025
|
-
if (r.uses.length > 0) {
|
|
3026
|
-
const useStrs = r.uses.map((u) => `${u.file}:${u.line}`);
|
|
3027
|
-
console.log(` Used in: ${useStrs.join(', ')}`);
|
|
3028
|
-
} else {
|
|
3029
|
-
console.log(' No uses found');
|
|
3030
|
-
}
|
|
3031
|
-
}
|
|
3032
|
-
} else {
|
|
3033
|
-
for (const r of data.results) {
|
|
3034
|
-
console.log(`\n# ${r.file}`);
|
|
3035
|
-
if (r.symbols.length > 0) {
|
|
3036
|
-
const symStrs = r.symbols.map((s) => `${s.name}:${s.line}`);
|
|
3037
|
-
console.log(` Symbols: ${symStrs.join(', ')}`);
|
|
3038
|
-
}
|
|
3039
|
-
if (r.imports.length > 0) {
|
|
3040
|
-
console.log(` Imports: ${r.imports.join(', ')}`);
|
|
3041
|
-
}
|
|
3042
|
-
if (r.importedBy.length > 0) {
|
|
3043
|
-
console.log(` Imported by: ${r.importedBy.join(', ')}`);
|
|
3044
|
-
}
|
|
3045
|
-
if (r.exported.length > 0) {
|
|
3046
|
-
console.log(` Exported: ${r.exported.join(', ')}`);
|
|
3047
|
-
}
|
|
3048
|
-
}
|
|
2291
|
+
const base = { target, mode: fileMode ? 'file' : 'symbol', results };
|
|
2292
|
+
return paginateResult(base, 'results', { limit: opts.limit, offset: opts.offset });
|
|
2293
|
+
} finally {
|
|
2294
|
+
db.close();
|
|
3049
2295
|
}
|
|
3050
|
-
console.log();
|
|
3051
2296
|
}
|
|
3052
2297
|
|
|
3053
2298
|
// ─── rolesData ──────────────────────────────────────────────────────────
|
|
3054
2299
|
|
|
3055
2300
|
export function rolesData(customDbPath, opts = {}) {
|
|
3056
2301
|
const db = openReadonlyOrFail(customDbPath);
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
const conditions = ['role IS NOT NULL'];
|
|
3062
|
-
const params = [];
|
|
3063
|
-
|
|
3064
|
-
if (filterRole) {
|
|
3065
|
-
conditions.push('role = ?');
|
|
3066
|
-
params.push(filterRole);
|
|
3067
|
-
}
|
|
3068
|
-
if (filterFile) {
|
|
3069
|
-
conditions.push('file LIKE ?');
|
|
3070
|
-
params.push(`%${filterFile}%`);
|
|
3071
|
-
}
|
|
3072
|
-
|
|
3073
|
-
let rows = db
|
|
3074
|
-
.prepare(
|
|
3075
|
-
`SELECT name, kind, file, line, end_line, role FROM nodes WHERE ${conditions.join(' AND ')} ORDER BY role, file, line`,
|
|
3076
|
-
)
|
|
3077
|
-
.all(...params);
|
|
3078
|
-
|
|
3079
|
-
if (noTests) rows = rows.filter((r) => !isTestFile(r.file));
|
|
3080
|
-
|
|
3081
|
-
const summary = {};
|
|
3082
|
-
for (const r of rows) {
|
|
3083
|
-
summary[r.role] = (summary[r.role] || 0) + 1;
|
|
3084
|
-
}
|
|
3085
|
-
|
|
3086
|
-
const hc = new Map();
|
|
3087
|
-
const symbols = rows.map((r) => normalizeSymbol(r, db, hc));
|
|
3088
|
-
db.close();
|
|
3089
|
-
const base = { count: symbols.length, summary, symbols };
|
|
3090
|
-
return paginateResult(base, 'symbols', { limit: opts.limit, offset: opts.offset });
|
|
3091
|
-
}
|
|
3092
|
-
|
|
3093
|
-
export function roles(customDbPath, opts = {}) {
|
|
3094
|
-
const data = rolesData(customDbPath, opts);
|
|
3095
|
-
if (opts.ndjson) {
|
|
3096
|
-
printNdjson(data, 'symbols');
|
|
3097
|
-
return;
|
|
3098
|
-
}
|
|
3099
|
-
if (opts.json) {
|
|
3100
|
-
console.log(JSON.stringify(data, null, 2));
|
|
3101
|
-
return;
|
|
3102
|
-
}
|
|
2302
|
+
try {
|
|
2303
|
+
const noTests = opts.noTests || false;
|
|
2304
|
+
const filterRole = opts.role || null;
|
|
2305
|
+
const filterFile = opts.file || null;
|
|
3103
2306
|
|
|
3104
|
-
|
|
3105
|
-
|
|
3106
|
-
return;
|
|
3107
|
-
}
|
|
2307
|
+
const conditions = ['role IS NOT NULL'];
|
|
2308
|
+
const params = [];
|
|
3108
2309
|
|
|
3109
|
-
|
|
3110
|
-
|
|
2310
|
+
if (filterRole) {
|
|
2311
|
+
conditions.push('role = ?');
|
|
2312
|
+
params.push(filterRole);
|
|
2313
|
+
}
|
|
2314
|
+
if (filterFile) {
|
|
2315
|
+
conditions.push('file LIKE ?');
|
|
2316
|
+
params.push(`%${filterFile}%`);
|
|
2317
|
+
}
|
|
3111
2318
|
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
2319
|
+
let rows = db
|
|
2320
|
+
.prepare(
|
|
2321
|
+
`SELECT name, kind, file, line, end_line, role FROM nodes WHERE ${conditions.join(' AND ')} ORDER BY role, file, line`,
|
|
2322
|
+
)
|
|
2323
|
+
.all(...params);
|
|
3116
2324
|
|
|
3117
|
-
|
|
3118
|
-
for (const s of data.symbols) {
|
|
3119
|
-
if (!byRole[s.role]) byRole[s.role] = [];
|
|
3120
|
-
byRole[s.role].push(s);
|
|
3121
|
-
}
|
|
2325
|
+
if (noTests) rows = rows.filter((r) => !isTestFile(r.file));
|
|
3122
2326
|
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
console.log(` ${kindIcon(s.kind)} ${s.name} ${s.file}:${s.line}`);
|
|
2327
|
+
const summary = {};
|
|
2328
|
+
for (const r of rows) {
|
|
2329
|
+
summary[r.role] = (summary[r.role] || 0) + 1;
|
|
3127
2330
|
}
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
2331
|
+
|
|
2332
|
+
const hc = new Map();
|
|
2333
|
+
const symbols = rows.map((r) => normalizeSymbol(r, db, hc));
|
|
2334
|
+
const base = { count: symbols.length, summary, symbols };
|
|
2335
|
+
return paginateResult(base, 'symbols', { limit: opts.limit, offset: opts.offset });
|
|
2336
|
+
} finally {
|
|
2337
|
+
db.close();
|
|
3132
2338
|
}
|
|
3133
2339
|
}
|
|
3134
2340
|
|
|
3135
2341
|
// ─── exportsData ─────────────────────────────────────────────────────
|
|
3136
2342
|
|
|
3137
|
-
function exportsFileImpl(db, target, noTests, getFileLines) {
|
|
2343
|
+
function exportsFileImpl(db, target, noTests, getFileLines, unused) {
|
|
3138
2344
|
const fileNodes = db
|
|
3139
2345
|
.prepare(`SELECT * FROM nodes WHERE file LIKE ? AND kind = 'file'`)
|
|
3140
2346
|
.all(`%${target}%`);
|
|
3141
2347
|
if (fileNodes.length === 0) return [];
|
|
3142
2348
|
|
|
2349
|
+
// Detect whether exported column exists
|
|
2350
|
+
let hasExportedCol = false;
|
|
2351
|
+
try {
|
|
2352
|
+
db.prepare('SELECT exported FROM nodes LIMIT 0').raw();
|
|
2353
|
+
hasExportedCol = true;
|
|
2354
|
+
} catch {
|
|
2355
|
+
/* old DB without exported column */
|
|
2356
|
+
}
|
|
2357
|
+
|
|
3143
2358
|
return fileNodes.map((fn) => {
|
|
3144
2359
|
const symbols = db
|
|
3145
2360
|
.prepare(`SELECT * FROM nodes WHERE file = ? AND kind != 'file' ORDER BY line`)
|
|
3146
2361
|
.all(fn.file);
|
|
3147
2362
|
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
|
|
2363
|
+
let exported;
|
|
2364
|
+
if (hasExportedCol) {
|
|
2365
|
+
// Use the exported column populated during build
|
|
2366
|
+
exported = db
|
|
3151
2367
|
.prepare(
|
|
3152
|
-
|
|
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'`,
|
|
2368
|
+
"SELECT * FROM nodes WHERE file = ? AND kind != 'file' AND exported = 1 ORDER BY line",
|
|
3156
2369
|
)
|
|
3157
|
-
.all(fn.file
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
2370
|
+
.all(fn.file);
|
|
2371
|
+
} else {
|
|
2372
|
+
// Fallback: symbols that have incoming calls from other files
|
|
2373
|
+
const exportedIds = new Set(
|
|
2374
|
+
db
|
|
2375
|
+
.prepare(
|
|
2376
|
+
`SELECT DISTINCT e.target_id FROM edges e
|
|
2377
|
+
JOIN nodes caller ON e.source_id = caller.id
|
|
2378
|
+
JOIN nodes target ON e.target_id = target.id
|
|
2379
|
+
WHERE target.file = ? AND caller.file != ? AND e.kind = 'calls'`,
|
|
2380
|
+
)
|
|
2381
|
+
.all(fn.file, fn.file)
|
|
2382
|
+
.map((r) => r.target_id),
|
|
2383
|
+
);
|
|
2384
|
+
exported = symbols.filter((s) => exportedIds.has(s.id));
|
|
2385
|
+
}
|
|
3162
2386
|
const internalCount = symbols.length - exported.length;
|
|
3163
2387
|
|
|
3164
2388
|
const results = exported.map((s) => {
|
|
@@ -3185,6 +2409,8 @@ function exportsFileImpl(db, target, noTests, getFileLines) {
|
|
|
3185
2409
|
};
|
|
3186
2410
|
});
|
|
3187
2411
|
|
|
2412
|
+
const totalUnused = results.filter((r) => r.consumerCount === 0).length;
|
|
2413
|
+
|
|
3188
2414
|
// Files that re-export this file (barrel → this file)
|
|
3189
2415
|
const reexports = db
|
|
3190
2416
|
.prepare(
|
|
@@ -3194,202 +2420,71 @@ function exportsFileImpl(db, target, noTests, getFileLines) {
|
|
|
3194
2420
|
.all(fn.id)
|
|
3195
2421
|
.map((r) => ({ file: r.file }));
|
|
3196
2422
|
|
|
2423
|
+
let filteredResults = results;
|
|
2424
|
+
if (unused) {
|
|
2425
|
+
filteredResults = results.filter((r) => r.consumerCount === 0);
|
|
2426
|
+
}
|
|
2427
|
+
|
|
3197
2428
|
return {
|
|
3198
2429
|
file: fn.file,
|
|
3199
|
-
results,
|
|
2430
|
+
results: filteredResults,
|
|
3200
2431
|
reexports,
|
|
3201
2432
|
totalExported: exported.length,
|
|
3202
2433
|
totalInternal: internalCount,
|
|
2434
|
+
totalUnused,
|
|
3203
2435
|
};
|
|
3204
2436
|
});
|
|
3205
2437
|
}
|
|
3206
2438
|
|
|
3207
2439
|
export function exportsData(file, customDbPath, opts = {}) {
|
|
3208
2440
|
const db = openReadonlyOrFail(customDbPath);
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
const dbFilePath = findDbPath(customDbPath);
|
|
3212
|
-
const repoRoot = path.resolve(path.dirname(dbFilePath), '..');
|
|
2441
|
+
try {
|
|
2442
|
+
const noTests = opts.noTests || false;
|
|
3213
2443
|
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
if (
|
|
2444
|
+
const dbFilePath = findDbPath(customDbPath);
|
|
2445
|
+
const repoRoot = path.resolve(path.dirname(dbFilePath), '..');
|
|
2446
|
+
|
|
2447
|
+
const fileCache = new Map();
|
|
2448
|
+
function getFileLines(file) {
|
|
2449
|
+
if (fileCache.has(file)) return fileCache.get(file);
|
|
2450
|
+
try {
|
|
2451
|
+
const absPath = safePath(repoRoot, file);
|
|
2452
|
+
if (!absPath) {
|
|
2453
|
+
fileCache.set(file, null);
|
|
2454
|
+
return null;
|
|
2455
|
+
}
|
|
2456
|
+
const lines = fs.readFileSync(absPath, 'utf-8').split('\n');
|
|
2457
|
+
fileCache.set(file, lines);
|
|
2458
|
+
return lines;
|
|
2459
|
+
} catch {
|
|
3220
2460
|
fileCache.set(file, null);
|
|
3221
2461
|
return null;
|
|
3222
2462
|
}
|
|
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
|
-
|
|
3295
|
-
export function fnImpact(name, customDbPath, opts = {}) {
|
|
3296
|
-
const data = fnImpactData(name, customDbPath, opts);
|
|
3297
|
-
if (opts.ndjson) {
|
|
3298
|
-
printNdjson(data, 'results');
|
|
3299
|
-
return;
|
|
3300
|
-
}
|
|
3301
|
-
if (opts.json) {
|
|
3302
|
-
console.log(JSON.stringify(data, null, 2));
|
|
3303
|
-
return;
|
|
3304
|
-
}
|
|
3305
|
-
if (data.results.length === 0) {
|
|
3306
|
-
console.log(`No function/method/class matching "${name}"`);
|
|
3307
|
-
return;
|
|
3308
|
-
}
|
|
3309
|
-
|
|
3310
|
-
for (const r of data.results) {
|
|
3311
|
-
console.log(`\nFunction impact: ${kindIcon(r.kind)} ${r.name} -- ${r.file}:${r.line}\n`);
|
|
3312
|
-
if (Object.keys(r.levels).length === 0) {
|
|
3313
|
-
console.log(` No callers found.`);
|
|
3314
|
-
} else {
|
|
3315
|
-
for (const [level, fns] of Object.entries(r.levels).sort((a, b) => a[0] - b[0])) {
|
|
3316
|
-
const l = parseInt(level, 10);
|
|
3317
|
-
console.log(` ${'--'.repeat(l)} Level ${level} (${fns.length} functions):`);
|
|
3318
|
-
for (const f of fns.slice(0, 20))
|
|
3319
|
-
console.log(` ${' '.repeat(l)}^ ${kindIcon(f.kind)} ${f.name} ${f.file}:${f.line}`);
|
|
3320
|
-
if (fns.length > 20) console.log(` ... and ${fns.length - 20} more`);
|
|
3321
|
-
}
|
|
3322
2463
|
}
|
|
3323
|
-
console.log(`\n Total: ${r.totalDependents} functions transitively depend on ${r.name}\n`);
|
|
3324
|
-
}
|
|
3325
|
-
}
|
|
3326
2464
|
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
console.log(diffImpactMermaid(customDbPath, opts));
|
|
3330
|
-
return;
|
|
3331
|
-
}
|
|
3332
|
-
const data = diffImpactData(customDbPath, opts);
|
|
3333
|
-
if (opts.ndjson) {
|
|
3334
|
-
printNdjson(data, 'affectedFunctions');
|
|
3335
|
-
return;
|
|
3336
|
-
}
|
|
3337
|
-
if (opts.json || opts.format === 'json') {
|
|
3338
|
-
console.log(JSON.stringify(data, null, 2));
|
|
3339
|
-
return;
|
|
3340
|
-
}
|
|
3341
|
-
if (data.error) {
|
|
3342
|
-
console.log(data.error);
|
|
3343
|
-
return;
|
|
3344
|
-
}
|
|
3345
|
-
if (data.changedFiles === 0) {
|
|
3346
|
-
console.log('No changes detected.');
|
|
3347
|
-
return;
|
|
3348
|
-
}
|
|
3349
|
-
if (data.affectedFunctions.length === 0) {
|
|
3350
|
-
console.log(
|
|
3351
|
-
' No function-level changes detected (changes may be in imports, types, or config).',
|
|
3352
|
-
);
|
|
3353
|
-
return;
|
|
3354
|
-
}
|
|
2465
|
+
const unused = opts.unused || false;
|
|
2466
|
+
const fileResults = exportsFileImpl(db, file, noTests, getFileLines, unused);
|
|
3355
2467
|
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
}
|
|
3362
|
-
if (data.historicallyCoupled && data.historicallyCoupled.length > 0) {
|
|
3363
|
-
console.log('\n Historically coupled (not in static graph):\n');
|
|
3364
|
-
for (const c of data.historicallyCoupled) {
|
|
3365
|
-
const pct = `${(c.jaccard * 100).toFixed(0)}%`;
|
|
3366
|
-
console.log(
|
|
3367
|
-
` ${c.file} <- coupled with ${c.coupledWith} (${pct}, ${c.commitCount} commits)`,
|
|
2468
|
+
if (fileResults.length === 0) {
|
|
2469
|
+
return paginateResult(
|
|
2470
|
+
{ file, results: [], reexports: [], totalExported: 0, totalInternal: 0, totalUnused: 0 },
|
|
2471
|
+
'results',
|
|
2472
|
+
{ limit: opts.limit, offset: opts.offset },
|
|
3368
2473
|
);
|
|
3369
2474
|
}
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
}
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
if (data.summary.historicallyCoupledCount > 0) {
|
|
3385
|
-
summaryLine += `, ${data.summary.historicallyCoupledCount} historically coupled`;
|
|
3386
|
-
}
|
|
3387
|
-
if (data.summary.ownersAffected > 0) {
|
|
3388
|
-
summaryLine += `, ${data.summary.ownersAffected} owners affected`;
|
|
3389
|
-
}
|
|
3390
|
-
if (data.summary.boundaryViolationCount > 0) {
|
|
3391
|
-
summaryLine += `, ${data.summary.boundaryViolationCount} boundary violations`;
|
|
3392
|
-
}
|
|
3393
|
-
console.log(`${summaryLine}\n`);
|
|
2475
|
+
|
|
2476
|
+
// For single-file match return flat; for multi-match return first (like explainData)
|
|
2477
|
+
const first = fileResults[0];
|
|
2478
|
+
const base = {
|
|
2479
|
+
file: first.file,
|
|
2480
|
+
results: first.results,
|
|
2481
|
+
reexports: first.reexports,
|
|
2482
|
+
totalExported: first.totalExported,
|
|
2483
|
+
totalInternal: first.totalInternal,
|
|
2484
|
+
totalUnused: first.totalUnused,
|
|
2485
|
+
};
|
|
2486
|
+
return paginateResult(base, 'results', { limit: opts.limit, offset: opts.offset });
|
|
2487
|
+
} finally {
|
|
2488
|
+
db.close();
|
|
3394
2489
|
}
|
|
3395
2490
|
}
|