@optave/codegraph 3.1.0 → 3.1.2
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 +5 -5
- package/grammars/tree-sitter-go.wasm +0 -0
- package/package.json +8 -9
- package/src/ast-analysis/engine.js +365 -0
- package/src/ast-analysis/metrics.js +118 -0
- 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-analysis/visitor-utils.js +176 -0
- package/src/ast-analysis/visitor.js +162 -0
- package/src/ast-analysis/visitors/ast-store-visitor.js +150 -0
- package/src/ast-analysis/visitors/cfg-visitor.js +792 -0
- package/src/ast-analysis/visitors/complexity-visitor.js +243 -0
- package/src/ast-analysis/visitors/dataflow-visitor.js +358 -0
- package/src/ast.js +26 -166
- package/src/audit.js +2 -88
- package/src/batch.js +0 -25
- package/src/boundaries.js +1 -1
- package/src/branch-compare.js +82 -172
- package/src/builder.js +48 -184
- package/src/cfg.js +148 -1174
- package/src/check.js +1 -84
- package/src/cli.js +118 -197
- package/src/cochange.js +1 -39
- package/src/commands/audit.js +88 -0
- package/src/commands/batch.js +26 -0
- package/src/commands/branch-compare.js +97 -0
- package/src/commands/cfg.js +55 -0
- package/src/commands/check.js +82 -0
- package/src/commands/cochange.js +37 -0
- package/src/commands/communities.js +69 -0
- package/src/commands/complexity.js +77 -0
- package/src/commands/dataflow.js +110 -0
- package/src/commands/flow.js +70 -0
- package/src/commands/manifesto.js +77 -0
- package/src/commands/owners.js +52 -0
- package/src/commands/query.js +21 -0
- package/src/commands/sequence.js +33 -0
- package/src/commands/structure.js +64 -0
- package/src/commands/triage.js +49 -0
- package/src/communities.js +22 -96
- package/src/complexity.js +234 -1591
- package/src/cycles.js +1 -1
- package/src/dataflow.js +274 -1352
- 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/build-stmts.js +104 -0
- package/src/db/repository/cfg.js +83 -0
- package/src/db/repository/cochange.js +41 -0
- package/src/db/repository/complexity.js +15 -0
- package/src/db/repository/dataflow.js +12 -0
- package/src/db/repository/edges.js +259 -0
- package/src/db/repository/embeddings.js +40 -0
- package/src/db/repository/graph-read.js +39 -0
- package/src/db/repository/index.js +42 -0
- package/src/db/repository/nodes.js +236 -0
- package/src/db.js +58 -399
- package/src/embedder.js +158 -174
- package/src/export.js +1 -1
- package/src/extractors/javascript.js +130 -5
- package/src/flow.js +153 -222
- package/src/index.js +53 -16
- package/src/infrastructure/result-formatter.js +21 -0
- package/src/infrastructure/test-filter.js +7 -0
- package/src/kinds.js +50 -0
- package/src/manifesto.js +1 -82
- package/src/mcp.js +37 -20
- package/src/owners.js +127 -182
- package/src/queries-cli.js +866 -0
- package/src/queries.js +1271 -2416
- package/src/sequence.js +179 -223
- package/src/structure.js +211 -269
- package/src/triage.js +117 -212
- package/src/viewer.js +1 -1
- package/src/watcher.js +7 -4
package/src/queries.js
CHANGED
|
@@ -5,12 +5,41 @@ 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
|
+
countCrossFileCallers,
|
|
10
|
+
findAllIncomingEdges,
|
|
11
|
+
findAllOutgoingEdges,
|
|
12
|
+
findCallees,
|
|
13
|
+
findCallers,
|
|
14
|
+
findCrossFileCallTargets,
|
|
15
|
+
findDbPath,
|
|
16
|
+
findDistinctCallers,
|
|
17
|
+
findFileNodes,
|
|
18
|
+
findImportDependents,
|
|
19
|
+
findImportSources,
|
|
20
|
+
findImportTargets,
|
|
21
|
+
findIntraFileCallEdges,
|
|
22
|
+
findNodeById,
|
|
23
|
+
findNodeChildren,
|
|
24
|
+
findNodesByFile,
|
|
25
|
+
findNodesWithFanIn,
|
|
26
|
+
getClassHierarchy,
|
|
27
|
+
getComplexityForNode,
|
|
28
|
+
iterateFunctionNodes,
|
|
29
|
+
listFunctionNodes,
|
|
30
|
+
openReadonlyOrFail,
|
|
31
|
+
testFilterSQL,
|
|
32
|
+
} from './db.js';
|
|
33
|
+
import { isTestFile } from './infrastructure/test-filter.js';
|
|
34
|
+
import { ALL_SYMBOL_KINDS } from './kinds.js';
|
|
9
35
|
import { debug } from './logger.js';
|
|
10
36
|
import { ownersForFiles } from './owners.js';
|
|
11
|
-
import { paginateResult
|
|
37
|
+
import { paginateResult } from './paginate.js';
|
|
12
38
|
import { LANGUAGE_REGISTRY } from './parser.js';
|
|
13
39
|
|
|
40
|
+
// Re-export from dedicated module for backward compat
|
|
41
|
+
export { isTestFile, TEST_PATTERN } from './infrastructure/test-filter.js';
|
|
42
|
+
|
|
14
43
|
/**
|
|
15
44
|
* Resolve a file path relative to repoRoot, rejecting traversal outside the repo.
|
|
16
45
|
* Returns null if the resolved path escapes repoRoot.
|
|
@@ -21,11 +50,6 @@ function safePath(repoRoot, file) {
|
|
|
21
50
|
return resolved;
|
|
22
51
|
}
|
|
23
52
|
|
|
24
|
-
const TEST_PATTERN = /\.(test|spec)\.|__test__|__tests__|\.stories\./;
|
|
25
|
-
export function isTestFile(filePath) {
|
|
26
|
-
return TEST_PATTERN.test(filePath);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
53
|
export const FALSE_POSITIVE_NAMES = new Set([
|
|
30
54
|
'run',
|
|
31
55
|
'get',
|
|
@@ -60,78 +84,17 @@ export const FALSE_POSITIVE_CALLER_THRESHOLD = 20;
|
|
|
60
84
|
|
|
61
85
|
const FUNCTION_KINDS = ['function', 'method', 'class'];
|
|
62
86
|
|
|
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'];
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* Get all ancestor class names for a given class using extends edges.
|
|
114
|
-
*/
|
|
115
|
-
function getClassHierarchy(db, classNodeId) {
|
|
116
|
-
const ancestors = new Set();
|
|
117
|
-
const queue = [classNodeId];
|
|
118
|
-
while (queue.length > 0) {
|
|
119
|
-
const current = queue.shift();
|
|
120
|
-
const parents = db
|
|
121
|
-
.prepare(`
|
|
122
|
-
SELECT n.id, n.name FROM edges e JOIN nodes n ON e.target_id = n.id
|
|
123
|
-
WHERE e.source_id = ? AND e.kind = 'extends'
|
|
124
|
-
`)
|
|
125
|
-
.all(current);
|
|
126
|
-
for (const p of parents) {
|
|
127
|
-
if (!ancestors.has(p.id)) {
|
|
128
|
-
ancestors.add(p.id);
|
|
129
|
-
queue.push(p.id);
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
return ancestors;
|
|
134
|
-
}
|
|
87
|
+
// Re-export kind/edge constants from kinds.js (canonical source)
|
|
88
|
+
export {
|
|
89
|
+
ALL_SYMBOL_KINDS,
|
|
90
|
+
CORE_EDGE_KINDS,
|
|
91
|
+
CORE_SYMBOL_KINDS,
|
|
92
|
+
EVERY_EDGE_KIND,
|
|
93
|
+
EVERY_SYMBOL_KIND,
|
|
94
|
+
EXTENDED_SYMBOL_KINDS,
|
|
95
|
+
STRUCTURAL_EDGE_KINDS,
|
|
96
|
+
VALID_ROLES,
|
|
97
|
+
} from './kinds.js';
|
|
135
98
|
|
|
136
99
|
function resolveMethodViaHierarchy(db, methodName) {
|
|
137
100
|
const methods = db
|
|
@@ -164,26 +127,9 @@ function resolveMethodViaHierarchy(db, methodName) {
|
|
|
164
127
|
* Scoring: exact=100, prefix=60, word-boundary=40, substring=10, plus fan-in tiebreaker.
|
|
165
128
|
*/
|
|
166
129
|
export 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
|
-
}
|
|
130
|
+
const kinds = opts.kind ? [opts.kind] : opts.kinds?.length ? opts.kinds : FUNCTION_KINDS;
|
|
176
131
|
|
|
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);
|
|
132
|
+
const rows = findNodesWithFanIn(db, `%${name}%`, { kinds, file: opts.file });
|
|
187
133
|
|
|
188
134
|
const nodes = opts.noTests ? rows.filter((n) => !isTestFile(n.file)) : rows;
|
|
189
135
|
|
|
@@ -240,859 +186,744 @@ export function kindIcon(kind) {
|
|
|
240
186
|
|
|
241
187
|
export function queryNameData(name, customDbPath, opts = {}) {
|
|
242
188
|
const db = openReadonlyOrFail(customDbPath);
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
189
|
+
try {
|
|
190
|
+
const noTests = opts.noTests || false;
|
|
191
|
+
let nodes = db.prepare(`SELECT * FROM nodes WHERE name LIKE ?`).all(`%${name}%`);
|
|
192
|
+
if (noTests) nodes = nodes.filter((n) => !isTestFile(n.file));
|
|
193
|
+
if (nodes.length === 0) {
|
|
194
|
+
return { query: name, results: [] };
|
|
195
|
+
}
|
|
250
196
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
.prepare(`
|
|
255
|
-
SELECT n.name, n.kind, n.file, n.line, e.kind as edge_kind
|
|
256
|
-
FROM edges e JOIN nodes n ON e.target_id = n.id
|
|
257
|
-
WHERE e.source_id = ?
|
|
258
|
-
`)
|
|
259
|
-
.all(node.id);
|
|
197
|
+
const hc = new Map();
|
|
198
|
+
const results = nodes.map((node) => {
|
|
199
|
+
let callees = findAllOutgoingEdges(db, node.id);
|
|
260
200
|
|
|
261
|
-
|
|
262
|
-
.prepare(`
|
|
263
|
-
SELECT n.name, n.kind, n.file, n.line, e.kind as edge_kind
|
|
264
|
-
FROM edges e JOIN nodes n ON e.source_id = n.id
|
|
265
|
-
WHERE e.target_id = ?
|
|
266
|
-
`)
|
|
267
|
-
.all(node.id);
|
|
201
|
+
let callers = findAllIncomingEdges(db, node.id);
|
|
268
202
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
203
|
+
if (noTests) {
|
|
204
|
+
callees = callees.filter((c) => !isTestFile(c.file));
|
|
205
|
+
callers = callers.filter((c) => !isTestFile(c.file));
|
|
206
|
+
}
|
|
273
207
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
208
|
+
return {
|
|
209
|
+
...normalizeSymbol(node, db, hc),
|
|
210
|
+
callees: callees.map((c) => ({
|
|
211
|
+
name: c.name,
|
|
212
|
+
kind: c.kind,
|
|
213
|
+
file: c.file,
|
|
214
|
+
line: c.line,
|
|
215
|
+
edgeKind: c.edge_kind,
|
|
216
|
+
})),
|
|
217
|
+
callers: callers.map((c) => ({
|
|
218
|
+
name: c.name,
|
|
219
|
+
kind: c.kind,
|
|
220
|
+
file: c.file,
|
|
221
|
+
line: c.line,
|
|
222
|
+
edgeKind: c.edge_kind,
|
|
223
|
+
})),
|
|
224
|
+
};
|
|
225
|
+
});
|
|
292
226
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
227
|
+
const base = { query: name, results };
|
|
228
|
+
return paginateResult(base, 'results', { limit: opts.limit, offset: opts.offset });
|
|
229
|
+
} finally {
|
|
230
|
+
db.close();
|
|
231
|
+
}
|
|
296
232
|
}
|
|
297
233
|
|
|
298
234
|
export function impactAnalysisData(file, customDbPath, opts = {}) {
|
|
299
235
|
const db = openReadonlyOrFail(customDbPath);
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
.
|
|
304
|
-
|
|
236
|
+
try {
|
|
237
|
+
const noTests = opts.noTests || false;
|
|
238
|
+
const fileNodes = findFileNodes(db, `%${file}%`);
|
|
239
|
+
if (fileNodes.length === 0) {
|
|
240
|
+
return { file, sources: [], levels: {}, totalDependents: 0 };
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const visited = new Set();
|
|
244
|
+
const queue = [];
|
|
245
|
+
const levels = new Map();
|
|
246
|
+
|
|
247
|
+
for (const fn of fileNodes) {
|
|
248
|
+
visited.add(fn.id);
|
|
249
|
+
queue.push(fn.id);
|
|
250
|
+
levels.set(fn.id, 0);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
while (queue.length > 0) {
|
|
254
|
+
const current = queue.shift();
|
|
255
|
+
const level = levels.get(current);
|
|
256
|
+
const dependents = findImportDependents(db, current);
|
|
257
|
+
for (const dep of dependents) {
|
|
258
|
+
if (!visited.has(dep.id) && (!noTests || !isTestFile(dep.file))) {
|
|
259
|
+
visited.add(dep.id);
|
|
260
|
+
queue.push(dep.id);
|
|
261
|
+
levels.set(dep.id, level + 1);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const byLevel = {};
|
|
267
|
+
for (const [id, level] of levels) {
|
|
268
|
+
if (level === 0) continue;
|
|
269
|
+
if (!byLevel[level]) byLevel[level] = [];
|
|
270
|
+
const node = findNodeById(db, id);
|
|
271
|
+
if (node) byLevel[level].push({ file: node.file });
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return {
|
|
275
|
+
file,
|
|
276
|
+
sources: fileNodes.map((f) => f.file),
|
|
277
|
+
levels: byLevel,
|
|
278
|
+
totalDependents: visited.size - fileNodes.length,
|
|
279
|
+
};
|
|
280
|
+
} finally {
|
|
305
281
|
db.close();
|
|
306
|
-
return { file, sources: [], levels: {}, totalDependents: 0 };
|
|
307
282
|
}
|
|
283
|
+
}
|
|
308
284
|
|
|
309
|
-
|
|
310
|
-
const
|
|
311
|
-
|
|
285
|
+
export function moduleMapData(customDbPath, limit = 20, opts = {}) {
|
|
286
|
+
const db = openReadonlyOrFail(customDbPath);
|
|
287
|
+
try {
|
|
288
|
+
const noTests = opts.noTests || false;
|
|
312
289
|
|
|
313
|
-
|
|
314
|
-
visited.add(fn.id);
|
|
315
|
-
queue.push(fn.id);
|
|
316
|
-
levels.set(fn.id, 0);
|
|
317
|
-
}
|
|
290
|
+
const testFilter = testFilterSQL('n.file', noTests);
|
|
318
291
|
|
|
319
|
-
|
|
320
|
-
const current = queue.shift();
|
|
321
|
-
const level = levels.get(current);
|
|
322
|
-
const dependents = db
|
|
292
|
+
const nodes = db
|
|
323
293
|
.prepare(`
|
|
324
|
-
SELECT n
|
|
325
|
-
|
|
294
|
+
SELECT n.*,
|
|
295
|
+
(SELECT COUNT(*) FROM edges WHERE source_id = n.id AND kind NOT IN ('contains', 'parameter_of', 'receiver')) as out_edges,
|
|
296
|
+
(SELECT COUNT(*) FROM edges WHERE target_id = n.id AND kind NOT IN ('contains', 'parameter_of', 'receiver')) as in_edges
|
|
297
|
+
FROM nodes n
|
|
298
|
+
WHERE n.kind = 'file'
|
|
299
|
+
${testFilter}
|
|
300
|
+
ORDER BY (SELECT COUNT(*) FROM edges WHERE target_id = n.id AND kind NOT IN ('contains', 'parameter_of', 'receiver')) DESC
|
|
301
|
+
LIMIT ?
|
|
326
302
|
`)
|
|
327
|
-
.all(
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
const byLevel = {};
|
|
338
|
-
for (const [id, level] of levels) {
|
|
339
|
-
if (level === 0) continue;
|
|
340
|
-
if (!byLevel[level]) byLevel[level] = [];
|
|
341
|
-
const node = db.prepare('SELECT * FROM nodes WHERE id = ?').get(id);
|
|
342
|
-
if (node) byLevel[level].push({ file: node.file });
|
|
343
|
-
}
|
|
303
|
+
.all(limit);
|
|
304
|
+
|
|
305
|
+
const topNodes = nodes.map((n) => ({
|
|
306
|
+
file: n.file,
|
|
307
|
+
dir: path.dirname(n.file) || '.',
|
|
308
|
+
inEdges: n.in_edges,
|
|
309
|
+
outEdges: n.out_edges,
|
|
310
|
+
coupling: n.in_edges + n.out_edges,
|
|
311
|
+
}));
|
|
344
312
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
file
|
|
348
|
-
sources: fileNodes.map((f) => f.file),
|
|
349
|
-
levels: byLevel,
|
|
350
|
-
totalDependents: visited.size - fileNodes.length,
|
|
351
|
-
};
|
|
352
|
-
}
|
|
313
|
+
const totalNodes = db.prepare('SELECT COUNT(*) as c FROM nodes').get().c;
|
|
314
|
+
const totalEdges = db.prepare('SELECT COUNT(*) as c FROM edges').get().c;
|
|
315
|
+
const totalFiles = db.prepare("SELECT COUNT(*) as c FROM nodes WHERE kind = 'file'").get().c;
|
|
353
316
|
|
|
354
|
-
|
|
355
|
-
|
|
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 } };
|
|
317
|
+
return { limit, topNodes, stats: { totalFiles, totalNodes, totalEdges } };
|
|
318
|
+
} finally {
|
|
319
|
+
db.close();
|
|
320
|
+
}
|
|
393
321
|
}
|
|
394
322
|
|
|
395
323
|
export function fileDepsData(file, customDbPath, opts = {}) {
|
|
396
324
|
const db = openReadonlyOrFail(customDbPath);
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
.
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
return { file, results: [] };
|
|
404
|
-
}
|
|
325
|
+
try {
|
|
326
|
+
const noTests = opts.noTests || false;
|
|
327
|
+
const fileNodes = findFileNodes(db, `%${file}%`);
|
|
328
|
+
if (fileNodes.length === 0) {
|
|
329
|
+
return { file, results: [] };
|
|
330
|
+
}
|
|
405
331
|
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
.
|
|
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));
|
|
332
|
+
const results = fileNodes.map((fn) => {
|
|
333
|
+
let importsTo = findImportTargets(db, fn.id);
|
|
334
|
+
if (noTests) importsTo = importsTo.filter((i) => !isTestFile(i.file));
|
|
414
335
|
|
|
415
|
-
|
|
416
|
-
.
|
|
417
|
-
SELECT n.file, e.kind as edge_kind FROM edges e JOIN nodes n ON e.source_id = n.id
|
|
418
|
-
WHERE e.target_id = ? AND e.kind IN ('imports', 'imports-type')
|
|
419
|
-
`)
|
|
420
|
-
.all(fn.id);
|
|
421
|
-
if (noTests) importedBy = importedBy.filter((i) => !isTestFile(i.file));
|
|
336
|
+
let importedBy = findImportSources(db, fn.id);
|
|
337
|
+
if (noTests) importedBy = importedBy.filter((i) => !isTestFile(i.file));
|
|
422
338
|
|
|
423
|
-
|
|
424
|
-
.prepare(`SELECT * FROM nodes WHERE file = ? AND kind != 'file' ORDER BY line`)
|
|
425
|
-
.all(fn.file);
|
|
339
|
+
const defs = findNodesByFile(db, fn.file);
|
|
426
340
|
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
341
|
+
return {
|
|
342
|
+
file: fn.file,
|
|
343
|
+
imports: importsTo.map((i) => ({ file: i.file, typeOnly: i.edge_kind === 'imports-type' })),
|
|
344
|
+
importedBy: importedBy.map((i) => ({ file: i.file })),
|
|
345
|
+
definitions: defs.map((d) => ({ name: d.name, kind: d.kind, line: d.line })),
|
|
346
|
+
};
|
|
347
|
+
});
|
|
434
348
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
349
|
+
const base = { file, results };
|
|
350
|
+
return paginateResult(base, 'results', { limit: opts.limit, offset: opts.offset });
|
|
351
|
+
} finally {
|
|
352
|
+
db.close();
|
|
353
|
+
}
|
|
438
354
|
}
|
|
439
355
|
|
|
440
356
|
export function fnDepsData(name, customDbPath, opts = {}) {
|
|
441
357
|
const db = openReadonlyOrFail(customDbPath);
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
358
|
+
try {
|
|
359
|
+
const depth = opts.depth || 3;
|
|
360
|
+
const noTests = opts.noTests || false;
|
|
361
|
+
const hc = new Map();
|
|
445
362
|
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
}
|
|
363
|
+
const nodes = findMatchingNodes(db, name, { noTests, file: opts.file, kind: opts.kind });
|
|
364
|
+
if (nodes.length === 0) {
|
|
365
|
+
return { name, results: [] };
|
|
366
|
+
}
|
|
451
367
|
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
.
|
|
455
|
-
SELECT n.name, n.kind, n.file, n.line, e.kind as edge_kind
|
|
456
|
-
FROM edges e JOIN nodes n ON e.target_id = n.id
|
|
457
|
-
WHERE e.source_id = ? AND e.kind = 'calls'
|
|
458
|
-
`)
|
|
459
|
-
.all(node.id);
|
|
460
|
-
const filteredCallees = noTests ? callees.filter((c) => !isTestFile(c.file)) : callees;
|
|
368
|
+
const results = nodes.map((node) => {
|
|
369
|
+
const callees = findCallees(db, node.id);
|
|
370
|
+
const filteredCallees = noTests ? callees.filter((c) => !isTestFile(c.file)) : callees;
|
|
461
371
|
|
|
462
|
-
|
|
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);
|
|
469
|
-
|
|
470
|
-
if (node.kind === 'method' && node.name.includes('.')) {
|
|
471
|
-
const methodName = node.name.split('.').pop();
|
|
472
|
-
const relatedMethods = resolveMethodViaHierarchy(db, methodName);
|
|
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
|
-
}
|
|
484
|
-
}
|
|
485
|
-
if (noTests) callers = callers.filter((c) => !isTestFile(c.file));
|
|
372
|
+
let callers = findCallers(db, node.id);
|
|
486
373
|
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
const
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
const upstream = db
|
|
506
|
-
.prepare(`
|
|
507
|
-
SELECT n.name, n.kind, n.file, n.line
|
|
508
|
-
FROM edges e JOIN nodes n ON e.source_id = n.id
|
|
509
|
-
WHERE e.target_id = ? AND e.kind = 'calls'
|
|
510
|
-
`)
|
|
511
|
-
.all(f.id);
|
|
512
|
-
for (const u of upstream) {
|
|
513
|
-
if (noTests && isTestFile(u.file)) continue;
|
|
514
|
-
const uid = db
|
|
374
|
+
if (node.kind === 'method' && node.name.includes('.')) {
|
|
375
|
+
const methodName = node.name.split('.').pop();
|
|
376
|
+
const relatedMethods = resolveMethodViaHierarchy(db, methodName);
|
|
377
|
+
for (const rm of relatedMethods) {
|
|
378
|
+
if (rm.id === node.id) continue;
|
|
379
|
+
const extraCallers = findCallers(db, rm.id);
|
|
380
|
+
callers.push(...extraCallers.map((c) => ({ ...c, viaHierarchy: rm.name })));
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
if (noTests) callers = callers.filter((c) => !isTestFile(c.file));
|
|
384
|
+
|
|
385
|
+
// Transitive callers
|
|
386
|
+
const transitiveCallers = {};
|
|
387
|
+
if (depth > 1) {
|
|
388
|
+
const visited = new Set([node.id]);
|
|
389
|
+
let frontier = callers
|
|
390
|
+
.map((c) => {
|
|
391
|
+
const row = db
|
|
515
392
|
.prepare('SELECT id FROM nodes WHERE name = ? AND kind = ? AND file = ? AND line = ?')
|
|
516
|
-
.get(
|
|
517
|
-
|
|
518
|
-
|
|
393
|
+
.get(c.name, c.kind, c.file, c.line);
|
|
394
|
+
return row ? { ...c, id: row.id } : null;
|
|
395
|
+
})
|
|
396
|
+
.filter(Boolean);
|
|
397
|
+
|
|
398
|
+
for (let d = 2; d <= depth; d++) {
|
|
399
|
+
const nextFrontier = [];
|
|
400
|
+
for (const f of frontier) {
|
|
401
|
+
if (visited.has(f.id)) continue;
|
|
402
|
+
visited.add(f.id);
|
|
403
|
+
const upstream = db
|
|
404
|
+
.prepare(`
|
|
405
|
+
SELECT n.name, n.kind, n.file, n.line
|
|
406
|
+
FROM edges e JOIN nodes n ON e.source_id = n.id
|
|
407
|
+
WHERE e.target_id = ? AND e.kind = 'calls'
|
|
408
|
+
`)
|
|
409
|
+
.all(f.id);
|
|
410
|
+
for (const u of upstream) {
|
|
411
|
+
if (noTests && isTestFile(u.file)) continue;
|
|
412
|
+
const uid = db
|
|
413
|
+
.prepare(
|
|
414
|
+
'SELECT id FROM nodes WHERE name = ? AND kind = ? AND file = ? AND line = ?',
|
|
415
|
+
)
|
|
416
|
+
.get(u.name, u.kind, u.file, u.line)?.id;
|
|
417
|
+
if (uid && !visited.has(uid)) {
|
|
418
|
+
nextFrontier.push({ ...u, id: uid });
|
|
419
|
+
}
|
|
519
420
|
}
|
|
520
421
|
}
|
|
422
|
+
if (nextFrontier.length > 0) {
|
|
423
|
+
transitiveCallers[d] = nextFrontier.map((n) => ({
|
|
424
|
+
name: n.name,
|
|
425
|
+
kind: n.kind,
|
|
426
|
+
file: n.file,
|
|
427
|
+
line: n.line,
|
|
428
|
+
}));
|
|
429
|
+
}
|
|
430
|
+
frontier = nextFrontier;
|
|
431
|
+
if (frontier.length === 0) break;
|
|
521
432
|
}
|
|
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
433
|
}
|
|
533
|
-
}
|
|
534
434
|
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
435
|
+
return {
|
|
436
|
+
...normalizeSymbol(node, db, hc),
|
|
437
|
+
callees: filteredCallees.map((c) => ({
|
|
438
|
+
name: c.name,
|
|
439
|
+
kind: c.kind,
|
|
440
|
+
file: c.file,
|
|
441
|
+
line: c.line,
|
|
442
|
+
})),
|
|
443
|
+
callers: callers.map((c) => ({
|
|
444
|
+
name: c.name,
|
|
445
|
+
kind: c.kind,
|
|
446
|
+
file: c.file,
|
|
447
|
+
line: c.line,
|
|
448
|
+
viaHierarchy: c.viaHierarchy || undefined,
|
|
449
|
+
})),
|
|
450
|
+
transitiveCallers,
|
|
451
|
+
};
|
|
452
|
+
});
|
|
553
453
|
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
454
|
+
const base = { name, results };
|
|
455
|
+
return paginateResult(base, 'results', { limit: opts.limit, offset: opts.offset });
|
|
456
|
+
} finally {
|
|
457
|
+
db.close();
|
|
458
|
+
}
|
|
557
459
|
}
|
|
558
460
|
|
|
559
461
|
export function fnImpactData(name, customDbPath, opts = {}) {
|
|
560
462
|
const db = openReadonlyOrFail(customDbPath);
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
463
|
+
try {
|
|
464
|
+
const maxDepth = opts.depth || 5;
|
|
465
|
+
const noTests = opts.noTests || false;
|
|
466
|
+
const hc = new Map();
|
|
564
467
|
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
468
|
+
const nodes = findMatchingNodes(db, name, { noTests, file: opts.file, kind: opts.kind });
|
|
469
|
+
if (nodes.length === 0) {
|
|
470
|
+
return { name, results: [] };
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
const results = nodes.map((node) => {
|
|
474
|
+
const visited = new Set([node.id]);
|
|
475
|
+
const levels = {};
|
|
476
|
+
let frontier = [node.id];
|
|
570
477
|
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
FROM edges e JOIN nodes n ON e.source_id = n.id
|
|
583
|
-
WHERE e.target_id = ? AND e.kind = 'calls'
|
|
584
|
-
`)
|
|
585
|
-
.all(fid);
|
|
586
|
-
for (const c of callers) {
|
|
587
|
-
if (!visited.has(c.id) && (!noTests || !isTestFile(c.file))) {
|
|
588
|
-
visited.add(c.id);
|
|
589
|
-
nextFrontier.push(c.id);
|
|
590
|
-
if (!levels[d]) levels[d] = [];
|
|
591
|
-
levels[d].push({ name: c.name, kind: c.kind, file: c.file, line: c.line });
|
|
478
|
+
for (let d = 1; d <= maxDepth; d++) {
|
|
479
|
+
const nextFrontier = [];
|
|
480
|
+
for (const fid of frontier) {
|
|
481
|
+
const callers = findDistinctCallers(db, fid);
|
|
482
|
+
for (const c of callers) {
|
|
483
|
+
if (!visited.has(c.id) && (!noTests || !isTestFile(c.file))) {
|
|
484
|
+
visited.add(c.id);
|
|
485
|
+
nextFrontier.push(c.id);
|
|
486
|
+
if (!levels[d]) levels[d] = [];
|
|
487
|
+
levels[d].push({ name: c.name, kind: c.kind, file: c.file, line: c.line });
|
|
488
|
+
}
|
|
592
489
|
}
|
|
593
490
|
}
|
|
491
|
+
frontier = nextFrontier;
|
|
492
|
+
if (frontier.length === 0) break;
|
|
594
493
|
}
|
|
595
|
-
frontier = nextFrontier;
|
|
596
|
-
if (frontier.length === 0) break;
|
|
597
|
-
}
|
|
598
494
|
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
495
|
+
return {
|
|
496
|
+
...normalizeSymbol(node, db, hc),
|
|
497
|
+
levels,
|
|
498
|
+
totalDependents: visited.size - 1,
|
|
499
|
+
};
|
|
500
|
+
});
|
|
605
501
|
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
502
|
+
const base = { name, results };
|
|
503
|
+
return paginateResult(base, 'results', { limit: opts.limit, offset: opts.offset });
|
|
504
|
+
} finally {
|
|
505
|
+
db.close();
|
|
506
|
+
}
|
|
609
507
|
}
|
|
610
508
|
|
|
611
509
|
export function pathData(from, to, customDbPath, opts = {}) {
|
|
612
510
|
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
|
-
}
|
|
511
|
+
try {
|
|
512
|
+
const noTests = opts.noTests || false;
|
|
513
|
+
const maxDepth = opts.maxDepth || 10;
|
|
514
|
+
const edgeKinds = opts.edgeKinds || ['calls'];
|
|
515
|
+
const reverse = opts.reverse || false;
|
|
634
516
|
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
517
|
+
const fromNodes = findMatchingNodes(db, from, {
|
|
518
|
+
noTests,
|
|
519
|
+
file: opts.fromFile,
|
|
520
|
+
kind: opts.kind,
|
|
521
|
+
});
|
|
522
|
+
if (fromNodes.length === 0) {
|
|
523
|
+
return {
|
|
524
|
+
from,
|
|
525
|
+
to,
|
|
526
|
+
found: false,
|
|
527
|
+
error: `No symbol matching "${from}"`,
|
|
528
|
+
fromCandidates: [],
|
|
529
|
+
toCandidates: [],
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
const toNodes = findMatchingNodes(db, to, {
|
|
534
|
+
noTests,
|
|
535
|
+
file: opts.toFile,
|
|
536
|
+
kind: opts.kind,
|
|
537
|
+
});
|
|
538
|
+
if (toNodes.length === 0) {
|
|
539
|
+
return {
|
|
540
|
+
from,
|
|
541
|
+
to,
|
|
542
|
+
found: false,
|
|
543
|
+
error: `No symbol matching "${to}"`,
|
|
544
|
+
fromCandidates: fromNodes
|
|
545
|
+
.slice(0, 5)
|
|
546
|
+
.map((n) => ({ name: n.name, kind: n.kind, file: n.file, line: n.line })),
|
|
547
|
+
toCandidates: [],
|
|
548
|
+
};
|
|
549
|
+
}
|
|
653
550
|
|
|
654
|
-
|
|
655
|
-
|
|
551
|
+
const sourceNode = fromNodes[0];
|
|
552
|
+
const targetNode = toNodes[0];
|
|
656
553
|
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
554
|
+
const fromCandidates = fromNodes
|
|
555
|
+
.slice(0, 5)
|
|
556
|
+
.map((n) => ({ name: n.name, kind: n.kind, file: n.file, line: n.line }));
|
|
557
|
+
const toCandidates = toNodes
|
|
558
|
+
.slice(0, 5)
|
|
559
|
+
.map((n) => ({ name: n.name, kind: n.kind, file: n.file, line: n.line }));
|
|
663
560
|
|
|
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
|
-
}
|
|
561
|
+
// Self-path
|
|
562
|
+
if (sourceNode.id === targetNode.id) {
|
|
563
|
+
return {
|
|
564
|
+
from,
|
|
565
|
+
to,
|
|
566
|
+
fromCandidates,
|
|
567
|
+
toCandidates,
|
|
568
|
+
found: true,
|
|
569
|
+
hops: 0,
|
|
570
|
+
path: [
|
|
571
|
+
{
|
|
572
|
+
name: sourceNode.name,
|
|
573
|
+
kind: sourceNode.kind,
|
|
574
|
+
file: sourceNode.file,
|
|
575
|
+
line: sourceNode.line,
|
|
576
|
+
edgeKind: null,
|
|
577
|
+
},
|
|
578
|
+
],
|
|
579
|
+
alternateCount: 0,
|
|
580
|
+
edgeKinds,
|
|
581
|
+
reverse,
|
|
582
|
+
maxDepth,
|
|
583
|
+
};
|
|
584
|
+
}
|
|
689
585
|
|
|
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
|
-
|
|
586
|
+
// Build edge kind filter
|
|
587
|
+
const kindPlaceholders = edgeKinds.map(() => '?').join(', ');
|
|
588
|
+
|
|
589
|
+
// BFS — direction depends on `reverse` flag
|
|
590
|
+
// Forward: source_id → target_id (A calls... calls B)
|
|
591
|
+
// Reverse: target_id → source_id (B is called by... called by A)
|
|
592
|
+
const neighborQuery = reverse
|
|
593
|
+
? `SELECT n.id, n.name, n.kind, n.file, n.line, e.kind AS edge_kind
|
|
594
|
+
FROM edges e JOIN nodes n ON e.source_id = n.id
|
|
595
|
+
WHERE e.target_id = ? AND e.kind IN (${kindPlaceholders})`
|
|
596
|
+
: `SELECT n.id, n.name, n.kind, n.file, n.line, e.kind AS edge_kind
|
|
597
|
+
FROM edges e JOIN nodes n ON e.target_id = n.id
|
|
598
|
+
WHERE e.source_id = ? AND e.kind IN (${kindPlaceholders})`;
|
|
599
|
+
const neighborStmt = db.prepare(neighborQuery);
|
|
600
|
+
|
|
601
|
+
const visited = new Set([sourceNode.id]);
|
|
602
|
+
// parent map: nodeId → { parentId, edgeKind }
|
|
603
|
+
const parent = new Map();
|
|
604
|
+
let queue = [sourceNode.id];
|
|
605
|
+
let found = false;
|
|
606
|
+
let alternateCount = 0;
|
|
607
|
+
let foundDepth = -1;
|
|
608
|
+
|
|
609
|
+
for (let depth = 1; depth <= maxDepth; depth++) {
|
|
610
|
+
const nextQueue = [];
|
|
611
|
+
for (const currentId of queue) {
|
|
612
|
+
const neighbors = neighborStmt.all(currentId, ...edgeKinds);
|
|
613
|
+
for (const n of neighbors) {
|
|
614
|
+
if (noTests && isTestFile(n.file)) continue;
|
|
615
|
+
if (n.id === targetNode.id) {
|
|
616
|
+
if (!found) {
|
|
617
|
+
found = true;
|
|
618
|
+
foundDepth = depth;
|
|
619
|
+
parent.set(n.id, { parentId: currentId, edgeKind: n.edge_kind });
|
|
620
|
+
}
|
|
621
|
+
alternateCount++;
|
|
622
|
+
continue;
|
|
623
|
+
}
|
|
624
|
+
if (!visited.has(n.id)) {
|
|
625
|
+
visited.add(n.id);
|
|
723
626
|
parent.set(n.id, { parentId: currentId, edgeKind: n.edge_kind });
|
|
627
|
+
nextQueue.push(n.id);
|
|
724
628
|
}
|
|
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
629
|
}
|
|
733
630
|
}
|
|
631
|
+
if (found) break;
|
|
632
|
+
queue = nextQueue;
|
|
633
|
+
if (queue.length === 0) break;
|
|
734
634
|
}
|
|
735
|
-
if (found) break;
|
|
736
|
-
queue = nextQueue;
|
|
737
|
-
if (queue.length === 0) break;
|
|
738
|
-
}
|
|
739
635
|
|
|
740
|
-
|
|
741
|
-
|
|
636
|
+
if (!found) {
|
|
637
|
+
return {
|
|
638
|
+
from,
|
|
639
|
+
to,
|
|
640
|
+
fromCandidates,
|
|
641
|
+
toCandidates,
|
|
642
|
+
found: false,
|
|
643
|
+
hops: null,
|
|
644
|
+
path: [],
|
|
645
|
+
alternateCount: 0,
|
|
646
|
+
edgeKinds,
|
|
647
|
+
reverse,
|
|
648
|
+
maxDepth,
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// alternateCount includes the one we kept; subtract 1 for "alternates"
|
|
653
|
+
alternateCount = Math.max(0, alternateCount - 1);
|
|
654
|
+
|
|
655
|
+
// Reconstruct path from target back to source
|
|
656
|
+
const pathIds = [targetNode.id];
|
|
657
|
+
let cur = targetNode.id;
|
|
658
|
+
while (cur !== sourceNode.id) {
|
|
659
|
+
const p = parent.get(cur);
|
|
660
|
+
pathIds.push(p.parentId);
|
|
661
|
+
cur = p.parentId;
|
|
662
|
+
}
|
|
663
|
+
pathIds.reverse();
|
|
664
|
+
|
|
665
|
+
// Build path with node info
|
|
666
|
+
const nodeCache = new Map();
|
|
667
|
+
const getNode = (id) => {
|
|
668
|
+
if (nodeCache.has(id)) return nodeCache.get(id);
|
|
669
|
+
const row = db.prepare('SELECT name, kind, file, line FROM nodes WHERE id = ?').get(id);
|
|
670
|
+
nodeCache.set(id, row);
|
|
671
|
+
return row;
|
|
672
|
+
};
|
|
673
|
+
|
|
674
|
+
const resultPath = pathIds.map((id, idx) => {
|
|
675
|
+
const node = getNode(id);
|
|
676
|
+
const edgeKind = idx === 0 ? null : parent.get(id).edgeKind;
|
|
677
|
+
return { name: node.name, kind: node.kind, file: node.file, line: node.line, edgeKind };
|
|
678
|
+
});
|
|
679
|
+
|
|
742
680
|
return {
|
|
743
681
|
from,
|
|
744
682
|
to,
|
|
745
683
|
fromCandidates,
|
|
746
684
|
toCandidates,
|
|
747
|
-
found:
|
|
748
|
-
hops:
|
|
749
|
-
path:
|
|
750
|
-
alternateCount
|
|
685
|
+
found: true,
|
|
686
|
+
hops: foundDepth,
|
|
687
|
+
path: resultPath,
|
|
688
|
+
alternateCount,
|
|
751
689
|
edgeKinds,
|
|
752
690
|
reverse,
|
|
753
691
|
maxDepth,
|
|
754
692
|
};
|
|
693
|
+
} finally {
|
|
694
|
+
db.close();
|
|
755
695
|
}
|
|
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
696
|
}
|
|
800
697
|
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
698
|
+
/**
|
|
699
|
+
* Fix #2: Shell injection vulnerability.
|
|
700
|
+
* Uses execFileSync instead of execSync to prevent shell interpretation of user input.
|
|
701
|
+
*/
|
|
702
|
+
export function diffImpactData(customDbPath, opts = {}) {
|
|
703
|
+
const db = openReadonlyOrFail(customDbPath);
|
|
704
|
+
try {
|
|
705
|
+
const noTests = opts.noTests || false;
|
|
706
|
+
const maxDepth = opts.depth || 3;
|
|
807
707
|
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
return;
|
|
811
|
-
}
|
|
708
|
+
const dbPath = findDbPath(customDbPath);
|
|
709
|
+
const repoRoot = path.resolve(path.dirname(dbPath), '..');
|
|
812
710
|
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
711
|
+
// Verify we're in a git repository before running git diff
|
|
712
|
+
let checkDir = repoRoot;
|
|
713
|
+
let isGitRepo = false;
|
|
714
|
+
while (checkDir) {
|
|
715
|
+
if (fs.existsSync(path.join(checkDir, '.git'))) {
|
|
716
|
+
isGitRepo = true;
|
|
717
|
+
break;
|
|
718
|
+
}
|
|
719
|
+
const parent = path.dirname(checkDir);
|
|
720
|
+
if (parent === checkDir) break;
|
|
721
|
+
checkDir = parent;
|
|
820
722
|
}
|
|
821
|
-
if (
|
|
822
|
-
|
|
823
|
-
` "${to}" matched ${data.toCandidates.length} symbols — using top match: ${data.toCandidates[0].name} (${data.toCandidates[0].file}:${data.toCandidates[0].line})`,
|
|
824
|
-
);
|
|
723
|
+
if (!isGitRepo) {
|
|
724
|
+
return { error: `Not a git repository: ${repoRoot}` };
|
|
825
725
|
}
|
|
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
726
|
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
727
|
+
let diffOutput;
|
|
728
|
+
try {
|
|
729
|
+
const args = opts.staged
|
|
730
|
+
? ['diff', '--cached', '--unified=0', '--no-color']
|
|
731
|
+
: ['diff', opts.ref || 'HEAD', '--unified=0', '--no-color'];
|
|
732
|
+
diffOutput = execFileSync('git', args, {
|
|
733
|
+
cwd: repoRoot,
|
|
734
|
+
encoding: 'utf-8',
|
|
735
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
736
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
737
|
+
});
|
|
738
|
+
} catch (e) {
|
|
739
|
+
return { error: `Failed to run git diff: ${e.message}` };
|
|
849
740
|
}
|
|
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
|
-
}
|
|
859
741
|
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
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;
|
|
742
|
+
if (!diffOutput.trim()) {
|
|
743
|
+
return {
|
|
744
|
+
changedFiles: 0,
|
|
745
|
+
newFiles: [],
|
|
746
|
+
affectedFunctions: [],
|
|
747
|
+
affectedFiles: [],
|
|
748
|
+
summary: null,
|
|
749
|
+
};
|
|
879
750
|
}
|
|
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
|
-
try {
|
|
891
|
-
const args = opts.staged
|
|
892
|
-
? ['diff', '--cached', '--unified=0', '--no-color']
|
|
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
|
-
}
|
|
904
|
-
|
|
905
|
-
if (!diffOutput.trim()) {
|
|
906
|
-
db.close();
|
|
907
|
-
return {
|
|
908
|
-
changedFiles: 0,
|
|
909
|
-
newFiles: [],
|
|
910
|
-
affectedFunctions: [],
|
|
911
|
-
affectedFiles: [],
|
|
912
|
-
summary: null,
|
|
913
|
-
};
|
|
914
|
-
}
|
|
915
751
|
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
752
|
+
const changedRanges = new Map();
|
|
753
|
+
const newFiles = new Set();
|
|
754
|
+
let currentFile = null;
|
|
755
|
+
let prevIsDevNull = false;
|
|
756
|
+
for (const line of diffOutput.split('\n')) {
|
|
757
|
+
if (line.startsWith('--- /dev/null')) {
|
|
758
|
+
prevIsDevNull = true;
|
|
759
|
+
continue;
|
|
760
|
+
}
|
|
761
|
+
if (line.startsWith('--- ')) {
|
|
762
|
+
prevIsDevNull = false;
|
|
763
|
+
continue;
|
|
764
|
+
}
|
|
765
|
+
const fileMatch = line.match(/^\+\+\+ b\/(.+)/);
|
|
766
|
+
if (fileMatch) {
|
|
767
|
+
currentFile = fileMatch[1];
|
|
768
|
+
if (!changedRanges.has(currentFile)) changedRanges.set(currentFile, []);
|
|
769
|
+
if (prevIsDevNull) newFiles.add(currentFile);
|
|
770
|
+
prevIsDevNull = false;
|
|
771
|
+
continue;
|
|
772
|
+
}
|
|
773
|
+
const hunkMatch = line.match(/^@@ .+ \+(\d+)(?:,(\d+))? @@/);
|
|
774
|
+
if (hunkMatch && currentFile) {
|
|
775
|
+
const start = parseInt(hunkMatch[1], 10);
|
|
776
|
+
const count = parseInt(hunkMatch[2] || '1', 10);
|
|
777
|
+
changedRanges.get(currentFile).push({ start, end: start + count - 1 });
|
|
778
|
+
}
|
|
942
779
|
}
|
|
943
|
-
}
|
|
944
780
|
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
}
|
|
954
|
-
}
|
|
781
|
+
if (changedRanges.size === 0) {
|
|
782
|
+
return {
|
|
783
|
+
changedFiles: 0,
|
|
784
|
+
newFiles: [],
|
|
785
|
+
affectedFunctions: [],
|
|
786
|
+
affectedFiles: [],
|
|
787
|
+
summary: null,
|
|
788
|
+
};
|
|
789
|
+
}
|
|
955
790
|
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
791
|
+
const affectedFunctions = [];
|
|
792
|
+
for (const [file, ranges] of changedRanges) {
|
|
793
|
+
if (noTests && isTestFile(file)) continue;
|
|
794
|
+
const defs = db
|
|
795
|
+
.prepare(
|
|
796
|
+
`SELECT * FROM nodes WHERE file = ? AND kind IN ('function', 'method', 'class') ORDER BY line`,
|
|
797
|
+
)
|
|
798
|
+
.all(file);
|
|
799
|
+
for (let i = 0; i < defs.length; i++) {
|
|
800
|
+
const def = defs[i];
|
|
801
|
+
const endLine = def.end_line || (defs[i + 1] ? defs[i + 1].line - 1 : 999999);
|
|
802
|
+
for (const range of ranges) {
|
|
803
|
+
if (range.start <= endLine && range.end >= def.line) {
|
|
804
|
+
affectedFunctions.push(def);
|
|
805
|
+
break;
|
|
806
|
+
}
|
|
971
807
|
}
|
|
972
808
|
}
|
|
973
809
|
}
|
|
974
|
-
}
|
|
975
810
|
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
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++;
|
|
811
|
+
const allAffected = new Set();
|
|
812
|
+
const functionResults = affectedFunctions.map((fn) => {
|
|
813
|
+
const visited = new Set([fn.id]);
|
|
814
|
+
let frontier = [fn.id];
|
|
815
|
+
let totalCallers = 0;
|
|
816
|
+
const levels = {};
|
|
817
|
+
const edges = [];
|
|
818
|
+
const idToKey = new Map();
|
|
819
|
+
idToKey.set(fn.id, `${fn.file}::${fn.name}:${fn.line}`);
|
|
820
|
+
for (let d = 1; d <= maxDepth; d++) {
|
|
821
|
+
const nextFrontier = [];
|
|
822
|
+
for (const fid of frontier) {
|
|
823
|
+
const callers = findDistinctCallers(db, fid);
|
|
824
|
+
for (const c of callers) {
|
|
825
|
+
if (!visited.has(c.id) && (!noTests || !isTestFile(c.file))) {
|
|
826
|
+
visited.add(c.id);
|
|
827
|
+
nextFrontier.push(c.id);
|
|
828
|
+
allAffected.add(`${c.file}:${c.name}`);
|
|
829
|
+
const callerKey = `${c.file}::${c.name}:${c.line}`;
|
|
830
|
+
idToKey.set(c.id, callerKey);
|
|
831
|
+
if (!levels[d]) levels[d] = [];
|
|
832
|
+
levels[d].push({ name: c.name, kind: c.kind, file: c.file, line: c.line });
|
|
833
|
+
edges.push({ from: idToKey.get(fid), to: callerKey });
|
|
834
|
+
totalCallers++;
|
|
835
|
+
}
|
|
1006
836
|
}
|
|
1007
837
|
}
|
|
838
|
+
frontier = nextFrontier;
|
|
839
|
+
if (frontier.length === 0) break;
|
|
1008
840
|
}
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
levels,
|
|
1019
|
-
edges,
|
|
1020
|
-
};
|
|
1021
|
-
});
|
|
1022
|
-
|
|
1023
|
-
const affectedFiles = new Set();
|
|
1024
|
-
for (const key of allAffected) affectedFiles.add(key.split(':')[0]);
|
|
1025
|
-
|
|
1026
|
-
// Look up historically coupled files from co-change data
|
|
1027
|
-
let historicallyCoupled = [];
|
|
1028
|
-
try {
|
|
1029
|
-
db.prepare('SELECT 1 FROM co_changes LIMIT 1').get();
|
|
1030
|
-
const changedFilesList = [...changedRanges.keys()];
|
|
1031
|
-
const coResults = coChangeForFiles(changedFilesList, db, {
|
|
1032
|
-
minJaccard: 0.3,
|
|
1033
|
-
limit: 20,
|
|
1034
|
-
noTests,
|
|
841
|
+
return {
|
|
842
|
+
name: fn.name,
|
|
843
|
+
kind: fn.kind,
|
|
844
|
+
file: fn.file,
|
|
845
|
+
line: fn.line,
|
|
846
|
+
transitiveCallers: totalCallers,
|
|
847
|
+
levels,
|
|
848
|
+
edges,
|
|
849
|
+
};
|
|
1035
850
|
});
|
|
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
851
|
|
|
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
|
-
}
|
|
852
|
+
const affectedFiles = new Set();
|
|
853
|
+
for (const key of allAffected) affectedFiles.add(key.split(':')[0]);
|
|
1057
854
|
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
scopeFiles: [...changedRanges.keys()],
|
|
855
|
+
// Look up historically coupled files from co-change data
|
|
856
|
+
let historicallyCoupled = [];
|
|
857
|
+
try {
|
|
858
|
+
db.prepare('SELECT 1 FROM co_changes LIMIT 1').get();
|
|
859
|
+
const changedFilesList = [...changedRanges.keys()];
|
|
860
|
+
const coResults = coChangeForFiles(changedFilesList, db, {
|
|
861
|
+
minJaccard: 0.3,
|
|
862
|
+
limit: 20,
|
|
1067
863
|
noTests,
|
|
1068
864
|
});
|
|
1069
|
-
|
|
1070
|
-
|
|
865
|
+
// Exclude files already found via static analysis
|
|
866
|
+
historicallyCoupled = coResults.filter((r) => !affectedFiles.has(r.file));
|
|
867
|
+
} catch {
|
|
868
|
+
/* co_changes table doesn't exist — skip silently */
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
// Look up CODEOWNERS for changed + affected files
|
|
872
|
+
let ownership = null;
|
|
873
|
+
try {
|
|
874
|
+
const allFilePaths = [...new Set([...changedRanges.keys(), ...affectedFiles])];
|
|
875
|
+
const ownerResult = ownersForFiles(allFilePaths, repoRoot);
|
|
876
|
+
if (ownerResult.affectedOwners.length > 0) {
|
|
877
|
+
ownership = {
|
|
878
|
+
owners: Object.fromEntries(ownerResult.owners),
|
|
879
|
+
affectedOwners: ownerResult.affectedOwners,
|
|
880
|
+
suggestedReviewers: ownerResult.suggestedReviewers,
|
|
881
|
+
};
|
|
882
|
+
}
|
|
883
|
+
} catch {
|
|
884
|
+
/* CODEOWNERS missing or unreadable — skip silently */
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
// Check boundary violations scoped to changed files
|
|
888
|
+
let boundaryViolations = [];
|
|
889
|
+
let boundaryViolationCount = 0;
|
|
890
|
+
try {
|
|
891
|
+
const config = loadConfig(repoRoot);
|
|
892
|
+
const boundaryConfig = config.manifesto?.boundaries;
|
|
893
|
+
if (boundaryConfig) {
|
|
894
|
+
const result = evaluateBoundaries(db, boundaryConfig, {
|
|
895
|
+
scopeFiles: [...changedRanges.keys()],
|
|
896
|
+
noTests,
|
|
897
|
+
});
|
|
898
|
+
boundaryViolations = result.violations;
|
|
899
|
+
boundaryViolationCount = result.violationCount;
|
|
900
|
+
}
|
|
901
|
+
} catch {
|
|
902
|
+
/* boundary check failed — skip silently */
|
|
1071
903
|
}
|
|
1072
|
-
} catch {
|
|
1073
|
-
/* boundary check failed — skip silently */
|
|
1074
|
-
}
|
|
1075
904
|
|
|
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,
|
|
905
|
+
const base = {
|
|
906
|
+
changedFiles: changedRanges.size,
|
|
907
|
+
newFiles: [...newFiles],
|
|
908
|
+
affectedFunctions: functionResults,
|
|
909
|
+
affectedFiles: [...affectedFiles],
|
|
910
|
+
historicallyCoupled,
|
|
911
|
+
ownership,
|
|
912
|
+
boundaryViolations,
|
|
1092
913
|
boundaryViolationCount,
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
914
|
+
summary: {
|
|
915
|
+
functionsChanged: affectedFunctions.length,
|
|
916
|
+
callersAffected: allAffected.size,
|
|
917
|
+
filesAffected: affectedFiles.size,
|
|
918
|
+
historicallyCoupledCount: historicallyCoupled.length,
|
|
919
|
+
ownersAffected: ownership ? ownership.affectedOwners.length : 0,
|
|
920
|
+
boundaryViolationCount,
|
|
921
|
+
},
|
|
922
|
+
};
|
|
923
|
+
return paginateResult(base, 'affectedFunctions', { limit: opts.limit, offset: opts.offset });
|
|
924
|
+
} finally {
|
|
925
|
+
db.close();
|
|
926
|
+
}
|
|
1096
927
|
}
|
|
1097
928
|
|
|
1098
929
|
export function diffImpactMermaid(customDbPath, opts = {}) {
|
|
@@ -1211,35 +1042,20 @@ export function diffImpactMermaid(customDbPath, opts = {}) {
|
|
|
1211
1042
|
|
|
1212
1043
|
export function listFunctionsData(customDbPath, opts = {}) {
|
|
1213
1044
|
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
|
-
}
|
|
1045
|
+
try {
|
|
1046
|
+
const noTests = opts.noTests || false;
|
|
1229
1047
|
|
|
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);
|
|
1048
|
+
let rows = listFunctionNodes(db, { file: opts.file, pattern: opts.pattern });
|
|
1235
1049
|
|
|
1236
|
-
|
|
1050
|
+
if (noTests) rows = rows.filter((r) => !isTestFile(r.file));
|
|
1237
1051
|
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1052
|
+
const hc = new Map();
|
|
1053
|
+
const functions = rows.map((r) => normalizeSymbol(r, db, hc));
|
|
1054
|
+
const base = { count: functions.length, functions };
|
|
1055
|
+
return paginateResult(base, 'functions', { limit: opts.limit, offset: opts.offset });
|
|
1056
|
+
} finally {
|
|
1057
|
+
db.close();
|
|
1058
|
+
}
|
|
1243
1059
|
}
|
|
1244
1060
|
|
|
1245
1061
|
/**
|
|
@@ -1255,25 +1071,8 @@ export function* iterListFunctions(customDbPath, opts = {}) {
|
|
|
1255
1071
|
const db = openReadonlyOrFail(customDbPath);
|
|
1256
1072
|
try {
|
|
1257
1073
|
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
|
-
|
|
1264
|
-
if (opts.file) {
|
|
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
1074
|
|
|
1273
|
-
const
|
|
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)) {
|
|
1075
|
+
for (const row of iterateFunctionNodes(db, { file: opts.file, pattern: opts.pattern })) {
|
|
1277
1076
|
if (noTests && isTestFile(row.file)) continue;
|
|
1278
1077
|
yield {
|
|
1279
1078
|
name: row.name,
|
|
@@ -1383,569 +1182,247 @@ export function* iterWhere(target, customDbPath, opts = {}) {
|
|
|
1383
1182
|
|
|
1384
1183
|
export function statsData(customDbPath, opts = {}) {
|
|
1385
1184
|
const db = openReadonlyOrFail(customDbPath);
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1185
|
+
try {
|
|
1186
|
+
const noTests = opts.noTests || false;
|
|
1187
|
+
|
|
1188
|
+
// Build set of test file IDs for filtering nodes and edges
|
|
1189
|
+
let testFileIds = null;
|
|
1190
|
+
if (noTests) {
|
|
1191
|
+
const allFileNodes = db.prepare("SELECT id, file FROM nodes WHERE kind = 'file'").all();
|
|
1192
|
+
testFileIds = new Set();
|
|
1193
|
+
const testFiles = new Set();
|
|
1194
|
+
for (const n of allFileNodes) {
|
|
1195
|
+
if (isTestFile(n.file)) {
|
|
1196
|
+
testFileIds.add(n.id);
|
|
1197
|
+
testFiles.add(n.file);
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
// Also collect non-file node IDs that belong to test files
|
|
1201
|
+
const allNodes = db.prepare('SELECT id, file FROM nodes').all();
|
|
1202
|
+
for (const n of allNodes) {
|
|
1203
|
+
if (testFiles.has(n.file)) testFileIds.add(n.id);
|
|
1398
1204
|
}
|
|
1399
1205
|
}
|
|
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
|
-
|
|
1407
|
-
// Node breakdown by kind
|
|
1408
|
-
let nodeRows;
|
|
1409
|
-
if (noTests) {
|
|
1410
|
-
const allNodes = db.prepare('SELECT id, kind, file FROM nodes').all();
|
|
1411
|
-
const filtered = allNodes.filter((n) => !testFileIds.has(n.id));
|
|
1412
|
-
const counts = {};
|
|
1413
|
-
for (const n of filtered) counts[n.kind] = (counts[n.kind] || 0) + 1;
|
|
1414
|
-
nodeRows = Object.entries(counts).map(([kind, c]) => ({ kind, c }));
|
|
1415
|
-
} else {
|
|
1416
|
-
nodeRows = db.prepare('SELECT kind, COUNT(*) as c FROM nodes GROUP BY kind').all();
|
|
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
1206
|
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
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);
|
|
1207
|
+
// Node breakdown by kind
|
|
1208
|
+
let nodeRows;
|
|
1209
|
+
if (noTests) {
|
|
1210
|
+
const allNodes = db.prepare('SELECT id, kind, file FROM nodes').all();
|
|
1211
|
+
const filtered = allNodes.filter((n) => !testFileIds.has(n.id));
|
|
1212
|
+
const counts = {};
|
|
1213
|
+
for (const n of filtered) counts[n.kind] = (counts[n.kind] || 0) + 1;
|
|
1214
|
+
nodeRows = Object.entries(counts).map(([kind, c]) => ({ kind, c }));
|
|
1215
|
+
} else {
|
|
1216
|
+
nodeRows = db.prepare('SELECT kind, COUNT(*) as c FROM nodes GROUP BY kind').all();
|
|
1450
1217
|
}
|
|
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
|
-
};
|
|
1218
|
+
const nodesByKind = {};
|
|
1219
|
+
let totalNodes = 0;
|
|
1220
|
+
for (const r of nodeRows) {
|
|
1221
|
+
nodesByKind[r.kind] = r.c;
|
|
1222
|
+
totalNodes += r.c;
|
|
1506
1223
|
}
|
|
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
1224
|
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
.
|
|
1596
|
-
|
|
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
|
-
|
|
1647
|
-
// Human-readable output
|
|
1648
|
-
console.log('\n# Codegraph Stats\n');
|
|
1649
|
-
|
|
1650
|
-
// Nodes
|
|
1651
|
-
console.log(`Nodes: ${data.nodes.total} total`);
|
|
1652
|
-
const kindEntries = Object.entries(data.nodes.byKind).sort((a, b) => b[1] - a[1]);
|
|
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)}`,
|
|
1225
|
+
// Edge breakdown by kind
|
|
1226
|
+
let edgeRows;
|
|
1227
|
+
if (noTests) {
|
|
1228
|
+
const allEdges = db.prepare('SELECT source_id, target_id, kind FROM edges').all();
|
|
1229
|
+
const filtered = allEdges.filter(
|
|
1230
|
+
(e) => !testFileIds.has(e.source_id) && !testFileIds.has(e.target_id),
|
|
1699
1231
|
);
|
|
1232
|
+
const counts = {};
|
|
1233
|
+
for (const e of filtered) counts[e.kind] = (counts[e.kind] || 0) + 1;
|
|
1234
|
+
edgeRows = Object.entries(counts).map(([kind, c]) => ({ kind, c }));
|
|
1235
|
+
} else {
|
|
1236
|
+
edgeRows = db.prepare('SELECT kind, COUNT(*) as c FROM edges GROUP BY kind').all();
|
|
1237
|
+
}
|
|
1238
|
+
const edgesByKind = {};
|
|
1239
|
+
let totalEdges = 0;
|
|
1240
|
+
for (const r of edgeRows) {
|
|
1241
|
+
edgesByKind[r.kind] = r.c;
|
|
1242
|
+
totalEdges += r.c;
|
|
1700
1243
|
}
|
|
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
1244
|
|
|
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}`);
|
|
1245
|
+
// File/language distribution — map extensions via LANGUAGE_REGISTRY
|
|
1246
|
+
const extToLang = new Map();
|
|
1247
|
+
for (const entry of LANGUAGE_REGISTRY) {
|
|
1248
|
+
for (const ext of entry.extensions) {
|
|
1249
|
+
extToLang.set(ext, entry.id);
|
|
1729
1250
|
}
|
|
1730
1251
|
}
|
|
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}`);
|
|
1252
|
+
let fileNodes = db.prepare("SELECT file FROM nodes WHERE kind = 'file'").all();
|
|
1253
|
+
if (noTests) fileNodes = fileNodes.filter((n) => !isTestFile(n.file));
|
|
1254
|
+
const byLanguage = {};
|
|
1255
|
+
for (const row of fileNodes) {
|
|
1256
|
+
const ext = path.extname(row.file).toLowerCase();
|
|
1257
|
+
const lang = extToLang.get(ext) || 'other';
|
|
1258
|
+
byLanguage[lang] = (byLanguage[lang] || 0) + 1;
|
|
1746
1259
|
}
|
|
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
|
-
}
|
|
1260
|
+
const langCount = Object.keys(byLanguage).length;
|
|
1757
1261
|
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
const
|
|
1761
|
-
console.log(
|
|
1762
|
-
`\nCommunities: ${cm.communityCount} detected | modularity: ${cm.modularity} | drift: ${cm.driftScore}%`,
|
|
1763
|
-
);
|
|
1764
|
-
}
|
|
1765
|
-
|
|
1766
|
-
console.log();
|
|
1767
|
-
}
|
|
1768
|
-
|
|
1769
|
-
// ─── Human-readable output (original formatting) ───────────────────────
|
|
1770
|
-
|
|
1771
|
-
export function queryName(name, customDbPath, opts = {}) {
|
|
1772
|
-
const data = queryNameData(name, customDbPath, {
|
|
1773
|
-
noTests: opts.noTests,
|
|
1774
|
-
limit: opts.limit,
|
|
1775
|
-
offset: opts.offset,
|
|
1776
|
-
});
|
|
1777
|
-
if (opts.ndjson) {
|
|
1778
|
-
printNdjson(data, 'results');
|
|
1779
|
-
return;
|
|
1780
|
-
}
|
|
1781
|
-
if (opts.json) {
|
|
1782
|
-
console.log(JSON.stringify(data, null, 2));
|
|
1783
|
-
return;
|
|
1784
|
-
}
|
|
1785
|
-
if (data.results.length === 0) {
|
|
1786
|
-
console.log(`No results for "${name}"`);
|
|
1787
|
-
return;
|
|
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
|
-
}
|
|
1262
|
+
// Cycles
|
|
1263
|
+
const fileCycles = findCycles(db, { fileLevel: true, noTests });
|
|
1264
|
+
const fnCycles = findCycles(db, { fileLevel: false, noTests });
|
|
1808
1265
|
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1266
|
+
// Top 5 coupling hotspots (fan-in + fan-out, file nodes)
|
|
1267
|
+
const testFilter = testFilterSQL('n.file', noTests);
|
|
1268
|
+
const hotspotRows = db
|
|
1269
|
+
.prepare(`
|
|
1270
|
+
SELECT n.file,
|
|
1271
|
+
(SELECT COUNT(*) FROM edges WHERE target_id = n.id) as fan_in,
|
|
1272
|
+
(SELECT COUNT(*) FROM edges WHERE source_id = n.id) as fan_out
|
|
1273
|
+
FROM nodes n
|
|
1274
|
+
WHERE n.kind = 'file' ${testFilter}
|
|
1275
|
+
ORDER BY (SELECT COUNT(*) FROM edges WHERE target_id = n.id)
|
|
1276
|
+
+ (SELECT COUNT(*) FROM edges WHERE source_id = n.id) DESC
|
|
1277
|
+
`)
|
|
1278
|
+
.all();
|
|
1279
|
+
const filteredHotspots = noTests ? hotspotRows.filter((r) => !isTestFile(r.file)) : hotspotRows;
|
|
1280
|
+
const hotspots = filteredHotspots.slice(0, 5).map((r) => ({
|
|
1281
|
+
file: r.file,
|
|
1282
|
+
fanIn: r.fan_in,
|
|
1283
|
+
fanOut: r.fan_out,
|
|
1284
|
+
}));
|
|
1823
1285
|
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1286
|
+
// Embeddings metadata
|
|
1287
|
+
let embeddings = null;
|
|
1288
|
+
try {
|
|
1289
|
+
const count = db.prepare('SELECT COUNT(*) as c FROM embeddings').get();
|
|
1290
|
+
if (count && count.c > 0) {
|
|
1291
|
+
const meta = {};
|
|
1292
|
+
const metaRows = db.prepare('SELECT key, value FROM embedding_meta').all();
|
|
1293
|
+
for (const r of metaRows) meta[r.key] = r.value;
|
|
1294
|
+
embeddings = {
|
|
1295
|
+
count: count.c,
|
|
1296
|
+
model: meta.model || null,
|
|
1297
|
+
dim: meta.dim ? parseInt(meta.dim, 10) : null,
|
|
1298
|
+
builtAt: meta.built_at || null,
|
|
1299
|
+
};
|
|
1300
|
+
}
|
|
1301
|
+
} catch {
|
|
1302
|
+
/* embeddings table may not exist */
|
|
1839
1303
|
}
|
|
1840
|
-
}
|
|
1841
|
-
console.log(`\n Total: ${data.totalDependents} files transitively depend on "${file}"\n`);
|
|
1842
|
-
}
|
|
1843
1304
|
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1305
|
+
// Graph quality metrics
|
|
1306
|
+
const qualityTestFilter = testFilter.replace(/n\.file/g, 'file');
|
|
1307
|
+
const totalCallable = db
|
|
1308
|
+
.prepare(
|
|
1309
|
+
`SELECT COUNT(*) as c FROM nodes WHERE kind IN ('function', 'method') ${qualityTestFilter}`,
|
|
1310
|
+
)
|
|
1311
|
+
.get().c;
|
|
1312
|
+
const callableWithCallers = db
|
|
1313
|
+
.prepare(`
|
|
1314
|
+
SELECT COUNT(DISTINCT e.target_id) as c FROM edges e
|
|
1315
|
+
JOIN nodes n ON e.target_id = n.id
|
|
1316
|
+
WHERE e.kind = 'calls' AND n.kind IN ('function', 'method') ${testFilter}
|
|
1317
|
+
`)
|
|
1318
|
+
.get().c;
|
|
1319
|
+
const callerCoverage = totalCallable > 0 ? callableWithCallers / totalCallable : 0;
|
|
1320
|
+
|
|
1321
|
+
const totalCallEdges = db
|
|
1322
|
+
.prepare("SELECT COUNT(*) as c FROM edges WHERE kind = 'calls'")
|
|
1323
|
+
.get().c;
|
|
1324
|
+
const highConfCallEdges = db
|
|
1325
|
+
.prepare("SELECT COUNT(*) as c FROM edges WHERE kind = 'calls' AND confidence >= 0.7")
|
|
1326
|
+
.get().c;
|
|
1327
|
+
const callConfidence = totalCallEdges > 0 ? highConfCallEdges / totalCallEdges : 0;
|
|
1328
|
+
|
|
1329
|
+
// False-positive warnings: generic names with > threshold callers
|
|
1330
|
+
const fpRows = db
|
|
1331
|
+
.prepare(`
|
|
1332
|
+
SELECT n.name, n.file, n.line, COUNT(e.source_id) as caller_count
|
|
1333
|
+
FROM nodes n
|
|
1334
|
+
LEFT JOIN edges e ON n.id = e.target_id AND e.kind = 'calls'
|
|
1335
|
+
WHERE n.kind IN ('function', 'method')
|
|
1336
|
+
GROUP BY n.id
|
|
1337
|
+
HAVING caller_count > ?
|
|
1338
|
+
ORDER BY caller_count DESC
|
|
1339
|
+
`)
|
|
1340
|
+
.all(FALSE_POSITIVE_CALLER_THRESHOLD);
|
|
1341
|
+
const falsePositiveWarnings = fpRows
|
|
1342
|
+
.filter((r) =>
|
|
1343
|
+
FALSE_POSITIVE_NAMES.has(r.name.includes('.') ? r.name.split('.').pop() : r.name),
|
|
1344
|
+
)
|
|
1345
|
+
.map((r) => ({ name: r.name, file: r.file, line: r.line, callerCount: r.caller_count }));
|
|
1850
1346
|
|
|
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
|
-
}
|
|
1347
|
+
// Edges from suspicious nodes
|
|
1348
|
+
let fpEdgeCount = 0;
|
|
1349
|
+
for (const fp of falsePositiveWarnings) fpEdgeCount += fp.callerCount;
|
|
1350
|
+
const falsePositiveRatio = totalCallEdges > 0 ? fpEdgeCount / totalCallEdges : 0;
|
|
1871
1351
|
|
|
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
|
-
}
|
|
1352
|
+
const score = Math.round(
|
|
1353
|
+
callerCoverage * 40 + callConfidence * 40 + (1 - falsePositiveRatio) * 20,
|
|
1354
|
+
);
|
|
1886
1355
|
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
}
|
|
1902
|
-
console.log();
|
|
1903
|
-
}
|
|
1904
|
-
}
|
|
1356
|
+
const quality = {
|
|
1357
|
+
score,
|
|
1358
|
+
callerCoverage: {
|
|
1359
|
+
ratio: callerCoverage,
|
|
1360
|
+
covered: callableWithCallers,
|
|
1361
|
+
total: totalCallable,
|
|
1362
|
+
},
|
|
1363
|
+
callConfidence: {
|
|
1364
|
+
ratio: callConfidence,
|
|
1365
|
+
highConf: highConfCallEdges,
|
|
1366
|
+
total: totalCallEdges,
|
|
1367
|
+
},
|
|
1368
|
+
falsePositiveWarnings,
|
|
1369
|
+
};
|
|
1905
1370
|
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1371
|
+
// Role distribution
|
|
1372
|
+
let roleRows;
|
|
1373
|
+
if (noTests) {
|
|
1374
|
+
const allRoleNodes = db.prepare('SELECT role, file FROM nodes WHERE role IS NOT NULL').all();
|
|
1375
|
+
const filtered = allRoleNodes.filter((n) => !isTestFile(n.file));
|
|
1376
|
+
const counts = {};
|
|
1377
|
+
for (const n of filtered) counts[n.role] = (counts[n.role] || 0) + 1;
|
|
1378
|
+
roleRows = Object.entries(counts).map(([role, c]) => ({ role, c }));
|
|
1379
|
+
} else {
|
|
1380
|
+
roleRows = db
|
|
1381
|
+
.prepare('SELECT role, COUNT(*) as c FROM nodes WHERE role IS NOT NULL GROUP BY role')
|
|
1382
|
+
.all();
|
|
1383
|
+
}
|
|
1384
|
+
const roles = {};
|
|
1385
|
+
for (const r of roleRows) roles[r.role] = r.c;
|
|
1920
1386
|
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1387
|
+
// Complexity summary
|
|
1388
|
+
let complexity = null;
|
|
1389
|
+
try {
|
|
1390
|
+
const cRows = db
|
|
1391
|
+
.prepare(
|
|
1392
|
+
`SELECT fc.cognitive, fc.cyclomatic, fc.max_nesting, fc.maintainability_index
|
|
1393
|
+
FROM function_complexity fc JOIN nodes n ON fc.node_id = n.id
|
|
1394
|
+
WHERE n.kind IN ('function','method') ${testFilter}`,
|
|
1395
|
+
)
|
|
1396
|
+
.all();
|
|
1397
|
+
if (cRows.length > 0) {
|
|
1398
|
+
const miValues = cRows.map((r) => r.maintainability_index || 0);
|
|
1399
|
+
complexity = {
|
|
1400
|
+
analyzed: cRows.length,
|
|
1401
|
+
avgCognitive: +(cRows.reduce((s, r) => s + r.cognitive, 0) / cRows.length).toFixed(1),
|
|
1402
|
+
avgCyclomatic: +(cRows.reduce((s, r) => s + r.cyclomatic, 0) / cRows.length).toFixed(1),
|
|
1403
|
+
maxCognitive: Math.max(...cRows.map((r) => r.cognitive)),
|
|
1404
|
+
maxCyclomatic: Math.max(...cRows.map((r) => r.cyclomatic)),
|
|
1405
|
+
avgMI: +(miValues.reduce((s, v) => s + v, 0) / miValues.length).toFixed(1),
|
|
1406
|
+
minMI: +Math.min(...miValues).toFixed(1),
|
|
1407
|
+
};
|
|
1933
1408
|
}
|
|
1409
|
+
} catch {
|
|
1410
|
+
/* table may not exist in older DBs */
|
|
1934
1411
|
}
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1412
|
+
|
|
1413
|
+
return {
|
|
1414
|
+
nodes: { total: totalNodes, byKind: nodesByKind },
|
|
1415
|
+
edges: { total: totalEdges, byKind: edgesByKind },
|
|
1416
|
+
files: { total: fileNodes.length, languages: langCount, byLanguage },
|
|
1417
|
+
cycles: { fileLevel: fileCycles.length, functionLevel: fnCycles.length },
|
|
1418
|
+
hotspots,
|
|
1419
|
+
embeddings,
|
|
1420
|
+
quality,
|
|
1421
|
+
roles,
|
|
1422
|
+
complexity,
|
|
1423
|
+
};
|
|
1424
|
+
} finally {
|
|
1425
|
+
db.close();
|
|
1949
1426
|
}
|
|
1950
1427
|
}
|
|
1951
1428
|
|
|
@@ -2063,347 +1540,210 @@ function extractSignature(fileLines, line) {
|
|
|
2063
1540
|
|
|
2064
1541
|
export function contextData(name, customDbPath, opts = {}) {
|
|
2065
1542
|
const db = openReadonlyOrFail(customDbPath);
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
1543
|
+
try {
|
|
1544
|
+
const depth = opts.depth || 0;
|
|
1545
|
+
const noSource = opts.noSource || false;
|
|
1546
|
+
const noTests = opts.noTests || false;
|
|
1547
|
+
const includeTests = opts.includeTests || false;
|
|
2070
1548
|
|
|
2071
|
-
|
|
2072
|
-
|
|
1549
|
+
const dbPath = findDbPath(customDbPath);
|
|
1550
|
+
const repoRoot = path.resolve(path.dirname(dbPath), '..');
|
|
2073
1551
|
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
}
|
|
1552
|
+
const nodes = findMatchingNodes(db, name, { noTests, file: opts.file, kind: opts.kind });
|
|
1553
|
+
if (nodes.length === 0) {
|
|
1554
|
+
return { name, results: [] };
|
|
1555
|
+
}
|
|
2079
1556
|
|
|
2080
|
-
|
|
1557
|
+
// No hardcoded slice — pagination handles bounding via limit/offset
|
|
2081
1558
|
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
1559
|
+
// File-lines cache to avoid re-reading the same file
|
|
1560
|
+
const fileCache = new Map();
|
|
1561
|
+
function getFileLines(file) {
|
|
1562
|
+
if (fileCache.has(file)) return fileCache.get(file);
|
|
1563
|
+
try {
|
|
1564
|
+
const absPath = safePath(repoRoot, file);
|
|
1565
|
+
if (!absPath) {
|
|
1566
|
+
fileCache.set(file, null);
|
|
1567
|
+
return null;
|
|
1568
|
+
}
|
|
1569
|
+
const lines = fs.readFileSync(absPath, 'utf-8').split('\n');
|
|
1570
|
+
fileCache.set(file, lines);
|
|
1571
|
+
return lines;
|
|
1572
|
+
} catch (e) {
|
|
1573
|
+
debug(`getFileLines failed for ${file}: ${e.message}`);
|
|
2089
1574
|
fileCache.set(file, null);
|
|
2090
1575
|
return null;
|
|
2091
1576
|
}
|
|
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
1577
|
}
|
|
2100
|
-
}
|
|
2101
1578
|
|
|
2102
|
-
|
|
2103
|
-
|
|
1579
|
+
const results = nodes.map((node) => {
|
|
1580
|
+
const fileLines = getFileLines(node.file);
|
|
2104
1581
|
|
|
2105
|
-
|
|
2106
|
-
|
|
1582
|
+
// Source
|
|
1583
|
+
const source = noSource
|
|
1584
|
+
? null
|
|
1585
|
+
: readSourceRange(repoRoot, node.file, node.line, node.end_line);
|
|
2107
1586
|
|
|
2108
|
-
|
|
2109
|
-
|
|
1587
|
+
// Signature
|
|
1588
|
+
const signature = fileLines ? extractSignature(fileLines, node.line) : null;
|
|
2110
1589
|
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
.
|
|
2114
|
-
`SELECT n.id, n.name, n.kind, n.file, n.line, n.end_line
|
|
2115
|
-
FROM edges e JOIN nodes n ON e.target_id = n.id
|
|
2116
|
-
WHERE e.source_id = ? AND e.kind = 'calls'`,
|
|
2117
|
-
)
|
|
2118
|
-
.all(node.id);
|
|
2119
|
-
const filteredCallees = noTests ? calleeRows.filter((c) => !isTestFile(c.file)) : calleeRows;
|
|
2120
|
-
|
|
2121
|
-
const callees = filteredCallees.map((c) => {
|
|
2122
|
-
const cLines = getFileLines(c.file);
|
|
2123
|
-
const summary = cLines ? extractSummary(cLines, c.line) : null;
|
|
2124
|
-
let calleeSource = null;
|
|
2125
|
-
if (depth >= 1) {
|
|
2126
|
-
calleeSource = readSourceRange(repoRoot, c.file, c.line, c.end_line);
|
|
2127
|
-
}
|
|
2128
|
-
return {
|
|
2129
|
-
name: c.name,
|
|
2130
|
-
kind: c.kind,
|
|
2131
|
-
file: c.file,
|
|
2132
|
-
line: c.line,
|
|
2133
|
-
endLine: c.end_line || null,
|
|
2134
|
-
summary,
|
|
2135
|
-
source: calleeSource,
|
|
2136
|
-
};
|
|
2137
|
-
});
|
|
1590
|
+
// Callees
|
|
1591
|
+
const calleeRows = findCallees(db, node.id);
|
|
1592
|
+
const filteredCallees = noTests ? calleeRows.filter((c) => !isTestFile(c.file)) : calleeRows;
|
|
2138
1593
|
|
|
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
|
-
|
|
1594
|
+
const callees = filteredCallees.map((c) => {
|
|
1595
|
+
const cLines = getFileLines(c.file);
|
|
1596
|
+
const summary = cLines ? extractSummary(cLines, c.line) : null;
|
|
1597
|
+
let calleeSource = null;
|
|
1598
|
+
if (depth >= 1) {
|
|
1599
|
+
calleeSource = readSourceRange(repoRoot, c.file, c.line, c.end_line);
|
|
1600
|
+
}
|
|
1601
|
+
return {
|
|
1602
|
+
name: c.name,
|
|
1603
|
+
kind: c.kind,
|
|
1604
|
+
file: c.file,
|
|
1605
|
+
line: c.line,
|
|
1606
|
+
endLine: c.end_line || null,
|
|
1607
|
+
summary,
|
|
1608
|
+
source: calleeSource,
|
|
1609
|
+
};
|
|
1610
|
+
});
|
|
1611
|
+
|
|
1612
|
+
// Deep callee expansion via BFS (depth > 1, capped at 5)
|
|
1613
|
+
if (depth > 1) {
|
|
1614
|
+
const visited = new Set(filteredCallees.map((c) => c.id));
|
|
1615
|
+
visited.add(node.id);
|
|
1616
|
+
let frontier = filteredCallees.map((c) => c.id);
|
|
1617
|
+
const maxDepth = Math.min(depth, 5);
|
|
1618
|
+
for (let d = 2; d <= maxDepth; d++) {
|
|
1619
|
+
const nextFrontier = [];
|
|
1620
|
+
for (const fid of frontier) {
|
|
1621
|
+
const deeper = findCallees(db, fid);
|
|
1622
|
+
for (const c of deeper) {
|
|
1623
|
+
if (!visited.has(c.id) && (!noTests || !isTestFile(c.file))) {
|
|
1624
|
+
visited.add(c.id);
|
|
1625
|
+
nextFrontier.push(c.id);
|
|
1626
|
+
const cLines = getFileLines(c.file);
|
|
1627
|
+
callees.push({
|
|
1628
|
+
name: c.name,
|
|
1629
|
+
kind: c.kind,
|
|
1630
|
+
file: c.file,
|
|
1631
|
+
line: c.line,
|
|
1632
|
+
endLine: c.end_line || null,
|
|
1633
|
+
summary: cLines ? extractSummary(cLines, c.line) : null,
|
|
1634
|
+
source: readSourceRange(repoRoot, c.file, c.line, c.end_line),
|
|
1635
|
+
});
|
|
1636
|
+
}
|
|
2169
1637
|
}
|
|
2170
1638
|
}
|
|
1639
|
+
frontier = nextFrontier;
|
|
1640
|
+
if (frontier.length === 0) break;
|
|
2171
1641
|
}
|
|
2172
|
-
frontier = nextFrontier;
|
|
2173
|
-
if (frontier.length === 0) break;
|
|
2174
|
-
}
|
|
2175
|
-
}
|
|
2176
|
-
|
|
2177
|
-
// Callers
|
|
2178
|
-
let callerRows = db
|
|
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
1642
|
}
|
|
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
1643
|
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
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]);
|
|
1644
|
+
// Callers
|
|
1645
|
+
let callerRows = findCallers(db, node.id);
|
|
1646
|
+
|
|
1647
|
+
// Method hierarchy resolution
|
|
1648
|
+
if (node.kind === 'method' && node.name.includes('.')) {
|
|
1649
|
+
const methodName = node.name.split('.').pop();
|
|
1650
|
+
const relatedMethods = resolveMethodViaHierarchy(db, methodName);
|
|
1651
|
+
for (const rm of relatedMethods) {
|
|
1652
|
+
if (rm.id === node.id) continue;
|
|
1653
|
+
const extraCallers = findCallers(db, rm.id);
|
|
1654
|
+
callerRows.push(...extraCallers.map((c) => ({ ...c, viaHierarchy: rm.name })));
|
|
2236
1655
|
}
|
|
2237
1656
|
}
|
|
2238
|
-
|
|
2239
|
-
relatedTests.push({
|
|
2240
|
-
file,
|
|
2241
|
-
testCount: testNames.length,
|
|
2242
|
-
testNames,
|
|
2243
|
-
source: testSource,
|
|
2244
|
-
});
|
|
2245
|
-
}
|
|
1657
|
+
if (noTests) callerRows = callerRows.filter((c) => !isTestFile(c.file));
|
|
2246
1658
|
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
.
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
.get(node.id);
|
|
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
|
-
}
|
|
2300
|
-
|
|
2301
|
-
export function context(name, customDbPath, opts = {}) {
|
|
2302
|
-
const data = contextData(name, customDbPath, opts);
|
|
2303
|
-
if (opts.ndjson) {
|
|
2304
|
-
printNdjson(data, 'results');
|
|
2305
|
-
return;
|
|
2306
|
-
}
|
|
2307
|
-
if (opts.json) {
|
|
2308
|
-
console.log(JSON.stringify(data, null, 2));
|
|
2309
|
-
return;
|
|
2310
|
-
}
|
|
2311
|
-
if (data.results.length === 0) {
|
|
2312
|
-
console.log(`No function/method/class matching "${name}"`);
|
|
2313
|
-
return;
|
|
2314
|
-
}
|
|
2315
|
-
|
|
2316
|
-
for (const r of data.results) {
|
|
2317
|
-
const lineRange = r.endLine ? `${r.line}-${r.endLine}` : `${r.line}`;
|
|
2318
|
-
const roleTag = r.role ? ` [${r.role}]` : '';
|
|
2319
|
-
console.log(`\n# ${r.name} (${r.kind})${roleTag} — ${r.file}:${lineRange}\n`);
|
|
1659
|
+
const callers = callerRows.map((c) => ({
|
|
1660
|
+
name: c.name,
|
|
1661
|
+
kind: c.kind,
|
|
1662
|
+
file: c.file,
|
|
1663
|
+
line: c.line,
|
|
1664
|
+
viaHierarchy: c.viaHierarchy || undefined,
|
|
1665
|
+
}));
|
|
2320
1666
|
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
if (r.signature.params != null) console.log(` Parameters: (${r.signature.params})`);
|
|
2325
|
-
if (r.signature.returnType) console.log(` Returns: ${r.signature.returnType}`);
|
|
2326
|
-
console.log();
|
|
2327
|
-
}
|
|
1667
|
+
// Related tests: callers that live in test files
|
|
1668
|
+
const testCallerRows = findCallers(db, node.id);
|
|
1669
|
+
const testCallers = testCallerRows.filter((c) => isTestFile(c.file));
|
|
2328
1670
|
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
console.log(` ${kindIcon(c.kind)} ${c.name} :${c.line}`);
|
|
1671
|
+
const testsByFile = new Map();
|
|
1672
|
+
for (const tc of testCallers) {
|
|
1673
|
+
if (!testsByFile.has(tc.file)) testsByFile.set(tc.file, []);
|
|
1674
|
+
testsByFile.get(tc.file).push(tc);
|
|
2334
1675
|
}
|
|
2335
|
-
console.log();
|
|
2336
|
-
}
|
|
2337
|
-
|
|
2338
|
-
// Complexity
|
|
2339
|
-
if (r.complexity) {
|
|
2340
|
-
const cx = r.complexity;
|
|
2341
|
-
const miPart = cx.maintainabilityIndex ? ` | MI: ${cx.maintainabilityIndex}` : '';
|
|
2342
|
-
console.log('## Complexity');
|
|
2343
|
-
console.log(
|
|
2344
|
-
` Cognitive: ${cx.cognitive} | Cyclomatic: ${cx.cyclomatic} | Max Nesting: ${cx.maxNesting}${miPart}`,
|
|
2345
|
-
);
|
|
2346
|
-
console.log();
|
|
2347
|
-
}
|
|
2348
1676
|
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
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}`);
|
|
1677
|
+
const relatedTests = [];
|
|
1678
|
+
for (const [file] of testsByFile) {
|
|
1679
|
+
const tLines = getFileLines(file);
|
|
1680
|
+
const testNames = [];
|
|
1681
|
+
if (tLines) {
|
|
1682
|
+
for (const tl of tLines) {
|
|
1683
|
+
const tm = tl.match(/(?:it|test|describe)\s*\(\s*['"`]([^'"`]+)['"`]/);
|
|
1684
|
+
if (tm) testNames.push(tm[1]);
|
|
2367
1685
|
}
|
|
2368
1686
|
}
|
|
1687
|
+
const testSource = includeTests && tLines ? tLines.join('\n') : undefined;
|
|
1688
|
+
relatedTests.push({
|
|
1689
|
+
file,
|
|
1690
|
+
testCount: testNames.length,
|
|
1691
|
+
testNames,
|
|
1692
|
+
source: testSource,
|
|
1693
|
+
});
|
|
2369
1694
|
}
|
|
2370
|
-
console.log();
|
|
2371
|
-
}
|
|
2372
1695
|
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
1696
|
+
// Complexity metrics
|
|
1697
|
+
let complexityMetrics = null;
|
|
1698
|
+
try {
|
|
1699
|
+
const cRow = getComplexityForNode(db, node.id);
|
|
1700
|
+
if (cRow) {
|
|
1701
|
+
complexityMetrics = {
|
|
1702
|
+
cognitive: cRow.cognitive,
|
|
1703
|
+
cyclomatic: cRow.cyclomatic,
|
|
1704
|
+
maxNesting: cRow.max_nesting,
|
|
1705
|
+
maintainabilityIndex: cRow.maintainability_index || 0,
|
|
1706
|
+
halsteadVolume: cRow.halstead_volume || 0,
|
|
1707
|
+
};
|
|
1708
|
+
}
|
|
1709
|
+
} catch {
|
|
1710
|
+
/* table may not exist */
|
|
2379
1711
|
}
|
|
2380
|
-
console.log();
|
|
2381
|
-
}
|
|
2382
1712
|
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
console.log(` | ${line}`);
|
|
2395
|
-
}
|
|
2396
|
-
}
|
|
1713
|
+
// Children (parameters, properties, constants)
|
|
1714
|
+
let nodeChildren = [];
|
|
1715
|
+
try {
|
|
1716
|
+
nodeChildren = findNodeChildren(db, node.id).map((c) => ({
|
|
1717
|
+
name: c.name,
|
|
1718
|
+
kind: c.kind,
|
|
1719
|
+
line: c.line,
|
|
1720
|
+
endLine: c.end_line || null,
|
|
1721
|
+
}));
|
|
1722
|
+
} catch {
|
|
1723
|
+
/* parent_id column may not exist */
|
|
2397
1724
|
}
|
|
2398
|
-
console.log();
|
|
2399
|
-
}
|
|
2400
1725
|
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
1726
|
+
return {
|
|
1727
|
+
name: node.name,
|
|
1728
|
+
kind: node.kind,
|
|
1729
|
+
file: node.file,
|
|
1730
|
+
line: node.line,
|
|
1731
|
+
role: node.role || null,
|
|
1732
|
+
endLine: node.end_line || null,
|
|
1733
|
+
source,
|
|
1734
|
+
signature,
|
|
1735
|
+
complexity: complexityMetrics,
|
|
1736
|
+
children: nodeChildren.length > 0 ? nodeChildren : undefined,
|
|
1737
|
+
callees,
|
|
1738
|
+
callers,
|
|
1739
|
+
relatedTests,
|
|
1740
|
+
};
|
|
1741
|
+
});
|
|
1742
|
+
|
|
1743
|
+
const base = { name, results };
|
|
1744
|
+
return paginateResult(base, 'results', { limit: opts.limit, offset: opts.offset });
|
|
1745
|
+
} finally {
|
|
1746
|
+
db.close();
|
|
2407
1747
|
}
|
|
2408
1748
|
}
|
|
2409
1749
|
|
|
@@ -2411,62 +1751,40 @@ export function context(name, customDbPath, opts = {}) {
|
|
|
2411
1751
|
|
|
2412
1752
|
export function childrenData(name, customDbPath, opts = {}) {
|
|
2413
1753
|
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
|
-
}
|
|
1754
|
+
try {
|
|
1755
|
+
const noTests = opts.noTests || false;
|
|
2421
1756
|
|
|
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 = [];
|
|
1757
|
+
const nodes = findMatchingNodes(db, name, { noTests, file: opts.file, kind: opts.kind });
|
|
1758
|
+
if (nodes.length === 0) {
|
|
1759
|
+
return { name, results: [] };
|
|
2430
1760
|
}
|
|
2431
|
-
if (noTests) children = children.filter((c) => !isTestFile(c.file || node.file));
|
|
2432
|
-
return {
|
|
2433
|
-
name: node.name,
|
|
2434
|
-
kind: node.kind,
|
|
2435
|
-
file: node.file,
|
|
2436
|
-
line: node.line,
|
|
2437
|
-
children: children.map((c) => ({
|
|
2438
|
-
name: c.name,
|
|
2439
|
-
kind: c.kind,
|
|
2440
|
-
line: c.line,
|
|
2441
|
-
endLine: c.end_line || null,
|
|
2442
|
-
})),
|
|
2443
|
-
};
|
|
2444
|
-
});
|
|
2445
|
-
|
|
2446
|
-
db.close();
|
|
2447
|
-
const base = { name, results };
|
|
2448
|
-
return paginateResult(base, 'results', { limit: opts.limit, offset: opts.offset });
|
|
2449
|
-
}
|
|
2450
1761
|
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
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}`);
|
|
1762
|
+
const results = nodes.map((node) => {
|
|
1763
|
+
let children;
|
|
1764
|
+
try {
|
|
1765
|
+
children = findNodeChildren(db, node.id);
|
|
1766
|
+
} catch {
|
|
1767
|
+
children = [];
|
|
2468
1768
|
}
|
|
2469
|
-
|
|
1769
|
+
if (noTests) children = children.filter((c) => !isTestFile(c.file || node.file));
|
|
1770
|
+
return {
|
|
1771
|
+
name: node.name,
|
|
1772
|
+
kind: node.kind,
|
|
1773
|
+
file: node.file,
|
|
1774
|
+
line: node.line,
|
|
1775
|
+
children: children.map((c) => ({
|
|
1776
|
+
name: c.name,
|
|
1777
|
+
kind: c.kind,
|
|
1778
|
+
line: c.line,
|
|
1779
|
+
endLine: c.end_line || null,
|
|
1780
|
+
})),
|
|
1781
|
+
};
|
|
1782
|
+
});
|
|
1783
|
+
|
|
1784
|
+
const base = { name, results };
|
|
1785
|
+
return paginateResult(base, 'results', { limit: opts.limit, offset: opts.offset });
|
|
1786
|
+
} finally {
|
|
1787
|
+
db.close();
|
|
2470
1788
|
}
|
|
2471
1789
|
}
|
|
2472
1790
|
|
|
@@ -2483,28 +1801,14 @@ function isFileLikeTarget(target) {
|
|
|
2483
1801
|
}
|
|
2484
1802
|
|
|
2485
1803
|
function explainFileImpl(db, target, getFileLines) {
|
|
2486
|
-
const fileNodes = db
|
|
2487
|
-
.prepare(`SELECT * FROM nodes WHERE file LIKE ? AND kind = 'file'`)
|
|
2488
|
-
.all(`%${target}%`);
|
|
1804
|
+
const fileNodes = findFileNodes(db, `%${target}%`);
|
|
2489
1805
|
if (fileNodes.length === 0) return [];
|
|
2490
1806
|
|
|
2491
1807
|
return fileNodes.map((fn) => {
|
|
2492
|
-
const symbols = db
|
|
2493
|
-
.prepare(`SELECT * FROM nodes WHERE file = ? AND kind != 'file' ORDER BY line`)
|
|
2494
|
-
.all(fn.file);
|
|
1808
|
+
const symbols = findNodesByFile(db, fn.file);
|
|
2495
1809
|
|
|
2496
1810
|
// IDs of symbols that have incoming calls from other files (public)
|
|
2497
|
-
const publicIds =
|
|
2498
|
-
db
|
|
2499
|
-
.prepare(
|
|
2500
|
-
`SELECT DISTINCT e.target_id FROM edges e
|
|
2501
|
-
JOIN nodes caller ON e.source_id = caller.id
|
|
2502
|
-
JOIN nodes target ON e.target_id = target.id
|
|
2503
|
-
WHERE target.file = ? AND caller.file != ? AND e.kind = 'calls'`,
|
|
2504
|
-
)
|
|
2505
|
-
.all(fn.file, fn.file)
|
|
2506
|
-
.map((r) => r.target_id),
|
|
2507
|
-
);
|
|
1811
|
+
const publicIds = findCrossFileCallTargets(db, fn.file);
|
|
2508
1812
|
|
|
2509
1813
|
const fileLines = getFileLines(fn.file);
|
|
2510
1814
|
const mapSymbol = (s) => ({
|
|
@@ -2520,33 +1824,12 @@ function explainFileImpl(db, target, getFileLines) {
|
|
|
2520
1824
|
const internal = symbols.filter((s) => !publicIds.has(s.id)).map(mapSymbol);
|
|
2521
1825
|
|
|
2522
1826
|
// Imports / importedBy
|
|
2523
|
-
const imports = db
|
|
2524
|
-
.prepare(
|
|
2525
|
-
`SELECT n.file FROM edges e JOIN nodes n ON e.target_id = n.id
|
|
2526
|
-
WHERE e.source_id = ? AND e.kind IN ('imports', 'imports-type')`,
|
|
2527
|
-
)
|
|
2528
|
-
.all(fn.id)
|
|
2529
|
-
.map((r) => ({ file: r.file }));
|
|
1827
|
+
const imports = findImportTargets(db, fn.id).map((r) => ({ file: r.file }));
|
|
2530
1828
|
|
|
2531
|
-
const importedBy = db
|
|
2532
|
-
.prepare(
|
|
2533
|
-
`SELECT n.file FROM edges e JOIN nodes n ON e.source_id = n.id
|
|
2534
|
-
WHERE e.target_id = ? AND e.kind IN ('imports', 'imports-type')`,
|
|
2535
|
-
)
|
|
2536
|
-
.all(fn.id)
|
|
2537
|
-
.map((r) => ({ file: r.file }));
|
|
1829
|
+
const importedBy = findImportSources(db, fn.id).map((r) => ({ file: r.file }));
|
|
2538
1830
|
|
|
2539
1831
|
// Intra-file data flow
|
|
2540
|
-
const intraEdges = db
|
|
2541
|
-
.prepare(
|
|
2542
|
-
`SELECT caller.name as caller_name, callee.name as callee_name
|
|
2543
|
-
FROM edges e
|
|
2544
|
-
JOIN nodes caller ON e.source_id = caller.id
|
|
2545
|
-
JOIN nodes callee ON e.target_id = callee.id
|
|
2546
|
-
WHERE caller.file = ? AND callee.file = ? AND e.kind = 'calls'
|
|
2547
|
-
ORDER BY caller.line`,
|
|
2548
|
-
)
|
|
2549
|
-
.all(fn.file, fn.file);
|
|
1832
|
+
const intraEdges = findIntraFileCallEdges(db, fn.file);
|
|
2550
1833
|
|
|
2551
1834
|
const dataFlowMap = new Map();
|
|
2552
1835
|
for (const edge of intraEdges) {
|
|
@@ -2599,43 +1882,31 @@ function explainFunctionImpl(db, target, noTests, getFileLines) {
|
|
|
2599
1882
|
const summary = fileLines ? extractSummary(fileLines, node.line) : null;
|
|
2600
1883
|
const signature = fileLines ? extractSignature(fileLines, node.line) : null;
|
|
2601
1884
|
|
|
2602
|
-
const callees = db
|
|
2603
|
-
.
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
.all(node.id)
|
|
2609
|
-
.map((c) => ({ name: c.name, kind: c.kind, file: c.file, line: c.line }));
|
|
1885
|
+
const callees = findCallees(db, node.id).map((c) => ({
|
|
1886
|
+
name: c.name,
|
|
1887
|
+
kind: c.kind,
|
|
1888
|
+
file: c.file,
|
|
1889
|
+
line: c.line,
|
|
1890
|
+
}));
|
|
2610
1891
|
|
|
2611
|
-
let callers = db
|
|
2612
|
-
.
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
.all(node.id)
|
|
2618
|
-
.map((c) => ({ name: c.name, kind: c.kind, file: c.file, line: c.line }));
|
|
1892
|
+
let callers = findCallers(db, node.id).map((c) => ({
|
|
1893
|
+
name: c.name,
|
|
1894
|
+
kind: c.kind,
|
|
1895
|
+
file: c.file,
|
|
1896
|
+
line: c.line,
|
|
1897
|
+
}));
|
|
2619
1898
|
if (noTests) callers = callers.filter((c) => !isTestFile(c.file));
|
|
2620
1899
|
|
|
2621
|
-
const testCallerRows = db
|
|
2622
|
-
|
|
2623
|
-
`SELECT DISTINCT n.file FROM edges e JOIN nodes n ON e.source_id = n.id
|
|
2624
|
-
WHERE e.target_id = ? AND e.kind = 'calls'`,
|
|
2625
|
-
)
|
|
2626
|
-
.all(node.id);
|
|
1900
|
+
const testCallerRows = findCallers(db, node.id);
|
|
1901
|
+
const seenFiles = new Set();
|
|
2627
1902
|
const relatedTests = testCallerRows
|
|
2628
|
-
.filter((r) => isTestFile(r.file))
|
|
1903
|
+
.filter((r) => isTestFile(r.file) && !seenFiles.has(r.file) && seenFiles.add(r.file))
|
|
2629
1904
|
.map((r) => ({ file: r.file }));
|
|
2630
1905
|
|
|
2631
1906
|
// Complexity metrics
|
|
2632
1907
|
let complexityMetrics = null;
|
|
2633
1908
|
try {
|
|
2634
|
-
const cRow = db
|
|
2635
|
-
.prepare(
|
|
2636
|
-
'SELECT cognitive, cyclomatic, max_nesting, maintainability_index, halstead_volume FROM function_complexity WHERE node_id = ?',
|
|
2637
|
-
)
|
|
2638
|
-
.get(node.id);
|
|
1909
|
+
const cRow = getComplexityForNode(db, node.id);
|
|
2639
1910
|
if (cRow) {
|
|
2640
1911
|
complexityMetrics = {
|
|
2641
1912
|
cognitive: cRow.cognitive,
|
|
@@ -2664,200 +1935,73 @@ function explainFunctionImpl(db, target, noTests, getFileLines) {
|
|
|
2664
1935
|
|
|
2665
1936
|
export function explainData(target, customDbPath, opts = {}) {
|
|
2666
1937
|
const db = openReadonlyOrFail(customDbPath);
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
1938
|
+
try {
|
|
1939
|
+
const noTests = opts.noTests || false;
|
|
1940
|
+
const depth = opts.depth || 0;
|
|
1941
|
+
const kind = isFileLikeTarget(target) ? 'file' : 'function';
|
|
1942
|
+
|
|
1943
|
+
const dbPath = findDbPath(customDbPath);
|
|
1944
|
+
const repoRoot = path.resolve(path.dirname(dbPath), '..');
|
|
1945
|
+
|
|
1946
|
+
const fileCache = new Map();
|
|
1947
|
+
function getFileLines(file) {
|
|
1948
|
+
if (fileCache.has(file)) return fileCache.get(file);
|
|
1949
|
+
try {
|
|
1950
|
+
const absPath = safePath(repoRoot, file);
|
|
1951
|
+
if (!absPath) {
|
|
1952
|
+
fileCache.set(file, null);
|
|
1953
|
+
return null;
|
|
1954
|
+
}
|
|
1955
|
+
const lines = fs.readFileSync(absPath, 'utf-8').split('\n');
|
|
1956
|
+
fileCache.set(file, lines);
|
|
1957
|
+
return lines;
|
|
1958
|
+
} catch (e) {
|
|
1959
|
+
debug(`getFileLines failed for ${file}: ${e.message}`);
|
|
2680
1960
|
fileCache.set(file, null);
|
|
2681
1961
|
return null;
|
|
2682
1962
|
}
|
|
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
1963
|
}
|
|
2691
|
-
}
|
|
2692
1964
|
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
1965
|
+
const results =
|
|
1966
|
+
kind === 'file'
|
|
1967
|
+
? explainFileImpl(db, target, getFileLines)
|
|
1968
|
+
: explainFunctionImpl(db, target, noTests, getFileLines);
|
|
1969
|
+
|
|
1970
|
+
// Recursive dependency explanation for function targets
|
|
1971
|
+
if (kind === 'function' && depth > 0 && results.length > 0) {
|
|
1972
|
+
const visited = new Set(results.map((r) => `${r.name}:${r.file}:${r.line}`));
|
|
1973
|
+
|
|
1974
|
+
function explainCallees(parentResults, currentDepth) {
|
|
1975
|
+
if (currentDepth <= 0) return;
|
|
1976
|
+
for (const r of parentResults) {
|
|
1977
|
+
const newCallees = [];
|
|
1978
|
+
for (const callee of r.callees) {
|
|
1979
|
+
const key = `${callee.name}:${callee.file}:${callee.line}`;
|
|
1980
|
+
if (visited.has(key)) continue;
|
|
1981
|
+
visited.add(key);
|
|
1982
|
+
const calleeResults = explainFunctionImpl(db, callee.name, noTests, getFileLines);
|
|
1983
|
+
const exact = calleeResults.find(
|
|
1984
|
+
(cr) => cr.file === callee.file && cr.line === callee.line,
|
|
1985
|
+
);
|
|
1986
|
+
if (exact) {
|
|
1987
|
+
exact._depth = (r._depth || 0) + 1;
|
|
1988
|
+
newCallees.push(exact);
|
|
1989
|
+
}
|
|
1990
|
+
}
|
|
1991
|
+
if (newCallees.length > 0) {
|
|
1992
|
+
r.depDetails = newCallees;
|
|
1993
|
+
explainCallees(newCallees, currentDepth - 1);
|
|
2717
1994
|
}
|
|
2718
1995
|
}
|
|
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
1996
|
}
|
|
2847
1997
|
|
|
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();
|
|
1998
|
+
explainCallees(results, depth);
|
|
2856
1999
|
}
|
|
2857
2000
|
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2001
|
+
const base = { target, kind, results };
|
|
2002
|
+
return paginateResult(base, 'results', { limit: opts.limit, offset: opts.offset });
|
|
2003
|
+
} finally {
|
|
2004
|
+
db.close();
|
|
2861
2005
|
}
|
|
2862
2006
|
}
|
|
2863
2007
|
|
|
@@ -2909,20 +2053,10 @@ function whereSymbolImpl(db, target, noTests) {
|
|
|
2909
2053
|
|
|
2910
2054
|
const hc = new Map();
|
|
2911
2055
|
return nodes.map((node) => {
|
|
2912
|
-
const
|
|
2913
|
-
|
|
2914
|
-
`SELECT COUNT(*) as cnt FROM edges e JOIN nodes n ON e.source_id = n.id
|
|
2915
|
-
WHERE e.target_id = ? AND e.kind = 'calls' AND n.file != ?`,
|
|
2916
|
-
)
|
|
2917
|
-
.get(node.id, node.file);
|
|
2918
|
-
const exported = crossFileCallers.cnt > 0;
|
|
2056
|
+
const crossCount = countCrossFileCallers(db, node.id, node.file);
|
|
2057
|
+
const exported = crossCount > 0;
|
|
2919
2058
|
|
|
2920
|
-
let uses = db
|
|
2921
|
-
.prepare(
|
|
2922
|
-
`SELECT n.name, n.file, n.line FROM edges e JOIN nodes n ON e.source_id = n.id
|
|
2923
|
-
WHERE e.target_id = ? AND e.kind = 'calls'`,
|
|
2924
|
-
)
|
|
2925
|
-
.all(node.id);
|
|
2059
|
+
let uses = findCallers(db, node.id);
|
|
2926
2060
|
if (noTests) uses = uses.filter((u) => !isTestFile(u.file));
|
|
2927
2061
|
|
|
2928
2062
|
return {
|
|
@@ -2934,43 +2068,17 @@ function whereSymbolImpl(db, target, noTests) {
|
|
|
2934
2068
|
}
|
|
2935
2069
|
|
|
2936
2070
|
function whereFileImpl(db, target) {
|
|
2937
|
-
const fileNodes = db
|
|
2938
|
-
.prepare(`SELECT * FROM nodes WHERE file LIKE ? AND kind = 'file'`)
|
|
2939
|
-
.all(`%${target}%`);
|
|
2071
|
+
const fileNodes = findFileNodes(db, `%${target}%`);
|
|
2940
2072
|
if (fileNodes.length === 0) return [];
|
|
2941
2073
|
|
|
2942
2074
|
return fileNodes.map((fn) => {
|
|
2943
|
-
const symbols = db
|
|
2944
|
-
.prepare(`SELECT * FROM nodes WHERE file = ? AND kind != 'file' ORDER BY line`)
|
|
2945
|
-
.all(fn.file);
|
|
2075
|
+
const symbols = findNodesByFile(db, fn.file);
|
|
2946
2076
|
|
|
2947
|
-
const imports = db
|
|
2948
|
-
.prepare(
|
|
2949
|
-
`SELECT n.file FROM edges e JOIN nodes n ON e.target_id = n.id
|
|
2950
|
-
WHERE e.source_id = ? AND e.kind IN ('imports', 'imports-type')`,
|
|
2951
|
-
)
|
|
2952
|
-
.all(fn.id)
|
|
2953
|
-
.map((r) => r.file);
|
|
2077
|
+
const imports = findImportTargets(db, fn.id).map((r) => r.file);
|
|
2954
2078
|
|
|
2955
|
-
const importedBy = db
|
|
2956
|
-
.prepare(
|
|
2957
|
-
`SELECT n.file FROM edges e JOIN nodes n ON e.source_id = n.id
|
|
2958
|
-
WHERE e.target_id = ? AND e.kind IN ('imports', 'imports-type')`,
|
|
2959
|
-
)
|
|
2960
|
-
.all(fn.id)
|
|
2961
|
-
.map((r) => r.file);
|
|
2079
|
+
const importedBy = findImportSources(db, fn.id).map((r) => r.file);
|
|
2962
2080
|
|
|
2963
|
-
const exportedIds =
|
|
2964
|
-
db
|
|
2965
|
-
.prepare(
|
|
2966
|
-
`SELECT DISTINCT e.target_id FROM edges e
|
|
2967
|
-
JOIN nodes caller ON e.source_id = caller.id
|
|
2968
|
-
JOIN nodes target ON e.target_id = target.id
|
|
2969
|
-
WHERE target.file = ? AND caller.file != ? AND e.kind = 'calls'`,
|
|
2970
|
-
)
|
|
2971
|
-
.all(fn.file, fn.file)
|
|
2972
|
-
.map((r) => r.target_id),
|
|
2973
|
-
);
|
|
2081
|
+
const exportedIds = findCrossFileCallTargets(db, fn.file);
|
|
2974
2082
|
|
|
2975
2083
|
const exported = symbols.filter((s) => exportedIds.has(s.id)).map((s) => s.name);
|
|
2976
2084
|
|
|
@@ -2987,157 +2095,66 @@ function whereFileImpl(db, target) {
|
|
|
2987
2095
|
|
|
2988
2096
|
export function whereData(target, customDbPath, opts = {}) {
|
|
2989
2097
|
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
|
-
}
|
|
2098
|
+
try {
|
|
2099
|
+
const noTests = opts.noTests || false;
|
|
2100
|
+
const fileMode = opts.file || false;
|
|
3010
2101
|
|
|
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
|
-
}
|
|
2102
|
+
const results = fileMode ? whereFileImpl(db, target) : whereSymbolImpl(db, target, noTests);
|
|
3019
2103
|
|
|
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
|
-
}
|
|
2104
|
+
const base = { target, mode: fileMode ? 'file' : 'symbol', results };
|
|
2105
|
+
return paginateResult(base, 'results', { limit: opts.limit, offset: opts.offset });
|
|
2106
|
+
} finally {
|
|
2107
|
+
db.close();
|
|
3049
2108
|
}
|
|
3050
|
-
console.log();
|
|
3051
2109
|
}
|
|
3052
2110
|
|
|
3053
2111
|
// ─── rolesData ──────────────────────────────────────────────────────────
|
|
3054
2112
|
|
|
3055
2113
|
export function rolesData(customDbPath, opts = {}) {
|
|
3056
2114
|
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
|
-
}
|
|
2115
|
+
try {
|
|
2116
|
+
const noTests = opts.noTests || false;
|
|
2117
|
+
const filterRole = opts.role || null;
|
|
2118
|
+
const filterFile = opts.file || null;
|
|
3103
2119
|
|
|
3104
|
-
|
|
3105
|
-
|
|
3106
|
-
return;
|
|
3107
|
-
}
|
|
2120
|
+
const conditions = ['role IS NOT NULL'];
|
|
2121
|
+
const params = [];
|
|
3108
2122
|
|
|
3109
|
-
|
|
3110
|
-
|
|
2123
|
+
if (filterRole) {
|
|
2124
|
+
conditions.push('role = ?');
|
|
2125
|
+
params.push(filterRole);
|
|
2126
|
+
}
|
|
2127
|
+
if (filterFile) {
|
|
2128
|
+
conditions.push('file LIKE ?');
|
|
2129
|
+
params.push(`%${filterFile}%`);
|
|
2130
|
+
}
|
|
3111
2131
|
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
2132
|
+
let rows = db
|
|
2133
|
+
.prepare(
|
|
2134
|
+
`SELECT name, kind, file, line, end_line, role FROM nodes WHERE ${conditions.join(' AND ')} ORDER BY role, file, line`,
|
|
2135
|
+
)
|
|
2136
|
+
.all(...params);
|
|
3116
2137
|
|
|
3117
|
-
|
|
3118
|
-
for (const s of data.symbols) {
|
|
3119
|
-
if (!byRole[s.role]) byRole[s.role] = [];
|
|
3120
|
-
byRole[s.role].push(s);
|
|
3121
|
-
}
|
|
2138
|
+
if (noTests) rows = rows.filter((r) => !isTestFile(r.file));
|
|
3122
2139
|
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
console.log(` ${kindIcon(s.kind)} ${s.name} ${s.file}:${s.line}`);
|
|
2140
|
+
const summary = {};
|
|
2141
|
+
for (const r of rows) {
|
|
2142
|
+
summary[r.role] = (summary[r.role] || 0) + 1;
|
|
3127
2143
|
}
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
2144
|
+
|
|
2145
|
+
const hc = new Map();
|
|
2146
|
+
const symbols = rows.map((r) => normalizeSymbol(r, db, hc));
|
|
2147
|
+
const base = { count: symbols.length, summary, symbols };
|
|
2148
|
+
return paginateResult(base, 'symbols', { limit: opts.limit, offset: opts.offset });
|
|
2149
|
+
} finally {
|
|
2150
|
+
db.close();
|
|
3132
2151
|
}
|
|
3133
2152
|
}
|
|
3134
2153
|
|
|
3135
2154
|
// ─── exportsData ─────────────────────────────────────────────────────
|
|
3136
2155
|
|
|
3137
2156
|
function exportsFileImpl(db, target, noTests, getFileLines, unused) {
|
|
3138
|
-
const fileNodes = db
|
|
3139
|
-
.prepare(`SELECT * FROM nodes WHERE file LIKE ? AND kind = 'file'`)
|
|
3140
|
-
.all(`%${target}%`);
|
|
2157
|
+
const fileNodes = findFileNodes(db, `%${target}%`);
|
|
3141
2158
|
if (fileNodes.length === 0) return [];
|
|
3142
2159
|
|
|
3143
2160
|
// Detect whether exported column exists
|
|
@@ -3150,9 +2167,7 @@ function exportsFileImpl(db, target, noTests, getFileLines, unused) {
|
|
|
3150
2167
|
}
|
|
3151
2168
|
|
|
3152
2169
|
return fileNodes.map((fn) => {
|
|
3153
|
-
const symbols = db
|
|
3154
|
-
.prepare(`SELECT * FROM nodes WHERE file = ? AND kind != 'file' ORDER BY line`)
|
|
3155
|
-
.all(fn.file);
|
|
2170
|
+
const symbols = findNodesByFile(db, fn.file);
|
|
3156
2171
|
|
|
3157
2172
|
let exported;
|
|
3158
2173
|
if (hasExportedCol) {
|
|
@@ -3164,17 +2179,7 @@ function exportsFileImpl(db, target, noTests, getFileLines, unused) {
|
|
|
3164
2179
|
.all(fn.file);
|
|
3165
2180
|
} else {
|
|
3166
2181
|
// Fallback: symbols that have incoming calls from other files
|
|
3167
|
-
const exportedIds =
|
|
3168
|
-
db
|
|
3169
|
-
.prepare(
|
|
3170
|
-
`SELECT DISTINCT e.target_id FROM edges e
|
|
3171
|
-
JOIN nodes caller ON e.source_id = caller.id
|
|
3172
|
-
JOIN nodes target ON e.target_id = target.id
|
|
3173
|
-
WHERE target.file = ? AND caller.file != ? AND e.kind = 'calls'`,
|
|
3174
|
-
)
|
|
3175
|
-
.all(fn.file, fn.file)
|
|
3176
|
-
.map((r) => r.target_id),
|
|
3177
|
-
);
|
|
2182
|
+
const exportedIds = findCrossFileCallTargets(db, fn.file);
|
|
3178
2183
|
exported = symbols.filter((s) => exportedIds.has(s.id));
|
|
3179
2184
|
}
|
|
3180
2185
|
const internalCount = symbols.length - exported.length;
|
|
@@ -3232,203 +2237,53 @@ function exportsFileImpl(db, target, noTests, getFileLines, unused) {
|
|
|
3232
2237
|
|
|
3233
2238
|
export function exportsData(file, customDbPath, opts = {}) {
|
|
3234
2239
|
const db = openReadonlyOrFail(customDbPath);
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
const dbFilePath = findDbPath(customDbPath);
|
|
3238
|
-
const repoRoot = path.resolve(path.dirname(dbFilePath), '..');
|
|
2240
|
+
try {
|
|
2241
|
+
const noTests = opts.noTests || false;
|
|
3239
2242
|
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
if (
|
|
2243
|
+
const dbFilePath = findDbPath(customDbPath);
|
|
2244
|
+
const repoRoot = path.resolve(path.dirname(dbFilePath), '..');
|
|
2245
|
+
|
|
2246
|
+
const fileCache = new Map();
|
|
2247
|
+
function getFileLines(file) {
|
|
2248
|
+
if (fileCache.has(file)) return fileCache.get(file);
|
|
2249
|
+
try {
|
|
2250
|
+
const absPath = safePath(repoRoot, file);
|
|
2251
|
+
if (!absPath) {
|
|
2252
|
+
fileCache.set(file, null);
|
|
2253
|
+
return null;
|
|
2254
|
+
}
|
|
2255
|
+
const lines = fs.readFileSync(absPath, 'utf-8').split('\n');
|
|
2256
|
+
fileCache.set(file, lines);
|
|
2257
|
+
return lines;
|
|
2258
|
+
} catch {
|
|
3246
2259
|
fileCache.set(file, null);
|
|
3247
2260
|
return null;
|
|
3248
2261
|
}
|
|
3249
|
-
const lines = fs.readFileSync(absPath, 'utf-8').split('\n');
|
|
3250
|
-
fileCache.set(file, lines);
|
|
3251
|
-
return lines;
|
|
3252
|
-
} catch {
|
|
3253
|
-
fileCache.set(file, null);
|
|
3254
|
-
return null;
|
|
3255
|
-
}
|
|
3256
|
-
}
|
|
3257
|
-
|
|
3258
|
-
const unused = opts.unused || false;
|
|
3259
|
-
const fileResults = exportsFileImpl(db, file, noTests, getFileLines, unused);
|
|
3260
|
-
db.close();
|
|
3261
|
-
|
|
3262
|
-
if (fileResults.length === 0) {
|
|
3263
|
-
return paginateResult(
|
|
3264
|
-
{ file, results: [], reexports: [], totalExported: 0, totalInternal: 0, totalUnused: 0 },
|
|
3265
|
-
'results',
|
|
3266
|
-
{ limit: opts.limit, offset: opts.offset },
|
|
3267
|
-
);
|
|
3268
|
-
}
|
|
3269
|
-
|
|
3270
|
-
// For single-file match return flat; for multi-match return first (like explainData)
|
|
3271
|
-
const first = fileResults[0];
|
|
3272
|
-
const base = {
|
|
3273
|
-
file: first.file,
|
|
3274
|
-
results: first.results,
|
|
3275
|
-
reexports: first.reexports,
|
|
3276
|
-
totalExported: first.totalExported,
|
|
3277
|
-
totalInternal: first.totalInternal,
|
|
3278
|
-
totalUnused: first.totalUnused,
|
|
3279
|
-
};
|
|
3280
|
-
return paginateResult(base, 'results', { limit: opts.limit, offset: opts.offset });
|
|
3281
|
-
}
|
|
3282
|
-
|
|
3283
|
-
export function fileExports(file, customDbPath, opts = {}) {
|
|
3284
|
-
const data = exportsData(file, customDbPath, opts);
|
|
3285
|
-
if (opts.ndjson) {
|
|
3286
|
-
printNdjson(data, 'results');
|
|
3287
|
-
return;
|
|
3288
|
-
}
|
|
3289
|
-
if (opts.json) {
|
|
3290
|
-
console.log(JSON.stringify(data, null, 2));
|
|
3291
|
-
return;
|
|
3292
|
-
}
|
|
3293
|
-
|
|
3294
|
-
if (data.results.length === 0) {
|
|
3295
|
-
if (opts.unused) {
|
|
3296
|
-
console.log(`No unused exports found for "${file}".`);
|
|
3297
|
-
} else {
|
|
3298
|
-
console.log(`No exported symbols found for "${file}". Run "codegraph build" first.`);
|
|
3299
|
-
}
|
|
3300
|
-
return;
|
|
3301
|
-
}
|
|
3302
|
-
|
|
3303
|
-
if (opts.unused) {
|
|
3304
|
-
console.log(
|
|
3305
|
-
`\n# ${data.file} — ${data.totalUnused} unused export${data.totalUnused !== 1 ? 's' : ''} (of ${data.totalExported} exported)\n`,
|
|
3306
|
-
);
|
|
3307
|
-
} else {
|
|
3308
|
-
const unusedNote = data.totalUnused > 0 ? ` (${data.totalUnused} unused)` : '';
|
|
3309
|
-
console.log(
|
|
3310
|
-
`\n# ${data.file} — ${data.totalExported} exported${unusedNote}, ${data.totalInternal} internal\n`,
|
|
3311
|
-
);
|
|
3312
|
-
}
|
|
3313
|
-
|
|
3314
|
-
for (const sym of data.results) {
|
|
3315
|
-
const icon = kindIcon(sym.kind);
|
|
3316
|
-
const sig = sym.signature?.params ? `(${sym.signature.params})` : '';
|
|
3317
|
-
const role = sym.role ? ` [${sym.role}]` : '';
|
|
3318
|
-
console.log(` ${icon} ${sym.name}${sig}${role} :${sym.line}`);
|
|
3319
|
-
if (sym.consumers.length === 0) {
|
|
3320
|
-
console.log(' (no consumers)');
|
|
3321
|
-
} else {
|
|
3322
|
-
for (const c of sym.consumers) {
|
|
3323
|
-
console.log(` <- ${c.name} (${c.file}:${c.line})`);
|
|
3324
|
-
}
|
|
3325
|
-
}
|
|
3326
|
-
}
|
|
3327
|
-
|
|
3328
|
-
if (data.reexports.length > 0) {
|
|
3329
|
-
console.log(`\n Re-exports: ${data.reexports.map((r) => r.file).join(', ')}`);
|
|
3330
|
-
}
|
|
3331
|
-
console.log();
|
|
3332
|
-
}
|
|
3333
|
-
|
|
3334
|
-
export function fnImpact(name, customDbPath, opts = {}) {
|
|
3335
|
-
const data = fnImpactData(name, customDbPath, opts);
|
|
3336
|
-
if (opts.ndjson) {
|
|
3337
|
-
printNdjson(data, 'results');
|
|
3338
|
-
return;
|
|
3339
|
-
}
|
|
3340
|
-
if (opts.json) {
|
|
3341
|
-
console.log(JSON.stringify(data, null, 2));
|
|
3342
|
-
return;
|
|
3343
|
-
}
|
|
3344
|
-
if (data.results.length === 0) {
|
|
3345
|
-
console.log(`No function/method/class matching "${name}"`);
|
|
3346
|
-
return;
|
|
3347
|
-
}
|
|
3348
|
-
|
|
3349
|
-
for (const r of data.results) {
|
|
3350
|
-
console.log(`\nFunction impact: ${kindIcon(r.kind)} ${r.name} -- ${r.file}:${r.line}\n`);
|
|
3351
|
-
if (Object.keys(r.levels).length === 0) {
|
|
3352
|
-
console.log(` No callers found.`);
|
|
3353
|
-
} else {
|
|
3354
|
-
for (const [level, fns] of Object.entries(r.levels).sort((a, b) => a[0] - b[0])) {
|
|
3355
|
-
const l = parseInt(level, 10);
|
|
3356
|
-
console.log(` ${'--'.repeat(l)} Level ${level} (${fns.length} functions):`);
|
|
3357
|
-
for (const f of fns.slice(0, 20))
|
|
3358
|
-
console.log(` ${' '.repeat(l)}^ ${kindIcon(f.kind)} ${f.name} ${f.file}:${f.line}`);
|
|
3359
|
-
if (fns.length > 20) console.log(` ... and ${fns.length - 20} more`);
|
|
3360
|
-
}
|
|
3361
2262
|
}
|
|
3362
|
-
console.log(`\n Total: ${r.totalDependents} functions transitively depend on ${r.name}\n`);
|
|
3363
|
-
}
|
|
3364
|
-
}
|
|
3365
2263
|
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
console.log(diffImpactMermaid(customDbPath, opts));
|
|
3369
|
-
return;
|
|
3370
|
-
}
|
|
3371
|
-
const data = diffImpactData(customDbPath, opts);
|
|
3372
|
-
if (opts.ndjson) {
|
|
3373
|
-
printNdjson(data, 'affectedFunctions');
|
|
3374
|
-
return;
|
|
3375
|
-
}
|
|
3376
|
-
if (opts.json || opts.format === 'json') {
|
|
3377
|
-
console.log(JSON.stringify(data, null, 2));
|
|
3378
|
-
return;
|
|
3379
|
-
}
|
|
3380
|
-
if (data.error) {
|
|
3381
|
-
console.log(data.error);
|
|
3382
|
-
return;
|
|
3383
|
-
}
|
|
3384
|
-
if (data.changedFiles === 0) {
|
|
3385
|
-
console.log('No changes detected.');
|
|
3386
|
-
return;
|
|
3387
|
-
}
|
|
3388
|
-
if (data.affectedFunctions.length === 0) {
|
|
3389
|
-
console.log(
|
|
3390
|
-
' No function-level changes detected (changes may be in imports, types, or config).',
|
|
3391
|
-
);
|
|
3392
|
-
return;
|
|
3393
|
-
}
|
|
2264
|
+
const unused = opts.unused || false;
|
|
2265
|
+
const fileResults = exportsFileImpl(db, file, noTests, getFileLines, unused);
|
|
3394
2266
|
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
}
|
|
3401
|
-
if (data.historicallyCoupled && data.historicallyCoupled.length > 0) {
|
|
3402
|
-
console.log('\n Historically coupled (not in static graph):\n');
|
|
3403
|
-
for (const c of data.historicallyCoupled) {
|
|
3404
|
-
const pct = `${(c.jaccard * 100).toFixed(0)}%`;
|
|
3405
|
-
console.log(
|
|
3406
|
-
` ${c.file} <- coupled with ${c.coupledWith} (${pct}, ${c.commitCount} commits)`,
|
|
2267
|
+
if (fileResults.length === 0) {
|
|
2268
|
+
return paginateResult(
|
|
2269
|
+
{ file, results: [], reexports: [], totalExported: 0, totalInternal: 0, totalUnused: 0 },
|
|
2270
|
+
'results',
|
|
2271
|
+
{ limit: opts.limit, offset: opts.offset },
|
|
3407
2272
|
);
|
|
3408
2273
|
}
|
|
3409
|
-
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
}
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
|
|
3423
|
-
if (data.summary.historicallyCoupledCount > 0) {
|
|
3424
|
-
summaryLine += `, ${data.summary.historicallyCoupledCount} historically coupled`;
|
|
3425
|
-
}
|
|
3426
|
-
if (data.summary.ownersAffected > 0) {
|
|
3427
|
-
summaryLine += `, ${data.summary.ownersAffected} owners affected`;
|
|
3428
|
-
}
|
|
3429
|
-
if (data.summary.boundaryViolationCount > 0) {
|
|
3430
|
-
summaryLine += `, ${data.summary.boundaryViolationCount} boundary violations`;
|
|
3431
|
-
}
|
|
3432
|
-
console.log(`${summaryLine}\n`);
|
|
2274
|
+
|
|
2275
|
+
// For single-file match return flat; for multi-match return first (like explainData)
|
|
2276
|
+
const first = fileResults[0];
|
|
2277
|
+
const base = {
|
|
2278
|
+
file: first.file,
|
|
2279
|
+
results: first.results,
|
|
2280
|
+
reexports: first.reexports,
|
|
2281
|
+
totalExported: first.totalExported,
|
|
2282
|
+
totalInternal: first.totalInternal,
|
|
2283
|
+
totalUnused: first.totalUnused,
|
|
2284
|
+
};
|
|
2285
|
+
return paginateResult(base, 'results', { limit: opts.limit, offset: opts.offset });
|
|
2286
|
+
} finally {
|
|
2287
|
+
db.close();
|
|
3433
2288
|
}
|
|
3434
2289
|
}
|