@optave/codegraph 3.9.0 → 3.9.1
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 +7 -6
- package/dist/ast-analysis/engine.d.ts.map +1 -1
- package/dist/ast-analysis/engine.js +78 -48
- package/dist/ast-analysis/engine.js.map +1 -1
- package/dist/ast-analysis/visitors/ast-store-visitor.d.ts.map +1 -1
- package/dist/ast-analysis/visitors/ast-store-visitor.js +15 -18
- package/dist/ast-analysis/visitors/ast-store-visitor.js.map +1 -1
- package/dist/db/connection.d.ts +1 -0
- package/dist/db/connection.d.ts.map +1 -1
- package/dist/db/connection.js +22 -4
- package/dist/db/connection.js.map +1 -1
- package/dist/db/repository/base.d.ts +35 -0
- package/dist/db/repository/base.d.ts.map +1 -1
- package/dist/db/repository/base.js +8 -0
- package/dist/db/repository/base.js.map +1 -1
- package/dist/db/repository/index.d.ts +1 -0
- package/dist/db/repository/index.d.ts.map +1 -1
- package/dist/db/repository/index.js.map +1 -1
- package/dist/db/repository/native-repository.d.ts +7 -1
- package/dist/db/repository/native-repository.d.ts.map +1 -1
- package/dist/db/repository/native-repository.js +46 -1
- package/dist/db/repository/native-repository.js.map +1 -1
- package/dist/domain/analysis/dependencies.d.ts +1 -28
- package/dist/domain/analysis/dependencies.d.ts.map +1 -1
- package/dist/domain/analysis/dependencies.js +12 -0
- package/dist/domain/analysis/dependencies.js.map +1 -1
- package/dist/domain/graph/builder/incremental.d.ts.map +1 -1
- package/dist/domain/graph/builder/incremental.js +18 -0
- package/dist/domain/graph/builder/incremental.js.map +1 -1
- package/dist/domain/graph/builder/pipeline.d.ts.map +1 -1
- package/dist/domain/graph/builder/pipeline.js +293 -296
- package/dist/domain/graph/builder/pipeline.js.map +1 -1
- package/dist/domain/graph/builder/stages/build-edges.d.ts.map +1 -1
- package/dist/domain/graph/builder/stages/build-edges.js +29 -2
- package/dist/domain/graph/builder/stages/build-edges.js.map +1 -1
- package/dist/domain/graph/builder/stages/resolve-imports.d.ts.map +1 -1
- package/dist/domain/graph/builder/stages/resolve-imports.js +19 -23
- package/dist/domain/graph/builder/stages/resolve-imports.js.map +1 -1
- package/dist/domain/graph/watcher.d.ts.map +1 -1
- package/dist/domain/graph/watcher.js +99 -95
- package/dist/domain/graph/watcher.js.map +1 -1
- package/dist/domain/parser.d.ts.map +1 -1
- package/dist/domain/parser.js +2 -0
- package/dist/domain/parser.js.map +1 -1
- package/dist/extractors/go.js +53 -35
- package/dist/extractors/go.js.map +1 -1
- package/dist/extractors/javascript.js +66 -27
- package/dist/extractors/javascript.js.map +1 -1
- package/dist/features/complexity.d.ts.map +1 -1
- package/dist/features/complexity.js +78 -58
- package/dist/features/complexity.js.map +1 -1
- package/dist/features/dataflow.d.ts.map +1 -1
- package/dist/features/dataflow.js +109 -118
- package/dist/features/dataflow.js.map +1 -1
- package/dist/features/structure.d.ts.map +1 -1
- package/dist/features/structure.js +147 -97
- package/dist/features/structure.js.map +1 -1
- package/dist/graph/algorithms/louvain.d.ts.map +1 -1
- package/dist/graph/algorithms/louvain.js +4 -2
- package/dist/graph/algorithms/louvain.js.map +1 -1
- package/dist/graph/classifiers/roles.d.ts +2 -0
- package/dist/graph/classifiers/roles.d.ts.map +1 -1
- package/dist/graph/classifiers/roles.js +13 -5
- package/dist/graph/classifiers/roles.js.map +1 -1
- package/dist/presentation/communities.d.ts.map +1 -1
- package/dist/presentation/communities.js +38 -34
- package/dist/presentation/communities.js.map +1 -1
- package/dist/presentation/manifesto.d.ts.map +1 -1
- package/dist/presentation/manifesto.js +31 -33
- package/dist/presentation/manifesto.js.map +1 -1
- package/dist/presentation/queries-cli/inspect.d.ts.map +1 -1
- package/dist/presentation/queries-cli/inspect.js +47 -46
- package/dist/presentation/queries-cli/inspect.js.map +1 -1
- package/dist/shared/file-utils.d.ts.map +1 -1
- package/dist/shared/file-utils.js +94 -72
- package/dist/shared/file-utils.js.map +1 -1
- package/dist/types.d.ts +81 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +7 -7
- package/src/ast-analysis/engine.ts +99 -55
- package/src/ast-analysis/visitors/ast-store-visitor.ts +19 -21
- package/src/db/connection.ts +24 -5
- package/src/db/repository/base.ts +43 -0
- package/src/db/repository/index.ts +1 -0
- package/src/db/repository/native-repository.ts +67 -1
- package/src/domain/analysis/dependencies.ts +13 -0
- package/src/domain/graph/builder/incremental.ts +21 -0
- package/src/domain/graph/builder/pipeline.ts +392 -362
- package/src/domain/graph/builder/stages/build-edges.ts +30 -1
- package/src/domain/graph/builder/stages/resolve-imports.ts +20 -20
- package/src/domain/graph/watcher.ts +118 -98
- package/src/domain/parser.ts +2 -0
- package/src/extractors/go.ts +57 -32
- package/src/extractors/javascript.ts +67 -27
- package/src/features/complexity.ts +94 -58
- package/src/features/dataflow.ts +153 -132
- package/src/features/structure.ts +167 -95
- package/src/graph/algorithms/louvain.ts +5 -2
- package/src/graph/classifiers/roles.ts +14 -5
- package/src/presentation/communities.ts +44 -39
- package/src/presentation/manifesto.ts +35 -38
- package/src/presentation/queries-cli/inspect.ts +48 -46
- package/src/shared/file-utils.ts +116 -77
- package/src/types.ts +85 -0
|
@@ -215,25 +215,37 @@ function runNativeAnalysis(
|
|
|
215
215
|
}
|
|
216
216
|
}
|
|
217
217
|
|
|
218
|
+
/** Index native results by line number and match to a definition by name. */
|
|
219
|
+
function indexNativeByLine<T extends { line: number; name: string }>(
|
|
220
|
+
results: T[],
|
|
221
|
+
): Map<number, T[]> {
|
|
222
|
+
const byLine = new Map<number, T[]>();
|
|
223
|
+
for (const r of results) {
|
|
224
|
+
if (!byLine.has(r.line)) byLine.set(r.line, []);
|
|
225
|
+
byLine.get(r.line)!.push(r);
|
|
226
|
+
}
|
|
227
|
+
return byLine;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function matchNativeResult<T extends { name: string }>(
|
|
231
|
+
candidates: T[] | undefined,
|
|
232
|
+
defName: string,
|
|
233
|
+
): T | undefined {
|
|
234
|
+
if (!candidates) return undefined;
|
|
235
|
+
if (candidates.length === 1) return candidates[0];
|
|
236
|
+
return candidates.find((r) => r.name === defName) ?? candidates[0];
|
|
237
|
+
}
|
|
238
|
+
|
|
218
239
|
/** Store native complexity results on definitions, matched by line number. */
|
|
219
240
|
function storeNativeComplexityResults(
|
|
220
241
|
results: NativeFunctionComplexityResult[],
|
|
221
242
|
defs: Definition[],
|
|
222
243
|
): void {
|
|
223
|
-
const byLine =
|
|
224
|
-
for (const r of results) {
|
|
225
|
-
if (!byLine.has(r.line)) byLine.set(r.line, []);
|
|
226
|
-
byLine.get(r.line)!.push(r);
|
|
227
|
-
}
|
|
244
|
+
const byLine = indexNativeByLine(results);
|
|
228
245
|
|
|
229
246
|
for (const def of defs) {
|
|
230
247
|
if ((def.kind === 'function' || def.kind === 'method') && def.line && !def.complexity) {
|
|
231
|
-
const
|
|
232
|
-
if (!candidates) continue;
|
|
233
|
-
const match =
|
|
234
|
-
candidates.length === 1
|
|
235
|
-
? candidates[0]
|
|
236
|
-
: (candidates.find((r) => r.name === def.name) ?? candidates[0]);
|
|
248
|
+
const match = matchNativeResult(byLine.get(def.line), def.name);
|
|
237
249
|
if (!match) continue;
|
|
238
250
|
const { complexity: c } = match;
|
|
239
251
|
def.complexity = {
|
|
@@ -284,11 +296,7 @@ function overrideCyclomaticFromCfg(def: Definition, cfgCyclomatic: number): void
|
|
|
284
296
|
|
|
285
297
|
/** Store native CFG results on definitions, matched by line number. */
|
|
286
298
|
function storeNativeCfgResults(results: NativeFunctionCfgResult[], defs: Definition[]): void {
|
|
287
|
-
const byLine =
|
|
288
|
-
for (const r of results) {
|
|
289
|
-
if (!byLine.has(r.line)) byLine.set(r.line, []);
|
|
290
|
-
byLine.get(r.line)!.push(r);
|
|
291
|
-
}
|
|
299
|
+
const byLine = indexNativeByLine(results);
|
|
292
300
|
|
|
293
301
|
for (const def of defs) {
|
|
294
302
|
if (
|
|
@@ -297,12 +305,7 @@ function storeNativeCfgResults(results: NativeFunctionCfgResult[], defs: Definit
|
|
|
297
305
|
def.cfg !== null &&
|
|
298
306
|
!def.cfg?.blocks?.length
|
|
299
307
|
) {
|
|
300
|
-
const
|
|
301
|
-
if (!candidates) continue;
|
|
302
|
-
const match =
|
|
303
|
-
candidates.length === 1
|
|
304
|
-
? candidates[0]
|
|
305
|
-
: (candidates.find((r) => r.name === def.name) ?? candidates[0]);
|
|
308
|
+
const match = matchNativeResult(byLine.get(def.line), def.name);
|
|
306
309
|
if (!match) continue;
|
|
307
310
|
def.cfg = match.cfg;
|
|
308
311
|
|
|
@@ -353,42 +356,61 @@ function reconcileCfgCyclomatic(fileSymbols: Map<string, ExtractorOutput>): void
|
|
|
353
356
|
|
|
354
357
|
// ─── WASM pre-parse ─────────────────────────────────────────────────────
|
|
355
358
|
|
|
359
|
+
/** Check whether a single file needs a WASM tree for any enabled analysis pass. */
|
|
360
|
+
function fileNeedsWasmTree(
|
|
361
|
+
relPath: string,
|
|
362
|
+
symbols: ExtractorOutput,
|
|
363
|
+
flags: { doAst: boolean; doComplexity: boolean; doCfg: boolean; doDataflow: boolean },
|
|
364
|
+
): boolean {
|
|
365
|
+
if (symbols._tree) return false;
|
|
366
|
+
const ext = path.extname(relPath).toLowerCase();
|
|
367
|
+
const defs = symbols.definitions || [];
|
|
368
|
+
const lid = symbols._langId || '';
|
|
369
|
+
|
|
370
|
+
if (
|
|
371
|
+
flags.doAst &&
|
|
372
|
+
!Array.isArray(symbols.astNodes) &&
|
|
373
|
+
(WALK_EXTENSIONS.has(ext) || AST_TYPE_MAPS.has(lid))
|
|
374
|
+
)
|
|
375
|
+
return true;
|
|
376
|
+
if (
|
|
377
|
+
flags.doComplexity &&
|
|
378
|
+
(COMPLEXITY_EXTENSIONS.has(ext) || COMPLEXITY_RULES.has(lid)) &&
|
|
379
|
+
defs.some((d) => hasFuncBody(d) && !d.complexity)
|
|
380
|
+
)
|
|
381
|
+
return true;
|
|
382
|
+
if (
|
|
383
|
+
flags.doCfg &&
|
|
384
|
+
(CFG_EXTENSIONS.has(ext) || CFG_RULES.has(lid)) &&
|
|
385
|
+
defs.some((d) => hasFuncBody(d) && d.cfg !== null && !Array.isArray(d.cfg?.blocks))
|
|
386
|
+
)
|
|
387
|
+
return true;
|
|
388
|
+
if (
|
|
389
|
+
flags.doDataflow &&
|
|
390
|
+
!symbols.dataflow &&
|
|
391
|
+
(DATAFLOW_EXTENSIONS.has(ext) || DATAFLOW_RULES.has(lid))
|
|
392
|
+
)
|
|
393
|
+
return true;
|
|
394
|
+
return false;
|
|
395
|
+
}
|
|
396
|
+
|
|
356
397
|
async function ensureWasmTreesIfNeeded(
|
|
357
398
|
fileSymbols: Map<string, ExtractorOutput>,
|
|
358
399
|
opts: AnalysisOpts,
|
|
359
400
|
rootDir: string,
|
|
360
401
|
): Promise<void> {
|
|
361
|
-
const
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
402
|
+
const flags = {
|
|
403
|
+
doAst: opts.ast !== false,
|
|
404
|
+
doComplexity: opts.complexity !== false,
|
|
405
|
+
doCfg: opts.cfg !== false,
|
|
406
|
+
doDataflow: opts.dataflow !== false,
|
|
407
|
+
};
|
|
365
408
|
|
|
366
|
-
if (!doAst && !doComplexity && !doCfg && !doDataflow) return;
|
|
409
|
+
if (!flags.doAst && !flags.doComplexity && !flags.doCfg && !flags.doDataflow) return;
|
|
367
410
|
|
|
368
411
|
let needsWasmTrees = false;
|
|
369
412
|
for (const [relPath, symbols] of fileSymbols) {
|
|
370
|
-
if (symbols
|
|
371
|
-
const ext = path.extname(relPath).toLowerCase();
|
|
372
|
-
const defs = symbols.definitions || [];
|
|
373
|
-
|
|
374
|
-
// AST: need tree when native didn't provide non-call astNodes
|
|
375
|
-
const lid = symbols._langId || '';
|
|
376
|
-
const needsAst =
|
|
377
|
-
doAst &&
|
|
378
|
-
!Array.isArray(symbols.astNodes) &&
|
|
379
|
-
(WALK_EXTENSIONS.has(ext) || AST_TYPE_MAPS.has(lid));
|
|
380
|
-
const needsComplexity =
|
|
381
|
-
doComplexity &&
|
|
382
|
-
(COMPLEXITY_EXTENSIONS.has(ext) || COMPLEXITY_RULES.has(lid)) &&
|
|
383
|
-
defs.some((d) => hasFuncBody(d) && !d.complexity);
|
|
384
|
-
const needsCfg =
|
|
385
|
-
doCfg &&
|
|
386
|
-
(CFG_EXTENSIONS.has(ext) || CFG_RULES.has(lid)) &&
|
|
387
|
-
defs.some((d) => hasFuncBody(d) && d.cfg !== null && !Array.isArray(d.cfg?.blocks));
|
|
388
|
-
const needsDataflow =
|
|
389
|
-
doDataflow && !symbols.dataflow && (DATAFLOW_EXTENSIONS.has(ext) || DATAFLOW_RULES.has(lid));
|
|
390
|
-
|
|
391
|
-
if (needsAst || needsComplexity || needsCfg || needsDataflow) {
|
|
413
|
+
if (fileNeedsWasmTree(relPath, symbols, flags)) {
|
|
392
414
|
needsWasmTrees = true;
|
|
393
415
|
break;
|
|
394
416
|
}
|
|
@@ -607,7 +629,7 @@ async function delegateToBuildFunctions(
|
|
|
607
629
|
} catch (err: unknown) {
|
|
608
630
|
debug(`buildAstNodes failed: ${toErrorMessage(err)}`);
|
|
609
631
|
}
|
|
610
|
-
timing.astMs
|
|
632
|
+
timing.astMs += performance.now() - t0;
|
|
611
633
|
}
|
|
612
634
|
|
|
613
635
|
if (opts.complexity !== false) {
|
|
@@ -618,7 +640,7 @@ async function delegateToBuildFunctions(
|
|
|
618
640
|
} catch (err: unknown) {
|
|
619
641
|
debug(`buildComplexityMetrics failed: ${toErrorMessage(err)}`);
|
|
620
642
|
}
|
|
621
|
-
timing.complexityMs
|
|
643
|
+
timing.complexityMs += performance.now() - t0;
|
|
622
644
|
}
|
|
623
645
|
|
|
624
646
|
if (opts.cfg !== false) {
|
|
@@ -629,7 +651,7 @@ async function delegateToBuildFunctions(
|
|
|
629
651
|
} catch (err: unknown) {
|
|
630
652
|
debug(`buildCFGData failed: ${toErrorMessage(err)}`);
|
|
631
653
|
}
|
|
632
|
-
timing.cfgMs
|
|
654
|
+
timing.cfgMs += performance.now() - t0;
|
|
633
655
|
}
|
|
634
656
|
|
|
635
657
|
if (opts.dataflow !== false) {
|
|
@@ -640,7 +662,7 @@ async function delegateToBuildFunctions(
|
|
|
640
662
|
} catch (err: unknown) {
|
|
641
663
|
debug(`buildDataflowEdges failed: ${toErrorMessage(err)}`);
|
|
642
664
|
}
|
|
643
|
-
timing.dataflowMs
|
|
665
|
+
timing.dataflowMs += performance.now() - t0;
|
|
644
666
|
}
|
|
645
667
|
}
|
|
646
668
|
|
|
@@ -668,7 +690,7 @@ export async function runAnalyses(
|
|
|
668
690
|
// This fills in complexity/CFG/dataflow for files that the native parse pipeline
|
|
669
691
|
// missed, avoiding the need to parse with WASM + run JS visitors.
|
|
670
692
|
const native = loadNative();
|
|
671
|
-
if (native?.analyzeComplexity
|
|
693
|
+
if (native?.analyzeComplexity || native?.buildCfgAnalysis || native?.extractDataflowAnalysis) {
|
|
672
694
|
const t0native = performance.now();
|
|
673
695
|
runNativeAnalysis(native, fileSymbols, rootDir, opts, extToLang);
|
|
674
696
|
debug(`native standalone analysis: ${(performance.now() - t0native).toFixed(1)}ms`);
|
|
@@ -677,7 +699,10 @@ export async function runAnalyses(
|
|
|
677
699
|
// WASM pre-parse for files that still need it (AST store, or native gaps)
|
|
678
700
|
await ensureWasmTreesIfNeeded(fileSymbols, opts, rootDir);
|
|
679
701
|
|
|
680
|
-
// Unified pre-walk: run all applicable visitors in a single DFS per file
|
|
702
|
+
// Unified pre-walk: run all applicable visitors in a single DFS per file.
|
|
703
|
+
// Time each file's walk and distribute equally among active visitors
|
|
704
|
+
// so that phase timers (astMs, complexityMs, etc.) reflect real work — not
|
|
705
|
+
// just the DB-write tail in delegateToBuildFunctions.
|
|
681
706
|
const t0walk = performance.now();
|
|
682
707
|
|
|
683
708
|
for (const [relPath, symbols] of fileSymbols) {
|
|
@@ -692,7 +717,22 @@ export async function runAnalyses(
|
|
|
692
717
|
|
|
693
718
|
if (visitors.length === 0) continue;
|
|
694
719
|
|
|
720
|
+
const walkStart = performance.now();
|
|
695
721
|
const results = walkWithVisitors(symbols._tree.rootNode, visitors, langId, walkerOpts);
|
|
722
|
+
const walkMs = performance.now() - walkStart;
|
|
723
|
+
|
|
724
|
+
// Distribute walk time equally among active visitors
|
|
725
|
+
const activeCount = [astVisitor, complexityVisitor, cfgVisitor, dataflowVisitor].filter(
|
|
726
|
+
Boolean,
|
|
727
|
+
).length;
|
|
728
|
+
if (activeCount > 0) {
|
|
729
|
+
const share = walkMs / activeCount;
|
|
730
|
+
if (astVisitor) timing.astMs += share;
|
|
731
|
+
if (complexityVisitor) timing.complexityMs += share;
|
|
732
|
+
if (cfgVisitor) timing.cfgMs += share;
|
|
733
|
+
if (dataflowVisitor) timing.dataflowMs += share;
|
|
734
|
+
}
|
|
735
|
+
|
|
696
736
|
const defs = symbols.definitions || [];
|
|
697
737
|
|
|
698
738
|
if (astVisitor) {
|
|
@@ -705,6 +745,10 @@ export async function runAnalyses(
|
|
|
705
745
|
if (dataflowVisitor) symbols.dataflow = results.dataflow as DataflowResult;
|
|
706
746
|
}
|
|
707
747
|
|
|
748
|
+
// Total wall-clock time for the unified walk loop, including per-file
|
|
749
|
+
// setupVisitors overhead. Walk time is already distributed into per-phase
|
|
750
|
+
// timers above, so this field overlaps with (astMs + complexityMs + ...).
|
|
751
|
+
// It is kept as a diagnostic cross-check, not an additive bucket.
|
|
708
752
|
timing._unifiedWalkMs = performance.now() - t0walk;
|
|
709
753
|
|
|
710
754
|
// Reconcile: apply CFG-derived cyclomatic override for any definitions that have
|
|
@@ -102,27 +102,25 @@ export function createAstStoreVisitor(
|
|
|
102
102
|
return nodeIdMap.get(`${parentDef.name}|${parentDef.kind}|${parentDef.line}`) || null;
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
return { name: undefined, text: null };
|
|
125
|
-
}
|
|
105
|
+
type NameTextResult = { name: string | null | undefined; text: string | null; skip?: boolean };
|
|
106
|
+
type KindHandler = (node: TreeSitterNode) => NameTextResult;
|
|
107
|
+
|
|
108
|
+
const kindHandlers: Record<string, KindHandler> = {
|
|
109
|
+
new: (node) => ({ name: extractNewName(node), text: truncate(node.text) }),
|
|
110
|
+
throw: (node) => ({ name: extractThrowName(node), text: extractExpressionText(node) }),
|
|
111
|
+
await: (node) => ({ name: extractAwaitName(node), text: extractExpressionText(node) }),
|
|
112
|
+
string: (node) => {
|
|
113
|
+
const content = node.text?.replace(/^['"`]|['"`]$/g, '') || '';
|
|
114
|
+
if (content.length < 2) return { name: null, text: null, skip: true };
|
|
115
|
+
return { name: truncate(content, 100), text: truncate(node.text) };
|
|
116
|
+
},
|
|
117
|
+
regex: (node) => ({ name: node.text || '?', text: truncate(node.text) }),
|
|
118
|
+
};
|
|
119
|
+
const defaultResult: NameTextResult = { name: undefined, text: null };
|
|
120
|
+
|
|
121
|
+
function resolveNameAndText(node: TreeSitterNode, kind: string): NameTextResult {
|
|
122
|
+
const handler = kindHandlers[kind];
|
|
123
|
+
return handler ? handler(node) : defaultResult;
|
|
126
124
|
}
|
|
127
125
|
|
|
128
126
|
function collectNode(node: TreeSitterNode, kind: string): void {
|
package/src/db/connection.ts
CHANGED
|
@@ -311,6 +311,7 @@ export function openReadonlyOrFail(customPath?: string): BetterSqlite3Database {
|
|
|
311
311
|
}
|
|
312
312
|
const Database = getDatabase();
|
|
313
313
|
const db = new Database(dbPath, { readonly: true }) as unknown as BetterSqlite3Database;
|
|
314
|
+
db.pragma('busy_timeout = 5000');
|
|
314
315
|
|
|
315
316
|
warnOnVersionMismatch(() => {
|
|
316
317
|
const row = db
|
|
@@ -359,7 +360,7 @@ function openRepoNative(customDbPath?: string): { repo: Repository; close(): voi
|
|
|
359
360
|
*/
|
|
360
361
|
export function openRepo(
|
|
361
362
|
customDbPath?: string,
|
|
362
|
-
opts: { repo?: Repository } = {},
|
|
363
|
+
opts: { repo?: Repository; engine?: 'native' | 'wasm' | 'auto' } = {},
|
|
363
364
|
): { repo: Repository; close(): void } {
|
|
364
365
|
if (opts.repo != null) {
|
|
365
366
|
if (!(opts.repo instanceof Repository)) {
|
|
@@ -370,15 +371,25 @@ export function openRepo(
|
|
|
370
371
|
return { repo: opts.repo, close() {} };
|
|
371
372
|
}
|
|
372
373
|
|
|
374
|
+
// Respect explicit engine selection: opts.engine > CODEGRAPH_ENGINE env > auto.
|
|
375
|
+
// This ensures --engine wasm and benchmark workers bypass the native path.
|
|
376
|
+
const engine = opts.engine || process.env.CODEGRAPH_ENGINE || 'auto';
|
|
377
|
+
|
|
373
378
|
// Try native rusqlite path first (Phase 6.14)
|
|
374
|
-
if (isNativeAvailable()) {
|
|
379
|
+
if (engine !== 'wasm' && isNativeAvailable()) {
|
|
375
380
|
try {
|
|
376
381
|
return openRepoNative(customDbPath);
|
|
377
382
|
} catch (e) {
|
|
378
383
|
// Re-throw user-visible errors (e.g. DB not found) — only silently
|
|
379
384
|
// fall back for native-engine failures (e.g. incompatible native binary).
|
|
380
385
|
if (e instanceof DbError) throw e;
|
|
381
|
-
|
|
386
|
+
// Re-throw locking/busy errors — falling back to better-sqlite3 would
|
|
387
|
+
// hit the same contention (and potentially hang without busy_timeout).
|
|
388
|
+
const msg = toErrorMessage(e);
|
|
389
|
+
if (/\b(busy|locked|SQLITE_BUSY|SQLITE_LOCKED)\b/i.test(msg)) {
|
|
390
|
+
throw new DbError(`Database is busy (another process may be writing): ${msg}`, {});
|
|
391
|
+
}
|
|
392
|
+
debug(`openRepo: native path failed, falling back to better-sqlite3: ${msg}`);
|
|
382
393
|
}
|
|
383
394
|
}
|
|
384
395
|
|
|
@@ -405,14 +416,22 @@ export function openReadonlyWithNative(customPath?: string): {
|
|
|
405
416
|
} {
|
|
406
417
|
const db = openReadonlyOrFail(customPath);
|
|
407
418
|
|
|
419
|
+
// Respect explicit engine selection, consistent with openRepo().
|
|
420
|
+
const engine = process.env.CODEGRAPH_ENGINE || 'auto';
|
|
421
|
+
|
|
408
422
|
let nativeDb: NativeDatabase | undefined;
|
|
409
|
-
if (isNativeAvailable()) {
|
|
423
|
+
if (engine !== 'wasm' && isNativeAvailable()) {
|
|
410
424
|
try {
|
|
411
425
|
const dbPath = findDbPath(customPath);
|
|
412
426
|
const native = getNative();
|
|
413
427
|
nativeDb = native.NativeDatabase.openReadonly(dbPath);
|
|
414
428
|
} catch (e) {
|
|
415
|
-
|
|
429
|
+
const msg = toErrorMessage(e);
|
|
430
|
+
if (/\b(busy|locked|SQLITE_BUSY|SQLITE_LOCKED)\b/i.test(msg)) {
|
|
431
|
+
debug(`openReadonlyWithNative: native path busy, skipping native DB: ${msg}`);
|
|
432
|
+
} else {
|
|
433
|
+
debug(`openReadonlyWithNative: native path failed: ${msg}`);
|
|
434
|
+
}
|
|
416
435
|
}
|
|
417
436
|
}
|
|
418
437
|
|
|
@@ -223,4 +223,47 @@ export class Repository implements IRepository {
|
|
|
223
223
|
hasCoChangesTable(): boolean {
|
|
224
224
|
return false;
|
|
225
225
|
}
|
|
226
|
+
|
|
227
|
+
// ── Composite queries ──────────────────────────────────────────────
|
|
228
|
+
/**
|
|
229
|
+
* Complete fnDeps query in a single call. Returns null when not natively
|
|
230
|
+
* supported — callers should fall back to the JS-orchestrated path.
|
|
231
|
+
*/
|
|
232
|
+
fnDeps(
|
|
233
|
+
_name: string,
|
|
234
|
+
_opts?: { depth?: number; noTests?: boolean; file?: string; kind?: string },
|
|
235
|
+
): FnDepsResult | null {
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// ── Composite query result types ────────────────────────────────────────
|
|
241
|
+
|
|
242
|
+
export interface FnDepsNode {
|
|
243
|
+
name: string;
|
|
244
|
+
kind: string;
|
|
245
|
+
file: string;
|
|
246
|
+
line: number | null;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export interface FnDepsCallerNode extends FnDepsNode {
|
|
250
|
+
viaHierarchy?: string;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export interface FnDepsEntry {
|
|
254
|
+
name: string;
|
|
255
|
+
kind: string;
|
|
256
|
+
file: string;
|
|
257
|
+
line: number | null;
|
|
258
|
+
endLine: number | null;
|
|
259
|
+
role: string | null;
|
|
260
|
+
fileHash: string | null;
|
|
261
|
+
callees: FnDepsNode[];
|
|
262
|
+
callers: FnDepsCallerNode[];
|
|
263
|
+
transitiveCallers: Record<number, FnDepsNode[]>;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export interface FnDepsResult {
|
|
267
|
+
name: string;
|
|
268
|
+
results: FnDepsEntry[];
|
|
226
269
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Barrel re-export for repository/ modules.
|
|
2
2
|
|
|
3
|
+
export type { FnDepsCallerNode, FnDepsEntry, FnDepsNode, FnDepsResult } from './base.js';
|
|
3
4
|
export { Repository } from './base.js';
|
|
4
5
|
export { purgeFileData, purgeFilesData } from './build-stmts.js';
|
|
5
6
|
export { cachedStmt } from './cached-stmt.js';
|
|
@@ -45,7 +45,13 @@ import type {
|
|
|
45
45
|
TriageNodeRow,
|
|
46
46
|
TriageQueryOpts,
|
|
47
47
|
} from '../../types.js';
|
|
48
|
-
import {
|
|
48
|
+
import {
|
|
49
|
+
type FnDepsCallerNode,
|
|
50
|
+
type FnDepsEntry,
|
|
51
|
+
type FnDepsNode,
|
|
52
|
+
type FnDepsResult,
|
|
53
|
+
Repository,
|
|
54
|
+
} from './base.js';
|
|
49
55
|
|
|
50
56
|
// ── Row converters (napi camelCase → Repository snake_case) ─────────────
|
|
51
57
|
|
|
@@ -461,4 +467,64 @@ export class NativeRepository extends Repository {
|
|
|
461
467
|
}
|
|
462
468
|
return false;
|
|
463
469
|
}
|
|
470
|
+
|
|
471
|
+
// ── Composite queries ──────────────────────────────────────────────
|
|
472
|
+
fnDeps(
|
|
473
|
+
name: string,
|
|
474
|
+
opts?: { depth?: number; noTests?: boolean; file?: string; kind?: string },
|
|
475
|
+
): FnDepsResult | null {
|
|
476
|
+
if (typeof this.#ndb.fnDeps !== 'function') return null;
|
|
477
|
+
const raw = this.#ndb.fnDeps(
|
|
478
|
+
name,
|
|
479
|
+
opts?.depth ?? undefined,
|
|
480
|
+
opts?.noTests ?? undefined,
|
|
481
|
+
opts?.file ?? undefined,
|
|
482
|
+
opts?.kind ?? undefined,
|
|
483
|
+
);
|
|
484
|
+
// Convert from native format (transitive_callers as array of groups)
|
|
485
|
+
// to JS format (transitiveCallers as Record<number, Array>)
|
|
486
|
+
return {
|
|
487
|
+
name: raw.name,
|
|
488
|
+
results: raw.results.map((entry: any): FnDepsEntry => {
|
|
489
|
+
const transitiveCallers: Record<number, FnDepsNode[]> = {};
|
|
490
|
+
for (const group of entry.transitiveCallers ?? []) {
|
|
491
|
+
transitiveCallers[group.depth] = (group.callers ?? []).map(
|
|
492
|
+
(c: any): FnDepsNode => ({
|
|
493
|
+
name: c.name,
|
|
494
|
+
kind: c.kind,
|
|
495
|
+
file: c.file,
|
|
496
|
+
line: c.line ?? null,
|
|
497
|
+
}),
|
|
498
|
+
);
|
|
499
|
+
}
|
|
500
|
+
return {
|
|
501
|
+
name: entry.name,
|
|
502
|
+
kind: entry.kind,
|
|
503
|
+
file: entry.file,
|
|
504
|
+
line: entry.line ?? null,
|
|
505
|
+
endLine: entry.endLine ?? entry.end_line ?? null,
|
|
506
|
+
role: entry.role ?? null,
|
|
507
|
+
fileHash: entry.fileHash ?? entry.file_hash ?? null,
|
|
508
|
+
callees: (entry.callees ?? []).map(
|
|
509
|
+
(c: any): FnDepsNode => ({
|
|
510
|
+
name: c.name,
|
|
511
|
+
kind: c.kind,
|
|
512
|
+
file: c.file,
|
|
513
|
+
line: c.line ?? null,
|
|
514
|
+
}),
|
|
515
|
+
),
|
|
516
|
+
callers: (entry.callers ?? []).map(
|
|
517
|
+
(c: any): FnDepsCallerNode => ({
|
|
518
|
+
name: c.name,
|
|
519
|
+
kind: c.kind,
|
|
520
|
+
file: c.file,
|
|
521
|
+
line: c.line ?? null,
|
|
522
|
+
viaHierarchy: c.viaHierarchy ?? c.via_hierarchy ?? undefined,
|
|
523
|
+
}),
|
|
524
|
+
),
|
|
525
|
+
transitiveCallers,
|
|
526
|
+
};
|
|
527
|
+
}),
|
|
528
|
+
};
|
|
529
|
+
}
|
|
464
530
|
}
|
|
@@ -173,6 +173,19 @@ export function fnDepsData(
|
|
|
173
173
|
} = {},
|
|
174
174
|
) {
|
|
175
175
|
return withRepo(customDbPath, (repo) => {
|
|
176
|
+
// Try native composite path — single NAPI call for the entire query.
|
|
177
|
+
const nativeResult = repo.fnDeps(name, {
|
|
178
|
+
depth: opts.depth,
|
|
179
|
+
noTests: opts.noTests,
|
|
180
|
+
file: opts.file,
|
|
181
|
+
kind: opts.kind,
|
|
182
|
+
});
|
|
183
|
+
if (nativeResult) {
|
|
184
|
+
const base = { name: nativeResult.name, results: nativeResult.results };
|
|
185
|
+
return paginateResult(base, 'results', { limit: opts.limit, offset: opts.offset });
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Fallback: JS-orchestrated path (used when native engine is unavailable)
|
|
176
189
|
const depth = opts.depth || 3;
|
|
177
190
|
const noTests = opts.noTests || false;
|
|
178
191
|
const hc = new Map();
|
|
@@ -366,6 +366,27 @@ function buildImportEdges(
|
|
|
366
366
|
stmts.insertEdge.run(fileNodeId, targetRow.id, edgeKind, 1.0, 0);
|
|
367
367
|
edgesAdded++;
|
|
368
368
|
|
|
369
|
+
// Type-only imports: create symbol-level edges so the target symbols
|
|
370
|
+
// get fan-in credit and aren't falsely classified as dead code.
|
|
371
|
+
if (imp.typeOnly) {
|
|
372
|
+
for (const name of imp.names) {
|
|
373
|
+
const cleanName = name.replace(/^\*\s+as\s+/, '');
|
|
374
|
+
let targetFile = resolvedPath;
|
|
375
|
+
if (db && isBarrelFile(db, resolvedPath)) {
|
|
376
|
+
const actual = resolveBarrelTarget(db, resolvedPath, cleanName);
|
|
377
|
+
if (actual) targetFile = actual;
|
|
378
|
+
}
|
|
379
|
+
const candidates = stmts.findNodeInFile.all(cleanName, targetFile) as Array<{
|
|
380
|
+
id: number;
|
|
381
|
+
file: string;
|
|
382
|
+
}>;
|
|
383
|
+
if (candidates.length > 0) {
|
|
384
|
+
stmts.insertEdge.run(fileNodeId, candidates[0]!.id, 'imports-type', 1.0, 0);
|
|
385
|
+
edgesAdded++;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
369
390
|
// Barrel resolution: create edges through re-export chains
|
|
370
391
|
if (!imp.reexport && db) {
|
|
371
392
|
edgesAdded += resolveBarrelImportEdges(db, stmts, fileNodeId, resolvedPath, imp);
|