Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 29 additions & 9 deletions src/ast-analysis/engine.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import { createDataflowVisitor } from './visitors/dataflow-visitor.js';
// ─── Extension sets for quick language-support checks ────────────────────

const CFG_EXTENSIONS = buildExtensionSet(CFG_RULES);
const COMPLEXITY_EXTENSIONS = buildExtensionSet(COMPLEXITY_RULES);
const DATAFLOW_EXTENSIONS = buildExtensionSet(DATAFLOW_RULES);
const WALK_EXTENSIONS = buildExtensionSet(AST_TYPE_MAPS);

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

// ── WASM pre-parse for files that need it ───────────────────────────
// CFG now runs as a visitor in the unified walk, so only dataflow
// triggers WASM pre-parse when no tree exists.
if (doDataflow) {
// The native engine only handles parsing (symbols, calls, imports).
// Complexity, CFG, and dataflow all require a WASM tree-sitter tree
// for their visitor walks. Without this, incremental rebuilds on the
// native engine silently lose these analyses for changed files (#468).
if (doComplexity || doCfg || doDataflow) {
let needsWasmTrees = false;
for (const [relPath, symbols] of fileSymbols) {
if (symbols._tree) continue;
const ext = path.extname(relPath).toLowerCase();

if (!symbols.dataflow && DATAFLOW_EXTENSIONS.has(ext)) {
const defs = symbols.definitions || [];

const needsComplexity =
doComplexity &&
COMPLEXITY_EXTENSIONS.has(ext) &&
defs.some((d) => (d.kind === 'function' || d.kind === 'method') && d.line && !d.complexity);
const needsCfg =
doCfg &&
CFG_EXTENSIONS.has(ext) &&
defs.some(
(d) =>
(d.kind === 'function' || d.kind === 'method') &&
d.line &&
d.cfg !== null &&
!Array.isArray(d.cfg?.blocks),
);
const needsDataflow = doDataflow && !symbols.dataflow && DATAFLOW_EXTENSIONS.has(ext);

if (needsComplexity || needsCfg || needsDataflow) {
needsWasmTrees = true;
break;
}
Expand Down Expand Up @@ -320,7 +340,7 @@ export async function runAnalyses(db, fileSymbols, rootDir, opts, engineOpts) {
if (doAst) {
const t0 = performance.now();
try {
const { buildAstNodes } = await import('../ast.js');
const { buildAstNodes } = await import('../features/ast.js');
await buildAstNodes(db, fileSymbols, rootDir, engineOpts);
} catch (err) {
debug(`buildAstNodes failed: ${err.message}`);
Expand All @@ -331,7 +351,7 @@ export async function runAnalyses(db, fileSymbols, rootDir, opts, engineOpts) {
if (doComplexity) {
const t0 = performance.now();
try {
const { buildComplexityMetrics } = await import('../complexity.js');
const { buildComplexityMetrics } = await import('../features/complexity.js');
await buildComplexityMetrics(db, fileSymbols, rootDir, engineOpts);
} catch (err) {
debug(`buildComplexityMetrics failed: ${err.message}`);
Expand All @@ -342,7 +362,7 @@ export async function runAnalyses(db, fileSymbols, rootDir, opts, engineOpts) {
if (doCfg) {
const t0 = performance.now();
try {
const { buildCFGData } = await import('../cfg.js');
const { buildCFGData } = await import('../features/cfg.js');
await buildCFGData(db, fileSymbols, rootDir, engineOpts);
} catch (err) {
debug(`buildCFGData failed: ${err.message}`);
Expand All @@ -353,7 +373,7 @@ export async function runAnalyses(db, fileSymbols, rootDir, opts, engineOpts) {
if (doDataflow) {
const t0 = performance.now();
try {
const { buildDataflowEdges } = await import('../dataflow.js');
const { buildDataflowEdges } = await import('../features/dataflow.js');
await buildDataflowEdges(db, fileSymbols, rootDir, engineOpts);
} catch (err) {
debug(`buildDataflowEdges failed: ${err.message}`);
Expand Down
64 changes: 64 additions & 0 deletions tests/integration/incremental-parity.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,49 @@ function readGraph(dbPath) {
return { nodes, edges };
}

function readAnalysisTables(dbPath) {
const db = new Database(dbPath, { readonly: true });
const result = {};
try {
try {
result.complexity = db
.prepare(
`SELECT fc.node_id, fc.cognitive, fc.cyclomatic, n.name, n.file
FROM function_complexity fc JOIN nodes n ON fc.node_id = n.id
ORDER BY n.name, n.file`,
)
.all();
} catch {
result.complexity = [];
}
try {
result.cfgBlocks = db
.prepare(
`SELECT cb.function_node_id, cb.block_index, cb.block_type, n.name, n.file
FROM cfg_blocks cb JOIN nodes n ON cb.function_node_id = n.id
ORDER BY n.name, n.file, cb.block_index`,
)
.all();
} catch {
result.cfgBlocks = [];
}
try {
result.dataflow = db
.prepare(
`SELECT d.source_id, d.kind, n.name, n.file
FROM dataflow d JOIN nodes n ON d.source_id = n.id
ORDER BY n.name, n.file, d.kind`,
)
.all();
} catch {
result.dataflow = [];
}
} finally {
db.close();
}
return result;
}

describe('Incremental build parity: full vs incremental', () => {
let fullDir;
let incrDir;
Expand Down Expand Up @@ -103,4 +146,25 @@ describe('Incremental build parity: full vs incremental', () => {
const incrGraph = readGraph(path.join(incrDir, '.codegraph', 'graph.db'));
expect(incrGraph.edges).toEqual(fullGraph.edges);
});

it('preserves complexity metrics for changed file (#468)', () => {
const fullAnalysis = readAnalysisTables(path.join(fullDir, '.codegraph', 'graph.db'));
const incrAnalysis = readAnalysisTables(path.join(incrDir, '.codegraph', 'graph.db'));
expect(incrAnalysis.complexity.length).toBeGreaterThan(0);
expect(incrAnalysis.complexity.length).toBe(fullAnalysis.complexity.length);
});

it('preserves CFG blocks for changed file (#468)', () => {
const fullAnalysis = readAnalysisTables(path.join(fullDir, '.codegraph', 'graph.db'));
const incrAnalysis = readAnalysisTables(path.join(incrDir, '.codegraph', 'graph.db'));
expect(incrAnalysis.cfgBlocks.length).toBeGreaterThan(0);
expect(incrAnalysis.cfgBlocks.length).toBe(fullAnalysis.cfgBlocks.length);
});

it('preserves dataflow edges for changed file (#468)', () => {
const fullAnalysis = readAnalysisTables(path.join(fullDir, '.codegraph', 'graph.db'));
const incrAnalysis = readAnalysisTables(path.join(incrDir, '.codegraph', 'graph.db'));
expect(incrAnalysis.dataflow.length).toBeGreaterThan(0);
expect(incrAnalysis.dataflow.length).toBe(fullAnalysis.dataflow.length);
});
});
Loading