@optave/codegraph 1.1.0 → 1.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +190 -190
- package/README.md +498 -311
- package/grammars/tree-sitter-c_sharp.wasm +0 -0
- package/grammars/tree-sitter-go.wasm +0 -0
- package/grammars/tree-sitter-hcl.wasm +0 -0
- package/grammars/tree-sitter-java.wasm +0 -0
- package/grammars/tree-sitter-javascript.wasm +0 -0
- package/grammars/tree-sitter-php.wasm +0 -0
- package/grammars/tree-sitter-python.wasm +0 -0
- package/grammars/tree-sitter-ruby.wasm +0 -0
- package/grammars/tree-sitter-rust.wasm +0 -0
- package/grammars/tree-sitter-tsx.wasm +0 -0
- package/grammars/tree-sitter-typescript.wasm +0 -0
- package/package.json +90 -69
- package/src/builder.js +161 -162
- package/src/cli.js +284 -224
- package/src/config.js +103 -55
- package/src/constants.js +41 -28
- package/src/cycles.js +125 -104
- package/src/db.js +129 -117
- package/src/embedder.js +253 -59
- package/src/export.js +150 -138
- package/src/index.js +50 -39
- package/src/logger.js +24 -20
- package/src/mcp.js +311 -139
- package/src/native.js +68 -0
- package/src/parser.js +2214 -573
- package/src/queries.js +334 -128
- package/src/resolve.js +171 -0
- package/src/watcher.js +81 -53
package/src/queries.js
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
import
|
|
3
|
-
import path from 'path';
|
|
4
|
-
import { execFileSync } from 'child_process';
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import path from 'node:path';
|
|
5
3
|
import { findDbPath, openReadonlyOrFail } from './db.js';
|
|
6
|
-
import { warn, debug } from './logger.js';
|
|
7
4
|
|
|
8
5
|
const TEST_PATTERN = /\.(test|spec)\.|__test__|__tests__|\.stories\./;
|
|
9
6
|
function isTestFile(filePath) {
|
|
@@ -18,10 +15,12 @@ function getClassHierarchy(db, classNodeId) {
|
|
|
18
15
|
const queue = [classNodeId];
|
|
19
16
|
while (queue.length > 0) {
|
|
20
17
|
const current = queue.shift();
|
|
21
|
-
const parents = db
|
|
18
|
+
const parents = db
|
|
19
|
+
.prepare(`
|
|
22
20
|
SELECT n.id, n.name FROM edges e JOIN nodes n ON e.target_id = n.id
|
|
23
21
|
WHERE e.source_id = ? AND e.kind = 'extends'
|
|
24
|
-
`)
|
|
22
|
+
`)
|
|
23
|
+
.all(current);
|
|
25
24
|
for (const p of parents) {
|
|
26
25
|
if (!ancestors.has(p.id)) {
|
|
27
26
|
ancestors.add(p.id);
|
|
@@ -33,25 +32,25 @@ function getClassHierarchy(db, classNodeId) {
|
|
|
33
32
|
}
|
|
34
33
|
|
|
35
34
|
function resolveMethodViaHierarchy(db, methodName) {
|
|
36
|
-
const methods = db
|
|
37
|
-
`SELECT * FROM nodes WHERE kind = 'method' AND name LIKE ?`
|
|
38
|
-
|
|
35
|
+
const methods = db
|
|
36
|
+
.prepare(`SELECT * FROM nodes WHERE kind = 'method' AND name LIKE ?`)
|
|
37
|
+
.all(`%.${methodName}`);
|
|
39
38
|
|
|
40
39
|
const results = [...methods];
|
|
41
40
|
for (const m of methods) {
|
|
42
41
|
const className = m.name.split('.')[0];
|
|
43
|
-
const classNode = db
|
|
44
|
-
`SELECT * FROM nodes WHERE name = ? AND kind = 'class' AND file = ?`
|
|
45
|
-
|
|
42
|
+
const classNode = db
|
|
43
|
+
.prepare(`SELECT * FROM nodes WHERE name = ? AND kind = 'class' AND file = ?`)
|
|
44
|
+
.get(className, m.file);
|
|
46
45
|
if (!classNode) continue;
|
|
47
46
|
|
|
48
47
|
const ancestors = getClassHierarchy(db, classNode.id);
|
|
49
48
|
for (const ancestorId of ancestors) {
|
|
50
49
|
const ancestor = db.prepare('SELECT name FROM nodes WHERE id = ?').get(ancestorId);
|
|
51
50
|
if (!ancestor) continue;
|
|
52
|
-
const parentMethods = db
|
|
53
|
-
`SELECT * FROM nodes WHERE name = ? AND kind = 'method'`
|
|
54
|
-
|
|
51
|
+
const parentMethods = db
|
|
52
|
+
.prepare(`SELECT * FROM nodes WHERE name = ? AND kind = 'method'`)
|
|
53
|
+
.all(`${ancestor.name}.${methodName}`);
|
|
55
54
|
results.push(...parentMethods);
|
|
56
55
|
}
|
|
57
56
|
}
|
|
@@ -60,13 +59,20 @@ function resolveMethodViaHierarchy(db, methodName) {
|
|
|
60
59
|
|
|
61
60
|
function kindIcon(kind) {
|
|
62
61
|
switch (kind) {
|
|
63
|
-
case 'function':
|
|
64
|
-
|
|
65
|
-
case '
|
|
66
|
-
|
|
67
|
-
case '
|
|
68
|
-
|
|
69
|
-
|
|
62
|
+
case 'function':
|
|
63
|
+
return 'f';
|
|
64
|
+
case 'class':
|
|
65
|
+
return '*';
|
|
66
|
+
case 'method':
|
|
67
|
+
return 'o';
|
|
68
|
+
case 'file':
|
|
69
|
+
return '#';
|
|
70
|
+
case 'interface':
|
|
71
|
+
return 'I';
|
|
72
|
+
case 'type':
|
|
73
|
+
return 'T';
|
|
74
|
+
default:
|
|
75
|
+
return '-';
|
|
70
76
|
}
|
|
71
77
|
}
|
|
72
78
|
|
|
@@ -75,25 +81,47 @@ function kindIcon(kind) {
|
|
|
75
81
|
export function queryNameData(name, customDbPath) {
|
|
76
82
|
const db = openReadonlyOrFail(customDbPath);
|
|
77
83
|
const nodes = db.prepare(`SELECT * FROM nodes WHERE name LIKE ?`).all(`%${name}%`);
|
|
78
|
-
if (nodes.length === 0) {
|
|
84
|
+
if (nodes.length === 0) {
|
|
85
|
+
db.close();
|
|
86
|
+
return { query: name, results: [] };
|
|
87
|
+
}
|
|
79
88
|
|
|
80
|
-
const results = nodes.map(node => {
|
|
81
|
-
const callees = db
|
|
89
|
+
const results = nodes.map((node) => {
|
|
90
|
+
const callees = db
|
|
91
|
+
.prepare(`
|
|
82
92
|
SELECT n.name, n.kind, n.file, n.line, e.kind as edge_kind
|
|
83
93
|
FROM edges e JOIN nodes n ON e.target_id = n.id
|
|
84
94
|
WHERE e.source_id = ?
|
|
85
|
-
`)
|
|
95
|
+
`)
|
|
96
|
+
.all(node.id);
|
|
86
97
|
|
|
87
|
-
const callers = db
|
|
98
|
+
const callers = db
|
|
99
|
+
.prepare(`
|
|
88
100
|
SELECT n.name, n.kind, n.file, n.line, e.kind as edge_kind
|
|
89
101
|
FROM edges e JOIN nodes n ON e.source_id = n.id
|
|
90
102
|
WHERE e.target_id = ?
|
|
91
|
-
`)
|
|
103
|
+
`)
|
|
104
|
+
.all(node.id);
|
|
92
105
|
|
|
93
106
|
return {
|
|
94
|
-
name: node.name,
|
|
95
|
-
|
|
96
|
-
|
|
107
|
+
name: node.name,
|
|
108
|
+
kind: node.kind,
|
|
109
|
+
file: node.file,
|
|
110
|
+
line: node.line,
|
|
111
|
+
callees: callees.map((c) => ({
|
|
112
|
+
name: c.name,
|
|
113
|
+
kind: c.kind,
|
|
114
|
+
file: c.file,
|
|
115
|
+
line: c.line,
|
|
116
|
+
edgeKind: c.edge_kind,
|
|
117
|
+
})),
|
|
118
|
+
callers: callers.map((c) => ({
|
|
119
|
+
name: c.name,
|
|
120
|
+
kind: c.kind,
|
|
121
|
+
file: c.file,
|
|
122
|
+
line: c.line,
|
|
123
|
+
edgeKind: c.edge_kind,
|
|
124
|
+
})),
|
|
97
125
|
};
|
|
98
126
|
});
|
|
99
127
|
|
|
@@ -103,8 +131,13 @@ export function queryNameData(name, customDbPath) {
|
|
|
103
131
|
|
|
104
132
|
export function impactAnalysisData(file, customDbPath) {
|
|
105
133
|
const db = openReadonlyOrFail(customDbPath);
|
|
106
|
-
const fileNodes = db
|
|
107
|
-
|
|
134
|
+
const fileNodes = db
|
|
135
|
+
.prepare(`SELECT * FROM nodes WHERE file LIKE ? AND kind = 'file'`)
|
|
136
|
+
.all(`%${file}%`);
|
|
137
|
+
if (fileNodes.length === 0) {
|
|
138
|
+
db.close();
|
|
139
|
+
return { file, sources: [], levels: {}, totalDependents: 0 };
|
|
140
|
+
}
|
|
108
141
|
|
|
109
142
|
const visited = new Set();
|
|
110
143
|
const queue = [];
|
|
@@ -119,10 +152,12 @@ export function impactAnalysisData(file, customDbPath) {
|
|
|
119
152
|
while (queue.length > 0) {
|
|
120
153
|
const current = queue.shift();
|
|
121
154
|
const level = levels.get(current);
|
|
122
|
-
const dependents = db
|
|
155
|
+
const dependents = db
|
|
156
|
+
.prepare(`
|
|
123
157
|
SELECT n.* FROM edges e JOIN nodes n ON e.source_id = n.id
|
|
124
158
|
WHERE e.target_id = ? AND e.kind IN ('imports', 'imports-type')
|
|
125
|
-
`)
|
|
159
|
+
`)
|
|
160
|
+
.all(current);
|
|
126
161
|
for (const dep of dependents) {
|
|
127
162
|
if (!visited.has(dep.id)) {
|
|
128
163
|
visited.add(dep.id);
|
|
@@ -143,7 +178,7 @@ export function impactAnalysisData(file, customDbPath) {
|
|
|
143
178
|
db.close();
|
|
144
179
|
return {
|
|
145
180
|
file,
|
|
146
|
-
sources: fileNodes.map(f => f.file),
|
|
181
|
+
sources: fileNodes.map((f) => f.file),
|
|
147
182
|
levels: byLevel,
|
|
148
183
|
totalDependents: visited.size - fileNodes.length,
|
|
149
184
|
};
|
|
@@ -152,7 +187,8 @@ export function impactAnalysisData(file, customDbPath) {
|
|
|
152
187
|
export function moduleMapData(customDbPath, limit = 20) {
|
|
153
188
|
const db = openReadonlyOrFail(customDbPath);
|
|
154
189
|
|
|
155
|
-
const nodes = db
|
|
190
|
+
const nodes = db
|
|
191
|
+
.prepare(`
|
|
156
192
|
SELECT n.*,
|
|
157
193
|
(SELECT COUNT(*) FROM edges WHERE source_id = n.id) as out_edges,
|
|
158
194
|
(SELECT COUNT(*) FROM edges WHERE target_id = n.id) as in_edges
|
|
@@ -163,9 +199,10 @@ export function moduleMapData(customDbPath, limit = 20) {
|
|
|
163
199
|
AND n.file NOT LIKE '%__test__%'
|
|
164
200
|
ORDER BY (SELECT COUNT(*) FROM edges WHERE target_id = n.id) DESC
|
|
165
201
|
LIMIT ?
|
|
166
|
-
`)
|
|
202
|
+
`)
|
|
203
|
+
.all(limit);
|
|
167
204
|
|
|
168
|
-
const topNodes = nodes.map(n => ({
|
|
205
|
+
const topNodes = nodes.map((n) => ({
|
|
169
206
|
file: n.file,
|
|
170
207
|
dir: path.dirname(n.file) || '.',
|
|
171
208
|
inEdges: n.in_edges,
|
|
@@ -182,27 +219,38 @@ export function moduleMapData(customDbPath, limit = 20) {
|
|
|
182
219
|
|
|
183
220
|
export function fileDepsData(file, customDbPath) {
|
|
184
221
|
const db = openReadonlyOrFail(customDbPath);
|
|
185
|
-
const fileNodes = db
|
|
186
|
-
|
|
222
|
+
const fileNodes = db
|
|
223
|
+
.prepare(`SELECT * FROM nodes WHERE file LIKE ? AND kind = 'file'`)
|
|
224
|
+
.all(`%${file}%`);
|
|
225
|
+
if (fileNodes.length === 0) {
|
|
226
|
+
db.close();
|
|
227
|
+
return { file, results: [] };
|
|
228
|
+
}
|
|
187
229
|
|
|
188
|
-
const results = fileNodes.map(fn => {
|
|
189
|
-
const importsTo = db
|
|
230
|
+
const results = fileNodes.map((fn) => {
|
|
231
|
+
const importsTo = db
|
|
232
|
+
.prepare(`
|
|
190
233
|
SELECT n.file, e.kind as edge_kind FROM edges e JOIN nodes n ON e.target_id = n.id
|
|
191
234
|
WHERE e.source_id = ? AND e.kind IN ('imports', 'imports-type')
|
|
192
|
-
`)
|
|
235
|
+
`)
|
|
236
|
+
.all(fn.id);
|
|
193
237
|
|
|
194
|
-
const importedBy = db
|
|
238
|
+
const importedBy = db
|
|
239
|
+
.prepare(`
|
|
195
240
|
SELECT n.file, e.kind as edge_kind FROM edges e JOIN nodes n ON e.source_id = n.id
|
|
196
241
|
WHERE e.target_id = ? AND e.kind IN ('imports', 'imports-type')
|
|
197
|
-
`)
|
|
242
|
+
`)
|
|
243
|
+
.all(fn.id);
|
|
198
244
|
|
|
199
|
-
const defs = db
|
|
245
|
+
const defs = db
|
|
246
|
+
.prepare(`SELECT * FROM nodes WHERE file = ? AND kind != 'file' ORDER BY line`)
|
|
247
|
+
.all(fn.file);
|
|
200
248
|
|
|
201
249
|
return {
|
|
202
250
|
file: fn.file,
|
|
203
|
-
imports: importsTo.map(i => ({ file: i.file, typeOnly: i.edge_kind === 'imports-type' })),
|
|
204
|
-
importedBy: importedBy.map(i => ({ file: i.file })),
|
|
205
|
-
definitions: defs.map(d => ({ name: d.name, kind: d.kind, line: d.line })),
|
|
251
|
+
imports: importsTo.map((i) => ({ file: i.file, typeOnly: i.edge_kind === 'imports-type' })),
|
|
252
|
+
importedBy: importedBy.map((i) => ({ file: i.file })),
|
|
253
|
+
definitions: defs.map((d) => ({ name: d.name, kind: d.kind, line: d.line })),
|
|
206
254
|
};
|
|
207
255
|
});
|
|
208
256
|
|
|
@@ -215,70 +263,94 @@ export function fnDepsData(name, customDbPath, opts = {}) {
|
|
|
215
263
|
const depth = opts.depth || 3;
|
|
216
264
|
const noTests = opts.noTests || false;
|
|
217
265
|
|
|
218
|
-
let nodes = db
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
266
|
+
let nodes = db
|
|
267
|
+
.prepare(
|
|
268
|
+
`SELECT * FROM nodes WHERE name LIKE ? AND kind IN ('function', 'method', 'class') ORDER BY file, line`,
|
|
269
|
+
)
|
|
270
|
+
.all(`%${name}%`);
|
|
271
|
+
if (noTests) nodes = nodes.filter((n) => !isTestFile(n.file));
|
|
272
|
+
if (nodes.length === 0) {
|
|
273
|
+
db.close();
|
|
274
|
+
return { name, results: [] };
|
|
275
|
+
}
|
|
223
276
|
|
|
224
|
-
const results = nodes.map(node => {
|
|
225
|
-
const callees = db
|
|
277
|
+
const results = nodes.map((node) => {
|
|
278
|
+
const callees = db
|
|
279
|
+
.prepare(`
|
|
226
280
|
SELECT n.name, n.kind, n.file, n.line, e.kind as edge_kind
|
|
227
281
|
FROM edges e JOIN nodes n ON e.target_id = n.id
|
|
228
282
|
WHERE e.source_id = ? AND e.kind = 'calls'
|
|
229
|
-
`)
|
|
230
|
-
|
|
283
|
+
`)
|
|
284
|
+
.all(node.id);
|
|
285
|
+
const filteredCallees = noTests ? callees.filter((c) => !isTestFile(c.file)) : callees;
|
|
231
286
|
|
|
232
|
-
let callers = db
|
|
287
|
+
let callers = db
|
|
288
|
+
.prepare(`
|
|
233
289
|
SELECT n.name, n.kind, n.file, n.line, e.kind as edge_kind
|
|
234
290
|
FROM edges e JOIN nodes n ON e.source_id = n.id
|
|
235
291
|
WHERE e.target_id = ? AND e.kind = 'calls'
|
|
236
|
-
`)
|
|
292
|
+
`)
|
|
293
|
+
.all(node.id);
|
|
237
294
|
|
|
238
295
|
if (node.kind === 'method' && node.name.includes('.')) {
|
|
239
296
|
const methodName = node.name.split('.').pop();
|
|
240
297
|
const relatedMethods = resolveMethodViaHierarchy(db, methodName);
|
|
241
298
|
for (const rm of relatedMethods) {
|
|
242
299
|
if (rm.id === node.id) continue;
|
|
243
|
-
const extraCallers = db
|
|
300
|
+
const extraCallers = db
|
|
301
|
+
.prepare(`
|
|
244
302
|
SELECT n.name, n.kind, n.file, n.line, e.kind as edge_kind
|
|
245
303
|
FROM edges e JOIN nodes n ON e.source_id = n.id
|
|
246
304
|
WHERE e.target_id = ? AND e.kind = 'calls'
|
|
247
|
-
`)
|
|
248
|
-
|
|
305
|
+
`)
|
|
306
|
+
.all(rm.id);
|
|
307
|
+
callers.push(...extraCallers.map((c) => ({ ...c, viaHierarchy: rm.name })));
|
|
249
308
|
}
|
|
250
309
|
}
|
|
251
|
-
if (noTests) callers = callers.filter(c => !isTestFile(c.file));
|
|
310
|
+
if (noTests) callers = callers.filter((c) => !isTestFile(c.file));
|
|
252
311
|
|
|
253
312
|
// Transitive callers
|
|
254
313
|
const transitiveCallers = {};
|
|
255
314
|
if (depth > 1) {
|
|
256
315
|
const visited = new Set([node.id]);
|
|
257
|
-
let frontier = callers
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
316
|
+
let frontier = callers
|
|
317
|
+
.map((c) => {
|
|
318
|
+
const row = db
|
|
319
|
+
.prepare('SELECT id FROM nodes WHERE name = ? AND kind = ? AND file = ? AND line = ?')
|
|
320
|
+
.get(c.name, c.kind, c.file, c.line);
|
|
321
|
+
return row ? { ...c, id: row.id } : null;
|
|
322
|
+
})
|
|
323
|
+
.filter(Boolean);
|
|
261
324
|
|
|
262
325
|
for (let d = 2; d <= depth; d++) {
|
|
263
326
|
const nextFrontier = [];
|
|
264
327
|
for (const f of frontier) {
|
|
265
328
|
if (visited.has(f.id)) continue;
|
|
266
329
|
visited.add(f.id);
|
|
267
|
-
const upstream = db
|
|
330
|
+
const upstream = db
|
|
331
|
+
.prepare(`
|
|
268
332
|
SELECT n.name, n.kind, n.file, n.line
|
|
269
333
|
FROM edges e JOIN nodes n ON e.source_id = n.id
|
|
270
334
|
WHERE e.target_id = ? AND e.kind = 'calls'
|
|
271
|
-
`)
|
|
335
|
+
`)
|
|
336
|
+
.all(f.id);
|
|
272
337
|
for (const u of upstream) {
|
|
273
338
|
if (noTests && isTestFile(u.file)) continue;
|
|
274
|
-
const uid = db
|
|
339
|
+
const uid = db
|
|
340
|
+
.prepare('SELECT id FROM nodes WHERE name = ? AND kind = ? AND file = ? AND line = ?')
|
|
341
|
+
.get(u.name, u.kind, u.file, u.line)?.id;
|
|
275
342
|
if (uid && !visited.has(uid)) {
|
|
276
343
|
nextFrontier.push({ ...u, id: uid });
|
|
277
344
|
}
|
|
278
345
|
}
|
|
279
346
|
}
|
|
280
347
|
if (nextFrontier.length > 0) {
|
|
281
|
-
transitiveCallers[d] = nextFrontier.map(n => ({
|
|
348
|
+
transitiveCallers[d] = nextFrontier.map((n) => ({
|
|
349
|
+
name: n.name,
|
|
350
|
+
kind: n.kind,
|
|
351
|
+
file: n.file,
|
|
352
|
+
line: n.line,
|
|
353
|
+
}));
|
|
282
354
|
}
|
|
283
355
|
frontier = nextFrontier;
|
|
284
356
|
if (frontier.length === 0) break;
|
|
@@ -286,9 +358,23 @@ export function fnDepsData(name, customDbPath, opts = {}) {
|
|
|
286
358
|
}
|
|
287
359
|
|
|
288
360
|
return {
|
|
289
|
-
name: node.name,
|
|
290
|
-
|
|
291
|
-
|
|
361
|
+
name: node.name,
|
|
362
|
+
kind: node.kind,
|
|
363
|
+
file: node.file,
|
|
364
|
+
line: node.line,
|
|
365
|
+
callees: filteredCallees.map((c) => ({
|
|
366
|
+
name: c.name,
|
|
367
|
+
kind: c.kind,
|
|
368
|
+
file: c.file,
|
|
369
|
+
line: c.line,
|
|
370
|
+
})),
|
|
371
|
+
callers: callers.map((c) => ({
|
|
372
|
+
name: c.name,
|
|
373
|
+
kind: c.kind,
|
|
374
|
+
file: c.file,
|
|
375
|
+
line: c.line,
|
|
376
|
+
viaHierarchy: c.viaHierarchy || undefined,
|
|
377
|
+
})),
|
|
292
378
|
transitiveCallers,
|
|
293
379
|
};
|
|
294
380
|
});
|
|
@@ -302,13 +388,16 @@ export function fnImpactData(name, customDbPath, opts = {}) {
|
|
|
302
388
|
const maxDepth = opts.depth || 5;
|
|
303
389
|
const noTests = opts.noTests || false;
|
|
304
390
|
|
|
305
|
-
let nodes = db
|
|
306
|
-
`SELECT * FROM nodes WHERE name LIKE ? AND kind IN ('function', 'method', 'class')`
|
|
307
|
-
|
|
308
|
-
if (noTests) nodes = nodes.filter(n => !isTestFile(n.file));
|
|
309
|
-
if (nodes.length === 0) {
|
|
391
|
+
let nodes = db
|
|
392
|
+
.prepare(`SELECT * FROM nodes WHERE name LIKE ? AND kind IN ('function', 'method', 'class')`)
|
|
393
|
+
.all(`%${name}%`);
|
|
394
|
+
if (noTests) nodes = nodes.filter((n) => !isTestFile(n.file));
|
|
395
|
+
if (nodes.length === 0) {
|
|
396
|
+
db.close();
|
|
397
|
+
return { name, results: [] };
|
|
398
|
+
}
|
|
310
399
|
|
|
311
|
-
const results = nodes.slice(0, 3).map(node => {
|
|
400
|
+
const results = nodes.slice(0, 3).map((node) => {
|
|
312
401
|
const visited = new Set([node.id]);
|
|
313
402
|
const levels = {};
|
|
314
403
|
let frontier = [node.id];
|
|
@@ -316,11 +405,13 @@ export function fnImpactData(name, customDbPath, opts = {}) {
|
|
|
316
405
|
for (let d = 1; d <= maxDepth; d++) {
|
|
317
406
|
const nextFrontier = [];
|
|
318
407
|
for (const fid of frontier) {
|
|
319
|
-
const callers = db
|
|
408
|
+
const callers = db
|
|
409
|
+
.prepare(`
|
|
320
410
|
SELECT DISTINCT n.id, n.name, n.kind, n.file, n.line
|
|
321
411
|
FROM edges e JOIN nodes n ON e.source_id = n.id
|
|
322
412
|
WHERE e.target_id = ? AND e.kind = 'calls'
|
|
323
|
-
`)
|
|
413
|
+
`)
|
|
414
|
+
.all(fid);
|
|
324
415
|
for (const c of callers) {
|
|
325
416
|
if (!visited.has(c.id) && (!noTests || !isTestFile(c.file))) {
|
|
326
417
|
visited.add(c.id);
|
|
@@ -335,7 +426,10 @@ export function fnImpactData(name, customDbPath, opts = {}) {
|
|
|
335
426
|
}
|
|
336
427
|
|
|
337
428
|
return {
|
|
338
|
-
name: node.name,
|
|
429
|
+
name: node.name,
|
|
430
|
+
kind: node.kind,
|
|
431
|
+
file: node.file,
|
|
432
|
+
line: node.line,
|
|
339
433
|
levels,
|
|
340
434
|
totalDependents: visited.size - 1,
|
|
341
435
|
};
|
|
@@ -366,36 +460,48 @@ export function diffImpactData(customDbPath, opts = {}) {
|
|
|
366
460
|
diffOutput = execFileSync('git', args, {
|
|
367
461
|
cwd: repoRoot,
|
|
368
462
|
encoding: 'utf-8',
|
|
369
|
-
maxBuffer: 10 * 1024 * 1024
|
|
463
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
370
464
|
});
|
|
371
465
|
} catch (e) {
|
|
372
466
|
db.close();
|
|
373
467
|
return { error: `Failed to run git diff: ${e.message}` };
|
|
374
468
|
}
|
|
375
469
|
|
|
376
|
-
if (!diffOutput.trim()) {
|
|
470
|
+
if (!diffOutput.trim()) {
|
|
471
|
+
db.close();
|
|
472
|
+
return { changedFiles: 0, affectedFunctions: [], affectedFiles: [], summary: null };
|
|
473
|
+
}
|
|
377
474
|
|
|
378
475
|
const changedRanges = new Map();
|
|
379
476
|
let currentFile = null;
|
|
380
477
|
for (const line of diffOutput.split('\n')) {
|
|
381
478
|
const fileMatch = line.match(/^\+\+\+ b\/(.+)/);
|
|
382
|
-
if (fileMatch) {
|
|
479
|
+
if (fileMatch) {
|
|
480
|
+
currentFile = fileMatch[1];
|
|
481
|
+
if (!changedRanges.has(currentFile)) changedRanges.set(currentFile, []);
|
|
482
|
+
continue;
|
|
483
|
+
}
|
|
383
484
|
const hunkMatch = line.match(/^@@ .+ \+(\d+)(?:,(\d+))? @@/);
|
|
384
485
|
if (hunkMatch && currentFile) {
|
|
385
|
-
const start = parseInt(hunkMatch[1]);
|
|
386
|
-
const count = parseInt(hunkMatch[2] || '1');
|
|
486
|
+
const start = parseInt(hunkMatch[1], 10);
|
|
487
|
+
const count = parseInt(hunkMatch[2] || '1', 10);
|
|
387
488
|
changedRanges.get(currentFile).push({ start, end: start + count - 1 });
|
|
388
489
|
}
|
|
389
490
|
}
|
|
390
491
|
|
|
391
|
-
if (changedRanges.size === 0) {
|
|
492
|
+
if (changedRanges.size === 0) {
|
|
493
|
+
db.close();
|
|
494
|
+
return { changedFiles: 0, affectedFunctions: [], affectedFiles: [], summary: null };
|
|
495
|
+
}
|
|
392
496
|
|
|
393
497
|
const affectedFunctions = [];
|
|
394
498
|
for (const [file, ranges] of changedRanges) {
|
|
395
499
|
if (noTests && isTestFile(file)) continue;
|
|
396
|
-
const defs = db
|
|
397
|
-
|
|
398
|
-
|
|
500
|
+
const defs = db
|
|
501
|
+
.prepare(
|
|
502
|
+
`SELECT * FROM nodes WHERE file = ? AND kind IN ('function', 'method', 'class') ORDER BY line`,
|
|
503
|
+
)
|
|
504
|
+
.all(file);
|
|
399
505
|
for (let i = 0; i < defs.length; i++) {
|
|
400
506
|
const def = defs[i];
|
|
401
507
|
const endLine = def.end_line || (defs[i + 1] ? defs[i + 1].line - 1 : 999999);
|
|
@@ -409,18 +515,20 @@ export function diffImpactData(customDbPath, opts = {}) {
|
|
|
409
515
|
}
|
|
410
516
|
|
|
411
517
|
const allAffected = new Set();
|
|
412
|
-
const functionResults = affectedFunctions.map(fn => {
|
|
518
|
+
const functionResults = affectedFunctions.map((fn) => {
|
|
413
519
|
const visited = new Set([fn.id]);
|
|
414
520
|
let frontier = [fn.id];
|
|
415
521
|
let totalCallers = 0;
|
|
416
522
|
for (let d = 1; d <= maxDepth; d++) {
|
|
417
523
|
const nextFrontier = [];
|
|
418
524
|
for (const fid of frontier) {
|
|
419
|
-
const callers = db
|
|
525
|
+
const callers = db
|
|
526
|
+
.prepare(`
|
|
420
527
|
SELECT DISTINCT n.id, n.name, n.kind, n.file, n.line
|
|
421
528
|
FROM edges e JOIN nodes n ON e.source_id = n.id
|
|
422
529
|
WHERE e.target_id = ? AND e.kind = 'calls'
|
|
423
|
-
`)
|
|
530
|
+
`)
|
|
531
|
+
.all(fid);
|
|
424
532
|
for (const c of callers) {
|
|
425
533
|
if (!visited.has(c.id) && (!noTests || !isTestFile(c.file))) {
|
|
426
534
|
visited.add(c.id);
|
|
@@ -433,7 +541,13 @@ export function diffImpactData(customDbPath, opts = {}) {
|
|
|
433
541
|
frontier = nextFrontier;
|
|
434
542
|
if (frontier.length === 0) break;
|
|
435
543
|
}
|
|
436
|
-
return {
|
|
544
|
+
return {
|
|
545
|
+
name: fn.name,
|
|
546
|
+
kind: fn.kind,
|
|
547
|
+
file: fn.file,
|
|
548
|
+
line: fn.line,
|
|
549
|
+
transitiveCallers: totalCallers,
|
|
550
|
+
};
|
|
437
551
|
});
|
|
438
552
|
|
|
439
553
|
const affectedFiles = new Set();
|
|
@@ -452,24 +566,62 @@ export function diffImpactData(customDbPath, opts = {}) {
|
|
|
452
566
|
};
|
|
453
567
|
}
|
|
454
568
|
|
|
569
|
+
export function listFunctionsData(customDbPath, opts = {}) {
|
|
570
|
+
const db = openReadonlyOrFail(customDbPath);
|
|
571
|
+
const noTests = opts.noTests || false;
|
|
572
|
+
const kinds = ['function', 'method', 'class'];
|
|
573
|
+
const placeholders = kinds.map(() => '?').join(', ');
|
|
574
|
+
|
|
575
|
+
const conditions = [`kind IN (${placeholders})`];
|
|
576
|
+
const params = [...kinds];
|
|
577
|
+
|
|
578
|
+
if (opts.file) {
|
|
579
|
+
conditions.push('file LIKE ?');
|
|
580
|
+
params.push(`%${opts.file}%`);
|
|
581
|
+
}
|
|
582
|
+
if (opts.pattern) {
|
|
583
|
+
conditions.push('name LIKE ?');
|
|
584
|
+
params.push(`%${opts.pattern}%`);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
let rows = db
|
|
588
|
+
.prepare(
|
|
589
|
+
`SELECT name, kind, file, line FROM nodes WHERE ${conditions.join(' AND ')} ORDER BY file, line`,
|
|
590
|
+
)
|
|
591
|
+
.all(...params);
|
|
592
|
+
|
|
593
|
+
if (noTests) rows = rows.filter((r) => !isTestFile(r.file));
|
|
594
|
+
|
|
595
|
+
db.close();
|
|
596
|
+
return { count: rows.length, functions: rows };
|
|
597
|
+
}
|
|
598
|
+
|
|
455
599
|
// ─── Human-readable output (original formatting) ───────────────────────
|
|
456
600
|
|
|
457
601
|
export function queryName(name, customDbPath, opts = {}) {
|
|
458
602
|
const data = queryNameData(name, customDbPath);
|
|
459
|
-
if (opts.json) {
|
|
460
|
-
|
|
603
|
+
if (opts.json) {
|
|
604
|
+
console.log(JSON.stringify(data, null, 2));
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
if (data.results.length === 0) {
|
|
608
|
+
console.log(`No results for "${name}"`);
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
461
611
|
|
|
462
612
|
console.log(`\nResults for "${name}":\n`);
|
|
463
613
|
for (const r of data.results) {
|
|
464
614
|
console.log(` ${kindIcon(r.kind)} ${r.name} (${r.kind}) -- ${r.file}:${r.line}`);
|
|
465
615
|
if (r.callees.length > 0) {
|
|
466
616
|
console.log(` -> calls/uses:`);
|
|
467
|
-
for (const c of r.callees.slice(0, 15))
|
|
617
|
+
for (const c of r.callees.slice(0, 15))
|
|
618
|
+
console.log(` -> ${c.name} (${c.edgeKind}) ${c.file}:${c.line}`);
|
|
468
619
|
if (r.callees.length > 15) console.log(` ... and ${r.callees.length - 15} more`);
|
|
469
620
|
}
|
|
470
621
|
if (r.callers.length > 0) {
|
|
471
622
|
console.log(` <- called by:`);
|
|
472
|
-
for (const c of r.callers.slice(0, 15))
|
|
623
|
+
for (const c of r.callers.slice(0, 15))
|
|
624
|
+
console.log(` <- ${c.name} (${c.edgeKind}) ${c.file}:${c.line}`);
|
|
473
625
|
if (r.callers.length > 15) console.log(` ... and ${r.callers.length - 15} more`);
|
|
474
626
|
}
|
|
475
627
|
console.log();
|
|
@@ -478,8 +630,14 @@ export function queryName(name, customDbPath, opts = {}) {
|
|
|
478
630
|
|
|
479
631
|
export function impactAnalysis(file, customDbPath, opts = {}) {
|
|
480
632
|
const data = impactAnalysisData(file, customDbPath);
|
|
481
|
-
if (opts.json) {
|
|
482
|
-
|
|
633
|
+
if (opts.json) {
|
|
634
|
+
console.log(JSON.stringify(data, null, 2));
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
if (data.sources.length === 0) {
|
|
638
|
+
console.log(`No file matching "${file}" in graph`);
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
483
641
|
|
|
484
642
|
console.log(`\nImpact analysis for files matching "${file}":\n`);
|
|
485
643
|
for (const s of data.sources) console.log(` # ${s} (source)`);
|
|
@@ -490,8 +648,11 @@ export function impactAnalysis(file, customDbPath, opts = {}) {
|
|
|
490
648
|
} else {
|
|
491
649
|
for (const level of Object.keys(levels).sort((a, b) => a - b)) {
|
|
492
650
|
const nodes = levels[level];
|
|
493
|
-
console.log(
|
|
494
|
-
|
|
651
|
+
console.log(
|
|
652
|
+
`\n ${'--'.repeat(parseInt(level, 10))} Level ${level} (${nodes.length} files):`,
|
|
653
|
+
);
|
|
654
|
+
for (const n of nodes.slice(0, 30))
|
|
655
|
+
console.log(` ${' '.repeat(parseInt(level, 10))}^ ${n.file}`);
|
|
495
656
|
if (nodes.length > 30) console.log(` ... and ${nodes.length - 30} more`);
|
|
496
657
|
}
|
|
497
658
|
}
|
|
@@ -500,7 +661,10 @@ export function impactAnalysis(file, customDbPath, opts = {}) {
|
|
|
500
661
|
|
|
501
662
|
export function moduleMap(customDbPath, limit = 20, opts = {}) {
|
|
502
663
|
const data = moduleMapData(customDbPath, limit);
|
|
503
|
-
if (opts.json) {
|
|
664
|
+
if (opts.json) {
|
|
665
|
+
console.log(JSON.stringify(data, null, 2));
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
504
668
|
|
|
505
669
|
console.log(`\nModule map (top ${limit} most-connected nodes):\n`);
|
|
506
670
|
const dirs = new Map();
|
|
@@ -513,16 +677,26 @@ export function moduleMap(customDbPath, limit = 20, opts = {}) {
|
|
|
513
677
|
for (const f of files) {
|
|
514
678
|
const total = f.inEdges + f.outEdges;
|
|
515
679
|
const bar = '#'.repeat(Math.min(total, 40));
|
|
516
|
-
console.log(
|
|
680
|
+
console.log(
|
|
681
|
+
` ${path.basename(f.file).padEnd(35)} <-${String(f.inEdges).padStart(3)} ->${String(f.outEdges).padStart(3)} ${bar}`,
|
|
682
|
+
);
|
|
517
683
|
}
|
|
518
684
|
}
|
|
519
|
-
console.log(
|
|
685
|
+
console.log(
|
|
686
|
+
`\n Total: ${data.stats.totalFiles} files, ${data.stats.totalNodes} symbols, ${data.stats.totalEdges} edges\n`,
|
|
687
|
+
);
|
|
520
688
|
}
|
|
521
689
|
|
|
522
690
|
export function fileDeps(file, customDbPath, opts = {}) {
|
|
523
691
|
const data = fileDepsData(file, customDbPath);
|
|
524
|
-
if (opts.json) {
|
|
525
|
-
|
|
692
|
+
if (opts.json) {
|
|
693
|
+
console.log(JSON.stringify(data, null, 2));
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
if (data.results.length === 0) {
|
|
697
|
+
console.log(`No file matching "${file}" in graph`);
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
526
700
|
|
|
527
701
|
for (const r of data.results) {
|
|
528
702
|
console.log(`\n# ${r.file}\n`);
|
|
@@ -535,7 +709,8 @@ export function fileDeps(file, customDbPath, opts = {}) {
|
|
|
535
709
|
for (const i of r.importedBy) console.log(` <- ${i.file}`);
|
|
536
710
|
if (r.definitions.length > 0) {
|
|
537
711
|
console.log(`\n Definitions (${r.definitions.length}):`);
|
|
538
|
-
for (const d of r.definitions.slice(0, 30))
|
|
712
|
+
for (const d of r.definitions.slice(0, 30))
|
|
713
|
+
console.log(` ${kindIcon(d.kind)} ${d.name} :${d.line}`);
|
|
539
714
|
if (r.definitions.length > 30) console.log(` ... and ${r.definitions.length - 30} more`);
|
|
540
715
|
}
|
|
541
716
|
console.log();
|
|
@@ -544,14 +719,21 @@ export function fileDeps(file, customDbPath, opts = {}) {
|
|
|
544
719
|
|
|
545
720
|
export function fnDeps(name, customDbPath, opts = {}) {
|
|
546
721
|
const data = fnDepsData(name, customDbPath, opts);
|
|
547
|
-
if (opts.json) {
|
|
548
|
-
|
|
722
|
+
if (opts.json) {
|
|
723
|
+
console.log(JSON.stringify(data, null, 2));
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
if (data.results.length === 0) {
|
|
727
|
+
console.log(`No function/method/class matching "${name}"`);
|
|
728
|
+
return;
|
|
729
|
+
}
|
|
549
730
|
|
|
550
731
|
for (const r of data.results) {
|
|
551
732
|
console.log(`\n${kindIcon(r.kind)} ${r.name} (${r.kind}) -- ${r.file}:${r.line}\n`);
|
|
552
733
|
if (r.callees.length > 0) {
|
|
553
734
|
console.log(` -> Calls (${r.callees.length}):`);
|
|
554
|
-
for (const c of r.callees)
|
|
735
|
+
for (const c of r.callees)
|
|
736
|
+
console.log(` -> ${kindIcon(c.kind)} ${c.name} ${c.file}:${c.line}`);
|
|
555
737
|
}
|
|
556
738
|
if (r.callers.length > 0) {
|
|
557
739
|
console.log(`\n <- Called by (${r.callers.length}):`);
|
|
@@ -561,8 +743,13 @@ export function fnDeps(name, customDbPath, opts = {}) {
|
|
|
561
743
|
}
|
|
562
744
|
}
|
|
563
745
|
for (const [d, fns] of Object.entries(r.transitiveCallers)) {
|
|
564
|
-
console.log(
|
|
565
|
-
|
|
746
|
+
console.log(
|
|
747
|
+
`\n ${'<-'.repeat(parseInt(d, 10))} Transitive callers (depth ${d}, ${fns.length}):`,
|
|
748
|
+
);
|
|
749
|
+
for (const n of fns.slice(0, 20))
|
|
750
|
+
console.log(
|
|
751
|
+
` ${' '.repeat(parseInt(d, 10) - 1)}<- ${kindIcon(n.kind)} ${n.name} ${n.file}:${n.line}`,
|
|
752
|
+
);
|
|
566
753
|
if (fns.length > 20) console.log(` ... and ${fns.length - 20} more`);
|
|
567
754
|
}
|
|
568
755
|
if (r.callees.length === 0 && r.callers.length === 0) {
|
|
@@ -574,8 +761,14 @@ export function fnDeps(name, customDbPath, opts = {}) {
|
|
|
574
761
|
|
|
575
762
|
export function fnImpact(name, customDbPath, opts = {}) {
|
|
576
763
|
const data = fnImpactData(name, customDbPath, opts);
|
|
577
|
-
if (opts.json) {
|
|
578
|
-
|
|
764
|
+
if (opts.json) {
|
|
765
|
+
console.log(JSON.stringify(data, null, 2));
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
if (data.results.length === 0) {
|
|
769
|
+
console.log(`No function/method/class matching "${name}"`);
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
579
772
|
|
|
580
773
|
for (const r of data.results) {
|
|
581
774
|
console.log(`\nFunction impact: ${kindIcon(r.kind)} ${r.name} -- ${r.file}:${r.line}\n`);
|
|
@@ -583,9 +776,10 @@ export function fnImpact(name, customDbPath, opts = {}) {
|
|
|
583
776
|
console.log(` No callers found.`);
|
|
584
777
|
} else {
|
|
585
778
|
for (const [level, fns] of Object.entries(r.levels).sort((a, b) => a[0] - b[0])) {
|
|
586
|
-
const l = parseInt(level);
|
|
779
|
+
const l = parseInt(level, 10);
|
|
587
780
|
console.log(` ${'--'.repeat(l)} Level ${level} (${fns.length} functions):`);
|
|
588
|
-
for (const f of fns.slice(0, 20))
|
|
781
|
+
for (const f of fns.slice(0, 20))
|
|
782
|
+
console.log(` ${' '.repeat(l)}^ ${kindIcon(f.kind)} ${f.name} ${f.file}:${f.line}`);
|
|
589
783
|
if (fns.length > 20) console.log(` ... and ${fns.length - 20} more`);
|
|
590
784
|
}
|
|
591
785
|
}
|
|
@@ -595,11 +789,22 @@ export function fnImpact(name, customDbPath, opts = {}) {
|
|
|
595
789
|
|
|
596
790
|
export function diffImpact(customDbPath, opts = {}) {
|
|
597
791
|
const data = diffImpactData(customDbPath, opts);
|
|
598
|
-
if (opts.json) {
|
|
599
|
-
|
|
600
|
-
|
|
792
|
+
if (opts.json) {
|
|
793
|
+
console.log(JSON.stringify(data, null, 2));
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
796
|
+
if (data.error) {
|
|
797
|
+
console.log(data.error);
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
800
|
+
if (data.changedFiles === 0) {
|
|
801
|
+
console.log('No changes detected.');
|
|
802
|
+
return;
|
|
803
|
+
}
|
|
601
804
|
if (data.affectedFunctions.length === 0) {
|
|
602
|
-
console.log(
|
|
805
|
+
console.log(
|
|
806
|
+
' No function-level changes detected (changes may be in imports, types, or config).',
|
|
807
|
+
);
|
|
603
808
|
return;
|
|
604
809
|
}
|
|
605
810
|
|
|
@@ -610,7 +815,8 @@ export function diffImpact(customDbPath, opts = {}) {
|
|
|
610
815
|
if (fn.transitiveCallers > 0) console.log(` ^ ${fn.transitiveCallers} transitive callers`);
|
|
611
816
|
}
|
|
612
817
|
if (data.summary) {
|
|
613
|
-
console.log(
|
|
818
|
+
console.log(
|
|
819
|
+
`\n Summary: ${data.summary.functionsChanged} functions changed -> ${data.summary.callersAffected} callers affected across ${data.summary.filesAffected} files\n`,
|
|
820
|
+
);
|
|
614
821
|
}
|
|
615
822
|
}
|
|
616
|
-
|