@optave/codegraph 3.9.2 → 3.9.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 +93 -10
- package/dist/ast-analysis/engine.d.ts.map +1 -1
- package/dist/ast-analysis/engine.js +64 -0
- package/dist/ast-analysis/engine.js.map +1 -1
- package/dist/domain/analysis/diff-impact.d.ts +12 -0
- package/dist/domain/analysis/diff-impact.d.ts.map +1 -1
- package/dist/domain/analysis/diff-impact.js +20 -1
- package/dist/domain/analysis/diff-impact.js.map +1 -1
- package/dist/domain/graph/builder/native-db-proxy.d.ts.map +1 -1
- package/dist/domain/graph/builder/native-db-proxy.js +8 -4
- package/dist/domain/graph/builder/native-db-proxy.js.map +1 -1
- package/dist/domain/graph/builder/pipeline.d.ts.map +1 -1
- package/dist/domain/graph/builder/pipeline.js +89 -84
- package/dist/domain/graph/builder/pipeline.js.map +1 -1
- package/dist/domain/parser.d.ts.map +1 -1
- package/dist/domain/parser.js +6 -2
- package/dist/domain/parser.js.map +1 -1
- package/dist/features/ast.js +2 -2
- package/dist/features/ast.js.map +1 -1
- package/dist/features/cfg.d.ts +1 -1
- package/dist/features/cfg.d.ts.map +1 -1
- package/dist/features/cfg.js +52 -6
- package/dist/features/cfg.js.map +1 -1
- package/dist/features/complexity.d.ts.map +1 -1
- package/dist/features/complexity.js +7 -0
- package/dist/features/complexity.js.map +1 -1
- package/dist/infrastructure/update-check.d.ts +1 -1
- package/dist/infrastructure/update-check.js +3 -3
- package/dist/infrastructure/update-check.js.map +1 -1
- package/dist/types.d.ts +1 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +7 -7
- package/src/ast-analysis/engine.ts +83 -0
- package/src/domain/analysis/diff-impact.ts +28 -1
- package/src/domain/graph/builder/native-db-proxy.ts +10 -4
- package/src/domain/graph/builder/pipeline.ts +108 -89
- package/src/domain/parser.ts +6 -2
- package/src/features/ast.ts +2 -2
- package/src/features/cfg.ts +51 -6
- package/src/features/complexity.ts +7 -0
- package/src/infrastructure/update-check.ts +3 -3
- package/src/types.ts +1 -0
|
@@ -23,7 +23,13 @@ import { loadNative } from '../../../infrastructure/native.js';
|
|
|
23
23
|
import { semverCompare } from '../../../infrastructure/update-check.js';
|
|
24
24
|
import { toErrorMessage } from '../../../shared/errors.js';
|
|
25
25
|
import { CODEGRAPH_VERSION } from '../../../shared/version.js';
|
|
26
|
-
import type {
|
|
26
|
+
import type {
|
|
27
|
+
BetterSqlite3Database,
|
|
28
|
+
BuildGraphOpts,
|
|
29
|
+
BuildResult,
|
|
30
|
+
Definition,
|
|
31
|
+
ExtractorOutput,
|
|
32
|
+
} from '../../../types.js';
|
|
27
33
|
import { getActiveEngine } from '../../parser.js';
|
|
28
34
|
import { setWorkspaces } from '../resolve.js';
|
|
29
35
|
import { PipelineContext } from './context.js';
|
|
@@ -120,15 +126,11 @@ function setupPipeline(ctx: PipelineContext): void {
|
|
|
120
126
|
const native = enginePref !== 'wasm' ? loadNative() : null;
|
|
121
127
|
ctx.nativeAvailable = !!native?.NativeDatabase;
|
|
122
128
|
|
|
123
|
-
//
|
|
124
|
-
// This eliminates the dual-connection WAL corruption problem
|
|
125
|
-
//
|
|
126
|
-
//
|
|
127
|
-
if (
|
|
128
|
-
ctx.nativeAvailable &&
|
|
129
|
-
native?.NativeDatabase &&
|
|
130
|
-
process.env.CODEGRAPH_FORCE_JS_PIPELINE !== '1'
|
|
131
|
-
) {
|
|
129
|
+
// When native is available, use a NativeDbProxy backed by a single rusqlite
|
|
130
|
+
// connection. This eliminates the dual-connection WAL corruption problem.
|
|
131
|
+
// The Rust orchestrator handles the full pipeline; the proxy is used for any
|
|
132
|
+
// JS post-processing (e.g. structure fallback on large builds).
|
|
133
|
+
if (ctx.nativeAvailable && native?.NativeDatabase) {
|
|
132
134
|
try {
|
|
133
135
|
const dir = path.dirname(ctx.dbPath);
|
|
134
136
|
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
@@ -264,13 +266,14 @@ interface NativeOrchestratorResult {
|
|
|
264
266
|
structureScope?: string[];
|
|
265
267
|
/** Whether the Rust pipeline handled the structure phase (small-incremental fast path). */
|
|
266
268
|
structureHandled?: boolean;
|
|
269
|
+
/** Whether the Rust pipeline wrote AST/complexity/CFG/dataflow to DB. */
|
|
270
|
+
analysisComplete?: boolean;
|
|
267
271
|
}
|
|
268
272
|
|
|
269
273
|
// ── Native orchestrator helpers ───────────────────────────────────────
|
|
270
274
|
|
|
271
275
|
/** Determine whether the native orchestrator should be skipped. Returns a reason string, or null if it should run. */
|
|
272
276
|
function shouldSkipNativeOrchestrator(ctx: PipelineContext): string | null {
|
|
273
|
-
if (process.env.CODEGRAPH_FORCE_JS_PIPELINE === '1') return 'CODEGRAPH_FORCE_JS_PIPELINE=1';
|
|
274
277
|
if (ctx.forceFullRebuild) return 'forceFullRebuild';
|
|
275
278
|
// v3.9.0 addon had buggy incremental purge (wrong SQL on analysis tables,
|
|
276
279
|
// scoped removal over-detection). Fixed in v3.9.1 by PR #865. Gate on
|
|
@@ -452,7 +455,11 @@ async function runPostNativeStructure(
|
|
|
452
455
|
return performance.now() - structureStart;
|
|
453
456
|
}
|
|
454
457
|
|
|
455
|
-
/**
|
|
458
|
+
/**
|
|
459
|
+
* JS fallback for AST/complexity/CFG/dataflow analysis after native orchestrator.
|
|
460
|
+
* Used when the Rust addon doesn't include analysis persistence (older addon
|
|
461
|
+
* version) or when analysis failed on the Rust side.
|
|
462
|
+
*/
|
|
456
463
|
async function runPostNativeAnalysis(
|
|
457
464
|
ctx: PipelineContext,
|
|
458
465
|
allFileSymbols: Map<string, ExtractorOutput>,
|
|
@@ -472,30 +479,43 @@ async function runPostNativeAnalysis(
|
|
|
472
479
|
analysisFileSymbols = allFileSymbols;
|
|
473
480
|
}
|
|
474
481
|
|
|
475
|
-
//
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
if (ctx.engineOpts) ctx.engineOpts.nativeDb = undefined;
|
|
485
|
-
}
|
|
482
|
+
// Reopen nativeDb for analysis features (suspend/resume WAL pattern).
|
|
483
|
+
const native = loadNative();
|
|
484
|
+
if (native?.NativeDatabase) {
|
|
485
|
+
try {
|
|
486
|
+
ctx.nativeDb = native.NativeDatabase.openReadWrite(ctx.dbPath);
|
|
487
|
+
if (ctx.engineOpts) ctx.engineOpts.nativeDb = ctx.nativeDb;
|
|
488
|
+
} catch {
|
|
489
|
+
ctx.nativeDb = undefined;
|
|
490
|
+
if (ctx.engineOpts) ctx.engineOpts.nativeDb = undefined;
|
|
486
491
|
}
|
|
487
|
-
}
|
|
488
|
-
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// Flush JS WAL pages once so Rust can see them, then no-op callbacks.
|
|
495
|
+
// Previously each feature called wal_checkpoint(TRUNCATE) individually
|
|
496
|
+
// (~68ms each × 3-4 features). One FULL checkpoint suffices.
|
|
497
|
+
if (ctx.nativeDb && ctx.engineOpts) {
|
|
498
|
+
ctx.db.pragma('wal_checkpoint(FULL)');
|
|
499
|
+
ctx.engineOpts.suspendJsDb = () => {};
|
|
500
|
+
ctx.engineOpts.resumeJsDb = () => {};
|
|
489
501
|
}
|
|
490
502
|
|
|
491
503
|
try {
|
|
492
|
-
const { runAnalyses: runAnalysesFn } = await import('../../../ast-analysis/engine.js')
|
|
504
|
+
const { runAnalyses: runAnalysesFn } = (await import('../../../ast-analysis/engine.js')) as {
|
|
505
|
+
runAnalyses: (
|
|
506
|
+
db: BetterSqlite3Database,
|
|
507
|
+
fileSymbols: Map<string, ExtractorOutput>,
|
|
508
|
+
rootDir: string,
|
|
509
|
+
opts: Record<string, unknown>,
|
|
510
|
+
engineOpts?: Record<string, unknown>,
|
|
511
|
+
) => Promise<{ astMs?: number; complexityMs?: number; cfgMs?: number; dataflowMs?: number }>;
|
|
512
|
+
};
|
|
493
513
|
const result = await runAnalysesFn(
|
|
494
514
|
ctx.db,
|
|
495
515
|
analysisFileSymbols,
|
|
496
516
|
ctx.rootDir,
|
|
497
|
-
ctx.opts,
|
|
498
|
-
ctx.engineOpts,
|
|
517
|
+
ctx.opts as Record<string, unknown>,
|
|
518
|
+
ctx.engineOpts as unknown as Record<string, unknown> | undefined,
|
|
499
519
|
);
|
|
500
520
|
timing.astMs = result.astMs ?? 0;
|
|
501
521
|
timing.complexityMs = result.complexityMs ?? 0;
|
|
@@ -505,8 +525,10 @@ async function runPostNativeAnalysis(
|
|
|
505
525
|
warn(`Analysis phases failed after native build: ${toErrorMessage(err)}`);
|
|
506
526
|
}
|
|
507
527
|
|
|
508
|
-
// Close nativeDb after analyses
|
|
509
|
-
|
|
528
|
+
// Close nativeDb after analyses — TRUNCATE checkpoint flushes all Rust
|
|
529
|
+
// WAL writes so JS and external readers can see them. Runs once after
|
|
530
|
+
// all analysis features complete (not per-feature).
|
|
531
|
+
if (ctx.nativeDb) {
|
|
510
532
|
try {
|
|
511
533
|
ctx.nativeDb.exec('PRAGMA wal_checkpoint(TRUNCATE)');
|
|
512
534
|
} catch {
|
|
@@ -518,7 +540,11 @@ async function runPostNativeAnalysis(
|
|
|
518
540
|
/* ignore close errors */
|
|
519
541
|
}
|
|
520
542
|
ctx.nativeDb = undefined;
|
|
521
|
-
if (ctx.engineOpts)
|
|
543
|
+
if (ctx.engineOpts) {
|
|
544
|
+
ctx.engineOpts.nativeDb = undefined;
|
|
545
|
+
ctx.engineOpts.suspendJsDb = undefined;
|
|
546
|
+
ctx.engineOpts.resumeJsDb = undefined;
|
|
547
|
+
}
|
|
522
548
|
}
|
|
523
549
|
|
|
524
550
|
return timing;
|
|
@@ -620,30 +646,40 @@ async function tryNativeOrchestrator(
|
|
|
620
646
|
);
|
|
621
647
|
|
|
622
648
|
// ── Post-native structure + analysis ──────────────────────────────
|
|
623
|
-
let analysisTiming = {
|
|
649
|
+
let analysisTiming = {
|
|
650
|
+
astMs: +(p.astMs ?? 0),
|
|
651
|
+
complexityMs: +(p.complexityMs ?? 0),
|
|
652
|
+
cfgMs: +(p.cfgMs ?? 0),
|
|
653
|
+
dataflowMs: +(p.dataflowMs ?? 0),
|
|
654
|
+
};
|
|
624
655
|
let structurePatchMs = 0;
|
|
625
|
-
const needsAnalysis =
|
|
626
|
-
ctx.opts.ast !== false ||
|
|
627
|
-
ctx.opts.complexity !== false ||
|
|
628
|
-
ctx.opts.cfg !== false ||
|
|
629
|
-
ctx.opts.dataflow !== false;
|
|
630
656
|
// Skip JS structure when the Rust pipeline's small-incremental fast path
|
|
631
657
|
// already handled it. For full builds and large incrementals where Rust
|
|
632
658
|
// skipped structure, we must run the JS fallback.
|
|
633
659
|
const needsStructure = !result.structureHandled;
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
660
|
+
// When the Rust addon doesn't include analysis persistence (older addon
|
|
661
|
+
// version or analysis failed), fall back to JS-side analysis.
|
|
662
|
+
const needsAnalysisFallback =
|
|
663
|
+
!result.analysisComplete &&
|
|
664
|
+
(ctx.opts.ast !== false ||
|
|
665
|
+
ctx.opts.complexity !== false ||
|
|
666
|
+
ctx.opts.cfg !== false ||
|
|
667
|
+
ctx.opts.dataflow !== false);
|
|
668
|
+
|
|
669
|
+
if (needsStructure || needsAnalysisFallback) {
|
|
670
|
+
// When analysis fallback is needed, handoff to better-sqlite3 — the
|
|
671
|
+
// analysis engine uses the suspend/resume WAL pattern that requires a
|
|
672
|
+
// real better-sqlite3 connection, not the NativeDbProxy.
|
|
673
|
+
if (needsAnalysisFallback && ctx.nativeFirstProxy) {
|
|
674
|
+
closeNativeDb(ctx, 'pre-analysis-fallback');
|
|
675
|
+
ctx.db = openDb(ctx.dbPath);
|
|
676
|
+
ctx.nativeFirstProxy = false;
|
|
677
|
+
} else if (!ctx.nativeFirstProxy && !handoffWalAfterNativeBuild(ctx)) {
|
|
638
678
|
// DB reopen failed — return partial result
|
|
639
679
|
return formatNativeTimingResult(p, 0, analysisTiming);
|
|
640
680
|
}
|
|
641
681
|
|
|
642
|
-
|
|
643
|
-
// analysis — no need to load the entire graph from DB. When structure
|
|
644
|
-
// was NOT handled, we need all files to build the complete directory tree.
|
|
645
|
-
const scopeFiles = needsStructure ? undefined : result.changedFiles;
|
|
646
|
-
const fileSymbols = reconstructFileSymbolsFromDb(ctx, scopeFiles);
|
|
682
|
+
const fileSymbols = reconstructFileSymbolsFromDb(ctx);
|
|
647
683
|
|
|
648
684
|
if (needsStructure) {
|
|
649
685
|
structurePatchMs = await runPostNativeStructure(
|
|
@@ -654,7 +690,7 @@ async function tryNativeOrchestrator(
|
|
|
654
690
|
);
|
|
655
691
|
}
|
|
656
692
|
|
|
657
|
-
if (
|
|
693
|
+
if (needsAnalysisFallback) {
|
|
658
694
|
analysisTiming = await runPostNativeAnalysis(ctx, fileSymbols, result.changedFiles);
|
|
659
695
|
}
|
|
660
696
|
}
|
|
@@ -666,30 +702,7 @@ async function tryNativeOrchestrator(
|
|
|
666
702
|
// ── Pipeline stages execution ───────────────────────────────────────────
|
|
667
703
|
|
|
668
704
|
async function runPipelineStages(ctx: PipelineContext): Promise<void> {
|
|
669
|
-
// ──
|
|
670
|
-
// When ctx.nativeFirstProxy is true, ctx.db is a NativeDbProxy backed by
|
|
671
|
-
// the single rusqlite connection (ctx.nativeDb). No dual-connection WAL
|
|
672
|
-
// dance is needed — every stage uses the same connection transparently.
|
|
673
|
-
if (ctx.nativeFirstProxy) {
|
|
674
|
-
// Ensure engineOpts.nativeDb is set so stages can use dedicated native methods.
|
|
675
|
-
if (ctx.engineOpts) {
|
|
676
|
-
ctx.engineOpts.nativeDb = ctx.nativeDb;
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
await collectFiles(ctx);
|
|
680
|
-
await detectChanges(ctx);
|
|
681
|
-
if (ctx.earlyExit) return;
|
|
682
|
-
await parseFiles(ctx);
|
|
683
|
-
await insertNodes(ctx);
|
|
684
|
-
await resolveImports(ctx);
|
|
685
|
-
await buildEdges(ctx);
|
|
686
|
-
await buildStructure(ctx);
|
|
687
|
-
await runAnalyses(ctx);
|
|
688
|
-
await finalize(ctx);
|
|
689
|
-
return;
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
// ── Legacy dual-connection mode (WASM / fallback) ────────────────────
|
|
705
|
+
// ── WASM / fallback dual-connection mode ─────────────────────────────
|
|
693
706
|
// NativeDatabase is deferred — not opened during setup. collectFiles and
|
|
694
707
|
// detectChanges only need better-sqlite3. If no files changed, we exit
|
|
695
708
|
// early without ever opening the native connection, saving ~5ms.
|
|
@@ -697,6 +710,13 @@ async function runPipelineStages(ctx: PipelineContext): Promise<void> {
|
|
|
697
710
|
// suspend it now to avoid dual-connection WAL corruption during stages.
|
|
698
711
|
if (ctx.db && ctx.nativeDb) {
|
|
699
712
|
suspendNativeDb(ctx, 'pre-collect');
|
|
713
|
+
// When nativeFirstProxy is true, ctx.db is a NativeDbProxy wrapping the
|
|
714
|
+
// now-closed NativeDatabase. Replace it with a real better-sqlite3
|
|
715
|
+
// connection so the JS pipeline stages can operate normally.
|
|
716
|
+
if (ctx.nativeFirstProxy) {
|
|
717
|
+
ctx.db = openDb(ctx.dbPath);
|
|
718
|
+
ctx.nativeFirstProxy = false;
|
|
719
|
+
}
|
|
700
720
|
}
|
|
701
721
|
|
|
702
722
|
await collectFiles(ctx);
|
|
@@ -728,25 +748,20 @@ async function runPipelineStages(ctx: PipelineContext): Promise<void> {
|
|
|
728
748
|
await buildEdges(ctx);
|
|
729
749
|
await buildStructure(ctx);
|
|
730
750
|
|
|
731
|
-
// Reopen nativeDb for feature modules (ast, cfg, complexity, dataflow)
|
|
732
|
-
// which use suspendJsDb/resumeJsDb WAL checkpoint before native writes.
|
|
751
|
+
// Reopen nativeDb for feature modules (ast, cfg, complexity, dataflow).
|
|
733
752
|
// Skip for small incremental builds — same rationale as insertNodes above.
|
|
753
|
+
//
|
|
754
|
+
// Perf: do ONE upfront FULL checkpoint to flush JS WAL pages so Rust
|
|
755
|
+
// can see the latest rows, then make suspendJsDb/resumeJsDb no-ops.
|
|
756
|
+
// Previously each feature called wal_checkpoint(TRUNCATE) individually
|
|
757
|
+
// (~68ms each × 3-4 features = ~200-270ms overhead on incremental builds).
|
|
734
758
|
if (ctx.nativeAvailable && !smallIncremental) {
|
|
735
759
|
reopenNativeDb(ctx, 'analyses');
|
|
736
760
|
if (ctx.nativeDb && ctx.engineOpts) {
|
|
761
|
+
ctx.db.pragma('wal_checkpoint(FULL)');
|
|
737
762
|
ctx.engineOpts.nativeDb = ctx.nativeDb;
|
|
738
|
-
ctx.engineOpts.suspendJsDb = () => {
|
|
739
|
-
|
|
740
|
-
};
|
|
741
|
-
ctx.engineOpts.resumeJsDb = () => {
|
|
742
|
-
try {
|
|
743
|
-
ctx.nativeDb?.exec('PRAGMA wal_checkpoint(TRUNCATE)');
|
|
744
|
-
} catch (e) {
|
|
745
|
-
debug(
|
|
746
|
-
`resumeJsDb: WAL checkpoint failed (nativeDb may already be closed): ${toErrorMessage(e)}`,
|
|
747
|
-
);
|
|
748
|
-
}
|
|
749
|
-
};
|
|
763
|
+
ctx.engineOpts.suspendJsDb = () => {};
|
|
764
|
+
ctx.engineOpts.resumeJsDb = () => {};
|
|
750
765
|
}
|
|
751
766
|
if (!ctx.nativeDb && ctx.engineOpts) {
|
|
752
767
|
ctx.engineOpts.nativeDb = undefined;
|
|
@@ -757,11 +772,15 @@ async function runPipelineStages(ctx: PipelineContext): Promise<void> {
|
|
|
757
772
|
|
|
758
773
|
await runAnalyses(ctx);
|
|
759
774
|
|
|
760
|
-
//
|
|
761
|
-
//
|
|
762
|
-
//
|
|
763
|
-
// valid page cache in case finalize falls back to JS paths (#751).
|
|
775
|
+
// Flush Rust WAL writes (AST, complexity, CFG, dataflow) so the JS
|
|
776
|
+
// connection and any post-build readers can see them. One TRUNCATE
|
|
777
|
+
// here replaces the N per-feature resumeJsDb checkpoints (#checkpoint-opt).
|
|
764
778
|
if (ctx.nativeDb) {
|
|
779
|
+
try {
|
|
780
|
+
ctx.nativeDb.exec('PRAGMA wal_checkpoint(TRUNCATE)');
|
|
781
|
+
} catch (e) {
|
|
782
|
+
debug(`post-analyses WAL checkpoint failed: ${toErrorMessage(e)}`);
|
|
783
|
+
}
|
|
765
784
|
refreshJsDb(ctx);
|
|
766
785
|
}
|
|
767
786
|
|
package/src/domain/parser.ts
CHANGED
|
@@ -780,7 +780,7 @@ export async function parseFileAuto(
|
|
|
780
780
|
const { native } = resolveEngine(opts);
|
|
781
781
|
|
|
782
782
|
if (native) {
|
|
783
|
-
const result = native.parseFile(filePath, source,
|
|
783
|
+
const result = native.parseFile(filePath, source, true, true);
|
|
784
784
|
if (!result) return null;
|
|
785
785
|
const patched = patchNativeResult(result);
|
|
786
786
|
// Always backfill typeMap for TS/TSX from WASM — native parser's type
|
|
@@ -878,7 +878,11 @@ export async function parseFilesAuto(
|
|
|
878
878
|
if (!native) return parseFilesWasm(filePaths, rootDir);
|
|
879
879
|
|
|
880
880
|
const result = new Map<string, ExtractorOutput>();
|
|
881
|
-
|
|
881
|
+
// Always extract all analysis data (dataflow + AST nodes) during native parse.
|
|
882
|
+
// This eliminates the need for any downstream WASM re-parse or native standalone calls.
|
|
883
|
+
const nativeResults = native.parseFilesFull
|
|
884
|
+
? native.parseFilesFull(filePaths, rootDir)
|
|
885
|
+
: native.parseFiles(filePaths, rootDir, true, true);
|
|
882
886
|
const needsTypeMap: { filePath: string; relPath: string }[] = [];
|
|
883
887
|
for (const r of nativeResults) {
|
|
884
888
|
if (!r) continue;
|
package/src/features/ast.ts
CHANGED
|
@@ -115,8 +115,8 @@ function tryNativeBulkInsert(
|
|
|
115
115
|
receiver: n.receiver ?? '',
|
|
116
116
|
})),
|
|
117
117
|
});
|
|
118
|
-
} else if (symbols.
|
|
119
|
-
return false; // needs JS fallback
|
|
118
|
+
} else if (symbols._tree) {
|
|
119
|
+
return false; // has WASM tree not yet processed — needs JS fallback
|
|
120
120
|
}
|
|
121
121
|
}
|
|
122
122
|
|
package/src/features/cfg.ts
CHANGED
|
@@ -369,7 +369,7 @@ export async function buildCFGData(
|
|
|
369
369
|
db: BetterSqlite3Database,
|
|
370
370
|
fileSymbols: Map<string, FileSymbols>,
|
|
371
371
|
rootDir: string,
|
|
372
|
-
|
|
372
|
+
engineOpts?: {
|
|
373
373
|
nativeDb?: { bulkInsertCfg?(entries: Array<Record<string, unknown>>): number };
|
|
374
374
|
suspendJsDb?: () => void;
|
|
375
375
|
resumeJsDb?: () => void;
|
|
@@ -379,11 +379,56 @@ export async function buildCFGData(
|
|
|
379
379
|
// skip WASM parser init, tree parsing, and JS visitor entirely — just persist.
|
|
380
380
|
const allNative = allCfgNative(fileSymbols);
|
|
381
381
|
|
|
382
|
-
//
|
|
383
|
-
// The
|
|
384
|
-
//
|
|
385
|
-
|
|
386
|
-
|
|
382
|
+
// ── Native bulk-insert fast path ──────────────────────────────────────
|
|
383
|
+
// The Rust bulkInsertCfg handles delete-before-insert atomically on a
|
|
384
|
+
// single rusqlite connection, so there is no dual-connection WAL conflict.
|
|
385
|
+
const nativeDb = engineOpts?.nativeDb;
|
|
386
|
+
if (allNative && nativeDb?.bulkInsertCfg) {
|
|
387
|
+
const entries: Array<Record<string, unknown>> = [];
|
|
388
|
+
for (const [relPath, symbols] of fileSymbols) {
|
|
389
|
+
const ext = path.extname(relPath).toLowerCase();
|
|
390
|
+
if (!CFG_EXTENSIONS.has(ext)) continue;
|
|
391
|
+
|
|
392
|
+
for (const def of symbols.definitions) {
|
|
393
|
+
if (def.kind !== 'function' && def.kind !== 'method') continue;
|
|
394
|
+
if (!def.line) continue;
|
|
395
|
+
|
|
396
|
+
const nodeId = getFunctionNodeId(db, def.name, relPath, def.line);
|
|
397
|
+
if (!nodeId) continue;
|
|
398
|
+
|
|
399
|
+
const cfg = def.cfg as { blocks?: CfgBuildBlock[]; edges?: CfgBuildEdge[] } | undefined;
|
|
400
|
+
if (!cfg?.blocks?.length) continue;
|
|
401
|
+
|
|
402
|
+
entries.push({
|
|
403
|
+
nodeId,
|
|
404
|
+
blocks: cfg.blocks.map((b) => ({
|
|
405
|
+
index: b.index,
|
|
406
|
+
blockType: b.type,
|
|
407
|
+
startLine: b.startLine ?? undefined,
|
|
408
|
+
endLine: b.endLine ?? undefined,
|
|
409
|
+
label: b.label ?? undefined,
|
|
410
|
+
})),
|
|
411
|
+
edges: (cfg.edges || []).map((e) => ({
|
|
412
|
+
sourceIndex: e.sourceIndex,
|
|
413
|
+
targetIndex: e.targetIndex,
|
|
414
|
+
kind: e.kind,
|
|
415
|
+
})),
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
if (entries.length > 0) {
|
|
421
|
+
let inserted = 0;
|
|
422
|
+
try {
|
|
423
|
+
engineOpts?.suspendJsDb?.();
|
|
424
|
+
inserted = nativeDb.bulkInsertCfg(entries);
|
|
425
|
+
} finally {
|
|
426
|
+
engineOpts?.resumeJsDb?.();
|
|
427
|
+
}
|
|
428
|
+
info(`CFG (native bulk): ${inserted} functions analyzed`);
|
|
429
|
+
}
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
387
432
|
|
|
388
433
|
const extToLang = buildExtToLangMap();
|
|
389
434
|
let parsers: unknown = null;
|
|
@@ -545,6 +545,10 @@ function collectNativeBulkRows(
|
|
|
545
545
|
const rows: Array<Record<string, unknown>> = [];
|
|
546
546
|
|
|
547
547
|
for (const [relPath, symbols] of fileSymbols) {
|
|
548
|
+
const ext = path.extname(relPath).toLowerCase();
|
|
549
|
+
const langId = symbols._langId || '';
|
|
550
|
+
const langSupported = COMPLEXITY_EXTENSIONS.has(ext) || COMPLEXITY_RULES.has(langId);
|
|
551
|
+
|
|
548
552
|
for (const def of symbols.definitions) {
|
|
549
553
|
if (def.kind !== 'function' && def.kind !== 'method') continue;
|
|
550
554
|
if (!def.line) continue;
|
|
@@ -554,6 +558,9 @@ function collectNativeBulkRows(
|
|
|
554
558
|
// of the native bulk-insert path for every TypeScript codebase (#846).
|
|
555
559
|
if (!def.complexity) {
|
|
556
560
|
if (def.name.includes('.') || !def.endLine || def.endLine <= def.line) continue;
|
|
561
|
+
// Languages without complexity rules will never have data — skip them
|
|
562
|
+
// rather than bailing out of the entire native bulk path.
|
|
563
|
+
if (!langSupported) continue;
|
|
557
564
|
return null; // genuine function body missing complexity — needs JS fallback
|
|
558
565
|
}
|
|
559
566
|
const nodeId = getFunctionNodeId(db, def.name, relPath, def.line);
|
|
@@ -18,11 +18,11 @@ interface UpdateCache {
|
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
20
|
* Minimal semver comparison. Returns -1, 0, or 1.
|
|
21
|
-
*
|
|
21
|
+
* Strips pre-release suffixes (e.g. "3.9.3-dev.6" → "3.9.3") before comparing.
|
|
22
22
|
*/
|
|
23
23
|
export function semverCompare(a: string, b: string): -1 | 0 | 1 {
|
|
24
|
-
const pa = a.split('.').map(Number);
|
|
25
|
-
const pb = b.split('.').map(Number);
|
|
24
|
+
const pa = a.replace(/-.*$/, '').split('.').map(Number);
|
|
25
|
+
const pb = b.replace(/-.*$/, '').split('.').map(Number);
|
|
26
26
|
for (let i = 0; i < 3; i++) {
|
|
27
27
|
const na = pa[i] || 0;
|
|
28
28
|
const nb = pb[i] || 0;
|
package/src/types.ts
CHANGED
|
@@ -1874,6 +1874,7 @@ export type StmtCache<TRow = unknown> = WeakMap<BetterSqlite3Database, SqliteSta
|
|
|
1874
1874
|
export interface NativeAddon {
|
|
1875
1875
|
parseFile(filePath: string, source: string, dataflow: boolean, ast: boolean): unknown;
|
|
1876
1876
|
parseFiles(files: string[], rootDir: string, dataflow: boolean, ast: boolean): unknown[];
|
|
1877
|
+
parseFilesFull?(files: string[], rootDir: string): unknown[];
|
|
1877
1878
|
resolveImport(fromFile: string, importSource: string, rootDir: string, aliases: unknown): string;
|
|
1878
1879
|
resolveImports(
|
|
1879
1880
|
items: Array<{ fromFile: string; importSource: string }>,
|