@optave/codegraph 3.1.1 → 3.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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 +48 -179
- package/src/cfg.js +89 -883
- package/src/check.js +1 -84
- package/src/cli.js +20 -19
- 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 +42 -356
- 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/cfg.js +83 -0
- package/src/db/repository/cochange.js +41 -0
- package/src/db/repository/complexity.js +15 -0
- package/src/db/repository/dataflow.js +12 -0
- package/src/db/repository/edges.js +259 -0
- package/src/db/repository/embeddings.js +40 -0
- package/src/db/repository/graph-read.js +39 -0
- package/src/db/repository/index.js +42 -0
- package/src/db/repository/nodes.js +236 -0
- package/src/db.js +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 +23 -19
- 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/owners.js +1 -56
- 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
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prepare all purge statements once, returning an object of runnable stmts.
|
|
3
|
+
* Optional tables are wrapped in try/catch — if the table doesn't exist,
|
|
4
|
+
* that slot is set to null.
|
|
5
|
+
*
|
|
6
|
+
* @param {object} db - Open read-write database handle
|
|
7
|
+
* @returns {object} prepared statements (some may be null)
|
|
8
|
+
*/
|
|
9
|
+
function preparePurgeStmts(db) {
|
|
10
|
+
const tryPrepare = (sql) => {
|
|
11
|
+
try {
|
|
12
|
+
return db.prepare(sql);
|
|
13
|
+
} catch {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
return {
|
|
19
|
+
embeddings: tryPrepare(
|
|
20
|
+
'DELETE FROM embeddings WHERE node_id IN (SELECT id FROM nodes WHERE file = ?)',
|
|
21
|
+
),
|
|
22
|
+
cfgEdges: tryPrepare(
|
|
23
|
+
'DELETE FROM cfg_edges WHERE function_node_id IN (SELECT id FROM nodes WHERE file = ?)',
|
|
24
|
+
),
|
|
25
|
+
cfgBlocks: tryPrepare(
|
|
26
|
+
'DELETE FROM cfg_blocks WHERE function_node_id IN (SELECT id FROM nodes WHERE file = ?)',
|
|
27
|
+
),
|
|
28
|
+
dataflow: tryPrepare(
|
|
29
|
+
'DELETE FROM dataflow WHERE source_id IN (SELECT id FROM nodes WHERE file = ?) OR target_id IN (SELECT id FROM nodes WHERE file = ?)',
|
|
30
|
+
),
|
|
31
|
+
complexity: tryPrepare(
|
|
32
|
+
'DELETE FROM function_complexity WHERE node_id IN (SELECT id FROM nodes WHERE file = ?)',
|
|
33
|
+
),
|
|
34
|
+
nodeMetrics: tryPrepare(
|
|
35
|
+
'DELETE FROM node_metrics WHERE node_id IN (SELECT id FROM nodes WHERE file = ?)',
|
|
36
|
+
),
|
|
37
|
+
astNodes: tryPrepare('DELETE FROM ast_nodes WHERE file = ?'),
|
|
38
|
+
// Core tables — always exist
|
|
39
|
+
edges: db.prepare(
|
|
40
|
+
'DELETE FROM edges WHERE source_id IN (SELECT id FROM nodes WHERE file = @f) OR target_id IN (SELECT id FROM nodes WHERE file = @f)',
|
|
41
|
+
),
|
|
42
|
+
nodes: db.prepare('DELETE FROM nodes WHERE file = ?'),
|
|
43
|
+
fileHashes: tryPrepare('DELETE FROM file_hashes WHERE file = ?'),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Cascade-delete all graph data for a single file across all tables.
|
|
49
|
+
* Order: dependent tables first, then edges, then nodes, then hashes.
|
|
50
|
+
*
|
|
51
|
+
* @param {object} db - Open read-write database handle
|
|
52
|
+
* @param {string} file - Relative file path to purge
|
|
53
|
+
* @param {object} [opts]
|
|
54
|
+
* @param {boolean} [opts.purgeHashes=true] - Also delete file_hashes entry
|
|
55
|
+
*/
|
|
56
|
+
export function purgeFileData(db, file, opts = {}) {
|
|
57
|
+
const stmts = preparePurgeStmts(db);
|
|
58
|
+
runPurge(stmts, file, opts);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Run purge using pre-prepared statements for a single file.
|
|
63
|
+
* @param {object} stmts - Prepared statements from preparePurgeStmts
|
|
64
|
+
* @param {string} file - Relative file path to purge
|
|
65
|
+
* @param {object} [opts]
|
|
66
|
+
* @param {boolean} [opts.purgeHashes=true]
|
|
67
|
+
*/
|
|
68
|
+
function runPurge(stmts, file, opts = {}) {
|
|
69
|
+
const { purgeHashes = true } = opts;
|
|
70
|
+
|
|
71
|
+
// Optional tables
|
|
72
|
+
stmts.embeddings?.run(file);
|
|
73
|
+
stmts.cfgEdges?.run(file);
|
|
74
|
+
stmts.cfgBlocks?.run(file);
|
|
75
|
+
stmts.dataflow?.run(file, file);
|
|
76
|
+
stmts.complexity?.run(file);
|
|
77
|
+
stmts.nodeMetrics?.run(file);
|
|
78
|
+
stmts.astNodes?.run(file);
|
|
79
|
+
|
|
80
|
+
// Core tables
|
|
81
|
+
stmts.edges.run({ f: file });
|
|
82
|
+
stmts.nodes.run(file);
|
|
83
|
+
|
|
84
|
+
if (purgeHashes) {
|
|
85
|
+
stmts.fileHashes?.run(file);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Purge all graph data for multiple files.
|
|
91
|
+
* Prepares statements once and loops over files for efficiency.
|
|
92
|
+
*
|
|
93
|
+
* @param {object} db - Open read-write database handle
|
|
94
|
+
* @param {string[]} files - Relative file paths to purge
|
|
95
|
+
* @param {object} [opts]
|
|
96
|
+
* @param {boolean} [opts.purgeHashes=true]
|
|
97
|
+
*/
|
|
98
|
+
export function purgeFilesData(db, files, opts = {}) {
|
|
99
|
+
if (!files || files.length === 0) return;
|
|
100
|
+
const stmts = preparePurgeStmts(db);
|
|
101
|
+
for (const file of files) {
|
|
102
|
+
runPurge(stmts, file, opts);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// ─── Statement caches (one prepared statement per db instance) ────────────
|
|
2
|
+
// WeakMap keys on the db object so statements are GC'd when the db closes.
|
|
3
|
+
const _getCfgBlocksStmt = new WeakMap();
|
|
4
|
+
const _getCfgEdgesStmt = new WeakMap();
|
|
5
|
+
const _deleteCfgEdgesStmt = new WeakMap();
|
|
6
|
+
const _deleteCfgBlocksStmt = new WeakMap();
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Check whether CFG tables exist.
|
|
10
|
+
* @param {object} db
|
|
11
|
+
* @returns {boolean}
|
|
12
|
+
*/
|
|
13
|
+
export function hasCfgTables(db) {
|
|
14
|
+
try {
|
|
15
|
+
db.prepare('SELECT 1 FROM cfg_blocks LIMIT 0').get();
|
|
16
|
+
return true;
|
|
17
|
+
} catch {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Get CFG blocks for a function node.
|
|
24
|
+
* @param {object} db
|
|
25
|
+
* @param {number} functionNodeId
|
|
26
|
+
* @returns {object[]}
|
|
27
|
+
*/
|
|
28
|
+
export function getCfgBlocks(db, functionNodeId) {
|
|
29
|
+
let stmt = _getCfgBlocksStmt.get(db);
|
|
30
|
+
if (!stmt) {
|
|
31
|
+
stmt = db.prepare(
|
|
32
|
+
`SELECT id, block_index, block_type, start_line, end_line, label
|
|
33
|
+
FROM cfg_blocks WHERE function_node_id = ?
|
|
34
|
+
ORDER BY block_index`,
|
|
35
|
+
);
|
|
36
|
+
_getCfgBlocksStmt.set(db, stmt);
|
|
37
|
+
}
|
|
38
|
+
return stmt.all(functionNodeId);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Get CFG edges for a function node (with block info).
|
|
43
|
+
* @param {object} db
|
|
44
|
+
* @param {number} functionNodeId
|
|
45
|
+
* @returns {object[]}
|
|
46
|
+
*/
|
|
47
|
+
export function getCfgEdges(db, functionNodeId) {
|
|
48
|
+
let stmt = _getCfgEdgesStmt.get(db);
|
|
49
|
+
if (!stmt) {
|
|
50
|
+
stmt = db.prepare(
|
|
51
|
+
`SELECT e.kind,
|
|
52
|
+
sb.block_index AS source_index, sb.block_type AS source_type,
|
|
53
|
+
tb.block_index AS target_index, tb.block_type AS target_type
|
|
54
|
+
FROM cfg_edges e
|
|
55
|
+
JOIN cfg_blocks sb ON e.source_block_id = sb.id
|
|
56
|
+
JOIN cfg_blocks tb ON e.target_block_id = tb.id
|
|
57
|
+
WHERE e.function_node_id = ?
|
|
58
|
+
ORDER BY sb.block_index, tb.block_index`,
|
|
59
|
+
);
|
|
60
|
+
_getCfgEdgesStmt.set(db, stmt);
|
|
61
|
+
}
|
|
62
|
+
return stmt.all(functionNodeId);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Delete all CFG data for a function node.
|
|
67
|
+
* @param {object} db
|
|
68
|
+
* @param {number} functionNodeId
|
|
69
|
+
*/
|
|
70
|
+
export function deleteCfgForNode(db, functionNodeId) {
|
|
71
|
+
let delEdges = _deleteCfgEdgesStmt.get(db);
|
|
72
|
+
if (!delEdges) {
|
|
73
|
+
delEdges = db.prepare('DELETE FROM cfg_edges WHERE function_node_id = ?');
|
|
74
|
+
_deleteCfgEdgesStmt.set(db, delEdges);
|
|
75
|
+
}
|
|
76
|
+
let delBlocks = _deleteCfgBlocksStmt.get(db);
|
|
77
|
+
if (!delBlocks) {
|
|
78
|
+
delBlocks = db.prepare('DELETE FROM cfg_blocks WHERE function_node_id = ?');
|
|
79
|
+
_deleteCfgBlocksStmt.set(db, delBlocks);
|
|
80
|
+
}
|
|
81
|
+
delEdges.run(functionNodeId);
|
|
82
|
+
delBlocks.run(functionNodeId);
|
|
83
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Check whether the co_changes table has data.
|
|
3
|
+
* @param {object} db
|
|
4
|
+
* @returns {boolean}
|
|
5
|
+
*/
|
|
6
|
+
export function hasCoChanges(db) {
|
|
7
|
+
try {
|
|
8
|
+
return !!db.prepare('SELECT 1 FROM co_changes LIMIT 1').get();
|
|
9
|
+
} catch {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Get all co-change metadata as a key-value map.
|
|
16
|
+
* @param {object} db
|
|
17
|
+
* @returns {Record<string, string>}
|
|
18
|
+
*/
|
|
19
|
+
export function getCoChangeMeta(db) {
|
|
20
|
+
const meta = {};
|
|
21
|
+
try {
|
|
22
|
+
for (const row of db.prepare('SELECT key, value FROM co_change_meta').all()) {
|
|
23
|
+
meta[row.key] = row.value;
|
|
24
|
+
}
|
|
25
|
+
} catch {
|
|
26
|
+
/* table may not exist */
|
|
27
|
+
}
|
|
28
|
+
return meta;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Upsert a co-change metadata key-value pair.
|
|
33
|
+
* @param {object} db
|
|
34
|
+
* @param {string} key
|
|
35
|
+
* @param {string} value
|
|
36
|
+
*/
|
|
37
|
+
export function upsertCoChangeMeta(db, key, value) {
|
|
38
|
+
db.prepare(
|
|
39
|
+
'INSERT INTO co_change_meta (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value',
|
|
40
|
+
).run(key, value);
|
|
41
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Get complexity metrics for a node.
|
|
3
|
+
* Used by contextData and explainFunctionImpl in queries.js.
|
|
4
|
+
* @param {object} db
|
|
5
|
+
* @param {number} nodeId
|
|
6
|
+
* @returns {{ cognitive: number, cyclomatic: number, max_nesting: number, maintainability_index: number, halstead_volume: number }|undefined}
|
|
7
|
+
*/
|
|
8
|
+
export function getComplexityForNode(db, nodeId) {
|
|
9
|
+
return db
|
|
10
|
+
.prepare(
|
|
11
|
+
`SELECT cognitive, cyclomatic, max_nesting, maintainability_index, halstead_volume
|
|
12
|
+
FROM function_complexity WHERE node_id = ?`,
|
|
13
|
+
)
|
|
14
|
+
.get(nodeId);
|
|
15
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Check whether the dataflow table exists and has data.
|
|
3
|
+
* @param {object} db
|
|
4
|
+
* @returns {boolean}
|
|
5
|
+
*/
|
|
6
|
+
export function hasDataflowTable(db) {
|
|
7
|
+
try {
|
|
8
|
+
return db.prepare('SELECT COUNT(*) AS c FROM dataflow').get().c > 0;
|
|
9
|
+
} catch {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
// ─── Call-edge queries ──────────────────────────────────────────────────
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Find all callees of a node (outgoing 'calls' edges).
|
|
5
|
+
* Returns full node info including end_line for source display.
|
|
6
|
+
* @param {object} db
|
|
7
|
+
* @param {number} nodeId
|
|
8
|
+
* @returns {{ id: number, name: string, kind: string, file: string, line: number, end_line: number|null }[]}
|
|
9
|
+
*/
|
|
10
|
+
export function findCallees(db, nodeId) {
|
|
11
|
+
return db
|
|
12
|
+
.prepare(
|
|
13
|
+
`SELECT DISTINCT n.id, n.name, n.kind, n.file, n.line, n.end_line
|
|
14
|
+
FROM edges e JOIN nodes n ON e.target_id = n.id
|
|
15
|
+
WHERE e.source_id = ? AND e.kind = 'calls'`,
|
|
16
|
+
)
|
|
17
|
+
.all(nodeId);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Find all callers of a node (incoming 'calls' edges).
|
|
22
|
+
* @param {object} db
|
|
23
|
+
* @param {number} nodeId
|
|
24
|
+
* @returns {{ id: number, name: string, kind: string, file: string, line: number }[]}
|
|
25
|
+
*/
|
|
26
|
+
export function findCallers(db, nodeId) {
|
|
27
|
+
return db
|
|
28
|
+
.prepare(
|
|
29
|
+
`SELECT n.id, n.name, n.kind, n.file, n.line
|
|
30
|
+
FROM edges e JOIN nodes n ON e.source_id = n.id
|
|
31
|
+
WHERE e.target_id = ? AND e.kind = 'calls'`,
|
|
32
|
+
)
|
|
33
|
+
.all(nodeId);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Find distinct callers of a node (for impact analysis BFS).
|
|
38
|
+
* @param {object} db
|
|
39
|
+
* @param {number} nodeId
|
|
40
|
+
* @returns {{ id: number, name: string, kind: string, file: string, line: number }[]}
|
|
41
|
+
*/
|
|
42
|
+
export function findDistinctCallers(db, nodeId) {
|
|
43
|
+
return db
|
|
44
|
+
.prepare(
|
|
45
|
+
`SELECT DISTINCT n.id, n.name, n.kind, n.file, n.line
|
|
46
|
+
FROM edges e JOIN nodes n ON e.source_id = n.id
|
|
47
|
+
WHERE e.target_id = ? AND e.kind = 'calls'`,
|
|
48
|
+
)
|
|
49
|
+
.all(nodeId);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ─── All-edge queries (no kind filter) ─────────────────────────────────
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Find all outgoing edges with edge kind (for queryNameData).
|
|
56
|
+
* @param {object} db
|
|
57
|
+
* @param {number} nodeId
|
|
58
|
+
* @returns {{ name: string, kind: string, file: string, line: number, edge_kind: string }[]}
|
|
59
|
+
*/
|
|
60
|
+
export function findAllOutgoingEdges(db, nodeId) {
|
|
61
|
+
return db
|
|
62
|
+
.prepare(
|
|
63
|
+
`SELECT n.name, n.kind, n.file, n.line, e.kind AS edge_kind
|
|
64
|
+
FROM edges e JOIN nodes n ON e.target_id = n.id
|
|
65
|
+
WHERE e.source_id = ?`,
|
|
66
|
+
)
|
|
67
|
+
.all(nodeId);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Find all incoming edges with edge kind (for queryNameData).
|
|
72
|
+
* @param {object} db
|
|
73
|
+
* @param {number} nodeId
|
|
74
|
+
* @returns {{ name: string, kind: string, file: string, line: number, edge_kind: string }[]}
|
|
75
|
+
*/
|
|
76
|
+
export function findAllIncomingEdges(db, nodeId) {
|
|
77
|
+
return db
|
|
78
|
+
.prepare(
|
|
79
|
+
`SELECT n.name, n.kind, n.file, n.line, e.kind AS edge_kind
|
|
80
|
+
FROM edges e JOIN nodes n ON e.source_id = n.id
|
|
81
|
+
WHERE e.target_id = ?`,
|
|
82
|
+
)
|
|
83
|
+
.all(nodeId);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ─── Name-only callee/caller lookups (for embedder) ────────────────────
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Get distinct callee names for a node, sorted alphabetically.
|
|
90
|
+
* @param {object} db
|
|
91
|
+
* @param {number} nodeId
|
|
92
|
+
* @returns {string[]}
|
|
93
|
+
*/
|
|
94
|
+
export function findCalleeNames(db, nodeId) {
|
|
95
|
+
return db
|
|
96
|
+
.prepare(
|
|
97
|
+
`SELECT DISTINCT n.name
|
|
98
|
+
FROM edges e JOIN nodes n ON e.target_id = n.id
|
|
99
|
+
WHERE e.source_id = ? AND e.kind = 'calls'
|
|
100
|
+
ORDER BY n.name`,
|
|
101
|
+
)
|
|
102
|
+
.all(nodeId)
|
|
103
|
+
.map((r) => r.name);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Get distinct caller names for a node, sorted alphabetically.
|
|
108
|
+
* @param {object} db
|
|
109
|
+
* @param {number} nodeId
|
|
110
|
+
* @returns {string[]}
|
|
111
|
+
*/
|
|
112
|
+
export function findCallerNames(db, nodeId) {
|
|
113
|
+
return db
|
|
114
|
+
.prepare(
|
|
115
|
+
`SELECT DISTINCT n.name
|
|
116
|
+
FROM edges e JOIN nodes n ON e.source_id = n.id
|
|
117
|
+
WHERE e.target_id = ? AND e.kind = 'calls'
|
|
118
|
+
ORDER BY n.name`,
|
|
119
|
+
)
|
|
120
|
+
.all(nodeId)
|
|
121
|
+
.map((r) => r.name);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ─── Import-edge queries ───────────────────────────────────────────────
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Find outgoing import edges (files this node imports).
|
|
128
|
+
* @param {object} db
|
|
129
|
+
* @param {number} nodeId
|
|
130
|
+
* @returns {{ file: string, edge_kind: string }[]}
|
|
131
|
+
*/
|
|
132
|
+
export function findImportTargets(db, nodeId) {
|
|
133
|
+
return db
|
|
134
|
+
.prepare(
|
|
135
|
+
`SELECT n.file, e.kind AS edge_kind
|
|
136
|
+
FROM edges e JOIN nodes n ON e.target_id = n.id
|
|
137
|
+
WHERE e.source_id = ? AND e.kind IN ('imports', 'imports-type')`,
|
|
138
|
+
)
|
|
139
|
+
.all(nodeId);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Find incoming import edges (files that import this node).
|
|
144
|
+
* @param {object} db
|
|
145
|
+
* @param {number} nodeId
|
|
146
|
+
* @returns {{ file: string, edge_kind: string }[]}
|
|
147
|
+
*/
|
|
148
|
+
export function findImportSources(db, nodeId) {
|
|
149
|
+
return db
|
|
150
|
+
.prepare(
|
|
151
|
+
`SELECT n.file, e.kind AS edge_kind
|
|
152
|
+
FROM edges e JOIN nodes n ON e.source_id = n.id
|
|
153
|
+
WHERE e.target_id = ? AND e.kind IN ('imports', 'imports-type')`,
|
|
154
|
+
)
|
|
155
|
+
.all(nodeId);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Find nodes that import a given node (BFS-ready, returns full node info).
|
|
160
|
+
* Used by impactAnalysisData for transitive import traversal.
|
|
161
|
+
* @param {object} db
|
|
162
|
+
* @param {number} nodeId
|
|
163
|
+
* @returns {object[]}
|
|
164
|
+
*/
|
|
165
|
+
export function findImportDependents(db, nodeId) {
|
|
166
|
+
return db
|
|
167
|
+
.prepare(
|
|
168
|
+
`SELECT n.* FROM edges e JOIN nodes n ON e.source_id = n.id
|
|
169
|
+
WHERE e.target_id = ? AND e.kind IN ('imports', 'imports-type')`,
|
|
170
|
+
)
|
|
171
|
+
.all(nodeId);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ─── Cross-file and hierarchy queries ──────────────────────────────────
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Get IDs of symbols in a file that are called from other files.
|
|
178
|
+
* Used for "exported" detection in explain/where/exports.
|
|
179
|
+
* @param {object} db
|
|
180
|
+
* @param {string} file
|
|
181
|
+
* @returns {Set<number>}
|
|
182
|
+
*/
|
|
183
|
+
export function findCrossFileCallTargets(db, file) {
|
|
184
|
+
return new Set(
|
|
185
|
+
db
|
|
186
|
+
.prepare(
|
|
187
|
+
`SELECT DISTINCT e.target_id FROM edges e
|
|
188
|
+
JOIN nodes caller ON e.source_id = caller.id
|
|
189
|
+
JOIN nodes target ON e.target_id = target.id
|
|
190
|
+
WHERE target.file = ? AND caller.file != ? AND e.kind = 'calls'`,
|
|
191
|
+
)
|
|
192
|
+
.all(file, file)
|
|
193
|
+
.map((r) => r.target_id),
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Count callers that are in a different file than the target node.
|
|
199
|
+
* Used by whereSymbolImpl to determine if a symbol is exported.
|
|
200
|
+
* @param {object} db
|
|
201
|
+
* @param {number} nodeId
|
|
202
|
+
* @param {string} file - The target node's file
|
|
203
|
+
* @returns {number}
|
|
204
|
+
*/
|
|
205
|
+
export function countCrossFileCallers(db, nodeId, file) {
|
|
206
|
+
return db
|
|
207
|
+
.prepare(
|
|
208
|
+
`SELECT COUNT(*) AS cnt FROM edges e JOIN nodes n ON e.source_id = n.id
|
|
209
|
+
WHERE e.target_id = ? AND e.kind = 'calls' AND n.file != ?`,
|
|
210
|
+
)
|
|
211
|
+
.get(nodeId, file).cnt;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Get all ancestor class IDs via extends edges (BFS).
|
|
216
|
+
* @param {object} db
|
|
217
|
+
* @param {number} classNodeId
|
|
218
|
+
* @returns {Set<number>}
|
|
219
|
+
*/
|
|
220
|
+
export function getClassHierarchy(db, classNodeId) {
|
|
221
|
+
const ancestors = new Set();
|
|
222
|
+
const queue = [classNodeId];
|
|
223
|
+
while (queue.length > 0) {
|
|
224
|
+
const current = queue.shift();
|
|
225
|
+
const parents = db
|
|
226
|
+
.prepare(
|
|
227
|
+
`SELECT n.id, n.name FROM edges e JOIN nodes n ON e.target_id = n.id
|
|
228
|
+
WHERE e.source_id = ? AND e.kind = 'extends'`,
|
|
229
|
+
)
|
|
230
|
+
.all(current);
|
|
231
|
+
for (const p of parents) {
|
|
232
|
+
if (!ancestors.has(p.id)) {
|
|
233
|
+
ancestors.add(p.id);
|
|
234
|
+
queue.push(p.id);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return ancestors;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Find intra-file call edges (caller → callee within the same file).
|
|
243
|
+
* Used by explainFileImpl for data flow visualization.
|
|
244
|
+
* @param {object} db
|
|
245
|
+
* @param {string} file
|
|
246
|
+
* @returns {{ caller_name: string, callee_name: string }[]}
|
|
247
|
+
*/
|
|
248
|
+
export function findIntraFileCallEdges(db, file) {
|
|
249
|
+
return db
|
|
250
|
+
.prepare(
|
|
251
|
+
`SELECT caller.name AS caller_name, callee.name AS callee_name
|
|
252
|
+
FROM edges e
|
|
253
|
+
JOIN nodes caller ON e.source_id = caller.id
|
|
254
|
+
JOIN nodes callee ON e.target_id = callee.id
|
|
255
|
+
WHERE caller.file = ? AND callee.file = ? AND e.kind = 'calls'
|
|
256
|
+
ORDER BY caller.line`,
|
|
257
|
+
)
|
|
258
|
+
.all(file, file);
|
|
259
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Check whether the embeddings table has data.
|
|
3
|
+
* @param {object} db
|
|
4
|
+
* @returns {boolean}
|
|
5
|
+
*/
|
|
6
|
+
export function hasEmbeddings(db) {
|
|
7
|
+
try {
|
|
8
|
+
return !!db.prepare('SELECT 1 FROM embeddings LIMIT 1').get();
|
|
9
|
+
} catch {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Get the count of embeddings.
|
|
16
|
+
* @param {object} db
|
|
17
|
+
* @returns {number}
|
|
18
|
+
*/
|
|
19
|
+
export function getEmbeddingCount(db) {
|
|
20
|
+
try {
|
|
21
|
+
return db.prepare('SELECT COUNT(*) AS c FROM embeddings').get().c;
|
|
22
|
+
} catch {
|
|
23
|
+
return 0;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Get a single embedding metadata value by key.
|
|
29
|
+
* @param {object} db
|
|
30
|
+
* @param {string} key
|
|
31
|
+
* @returns {string|undefined}
|
|
32
|
+
*/
|
|
33
|
+
export function getEmbeddingMeta(db, key) {
|
|
34
|
+
try {
|
|
35
|
+
const row = db.prepare('SELECT value FROM embedding_meta WHERE key = ?').get(key);
|
|
36
|
+
return row?.value;
|
|
37
|
+
} catch {
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Get callable nodes (function/method/class) for community detection.
|
|
3
|
+
* @param {object} db
|
|
4
|
+
* @returns {{ id: number, name: string, kind: string, file: string }[]}
|
|
5
|
+
*/
|
|
6
|
+
export function getCallableNodes(db) {
|
|
7
|
+
return db
|
|
8
|
+
.prepare("SELECT id, name, kind, file FROM nodes WHERE kind IN ('function','method','class')")
|
|
9
|
+
.all();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Get all 'calls' edges.
|
|
14
|
+
* @param {object} db
|
|
15
|
+
* @returns {{ source_id: number, target_id: number }[]}
|
|
16
|
+
*/
|
|
17
|
+
export function getCallEdges(db) {
|
|
18
|
+
return db.prepare("SELECT source_id, target_id FROM edges WHERE kind = 'calls'").all();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Get all file-kind nodes.
|
|
23
|
+
* @param {object} db
|
|
24
|
+
* @returns {{ id: number, name: string, file: string }[]}
|
|
25
|
+
*/
|
|
26
|
+
export function getFileNodesAll(db) {
|
|
27
|
+
return db.prepare("SELECT id, name, file FROM nodes WHERE kind = 'file'").all();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Get all import edges.
|
|
32
|
+
* @param {object} db
|
|
33
|
+
* @returns {{ source_id: number, target_id: number }[]}
|
|
34
|
+
*/
|
|
35
|
+
export function getImportEdges(db) {
|
|
36
|
+
return db
|
|
37
|
+
.prepare("SELECT source_id, target_id FROM edges WHERE kind IN ('imports','imports-type')")
|
|
38
|
+
.all();
|
|
39
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Barrel re-export for repository/ modules.
|
|
2
|
+
|
|
3
|
+
export { purgeFileData, purgeFilesData } from './build-stmts.js';
|
|
4
|
+
export { deleteCfgForNode, getCfgBlocks, getCfgEdges, hasCfgTables } from './cfg.js';
|
|
5
|
+
export { getCoChangeMeta, hasCoChanges, upsertCoChangeMeta } from './cochange.js';
|
|
6
|
+
|
|
7
|
+
export { getComplexityForNode } from './complexity.js';
|
|
8
|
+
export { hasDataflowTable } from './dataflow.js';
|
|
9
|
+
export {
|
|
10
|
+
countCrossFileCallers,
|
|
11
|
+
findAllIncomingEdges,
|
|
12
|
+
findAllOutgoingEdges,
|
|
13
|
+
findCalleeNames,
|
|
14
|
+
findCallees,
|
|
15
|
+
findCallerNames,
|
|
16
|
+
findCallers,
|
|
17
|
+
findCrossFileCallTargets,
|
|
18
|
+
findDistinctCallers,
|
|
19
|
+
findImportDependents,
|
|
20
|
+
findImportSources,
|
|
21
|
+
findImportTargets,
|
|
22
|
+
findIntraFileCallEdges,
|
|
23
|
+
getClassHierarchy,
|
|
24
|
+
} from './edges.js';
|
|
25
|
+
export { getEmbeddingCount, getEmbeddingMeta, hasEmbeddings } from './embeddings.js';
|
|
26
|
+
export { getCallableNodes, getCallEdges, getFileNodesAll, getImportEdges } from './graph-read.js';
|
|
27
|
+
export {
|
|
28
|
+
bulkNodeIdsByFile,
|
|
29
|
+
countEdges,
|
|
30
|
+
countFiles,
|
|
31
|
+
countNodes,
|
|
32
|
+
findFileNodes,
|
|
33
|
+
findNodeById,
|
|
34
|
+
findNodeChildren,
|
|
35
|
+
findNodesByFile,
|
|
36
|
+
findNodesForTriage,
|
|
37
|
+
findNodesWithFanIn,
|
|
38
|
+
getFunctionNodeId,
|
|
39
|
+
getNodeId,
|
|
40
|
+
iterateFunctionNodes,
|
|
41
|
+
listFunctionNodes,
|
|
42
|
+
} from './nodes.js';
|