@@ -42,6 +42,8 @@ import {
4242 parseFilesWasmForBackfill ,
4343} from '../../../parser.js' ;
4444import { computeConfidence } from '../../resolve.js' ;
45+ import type { CallNodeLookup } from '../call-resolver.js' ;
46+ import { findCaller , resolveByMethodOrGlobal } from '../call-resolver.js' ;
4547import type { PipelineContext } from '../context.js' ;
4648import {
4749 batchInsertEdges ,
@@ -558,6 +560,183 @@ function runPostNativeCha(db: BetterSqlite3Database): Set<number> {
558560 return newTargetIds ;
559561}
560562
563+ /**
564+ * Post-pass: backfill prototype-based method definitions and their call edges.
565+ *
566+ * The Rust engine does not recognise `Foo.prototype.bar = function(){}` as a
567+ * method definition, so those nodes are absent from the DB after the native
568+ * orchestrator completes. This pass:
569+ * 1. Re-parses JS/TS files via WASM to obtain the full ExtractorOutput
570+ * (including definitions emitted by extractPrototypeMethodsWalk).
571+ * 2. Inserts any method nodes that are missing from the DB.
572+ * 3. Resolves call edges to those newly-inserted nodes using the WASM typeMap
573+ * and the existing DB node table as a lookup.
574+ */
575+ async function runPostNativePrototypeMethods (
576+ db : BetterSqlite3Database ,
577+ rootDir : string ,
578+ ) : Promise < void > {
579+ // Collect JS/TS file paths from the DB — only extensions where prototype
580+ // patterns can appear.
581+ const jsExts = new Set ( [ '.js' , '.mjs' , '.cjs' , '.ts' , '.tsx' ] ) ;
582+ const fileRows = db
583+ . prepare (
584+ `SELECT DISTINCT file FROM nodes WHERE kind = 'file' AND file IS NOT NULL ORDER BY file` ,
585+ )
586+ . all ( ) as Array < { file : string } > ;
587+
588+ const jsFiles = fileRows
589+ . map ( ( r ) => r . file )
590+ . filter ( ( f ) => jsExts . has ( path . extname ( f ) . toLowerCase ( ) ) ) ;
591+
592+ if ( jsFiles . length === 0 ) return ;
593+
594+ // WASM-parse all JS/TS files to get full ExtractorOutput including
595+ // prototype method definitions and typeMap entries.
596+ const absPaths = jsFiles . map ( ( f ) => path . join ( rootDir , f ) ) ;
597+ let wasmResults : Map < string , ExtractorOutput > ;
598+ try {
599+ wasmResults = await parseFilesWasmForBackfill ( absPaths , rootDir ) ;
600+ } catch ( e ) {
601+ debug ( `runPostNativePrototypeMethods: WASM parse failed: ${ toErrorMessage ( e ) } ` ) ;
602+ return ;
603+ }
604+
605+ if ( wasmResults . size === 0 ) return ;
606+
607+ // Check which nodes already exist — INSERT OR IGNORE handles races but
608+ // we need the IDs of newly inserted rows, so we check first.
609+ const existsStmt = db . prepare (
610+ `SELECT id FROM nodes WHERE name = ? AND kind = 'method' AND file = ?` ,
611+ ) ;
612+
613+ // Insert rows: [name, kind, file, line, end_line, parent_id, qualified_name, scope, visibility]
614+ const newNodeRows : unknown [ ] [ ] = [ ] ;
615+ // Track which file+definition combos are new so we can insert edges for them.
616+ const newDefs : Array < { name : string ; file : string ; line : number } > = [ ] ;
617+
618+ for ( const [ relPath , symbols ] of wasmResults ) {
619+ for ( const def of symbols . definitions ?? [ ] ) {
620+ if ( def . kind !== 'method' ) continue ;
621+ const dotIdx = def . name . indexOf ( '.' ) ;
622+ if ( dotIdx === - 1 ) continue ; // skip bare method names (shouldn't happen, but guard)
623+
624+ // Only insert if the node is not already in the DB.
625+ const existing = existsStmt . get ( def . name , relPath ) as { id : number } | undefined ;
626+ if ( existing ) continue ;
627+
628+ const scope = def . name . slice ( 0 , dotIdx ) ;
629+ newNodeRows . push ( [
630+ def . name ,
631+ 'method' ,
632+ relPath ,
633+ def . line ,
634+ def . endLine ?? null ,
635+ null ,
636+ def . name ,
637+ scope ,
638+ null ,
639+ ] ) ;
640+ newDefs . push ( { name : def . name , file : relPath , line : def . line } ) ;
641+ }
642+ }
643+
644+ if ( newNodeRows . length === 0 ) return ;
645+
646+ db . transaction ( ( ) => batchInsertNodes ( db , newNodeRows ) ) ( ) ;
647+
648+ // Build a name → node lookup from all DB nodes (including newly inserted ones).
649+ type NodeEntry = { id : number ; file : string ; kind : string } ;
650+ const byNameMap = new Map < string , NodeEntry [ ] > ( ) ;
651+ const byNameFileMap = new Map < string , NodeEntry [ ] > ( ) ;
652+ const byIdKey = new Map < string , { id : number } > ( ) ;
653+
654+ const allNodes = db
655+ . prepare ( `SELECT id, name, kind, file, line FROM nodes WHERE kind != 'file'` )
656+ . all ( ) as Array < { id : number ; name : string ; kind : string ; file : string ; line : number } > ;
657+
658+ for ( const n of allNodes ) {
659+ const list = byNameMap . get ( n . name ) ;
660+ if ( list ) list . push ( n ) ;
661+ else byNameMap . set ( n . name , [ n ] ) ;
662+
663+ const fk = `${ n . name } ::${ n . file } ` ;
664+ const flist = byNameFileMap . get ( fk ) ;
665+ if ( flist ) flist . push ( n ) ;
666+ else byNameFileMap . set ( fk , [ n ] ) ;
667+
668+ byIdKey . set ( `${ n . name } |${ n . kind } |${ n . file } |${ n . line } ` , { id : n . id } ) ;
669+ }
670+
671+ const lookup : CallNodeLookup = {
672+ byName : ( name ) => byNameMap . get ( name ) ?? [ ] ,
673+ byNameAndFile : ( name , file ) => byNameFileMap . get ( `${ name } ::${ file } ` ) ?? [ ] ,
674+ isBarrel : ( ) => false ,
675+ resolveBarrel : ( ) => null ,
676+ nodeId : ( name , kind , file , line ) => byIdKey . get ( `${ name } |${ kind } |${ file } |${ line } ` ) ,
677+ } ;
678+
679+ // Build a fast set of newly-inserted node IDs so we only emit edges to
680+ // prototype nodes that the Rust engine missed (avoids duplicating existing edges).
681+ const newNodeIds = new Set < number > (
682+ newDefs . flatMap ( ( d ) => {
683+ const nodes = byNameFileMap . get ( `${ d . name } ::${ d . file } ` ) ;
684+ return nodes ? nodes . map ( ( n ) => n . id ) : [ ] ;
685+ } ) ,
686+ ) ;
687+
688+ // Seed seenByPair from existing call edges to avoid duplicates.
689+ const existingPairs = db
690+ . prepare ( `SELECT source_id, target_id FROM edges WHERE kind = 'calls'` )
691+ . all ( ) as Array < { source_id : number ; target_id : number } > ;
692+ const seenByPair = new Set < string > ( existingPairs . map ( ( e ) => `${ e . source_id } |${ e . target_id } ` ) ) ;
693+
694+ // For each file that produced new prototype nodes, resolve call edges.
695+ const newEdgeRows : unknown [ ] [ ] = [ ] ;
696+ const newDefFiles = new Set ( newDefs . map ( ( d ) => d . file ) ) ;
697+
698+ for ( const [ relPath , symbols ] of wasmResults ) {
699+ if ( ! newDefFiles . has ( relPath ) ) continue ;
700+
701+ const fileNodeRow = db
702+ . prepare ( `SELECT id FROM nodes WHERE kind = 'file' AND file = ?` )
703+ . get ( relPath ) as { id : number } | undefined ;
704+ if ( ! fileNodeRow ) continue ;
705+
706+ const typeMap = symbols . typeMap instanceof Map ? symbols . typeMap : new Map ( ) ;
707+
708+ for ( const call of symbols . calls ?? [ ] ) {
709+ if ( ! call . receiver ) continue ; // prototype resolution only applies to receiver calls
710+
711+ const caller = findCaller ( lookup , call , symbols . definitions ?? [ ] , relPath , fileNodeRow ) ;
712+
713+ const targets = resolveByMethodOrGlobal (
714+ lookup ,
715+ call ,
716+ relPath ,
717+ typeMap as Map < string , unknown > ,
718+ caller . callerName ,
719+ ) ;
720+
721+ for ( const t of targets ) {
722+ // Only emit edges to newly-inserted prototype nodes to avoid
723+ // duplicating edges the Rust engine already built.
724+ if ( ! newNodeIds . has ( t . id ) ) continue ;
725+ const key = `${ caller . id } |${ t . id } ` ;
726+ if ( seenByPair . has ( key ) ) continue ;
727+ seenByPair . add ( key ) ;
728+ const conf = computeConfidence ( relPath , t . file , null ) ;
729+ if ( conf <= 0 ) continue ;
730+ newEdgeRows . push ( [ caller . id , t . id , 'calls' , conf , 0 , 'receiver-typed' ] ) ;
731+ }
732+ }
733+ }
734+
735+ if ( newEdgeRows . length > 0 ) {
736+ db . transaction ( ( ) => batchInsertEdges ( db , newEdgeRows ) ) ( ) ;
737+ }
738+ }
739+
561740/** Format timing result from native orchestrator phases + JS post-processing. */
562741function formatNativeTimingResult (
563742 p : Record < string , number > ,
@@ -1195,6 +1374,15 @@ export async function tryNativeOrchestrator(
11951374 }
11961375 }
11971376
1377+ // Prototype method post-pass: the Rust engine does not recognise pre-ES6
1378+ // `Foo.prototype.bar = function(){}` patterns. Re-parse JS/TS files via
1379+ // WASM to insert missing method nodes and their call edges.
1380+ try {
1381+ await runPostNativePrototypeMethods ( ctx . db as unknown as BetterSqlite3Database , ctx . rootDir ) ;
1382+ } catch ( err ) {
1383+ debug ( `Prototype methods post-pass failed: ${ toErrorMessage ( err ) } ` ) ;
1384+ }
1385+
11981386 // Backfill the `technique` column on `calls` edges written by the Rust
11991387 // orchestrator, which does not write the column. Runs after all edge-writing
12001388 // phases (including the WASM dropped-language backfill and CHA post-pass) so
0 commit comments