@optave/codegraph 3.1.1 → 3.1.3
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 +6 -6
- package/package.json +7 -7
- package/src/ast-analysis/engine.js +365 -0
- package/src/ast-analysis/metrics.js +118 -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 +13 -140
- package/src/audit.js +2 -87
- package/src/batch.js +0 -25
- package/src/boundaries.js +1 -1
- package/src/branch-compare.js +1 -96
- package/src/builder.js +60 -178
- package/src/cfg.js +89 -883
- package/src/check.js +1 -84
- package/src/cli.js +31 -22
- 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 +12 -83
- package/src/complexity.js +43 -357
- package/src/cycles.js +1 -1
- package/src/dataflow.js +12 -665
- package/src/db/repository/build-stmts.js +104 -0
- package/src/db/repository/cached-stmt.js +19 -0
- package/src/db/repository/cfg.js +72 -0
- package/src/db/repository/cochange.js +54 -0
- package/src/db/repository/complexity.js +20 -0
- package/src/db/repository/dataflow.js +17 -0
- package/src/db/repository/edges.js +281 -0
- package/src/db/repository/embeddings.js +51 -0
- package/src/db/repository/graph-read.js +59 -0
- package/src/db/repository/index.js +43 -0
- package/src/db/repository/nodes.js +247 -0
- package/src/db.js +40 -1
- package/src/embedder.js +14 -34
- package/src/export.js +1 -1
- package/src/extractors/javascript.js +130 -5
- package/src/flow.js +2 -70
- package/src/index.js +30 -20
- package/src/{result-formatter.js → infrastructure/result-formatter.js} +1 -1
- package/src/kinds.js +1 -0
- package/src/manifesto.js +0 -76
- package/src/native.js +31 -9
- package/src/owners.js +1 -56
- package/src/parser.js +53 -2
- package/src/queries-cli.js +1 -1
- package/src/queries.js +79 -280
- package/src/sequence.js +5 -44
- package/src/structure.js +16 -75
- package/src/triage.js +1 -54
- package/src/viewer.js +1 -1
- package/src/watcher.js +7 -4
- package/src/db/repository.js +0 -134
- /package/src/{test-filter.js → infrastructure/test-filter.js} +0 -0
package/src/queries.js
CHANGED
|
@@ -6,22 +6,39 @@ import { coChangeForFiles } from './cochange.js';
|
|
|
6
6
|
import { loadConfig } from './config.js';
|
|
7
7
|
import { findCycles } from './cycles.js';
|
|
8
8
|
import {
|
|
9
|
+
countCrossFileCallers,
|
|
10
|
+
findAllIncomingEdges,
|
|
11
|
+
findAllOutgoingEdges,
|
|
12
|
+
findCallees,
|
|
13
|
+
findCallers,
|
|
14
|
+
findCrossFileCallTargets,
|
|
9
15
|
findDbPath,
|
|
16
|
+
findDistinctCallers,
|
|
17
|
+
findFileNodes,
|
|
18
|
+
findImportDependents,
|
|
19
|
+
findImportSources,
|
|
20
|
+
findImportTargets,
|
|
21
|
+
findIntraFileCallEdges,
|
|
22
|
+
findNodeById,
|
|
23
|
+
findNodeChildren,
|
|
24
|
+
findNodesByFile,
|
|
10
25
|
findNodesWithFanIn,
|
|
26
|
+
getClassHierarchy,
|
|
27
|
+
getComplexityForNode,
|
|
11
28
|
iterateFunctionNodes,
|
|
12
29
|
listFunctionNodes,
|
|
13
30
|
openReadonlyOrFail,
|
|
14
31
|
testFilterSQL,
|
|
15
32
|
} from './db.js';
|
|
33
|
+
import { isTestFile } from './infrastructure/test-filter.js';
|
|
16
34
|
import { ALL_SYMBOL_KINDS } from './kinds.js';
|
|
17
35
|
import { debug } from './logger.js';
|
|
18
36
|
import { ownersForFiles } from './owners.js';
|
|
19
37
|
import { paginateResult } from './paginate.js';
|
|
20
38
|
import { LANGUAGE_REGISTRY } from './parser.js';
|
|
21
|
-
import { isTestFile } from './test-filter.js';
|
|
22
39
|
|
|
23
40
|
// Re-export from dedicated module for backward compat
|
|
24
|
-
export { isTestFile, TEST_PATTERN } from './test-filter.js';
|
|
41
|
+
export { isTestFile, TEST_PATTERN } from './infrastructure/test-filter.js';
|
|
25
42
|
|
|
26
43
|
/**
|
|
27
44
|
* Resolve a file path relative to repoRoot, rejecting traversal outside the repo.
|
|
@@ -79,30 +96,6 @@ export {
|
|
|
79
96
|
VALID_ROLES,
|
|
80
97
|
} from './kinds.js';
|
|
81
98
|
|
|
82
|
-
/**
|
|
83
|
-
* Get all ancestor class names for a given class using extends edges.
|
|
84
|
-
*/
|
|
85
|
-
function getClassHierarchy(db, classNodeId) {
|
|
86
|
-
const ancestors = new Set();
|
|
87
|
-
const queue = [classNodeId];
|
|
88
|
-
while (queue.length > 0) {
|
|
89
|
-
const current = queue.shift();
|
|
90
|
-
const parents = db
|
|
91
|
-
.prepare(`
|
|
92
|
-
SELECT n.id, n.name FROM edges e JOIN nodes n ON e.target_id = n.id
|
|
93
|
-
WHERE e.source_id = ? AND e.kind = 'extends'
|
|
94
|
-
`)
|
|
95
|
-
.all(current);
|
|
96
|
-
for (const p of parents) {
|
|
97
|
-
if (!ancestors.has(p.id)) {
|
|
98
|
-
ancestors.add(p.id);
|
|
99
|
-
queue.push(p.id);
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
return ancestors;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
99
|
function resolveMethodViaHierarchy(db, methodName) {
|
|
107
100
|
const methods = db
|
|
108
101
|
.prepare(`SELECT * FROM nodes WHERE kind = 'method' AND name LIKE ?`)
|
|
@@ -203,21 +196,9 @@ export function queryNameData(name, customDbPath, opts = {}) {
|
|
|
203
196
|
|
|
204
197
|
const hc = new Map();
|
|
205
198
|
const results = nodes.map((node) => {
|
|
206
|
-
let callees = db
|
|
207
|
-
.prepare(`
|
|
208
|
-
SELECT n.name, n.kind, n.file, n.line, e.kind as edge_kind
|
|
209
|
-
FROM edges e JOIN nodes n ON e.target_id = n.id
|
|
210
|
-
WHERE e.source_id = ?
|
|
211
|
-
`)
|
|
212
|
-
.all(node.id);
|
|
199
|
+
let callees = findAllOutgoingEdges(db, node.id);
|
|
213
200
|
|
|
214
|
-
let callers = db
|
|
215
|
-
.prepare(`
|
|
216
|
-
SELECT n.name, n.kind, n.file, n.line, e.kind as edge_kind
|
|
217
|
-
FROM edges e JOIN nodes n ON e.source_id = n.id
|
|
218
|
-
WHERE e.target_id = ?
|
|
219
|
-
`)
|
|
220
|
-
.all(node.id);
|
|
201
|
+
let callers = findAllIncomingEdges(db, node.id);
|
|
221
202
|
|
|
222
203
|
if (noTests) {
|
|
223
204
|
callees = callees.filter((c) => !isTestFile(c.file));
|
|
@@ -254,9 +235,7 @@ export function impactAnalysisData(file, customDbPath, opts = {}) {
|
|
|
254
235
|
const db = openReadonlyOrFail(customDbPath);
|
|
255
236
|
try {
|
|
256
237
|
const noTests = opts.noTests || false;
|
|
257
|
-
const fileNodes = db
|
|
258
|
-
.prepare(`SELECT * FROM nodes WHERE file LIKE ? AND kind = 'file'`)
|
|
259
|
-
.all(`%${file}%`);
|
|
238
|
+
const fileNodes = findFileNodes(db, `%${file}%`);
|
|
260
239
|
if (fileNodes.length === 0) {
|
|
261
240
|
return { file, sources: [], levels: {}, totalDependents: 0 };
|
|
262
241
|
}
|
|
@@ -274,12 +253,7 @@ export function impactAnalysisData(file, customDbPath, opts = {}) {
|
|
|
274
253
|
while (queue.length > 0) {
|
|
275
254
|
const current = queue.shift();
|
|
276
255
|
const level = levels.get(current);
|
|
277
|
-
const dependents = db
|
|
278
|
-
.prepare(`
|
|
279
|
-
SELECT n.* FROM edges e JOIN nodes n ON e.source_id = n.id
|
|
280
|
-
WHERE e.target_id = ? AND e.kind IN ('imports', 'imports-type')
|
|
281
|
-
`)
|
|
282
|
-
.all(current);
|
|
256
|
+
const dependents = findImportDependents(db, current);
|
|
283
257
|
for (const dep of dependents) {
|
|
284
258
|
if (!visited.has(dep.id) && (!noTests || !isTestFile(dep.file))) {
|
|
285
259
|
visited.add(dep.id);
|
|
@@ -293,7 +267,7 @@ export function impactAnalysisData(file, customDbPath, opts = {}) {
|
|
|
293
267
|
for (const [id, level] of levels) {
|
|
294
268
|
if (level === 0) continue;
|
|
295
269
|
if (!byLevel[level]) byLevel[level] = [];
|
|
296
|
-
const node = db
|
|
270
|
+
const node = findNodeById(db, id);
|
|
297
271
|
if (node) byLevel[level].push({ file: node.file });
|
|
298
272
|
}
|
|
299
273
|
|
|
@@ -350,33 +324,19 @@ export function fileDepsData(file, customDbPath, opts = {}) {
|
|
|
350
324
|
const db = openReadonlyOrFail(customDbPath);
|
|
351
325
|
try {
|
|
352
326
|
const noTests = opts.noTests || false;
|
|
353
|
-
const fileNodes = db
|
|
354
|
-
.prepare(`SELECT * FROM nodes WHERE file LIKE ? AND kind = 'file'`)
|
|
355
|
-
.all(`%${file}%`);
|
|
327
|
+
const fileNodes = findFileNodes(db, `%${file}%`);
|
|
356
328
|
if (fileNodes.length === 0) {
|
|
357
329
|
return { file, results: [] };
|
|
358
330
|
}
|
|
359
331
|
|
|
360
332
|
const results = fileNodes.map((fn) => {
|
|
361
|
-
let importsTo = db
|
|
362
|
-
.prepare(`
|
|
363
|
-
SELECT n.file, e.kind as edge_kind FROM edges e JOIN nodes n ON e.target_id = n.id
|
|
364
|
-
WHERE e.source_id = ? AND e.kind IN ('imports', 'imports-type')
|
|
365
|
-
`)
|
|
366
|
-
.all(fn.id);
|
|
333
|
+
let importsTo = findImportTargets(db, fn.id);
|
|
367
334
|
if (noTests) importsTo = importsTo.filter((i) => !isTestFile(i.file));
|
|
368
335
|
|
|
369
|
-
let importedBy = db
|
|
370
|
-
.prepare(`
|
|
371
|
-
SELECT n.file, e.kind as edge_kind FROM edges e JOIN nodes n ON e.source_id = n.id
|
|
372
|
-
WHERE e.target_id = ? AND e.kind IN ('imports', 'imports-type')
|
|
373
|
-
`)
|
|
374
|
-
.all(fn.id);
|
|
336
|
+
let importedBy = findImportSources(db, fn.id);
|
|
375
337
|
if (noTests) importedBy = importedBy.filter((i) => !isTestFile(i.file));
|
|
376
338
|
|
|
377
|
-
const defs = db
|
|
378
|
-
.prepare(`SELECT * FROM nodes WHERE file = ? AND kind != 'file' ORDER BY line`)
|
|
379
|
-
.all(fn.file);
|
|
339
|
+
const defs = findNodesByFile(db, fn.file);
|
|
380
340
|
|
|
381
341
|
return {
|
|
382
342
|
file: fn.file,
|
|
@@ -406,35 +366,17 @@ export function fnDepsData(name, customDbPath, opts = {}) {
|
|
|
406
366
|
}
|
|
407
367
|
|
|
408
368
|
const results = nodes.map((node) => {
|
|
409
|
-
const callees = db
|
|
410
|
-
.prepare(`
|
|
411
|
-
SELECT n.name, n.kind, n.file, n.line, e.kind as edge_kind
|
|
412
|
-
FROM edges e JOIN nodes n ON e.target_id = n.id
|
|
413
|
-
WHERE e.source_id = ? AND e.kind = 'calls'
|
|
414
|
-
`)
|
|
415
|
-
.all(node.id);
|
|
369
|
+
const callees = findCallees(db, node.id);
|
|
416
370
|
const filteredCallees = noTests ? callees.filter((c) => !isTestFile(c.file)) : callees;
|
|
417
371
|
|
|
418
|
-
let callers = db
|
|
419
|
-
.prepare(`
|
|
420
|
-
SELECT n.name, n.kind, n.file, n.line, e.kind as edge_kind
|
|
421
|
-
FROM edges e JOIN nodes n ON e.source_id = n.id
|
|
422
|
-
WHERE e.target_id = ? AND e.kind = 'calls'
|
|
423
|
-
`)
|
|
424
|
-
.all(node.id);
|
|
372
|
+
let callers = findCallers(db, node.id);
|
|
425
373
|
|
|
426
374
|
if (node.kind === 'method' && node.name.includes('.')) {
|
|
427
375
|
const methodName = node.name.split('.').pop();
|
|
428
376
|
const relatedMethods = resolveMethodViaHierarchy(db, methodName);
|
|
429
377
|
for (const rm of relatedMethods) {
|
|
430
378
|
if (rm.id === node.id) continue;
|
|
431
|
-
const extraCallers = db
|
|
432
|
-
.prepare(`
|
|
433
|
-
SELECT n.name, n.kind, n.file, n.line, e.kind as edge_kind
|
|
434
|
-
FROM edges e JOIN nodes n ON e.source_id = n.id
|
|
435
|
-
WHERE e.target_id = ? AND e.kind = 'calls'
|
|
436
|
-
`)
|
|
437
|
-
.all(rm.id);
|
|
379
|
+
const extraCallers = findCallers(db, rm.id);
|
|
438
380
|
callers.push(...extraCallers.map((c) => ({ ...c, viaHierarchy: rm.name })));
|
|
439
381
|
}
|
|
440
382
|
}
|
|
@@ -536,13 +478,7 @@ export function fnImpactData(name, customDbPath, opts = {}) {
|
|
|
536
478
|
for (let d = 1; d <= maxDepth; d++) {
|
|
537
479
|
const nextFrontier = [];
|
|
538
480
|
for (const fid of frontier) {
|
|
539
|
-
const callers = db
|
|
540
|
-
.prepare(`
|
|
541
|
-
SELECT DISTINCT n.id, n.name, n.kind, n.file, n.line
|
|
542
|
-
FROM edges e JOIN nodes n ON e.source_id = n.id
|
|
543
|
-
WHERE e.target_id = ? AND e.kind = 'calls'
|
|
544
|
-
`)
|
|
545
|
-
.all(fid);
|
|
481
|
+
const callers = findDistinctCallers(db, fid);
|
|
546
482
|
for (const c of callers) {
|
|
547
483
|
if (!visited.has(c.id) && (!noTests || !isTestFile(c.file))) {
|
|
548
484
|
visited.add(c.id);
|
|
@@ -884,13 +820,7 @@ export function diffImpactData(customDbPath, opts = {}) {
|
|
|
884
820
|
for (let d = 1; d <= maxDepth; d++) {
|
|
885
821
|
const nextFrontier = [];
|
|
886
822
|
for (const fid of frontier) {
|
|
887
|
-
const callers = db
|
|
888
|
-
.prepare(`
|
|
889
|
-
SELECT DISTINCT n.id, n.name, n.kind, n.file, n.line
|
|
890
|
-
FROM edges e JOIN nodes n ON e.source_id = n.id
|
|
891
|
-
WHERE e.target_id = ? AND e.kind = 'calls'
|
|
892
|
-
`)
|
|
893
|
-
.all(fid);
|
|
823
|
+
const callers = findDistinctCallers(db, fid);
|
|
894
824
|
for (const c of callers) {
|
|
895
825
|
if (!visited.has(c.id) && (!noTests || !isTestFile(c.file))) {
|
|
896
826
|
visited.add(c.id);
|
|
@@ -1658,13 +1588,7 @@ export function contextData(name, customDbPath, opts = {}) {
|
|
|
1658
1588
|
const signature = fileLines ? extractSignature(fileLines, node.line) : null;
|
|
1659
1589
|
|
|
1660
1590
|
// Callees
|
|
1661
|
-
const calleeRows = db
|
|
1662
|
-
.prepare(
|
|
1663
|
-
`SELECT n.id, n.name, n.kind, n.file, n.line, n.end_line
|
|
1664
|
-
FROM edges e JOIN nodes n ON e.target_id = n.id
|
|
1665
|
-
WHERE e.source_id = ? AND e.kind = 'calls'`,
|
|
1666
|
-
)
|
|
1667
|
-
.all(node.id);
|
|
1591
|
+
const calleeRows = findCallees(db, node.id);
|
|
1668
1592
|
const filteredCallees = noTests ? calleeRows.filter((c) => !isTestFile(c.file)) : calleeRows;
|
|
1669
1593
|
|
|
1670
1594
|
const callees = filteredCallees.map((c) => {
|
|
@@ -1694,13 +1618,7 @@ export function contextData(name, customDbPath, opts = {}) {
|
|
|
1694
1618
|
for (let d = 2; d <= maxDepth; d++) {
|
|
1695
1619
|
const nextFrontier = [];
|
|
1696
1620
|
for (const fid of frontier) {
|
|
1697
|
-
const deeper = db
|
|
1698
|
-
.prepare(
|
|
1699
|
-
`SELECT n.id, n.name, n.kind, n.file, n.line, n.end_line
|
|
1700
|
-
FROM edges e JOIN nodes n ON e.target_id = n.id
|
|
1701
|
-
WHERE e.source_id = ? AND e.kind = 'calls'`,
|
|
1702
|
-
)
|
|
1703
|
-
.all(fid);
|
|
1621
|
+
const deeper = findCallees(db, fid);
|
|
1704
1622
|
for (const c of deeper) {
|
|
1705
1623
|
if (!visited.has(c.id) && (!noTests || !isTestFile(c.file))) {
|
|
1706
1624
|
visited.add(c.id);
|
|
@@ -1724,13 +1642,7 @@ export function contextData(name, customDbPath, opts = {}) {
|
|
|
1724
1642
|
}
|
|
1725
1643
|
|
|
1726
1644
|
// Callers
|
|
1727
|
-
let callerRows = db
|
|
1728
|
-
.prepare(
|
|
1729
|
-
`SELECT n.name, n.kind, n.file, n.line
|
|
1730
|
-
FROM edges e JOIN nodes n ON e.source_id = n.id
|
|
1731
|
-
WHERE e.target_id = ? AND e.kind = 'calls'`,
|
|
1732
|
-
)
|
|
1733
|
-
.all(node.id);
|
|
1645
|
+
let callerRows = findCallers(db, node.id);
|
|
1734
1646
|
|
|
1735
1647
|
// Method hierarchy resolution
|
|
1736
1648
|
if (node.kind === 'method' && node.name.includes('.')) {
|
|
@@ -1738,13 +1650,7 @@ export function contextData(name, customDbPath, opts = {}) {
|
|
|
1738
1650
|
const relatedMethods = resolveMethodViaHierarchy(db, methodName);
|
|
1739
1651
|
for (const rm of relatedMethods) {
|
|
1740
1652
|
if (rm.id === node.id) continue;
|
|
1741
|
-
const extraCallers = db
|
|
1742
|
-
.prepare(
|
|
1743
|
-
`SELECT n.name, n.kind, n.file, n.line
|
|
1744
|
-
FROM edges e JOIN nodes n ON e.source_id = n.id
|
|
1745
|
-
WHERE e.target_id = ? AND e.kind = 'calls'`,
|
|
1746
|
-
)
|
|
1747
|
-
.all(rm.id);
|
|
1653
|
+
const extraCallers = findCallers(db, rm.id);
|
|
1748
1654
|
callerRows.push(...extraCallers.map((c) => ({ ...c, viaHierarchy: rm.name })));
|
|
1749
1655
|
}
|
|
1750
1656
|
}
|
|
@@ -1759,13 +1665,7 @@ export function contextData(name, customDbPath, opts = {}) {
|
|
|
1759
1665
|
}));
|
|
1760
1666
|
|
|
1761
1667
|
// Related tests: callers that live in test files
|
|
1762
|
-
const testCallerRows = db
|
|
1763
|
-
.prepare(
|
|
1764
|
-
`SELECT n.name, n.kind, n.file, n.line
|
|
1765
|
-
FROM edges e JOIN nodes n ON e.source_id = n.id
|
|
1766
|
-
WHERE e.target_id = ? AND e.kind = 'calls'`,
|
|
1767
|
-
)
|
|
1768
|
-
.all(node.id);
|
|
1668
|
+
const testCallerRows = findCallers(db, node.id);
|
|
1769
1669
|
const testCallers = testCallerRows.filter((c) => isTestFile(c.file));
|
|
1770
1670
|
|
|
1771
1671
|
const testsByFile = new Map();
|
|
@@ -1796,11 +1696,7 @@ export function contextData(name, customDbPath, opts = {}) {
|
|
|
1796
1696
|
// Complexity metrics
|
|
1797
1697
|
let complexityMetrics = null;
|
|
1798
1698
|
try {
|
|
1799
|
-
const cRow = db
|
|
1800
|
-
.prepare(
|
|
1801
|
-
'SELECT cognitive, cyclomatic, max_nesting, maintainability_index, halstead_volume FROM function_complexity WHERE node_id = ?',
|
|
1802
|
-
)
|
|
1803
|
-
.get(node.id);
|
|
1699
|
+
const cRow = getComplexityForNode(db, node.id);
|
|
1804
1700
|
if (cRow) {
|
|
1805
1701
|
complexityMetrics = {
|
|
1806
1702
|
cognitive: cRow.cognitive,
|
|
@@ -1817,10 +1713,12 @@ export function contextData(name, customDbPath, opts = {}) {
|
|
|
1817
1713
|
// Children (parameters, properties, constants)
|
|
1818
1714
|
let nodeChildren = [];
|
|
1819
1715
|
try {
|
|
1820
|
-
nodeChildren = db
|
|
1821
|
-
.
|
|
1822
|
-
.
|
|
1823
|
-
|
|
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
|
+
}));
|
|
1824
1722
|
} catch {
|
|
1825
1723
|
/* parent_id column may not exist */
|
|
1826
1724
|
}
|
|
@@ -1864,9 +1762,7 @@ export function childrenData(name, customDbPath, opts = {}) {
|
|
|
1864
1762
|
const results = nodes.map((node) => {
|
|
1865
1763
|
let children;
|
|
1866
1764
|
try {
|
|
1867
|
-
children = db
|
|
1868
|
-
.prepare('SELECT name, kind, line, end_line FROM nodes WHERE parent_id = ? ORDER BY line')
|
|
1869
|
-
.all(node.id);
|
|
1765
|
+
children = findNodeChildren(db, node.id);
|
|
1870
1766
|
} catch {
|
|
1871
1767
|
children = [];
|
|
1872
1768
|
}
|
|
@@ -1905,28 +1801,14 @@ function isFileLikeTarget(target) {
|
|
|
1905
1801
|
}
|
|
1906
1802
|
|
|
1907
1803
|
function explainFileImpl(db, target, getFileLines) {
|
|
1908
|
-
const fileNodes = db
|
|
1909
|
-
.prepare(`SELECT * FROM nodes WHERE file LIKE ? AND kind = 'file'`)
|
|
1910
|
-
.all(`%${target}%`);
|
|
1804
|
+
const fileNodes = findFileNodes(db, `%${target}%`);
|
|
1911
1805
|
if (fileNodes.length === 0) return [];
|
|
1912
1806
|
|
|
1913
1807
|
return fileNodes.map((fn) => {
|
|
1914
|
-
const symbols = db
|
|
1915
|
-
.prepare(`SELECT * FROM nodes WHERE file = ? AND kind != 'file' ORDER BY line`)
|
|
1916
|
-
.all(fn.file);
|
|
1808
|
+
const symbols = findNodesByFile(db, fn.file);
|
|
1917
1809
|
|
|
1918
1810
|
// IDs of symbols that have incoming calls from other files (public)
|
|
1919
|
-
const publicIds =
|
|
1920
|
-
db
|
|
1921
|
-
.prepare(
|
|
1922
|
-
`SELECT DISTINCT e.target_id FROM edges e
|
|
1923
|
-
JOIN nodes caller ON e.source_id = caller.id
|
|
1924
|
-
JOIN nodes target ON e.target_id = target.id
|
|
1925
|
-
WHERE target.file = ? AND caller.file != ? AND e.kind = 'calls'`,
|
|
1926
|
-
)
|
|
1927
|
-
.all(fn.file, fn.file)
|
|
1928
|
-
.map((r) => r.target_id),
|
|
1929
|
-
);
|
|
1811
|
+
const publicIds = findCrossFileCallTargets(db, fn.file);
|
|
1930
1812
|
|
|
1931
1813
|
const fileLines = getFileLines(fn.file);
|
|
1932
1814
|
const mapSymbol = (s) => ({
|
|
@@ -1942,33 +1824,12 @@ function explainFileImpl(db, target, getFileLines) {
|
|
|
1942
1824
|
const internal = symbols.filter((s) => !publicIds.has(s.id)).map(mapSymbol);
|
|
1943
1825
|
|
|
1944
1826
|
// Imports / importedBy
|
|
1945
|
-
const imports = db
|
|
1946
|
-
.prepare(
|
|
1947
|
-
`SELECT n.file FROM edges e JOIN nodes n ON e.target_id = n.id
|
|
1948
|
-
WHERE e.source_id = ? AND e.kind IN ('imports', 'imports-type')`,
|
|
1949
|
-
)
|
|
1950
|
-
.all(fn.id)
|
|
1951
|
-
.map((r) => ({ file: r.file }));
|
|
1827
|
+
const imports = findImportTargets(db, fn.id).map((r) => ({ file: r.file }));
|
|
1952
1828
|
|
|
1953
|
-
const importedBy = db
|
|
1954
|
-
.prepare(
|
|
1955
|
-
`SELECT n.file FROM edges e JOIN nodes n ON e.source_id = n.id
|
|
1956
|
-
WHERE e.target_id = ? AND e.kind IN ('imports', 'imports-type')`,
|
|
1957
|
-
)
|
|
1958
|
-
.all(fn.id)
|
|
1959
|
-
.map((r) => ({ file: r.file }));
|
|
1829
|
+
const importedBy = findImportSources(db, fn.id).map((r) => ({ file: r.file }));
|
|
1960
1830
|
|
|
1961
1831
|
// Intra-file data flow
|
|
1962
|
-
const intraEdges = db
|
|
1963
|
-
.prepare(
|
|
1964
|
-
`SELECT caller.name as caller_name, callee.name as callee_name
|
|
1965
|
-
FROM edges e
|
|
1966
|
-
JOIN nodes caller ON e.source_id = caller.id
|
|
1967
|
-
JOIN nodes callee ON e.target_id = callee.id
|
|
1968
|
-
WHERE caller.file = ? AND callee.file = ? AND e.kind = 'calls'
|
|
1969
|
-
ORDER BY caller.line`,
|
|
1970
|
-
)
|
|
1971
|
-
.all(fn.file, fn.file);
|
|
1832
|
+
const intraEdges = findIntraFileCallEdges(db, fn.file);
|
|
1972
1833
|
|
|
1973
1834
|
const dataFlowMap = new Map();
|
|
1974
1835
|
for (const edge of intraEdges) {
|
|
@@ -2021,43 +1882,31 @@ function explainFunctionImpl(db, target, noTests, getFileLines) {
|
|
|
2021
1882
|
const summary = fileLines ? extractSummary(fileLines, node.line) : null;
|
|
2022
1883
|
const signature = fileLines ? extractSignature(fileLines, node.line) : null;
|
|
2023
1884
|
|
|
2024
|
-
const callees = db
|
|
2025
|
-
.
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
.all(node.id)
|
|
2031
|
-
.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
|
+
}));
|
|
2032
1891
|
|
|
2033
|
-
let callers = db
|
|
2034
|
-
.
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
.all(node.id)
|
|
2040
|
-
.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
|
+
}));
|
|
2041
1898
|
if (noTests) callers = callers.filter((c) => !isTestFile(c.file));
|
|
2042
1899
|
|
|
2043
|
-
const testCallerRows = db
|
|
2044
|
-
|
|
2045
|
-
`SELECT DISTINCT n.file FROM edges e JOIN nodes n ON e.source_id = n.id
|
|
2046
|
-
WHERE e.target_id = ? AND e.kind = 'calls'`,
|
|
2047
|
-
)
|
|
2048
|
-
.all(node.id);
|
|
1900
|
+
const testCallerRows = findCallers(db, node.id);
|
|
1901
|
+
const seenFiles = new Set();
|
|
2049
1902
|
const relatedTests = testCallerRows
|
|
2050
|
-
.filter((r) => isTestFile(r.file))
|
|
1903
|
+
.filter((r) => isTestFile(r.file) && !seenFiles.has(r.file) && seenFiles.add(r.file))
|
|
2051
1904
|
.map((r) => ({ file: r.file }));
|
|
2052
1905
|
|
|
2053
1906
|
// Complexity metrics
|
|
2054
1907
|
let complexityMetrics = null;
|
|
2055
1908
|
try {
|
|
2056
|
-
const cRow = db
|
|
2057
|
-
.prepare(
|
|
2058
|
-
'SELECT cognitive, cyclomatic, max_nesting, maintainability_index, halstead_volume FROM function_complexity WHERE node_id = ?',
|
|
2059
|
-
)
|
|
2060
|
-
.get(node.id);
|
|
1909
|
+
const cRow = getComplexityForNode(db, node.id);
|
|
2061
1910
|
if (cRow) {
|
|
2062
1911
|
complexityMetrics = {
|
|
2063
1912
|
cognitive: cRow.cognitive,
|
|
@@ -2204,20 +2053,10 @@ function whereSymbolImpl(db, target, noTests) {
|
|
|
2204
2053
|
|
|
2205
2054
|
const hc = new Map();
|
|
2206
2055
|
return nodes.map((node) => {
|
|
2207
|
-
const
|
|
2208
|
-
|
|
2209
|
-
`SELECT COUNT(*) as cnt FROM edges e JOIN nodes n ON e.source_id = n.id
|
|
2210
|
-
WHERE e.target_id = ? AND e.kind = 'calls' AND n.file != ?`,
|
|
2211
|
-
)
|
|
2212
|
-
.get(node.id, node.file);
|
|
2213
|
-
const exported = crossFileCallers.cnt > 0;
|
|
2056
|
+
const crossCount = countCrossFileCallers(db, node.id, node.file);
|
|
2057
|
+
const exported = crossCount > 0;
|
|
2214
2058
|
|
|
2215
|
-
let uses = db
|
|
2216
|
-
.prepare(
|
|
2217
|
-
`SELECT n.name, n.file, n.line FROM edges e JOIN nodes n ON e.source_id = n.id
|
|
2218
|
-
WHERE e.target_id = ? AND e.kind = 'calls'`,
|
|
2219
|
-
)
|
|
2220
|
-
.all(node.id);
|
|
2059
|
+
let uses = findCallers(db, node.id);
|
|
2221
2060
|
if (noTests) uses = uses.filter((u) => !isTestFile(u.file));
|
|
2222
2061
|
|
|
2223
2062
|
return {
|
|
@@ -2229,43 +2068,17 @@ function whereSymbolImpl(db, target, noTests) {
|
|
|
2229
2068
|
}
|
|
2230
2069
|
|
|
2231
2070
|
function whereFileImpl(db, target) {
|
|
2232
|
-
const fileNodes = db
|
|
2233
|
-
.prepare(`SELECT * FROM nodes WHERE file LIKE ? AND kind = 'file'`)
|
|
2234
|
-
.all(`%${target}%`);
|
|
2071
|
+
const fileNodes = findFileNodes(db, `%${target}%`);
|
|
2235
2072
|
if (fileNodes.length === 0) return [];
|
|
2236
2073
|
|
|
2237
2074
|
return fileNodes.map((fn) => {
|
|
2238
|
-
const symbols = db
|
|
2239
|
-
.prepare(`SELECT * FROM nodes WHERE file = ? AND kind != 'file' ORDER BY line`)
|
|
2240
|
-
.all(fn.file);
|
|
2075
|
+
const symbols = findNodesByFile(db, fn.file);
|
|
2241
2076
|
|
|
2242
|
-
const imports = db
|
|
2243
|
-
.prepare(
|
|
2244
|
-
`SELECT n.file FROM edges e JOIN nodes n ON e.target_id = n.id
|
|
2245
|
-
WHERE e.source_id = ? AND e.kind IN ('imports', 'imports-type')`,
|
|
2246
|
-
)
|
|
2247
|
-
.all(fn.id)
|
|
2248
|
-
.map((r) => r.file);
|
|
2077
|
+
const imports = findImportTargets(db, fn.id).map((r) => r.file);
|
|
2249
2078
|
|
|
2250
|
-
const importedBy = db
|
|
2251
|
-
.prepare(
|
|
2252
|
-
`SELECT n.file FROM edges e JOIN nodes n ON e.source_id = n.id
|
|
2253
|
-
WHERE e.target_id = ? AND e.kind IN ('imports', 'imports-type')`,
|
|
2254
|
-
)
|
|
2255
|
-
.all(fn.id)
|
|
2256
|
-
.map((r) => r.file);
|
|
2079
|
+
const importedBy = findImportSources(db, fn.id).map((r) => r.file);
|
|
2257
2080
|
|
|
2258
|
-
const exportedIds =
|
|
2259
|
-
db
|
|
2260
|
-
.prepare(
|
|
2261
|
-
`SELECT DISTINCT e.target_id FROM edges e
|
|
2262
|
-
JOIN nodes caller ON e.source_id = caller.id
|
|
2263
|
-
JOIN nodes target ON e.target_id = target.id
|
|
2264
|
-
WHERE target.file = ? AND caller.file != ? AND e.kind = 'calls'`,
|
|
2265
|
-
)
|
|
2266
|
-
.all(fn.file, fn.file)
|
|
2267
|
-
.map((r) => r.target_id),
|
|
2268
|
-
);
|
|
2081
|
+
const exportedIds = findCrossFileCallTargets(db, fn.file);
|
|
2269
2082
|
|
|
2270
2083
|
const exported = symbols.filter((s) => exportedIds.has(s.id)).map((s) => s.name);
|
|
2271
2084
|
|
|
@@ -2341,9 +2154,7 @@ export function rolesData(customDbPath, opts = {}) {
|
|
|
2341
2154
|
// ─── exportsData ─────────────────────────────────────────────────────
|
|
2342
2155
|
|
|
2343
2156
|
function exportsFileImpl(db, target, noTests, getFileLines, unused) {
|
|
2344
|
-
const fileNodes = db
|
|
2345
|
-
.prepare(`SELECT * FROM nodes WHERE file LIKE ? AND kind = 'file'`)
|
|
2346
|
-
.all(`%${target}%`);
|
|
2157
|
+
const fileNodes = findFileNodes(db, `%${target}%`);
|
|
2347
2158
|
if (fileNodes.length === 0) return [];
|
|
2348
2159
|
|
|
2349
2160
|
// Detect whether exported column exists
|
|
@@ -2356,9 +2167,7 @@ function exportsFileImpl(db, target, noTests, getFileLines, unused) {
|
|
|
2356
2167
|
}
|
|
2357
2168
|
|
|
2358
2169
|
return fileNodes.map((fn) => {
|
|
2359
|
-
const symbols = db
|
|
2360
|
-
.prepare(`SELECT * FROM nodes WHERE file = ? AND kind != 'file' ORDER BY line`)
|
|
2361
|
-
.all(fn.file);
|
|
2170
|
+
const symbols = findNodesByFile(db, fn.file);
|
|
2362
2171
|
|
|
2363
2172
|
let exported;
|
|
2364
2173
|
if (hasExportedCol) {
|
|
@@ -2370,17 +2179,7 @@ function exportsFileImpl(db, target, noTests, getFileLines, unused) {
|
|
|
2370
2179
|
.all(fn.file);
|
|
2371
2180
|
} else {
|
|
2372
2181
|
// Fallback: symbols that have incoming calls from other files
|
|
2373
|
-
const exportedIds =
|
|
2374
|
-
db
|
|
2375
|
-
.prepare(
|
|
2376
|
-
`SELECT DISTINCT e.target_id FROM edges e
|
|
2377
|
-
JOIN nodes caller ON e.source_id = caller.id
|
|
2378
|
-
JOIN nodes target ON e.target_id = target.id
|
|
2379
|
-
WHERE target.file = ? AND caller.file != ? AND e.kind = 'calls'`,
|
|
2380
|
-
)
|
|
2381
|
-
.all(fn.file, fn.file)
|
|
2382
|
-
.map((r) => r.target_id),
|
|
2383
|
-
);
|
|
2182
|
+
const exportedIds = findCrossFileCallTargets(db, fn.file);
|
|
2384
2183
|
exported = symbols.filter((s) => exportedIds.has(s.id));
|
|
2385
2184
|
}
|
|
2386
2185
|
const internalCount = symbols.length - exported.length;
|
package/src/sequence.js
CHANGED
|
@@ -6,12 +6,11 @@
|
|
|
6
6
|
* sequence-diagram conventions.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { openReadonlyOrFail } from './db.js';
|
|
9
|
+
import { findCallees, openReadonlyOrFail } from './db.js';
|
|
10
|
+
import { isTestFile } from './infrastructure/test-filter.js';
|
|
10
11
|
import { paginateResult } from './paginate.js';
|
|
11
|
-
import { findMatchingNodes
|
|
12
|
-
import { outputResult } from './result-formatter.js';
|
|
12
|
+
import { findMatchingNodes } from './queries.js';
|
|
13
13
|
import { FRAMEWORK_ENTRY_PREFIXES } from './structure.js';
|
|
14
|
-
import { isTestFile } from './test-filter.js';
|
|
15
14
|
|
|
16
15
|
// ─── Alias generation ────────────────────────────────────────────────
|
|
17
16
|
|
|
@@ -130,17 +129,11 @@ export function sequenceData(name, dbPath, opts = {}) {
|
|
|
130
129
|
idToNode.set(matchNode.id, matchNode);
|
|
131
130
|
let truncated = false;
|
|
132
131
|
|
|
133
|
-
const getCallees = db.prepare(
|
|
134
|
-
`SELECT DISTINCT n.id, n.name, n.kind, n.file, n.line
|
|
135
|
-
FROM edges e JOIN nodes n ON e.target_id = n.id
|
|
136
|
-
WHERE e.source_id = ? AND e.kind = 'calls'`,
|
|
137
|
-
);
|
|
138
|
-
|
|
139
132
|
for (let d = 1; d <= maxDepth; d++) {
|
|
140
133
|
const nextFrontier = [];
|
|
141
134
|
|
|
142
135
|
for (const fid of frontier) {
|
|
143
|
-
const callees =
|
|
136
|
+
const callees = findCallees(db, fid);
|
|
144
137
|
|
|
145
138
|
const caller = idToNode.get(fid);
|
|
146
139
|
|
|
@@ -170,7 +163,7 @@ export function sequenceData(name, dbPath, opts = {}) {
|
|
|
170
163
|
|
|
171
164
|
if (d === maxDepth && frontier.length > 0) {
|
|
172
165
|
// Only mark truncated if at least one frontier node has further callees
|
|
173
|
-
const hasMoreCalls = frontier.some((fid) =>
|
|
166
|
+
const hasMoreCalls = frontier.some((fid) => findCallees(db, fid).length > 0);
|
|
174
167
|
if (hasMoreCalls) truncated = true;
|
|
175
168
|
}
|
|
176
169
|
}
|
|
@@ -330,35 +323,3 @@ export function sequenceToMermaid(seqResult) {
|
|
|
330
323
|
|
|
331
324
|
return lines.join('\n');
|
|
332
325
|
}
|
|
333
|
-
|
|
334
|
-
// ─── CLI formatter ───────────────────────────────────────────────────
|
|
335
|
-
|
|
336
|
-
/**
|
|
337
|
-
* CLI entry point — format sequence data as mermaid, JSON, or ndjson.
|
|
338
|
-
*/
|
|
339
|
-
export function sequence(name, dbPath, opts = {}) {
|
|
340
|
-
const data = sequenceData(name, dbPath, opts);
|
|
341
|
-
|
|
342
|
-
if (outputResult(data, 'messages', opts)) return;
|
|
343
|
-
|
|
344
|
-
// Default: mermaid format
|
|
345
|
-
if (!data.entry) {
|
|
346
|
-
console.log(`No matching function found for "${name}".`);
|
|
347
|
-
return;
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
const e = data.entry;
|
|
351
|
-
console.log(`\nSequence from: [${kindIcon(e.kind)}] ${e.name} ${e.file}:${e.line}`);
|
|
352
|
-
console.log(`Participants: ${data.participants.length} Messages: ${data.totalMessages}`);
|
|
353
|
-
if (data.truncated) {
|
|
354
|
-
console.log(` (truncated at depth ${data.depth})`);
|
|
355
|
-
}
|
|
356
|
-
console.log();
|
|
357
|
-
|
|
358
|
-
if (data.messages.length === 0) {
|
|
359
|
-
console.log(' (leaf node — no callees)');
|
|
360
|
-
return;
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
console.log(sequenceToMermaid(data));
|
|
364
|
-
}
|