@optave/codegraph 3.1.2 → 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/builder.js +14 -1
- package/src/cli.js +11 -3
- package/src/complexity.js +1 -1
- package/src/db/repository/cached-stmt.js +19 -0
- package/src/db/repository/cfg.js +27 -38
- package/src/db/repository/cochange.js +16 -3
- package/src/db/repository/complexity.js +11 -6
- package/src/db/repository/dataflow.js +6 -1
- package/src/db/repository/edges.js +120 -98
- package/src/db/repository/embeddings.js +14 -3
- package/src/db/repository/graph-read.js +28 -8
- package/src/db/repository/index.js +1 -0
- package/src/db/repository/nodes.js +48 -37
- package/src/index.js +7 -1
- package/src/native.js +31 -9
- package/src/parser.js +53 -2
package/README.md
CHANGED
|
@@ -562,14 +562,14 @@ Self-measured on every release via CI ([build benchmarks](generated/benchmarks/B
|
|
|
562
562
|
|
|
563
563
|
| Metric | Latest |
|
|
564
564
|
|---|---|
|
|
565
|
-
| Build speed (native) | **
|
|
566
|
-
| Build speed (WASM) | **
|
|
567
|
-
| Query time | **
|
|
568
|
-
| No-op rebuild (native) | **
|
|
569
|
-
| 1-file rebuild (native) | **
|
|
565
|
+
| Build speed (native) | **5.2 ms/file** |
|
|
566
|
+
| Build speed (WASM) | **15 ms/file** |
|
|
567
|
+
| Query time | **4ms** |
|
|
568
|
+
| No-op rebuild (native) | **6ms** |
|
|
569
|
+
| 1-file rebuild (native) | **296ms** |
|
|
570
570
|
| Query: fn-deps | **0.8ms** |
|
|
571
571
|
| Query: path | **0.8ms** |
|
|
572
|
-
| ~50,000 files (est.) | **~
|
|
572
|
+
| ~50,000 files (est.) | **~260.0s build** |
|
|
573
573
|
|
|
574
574
|
Metrics are normalized per file for cross-version comparability. Times above are for a full initial build — incremental rebuilds only re-parse changed files.
|
|
575
575
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@optave/codegraph",
|
|
3
|
-
"version": "3.1.
|
|
3
|
+
"version": "3.1.3",
|
|
4
4
|
"description": "Local code graph CLI — parse codebases with tree-sitter, build dependency graphs, query them",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -71,12 +71,12 @@
|
|
|
71
71
|
},
|
|
72
72
|
"optionalDependencies": {
|
|
73
73
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
74
|
-
"@optave/codegraph-darwin-arm64": "3.1.
|
|
75
|
-
"@optave/codegraph-darwin-x64": "3.1.
|
|
76
|
-
"@optave/codegraph-linux-arm64-gnu": "3.1.
|
|
77
|
-
"@optave/codegraph-linux-x64-gnu": "3.1.
|
|
78
|
-
"@optave/codegraph-linux-x64-musl": "3.1.
|
|
79
|
-
"@optave/codegraph-win32-x64-msvc": "3.1.
|
|
74
|
+
"@optave/codegraph-darwin-arm64": "3.1.3",
|
|
75
|
+
"@optave/codegraph-darwin-x64": "3.1.3",
|
|
76
|
+
"@optave/codegraph-linux-arm64-gnu": "3.1.3",
|
|
77
|
+
"@optave/codegraph-linux-x64-gnu": "3.1.3",
|
|
78
|
+
"@optave/codegraph-linux-x64-musl": "3.1.3",
|
|
79
|
+
"@optave/codegraph-win32-x64-msvc": "3.1.3"
|
|
80
80
|
},
|
|
81
81
|
"devDependencies": {
|
|
82
82
|
"@biomejs/biome": "^2.4.4",
|
package/src/builder.js
CHANGED
|
@@ -364,6 +364,7 @@ export function purgeFilesFromGraph(db, files, options = {}) {
|
|
|
364
364
|
}
|
|
365
365
|
|
|
366
366
|
export async function buildGraph(rootDir, opts = {}) {
|
|
367
|
+
const _t_buildStart = performance.now();
|
|
367
368
|
rootDir = path.resolve(rootDir);
|
|
368
369
|
const dbPath = path.join(rootDir, '.codegraph', 'graph.db');
|
|
369
370
|
const db = openDb(dbPath);
|
|
@@ -668,6 +669,7 @@ export async function buildGraph(rootDir, opts = {}) {
|
|
|
668
669
|
|
|
669
670
|
// ── Phase timing ────────────────────────────────────────────────────
|
|
670
671
|
const _t = {};
|
|
672
|
+
_t.setupMs = performance.now() - _t_buildStart;
|
|
671
673
|
|
|
672
674
|
// ── Unified parse via parseFilesAuto ───────────────────────────────
|
|
673
675
|
const filePaths = filesToParse.map((item) => item.file);
|
|
@@ -1350,8 +1352,15 @@ export async function buildGraph(rootDir, opts = {}) {
|
|
|
1350
1352
|
}
|
|
1351
1353
|
}
|
|
1352
1354
|
|
|
1353
|
-
|
|
1355
|
+
_t.finalize0 = performance.now();
|
|
1356
|
+
|
|
1357
|
+
// Release any remaining cached WASM trees — call .delete() to free WASM memory
|
|
1354
1358
|
for (const [, symbols] of allSymbols) {
|
|
1359
|
+
if (symbols._tree && typeof symbols._tree.delete === 'function') {
|
|
1360
|
+
try {
|
|
1361
|
+
symbols._tree.delete();
|
|
1362
|
+
} catch {}
|
|
1363
|
+
}
|
|
1355
1364
|
symbols._tree = null;
|
|
1356
1365
|
symbols._langId = null;
|
|
1357
1366
|
}
|
|
@@ -1456,8 +1465,11 @@ export async function buildGraph(rootDir, opts = {}) {
|
|
|
1456
1465
|
}
|
|
1457
1466
|
}
|
|
1458
1467
|
|
|
1468
|
+
_t.finalizeMs = performance.now() - _t.finalize0;
|
|
1469
|
+
|
|
1459
1470
|
return {
|
|
1460
1471
|
phases: {
|
|
1472
|
+
setupMs: +_t.setupMs.toFixed(1),
|
|
1461
1473
|
parseMs: +_t.parseMs.toFixed(1),
|
|
1462
1474
|
insertMs: +_t.insertMs.toFixed(1),
|
|
1463
1475
|
resolveMs: +_t.resolveMs.toFixed(1),
|
|
@@ -1468,6 +1480,7 @@ export async function buildGraph(rootDir, opts = {}) {
|
|
|
1468
1480
|
complexityMs: +_t.complexityMs.toFixed(1),
|
|
1469
1481
|
...(_t.cfgMs != null && { cfgMs: +_t.cfgMs.toFixed(1) }),
|
|
1470
1482
|
...(_t.dataflowMs != null && { dataflowMs: +_t.dataflowMs.toFixed(1) }),
|
|
1483
|
+
finalizeMs: +_t.finalizeMs.toFixed(1),
|
|
1471
1484
|
},
|
|
1472
1485
|
};
|
|
1473
1486
|
}
|
package/src/cli.js
CHANGED
|
@@ -1390,7 +1390,7 @@ program
|
|
|
1390
1390
|
.command('info')
|
|
1391
1391
|
.description('Show codegraph engine info and diagnostics')
|
|
1392
1392
|
.action(async () => {
|
|
1393
|
-
const { isNativeAvailable, loadNative } = await import('./native.js');
|
|
1393
|
+
const { getNativePackageVersion, isNativeAvailable, loadNative } = await import('./native.js');
|
|
1394
1394
|
const { getActiveEngine } = await import('./parser.js');
|
|
1395
1395
|
|
|
1396
1396
|
const engine = program.opts().engine;
|
|
@@ -1405,9 +1405,17 @@ program
|
|
|
1405
1405
|
console.log(` Native engine : ${nativeAvailable ? 'available' : 'unavailable'}`);
|
|
1406
1406
|
if (nativeAvailable) {
|
|
1407
1407
|
const native = loadNative();
|
|
1408
|
-
const
|
|
1408
|
+
const binaryVersion =
|
|
1409
1409
|
typeof native.engineVersion === 'function' ? native.engineVersion() : 'unknown';
|
|
1410
|
-
|
|
1410
|
+
const pkgVersion = getNativePackageVersion();
|
|
1411
|
+
const knownBinaryVersion = binaryVersion !== 'unknown' ? binaryVersion : null;
|
|
1412
|
+
if (pkgVersion && knownBinaryVersion && pkgVersion !== knownBinaryVersion) {
|
|
1413
|
+
console.log(
|
|
1414
|
+
` Native version: ${pkgVersion} (binary reports ${knownBinaryVersion} — stale)`,
|
|
1415
|
+
);
|
|
1416
|
+
} else {
|
|
1417
|
+
console.log(` Native version: ${pkgVersion ?? binaryVersion}`);
|
|
1418
|
+
}
|
|
1411
1419
|
}
|
|
1412
1420
|
console.log(` Engine flag : --engine ${engine}`);
|
|
1413
1421
|
console.log(` Active engine : ${activeName}${activeVersion ? ` (v${activeVersion})` : ''}`);
|
package/src/complexity.js
CHANGED
|
@@ -454,7 +454,7 @@ export async function buildComplexityMetrics(db, fileSymbols, rootDir, _engineOp
|
|
|
454
454
|
// Fallback: compute from AST tree
|
|
455
455
|
if (!tree || !rules) continue;
|
|
456
456
|
|
|
457
|
-
const funcNode =
|
|
457
|
+
const funcNode = _findFunctionNode(tree.rootNode, def.line, def.endLine, rules);
|
|
458
458
|
if (!funcNode) continue;
|
|
459
459
|
|
|
460
460
|
// Single-pass: complexity + Halstead + LOC + MI in one DFS walk
|
|
@@ -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
|
+
}
|
package/src/db/repository/cfg.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { cachedStmt } from './cached-stmt.js';
|
|
2
|
+
|
|
1
3
|
// ─── 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
4
|
const _getCfgBlocksStmt = new WeakMap();
|
|
4
5
|
const _getCfgEdgesStmt = new WeakMap();
|
|
5
6
|
const _deleteCfgEdgesStmt = new WeakMap();
|
|
@@ -26,16 +27,13 @@ export function hasCfgTables(db) {
|
|
|
26
27
|
* @returns {object[]}
|
|
27
28
|
*/
|
|
28
29
|
export function getCfgBlocks(db, functionNodeId) {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
_getCfgBlocksStmt.set(db, stmt);
|
|
37
|
-
}
|
|
38
|
-
return stmt.all(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);
|
|
39
37
|
}
|
|
40
38
|
|
|
41
39
|
/**
|
|
@@ -45,21 +43,18 @@ export function getCfgBlocks(db, functionNodeId) {
|
|
|
45
43
|
* @returns {object[]}
|
|
46
44
|
*/
|
|
47
45
|
export function getCfgEdges(db, functionNodeId) {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
_getCfgEdgesStmt.set(db, stmt);
|
|
61
|
-
}
|
|
62
|
-
return stmt.all(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);
|
|
63
58
|
}
|
|
64
59
|
|
|
65
60
|
/**
|
|
@@ -68,16 +63,10 @@ export function getCfgEdges(db, functionNodeId) {
|
|
|
68
63
|
* @param {number} functionNodeId
|
|
69
64
|
*/
|
|
70
65
|
export function deleteCfgForNode(db, functionNodeId) {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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);
|
|
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
|
+
);
|
|
83
72
|
}
|
|
@@ -1,3 +1,10 @@
|
|
|
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
|
+
|
|
1
8
|
/**
|
|
2
9
|
* Check whether the co_changes table has data.
|
|
3
10
|
* @param {object} db
|
|
@@ -5,7 +12,7 @@
|
|
|
5
12
|
*/
|
|
6
13
|
export function hasCoChanges(db) {
|
|
7
14
|
try {
|
|
8
|
-
return !!db
|
|
15
|
+
return !!cachedStmt(_hasCoChangesStmt, db, 'SELECT 1 FROM co_changes LIMIT 1').get();
|
|
9
16
|
} catch {
|
|
10
17
|
return false;
|
|
11
18
|
}
|
|
@@ -19,7 +26,11 @@ export function hasCoChanges(db) {
|
|
|
19
26
|
export function getCoChangeMeta(db) {
|
|
20
27
|
const meta = {};
|
|
21
28
|
try {
|
|
22
|
-
for (const row of
|
|
29
|
+
for (const row of cachedStmt(
|
|
30
|
+
_getCoChangeMetaStmt,
|
|
31
|
+
db,
|
|
32
|
+
'SELECT key, value FROM co_change_meta',
|
|
33
|
+
).all()) {
|
|
23
34
|
meta[row.key] = row.value;
|
|
24
35
|
}
|
|
25
36
|
} catch {
|
|
@@ -35,7 +46,9 @@ export function getCoChangeMeta(db) {
|
|
|
35
46
|
* @param {string} value
|
|
36
47
|
*/
|
|
37
48
|
export function upsertCoChangeMeta(db, key, value) {
|
|
38
|
-
|
|
49
|
+
cachedStmt(
|
|
50
|
+
_upsertCoChangeMetaStmt,
|
|
51
|
+
db,
|
|
39
52
|
'INSERT INTO co_change_meta (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value',
|
|
40
53
|
).run(key, value);
|
|
41
54
|
}
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import { cachedStmt } from './cached-stmt.js';
|
|
2
|
+
|
|
3
|
+
// ─── Statement caches (one prepared statement per db instance) ────────────
|
|
4
|
+
const _getComplexityForNodeStmt = new WeakMap();
|
|
5
|
+
|
|
1
6
|
/**
|
|
2
7
|
* Get complexity metrics for a node.
|
|
3
8
|
* Used by contextData and explainFunctionImpl in queries.js.
|
|
@@ -6,10 +11,10 @@
|
|
|
6
11
|
* @returns {{ cognitive: number, cyclomatic: number, max_nesting: number, maintainability_index: number, halstead_volume: number }|undefined}
|
|
7
12
|
*/
|
|
8
13
|
export function getComplexityForNode(db, nodeId) {
|
|
9
|
-
return
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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);
|
|
15
20
|
}
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import { cachedStmt } from './cached-stmt.js';
|
|
2
|
+
|
|
3
|
+
// ─── Statement caches (one prepared statement per db instance) ────────────
|
|
4
|
+
const _hasDataflowTableStmt = new WeakMap();
|
|
5
|
+
|
|
1
6
|
/**
|
|
2
7
|
* Check whether the dataflow table exists and has data.
|
|
3
8
|
* @param {object} db
|
|
@@ -5,7 +10,7 @@
|
|
|
5
10
|
*/
|
|
6
11
|
export function hasDataflowTable(db) {
|
|
7
12
|
try {
|
|
8
|
-
return db
|
|
13
|
+
return cachedStmt(_hasDataflowTableStmt, db, 'SELECT COUNT(*) AS c FROM dataflow').get().c > 0;
|
|
9
14
|
} catch {
|
|
10
15
|
return false;
|
|
11
16
|
}
|
|
@@ -1,3 +1,21 @@
|
|
|
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
|
+
|
|
1
19
|
// ─── Call-edge queries ──────────────────────────────────────────────────
|
|
2
20
|
|
|
3
21
|
/**
|
|
@@ -8,13 +26,13 @@
|
|
|
8
26
|
* @returns {{ id: number, name: string, kind: string, file: string, line: number, end_line: number|null }[]}
|
|
9
27
|
*/
|
|
10
28
|
export function findCallees(db, nodeId) {
|
|
11
|
-
return
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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);
|
|
18
36
|
}
|
|
19
37
|
|
|
20
38
|
/**
|
|
@@ -24,13 +42,13 @@ export function findCallees(db, nodeId) {
|
|
|
24
42
|
* @returns {{ id: number, name: string, kind: string, file: string, line: number }[]}
|
|
25
43
|
*/
|
|
26
44
|
export function findCallers(db, nodeId) {
|
|
27
|
-
return
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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);
|
|
34
52
|
}
|
|
35
53
|
|
|
36
54
|
/**
|
|
@@ -40,13 +58,13 @@ export function findCallers(db, nodeId) {
|
|
|
40
58
|
* @returns {{ id: number, name: string, kind: string, file: string, line: number }[]}
|
|
41
59
|
*/
|
|
42
60
|
export function findDistinctCallers(db, nodeId) {
|
|
43
|
-
return
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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);
|
|
50
68
|
}
|
|
51
69
|
|
|
52
70
|
// ─── All-edge queries (no kind filter) ─────────────────────────────────
|
|
@@ -58,13 +76,13 @@ export function findDistinctCallers(db, nodeId) {
|
|
|
58
76
|
* @returns {{ name: string, kind: string, file: string, line: number, edge_kind: string }[]}
|
|
59
77
|
*/
|
|
60
78
|
export function findAllOutgoingEdges(db, nodeId) {
|
|
61
|
-
return
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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);
|
|
68
86
|
}
|
|
69
87
|
|
|
70
88
|
/**
|
|
@@ -74,13 +92,13 @@ export function findAllOutgoingEdges(db, nodeId) {
|
|
|
74
92
|
* @returns {{ name: string, kind: string, file: string, line: number, edge_kind: string }[]}
|
|
75
93
|
*/
|
|
76
94
|
export function findAllIncomingEdges(db, nodeId) {
|
|
77
|
-
return
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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);
|
|
84
102
|
}
|
|
85
103
|
|
|
86
104
|
// ─── Name-only callee/caller lookups (for embedder) ────────────────────
|
|
@@ -92,13 +110,14 @@ export function findAllIncomingEdges(db, nodeId) {
|
|
|
92
110
|
* @returns {string[]}
|
|
93
111
|
*/
|
|
94
112
|
export function findCalleeNames(db, nodeId) {
|
|
95
|
-
return
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
+
)
|
|
102
121
|
.all(nodeId)
|
|
103
122
|
.map((r) => r.name);
|
|
104
123
|
}
|
|
@@ -110,13 +129,14 @@ export function findCalleeNames(db, nodeId) {
|
|
|
110
129
|
* @returns {string[]}
|
|
111
130
|
*/
|
|
112
131
|
export function findCallerNames(db, nodeId) {
|
|
113
|
-
return
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
+
)
|
|
120
140
|
.all(nodeId)
|
|
121
141
|
.map((r) => r.name);
|
|
122
142
|
}
|
|
@@ -130,13 +150,13 @@ export function findCallerNames(db, nodeId) {
|
|
|
130
150
|
* @returns {{ file: string, edge_kind: string }[]}
|
|
131
151
|
*/
|
|
132
152
|
export function findImportTargets(db, nodeId) {
|
|
133
|
-
return
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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);
|
|
140
160
|
}
|
|
141
161
|
|
|
142
162
|
/**
|
|
@@ -146,13 +166,13 @@ export function findImportTargets(db, nodeId) {
|
|
|
146
166
|
* @returns {{ file: string, edge_kind: string }[]}
|
|
147
167
|
*/
|
|
148
168
|
export function findImportSources(db, nodeId) {
|
|
149
|
-
return
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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);
|
|
156
176
|
}
|
|
157
177
|
|
|
158
178
|
/**
|
|
@@ -163,12 +183,12 @@ export function findImportSources(db, nodeId) {
|
|
|
163
183
|
* @returns {object[]}
|
|
164
184
|
*/
|
|
165
185
|
export function findImportDependents(db, nodeId) {
|
|
166
|
-
return
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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);
|
|
172
192
|
}
|
|
173
193
|
|
|
174
194
|
// ─── Cross-file and hierarchy queries ──────────────────────────────────
|
|
@@ -182,13 +202,14 @@ export function findImportDependents(db, nodeId) {
|
|
|
182
202
|
*/
|
|
183
203
|
export function findCrossFileCallTargets(db, file) {
|
|
184
204
|
return new Set(
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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
|
+
)
|
|
192
213
|
.all(file, file)
|
|
193
214
|
.map((r) => r.target_id),
|
|
194
215
|
);
|
|
@@ -203,12 +224,12 @@ export function findCrossFileCallTargets(db, file) {
|
|
|
203
224
|
* @returns {number}
|
|
204
225
|
*/
|
|
205
226
|
export function countCrossFileCallers(db, nodeId, file) {
|
|
206
|
-
return
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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;
|
|
212
233
|
}
|
|
213
234
|
|
|
214
235
|
/**
|
|
@@ -220,14 +241,15 @@ export function countCrossFileCallers(db, nodeId, file) {
|
|
|
220
241
|
export function getClassHierarchy(db, classNodeId) {
|
|
221
242
|
const ancestors = new Set();
|
|
222
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
|
+
);
|
|
223
250
|
while (queue.length > 0) {
|
|
224
251
|
const current = queue.shift();
|
|
225
|
-
const parents =
|
|
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);
|
|
252
|
+
const parents = stmt.all(current);
|
|
231
253
|
for (const p of parents) {
|
|
232
254
|
if (!ancestors.has(p.id)) {
|
|
233
255
|
ancestors.add(p.id);
|
|
@@ -246,14 +268,14 @@ export function getClassHierarchy(db, classNodeId) {
|
|
|
246
268
|
* @returns {{ caller_name: string, callee_name: string }[]}
|
|
247
269
|
*/
|
|
248
270
|
export function findIntraFileCallEdges(db, file) {
|
|
249
|
-
return
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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);
|
|
259
281
|
}
|
|
@@ -1,3 +1,10 @@
|
|
|
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
|
+
|
|
1
8
|
/**
|
|
2
9
|
* Check whether the embeddings table has data.
|
|
3
10
|
* @param {object} db
|
|
@@ -5,7 +12,7 @@
|
|
|
5
12
|
*/
|
|
6
13
|
export function hasEmbeddings(db) {
|
|
7
14
|
try {
|
|
8
|
-
return !!db
|
|
15
|
+
return !!cachedStmt(_hasEmbeddingsStmt, db, 'SELECT 1 FROM embeddings LIMIT 1').get();
|
|
9
16
|
} catch {
|
|
10
17
|
return false;
|
|
11
18
|
}
|
|
@@ -18,7 +25,7 @@ export function hasEmbeddings(db) {
|
|
|
18
25
|
*/
|
|
19
26
|
export function getEmbeddingCount(db) {
|
|
20
27
|
try {
|
|
21
|
-
return db
|
|
28
|
+
return cachedStmt(_getEmbeddingCountStmt, db, 'SELECT COUNT(*) AS c FROM embeddings').get().c;
|
|
22
29
|
} catch {
|
|
23
30
|
return 0;
|
|
24
31
|
}
|
|
@@ -32,7 +39,11 @@ export function getEmbeddingCount(db) {
|
|
|
32
39
|
*/
|
|
33
40
|
export function getEmbeddingMeta(db, key) {
|
|
34
41
|
try {
|
|
35
|
-
const row =
|
|
42
|
+
const row = cachedStmt(
|
|
43
|
+
_getEmbeddingMetaStmt,
|
|
44
|
+
db,
|
|
45
|
+
'SELECT value FROM embedding_meta WHERE key = ?',
|
|
46
|
+
).get(key);
|
|
36
47
|
return row?.value;
|
|
37
48
|
} catch {
|
|
38
49
|
return undefined;
|
|
@@ -1,12 +1,22 @@
|
|
|
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
|
+
|
|
1
9
|
/**
|
|
2
10
|
* Get callable nodes (function/method/class) for community detection.
|
|
3
11
|
* @param {object} db
|
|
4
12
|
* @returns {{ id: number, name: string, kind: string, file: string }[]}
|
|
5
13
|
*/
|
|
6
14
|
export function getCallableNodes(db) {
|
|
7
|
-
return
|
|
8
|
-
|
|
9
|
-
|
|
15
|
+
return cachedStmt(
|
|
16
|
+
_getCallableNodesStmt,
|
|
17
|
+
db,
|
|
18
|
+
"SELECT id, name, kind, file FROM nodes WHERE kind IN ('function','method','class')",
|
|
19
|
+
).all();
|
|
10
20
|
}
|
|
11
21
|
|
|
12
22
|
/**
|
|
@@ -15,7 +25,11 @@ export function getCallableNodes(db) {
|
|
|
15
25
|
* @returns {{ source_id: number, target_id: number }[]}
|
|
16
26
|
*/
|
|
17
27
|
export function getCallEdges(db) {
|
|
18
|
-
return
|
|
28
|
+
return cachedStmt(
|
|
29
|
+
_getCallEdgesStmt,
|
|
30
|
+
db,
|
|
31
|
+
"SELECT source_id, target_id FROM edges WHERE kind = 'calls'",
|
|
32
|
+
).all();
|
|
19
33
|
}
|
|
20
34
|
|
|
21
35
|
/**
|
|
@@ -24,7 +38,11 @@ export function getCallEdges(db) {
|
|
|
24
38
|
* @returns {{ id: number, name: string, file: string }[]}
|
|
25
39
|
*/
|
|
26
40
|
export function getFileNodesAll(db) {
|
|
27
|
-
return
|
|
41
|
+
return cachedStmt(
|
|
42
|
+
_getFileNodesAllStmt,
|
|
43
|
+
db,
|
|
44
|
+
"SELECT id, name, file FROM nodes WHERE kind = 'file'",
|
|
45
|
+
).all();
|
|
28
46
|
}
|
|
29
47
|
|
|
30
48
|
/**
|
|
@@ -33,7 +51,9 @@ export function getFileNodesAll(db) {
|
|
|
33
51
|
* @returns {{ source_id: number, target_id: number }[]}
|
|
34
52
|
*/
|
|
35
53
|
export function getImportEdges(db) {
|
|
36
|
-
return
|
|
37
|
-
|
|
38
|
-
|
|
54
|
+
return cachedStmt(
|
|
55
|
+
_getImportEdgesStmt,
|
|
56
|
+
db,
|
|
57
|
+
"SELECT source_id, target_id FROM edges WHERE kind IN ('imports','imports-type')",
|
|
58
|
+
).all();
|
|
39
59
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Barrel re-export for repository/ modules.
|
|
2
2
|
|
|
3
3
|
export { purgeFileData, purgeFilesData } from './build-stmts.js';
|
|
4
|
+
export { cachedStmt } from './cached-stmt.js';
|
|
4
5
|
export { deleteCfgForNode, getCfgBlocks, getCfgEdges, hasCfgTables } from './cfg.js';
|
|
5
6
|
export { getCoChangeMeta, hasCoChanges, upsertCoChangeMeta } from './cochange.js';
|
|
6
7
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { EVERY_SYMBOL_KIND, VALID_ROLES } from '../../kinds.js';
|
|
2
2
|
import { NodeQuery } from '../query-builder.js';
|
|
3
|
+
import { cachedStmt } from './cached-stmt.js';
|
|
3
4
|
|
|
4
5
|
// ─── Query-builder based lookups (moved from src/db/repository.js) ─────
|
|
5
6
|
|
|
@@ -100,13 +101,26 @@ export function iterateFunctionNodes(db, opts = {}) {
|
|
|
100
101
|
return _functionNodeQuery(opts).iterate(db);
|
|
101
102
|
}
|
|
102
103
|
|
|
104
|
+
// ─── Statement caches (one prepared statement per db instance) ────────────
|
|
105
|
+
// WeakMap keys on the db object so statements are GC'd when the db closes.
|
|
106
|
+
const _countNodesStmt = new WeakMap();
|
|
107
|
+
const _countEdgesStmt = new WeakMap();
|
|
108
|
+
const _countFilesStmt = new WeakMap();
|
|
109
|
+
const _findNodeByIdStmt = new WeakMap();
|
|
110
|
+
const _findNodesByFileStmt = new WeakMap();
|
|
111
|
+
const _findFileNodesStmt = new WeakMap();
|
|
112
|
+
const _getNodeIdStmt = new WeakMap();
|
|
113
|
+
const _getFunctionNodeIdStmt = new WeakMap();
|
|
114
|
+
const _bulkNodeIdsByFileStmt = new WeakMap();
|
|
115
|
+
const _findNodeChildrenStmt = new WeakMap();
|
|
116
|
+
|
|
103
117
|
/**
|
|
104
118
|
* Count total nodes.
|
|
105
119
|
* @param {object} db
|
|
106
120
|
* @returns {number}
|
|
107
121
|
*/
|
|
108
122
|
export function countNodes(db) {
|
|
109
|
-
return db
|
|
123
|
+
return cachedStmt(_countNodesStmt, db, 'SELECT COUNT(*) AS cnt FROM nodes').get().cnt;
|
|
110
124
|
}
|
|
111
125
|
|
|
112
126
|
/**
|
|
@@ -115,7 +129,7 @@ export function countNodes(db) {
|
|
|
115
129
|
* @returns {number}
|
|
116
130
|
*/
|
|
117
131
|
export function countEdges(db) {
|
|
118
|
-
return db
|
|
132
|
+
return cachedStmt(_countEdgesStmt, db, 'SELECT COUNT(*) AS cnt FROM edges').get().cnt;
|
|
119
133
|
}
|
|
120
134
|
|
|
121
135
|
/**
|
|
@@ -124,7 +138,7 @@ export function countEdges(db) {
|
|
|
124
138
|
* @returns {number}
|
|
125
139
|
*/
|
|
126
140
|
export function countFiles(db) {
|
|
127
|
-
return db
|
|
141
|
+
return cachedStmt(_countFilesStmt, db, 'SELECT COUNT(DISTINCT file) AS cnt FROM nodes').get().cnt;
|
|
128
142
|
}
|
|
129
143
|
|
|
130
144
|
// ─── Shared node lookups ───────────────────────────────────────────────
|
|
@@ -136,7 +150,7 @@ export function countFiles(db) {
|
|
|
136
150
|
* @returns {object|undefined}
|
|
137
151
|
*/
|
|
138
152
|
export function findNodeById(db, id) {
|
|
139
|
-
return db
|
|
153
|
+
return cachedStmt(_findNodeByIdStmt, db, 'SELECT * FROM nodes WHERE id = ?').get(id);
|
|
140
154
|
}
|
|
141
155
|
|
|
142
156
|
/**
|
|
@@ -146,9 +160,11 @@ export function findNodeById(db, id) {
|
|
|
146
160
|
* @returns {object[]}
|
|
147
161
|
*/
|
|
148
162
|
export function findNodesByFile(db, file) {
|
|
149
|
-
return
|
|
150
|
-
|
|
151
|
-
|
|
163
|
+
return cachedStmt(
|
|
164
|
+
_findNodesByFileStmt,
|
|
165
|
+
db,
|
|
166
|
+
"SELECT * FROM nodes WHERE file = ? AND kind != 'file' ORDER BY line",
|
|
167
|
+
).all(file);
|
|
152
168
|
}
|
|
153
169
|
|
|
154
170
|
/**
|
|
@@ -158,15 +174,13 @@ export function findNodesByFile(db, file) {
|
|
|
158
174
|
* @returns {object[]}
|
|
159
175
|
*/
|
|
160
176
|
export function findFileNodes(db, fileLike) {
|
|
161
|
-
return
|
|
177
|
+
return cachedStmt(
|
|
178
|
+
_findFileNodesStmt,
|
|
179
|
+
db,
|
|
180
|
+
"SELECT * FROM nodes WHERE file LIKE ? AND kind = 'file'",
|
|
181
|
+
).all(fileLike);
|
|
162
182
|
}
|
|
163
183
|
|
|
164
|
-
// ─── Statement caches (one prepared statement per db instance) ────────────
|
|
165
|
-
// WeakMap keys on the db object so statements are GC'd when the db closes.
|
|
166
|
-
const _getNodeIdStmt = new WeakMap();
|
|
167
|
-
const _getFunctionNodeIdStmt = new WeakMap();
|
|
168
|
-
const _bulkNodeIdsByFileStmt = new WeakMap();
|
|
169
|
-
|
|
170
184
|
/**
|
|
171
185
|
* Look up a node's ID by its unique (name, kind, file, line) tuple.
|
|
172
186
|
* Shared by builder, watcher, structure, complexity, cfg, engine.
|
|
@@ -178,12 +192,11 @@ const _bulkNodeIdsByFileStmt = new WeakMap();
|
|
|
178
192
|
* @returns {number|undefined}
|
|
179
193
|
*/
|
|
180
194
|
export function getNodeId(db, name, kind, file, line) {
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
return stmt.get(name, kind, file, line)?.id;
|
|
195
|
+
return cachedStmt(
|
|
196
|
+
_getNodeIdStmt,
|
|
197
|
+
db,
|
|
198
|
+
'SELECT id FROM nodes WHERE name = ? AND kind = ? AND file = ? AND line = ?',
|
|
199
|
+
).get(name, kind, file, line)?.id;
|
|
187
200
|
}
|
|
188
201
|
|
|
189
202
|
/**
|
|
@@ -196,14 +209,11 @@ export function getNodeId(db, name, kind, file, line) {
|
|
|
196
209
|
* @returns {number|undefined}
|
|
197
210
|
*/
|
|
198
211
|
export function getFunctionNodeId(db, name, file, line) {
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
_getFunctionNodeIdStmt.set(db, stmt);
|
|
205
|
-
}
|
|
206
|
-
return stmt.get(name, file, line)?.id;
|
|
212
|
+
return cachedStmt(
|
|
213
|
+
_getFunctionNodeIdStmt,
|
|
214
|
+
db,
|
|
215
|
+
"SELECT id FROM nodes WHERE name = ? AND kind IN ('function','method') AND file = ? AND line = ?",
|
|
216
|
+
).get(name, file, line)?.id;
|
|
207
217
|
}
|
|
208
218
|
|
|
209
219
|
/**
|
|
@@ -215,12 +225,11 @@ export function getFunctionNodeId(db, name, file, line) {
|
|
|
215
225
|
* @returns {{ id: number, name: string, kind: string, line: number }[]}
|
|
216
226
|
*/
|
|
217
227
|
export function bulkNodeIdsByFile(db, file) {
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
return stmt.all(file);
|
|
228
|
+
return cachedStmt(
|
|
229
|
+
_bulkNodeIdsByFileStmt,
|
|
230
|
+
db,
|
|
231
|
+
'SELECT id, name, kind, line FROM nodes WHERE file = ?',
|
|
232
|
+
).all(file);
|
|
224
233
|
}
|
|
225
234
|
|
|
226
235
|
/**
|
|
@@ -230,7 +239,9 @@ export function bulkNodeIdsByFile(db, file) {
|
|
|
230
239
|
* @returns {{ name: string, kind: string, line: number, end_line: number|null }[]}
|
|
231
240
|
*/
|
|
232
241
|
export function findNodeChildren(db, parentId) {
|
|
233
|
-
return
|
|
234
|
-
|
|
235
|
-
|
|
242
|
+
return cachedStmt(
|
|
243
|
+
_findNodeChildrenStmt,
|
|
244
|
+
db,
|
|
245
|
+
'SELECT name, kind, line, end_line FROM nodes WHERE parent_id = ? ORDER BY line',
|
|
246
|
+
).all(parentId);
|
|
236
247
|
}
|
package/src/index.js
CHANGED
|
@@ -145,7 +145,13 @@ export { matchOwners, ownersData, ownersForFiles, parseCodeowners } from './owne
|
|
|
145
145
|
// Pagination utilities
|
|
146
146
|
export { MCP_DEFAULTS, MCP_MAX_LIMIT, paginate, paginateResult, printNdjson } from './paginate.js';
|
|
147
147
|
// Unified parser API
|
|
148
|
-
export {
|
|
148
|
+
export {
|
|
149
|
+
disposeParsers,
|
|
150
|
+
getActiveEngine,
|
|
151
|
+
isWasmAvailable,
|
|
152
|
+
parseFileAuto,
|
|
153
|
+
parseFilesAuto,
|
|
154
|
+
} from './parser.js';
|
|
149
155
|
// Query functions (data-returning)
|
|
150
156
|
export {
|
|
151
157
|
ALL_SYMBOL_KINDS,
|
package/src/native.js
CHANGED
|
@@ -11,6 +11,7 @@ import os from 'node:os';
|
|
|
11
11
|
|
|
12
12
|
let _cached; // undefined = not yet tried, null = failed, object = module
|
|
13
13
|
let _loadError = null;
|
|
14
|
+
const _require = createRequire(import.meta.url);
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* Detect whether the current Linux environment uses glibc or musl.
|
|
@@ -18,7 +19,7 @@ let _loadError = null;
|
|
|
18
19
|
*/
|
|
19
20
|
function detectLibc() {
|
|
20
21
|
try {
|
|
21
|
-
const { readdirSync } =
|
|
22
|
+
const { readdirSync } = _require('node:fs');
|
|
22
23
|
const files = readdirSync('/lib');
|
|
23
24
|
if (files.some((f) => f.startsWith('ld-musl-') && f.endsWith('.so.1'))) {
|
|
24
25
|
return 'musl';
|
|
@@ -38,6 +39,17 @@ const PLATFORM_PACKAGES = {
|
|
|
38
39
|
'win32-x64': '@optave/codegraph-win32-x64-msvc',
|
|
39
40
|
};
|
|
40
41
|
|
|
42
|
+
/**
|
|
43
|
+
* Resolve the platform-specific npm package name for the native addon.
|
|
44
|
+
* Returns null if the current platform is not supported.
|
|
45
|
+
*/
|
|
46
|
+
function resolvePlatformPackage() {
|
|
47
|
+
const platform = os.platform();
|
|
48
|
+
const arch = os.arch();
|
|
49
|
+
const key = platform === 'linux' ? `${platform}-${arch}-${detectLibc()}` : `${platform}-${arch}`;
|
|
50
|
+
return PLATFORM_PACKAGES[key] || null;
|
|
51
|
+
}
|
|
52
|
+
|
|
41
53
|
/**
|
|
42
54
|
* Try to load the native napi addon.
|
|
43
55
|
* Returns the module on success, null on failure.
|
|
@@ -45,21 +57,16 @@ const PLATFORM_PACKAGES = {
|
|
|
45
57
|
export function loadNative() {
|
|
46
58
|
if (_cached !== undefined) return _cached;
|
|
47
59
|
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
const platform = os.platform();
|
|
51
|
-
const arch = os.arch();
|
|
52
|
-
const key = platform === 'linux' ? `${platform}-${arch}-${detectLibc()}` : `${platform}-${arch}`;
|
|
53
|
-
const pkg = PLATFORM_PACKAGES[key];
|
|
60
|
+
const pkg = resolvePlatformPackage();
|
|
54
61
|
if (pkg) {
|
|
55
62
|
try {
|
|
56
|
-
_cached =
|
|
63
|
+
_cached = _require(pkg);
|
|
57
64
|
return _cached;
|
|
58
65
|
} catch (err) {
|
|
59
66
|
_loadError = err;
|
|
60
67
|
}
|
|
61
68
|
} else {
|
|
62
|
-
_loadError = new Error(`Unsupported platform: ${
|
|
69
|
+
_loadError = new Error(`Unsupported platform: ${os.platform()}-${os.arch()}`);
|
|
63
70
|
}
|
|
64
71
|
|
|
65
72
|
_cached = null;
|
|
@@ -73,6 +80,21 @@ export function isNativeAvailable() {
|
|
|
73
80
|
return loadNative() !== null;
|
|
74
81
|
}
|
|
75
82
|
|
|
83
|
+
/**
|
|
84
|
+
* Read the version from the platform-specific npm package.json.
|
|
85
|
+
* Returns null if the package is not installed or has no version.
|
|
86
|
+
*/
|
|
87
|
+
export function getNativePackageVersion() {
|
|
88
|
+
const pkg = resolvePlatformPackage();
|
|
89
|
+
if (!pkg) return null;
|
|
90
|
+
try {
|
|
91
|
+
const pkgJson = _require(`${pkg}/package.json`);
|
|
92
|
+
return pkgJson.version || null;
|
|
93
|
+
} catch {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
76
98
|
/**
|
|
77
99
|
* Return the native module or throw if not available.
|
|
78
100
|
*/
|
package/src/parser.js
CHANGED
|
@@ -3,7 +3,7 @@ import path from 'node:path';
|
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
4
|
import { Language, Parser, Query } from 'web-tree-sitter';
|
|
5
5
|
import { warn } from './logger.js';
|
|
6
|
-
import { getNative, loadNative } from './native.js';
|
|
6
|
+
import { getNative, getNativePackageVersion, loadNative } from './native.js';
|
|
7
7
|
|
|
8
8
|
// Re-export all extractors for backward compatibility
|
|
9
9
|
export {
|
|
@@ -41,6 +41,9 @@ let _initialized = false;
|
|
|
41
41
|
// Memoized parsers — avoids reloading WASM grammars on every createParsers() call
|
|
42
42
|
let _cachedParsers = null;
|
|
43
43
|
|
|
44
|
+
// Cached Language objects — WASM-backed, must be .delete()'d explicitly
|
|
45
|
+
let _cachedLanguages = null;
|
|
46
|
+
|
|
44
47
|
// Query cache for JS/TS/TSX extractors (populated during createParsers)
|
|
45
48
|
const _queryCache = new Map();
|
|
46
49
|
|
|
@@ -77,12 +80,14 @@ export async function createParsers() {
|
|
|
77
80
|
}
|
|
78
81
|
|
|
79
82
|
const parsers = new Map();
|
|
83
|
+
const languages = new Map();
|
|
80
84
|
for (const entry of LANGUAGE_REGISTRY) {
|
|
81
85
|
try {
|
|
82
86
|
const lang = await Language.load(grammarPath(entry.grammarFile));
|
|
83
87
|
const parser = new Parser();
|
|
84
88
|
parser.setLanguage(lang);
|
|
85
89
|
parsers.set(entry.id, parser);
|
|
90
|
+
languages.set(entry.id, lang);
|
|
86
91
|
// Compile and cache tree-sitter Query for JS/TS/TSX extractors
|
|
87
92
|
if (entry.extractor === extractSymbols && !_queryCache.has(entry.id)) {
|
|
88
93
|
const isTS = entry.id === 'typescript' || entry.id === 'tsx';
|
|
@@ -100,9 +105,47 @@ export async function createParsers() {
|
|
|
100
105
|
}
|
|
101
106
|
}
|
|
102
107
|
_cachedParsers = parsers;
|
|
108
|
+
_cachedLanguages = languages;
|
|
103
109
|
return parsers;
|
|
104
110
|
}
|
|
105
111
|
|
|
112
|
+
/**
|
|
113
|
+
* Dispose all cached WASM parsers and queries to free WASM linear memory.
|
|
114
|
+
* Call this between repeated builds in the same process (e.g. benchmarks)
|
|
115
|
+
* to prevent memory accumulation that can cause segfaults.
|
|
116
|
+
*/
|
|
117
|
+
export function disposeParsers() {
|
|
118
|
+
if (_cachedParsers) {
|
|
119
|
+
for (const [, parser] of _cachedParsers) {
|
|
120
|
+
if (parser && typeof parser.delete === 'function') {
|
|
121
|
+
try {
|
|
122
|
+
parser.delete();
|
|
123
|
+
} catch {}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
_cachedParsers = null;
|
|
127
|
+
}
|
|
128
|
+
for (const [, query] of _queryCache) {
|
|
129
|
+
if (query && typeof query.delete === 'function') {
|
|
130
|
+
try {
|
|
131
|
+
query.delete();
|
|
132
|
+
} catch {}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
_queryCache.clear();
|
|
136
|
+
if (_cachedLanguages) {
|
|
137
|
+
for (const [, lang] of _cachedLanguages) {
|
|
138
|
+
if (lang && typeof lang.delete === 'function') {
|
|
139
|
+
try {
|
|
140
|
+
lang.delete();
|
|
141
|
+
} catch {}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
_cachedLanguages = null;
|
|
145
|
+
}
|
|
146
|
+
_initialized = false;
|
|
147
|
+
}
|
|
148
|
+
|
|
106
149
|
export function getParser(parsers, filePath) {
|
|
107
150
|
const ext = path.extname(filePath);
|
|
108
151
|
const entry = _extToLang.get(ext);
|
|
@@ -214,6 +257,7 @@ function patchNativeResult(r) {
|
|
|
214
257
|
if (i.csharpUsing === undefined) i.csharpUsing = i.csharp_using;
|
|
215
258
|
if (i.rubyRequire === undefined) i.rubyRequire = i.ruby_require;
|
|
216
259
|
if (i.phpUse === undefined) i.phpUse = i.php_use;
|
|
260
|
+
if (i.dynamicImport === undefined) i.dynamicImport = i.dynamic_import;
|
|
217
261
|
}
|
|
218
262
|
}
|
|
219
263
|
|
|
@@ -429,11 +473,18 @@ export async function parseFilesAuto(filePaths, rootDir, opts = {}) {
|
|
|
429
473
|
*/
|
|
430
474
|
export function getActiveEngine(opts = {}) {
|
|
431
475
|
const { name, native } = resolveEngine(opts);
|
|
432
|
-
|
|
476
|
+
let version = native
|
|
433
477
|
? typeof native.engineVersion === 'function'
|
|
434
478
|
? native.engineVersion()
|
|
435
479
|
: null
|
|
436
480
|
: null;
|
|
481
|
+
// Prefer platform package.json version over binary-embedded version
|
|
482
|
+
// to handle stale binaries that weren't recompiled during a release
|
|
483
|
+
if (native) {
|
|
484
|
+
try {
|
|
485
|
+
version = getNativePackageVersion() ?? version;
|
|
486
|
+
} catch {}
|
|
487
|
+
}
|
|
437
488
|
return { name, version };
|
|
438
489
|
}
|
|
439
490
|
|