@optave/codegraph 2.1.1-dev.3c12b64 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +49 -31
- package/package.json +5 -5
- package/src/builder.js +238 -33
- package/src/cli.js +93 -9
- package/src/cycles.js +13 -1
- package/src/db.js +4 -0
- package/src/export.js +20 -7
- package/src/extractors/csharp.js +6 -1
- package/src/extractors/go.js +6 -1
- package/src/extractors/java.js +4 -1
- package/src/extractors/javascript.js +145 -5
- package/src/extractors/php.js +8 -2
- package/src/extractors/python.js +8 -1
- package/src/extractors/ruby.js +4 -1
- package/src/extractors/rust.js +12 -2
- package/src/index.js +6 -0
- package/src/journal.js +109 -0
- package/src/mcp.js +131 -7
- package/src/parser.js +1 -0
- package/src/queries.js +1143 -38
- package/src/structure.js +21 -7
- package/src/watcher.js +25 -0
package/src/cli.js
CHANGED
|
@@ -11,7 +11,10 @@ import { buildEmbeddings, MODELS, search } from './embedder.js';
|
|
|
11
11
|
import { exportDOT, exportJSON, exportMermaid } from './export.js';
|
|
12
12
|
import { setVerbose } from './logger.js';
|
|
13
13
|
import {
|
|
14
|
+
ALL_SYMBOL_KINDS,
|
|
15
|
+
context,
|
|
14
16
|
diffImpact,
|
|
17
|
+
explain,
|
|
15
18
|
fileDeps,
|
|
16
19
|
fnDeps,
|
|
17
20
|
fnImpact,
|
|
@@ -19,6 +22,7 @@ import {
|
|
|
19
22
|
moduleMap,
|
|
20
23
|
queryName,
|
|
21
24
|
stats,
|
|
25
|
+
where,
|
|
22
26
|
} from './queries.js';
|
|
23
27
|
import {
|
|
24
28
|
listRepos,
|
|
@@ -58,18 +62,20 @@ program
|
|
|
58
62
|
.command('query <name>')
|
|
59
63
|
.description('Find a function/class, show callers and callees')
|
|
60
64
|
.option('-d, --db <path>', 'Path to graph.db')
|
|
65
|
+
.option('-T, --no-tests', 'Exclude test/spec files from results')
|
|
61
66
|
.option('-j, --json', 'Output as JSON')
|
|
62
67
|
.action((name, opts) => {
|
|
63
|
-
queryName(name, opts.db, { json: opts.json });
|
|
68
|
+
queryName(name, opts.db, { noTests: !opts.tests, json: opts.json });
|
|
64
69
|
});
|
|
65
70
|
|
|
66
71
|
program
|
|
67
72
|
.command('impact <file>')
|
|
68
73
|
.description('Show what depends on this file (transitive)')
|
|
69
74
|
.option('-d, --db <path>', 'Path to graph.db')
|
|
75
|
+
.option('-T, --no-tests', 'Exclude test/spec files from results')
|
|
70
76
|
.option('-j, --json', 'Output as JSON')
|
|
71
77
|
.action((file, opts) => {
|
|
72
|
-
impactAnalysis(file, opts.db, { json: opts.json });
|
|
78
|
+
impactAnalysis(file, opts.db, { noTests: !opts.tests, json: opts.json });
|
|
73
79
|
});
|
|
74
80
|
|
|
75
81
|
program
|
|
@@ -77,27 +83,30 @@ program
|
|
|
77
83
|
.description('High-level module overview with most-connected nodes')
|
|
78
84
|
.option('-d, --db <path>', 'Path to graph.db')
|
|
79
85
|
.option('-n, --limit <number>', 'Number of top nodes', '20')
|
|
86
|
+
.option('-T, --no-tests', 'Exclude test/spec files from results')
|
|
80
87
|
.option('-j, --json', 'Output as JSON')
|
|
81
88
|
.action((opts) => {
|
|
82
|
-
moduleMap(opts.db, parseInt(opts.limit, 10), { json: opts.json });
|
|
89
|
+
moduleMap(opts.db, parseInt(opts.limit, 10), { noTests: !opts.tests, json: opts.json });
|
|
83
90
|
});
|
|
84
91
|
|
|
85
92
|
program
|
|
86
93
|
.command('stats')
|
|
87
94
|
.description('Show graph health overview: nodes, edges, languages, cycles, hotspots, embeddings')
|
|
88
95
|
.option('-d, --db <path>', 'Path to graph.db')
|
|
96
|
+
.option('-T, --no-tests', 'Exclude test/spec files from results')
|
|
89
97
|
.option('-j, --json', 'Output as JSON')
|
|
90
98
|
.action((opts) => {
|
|
91
|
-
stats(opts.db, { json: opts.json });
|
|
99
|
+
stats(opts.db, { noTests: !opts.tests, json: opts.json });
|
|
92
100
|
});
|
|
93
101
|
|
|
94
102
|
program
|
|
95
103
|
.command('deps <file>')
|
|
96
104
|
.description('Show what this file imports and what imports it')
|
|
97
105
|
.option('-d, --db <path>', 'Path to graph.db')
|
|
106
|
+
.option('-T, --no-tests', 'Exclude test/spec files from results')
|
|
98
107
|
.option('-j, --json', 'Output as JSON')
|
|
99
108
|
.action((file, opts) => {
|
|
100
|
-
fileDeps(file, opts.db, { json: opts.json });
|
|
109
|
+
fileDeps(file, opts.db, { noTests: !opts.tests, json: opts.json });
|
|
101
110
|
});
|
|
102
111
|
|
|
103
112
|
program
|
|
@@ -105,11 +114,19 @@ program
|
|
|
105
114
|
.description('Function-level dependencies: callers, callees, and transitive call chain')
|
|
106
115
|
.option('-d, --db <path>', 'Path to graph.db')
|
|
107
116
|
.option('--depth <n>', 'Transitive caller depth', '3')
|
|
117
|
+
.option('-f, --file <path>', 'Scope search to functions in this file (partial match)')
|
|
118
|
+
.option('-k, --kind <kind>', 'Filter to a specific symbol kind')
|
|
108
119
|
.option('-T, --no-tests', 'Exclude test/spec files from results')
|
|
109
120
|
.option('-j, --json', 'Output as JSON')
|
|
110
121
|
.action((name, opts) => {
|
|
122
|
+
if (opts.kind && !ALL_SYMBOL_KINDS.includes(opts.kind)) {
|
|
123
|
+
console.error(`Invalid kind "${opts.kind}". Valid: ${ALL_SYMBOL_KINDS.join(', ')}`);
|
|
124
|
+
process.exit(1);
|
|
125
|
+
}
|
|
111
126
|
fnDeps(name, opts.db, {
|
|
112
127
|
depth: parseInt(opts.depth, 10),
|
|
128
|
+
file: opts.file,
|
|
129
|
+
kind: opts.kind,
|
|
113
130
|
noTests: !opts.tests,
|
|
114
131
|
json: opts.json,
|
|
115
132
|
});
|
|
@@ -120,16 +137,77 @@ program
|
|
|
120
137
|
.description('Function-level impact: what functions break if this one changes')
|
|
121
138
|
.option('-d, --db <path>', 'Path to graph.db')
|
|
122
139
|
.option('--depth <n>', 'Max transitive depth', '5')
|
|
140
|
+
.option('-f, --file <path>', 'Scope search to functions in this file (partial match)')
|
|
141
|
+
.option('-k, --kind <kind>', 'Filter to a specific symbol kind')
|
|
123
142
|
.option('-T, --no-tests', 'Exclude test/spec files from results')
|
|
124
143
|
.option('-j, --json', 'Output as JSON')
|
|
125
144
|
.action((name, opts) => {
|
|
145
|
+
if (opts.kind && !ALL_SYMBOL_KINDS.includes(opts.kind)) {
|
|
146
|
+
console.error(`Invalid kind "${opts.kind}". Valid: ${ALL_SYMBOL_KINDS.join(', ')}`);
|
|
147
|
+
process.exit(1);
|
|
148
|
+
}
|
|
126
149
|
fnImpact(name, opts.db, {
|
|
127
150
|
depth: parseInt(opts.depth, 10),
|
|
151
|
+
file: opts.file,
|
|
152
|
+
kind: opts.kind,
|
|
153
|
+
noTests: !opts.tests,
|
|
154
|
+
json: opts.json,
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
program
|
|
159
|
+
.command('context <name>')
|
|
160
|
+
.description('Full context for a function: source, deps, callers, tests, signature')
|
|
161
|
+
.option('-d, --db <path>', 'Path to graph.db')
|
|
162
|
+
.option('--depth <n>', 'Include callee source up to N levels deep', '0')
|
|
163
|
+
.option('-f, --file <path>', 'Scope search to functions in this file (partial match)')
|
|
164
|
+
.option('-k, --kind <kind>', 'Filter to a specific symbol kind')
|
|
165
|
+
.option('--no-source', 'Metadata only (skip source extraction)')
|
|
166
|
+
.option('--include-tests', 'Include test source code')
|
|
167
|
+
.option('-T, --no-tests', 'Exclude test/spec files from results')
|
|
168
|
+
.option('-j, --json', 'Output as JSON')
|
|
169
|
+
.action((name, opts) => {
|
|
170
|
+
if (opts.kind && !ALL_SYMBOL_KINDS.includes(opts.kind)) {
|
|
171
|
+
console.error(`Invalid kind "${opts.kind}". Valid: ${ALL_SYMBOL_KINDS.join(', ')}`);
|
|
172
|
+
process.exit(1);
|
|
173
|
+
}
|
|
174
|
+
context(name, opts.db, {
|
|
175
|
+
depth: parseInt(opts.depth, 10),
|
|
176
|
+
file: opts.file,
|
|
177
|
+
kind: opts.kind,
|
|
178
|
+
noSource: !opts.source,
|
|
128
179
|
noTests: !opts.tests,
|
|
180
|
+
includeTests: opts.includeTests,
|
|
129
181
|
json: opts.json,
|
|
130
182
|
});
|
|
131
183
|
});
|
|
132
184
|
|
|
185
|
+
program
|
|
186
|
+
.command('explain <target>')
|
|
187
|
+
.description('Structural summary of a file or function (no LLM needed)')
|
|
188
|
+
.option('-d, --db <path>', 'Path to graph.db')
|
|
189
|
+
.option('-T, --no-tests', 'Exclude test/spec files from results')
|
|
190
|
+
.option('-j, --json', 'Output as JSON')
|
|
191
|
+
.action((target, opts) => {
|
|
192
|
+
explain(target, opts.db, { noTests: !opts.tests, json: opts.json });
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
program
|
|
196
|
+
.command('where [name]')
|
|
197
|
+
.description('Find where a symbol is defined and used (minimal, fast lookup)')
|
|
198
|
+
.option('-d, --db <path>', 'Path to graph.db')
|
|
199
|
+
.option('-f, --file <path>', 'File overview: list symbols, imports, exports')
|
|
200
|
+
.option('-T, --no-tests', 'Exclude test/spec files from results')
|
|
201
|
+
.option('-j, --json', 'Output as JSON')
|
|
202
|
+
.action((name, opts) => {
|
|
203
|
+
if (!name && !opts.file) {
|
|
204
|
+
console.error('Provide a symbol name or use --file <path>');
|
|
205
|
+
process.exit(1);
|
|
206
|
+
}
|
|
207
|
+
const target = opts.file || name;
|
|
208
|
+
where(target, opts.db, { file: !!opts.file, noTests: !opts.tests, json: opts.json });
|
|
209
|
+
});
|
|
210
|
+
|
|
133
211
|
program
|
|
134
212
|
.command('diff-impact [ref]')
|
|
135
213
|
.description('Show impact of git changes (unstaged, staged, or vs a ref)')
|
|
@@ -156,10 +234,11 @@ program
|
|
|
156
234
|
.option('-d, --db <path>', 'Path to graph.db')
|
|
157
235
|
.option('-f, --format <format>', 'Output format: dot, mermaid, json', 'dot')
|
|
158
236
|
.option('--functions', 'Function-level graph instead of file-level')
|
|
237
|
+
.option('-T, --no-tests', 'Exclude test/spec files')
|
|
159
238
|
.option('-o, --output <file>', 'Write to file instead of stdout')
|
|
160
239
|
.action((opts) => {
|
|
161
240
|
const db = new Database(findDbPath(opts.db), { readonly: true });
|
|
162
|
-
const exportOpts = { fileLevel: !opts.functions };
|
|
241
|
+
const exportOpts = { fileLevel: !opts.functions, noTests: !opts.tests };
|
|
163
242
|
|
|
164
243
|
let output;
|
|
165
244
|
switch (opts.format) {
|
|
@@ -167,7 +246,7 @@ program
|
|
|
167
246
|
output = exportMermaid(db, exportOpts);
|
|
168
247
|
break;
|
|
169
248
|
case 'json':
|
|
170
|
-
output = JSON.stringify(exportJSON(db), null, 2);
|
|
249
|
+
output = JSON.stringify(exportJSON(db, exportOpts), null, 2);
|
|
171
250
|
break;
|
|
172
251
|
default:
|
|
173
252
|
output = exportDOT(db, exportOpts);
|
|
@@ -189,10 +268,11 @@ program
|
|
|
189
268
|
.description('Detect circular dependencies in the codebase')
|
|
190
269
|
.option('-d, --db <path>', 'Path to graph.db')
|
|
191
270
|
.option('--functions', 'Function-level cycle detection')
|
|
271
|
+
.option('-T, --no-tests', 'Exclude test/spec files')
|
|
192
272
|
.option('-j, --json', 'Output as JSON')
|
|
193
273
|
.action((opts) => {
|
|
194
274
|
const db = new Database(findDbPath(opts.db), { readonly: true });
|
|
195
|
-
const cycles = findCycles(db, { fileLevel: !opts.functions });
|
|
275
|
+
const cycles = findCycles(db, { fileLevel: !opts.functions, noTests: !opts.tests });
|
|
196
276
|
db.close();
|
|
197
277
|
|
|
198
278
|
if (opts.json) {
|
|
@@ -322,7 +402,7 @@ program
|
|
|
322
402
|
.option('-d, --db <path>', 'Path to graph.db')
|
|
323
403
|
.option('-m, --model <name>', 'Override embedding model (auto-detects from DB)')
|
|
324
404
|
.option('-n, --limit <number>', 'Max results', '15')
|
|
325
|
-
.option('-T, --no-tests', 'Exclude test/spec files')
|
|
405
|
+
.option('-T, --no-tests', 'Exclude test/spec files from results')
|
|
326
406
|
.option('--min-score <score>', 'Minimum similarity threshold', '0.2')
|
|
327
407
|
.option('-k, --kind <kind>', 'Filter by kind: function, method, class')
|
|
328
408
|
.option('--file <pattern>', 'Filter by file path pattern')
|
|
@@ -347,6 +427,7 @@ program
|
|
|
347
427
|
.option('-d, --db <path>', 'Path to graph.db')
|
|
348
428
|
.option('--depth <n>', 'Max directory depth')
|
|
349
429
|
.option('--sort <metric>', 'Sort by: cohesion | fan-in | fan-out | density | files', 'files')
|
|
430
|
+
.option('-T, --no-tests', 'Exclude test/spec files')
|
|
350
431
|
.option('-j, --json', 'Output as JSON')
|
|
351
432
|
.action(async (dir, opts) => {
|
|
352
433
|
const { structureData, formatStructure } = await import('./structure.js');
|
|
@@ -354,6 +435,7 @@ program
|
|
|
354
435
|
directory: dir,
|
|
355
436
|
depth: opts.depth ? parseInt(opts.depth, 10) : undefined,
|
|
356
437
|
sort: opts.sort,
|
|
438
|
+
noTests: !opts.tests,
|
|
357
439
|
});
|
|
358
440
|
if (opts.json) {
|
|
359
441
|
console.log(JSON.stringify(data, null, 2));
|
|
@@ -371,6 +453,7 @@ program
|
|
|
371
453
|
.option('-n, --limit <number>', 'Number of results', '10')
|
|
372
454
|
.option('--metric <metric>', 'fan-in | fan-out | density | coupling', 'fan-in')
|
|
373
455
|
.option('--level <level>', 'file | directory', 'file')
|
|
456
|
+
.option('-T, --no-tests', 'Exclude test/spec files from results')
|
|
374
457
|
.option('-j, --json', 'Output as JSON')
|
|
375
458
|
.action(async (opts) => {
|
|
376
459
|
const { hotspotsData, formatHotspots } = await import('./structure.js');
|
|
@@ -378,6 +461,7 @@ program
|
|
|
378
461
|
metric: opts.metric,
|
|
379
462
|
level: opts.level,
|
|
380
463
|
limit: parseInt(opts.limit, 10),
|
|
464
|
+
noTests: !opts.tests,
|
|
381
465
|
});
|
|
382
466
|
if (opts.json) {
|
|
383
467
|
console.log(JSON.stringify(data, null, 2));
|
package/src/cycles.js
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import { loadNative } from './native.js';
|
|
2
|
+
import { isTestFile } from './queries.js';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Detect circular dependencies in the codebase using Tarjan's SCC algorithm.
|
|
5
6
|
* Dispatches to native Rust implementation when available, falls back to JS.
|
|
6
7
|
* @param {object} db - Open SQLite database
|
|
7
|
-
* @param {object} opts - { fileLevel: true }
|
|
8
|
+
* @param {object} opts - { fileLevel: true, noTests: false }
|
|
8
9
|
* @returns {string[][]} Array of cycles, each cycle is an array of file paths
|
|
9
10
|
*/
|
|
10
11
|
export function findCycles(db, opts = {}) {
|
|
11
12
|
const fileLevel = opts.fileLevel !== false;
|
|
13
|
+
const noTests = opts.noTests || false;
|
|
12
14
|
|
|
13
15
|
// Build adjacency list from SQLite (stays in JS — only the algorithm can move to Rust)
|
|
14
16
|
let edges;
|
|
@@ -22,6 +24,9 @@ export function findCycles(db, opts = {}) {
|
|
|
22
24
|
WHERE n1.file != n2.file AND e.kind IN ('imports', 'imports-type')
|
|
23
25
|
`)
|
|
24
26
|
.all();
|
|
27
|
+
if (noTests) {
|
|
28
|
+
edges = edges.filter((e) => !isTestFile(e.source) && !isTestFile(e.target));
|
|
29
|
+
}
|
|
25
30
|
} else {
|
|
26
31
|
edges = db
|
|
27
32
|
.prepare(`
|
|
@@ -37,6 +42,13 @@ export function findCycles(db, opts = {}) {
|
|
|
37
42
|
AND n1.id != n2.id
|
|
38
43
|
`)
|
|
39
44
|
.all();
|
|
45
|
+
if (noTests) {
|
|
46
|
+
edges = edges.filter((e) => {
|
|
47
|
+
const sourceFile = e.source.split('|').pop();
|
|
48
|
+
const targetFile = e.target.split('|').pop();
|
|
49
|
+
return !isTestFile(sourceFile) && !isTestFile(targetFile);
|
|
50
|
+
});
|
|
51
|
+
}
|
|
40
52
|
}
|
|
41
53
|
|
|
42
54
|
// Try native Rust implementation
|
package/src/db.js
CHANGED
package/src/export.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
+
import { isTestFile } from './queries.js';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Export the dependency graph in DOT (Graphviz) format.
|
|
5
6
|
*/
|
|
6
7
|
export function exportDOT(db, opts = {}) {
|
|
7
8
|
const fileLevel = opts.fileLevel !== false;
|
|
9
|
+
const noTests = opts.noTests || false;
|
|
8
10
|
const lines = [
|
|
9
11
|
'digraph codegraph {',
|
|
10
12
|
' rankdir=LR;',
|
|
@@ -14,7 +16,7 @@ export function exportDOT(db, opts = {}) {
|
|
|
14
16
|
];
|
|
15
17
|
|
|
16
18
|
if (fileLevel) {
|
|
17
|
-
|
|
19
|
+
let edges = db
|
|
18
20
|
.prepare(`
|
|
19
21
|
SELECT DISTINCT n1.file AS source, n2.file AS target
|
|
20
22
|
FROM edges e
|
|
@@ -23,6 +25,7 @@ export function exportDOT(db, opts = {}) {
|
|
|
23
25
|
WHERE n1.file != n2.file AND e.kind IN ('imports', 'imports-type', 'calls')
|
|
24
26
|
`)
|
|
25
27
|
.all();
|
|
28
|
+
if (noTests) edges = edges.filter((e) => !isTestFile(e.source) && !isTestFile(e.target));
|
|
26
29
|
|
|
27
30
|
// Try to use directory nodes from DB (built by structure analysis)
|
|
28
31
|
const hasDirectoryNodes =
|
|
@@ -89,7 +92,7 @@ export function exportDOT(db, opts = {}) {
|
|
|
89
92
|
lines.push(` "${source}" -> "${target}";`);
|
|
90
93
|
}
|
|
91
94
|
} else {
|
|
92
|
-
|
|
95
|
+
let edges = db
|
|
93
96
|
.prepare(`
|
|
94
97
|
SELECT n1.name AS source_name, n1.kind AS source_kind, n1.file AS source_file,
|
|
95
98
|
n2.name AS target_name, n2.kind AS target_kind, n2.file AS target_file,
|
|
@@ -101,6 +104,8 @@ export function exportDOT(db, opts = {}) {
|
|
|
101
104
|
AND e.kind = 'calls'
|
|
102
105
|
`)
|
|
103
106
|
.all();
|
|
107
|
+
if (noTests)
|
|
108
|
+
edges = edges.filter((e) => !isTestFile(e.source_file) && !isTestFile(e.target_file));
|
|
104
109
|
|
|
105
110
|
for (const e of edges) {
|
|
106
111
|
const sId = `${e.source_file}:${e.source_name}`.replace(/[^a-zA-Z0-9_]/g, '_');
|
|
@@ -120,10 +125,11 @@ export function exportDOT(db, opts = {}) {
|
|
|
120
125
|
*/
|
|
121
126
|
export function exportMermaid(db, opts = {}) {
|
|
122
127
|
const fileLevel = opts.fileLevel !== false;
|
|
128
|
+
const noTests = opts.noTests || false;
|
|
123
129
|
const lines = ['graph LR'];
|
|
124
130
|
|
|
125
131
|
if (fileLevel) {
|
|
126
|
-
|
|
132
|
+
let edges = db
|
|
127
133
|
.prepare(`
|
|
128
134
|
SELECT DISTINCT n1.file AS source, n2.file AS target
|
|
129
135
|
FROM edges e
|
|
@@ -132,6 +138,7 @@ export function exportMermaid(db, opts = {}) {
|
|
|
132
138
|
WHERE n1.file != n2.file AND e.kind IN ('imports', 'imports-type', 'calls')
|
|
133
139
|
`)
|
|
134
140
|
.all();
|
|
141
|
+
if (noTests) edges = edges.filter((e) => !isTestFile(e.source) && !isTestFile(e.target));
|
|
135
142
|
|
|
136
143
|
for (const { source, target } of edges) {
|
|
137
144
|
const s = source.replace(/[^a-zA-Z0-9]/g, '_');
|
|
@@ -139,7 +146,7 @@ export function exportMermaid(db, opts = {}) {
|
|
|
139
146
|
lines.push(` ${s}["${source}"] --> ${t}["${target}"]`);
|
|
140
147
|
}
|
|
141
148
|
} else {
|
|
142
|
-
|
|
149
|
+
let edges = db
|
|
143
150
|
.prepare(`
|
|
144
151
|
SELECT n1.name AS source_name, n1.file AS source_file,
|
|
145
152
|
n2.name AS target_name, n2.file AS target_file
|
|
@@ -150,6 +157,8 @@ export function exportMermaid(db, opts = {}) {
|
|
|
150
157
|
AND e.kind = 'calls'
|
|
151
158
|
`)
|
|
152
159
|
.all();
|
|
160
|
+
if (noTests)
|
|
161
|
+
edges = edges.filter((e) => !isTestFile(e.source_file) && !isTestFile(e.target_file));
|
|
153
162
|
|
|
154
163
|
for (const e of edges) {
|
|
155
164
|
const sId = `${e.source_file}_${e.source_name}`.replace(/[^a-zA-Z0-9]/g, '_');
|
|
@@ -164,14 +173,17 @@ export function exportMermaid(db, opts = {}) {
|
|
|
164
173
|
/**
|
|
165
174
|
* Export as JSON adjacency list.
|
|
166
175
|
*/
|
|
167
|
-
export function exportJSON(db) {
|
|
168
|
-
const
|
|
176
|
+
export function exportJSON(db, opts = {}) {
|
|
177
|
+
const noTests = opts.noTests || false;
|
|
178
|
+
|
|
179
|
+
let nodes = db
|
|
169
180
|
.prepare(`
|
|
170
181
|
SELECT id, name, kind, file, line FROM nodes WHERE kind = 'file'
|
|
171
182
|
`)
|
|
172
183
|
.all();
|
|
184
|
+
if (noTests) nodes = nodes.filter((n) => !isTestFile(n.file));
|
|
173
185
|
|
|
174
|
-
|
|
186
|
+
let edges = db
|
|
175
187
|
.prepare(`
|
|
176
188
|
SELECT DISTINCT n1.file AS source, n2.file AS target, e.kind
|
|
177
189
|
FROM edges e
|
|
@@ -180,6 +192,7 @@ export function exportJSON(db) {
|
|
|
180
192
|
WHERE n1.file != n2.file
|
|
181
193
|
`)
|
|
182
194
|
.all();
|
|
195
|
+
if (noTests) edges = edges.filter((e) => !isTestFile(e.source) && !isTestFile(e.target));
|
|
183
196
|
|
|
184
197
|
return { nodes, edges };
|
|
185
198
|
}
|
package/src/extractors/csharp.js
CHANGED
|
@@ -186,7 +186,12 @@ export function extractCSharpSymbols(tree, _filePath) {
|
|
|
186
186
|
calls.push({ name: fn.text, line: node.startPosition.row + 1 });
|
|
187
187
|
} else if (fn.type === 'member_access_expression') {
|
|
188
188
|
const name = fn.childForFieldName('name');
|
|
189
|
-
if (name)
|
|
189
|
+
if (name) {
|
|
190
|
+
const expr = fn.childForFieldName('expression');
|
|
191
|
+
const call = { name: name.text, line: node.startPosition.row + 1 };
|
|
192
|
+
if (expr) call.receiver = expr.text;
|
|
193
|
+
calls.push(call);
|
|
194
|
+
}
|
|
190
195
|
} else if (fn.type === 'generic_name' || fn.type === 'member_binding_expression') {
|
|
191
196
|
const name = fn.childForFieldName('name') || fn.child(0);
|
|
192
197
|
if (name) calls.push({ name: name.text, line: node.startPosition.row + 1 });
|
package/src/extractors/go.js
CHANGED
|
@@ -152,7 +152,12 @@ export function extractGoSymbols(tree, _filePath) {
|
|
|
152
152
|
calls.push({ name: fn.text, line: node.startPosition.row + 1 });
|
|
153
153
|
} else if (fn.type === 'selector_expression') {
|
|
154
154
|
const field = fn.childForFieldName('field');
|
|
155
|
-
if (field)
|
|
155
|
+
if (field) {
|
|
156
|
+
const operand = fn.childForFieldName('operand');
|
|
157
|
+
const call = { name: field.text, line: node.startPosition.row + 1 };
|
|
158
|
+
if (operand) call.receiver = operand.text;
|
|
159
|
+
calls.push(call);
|
|
160
|
+
}
|
|
156
161
|
}
|
|
157
162
|
}
|
|
158
163
|
break;
|
package/src/extractors/java.js
CHANGED
|
@@ -203,7 +203,10 @@ export function extractJavaSymbols(tree, _filePath) {
|
|
|
203
203
|
case 'method_invocation': {
|
|
204
204
|
const nameNode = node.childForFieldName('name');
|
|
205
205
|
if (nameNode) {
|
|
206
|
-
|
|
206
|
+
const obj = node.childForFieldName('object');
|
|
207
|
+
const call = { name: nameNode.text, line: node.startPosition.row + 1 };
|
|
208
|
+
if (obj) call.receiver = obj.text;
|
|
209
|
+
calls.push(call);
|
|
207
210
|
}
|
|
208
211
|
break;
|
|
209
212
|
}
|
|
@@ -140,6 +140,8 @@ export function extractSymbols(tree, _filePath) {
|
|
|
140
140
|
calls.push(callInfo);
|
|
141
141
|
}
|
|
142
142
|
}
|
|
143
|
+
const cbDef = extractCallbackDefinition(node);
|
|
144
|
+
if (cbDef) definitions.push(cbDef);
|
|
143
145
|
break;
|
|
144
146
|
}
|
|
145
147
|
|
|
@@ -313,6 +315,18 @@ function extractImplementsFromNode(node) {
|
|
|
313
315
|
return result;
|
|
314
316
|
}
|
|
315
317
|
|
|
318
|
+
function extractReceiverName(objNode) {
|
|
319
|
+
if (!objNode) return undefined;
|
|
320
|
+
if (objNode.type === 'identifier') return objNode.text;
|
|
321
|
+
if (objNode.type === 'this') return 'this';
|
|
322
|
+
if (objNode.type === 'super') return 'super';
|
|
323
|
+
if (objNode.type === 'member_expression') {
|
|
324
|
+
const prop = objNode.childForFieldName('property');
|
|
325
|
+
if (prop) return objNode.text;
|
|
326
|
+
}
|
|
327
|
+
return objNode.text;
|
|
328
|
+
}
|
|
329
|
+
|
|
316
330
|
function extractCallInfo(fn, callNode) {
|
|
317
331
|
if (fn.type === 'identifier') {
|
|
318
332
|
return { name: fn.text, line: callNode.startPosition.row + 1 };
|
|
@@ -335,21 +349,147 @@ function extractCallInfo(fn, callNode) {
|
|
|
335
349
|
|
|
336
350
|
if (prop.type === 'string' || prop.type === 'string_fragment') {
|
|
337
351
|
const methodName = prop.text.replace(/['"]/g, '');
|
|
338
|
-
if (methodName)
|
|
339
|
-
|
|
352
|
+
if (methodName) {
|
|
353
|
+
const receiver = extractReceiverName(obj);
|
|
354
|
+
return { name: methodName, line: callNode.startPosition.row + 1, dynamic: true, receiver };
|
|
355
|
+
}
|
|
340
356
|
}
|
|
341
357
|
|
|
342
|
-
|
|
358
|
+
const receiver = extractReceiverName(obj);
|
|
359
|
+
return { name: prop.text, line: callNode.startPosition.row + 1, receiver };
|
|
343
360
|
}
|
|
344
361
|
|
|
345
362
|
if (fn.type === 'subscript_expression') {
|
|
363
|
+
const obj = fn.childForFieldName('object');
|
|
346
364
|
const index = fn.childForFieldName('index');
|
|
347
365
|
if (index && (index.type === 'string' || index.type === 'template_string')) {
|
|
348
366
|
const methodName = index.text.replace(/['"`]/g, '');
|
|
349
|
-
if (methodName && !methodName.includes('$'))
|
|
350
|
-
|
|
367
|
+
if (methodName && !methodName.includes('$')) {
|
|
368
|
+
const receiver = extractReceiverName(obj);
|
|
369
|
+
return { name: methodName, line: callNode.startPosition.row + 1, dynamic: true, receiver };
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
return null;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function findAnonymousCallback(argsNode) {
|
|
378
|
+
for (let i = 0; i < argsNode.childCount; i++) {
|
|
379
|
+
const child = argsNode.child(i);
|
|
380
|
+
if (child && (child.type === 'arrow_function' || child.type === 'function_expression')) {
|
|
381
|
+
return child;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
return null;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function findFirstStringArg(argsNode) {
|
|
388
|
+
for (let i = 0; i < argsNode.childCount; i++) {
|
|
389
|
+
const child = argsNode.child(i);
|
|
390
|
+
if (child && child.type === 'string') {
|
|
391
|
+
return child.text.replace(/['"]/g, '');
|
|
351
392
|
}
|
|
352
393
|
}
|
|
394
|
+
return null;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function walkCallChain(startNode, methodName) {
|
|
398
|
+
let current = startNode;
|
|
399
|
+
while (current) {
|
|
400
|
+
if (current.type === 'call_expression') {
|
|
401
|
+
const fn = current.childForFieldName('function');
|
|
402
|
+
if (fn && fn.type === 'member_expression') {
|
|
403
|
+
const prop = fn.childForFieldName('property');
|
|
404
|
+
if (prop && prop.text === methodName) {
|
|
405
|
+
return current;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
if (current.type === 'member_expression') {
|
|
410
|
+
const obj = current.childForFieldName('object');
|
|
411
|
+
current = obj;
|
|
412
|
+
} else if (current.type === 'call_expression') {
|
|
413
|
+
const fn = current.childForFieldName('function');
|
|
414
|
+
current = fn;
|
|
415
|
+
} else {
|
|
416
|
+
break;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
return null;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const EXPRESS_METHODS = new Set([
|
|
423
|
+
'get',
|
|
424
|
+
'post',
|
|
425
|
+
'put',
|
|
426
|
+
'delete',
|
|
427
|
+
'patch',
|
|
428
|
+
'options',
|
|
429
|
+
'head',
|
|
430
|
+
'all',
|
|
431
|
+
'use',
|
|
432
|
+
]);
|
|
433
|
+
const EVENT_METHODS = new Set(['on', 'once', 'addEventListener', 'addListener']);
|
|
434
|
+
|
|
435
|
+
function extractCallbackDefinition(callNode) {
|
|
436
|
+
const fn = callNode.childForFieldName('function');
|
|
437
|
+
if (!fn || fn.type !== 'member_expression') return null;
|
|
438
|
+
|
|
439
|
+
const prop = fn.childForFieldName('property');
|
|
440
|
+
if (!prop) return null;
|
|
441
|
+
const method = prop.text;
|
|
442
|
+
|
|
443
|
+
const args = callNode.childForFieldName('arguments') || findChild(callNode, 'arguments');
|
|
444
|
+
if (!args) return null;
|
|
445
|
+
|
|
446
|
+
// Commander: .action(callback) with .command('name') in chain
|
|
447
|
+
if (method === 'action') {
|
|
448
|
+
const cb = findAnonymousCallback(args);
|
|
449
|
+
if (!cb) return null;
|
|
450
|
+
const commandCall = walkCallChain(fn.childForFieldName('object'), 'command');
|
|
451
|
+
if (!commandCall) return null;
|
|
452
|
+
const cmdArgs =
|
|
453
|
+
commandCall.childForFieldName('arguments') || findChild(commandCall, 'arguments');
|
|
454
|
+
if (!cmdArgs) return null;
|
|
455
|
+
const cmdName = findFirstStringArg(cmdArgs);
|
|
456
|
+
if (!cmdName) return null;
|
|
457
|
+
const firstWord = cmdName.split(/\s/)[0];
|
|
458
|
+
return {
|
|
459
|
+
name: `command:${firstWord}`,
|
|
460
|
+
kind: 'function',
|
|
461
|
+
line: cb.startPosition.row + 1,
|
|
462
|
+
endLine: nodeEndLine(cb),
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Express: app.get('/path', callback)
|
|
467
|
+
if (EXPRESS_METHODS.has(method)) {
|
|
468
|
+
const strArg = findFirstStringArg(args);
|
|
469
|
+
if (!strArg || !strArg.startsWith('/')) return null;
|
|
470
|
+
const cb = findAnonymousCallback(args);
|
|
471
|
+
if (!cb) return null;
|
|
472
|
+
return {
|
|
473
|
+
name: `route:${method.toUpperCase()} ${strArg}`,
|
|
474
|
+
kind: 'function',
|
|
475
|
+
line: cb.startPosition.row + 1,
|
|
476
|
+
endLine: nodeEndLine(cb),
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Events: emitter.on('event', callback)
|
|
481
|
+
if (EVENT_METHODS.has(method)) {
|
|
482
|
+
const eventName = findFirstStringArg(args);
|
|
483
|
+
if (!eventName) return null;
|
|
484
|
+
const cb = findAnonymousCallback(args);
|
|
485
|
+
if (!cb) return null;
|
|
486
|
+
return {
|
|
487
|
+
name: `event:${eventName}`,
|
|
488
|
+
kind: 'function',
|
|
489
|
+
line: cb.startPosition.row + 1,
|
|
490
|
+
endLine: nodeEndLine(cb),
|
|
491
|
+
};
|
|
492
|
+
}
|
|
353
493
|
|
|
354
494
|
return null;
|
|
355
495
|
}
|
package/src/extractors/php.js
CHANGED
|
@@ -206,7 +206,10 @@ export function extractPHPSymbols(tree, _filePath) {
|
|
|
206
206
|
case 'member_call_expression': {
|
|
207
207
|
const name = node.childForFieldName('name');
|
|
208
208
|
if (name) {
|
|
209
|
-
|
|
209
|
+
const obj = node.childForFieldName('object');
|
|
210
|
+
const call = { name: name.text, line: node.startPosition.row + 1 };
|
|
211
|
+
if (obj) call.receiver = obj.text;
|
|
212
|
+
calls.push(call);
|
|
210
213
|
}
|
|
211
214
|
break;
|
|
212
215
|
}
|
|
@@ -214,7 +217,10 @@ export function extractPHPSymbols(tree, _filePath) {
|
|
|
214
217
|
case 'scoped_call_expression': {
|
|
215
218
|
const name = node.childForFieldName('name');
|
|
216
219
|
if (name) {
|
|
217
|
-
|
|
220
|
+
const scope = node.childForFieldName('scope');
|
|
221
|
+
const call = { name: name.text, line: node.startPosition.row + 1 };
|
|
222
|
+
if (scope) call.receiver = scope.text;
|
|
223
|
+
calls.push(call);
|
|
218
224
|
}
|
|
219
225
|
break;
|
|
220
226
|
}
|
package/src/extractors/python.js
CHANGED
|
@@ -69,12 +69,19 @@ export function extractPythonSymbols(tree, _filePath) {
|
|
|
69
69
|
const fn = node.childForFieldName('function');
|
|
70
70
|
if (fn) {
|
|
71
71
|
let callName = null;
|
|
72
|
+
let receiver;
|
|
72
73
|
if (fn.type === 'identifier') callName = fn.text;
|
|
73
74
|
else if (fn.type === 'attribute') {
|
|
74
75
|
const attr = fn.childForFieldName('attribute');
|
|
75
76
|
if (attr) callName = attr.text;
|
|
77
|
+
const obj = fn.childForFieldName('object');
|
|
78
|
+
if (obj) receiver = obj.text;
|
|
79
|
+
}
|
|
80
|
+
if (callName) {
|
|
81
|
+
const call = { name: callName, line: node.startPosition.row + 1 };
|
|
82
|
+
if (receiver) call.receiver = receiver;
|
|
83
|
+
calls.push(call);
|
|
76
84
|
}
|
|
77
|
-
if (callName) calls.push({ name: callName, line: node.startPosition.row + 1 });
|
|
78
85
|
}
|
|
79
86
|
break;
|
|
80
87
|
}
|