@optave/codegraph 3.11.0 → 3.11.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 +38 -31
- package/dist/ast-analysis/engine.d.ts.map +1 -1
- package/dist/ast-analysis/engine.js +91 -60
- package/dist/ast-analysis/engine.js.map +1 -1
- package/dist/ast-analysis/visitor-utils.d.ts +3 -0
- package/dist/ast-analysis/visitor-utils.d.ts.map +1 -1
- package/dist/ast-analysis/visitor-utils.js +83 -49
- package/dist/ast-analysis/visitor-utils.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 +78 -62
- package/dist/ast-analysis/visitors/ast-store-visitor.js.map +1 -1
- package/dist/ast-analysis/visitors/dataflow-visitor.d.ts.map +1 -1
- package/dist/ast-analysis/visitors/dataflow-visitor.js +61 -42
- package/dist/ast-analysis/visitors/dataflow-visitor.js.map +1 -1
- package/dist/cli/commands/embed.d.ts.map +1 -1
- package/dist/cli/commands/embed.js +49 -4
- package/dist/cli/commands/embed.js.map +1 -1
- package/dist/domain/analysis/dependencies.d.ts.map +1 -1
- package/dist/domain/analysis/dependencies.js +106 -80
- package/dist/domain/analysis/dependencies.js.map +1 -1
- package/dist/domain/analysis/fn-impact.d.ts.map +1 -1
- package/dist/domain/analysis/fn-impact.js +77 -52
- package/dist/domain/analysis/fn-impact.js.map +1 -1
- package/dist/domain/analysis/module-map.d.ts.map +1 -1
- package/dist/domain/analysis/module-map.js +132 -121
- package/dist/domain/analysis/module-map.js.map +1 -1
- package/dist/domain/graph/builder/helpers.d.ts +4 -4
- package/dist/domain/graph/builder/helpers.d.ts.map +1 -1
- package/dist/domain/graph/builder/helpers.js +47 -33
- package/dist/domain/graph/builder/helpers.js.map +1 -1
- package/dist/domain/graph/builder/incremental.d.ts +6 -0
- package/dist/domain/graph/builder/incremental.d.ts.map +1 -1
- package/dist/domain/graph/builder/incremental.js +142 -76
- package/dist/domain/graph/builder/incremental.js.map +1 -1
- package/dist/domain/graph/builder/pipeline.d.ts +1 -44
- package/dist/domain/graph/builder/pipeline.d.ts.map +1 -1
- package/dist/domain/graph/builder/pipeline.js +10 -766
- 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 +133 -96
- package/dist/domain/graph/builder/stages/build-edges.js.map +1 -1
- package/dist/domain/graph/builder/stages/build-structure.d.ts.map +1 -1
- package/dist/domain/graph/builder/stages/build-structure.js +82 -65
- package/dist/domain/graph/builder/stages/build-structure.js.map +1 -1
- package/dist/domain/graph/builder/stages/detect-changes.d.ts.map +1 -1
- package/dist/domain/graph/builder/stages/detect-changes.js +84 -56
- package/dist/domain/graph/builder/stages/detect-changes.js.map +1 -1
- package/dist/domain/graph/builder/stages/finalize.d.ts.map +1 -1
- package/dist/domain/graph/builder/stages/finalize.js +60 -51
- package/dist/domain/graph/builder/stages/finalize.js.map +1 -1
- package/dist/domain/graph/builder/stages/insert-nodes.d.ts +8 -6
- package/dist/domain/graph/builder/stages/insert-nodes.d.ts.map +1 -1
- package/dist/domain/graph/builder/stages/insert-nodes.js +107 -122
- package/dist/domain/graph/builder/stages/insert-nodes.js.map +1 -1
- package/dist/domain/graph/builder/stages/native-db-lifecycle.d.ts +14 -0
- package/dist/domain/graph/builder/stages/native-db-lifecycle.d.ts.map +1 -0
- package/dist/domain/graph/builder/stages/native-db-lifecycle.js +77 -0
- package/dist/domain/graph/builder/stages/native-db-lifecycle.js.map +1 -0
- package/dist/domain/graph/builder/stages/native-orchestrator.d.ts +62 -0
- package/dist/domain/graph/builder/stages/native-orchestrator.d.ts.map +1 -0
- package/dist/domain/graph/builder/stages/native-orchestrator.js +747 -0
- package/dist/domain/graph/builder/stages/native-orchestrator.js.map +1 -0
- package/dist/domain/graph/cycles.d.ts +6 -4
- package/dist/domain/graph/cycles.d.ts.map +1 -1
- package/dist/domain/graph/cycles.js +50 -55
- package/dist/domain/graph/cycles.js.map +1 -1
- package/dist/domain/graph/journal.d.ts.map +1 -1
- package/dist/domain/graph/journal.js +89 -70
- package/dist/domain/graph/journal.js.map +1 -1
- package/dist/domain/graph/watcher.d.ts.map +1 -1
- package/dist/domain/graph/watcher.js +5 -2
- package/dist/domain/graph/watcher.js.map +1 -1
- package/dist/domain/parser.d.ts +12 -23
- package/dist/domain/parser.d.ts.map +1 -1
- package/dist/domain/parser.js +126 -79
- package/dist/domain/parser.js.map +1 -1
- package/dist/domain/search/generator.d.ts +3 -1
- package/dist/domain/search/generator.d.ts.map +1 -1
- package/dist/domain/search/generator.js +68 -45
- package/dist/domain/search/generator.js.map +1 -1
- package/dist/domain/search/models.d.ts +2 -0
- package/dist/domain/search/models.d.ts.map +1 -1
- package/dist/domain/search/models.js +37 -3
- package/dist/domain/search/models.js.map +1 -1
- package/dist/domain/search/search/hybrid.d.ts.map +1 -1
- package/dist/domain/search/search/hybrid.js +49 -40
- package/dist/domain/search/search/hybrid.js.map +1 -1
- package/dist/domain/search/search/semantic.d.ts.map +1 -1
- package/dist/domain/search/search/semantic.js +69 -49
- package/dist/domain/search/search/semantic.js.map +1 -1
- package/dist/domain/wasm-worker-entry.js +201 -136
- package/dist/domain/wasm-worker-entry.js.map +1 -1
- package/dist/extractors/elixir.js +95 -71
- package/dist/extractors/elixir.js.map +1 -1
- package/dist/extractors/gleam.d.ts.map +1 -1
- package/dist/extractors/gleam.js +23 -31
- package/dist/extractors/gleam.js.map +1 -1
- package/dist/extractors/helpers.d.ts +79 -1
- package/dist/extractors/helpers.d.ts.map +1 -1
- package/dist/extractors/helpers.js +137 -0
- package/dist/extractors/helpers.js.map +1 -1
- package/dist/extractors/java.d.ts.map +1 -1
- package/dist/extractors/java.js +37 -49
- package/dist/extractors/java.js.map +1 -1
- package/dist/extractors/javascript.d.ts.map +1 -1
- package/dist/extractors/javascript.js +44 -44
- package/dist/extractors/javascript.js.map +1 -1
- package/dist/extractors/julia.js +27 -34
- package/dist/extractors/julia.js.map +1 -1
- package/dist/extractors/r.d.ts.map +1 -1
- package/dist/extractors/r.js +33 -58
- package/dist/extractors/r.js.map +1 -1
- package/dist/extractors/solidity.d.ts.map +1 -1
- package/dist/extractors/solidity.js +38 -61
- package/dist/extractors/solidity.js.map +1 -1
- package/dist/features/boundaries.d.ts.map +1 -1
- package/dist/features/boundaries.js +49 -39
- package/dist/features/boundaries.js.map +1 -1
- package/dist/features/cfg.d.ts.map +1 -1
- package/dist/features/cfg.js +90 -63
- package/dist/features/cfg.js.map +1 -1
- package/dist/features/check.d.ts.map +1 -1
- package/dist/features/check.js +43 -34
- package/dist/features/check.js.map +1 -1
- package/dist/features/cochange.d.ts.map +1 -1
- package/dist/features/cochange.js +68 -56
- package/dist/features/cochange.js.map +1 -1
- package/dist/features/complexity.d.ts.map +1 -1
- package/dist/features/complexity.js +105 -75
- package/dist/features/complexity.js.map +1 -1
- package/dist/features/dataflow.d.ts.map +1 -1
- package/dist/features/dataflow.js +37 -29
- package/dist/features/dataflow.js.map +1 -1
- package/dist/features/flow.d.ts.map +1 -1
- package/dist/features/flow.js +31 -22
- package/dist/features/flow.js.map +1 -1
- package/dist/features/graph-enrichment.d.ts.map +1 -1
- package/dist/features/graph-enrichment.js +77 -70
- package/dist/features/graph-enrichment.js.map +1 -1
- package/dist/features/owners.d.ts +17 -26
- package/dist/features/owners.d.ts.map +1 -1
- package/dist/features/owners.js +120 -109
- package/dist/features/owners.js.map +1 -1
- package/dist/features/sequence.d.ts.map +1 -1
- package/dist/features/sequence.js +59 -54
- package/dist/features/sequence.js.map +1 -1
- package/dist/features/structure-query.d.ts.map +1 -1
- package/dist/features/structure-query.js +60 -60
- package/dist/features/structure-query.js.map +1 -1
- package/dist/features/structure.js +28 -36
- package/dist/features/structure.js.map +1 -1
- package/dist/graph/algorithms/leiden/optimiser.d.ts.map +1 -1
- package/dist/graph/algorithms/leiden/optimiser.js +100 -69
- package/dist/graph/algorithms/leiden/optimiser.js.map +1 -1
- package/dist/graph/classifiers/roles.d.ts.map +1 -1
- package/dist/graph/classifiers/roles.js +63 -59
- package/dist/graph/classifiers/roles.js.map +1 -1
- package/dist/infrastructure/config.d.ts +1 -1
- package/dist/infrastructure/config.d.ts.map +1 -1
- package/dist/infrastructure/config.js +1 -1
- package/dist/infrastructure/config.js.map +1 -1
- package/dist/presentation/cfg.d.ts.map +1 -1
- package/dist/presentation/cfg.js +44 -29
- package/dist/presentation/cfg.js.map +1 -1
- package/dist/presentation/flow.d.ts.map +1 -1
- package/dist/presentation/flow.js +58 -38
- package/dist/presentation/flow.js.map +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +7 -7
- package/src/ast-analysis/engine.ts +145 -61
- package/src/ast-analysis/visitor-utils.ts +86 -46
- package/src/ast-analysis/visitors/ast-store-visitor.ts +104 -69
- package/src/ast-analysis/visitors/dataflow-visitor.ts +86 -47
- package/src/cli/commands/embed.ts +54 -4
- package/src/domain/analysis/dependencies.ts +166 -85
- package/src/domain/analysis/fn-impact.ts +120 -50
- package/src/domain/analysis/module-map.ts +175 -140
- package/src/domain/graph/builder/helpers.ts +85 -76
- package/src/domain/graph/builder/incremental.ts +217 -90
- package/src/domain/graph/builder/pipeline.ts +19 -957
- package/src/domain/graph/builder/stages/build-edges.ts +198 -140
- package/src/domain/graph/builder/stages/build-structure.ts +115 -82
- package/src/domain/graph/builder/stages/detect-changes.ts +107 -64
- package/src/domain/graph/builder/stages/finalize.ts +72 -70
- package/src/domain/graph/builder/stages/insert-nodes.ts +154 -120
- package/src/domain/graph/builder/stages/native-db-lifecycle.ts +74 -0
- package/src/domain/graph/builder/stages/native-orchestrator.ts +942 -0
- package/src/domain/graph/cycles.ts +51 -49
- package/src/domain/graph/journal.ts +84 -69
- package/src/domain/graph/watcher.ts +8 -2
- package/src/domain/parser.ts +143 -66
- package/src/domain/search/generator.ts +132 -74
- package/src/domain/search/models.ts +39 -3
- package/src/domain/search/search/hybrid.ts +53 -42
- package/src/domain/search/search/semantic.ts +105 -65
- package/src/domain/wasm-worker-entry.ts +235 -152
- package/src/extractors/elixir.ts +91 -64
- package/src/extractors/gleam.ts +33 -37
- package/src/extractors/helpers.ts +205 -1
- package/src/extractors/java.ts +42 -45
- package/src/extractors/javascript.ts +44 -43
- package/src/extractors/julia.ts +28 -35
- package/src/extractors/r.ts +38 -56
- package/src/extractors/solidity.ts +43 -71
- package/src/features/boundaries.ts +64 -46
- package/src/features/cfg.ts +145 -74
- package/src/features/check.ts +60 -43
- package/src/features/cochange.ts +95 -72
- package/src/features/complexity.ts +134 -79
- package/src/features/dataflow.ts +57 -34
- package/src/features/flow.ts +48 -24
- package/src/features/graph-enrichment.ts +105 -70
- package/src/features/owners.ts +186 -146
- package/src/features/sequence.ts +99 -69
- package/src/features/structure-query.ts +94 -79
- package/src/features/structure.ts +56 -56
- package/src/graph/algorithms/leiden/optimiser.ts +142 -87
- package/src/graph/classifiers/roles.ts +64 -54
- package/src/infrastructure/config.ts +1 -1
- package/src/presentation/cfg.ts +48 -32
- package/src/presentation/flow.ts +100 -52
- package/src/types.ts +1 -1
package/src/features/cfg.ts
CHANGED
|
@@ -18,7 +18,13 @@ import {
|
|
|
18
18
|
} from '../db/index.js';
|
|
19
19
|
import { debug, info } from '../infrastructure/logger.js';
|
|
20
20
|
import { paginateResult } from '../shared/paginate.js';
|
|
21
|
-
import type {
|
|
21
|
+
import type {
|
|
22
|
+
BetterSqlite3Database,
|
|
23
|
+
CfgRulesConfig,
|
|
24
|
+
Definition,
|
|
25
|
+
NodeRow,
|
|
26
|
+
TreeSitterNode,
|
|
27
|
+
} from '../types.js';
|
|
22
28
|
import { findNodes } from './shared/find-nodes.js';
|
|
23
29
|
|
|
24
30
|
export { _makeCfgRules as makeCfgRules, CFG_RULES };
|
|
@@ -122,9 +128,8 @@ async function initCfgParsers(
|
|
|
122
128
|
let getParserFn: unknown = null;
|
|
123
129
|
|
|
124
130
|
if (needsFallback) {
|
|
125
|
-
const { createParsers } = await import('../domain/parser.js');
|
|
126
|
-
parsers = await createParsers();
|
|
127
131
|
const mod = await import('../domain/parser.js');
|
|
132
|
+
parsers = await mod.createParsers();
|
|
128
133
|
getParserFn = mod.getParser;
|
|
129
134
|
}
|
|
130
135
|
|
|
@@ -187,7 +192,7 @@ interface VisitorCfgResult {
|
|
|
187
192
|
|
|
188
193
|
function buildVisitorCfgMap(
|
|
189
194
|
tree: { rootNode: TreeSitterNode } | null | undefined,
|
|
190
|
-
cfgRules:
|
|
195
|
+
cfgRules: CfgRulesConfig,
|
|
191
196
|
symbols: FileSymbols,
|
|
192
197
|
langId: string,
|
|
193
198
|
): Map<number, VisitorCfgResult[]> | null {
|
|
@@ -203,9 +208,8 @@ function buildVisitorCfgMap(
|
|
|
203
208
|
if (!needsVisitor) return null;
|
|
204
209
|
|
|
205
210
|
const visitor = createCfgVisitor(cfgRules);
|
|
206
|
-
const typedRules = cfgRules as { functionNodes: string[] };
|
|
207
211
|
const walkerOpts = {
|
|
208
|
-
functionNodeTypes: new Set(
|
|
212
|
+
functionNodeTypes: new Set(cfgRules.functionNodes),
|
|
209
213
|
nestingNodeTypes: new Set<string>(),
|
|
210
214
|
getFunctionName: (node: TreeSitterNode) => {
|
|
211
215
|
const nameNode = node.childForFieldName?.('name');
|
|
@@ -365,79 +369,91 @@ function persistVisitorFileCfg(
|
|
|
365
369
|
return count;
|
|
366
370
|
}
|
|
367
371
|
|
|
368
|
-
|
|
372
|
+
/**
|
|
373
|
+
* Build a single native bulk-insert entry for one definition.
|
|
374
|
+
* Returns null when the def has no CFG blocks or no associated node row.
|
|
375
|
+
*/
|
|
376
|
+
function buildNativeCfgEntry(
|
|
369
377
|
db: BetterSqlite3Database,
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
)
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
378
|
+
def: Definition,
|
|
379
|
+
relPath: string,
|
|
380
|
+
): Record<string, unknown> | null {
|
|
381
|
+
if (def.kind !== 'function' && def.kind !== 'method') return null;
|
|
382
|
+
if (!def.line) return null;
|
|
383
|
+
|
|
384
|
+
const nodeId = getFunctionNodeId(db, def.name, relPath, def.line);
|
|
385
|
+
if (!nodeId) return null;
|
|
386
|
+
|
|
387
|
+
const cfg = def.cfg as { blocks?: CfgBuildBlock[]; edges?: CfgBuildEdge[] } | undefined;
|
|
388
|
+
if (!cfg?.blocks?.length) return null;
|
|
389
|
+
|
|
390
|
+
return {
|
|
391
|
+
nodeId,
|
|
392
|
+
blocks: cfg.blocks.map((b) => ({
|
|
393
|
+
index: b.index,
|
|
394
|
+
blockType: b.type,
|
|
395
|
+
startLine: b.startLine ?? undefined,
|
|
396
|
+
endLine: b.endLine ?? undefined,
|
|
397
|
+
label: b.label ?? undefined,
|
|
398
|
+
})),
|
|
399
|
+
edges: (cfg.edges || []).map((e) => ({
|
|
400
|
+
sourceIndex: e.sourceIndex,
|
|
401
|
+
targetIndex: e.targetIndex,
|
|
402
|
+
kind: e.kind,
|
|
403
|
+
})),
|
|
404
|
+
};
|
|
405
|
+
}
|
|
381
406
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
407
|
+
/**
|
|
408
|
+
* Native bulk-insert fast path. The Rust bulkInsertCfg handles
|
|
409
|
+
* delete-before-insert atomically on a single rusqlite connection, so there
|
|
410
|
+
* is no dual-connection WAL conflict. Returns true if this path handled the
|
|
411
|
+
* request (caller should return early); false to fall through to WASM/JS.
|
|
412
|
+
*/
|
|
413
|
+
function tryNativeBulkInsertCfg(
|
|
414
|
+
db: BetterSqlite3Database,
|
|
415
|
+
fileSymbols: Map<string, FileSymbols>,
|
|
416
|
+
engineOpts:
|
|
417
|
+
| {
|
|
418
|
+
nativeDb?: { bulkInsertCfg?(entries: Array<Record<string, unknown>>): number };
|
|
419
|
+
suspendJsDb?: () => void;
|
|
420
|
+
resumeJsDb?: () => void;
|
|
421
|
+
}
|
|
422
|
+
| undefined,
|
|
423
|
+
): boolean {
|
|
385
424
|
const nativeDb = engineOpts?.nativeDb;
|
|
386
|
-
if (
|
|
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;
|
|
425
|
+
if (!nativeDb?.bulkInsertCfg) return false;
|
|
391
426
|
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
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
|
-
}
|
|
427
|
+
const entries: Array<Record<string, unknown>> = [];
|
|
428
|
+
for (const [relPath, symbols] of fileSymbols) {
|
|
429
|
+
const ext = path.extname(relPath).toLowerCase();
|
|
430
|
+
if (!CFG_EXTENSIONS.has(ext)) continue;
|
|
419
431
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
engineOpts?.suspendJsDb?.();
|
|
424
|
-
inserted = nativeDb.bulkInsertCfg(entries);
|
|
425
|
-
} finally {
|
|
426
|
-
engineOpts?.resumeJsDb?.();
|
|
427
|
-
}
|
|
428
|
-
info(`CFG (native bulk): ${inserted} functions analyzed`);
|
|
432
|
+
for (const def of symbols.definitions) {
|
|
433
|
+
const entry = buildNativeCfgEntry(db, def, relPath);
|
|
434
|
+
if (entry) entries.push(entry);
|
|
429
435
|
}
|
|
430
|
-
return;
|
|
431
436
|
}
|
|
432
437
|
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
438
|
+
if (entries.length > 0) {
|
|
439
|
+
let inserted = 0;
|
|
440
|
+
try {
|
|
441
|
+
engineOpts?.suspendJsDb?.();
|
|
442
|
+
inserted = nativeDb.bulkInsertCfg(entries);
|
|
443
|
+
} finally {
|
|
444
|
+
engineOpts?.resumeJsDb?.();
|
|
445
|
+
}
|
|
446
|
+
info(`CFG (native bulk): ${inserted} functions analyzed`);
|
|
439
447
|
}
|
|
448
|
+
return true;
|
|
449
|
+
}
|
|
440
450
|
|
|
451
|
+
interface CfgInsertStatements {
|
|
452
|
+
insertBlock: ReturnType<BetterSqlite3Database['prepare']>;
|
|
453
|
+
insertEdge: ReturnType<BetterSqlite3Database['prepare']>;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function prepareCfgInsertStatements(db: BetterSqlite3Database): CfgInsertStatements {
|
|
441
457
|
const insertBlock = db.prepare(
|
|
442
458
|
`INSERT INTO cfg_blocks (function_node_id, block_index, block_type, start_line, end_line, label)
|
|
443
459
|
VALUES (?, ?, ?, ?, ?, ?)`,
|
|
@@ -446,15 +462,31 @@ export async function buildCFGData(
|
|
|
446
462
|
`INSERT INTO cfg_edges (function_node_id, source_block_id, target_block_id, kind)
|
|
447
463
|
VALUES (?, ?, ?, ?)`,
|
|
448
464
|
);
|
|
449
|
-
|
|
465
|
+
return { insertBlock, insertEdge };
|
|
466
|
+
}
|
|
450
467
|
|
|
468
|
+
/**
|
|
469
|
+
* Persist CFG for every CFG-eligible file inside a single transaction.
|
|
470
|
+
* Dispatches to native fast path or visitor path per file.
|
|
471
|
+
*/
|
|
472
|
+
function persistAllFileCfgs(
|
|
473
|
+
db: BetterSqlite3Database,
|
|
474
|
+
fileSymbols: Map<string, FileSymbols>,
|
|
475
|
+
rootDir: string,
|
|
476
|
+
allNative: boolean,
|
|
477
|
+
extToLang: Map<string, string>,
|
|
478
|
+
parsers: unknown,
|
|
479
|
+
getParserFn: unknown,
|
|
480
|
+
stmts: CfgInsertStatements,
|
|
481
|
+
): number {
|
|
482
|
+
let analyzed = 0;
|
|
451
483
|
const tx = db.transaction(() => {
|
|
452
484
|
for (const [relPath, symbols] of fileSymbols) {
|
|
453
485
|
const ext = path.extname(relPath).toLowerCase();
|
|
454
486
|
if (!CFG_EXTENSIONS.has(ext)) continue;
|
|
455
487
|
|
|
456
488
|
if (allNative && !symbols._tree) {
|
|
457
|
-
analyzed += persistNativeFileCfg(db, symbols, relPath, insertBlock, insertEdge);
|
|
489
|
+
analyzed += persistNativeFileCfg(db, symbols, relPath, stmts.insertBlock, stmts.insertEdge);
|
|
458
490
|
continue;
|
|
459
491
|
}
|
|
460
492
|
|
|
@@ -466,13 +498,52 @@ export async function buildCFGData(
|
|
|
466
498
|
extToLang,
|
|
467
499
|
parsers,
|
|
468
500
|
getParserFn,
|
|
469
|
-
insertBlock,
|
|
470
|
-
insertEdge,
|
|
501
|
+
stmts.insertBlock,
|
|
502
|
+
stmts.insertEdge,
|
|
471
503
|
);
|
|
472
504
|
}
|
|
473
505
|
});
|
|
474
|
-
|
|
475
506
|
tx();
|
|
507
|
+
return analyzed;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
export async function buildCFGData(
|
|
511
|
+
db: BetterSqlite3Database,
|
|
512
|
+
fileSymbols: Map<string, FileSymbols>,
|
|
513
|
+
rootDir: string,
|
|
514
|
+
engineOpts?: {
|
|
515
|
+
nativeDb?: { bulkInsertCfg?(entries: Array<Record<string, unknown>>): number };
|
|
516
|
+
suspendJsDb?: () => void;
|
|
517
|
+
resumeJsDb?: () => void;
|
|
518
|
+
},
|
|
519
|
+
): Promise<void> {
|
|
520
|
+
// Fast path: when all function/method defs already have native CFG data,
|
|
521
|
+
// skip WASM parser init, tree parsing, and JS visitor entirely — just persist.
|
|
522
|
+
const allNative = allCfgNative(fileSymbols);
|
|
523
|
+
|
|
524
|
+
if (allNative && tryNativeBulkInsertCfg(db, fileSymbols, engineOpts)) {
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
const extToLang = buildExtToLangMap();
|
|
529
|
+
let parsers: unknown = null;
|
|
530
|
+
let getParserFn: unknown = null;
|
|
531
|
+
|
|
532
|
+
if (!allNative) {
|
|
533
|
+
({ parsers, getParserFn } = await initCfgParsers(fileSymbols));
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
const stmts = prepareCfgInsertStatements(db);
|
|
537
|
+
const analyzed = persistAllFileCfgs(
|
|
538
|
+
db,
|
|
539
|
+
fileSymbols,
|
|
540
|
+
rootDir,
|
|
541
|
+
allNative,
|
|
542
|
+
extToLang,
|
|
543
|
+
parsers,
|
|
544
|
+
getParserFn,
|
|
545
|
+
stmts,
|
|
546
|
+
);
|
|
476
547
|
|
|
477
548
|
if (analyzed > 0) {
|
|
478
549
|
info(`CFG: ${analyzed} functions analyzed`);
|
package/src/features/check.ts
CHANGED
|
@@ -22,6 +22,29 @@ interface ParsedDiff {
|
|
|
22
22
|
newFiles: Set<string>;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
const HUNK_RE = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/;
|
|
26
|
+
const NEW_FILE_RE = /^\+\+\+ b\/(.+)/;
|
|
27
|
+
|
|
28
|
+
function pushHunkRanges(
|
|
29
|
+
line: string,
|
|
30
|
+
currentFile: string,
|
|
31
|
+
changedRanges: Map<string, DiffRange[]>,
|
|
32
|
+
oldRanges: Map<string, DiffRange[]>,
|
|
33
|
+
): void {
|
|
34
|
+
const hunkMatch = line.match(HUNK_RE);
|
|
35
|
+
if (!hunkMatch) return;
|
|
36
|
+
const oldStart = parseInt(hunkMatch[1]!, 10);
|
|
37
|
+
const oldCount = parseInt(hunkMatch[2] || '1', 10);
|
|
38
|
+
if (oldCount > 0) {
|
|
39
|
+
oldRanges.get(currentFile)!.push({ start: oldStart, end: oldStart + oldCount - 1 });
|
|
40
|
+
}
|
|
41
|
+
const newStart = parseInt(hunkMatch[3]!, 10);
|
|
42
|
+
const newCount = parseInt(hunkMatch[4] || '1', 10);
|
|
43
|
+
if (newCount > 0) {
|
|
44
|
+
changedRanges.get(currentFile)!.push({ start: newStart, end: newStart + newCount - 1 });
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
25
48
|
export function parseDiffOutput(diffOutput: string): ParsedDiff {
|
|
26
49
|
const changedRanges = new Map<string, DiffRange[]>();
|
|
27
50
|
const oldRanges = new Map<string, DiffRange[]>();
|
|
@@ -38,7 +61,7 @@ export function parseDiffOutput(diffOutput: string): ParsedDiff {
|
|
|
38
61
|
prevIsDevNull = false;
|
|
39
62
|
continue;
|
|
40
63
|
}
|
|
41
|
-
const fileMatch = line.match(
|
|
64
|
+
const fileMatch = line.match(NEW_FILE_RE);
|
|
42
65
|
if (fileMatch) {
|
|
43
66
|
currentFile = fileMatch[1]!;
|
|
44
67
|
if (!changedRanges.has(currentFile)) changedRanges.set(currentFile, []);
|
|
@@ -47,19 +70,7 @@ export function parseDiffOutput(diffOutput: string): ParsedDiff {
|
|
|
47
70
|
prevIsDevNull = false;
|
|
48
71
|
continue;
|
|
49
72
|
}
|
|
50
|
-
|
|
51
|
-
if (hunkMatch && currentFile) {
|
|
52
|
-
const oldStart = parseInt(hunkMatch[1]!, 10);
|
|
53
|
-
const oldCount = parseInt(hunkMatch[2] || '1', 10);
|
|
54
|
-
if (oldCount > 0) {
|
|
55
|
-
oldRanges.get(currentFile)!.push({ start: oldStart, end: oldStart + oldCount - 1 });
|
|
56
|
-
}
|
|
57
|
-
const newStart = parseInt(hunkMatch[3]!, 10);
|
|
58
|
-
const newCount = parseInt(hunkMatch[4] || '1', 10);
|
|
59
|
-
if (newCount > 0) {
|
|
60
|
-
changedRanges.get(currentFile)!.push({ start: newStart, end: newStart + newCount - 1 });
|
|
61
|
-
}
|
|
62
|
-
}
|
|
73
|
+
if (currentFile) pushHunkRanges(line, currentFile, changedRanges, oldRanges);
|
|
63
74
|
}
|
|
64
75
|
return { changedRanges, oldRanges, newFiles };
|
|
65
76
|
}
|
|
@@ -96,6 +107,26 @@ interface BlastRadiusResult {
|
|
|
96
107
|
violations: BlastRadiusViolation[];
|
|
97
108
|
}
|
|
98
109
|
|
|
110
|
+
type DefRow = {
|
|
111
|
+
id: number;
|
|
112
|
+
name: string;
|
|
113
|
+
kind: string;
|
|
114
|
+
file: string;
|
|
115
|
+
line: number;
|
|
116
|
+
end_line: number | null;
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
function rangesOverlap(defLine: number, endLine: number, ranges: DiffRange[]): boolean {
|
|
120
|
+
for (const range of ranges) {
|
|
121
|
+
if (range.start <= endLine && range.end >= defLine) return true;
|
|
122
|
+
}
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function defEndLine(def: DefRow, nextDef: DefRow | undefined): number {
|
|
127
|
+
return def.end_line || (nextDef ? nextDef.line - 1 : 999999);
|
|
128
|
+
}
|
|
129
|
+
|
|
99
130
|
export function checkMaxBlastRadius(
|
|
100
131
|
db: BetterSqlite3Database,
|
|
101
132
|
changedRanges: Map<string, DiffRange[]>,
|
|
@@ -105,34 +136,18 @@ export function checkMaxBlastRadius(
|
|
|
105
136
|
): BlastRadiusResult {
|
|
106
137
|
const violations: BlastRadiusViolation[] = [];
|
|
107
138
|
let maxFound = 0;
|
|
139
|
+
const defsStmt = db.prepare(
|
|
140
|
+
`SELECT * FROM nodes WHERE file = ? AND kind IN ('function', 'method', 'class') ORDER BY line`,
|
|
141
|
+
);
|
|
108
142
|
|
|
109
143
|
for (const [file, ranges] of changedRanges) {
|
|
110
144
|
if (noTests && isTestFile(file)) continue;
|
|
111
|
-
const defs =
|
|
112
|
-
.prepare(
|
|
113
|
-
`SELECT * FROM nodes WHERE file = ? AND kind IN ('function', 'method', 'class') ORDER BY line`,
|
|
114
|
-
)
|
|
115
|
-
.all(file) as Array<{
|
|
116
|
-
id: number;
|
|
117
|
-
name: string;
|
|
118
|
-
kind: string;
|
|
119
|
-
file: string;
|
|
120
|
-
line: number;
|
|
121
|
-
end_line: number | null;
|
|
122
|
-
}>;
|
|
145
|
+
const defs = defsStmt.all(file) as DefRow[];
|
|
123
146
|
|
|
124
147
|
for (let i = 0; i < defs.length; i++) {
|
|
125
148
|
const def = defs[i]!;
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
let overlaps = false;
|
|
129
|
-
for (const range of ranges) {
|
|
130
|
-
if (range.start <= endLine && range.end >= def.line) {
|
|
131
|
-
overlaps = true;
|
|
132
|
-
break;
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
if (!overlaps) continue;
|
|
149
|
+
const endLine = defEndLine(def, defs[i + 1]);
|
|
150
|
+
if (!rangesOverlap(def.line, endLine, ranges)) continue;
|
|
136
151
|
|
|
137
152
|
const { totalDependents: totalCallers } = bfsTransitiveCallers(db, def.id, {
|
|
138
153
|
noTests,
|
|
@@ -364,11 +379,13 @@ function runPredicates(
|
|
|
364
379
|
return predicates;
|
|
365
380
|
}
|
|
366
381
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
382
|
+
function makeEmptyCheck(): CheckResult {
|
|
383
|
+
return {
|
|
384
|
+
predicates: [],
|
|
385
|
+
summary: { total: 0, passed: 0, failed: 0, changedFiles: 0, newFiles: 0 },
|
|
386
|
+
passed: true,
|
|
387
|
+
};
|
|
388
|
+
}
|
|
372
389
|
|
|
373
390
|
export function checkData(customDbPath: string | undefined, opts: CheckOpts = {}): CheckResult {
|
|
374
391
|
const db = openReadonlyOrFail(customDbPath);
|
|
@@ -394,10 +411,10 @@ export function checkData(customDbPath: string | undefined, opts: CheckOpts = {}
|
|
|
394
411
|
return { error: `Failed to run git diff: ${(e as Error).message}` };
|
|
395
412
|
}
|
|
396
413
|
|
|
397
|
-
if (!diffOutput.trim()) return
|
|
414
|
+
if (!diffOutput.trim()) return makeEmptyCheck();
|
|
398
415
|
|
|
399
416
|
const diff = parseDiffOutput(diffOutput);
|
|
400
|
-
if (diff.changedRanges.size === 0) return
|
|
417
|
+
if (diff.changedRanges.size === 0) return makeEmptyCheck();
|
|
401
418
|
|
|
402
419
|
const predicates = runPredicates(db, diff, flags, repoRoot, noTests, maxDepth);
|
|
403
420
|
|
package/src/features/cochange.ts
CHANGED
|
@@ -137,77 +137,50 @@ export function computeCoChanges(
|
|
|
137
137
|
return { pairs: results, fileCommitCounts };
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
const db = openDb(dbPath);
|
|
153
|
-
initSchema(db);
|
|
154
|
-
|
|
155
|
-
const repoRoot = path.resolve(path.dirname(dbPath), '..');
|
|
156
|
-
|
|
157
|
-
if (!fs.existsSync(path.join(repoRoot, '.git'))) {
|
|
158
|
-
closeDb(db);
|
|
159
|
-
return { error: `Not a git repository: ${repoRoot}` };
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
const since = opts.since || '1 year ago';
|
|
163
|
-
const minSupport = opts.minSupport ?? 3;
|
|
164
|
-
const maxFilesPerCommit = opts.maxFilesPerCommit ?? 50;
|
|
165
|
-
|
|
166
|
-
// Check for incremental state
|
|
167
|
-
let afterSha: string | null = null;
|
|
168
|
-
if (!opts.full) {
|
|
169
|
-
try {
|
|
170
|
-
const row = db
|
|
171
|
-
.prepare<{ value: string }>(
|
|
172
|
-
"SELECT value FROM co_change_meta WHERE key = 'last_analyzed_commit'",
|
|
173
|
-
)
|
|
174
|
-
.get();
|
|
175
|
-
if (row) afterSha = row.value;
|
|
176
|
-
} catch {
|
|
177
|
-
/* table may not exist yet */
|
|
178
|
-
}
|
|
140
|
+
/** Read the SHA of the most recently analyzed commit (incremental state). */
|
|
141
|
+
function loadLastAnalyzedSha(db: BetterSqlite3Database): string | null {
|
|
142
|
+
try {
|
|
143
|
+
const row = db
|
|
144
|
+
.prepare<{ value: string }>(
|
|
145
|
+
"SELECT value FROM co_change_meta WHERE key = 'last_analyzed_commit'",
|
|
146
|
+
)
|
|
147
|
+
.get();
|
|
148
|
+
return row ? row.value : null;
|
|
149
|
+
} catch {
|
|
150
|
+
/* table may not exist yet */
|
|
151
|
+
return null;
|
|
179
152
|
}
|
|
153
|
+
}
|
|
180
154
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
155
|
+
/** Wipe all co-change tables for a full re-scan. */
|
|
156
|
+
function clearCoChangeTables(db: BetterSqlite3Database): void {
|
|
157
|
+
db.exec('DELETE FROM co_changes');
|
|
158
|
+
db.exec('DELETE FROM co_change_meta');
|
|
159
|
+
db.exec('DELETE FROM file_commit_counts');
|
|
160
|
+
}
|
|
187
161
|
|
|
188
|
-
|
|
189
|
-
|
|
162
|
+
/** Collect the set of files currently tracked by the graph for filtering. */
|
|
163
|
+
function loadKnownFiles(db: BetterSqlite3Database): Set<string> | null {
|
|
190
164
|
try {
|
|
191
165
|
const rows = db.prepare<{ file: string }>('SELECT DISTINCT file FROM nodes').all();
|
|
192
|
-
|
|
166
|
+
return new Set(rows.map((r) => r.file));
|
|
193
167
|
} catch {
|
|
194
168
|
/* nodes table may not exist */
|
|
169
|
+
return null;
|
|
195
170
|
}
|
|
171
|
+
}
|
|
196
172
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
// Upsert per-file commit counts so Jaccard can be recomputed from totals
|
|
173
|
+
/** Upsert per-file commit counts and pair counts (Jaccard recomputed later). */
|
|
174
|
+
function persistCoChangeResults(
|
|
175
|
+
db: BetterSqlite3Database,
|
|
176
|
+
fileCommitCounts: Map<string, number>,
|
|
177
|
+
coChanges: Map<string, CoChangePair>,
|
|
178
|
+
): void {
|
|
205
179
|
const fileCountUpsert = db.prepare(`
|
|
206
180
|
INSERT INTO file_commit_counts (file, commit_count) VALUES (?, ?)
|
|
207
181
|
ON CONFLICT(file) DO UPDATE SET commit_count = commit_count + excluded.commit_count
|
|
208
182
|
`);
|
|
209
183
|
|
|
210
|
-
// Upsert pair counts (accumulate commit_count, jaccard placeholder — recomputed below)
|
|
211
184
|
const pairUpsert = db.prepare(`
|
|
212
185
|
INSERT INTO co_changes (file_a, file_b, commit_count, jaccard, last_commit_epoch)
|
|
213
186
|
VALUES (?, ?, ?, 0, ?)
|
|
@@ -226,24 +199,31 @@ export function analyzeCoChanges(
|
|
|
226
199
|
}
|
|
227
200
|
});
|
|
228
201
|
insertMany();
|
|
202
|
+
}
|
|
229
203
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
if (affectedFiles.length
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
)
|
|
239
|
-
FROM file_commit_counts fa, file_commit_counts fb
|
|
240
|
-
WHERE fa.file = co_changes.file_a AND fb.file = co_changes.file_b
|
|
204
|
+
/** Recompute Jaccard for every pair touching any file in `affectedFiles`. */
|
|
205
|
+
function recomputeJaccardForAffected(db: BetterSqlite3Database, affectedFiles: string[]): void {
|
|
206
|
+
if (affectedFiles.length === 0) return;
|
|
207
|
+
const ph = affectedFiles.map(() => '?').join(',');
|
|
208
|
+
db.prepare(`
|
|
209
|
+
UPDATE co_changes SET jaccard = (
|
|
210
|
+
SELECT CAST(co_changes.commit_count AS REAL) / (
|
|
211
|
+
COALESCE(fa.commit_count, 0) + COALESCE(fb.commit_count, 0) - co_changes.commit_count
|
|
241
212
|
)
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
213
|
+
FROM file_commit_counts fa, file_commit_counts fb
|
|
214
|
+
WHERE fa.file = co_changes.file_a AND fb.file = co_changes.file_b
|
|
215
|
+
)
|
|
216
|
+
WHERE file_a IN (${ph}) OR file_b IN (${ph})
|
|
217
|
+
`).run(...affectedFiles, ...affectedFiles);
|
|
218
|
+
}
|
|
245
219
|
|
|
246
|
-
|
|
220
|
+
/** Update co_change_meta with the latest analyzer run parameters. */
|
|
221
|
+
function updateCoChangeMeta(
|
|
222
|
+
db: BetterSqlite3Database,
|
|
223
|
+
commits: CommitEntry[],
|
|
224
|
+
since: string,
|
|
225
|
+
minSupport: number,
|
|
226
|
+
): void {
|
|
247
227
|
const metaUpsert = db.prepare(`
|
|
248
228
|
INSERT INTO co_change_meta (key, value) VALUES (?, ?)
|
|
249
229
|
ON CONFLICT(key) DO UPDATE SET value = excluded.value
|
|
@@ -254,6 +234,49 @@ export function analyzeCoChanges(
|
|
|
254
234
|
metaUpsert.run('analyzed_at', new Date().toISOString());
|
|
255
235
|
metaUpsert.run('since', since);
|
|
256
236
|
metaUpsert.run('min_support', String(minSupport));
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export function analyzeCoChanges(
|
|
240
|
+
customDbPath?: string,
|
|
241
|
+
opts: {
|
|
242
|
+
since?: string;
|
|
243
|
+
minSupport?: number;
|
|
244
|
+
maxFilesPerCommit?: number;
|
|
245
|
+
full?: boolean;
|
|
246
|
+
} = {},
|
|
247
|
+
):
|
|
248
|
+
| { pairsFound: number; commitsScanned: number; since: string; minSupport: number }
|
|
249
|
+
| { error: string } {
|
|
250
|
+
const dbPath = findDbPath(customDbPath);
|
|
251
|
+
const db = openDb(dbPath);
|
|
252
|
+
initSchema(db);
|
|
253
|
+
|
|
254
|
+
const repoRoot = path.resolve(path.dirname(dbPath), '..');
|
|
255
|
+
|
|
256
|
+
if (!fs.existsSync(path.join(repoRoot, '.git'))) {
|
|
257
|
+
closeDb(db);
|
|
258
|
+
return { error: `Not a git repository: ${repoRoot}` };
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const since = opts.since || '1 year ago';
|
|
262
|
+
const minSupport = opts.minSupport ?? 3;
|
|
263
|
+
const maxFilesPerCommit = opts.maxFilesPerCommit ?? 50;
|
|
264
|
+
|
|
265
|
+
const afterSha = opts.full ? null : loadLastAnalyzedSha(db);
|
|
266
|
+
if (opts.full) clearCoChangeTables(db);
|
|
267
|
+
|
|
268
|
+
const knownFiles = loadKnownFiles(db);
|
|
269
|
+
|
|
270
|
+
const { commits } = scanGitHistory(repoRoot, { since, afterSha });
|
|
271
|
+
const { pairs: coChanges, fileCommitCounts } = computeCoChanges(commits, {
|
|
272
|
+
minSupport,
|
|
273
|
+
maxFilesPerCommit,
|
|
274
|
+
knownFiles,
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
persistCoChangeResults(db, fileCommitCounts, coChanges);
|
|
278
|
+
recomputeJaccardForAffected(db, [...fileCommitCounts.keys()]);
|
|
279
|
+
updateCoChangeMeta(db, commits, since, minSupport);
|
|
257
280
|
|
|
258
281
|
const totalPairs = db
|
|
259
282
|
.prepare<{ cnt: number }>('SELECT COUNT(*) as cnt FROM co_changes')
|