@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
|
@@ -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,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve a cached prepared statement, compiling on first use per db.
|
|
3
|
+
* Each `cache` WeakMap must always be called with the same `sql` —
|
|
4
|
+
* the sql argument is only used on the first compile; subsequent calls
|
|
5
|
+
* return the cached statement regardless of the sql passed.
|
|
6
|
+
*
|
|
7
|
+
* @param {WeakMap} cache - WeakMap keyed by db instance
|
|
8
|
+
* @param {object} db - better-sqlite3 database instance
|
|
9
|
+
* @param {string} sql - SQL to compile on first use
|
|
10
|
+
* @returns {object} prepared statement
|
|
11
|
+
*/
|
|
12
|
+
export function cachedStmt(cache, db, sql) {
|
|
13
|
+
let stmt = cache.get(db);
|
|
14
|
+
if (!stmt) {
|
|
15
|
+
stmt = db.prepare(sql);
|
|
16
|
+
cache.set(db, stmt);
|
|
17
|
+
}
|
|
18
|
+
return stmt;
|
|
19
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { cachedStmt } from './cached-stmt.js';
|
|
2
|
+
|
|
3
|
+
// ─── Statement caches (one prepared statement per db instance) ────────────
|
|
4
|
+
const _getCfgBlocksStmt = new WeakMap();
|
|
5
|
+
const _getCfgEdgesStmt = new WeakMap();
|
|
6
|
+
const _deleteCfgEdgesStmt = new WeakMap();
|
|
7
|
+
const _deleteCfgBlocksStmt = new WeakMap();
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Check whether CFG tables exist.
|
|
11
|
+
* @param {object} db
|
|
12
|
+
* @returns {boolean}
|
|
13
|
+
*/
|
|
14
|
+
export function hasCfgTables(db) {
|
|
15
|
+
try {
|
|
16
|
+
db.prepare('SELECT 1 FROM cfg_blocks LIMIT 0').get();
|
|
17
|
+
return true;
|
|
18
|
+
} catch {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Get CFG blocks for a function node.
|
|
25
|
+
* @param {object} db
|
|
26
|
+
* @param {number} functionNodeId
|
|
27
|
+
* @returns {object[]}
|
|
28
|
+
*/
|
|
29
|
+
export function getCfgBlocks(db, functionNodeId) {
|
|
30
|
+
return cachedStmt(
|
|
31
|
+
_getCfgBlocksStmt,
|
|
32
|
+
db,
|
|
33
|
+
`SELECT id, block_index, block_type, start_line, end_line, label
|
|
34
|
+
FROM cfg_blocks WHERE function_node_id = ?
|
|
35
|
+
ORDER BY block_index`,
|
|
36
|
+
).all(functionNodeId);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Get CFG edges for a function node (with block info).
|
|
41
|
+
* @param {object} db
|
|
42
|
+
* @param {number} functionNodeId
|
|
43
|
+
* @returns {object[]}
|
|
44
|
+
*/
|
|
45
|
+
export function getCfgEdges(db, functionNodeId) {
|
|
46
|
+
return cachedStmt(
|
|
47
|
+
_getCfgEdgesStmt,
|
|
48
|
+
db,
|
|
49
|
+
`SELECT e.kind,
|
|
50
|
+
sb.block_index AS source_index, sb.block_type AS source_type,
|
|
51
|
+
tb.block_index AS target_index, tb.block_type AS target_type
|
|
52
|
+
FROM cfg_edges e
|
|
53
|
+
JOIN cfg_blocks sb ON e.source_block_id = sb.id
|
|
54
|
+
JOIN cfg_blocks tb ON e.target_block_id = tb.id
|
|
55
|
+
WHERE e.function_node_id = ?
|
|
56
|
+
ORDER BY sb.block_index, tb.block_index`,
|
|
57
|
+
).all(functionNodeId);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Delete all CFG data for a function node.
|
|
62
|
+
* @param {object} db
|
|
63
|
+
* @param {number} functionNodeId
|
|
64
|
+
*/
|
|
65
|
+
export function deleteCfgForNode(db, functionNodeId) {
|
|
66
|
+
cachedStmt(_deleteCfgEdgesStmt, db, 'DELETE FROM cfg_edges WHERE function_node_id = ?').run(
|
|
67
|
+
functionNodeId,
|
|
68
|
+
);
|
|
69
|
+
cachedStmt(_deleteCfgBlocksStmt, db, 'DELETE FROM cfg_blocks WHERE function_node_id = ?').run(
|
|
70
|
+
functionNodeId,
|
|
71
|
+
);
|
|
72
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { cachedStmt } from './cached-stmt.js';
|
|
2
|
+
|
|
3
|
+
// ─── Statement caches (one prepared statement per db instance) ────────────
|
|
4
|
+
const _hasCoChangesStmt = new WeakMap();
|
|
5
|
+
const _getCoChangeMetaStmt = new WeakMap();
|
|
6
|
+
const _upsertCoChangeMetaStmt = new WeakMap();
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Check whether the co_changes table has data.
|
|
10
|
+
* @param {object} db
|
|
11
|
+
* @returns {boolean}
|
|
12
|
+
*/
|
|
13
|
+
export function hasCoChanges(db) {
|
|
14
|
+
try {
|
|
15
|
+
return !!cachedStmt(_hasCoChangesStmt, db, 'SELECT 1 FROM co_changes LIMIT 1').get();
|
|
16
|
+
} catch {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Get all co-change metadata as a key-value map.
|
|
23
|
+
* @param {object} db
|
|
24
|
+
* @returns {Record<string, string>}
|
|
25
|
+
*/
|
|
26
|
+
export function getCoChangeMeta(db) {
|
|
27
|
+
const meta = {};
|
|
28
|
+
try {
|
|
29
|
+
for (const row of cachedStmt(
|
|
30
|
+
_getCoChangeMetaStmt,
|
|
31
|
+
db,
|
|
32
|
+
'SELECT key, value FROM co_change_meta',
|
|
33
|
+
).all()) {
|
|
34
|
+
meta[row.key] = row.value;
|
|
35
|
+
}
|
|
36
|
+
} catch {
|
|
37
|
+
/* table may not exist */
|
|
38
|
+
}
|
|
39
|
+
return meta;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Upsert a co-change metadata key-value pair.
|
|
44
|
+
* @param {object} db
|
|
45
|
+
* @param {string} key
|
|
46
|
+
* @param {string} value
|
|
47
|
+
*/
|
|
48
|
+
export function upsertCoChangeMeta(db, key, value) {
|
|
49
|
+
cachedStmt(
|
|
50
|
+
_upsertCoChangeMetaStmt,
|
|
51
|
+
db,
|
|
52
|
+
'INSERT INTO co_change_meta (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value',
|
|
53
|
+
).run(key, value);
|
|
54
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { cachedStmt } from './cached-stmt.js';
|
|
2
|
+
|
|
3
|
+
// ─── Statement caches (one prepared statement per db instance) ────────────
|
|
4
|
+
const _getComplexityForNodeStmt = new WeakMap();
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Get complexity metrics for a node.
|
|
8
|
+
* Used by contextData and explainFunctionImpl in queries.js.
|
|
9
|
+
* @param {object} db
|
|
10
|
+
* @param {number} nodeId
|
|
11
|
+
* @returns {{ cognitive: number, cyclomatic: number, max_nesting: number, maintainability_index: number, halstead_volume: number }|undefined}
|
|
12
|
+
*/
|
|
13
|
+
export function getComplexityForNode(db, nodeId) {
|
|
14
|
+
return cachedStmt(
|
|
15
|
+
_getComplexityForNodeStmt,
|
|
16
|
+
db,
|
|
17
|
+
`SELECT cognitive, cyclomatic, max_nesting, maintainability_index, halstead_volume
|
|
18
|
+
FROM function_complexity WHERE node_id = ?`,
|
|
19
|
+
).get(nodeId);
|
|
20
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { cachedStmt } from './cached-stmt.js';
|
|
2
|
+
|
|
3
|
+
// ─── Statement caches (one prepared statement per db instance) ────────────
|
|
4
|
+
const _hasDataflowTableStmt = new WeakMap();
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Check whether the dataflow table exists and has data.
|
|
8
|
+
* @param {object} db
|
|
9
|
+
* @returns {boolean}
|
|
10
|
+
*/
|
|
11
|
+
export function hasDataflowTable(db) {
|
|
12
|
+
try {
|
|
13
|
+
return cachedStmt(_hasDataflowTableStmt, db, 'SELECT COUNT(*) AS c FROM dataflow').get().c > 0;
|
|
14
|
+
} catch {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import { cachedStmt } from './cached-stmt.js';
|
|
2
|
+
|
|
3
|
+
// ─── Prepared-statement caches (one per db instance) ────────────────────
|
|
4
|
+
const _findCalleesStmt = new WeakMap();
|
|
5
|
+
const _findCallersStmt = new WeakMap();
|
|
6
|
+
const _findDistinctCallersStmt = new WeakMap();
|
|
7
|
+
const _findAllOutgoingStmt = new WeakMap();
|
|
8
|
+
const _findAllIncomingStmt = new WeakMap();
|
|
9
|
+
const _findCalleeNamesStmt = new WeakMap();
|
|
10
|
+
const _findCallerNamesStmt = new WeakMap();
|
|
11
|
+
const _findImportTargetsStmt = new WeakMap();
|
|
12
|
+
const _findImportSourcesStmt = new WeakMap();
|
|
13
|
+
const _findImportDependentsStmt = new WeakMap();
|
|
14
|
+
const _findCrossFileCallTargetsStmt = new WeakMap();
|
|
15
|
+
const _countCrossFileCallersStmt = new WeakMap();
|
|
16
|
+
const _getClassAncestorsStmt = new WeakMap();
|
|
17
|
+
const _findIntraFileCallEdgesStmt = new WeakMap();
|
|
18
|
+
|
|
19
|
+
// ─── Call-edge queries ──────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Find all callees of a node (outgoing 'calls' edges).
|
|
23
|
+
* Returns full node info including end_line for source display.
|
|
24
|
+
* @param {object} db
|
|
25
|
+
* @param {number} nodeId
|
|
26
|
+
* @returns {{ id: number, name: string, kind: string, file: string, line: number, end_line: number|null }[]}
|
|
27
|
+
*/
|
|
28
|
+
export function findCallees(db, nodeId) {
|
|
29
|
+
return cachedStmt(
|
|
30
|
+
_findCalleesStmt,
|
|
31
|
+
db,
|
|
32
|
+
`SELECT DISTINCT n.id, n.name, n.kind, n.file, n.line, n.end_line
|
|
33
|
+
FROM edges e JOIN nodes n ON e.target_id = n.id
|
|
34
|
+
WHERE e.source_id = ? AND e.kind = 'calls'`,
|
|
35
|
+
).all(nodeId);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Find all callers of a node (incoming 'calls' edges).
|
|
40
|
+
* @param {object} db
|
|
41
|
+
* @param {number} nodeId
|
|
42
|
+
* @returns {{ id: number, name: string, kind: string, file: string, line: number }[]}
|
|
43
|
+
*/
|
|
44
|
+
export function findCallers(db, nodeId) {
|
|
45
|
+
return cachedStmt(
|
|
46
|
+
_findCallersStmt,
|
|
47
|
+
db,
|
|
48
|
+
`SELECT n.id, n.name, n.kind, n.file, n.line
|
|
49
|
+
FROM edges e JOIN nodes n ON e.source_id = n.id
|
|
50
|
+
WHERE e.target_id = ? AND e.kind = 'calls'`,
|
|
51
|
+
).all(nodeId);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Find distinct callers of a node (for impact analysis BFS).
|
|
56
|
+
* @param {object} db
|
|
57
|
+
* @param {number} nodeId
|
|
58
|
+
* @returns {{ id: number, name: string, kind: string, file: string, line: number }[]}
|
|
59
|
+
*/
|
|
60
|
+
export function findDistinctCallers(db, nodeId) {
|
|
61
|
+
return cachedStmt(
|
|
62
|
+
_findDistinctCallersStmt,
|
|
63
|
+
db,
|
|
64
|
+
`SELECT DISTINCT n.id, n.name, n.kind, n.file, n.line
|
|
65
|
+
FROM edges e JOIN nodes n ON e.source_id = n.id
|
|
66
|
+
WHERE e.target_id = ? AND e.kind = 'calls'`,
|
|
67
|
+
).all(nodeId);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ─── All-edge queries (no kind filter) ─────────────────────────────────
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Find all outgoing edges with edge kind (for queryNameData).
|
|
74
|
+
* @param {object} db
|
|
75
|
+
* @param {number} nodeId
|
|
76
|
+
* @returns {{ name: string, kind: string, file: string, line: number, edge_kind: string }[]}
|
|
77
|
+
*/
|
|
78
|
+
export function findAllOutgoingEdges(db, nodeId) {
|
|
79
|
+
return cachedStmt(
|
|
80
|
+
_findAllOutgoingStmt,
|
|
81
|
+
db,
|
|
82
|
+
`SELECT n.name, n.kind, n.file, n.line, e.kind AS edge_kind
|
|
83
|
+
FROM edges e JOIN nodes n ON e.target_id = n.id
|
|
84
|
+
WHERE e.source_id = ?`,
|
|
85
|
+
).all(nodeId);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Find all incoming edges with edge kind (for queryNameData).
|
|
90
|
+
* @param {object} db
|
|
91
|
+
* @param {number} nodeId
|
|
92
|
+
* @returns {{ name: string, kind: string, file: string, line: number, edge_kind: string }[]}
|
|
93
|
+
*/
|
|
94
|
+
export function findAllIncomingEdges(db, nodeId) {
|
|
95
|
+
return cachedStmt(
|
|
96
|
+
_findAllIncomingStmt,
|
|
97
|
+
db,
|
|
98
|
+
`SELECT n.name, n.kind, n.file, n.line, e.kind AS edge_kind
|
|
99
|
+
FROM edges e JOIN nodes n ON e.source_id = n.id
|
|
100
|
+
WHERE e.target_id = ?`,
|
|
101
|
+
).all(nodeId);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ─── Name-only callee/caller lookups (for embedder) ────────────────────
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Get distinct callee names for a node, sorted alphabetically.
|
|
108
|
+
* @param {object} db
|
|
109
|
+
* @param {number} nodeId
|
|
110
|
+
* @returns {string[]}
|
|
111
|
+
*/
|
|
112
|
+
export function findCalleeNames(db, nodeId) {
|
|
113
|
+
return cachedStmt(
|
|
114
|
+
_findCalleeNamesStmt,
|
|
115
|
+
db,
|
|
116
|
+
`SELECT DISTINCT n.name
|
|
117
|
+
FROM edges e JOIN nodes n ON e.target_id = n.id
|
|
118
|
+
WHERE e.source_id = ? AND e.kind = 'calls'
|
|
119
|
+
ORDER BY n.name`,
|
|
120
|
+
)
|
|
121
|
+
.all(nodeId)
|
|
122
|
+
.map((r) => r.name);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Get distinct caller names for a node, sorted alphabetically.
|
|
127
|
+
* @param {object} db
|
|
128
|
+
* @param {number} nodeId
|
|
129
|
+
* @returns {string[]}
|
|
130
|
+
*/
|
|
131
|
+
export function findCallerNames(db, nodeId) {
|
|
132
|
+
return cachedStmt(
|
|
133
|
+
_findCallerNamesStmt,
|
|
134
|
+
db,
|
|
135
|
+
`SELECT DISTINCT n.name
|
|
136
|
+
FROM edges e JOIN nodes n ON e.source_id = n.id
|
|
137
|
+
WHERE e.target_id = ? AND e.kind = 'calls'
|
|
138
|
+
ORDER BY n.name`,
|
|
139
|
+
)
|
|
140
|
+
.all(nodeId)
|
|
141
|
+
.map((r) => r.name);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ─── Import-edge queries ───────────────────────────────────────────────
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Find outgoing import edges (files this node imports).
|
|
148
|
+
* @param {object} db
|
|
149
|
+
* @param {number} nodeId
|
|
150
|
+
* @returns {{ file: string, edge_kind: string }[]}
|
|
151
|
+
*/
|
|
152
|
+
export function findImportTargets(db, nodeId) {
|
|
153
|
+
return cachedStmt(
|
|
154
|
+
_findImportTargetsStmt,
|
|
155
|
+
db,
|
|
156
|
+
`SELECT n.file, e.kind AS edge_kind
|
|
157
|
+
FROM edges e JOIN nodes n ON e.target_id = n.id
|
|
158
|
+
WHERE e.source_id = ? AND e.kind IN ('imports', 'imports-type')`,
|
|
159
|
+
).all(nodeId);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Find incoming import edges (files that import this node).
|
|
164
|
+
* @param {object} db
|
|
165
|
+
* @param {number} nodeId
|
|
166
|
+
* @returns {{ file: string, edge_kind: string }[]}
|
|
167
|
+
*/
|
|
168
|
+
export function findImportSources(db, nodeId) {
|
|
169
|
+
return cachedStmt(
|
|
170
|
+
_findImportSourcesStmt,
|
|
171
|
+
db,
|
|
172
|
+
`SELECT n.file, e.kind AS edge_kind
|
|
173
|
+
FROM edges e JOIN nodes n ON e.source_id = n.id
|
|
174
|
+
WHERE e.target_id = ? AND e.kind IN ('imports', 'imports-type')`,
|
|
175
|
+
).all(nodeId);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Find nodes that import a given node (BFS-ready, returns full node info).
|
|
180
|
+
* Used by impactAnalysisData for transitive import traversal.
|
|
181
|
+
* @param {object} db
|
|
182
|
+
* @param {number} nodeId
|
|
183
|
+
* @returns {object[]}
|
|
184
|
+
*/
|
|
185
|
+
export function findImportDependents(db, nodeId) {
|
|
186
|
+
return cachedStmt(
|
|
187
|
+
_findImportDependentsStmt,
|
|
188
|
+
db,
|
|
189
|
+
`SELECT n.* FROM edges e JOIN nodes n ON e.source_id = n.id
|
|
190
|
+
WHERE e.target_id = ? AND e.kind IN ('imports', 'imports-type')`,
|
|
191
|
+
).all(nodeId);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// ─── Cross-file and hierarchy queries ──────────────────────────────────
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Get IDs of symbols in a file that are called from other files.
|
|
198
|
+
* Used for "exported" detection in explain/where/exports.
|
|
199
|
+
* @param {object} db
|
|
200
|
+
* @param {string} file
|
|
201
|
+
* @returns {Set<number>}
|
|
202
|
+
*/
|
|
203
|
+
export function findCrossFileCallTargets(db, file) {
|
|
204
|
+
return new Set(
|
|
205
|
+
cachedStmt(
|
|
206
|
+
_findCrossFileCallTargetsStmt,
|
|
207
|
+
db,
|
|
208
|
+
`SELECT DISTINCT e.target_id FROM edges e
|
|
209
|
+
JOIN nodes caller ON e.source_id = caller.id
|
|
210
|
+
JOIN nodes target ON e.target_id = target.id
|
|
211
|
+
WHERE target.file = ? AND caller.file != ? AND e.kind = 'calls'`,
|
|
212
|
+
)
|
|
213
|
+
.all(file, file)
|
|
214
|
+
.map((r) => r.target_id),
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Count callers that are in a different file than the target node.
|
|
220
|
+
* Used by whereSymbolImpl to determine if a symbol is exported.
|
|
221
|
+
* @param {object} db
|
|
222
|
+
* @param {number} nodeId
|
|
223
|
+
* @param {string} file - The target node's file
|
|
224
|
+
* @returns {number}
|
|
225
|
+
*/
|
|
226
|
+
export function countCrossFileCallers(db, nodeId, file) {
|
|
227
|
+
return cachedStmt(
|
|
228
|
+
_countCrossFileCallersStmt,
|
|
229
|
+
db,
|
|
230
|
+
`SELECT COUNT(*) AS cnt FROM edges e JOIN nodes n ON e.source_id = n.id
|
|
231
|
+
WHERE e.target_id = ? AND e.kind = 'calls' AND n.file != ?`,
|
|
232
|
+
).get(nodeId, file).cnt;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Get all ancestor class IDs via extends edges (BFS).
|
|
237
|
+
* @param {object} db
|
|
238
|
+
* @param {number} classNodeId
|
|
239
|
+
* @returns {Set<number>}
|
|
240
|
+
*/
|
|
241
|
+
export function getClassHierarchy(db, classNodeId) {
|
|
242
|
+
const ancestors = new Set();
|
|
243
|
+
const queue = [classNodeId];
|
|
244
|
+
const stmt = cachedStmt(
|
|
245
|
+
_getClassAncestorsStmt,
|
|
246
|
+
db,
|
|
247
|
+
`SELECT n.id, n.name FROM edges e JOIN nodes n ON e.target_id = n.id
|
|
248
|
+
WHERE e.source_id = ? AND e.kind = 'extends'`,
|
|
249
|
+
);
|
|
250
|
+
while (queue.length > 0) {
|
|
251
|
+
const current = queue.shift();
|
|
252
|
+
const parents = stmt.all(current);
|
|
253
|
+
for (const p of parents) {
|
|
254
|
+
if (!ancestors.has(p.id)) {
|
|
255
|
+
ancestors.add(p.id);
|
|
256
|
+
queue.push(p.id);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return ancestors;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Find intra-file call edges (caller → callee within the same file).
|
|
265
|
+
* Used by explainFileImpl for data flow visualization.
|
|
266
|
+
* @param {object} db
|
|
267
|
+
* @param {string} file
|
|
268
|
+
* @returns {{ caller_name: string, callee_name: string }[]}
|
|
269
|
+
*/
|
|
270
|
+
export function findIntraFileCallEdges(db, file) {
|
|
271
|
+
return cachedStmt(
|
|
272
|
+
_findIntraFileCallEdgesStmt,
|
|
273
|
+
db,
|
|
274
|
+
`SELECT caller.name AS caller_name, callee.name AS callee_name
|
|
275
|
+
FROM edges e
|
|
276
|
+
JOIN nodes caller ON e.source_id = caller.id
|
|
277
|
+
JOIN nodes callee ON e.target_id = callee.id
|
|
278
|
+
WHERE caller.file = ? AND callee.file = ? AND e.kind = 'calls'
|
|
279
|
+
ORDER BY caller.line`,
|
|
280
|
+
).all(file, file);
|
|
281
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { cachedStmt } from './cached-stmt.js';
|
|
2
|
+
|
|
3
|
+
// ─── Statement caches (one prepared statement per db instance) ────────────
|
|
4
|
+
const _hasEmbeddingsStmt = new WeakMap();
|
|
5
|
+
const _getEmbeddingCountStmt = new WeakMap();
|
|
6
|
+
const _getEmbeddingMetaStmt = new WeakMap();
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Check whether the embeddings table has data.
|
|
10
|
+
* @param {object} db
|
|
11
|
+
* @returns {boolean}
|
|
12
|
+
*/
|
|
13
|
+
export function hasEmbeddings(db) {
|
|
14
|
+
try {
|
|
15
|
+
return !!cachedStmt(_hasEmbeddingsStmt, db, 'SELECT 1 FROM embeddings LIMIT 1').get();
|
|
16
|
+
} catch {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Get the count of embeddings.
|
|
23
|
+
* @param {object} db
|
|
24
|
+
* @returns {number}
|
|
25
|
+
*/
|
|
26
|
+
export function getEmbeddingCount(db) {
|
|
27
|
+
try {
|
|
28
|
+
return cachedStmt(_getEmbeddingCountStmt, db, 'SELECT COUNT(*) AS c FROM embeddings').get().c;
|
|
29
|
+
} catch {
|
|
30
|
+
return 0;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Get a single embedding metadata value by key.
|
|
36
|
+
* @param {object} db
|
|
37
|
+
* @param {string} key
|
|
38
|
+
* @returns {string|undefined}
|
|
39
|
+
*/
|
|
40
|
+
export function getEmbeddingMeta(db, key) {
|
|
41
|
+
try {
|
|
42
|
+
const row = cachedStmt(
|
|
43
|
+
_getEmbeddingMetaStmt,
|
|
44
|
+
db,
|
|
45
|
+
'SELECT value FROM embedding_meta WHERE key = ?',
|
|
46
|
+
).get(key);
|
|
47
|
+
return row?.value;
|
|
48
|
+
} catch {
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { cachedStmt } from './cached-stmt.js';
|
|
2
|
+
|
|
3
|
+
// ─── Statement caches (one prepared statement per db instance) ────────────
|
|
4
|
+
const _getCallableNodesStmt = new WeakMap();
|
|
5
|
+
const _getCallEdgesStmt = new WeakMap();
|
|
6
|
+
const _getFileNodesAllStmt = new WeakMap();
|
|
7
|
+
const _getImportEdgesStmt = new WeakMap();
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Get callable nodes (function/method/class) for community detection.
|
|
11
|
+
* @param {object} db
|
|
12
|
+
* @returns {{ id: number, name: string, kind: string, file: string }[]}
|
|
13
|
+
*/
|
|
14
|
+
export function getCallableNodes(db) {
|
|
15
|
+
return cachedStmt(
|
|
16
|
+
_getCallableNodesStmt,
|
|
17
|
+
db,
|
|
18
|
+
"SELECT id, name, kind, file FROM nodes WHERE kind IN ('function','method','class')",
|
|
19
|
+
).all();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Get all 'calls' edges.
|
|
24
|
+
* @param {object} db
|
|
25
|
+
* @returns {{ source_id: number, target_id: number }[]}
|
|
26
|
+
*/
|
|
27
|
+
export function getCallEdges(db) {
|
|
28
|
+
return cachedStmt(
|
|
29
|
+
_getCallEdgesStmt,
|
|
30
|
+
db,
|
|
31
|
+
"SELECT source_id, target_id FROM edges WHERE kind = 'calls'",
|
|
32
|
+
).all();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Get all file-kind nodes.
|
|
37
|
+
* @param {object} db
|
|
38
|
+
* @returns {{ id: number, name: string, file: string }[]}
|
|
39
|
+
*/
|
|
40
|
+
export function getFileNodesAll(db) {
|
|
41
|
+
return cachedStmt(
|
|
42
|
+
_getFileNodesAllStmt,
|
|
43
|
+
db,
|
|
44
|
+
"SELECT id, name, file FROM nodes WHERE kind = 'file'",
|
|
45
|
+
).all();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Get all import edges.
|
|
50
|
+
* @param {object} db
|
|
51
|
+
* @returns {{ source_id: number, target_id: number }[]}
|
|
52
|
+
*/
|
|
53
|
+
export function getImportEdges(db) {
|
|
54
|
+
return cachedStmt(
|
|
55
|
+
_getImportEdgesStmt,
|
|
56
|
+
db,
|
|
57
|
+
"SELECT source_id, target_id FROM edges WHERE kind IN ('imports','imports-type')",
|
|
58
|
+
).all();
|
|
59
|
+
}
|