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
23 changes: 18 additions & 5 deletions src/builder.js
Original file line number Diff line number Diff line change
Expand Up @@ -1272,7 +1272,7 @@ export async function buildGraph(rootDir, opts = {}) {
}
_t.rolesMs = performance.now() - _t.roles0;

// For incremental builds, filter out reverse-dep-only files from AST/complexity
// For incremental builds, filter out reverse-dep-only files from AST/complexity/CFG/dataflow
// — their content didn't change, so existing ast_nodes/function_complexity rows are valid.
let astComplexitySymbols = allSymbols;
if (!isFullBuild) {
Expand All @@ -1287,13 +1287,12 @@ export async function buildGraph(rootDir, opts = {}) {
}
}
debug(
`AST/complexity: processing ${astComplexitySymbols.size} changed files (skipping ${reverseDepFiles.size} reverse-deps)`,
`AST/complexity/CFG/dataflow: processing ${astComplexitySymbols.size} changed files (skipping ${reverseDepFiles.size} reverse-deps)`,
);
}
}

// AST node extraction (calls, new, string, regex, throw, await)
// Must run before complexity which releases _tree references
_t.ast0 = performance.now();
if (opts.ast !== false) {
try {
Expand All @@ -1317,12 +1316,25 @@ export async function buildGraph(rootDir, opts = {}) {
}
_t.complexityMs = performance.now() - _t.complexity0;

// Pre-parse files missing WASM trees (native builds) so CFG + dataflow
// share a single parse pass instead of each creating parsers independently
if (opts.cfg !== false || opts.dataflow !== false) {
_t.wasmPre0 = performance.now();
try {
const { ensureWasmTrees } = await import('./parser.js');
await ensureWasmTrees(astComplexitySymbols, rootDir);
} catch (err) {
debug(`WASM pre-parse failed: ${err.message}`);
}
_t.wasmPreMs = performance.now() - _t.wasmPre0;
}

// CFG analysis (skip with --no-cfg)
if (opts.cfg !== false) {
_t.cfg0 = performance.now();
try {
const { buildCFGData } = await import('./cfg.js');
await buildCFGData(db, allSymbols, rootDir, engineOpts);
await buildCFGData(db, astComplexitySymbols, rootDir, engineOpts);
} catch (err) {
debug(`CFG analysis failed: ${err.message}`);
}
Expand All @@ -1334,7 +1346,7 @@ export async function buildGraph(rootDir, opts = {}) {
_t.dataflow0 = performance.now();
try {
const { buildDataflowEdges } = await import('./dataflow.js');
await buildDataflowEdges(db, allSymbols, rootDir, engineOpts);
await buildDataflowEdges(db, astComplexitySymbols, rootDir, engineOpts);
} catch (err) {
debug(`Dataflow analysis failed: ${err.message}`);
}
Expand Down Expand Up @@ -1434,6 +1446,7 @@ export async function buildGraph(rootDir, opts = {}) {
rolesMs: +_t.rolesMs.toFixed(1),
astMs: +_t.astMs.toFixed(1),
complexityMs: +_t.complexityMs.toFixed(1),
...(_t.wasmPreMs != null && { wasmPreMs: +_t.wasmPreMs.toFixed(1) }),
...(_t.cfgMs != null && { cfgMs: +_t.cfgMs.toFixed(1) }),
...(_t.dataflowMs != null && { dataflowMs: +_t.dataflowMs.toFixed(1) }),
},
Expand Down
3 changes: 0 additions & 3 deletions src/complexity.js
Original file line number Diff line number Diff line change
Expand Up @@ -1769,9 +1769,6 @@ export async function buildComplexityMetrics(db, fileSymbols, rootDir, _engineOp
);
analyzed++;
}

// Release cached tree for GC
symbols._tree = null;
}
});

Expand Down
54 changes: 54 additions & 0 deletions src/parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ function grammarPath(name) {

let _initialized = false;

// Memoized parsers — avoids reloading WASM grammars on every createParsers() call
let _cachedParsers = null;

// Query cache for JS/TS/TSX extractors (populated during createParsers)
const _queryCache = new Map();

Expand Down Expand Up @@ -66,6 +69,8 @@ const TS_EXTRA_PATTERNS = [
];

export async function createParsers() {
if (_cachedParsers) return _cachedParsers;

if (!_initialized) {
await Parser.init();
_initialized = true;
Expand Down Expand Up @@ -94,6 +99,7 @@ export async function createParsers() {
parsers.set(entry.id, null);
}
}
_cachedParsers = parsers;
return parsers;
}

Expand All @@ -104,6 +110,54 @@ export function getParser(parsers, filePath) {
return parsers.get(entry.id) || null;
}

/**
* Pre-parse files missing `_tree` via WASM so downstream phases (CFG, dataflow)
* don't each need to create parsers and re-parse independently.
* Only parses files whose extension is in SUPPORTED_EXTENSIONS.
*
* @param {Map<string, object>} fileSymbols - Map<relPath, { definitions, _tree, _langId, ... }>
* @param {string} rootDir - absolute project root
*/
export async function ensureWasmTrees(fileSymbols, rootDir) {
// Check if any file needs a tree
let needsParse = false;
for (const [relPath, symbols] of fileSymbols) {
if (!symbols._tree) {
const ext = path.extname(relPath).toLowerCase();
if (_extToLang.has(ext)) {
needsParse = true;
break;
}
}
}
if (!needsParse) return;

const parsers = await createParsers();

for (const [relPath, symbols] of fileSymbols) {
if (symbols._tree) continue;
const ext = path.extname(relPath).toLowerCase();
const entry = _extToLang.get(ext);
if (!entry) continue;
const parser = parsers.get(entry.id);
if (!parser) continue;

const absPath = path.join(rootDir, relPath);
let code;
try {
code = fs.readFileSync(absPath, 'utf-8');
} catch {
continue;
}
try {
symbols._tree = parser.parse(code);
symbols._langId = entry.id;
} catch {
// skip files that fail to parse
}
}
}

/**
* Check whether the required WASM grammar files exist on disk.
*/
Expand Down
Loading