@@ -84,6 +84,24 @@ interface FileSymbols {
8484 _langId ?: string ;
8585}
8686
87+ /**
88+ * Check whether all function/method definitions in a single file already
89+ * have native CFG data (blocks populated by the Rust extractor).
90+ * cfg === null means no body (expected); cfg with empty blocks means not computed.
91+ */
92+ function hasNativeCfgForFile ( symbols : FileSymbols ) : boolean {
93+ return symbols . definitions
94+ . filter (
95+ ( d ) =>
96+ ( d . kind === 'function' || d . kind === 'method' ) &&
97+ d . line > 0 &&
98+ d . endLine != null &&
99+ d . endLine > d . line &&
100+ ! d . name . includes ( '.' ) ,
101+ )
102+ . every ( ( d ) => d . cfg === null || ( d . cfg ?. blocks ?. length ?? 0 ) > 0 ) ;
103+ }
104+
87105async function initCfgParsers (
88106 fileSymbols : Map < string , FileSymbols > ,
89107) : Promise < { parsers : unknown ; getParserFn : unknown } > {
@@ -93,17 +111,7 @@ async function initCfgParsers(
93111 if ( ! symbols . _tree ) {
94112 const ext = path . extname ( relPath ) . toLowerCase ( ) ;
95113 if ( CFG_EXTENSIONS . has ( ext ) ) {
96- const hasNativeCfg = symbols . definitions
97- . filter (
98- ( d ) =>
99- ( d . kind === 'function' || d . kind === 'method' ) &&
100- d . line > 0 &&
101- d . endLine != null &&
102- d . endLine > d . line &&
103- ! d . name . includes ( '.' ) ,
104- )
105- . every ( ( d ) => d . cfg === null || ( d . cfg ?. blocks ?. length ?? 0 ) > 0 ) ;
106- if ( ! hasNativeCfg ) {
114+ if ( ! hasNativeCfgForFile ( symbols ) ) {
107115 needsFallback = true ;
108116 break ;
109117 }
@@ -136,11 +144,7 @@ function getTreeAndLang(
136144 let tree = symbols . _tree ;
137145 let langId = symbols . _langId ;
138146
139- const allNative = symbols . definitions
140- . filter ( ( d ) => ( d . kind === 'function' || d . kind === 'method' ) && d . line )
141- . every ( ( d ) => d . cfg === null || ( d . cfg ?. blocks ?. length ?? 0 ) > 0 ) ;
142-
143- if ( ! tree && ! allNative ) {
147+ if ( ! tree && ! hasNativeCfgForFile ( symbols ) ) {
144148 if ( ! getParserFn ) return null ;
145149 langId = extToLang . get ( ext ) ;
146150 if ( ! langId || ! CFG_RULES . has ( langId ) ) return null ;
@@ -253,14 +257,43 @@ function persistCfg(
253257
254258// ─── Build-Time: Compute CFG for Changed Files ─────────────────────────
255259
260+ /**
261+ * Check if all function/method definitions across all files already have
262+ * native CFG data (blocks array populated by the Rust extractor).
263+ * When true, the WASM parser and JS CFG visitor can be fully bypassed.
264+ */
265+ function allCfgNative ( fileSymbols : Map < string , FileSymbols > ) : boolean {
266+ let hasCfgFile = false ;
267+ for ( const [ relPath , symbols ] of fileSymbols ) {
268+ if ( symbols . _tree ) continue ; // already parsed via WASM; will use _tree in slow path
269+ const ext = path . extname ( relPath ) . toLowerCase ( ) ;
270+ if ( ! CFG_EXTENSIONS . has ( ext ) ) continue ;
271+ hasCfgFile = true ;
272+
273+ if ( ! hasNativeCfgForFile ( symbols ) ) return false ;
274+ }
275+ // Return false when no CFG files found (empty map, all _tree, or all non-CFG
276+ // extensions) to avoid vacuously triggering the fast path.
277+ return hasCfgFile ;
278+ }
279+
256280export async function buildCFGData (
257281 db : BetterSqlite3Database ,
258282 fileSymbols : Map < string , FileSymbols > ,
259283 rootDir : string ,
260284 _engineOpts ?: unknown ,
261285) : Promise < void > {
286+ // Fast path: when all function/method defs already have native CFG data,
287+ // skip WASM parser init, tree parsing, and JS visitor entirely — just persist.
288+ const allNative = allCfgNative ( fileSymbols ) ;
289+
262290 const extToLang = buildExtToLangMap ( ) ;
263- const { parsers, getParserFn } = await initCfgParsers ( fileSymbols ) ;
291+ let parsers : unknown = null ;
292+ let getParserFn : unknown = null ;
293+
294+ if ( ! allNative ) {
295+ ( { parsers, getParserFn } = await initCfgParsers ( fileSymbols ) ) ;
296+ }
264297
265298 const insertBlock = db . prepare (
266299 `INSERT INTO cfg_blocks (function_node_id, block_index, block_type, start_line, end_line, label)
@@ -277,6 +310,35 @@ export async function buildCFGData(
277310 const ext = path . extname ( relPath ) . toLowerCase ( ) ;
278311 if ( ! CFG_EXTENSIONS . has ( ext ) ) continue ;
279312
313+ // Native fast path: skip tree/visitor setup when all CFG is pre-computed.
314+ // Only apply to files without _tree — files with _tree were WASM-parsed
315+ // and need the slow path (visitor) to compute CFG.
316+ if ( allNative && ! symbols . _tree ) {
317+ for ( const def of symbols . definitions ) {
318+ if ( def . kind !== 'function' && def . kind !== 'method' ) continue ;
319+ if ( ! def . line ) continue ;
320+
321+ const nodeId = getFunctionNodeId ( db , def . name , relPath , def . line ) ;
322+ if ( ! nodeId ) continue ;
323+
324+ // Always delete stale CFG rows (handles body-removed case)
325+ deleteCfgForNode ( db , nodeId ) ;
326+ if ( ! def . cfg ?. blocks ?. length ) continue ;
327+
328+ persistCfg (
329+ def . cfg as unknown as { blocks : CfgBuildBlock [ ] ; edges : CfgBuildEdge [ ] } ,
330+ nodeId ,
331+ insertBlock ,
332+ insertEdge ,
333+ ) ;
334+ analyzed ++ ;
335+ }
336+ continue ;
337+ }
338+
339+ // When allNative=true, parsers/getParserFn are null. This is safe because
340+ // _tree files use symbols._tree directly in getTreeAndLang (the parser
341+ // code path is never reached). Non-_tree files are handled by the fast path above.
280342 const treeLang = getTreeAndLang ( symbols , relPath , rootDir , extToLang , parsers , getParserFn ) ;
281343 if ( ! treeLang ) continue ;
282344 const { tree, langId } = treeLang ;
@@ -309,9 +371,10 @@ export async function buildCFGData(
309371 if ( r ) cfg = { blocks : r . blocks , edges : r . edges } ;
310372 }
311373
374+ // Always purge stale rows (handles body-removed case)
375+ deleteCfgForNode ( db , nodeId ) ;
312376 if ( ! cfg || cfg . blocks . length === 0 ) continue ;
313377
314- deleteCfgForNode ( db , nodeId ) ;
315378 persistCfg ( cfg , nodeId , insertBlock , insertEdge ) ;
316379 analyzed ++ ;
317380 }
0 commit comments