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