Skip to content

Commit d9085b9

Browse files
committed
Merge remote-tracking branch 'origin/main' into feat/unified-ast-analysis-framework
Impact: 74 functions changed, 55 affected
2 parents 3db3861 + a25a544 commit d9085b9

45 files changed

Lines changed: 2404 additions & 1863 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

src/ast-analysis/engine.js

Lines changed: 86 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,12 @@
77
* - CFG construction (basic blocks + edges)
88
* - Dataflow analysis (define-use chains, arg flows, mutations)
99
*
10-
* Two modes:
11-
* Mode A (node-level visitor): AST + complexity + dataflow — single DFS per file
12-
* Mode B (statement-level): CFG keeps its own traversal via buildFunctionCFG
10+
* All 4 analyses run as visitors in a single DFS walk via walkWithVisitors.
1311
*
1412
* Optimization strategy: for files with WASM trees, run all applicable visitors
15-
* in a single walkWithVisitors call, then store results in the format that the
16-
* existing buildXxx functions expect as pre-computed data. This eliminates ~3
17-
* redundant tree traversals per file.
13+
* in a single walkWithVisitors call. Store results in the format that buildXxx
14+
* functions already expect as pre-computed data (same fields as native engine
15+
* output). This eliminates redundant tree traversals per file.
1816
*/
1917

2018
import path from 'node:path';
@@ -33,6 +31,7 @@ import { buildExtensionSet, buildExtToLangMap } from './shared.js';
3331
import { walkWithVisitors } from './visitor.js';
3432
import { functionName as getFuncName } from './visitor-utils.js';
3533
import { createAstStoreVisitor } from './visitors/ast-store-visitor.js';
34+
import { createCfgVisitor } from './visitors/cfg-visitor.js';
3635
import { createComplexityVisitor } from './visitors/complexity-visitor.js';
3736
import { createDataflowVisitor } from './visitors/dataflow-visitor.js';
3837

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

7776
// ── WASM pre-parse for files that need it ───────────────────────────
78-
if (doCfg || doDataflow) {
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) {
7980
let needsWasmTrees = false;
8081
for (const [relPath, symbols] of fileSymbols) {
8182
if (symbols._tree) continue;
8283
const ext = path.extname(relPath).toLowerCase();
8384

84-
if (doCfg && CFG_EXTENSIONS.has(ext)) {
85-
const fnDefs = (symbols.definitions || []).filter(
86-
(d) => (d.kind === 'function' || d.kind === 'method') && d.line,
87-
);
88-
if (
89-
fnDefs.length > 0 &&
90-
!fnDefs.every((d) => d.cfg === null || Array.isArray(d.cfg?.blocks))
91-
) {
92-
needsWasmTrees = true;
93-
break;
94-
}
95-
}
96-
if (doDataflow && !symbols.dataflow && DATAFLOW_EXTENSIONS.has(ext)) {
85+
if (!symbols.dataflow && DATAFLOW_EXTENSIONS.has(ext)) {
9786
needsWasmTrees = true;
9887
break;
9988
}
@@ -179,6 +168,24 @@ export async function runAnalyses(db, fileSymbols, rootDir, opts, engineOpts) {
179168
}
180169
}
181170

171+
// ─ CFG visitor ─
172+
const cfgRulesForLang = CFG_RULES.get(langId);
173+
let cfgVisitor = null;
174+
if (doCfg && cfgRulesForLang && CFG_EXTENSIONS.has(ext)) {
175+
// Only use visitor if some functions lack pre-computed CFG
176+
const needsWasmCfg = defs.some(
177+
(d) =>
178+
(d.kind === 'function' || d.kind === 'method') &&
179+
d.line &&
180+
d.cfg !== null &&
181+
!Array.isArray(d.cfg?.blocks),
182+
);
183+
if (needsWasmCfg) {
184+
cfgVisitor = createCfgVisitor(cfgRulesForLang);
185+
visitors.push(cfgVisitor);
186+
}
187+
}
188+
182189
// ─ Dataflow visitor ─
183190
const dfRules = DATAFLOW_RULES.get(langId);
184191
let dataflowVisitor = null;
@@ -210,12 +217,21 @@ export async function runAnalyses(db, fileSymbols, rootDir, opts, engineOpts) {
210217
for (const r of complexityResults) {
211218
if (r.funcNode) {
212219
const line = r.funcNode.startPosition.row + 1;
213-
resultByLine.set(line, r);
220+
if (!resultByLine.has(line)) resultByLine.set(line, []);
221+
resultByLine.get(line).push(r);
214222
}
215223
}
216224
for (const def of defs) {
217225
if ((def.kind === 'function' || def.kind === 'method') && def.line && !def.complexity) {
218-
const funcResult = resultByLine.get(def.line);
226+
const candidates = resultByLine.get(def.line);
227+
const funcResult = !candidates
228+
? undefined
229+
: candidates.length === 1
230+
? candidates[0]
231+
: (candidates.find((r) => {
232+
const n = r.funcNode.childForFieldName('name');
233+
return n && n.text === def.name;
234+
}) ?? candidates[0]);
219235
if (funcResult) {
220236
const { metrics } = funcResult;
221237
const loc = computeLOCMetrics(funcResult.funcNode, langId);
@@ -241,6 +257,54 @@ export async function runAnalyses(db, fileSymbols, rootDir, opts, engineOpts) {
241257
}
242258
}
243259

260+
// ─ Store CFG results on definitions (buildCFGData will find def.cfg and skip its walk) ─
261+
if (cfgVisitor) {
262+
const cfgResults = results.cfg || [];
263+
const cfgByLine = new Map();
264+
for (const r of cfgResults) {
265+
if (r.funcNode) {
266+
const line = r.funcNode.startPosition.row + 1;
267+
if (!cfgByLine.has(line)) cfgByLine.set(line, []);
268+
cfgByLine.get(line).push(r);
269+
}
270+
}
271+
for (const def of defs) {
272+
if (
273+
(def.kind === 'function' || def.kind === 'method') &&
274+
def.line &&
275+
!def.cfg?.blocks?.length
276+
) {
277+
const candidates = cfgByLine.get(def.line);
278+
const cfgResult = !candidates
279+
? undefined
280+
: candidates.length === 1
281+
? candidates[0]
282+
: (candidates.find((r) => {
283+
const n = r.funcNode.childForFieldName('name');
284+
return n && n.text === def.name;
285+
}) ?? candidates[0]);
286+
if (cfgResult) {
287+
def.cfg = { blocks: cfgResult.blocks, edges: cfgResult.edges };
288+
289+
// Override complexity's cyclomatic with CFG-derived value (single source of truth)
290+
// and recompute maintainability index to stay consistent
291+
if (def.complexity && cfgResult.cyclomatic != null) {
292+
def.complexity.cyclomatic = cfgResult.cyclomatic;
293+
const { loc, halstead } = def.complexity;
294+
const volume = halstead ? halstead.volume : 0;
295+
const commentRatio = loc?.loc > 0 ? loc.commentLines / loc.loc : 0;
296+
def.complexity.maintainabilityIndex = computeMaintainabilityIndex(
297+
volume,
298+
cfgResult.cyclomatic,
299+
loc?.sloc ?? 0,
300+
commentRatio,
301+
);
302+
}
303+
}
304+
}
305+
}
306+
}
307+
244308
// ─ Store dataflow results (buildDataflowEdges will find symbols.dataflow and skip its walk) ─
245309
if (dataflowVisitor) {
246310
symbols.dataflow = results.dataflow;

0 commit comments

Comments
 (0)