Skip to content

Commit 2cebba1

Browse files
authored
fix: restore complexity/CFG/dataflow analysis in builds (#469)
* fix: restore complexity/CFG/dataflow analysis in builds (#468) Two bugs fixed: 1. Broken import paths in ast-analysis/engine.js — the buildXxx functions (buildComplexityMetrics, buildCFGData, buildDataflowEdges, buildAstNodes) were imported as ../complexity.js etc. which resolved to non-existent src/complexity.js instead of src/features/complexity.js. The dynamic imports failed silently inside try/catch, so all analysis tables (function_complexity, cfg_blocks, dataflow) were always empty on every build — both full and incremental, both engines. 2. Missing WASM tree pre-parse for native engine incremental rebuilds — ensureWasmTrees was only called when dataflow needed it, but complexity and CFG visitors also require a WASM tree. On native incremental rebuilds, changed files had their analysis data purged but never re-computed because no WASM tree was available for the visitor walk. Impact: 1 functions changed, 0 affected * fix: align needsCfg gate with walk guard and prevent vacuous test passes Add d.cfg !== null check to needsCfg predicate, matching the unified walk guard and avoiding unnecessary WASM loading for null-CFG defs. Fix wrong column names in readAnalysisTables (node_id -> function_node_id, source_node_id -> source_id) that caused CFG/dataflow queries to silently return empty arrays. Add toBeGreaterThan(0) assertions so these tests fail fast if analysis data is missing. Impact: 1 functions changed, 0 affected * fix: add complexity extension filter and guard db.close with finally Impact: 1 functions changed, 0 affected
1 parent f0770f5 commit 2cebba1

2 files changed

Lines changed: 93 additions & 9 deletions

File tree

src/ast-analysis/engine.js

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import { createDataflowVisitor } from './visitors/dataflow-visitor.js';
3838
// ─── Extension sets for quick language-support checks ────────────────────
3939

4040
const CFG_EXTENSIONS = buildExtensionSet(CFG_RULES);
41+
const COMPLEXITY_EXTENSIONS = buildExtensionSet(COMPLEXITY_RULES);
4142
const DATAFLOW_EXTENSIONS = buildExtensionSet(DATAFLOW_RULES);
4243
const WALK_EXTENSIONS = buildExtensionSet(AST_TYPE_MAPS);
4344

@@ -74,15 +75,34 @@ export async function runAnalyses(db, fileSymbols, rootDir, opts, engineOpts) {
7475
const extToLang = buildExtToLangMap();
7576

7677
// ── WASM pre-parse for files that need it ───────────────────────────
77-
// CFG now runs as a visitor in the unified walk, so only dataflow
78-
// triggers WASM pre-parse when no tree exists.
79-
if (doDataflow) {
78+
// The native engine only handles parsing (symbols, calls, imports).
79+
// Complexity, CFG, and dataflow all require a WASM tree-sitter tree
80+
// for their visitor walks. Without this, incremental rebuilds on the
81+
// native engine silently lose these analyses for changed files (#468).
82+
if (doComplexity || doCfg || doDataflow) {
8083
let needsWasmTrees = false;
8184
for (const [relPath, symbols] of fileSymbols) {
8285
if (symbols._tree) continue;
8386
const ext = path.extname(relPath).toLowerCase();
84-
85-
if (!symbols.dataflow && DATAFLOW_EXTENSIONS.has(ext)) {
87+
const defs = symbols.definitions || [];
88+
89+
const needsComplexity =
90+
doComplexity &&
91+
COMPLEXITY_EXTENSIONS.has(ext) &&
92+
defs.some((d) => (d.kind === 'function' || d.kind === 'method') && d.line && !d.complexity);
93+
const needsCfg =
94+
doCfg &&
95+
CFG_EXTENSIONS.has(ext) &&
96+
defs.some(
97+
(d) =>
98+
(d.kind === 'function' || d.kind === 'method') &&
99+
d.line &&
100+
d.cfg !== null &&
101+
!Array.isArray(d.cfg?.blocks),
102+
);
103+
const needsDataflow = doDataflow && !symbols.dataflow && DATAFLOW_EXTENSIONS.has(ext);
104+
105+
if (needsComplexity || needsCfg || needsDataflow) {
86106
needsWasmTrees = true;
87107
break;
88108
}
@@ -320,7 +340,7 @@ export async function runAnalyses(db, fileSymbols, rootDir, opts, engineOpts) {
320340
if (doAst) {
321341
const t0 = performance.now();
322342
try {
323-
const { buildAstNodes } = await import('../ast.js');
343+
const { buildAstNodes } = await import('../features/ast.js');
324344
await buildAstNodes(db, fileSymbols, rootDir, engineOpts);
325345
} catch (err) {
326346
debug(`buildAstNodes failed: ${err.message}`);
@@ -331,7 +351,7 @@ export async function runAnalyses(db, fileSymbols, rootDir, opts, engineOpts) {
331351
if (doComplexity) {
332352
const t0 = performance.now();
333353
try {
334-
const { buildComplexityMetrics } = await import('../complexity.js');
354+
const { buildComplexityMetrics } = await import('../features/complexity.js');
335355
await buildComplexityMetrics(db, fileSymbols, rootDir, engineOpts);
336356
} catch (err) {
337357
debug(`buildComplexityMetrics failed: ${err.message}`);
@@ -342,7 +362,7 @@ export async function runAnalyses(db, fileSymbols, rootDir, opts, engineOpts) {
342362
if (doCfg) {
343363
const t0 = performance.now();
344364
try {
345-
const { buildCFGData } = await import('../cfg.js');
365+
const { buildCFGData } = await import('../features/cfg.js');
346366
await buildCFGData(db, fileSymbols, rootDir, engineOpts);
347367
} catch (err) {
348368
debug(`buildCFGData failed: ${err.message}`);
@@ -353,7 +373,7 @@ export async function runAnalyses(db, fileSymbols, rootDir, opts, engineOpts) {
353373
if (doDataflow) {
354374
const t0 = performance.now();
355375
try {
356-
const { buildDataflowEdges } = await import('../dataflow.js');
376+
const { buildDataflowEdges } = await import('../features/dataflow.js');
357377
await buildDataflowEdges(db, fileSymbols, rootDir, engineOpts);
358378
} catch (err) {
359379
debug(`buildDataflowEdges failed: ${err.message}`);

tests/integration/incremental-parity.test.js

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,49 @@ function readGraph(dbPath) {
4343
return { nodes, edges };
4444
}
4545

46+
function readAnalysisTables(dbPath) {
47+
const db = new Database(dbPath, { readonly: true });
48+
const result = {};
49+
try {
50+
try {
51+
result.complexity = db
52+
.prepare(
53+
`SELECT fc.node_id, fc.cognitive, fc.cyclomatic, n.name, n.file
54+
FROM function_complexity fc JOIN nodes n ON fc.node_id = n.id
55+
ORDER BY n.name, n.file`,
56+
)
57+
.all();
58+
} catch {
59+
result.complexity = [];
60+
}
61+
try {
62+
result.cfgBlocks = db
63+
.prepare(
64+
`SELECT cb.function_node_id, cb.block_index, cb.block_type, n.name, n.file
65+
FROM cfg_blocks cb JOIN nodes n ON cb.function_node_id = n.id
66+
ORDER BY n.name, n.file, cb.block_index`,
67+
)
68+
.all();
69+
} catch {
70+
result.cfgBlocks = [];
71+
}
72+
try {
73+
result.dataflow = db
74+
.prepare(
75+
`SELECT d.source_id, d.kind, n.name, n.file
76+
FROM dataflow d JOIN nodes n ON d.source_id = n.id
77+
ORDER BY n.name, n.file, d.kind`,
78+
)
79+
.all();
80+
} catch {
81+
result.dataflow = [];
82+
}
83+
} finally {
84+
db.close();
85+
}
86+
return result;
87+
}
88+
4689
describe('Incremental build parity: full vs incremental', () => {
4790
let fullDir;
4891
let incrDir;
@@ -103,4 +146,25 @@ describe('Incremental build parity: full vs incremental', () => {
103146
const incrGraph = readGraph(path.join(incrDir, '.codegraph', 'graph.db'));
104147
expect(incrGraph.edges).toEqual(fullGraph.edges);
105148
});
149+
150+
it('preserves complexity metrics for changed file (#468)', () => {
151+
const fullAnalysis = readAnalysisTables(path.join(fullDir, '.codegraph', 'graph.db'));
152+
const incrAnalysis = readAnalysisTables(path.join(incrDir, '.codegraph', 'graph.db'));
153+
expect(incrAnalysis.complexity.length).toBeGreaterThan(0);
154+
expect(incrAnalysis.complexity.length).toBe(fullAnalysis.complexity.length);
155+
});
156+
157+
it('preserves CFG blocks for changed file (#468)', () => {
158+
const fullAnalysis = readAnalysisTables(path.join(fullDir, '.codegraph', 'graph.db'));
159+
const incrAnalysis = readAnalysisTables(path.join(incrDir, '.codegraph', 'graph.db'));
160+
expect(incrAnalysis.cfgBlocks.length).toBeGreaterThan(0);
161+
expect(incrAnalysis.cfgBlocks.length).toBe(fullAnalysis.cfgBlocks.length);
162+
});
163+
164+
it('preserves dataflow edges for changed file (#468)', () => {
165+
const fullAnalysis = readAnalysisTables(path.join(fullDir, '.codegraph', 'graph.db'));
166+
const incrAnalysis = readAnalysisTables(path.join(incrDir, '.codegraph', 'graph.db'));
167+
expect(incrAnalysis.dataflow.length).toBeGreaterThan(0);
168+
expect(incrAnalysis.dataflow.length).toBe(fullAnalysis.dataflow.length);
169+
});
106170
});

0 commit comments

Comments
 (0)