@@ -669,6 +669,51 @@ async function runPostNativePrototypeMethods(
669669
670670 db . transaction ( ( ) => batchInsertNodes ( db , newNodeRows ) ) ( ) ;
671671
672+ // ── Caller-only second pass (#1371) ──────────────────────────────────────
673+ // `wasmResults` only covers `protoFiles` (definition files). A file that
674+ // only *calls* a newly-inserted method (e.g. `f.method()`) was excluded from
675+ // the pre-filter, so its call edges to the new nodes are silently dropped.
676+ // After node insertion we know the method name suffixes; text-search the
677+ // remaining JS/TS files and WASM-parse any that contain a matching call.
678+ const newMethodSuffixes = new Set (
679+ newDefs . map ( ( d ) => {
680+ const dotIdx = d . name . indexOf ( '.' ) ;
681+ return dotIdx !== - 1 ? d . name . slice ( dotIdx + 1 ) : d . name ;
682+ } ) ,
683+ ) ;
684+
685+ let mergedWasmResults = wasmResults ;
686+ if ( newMethodSuffixes . size > 0 ) {
687+ // Pre-compile patterns once — avoids re-compiling up to newMethodSuffixes.size
688+ // regexes on every file in the scan loop.
689+ const suffixPatterns = [ ...newMethodSuffixes ] . map ( ( m ) => {
690+ const escaped = m . replace ( / [ . * + ? ^ $ { } ( ) | [ \] \\ ] / g, '\\$&' ) ;
691+ return new RegExp ( `\\.${ escaped } \\s*\\(` ) ;
692+ } ) ;
693+ const protoFileSet = new Set ( protoFiles ) ;
694+ const callerCandidateAbs : string [ ] = [ ] ;
695+ for ( const relPath of jsFiles ) {
696+ if ( protoFileSet . has ( relPath ) ) continue ; // already parsed in first pass
697+ try {
698+ const content = readFileSafe ( path . join ( rootDir , relPath ) ) ;
699+ const matchesAny = suffixPatterns . some ( ( re ) => re . test ( content ) ) ;
700+ if ( matchesAny ) callerCandidateAbs . push ( path . join ( rootDir , relPath ) ) ;
701+ } catch {
702+ /* skip unreadable files */
703+ }
704+ }
705+ if ( callerCandidateAbs . length > 0 ) {
706+ try {
707+ const callerWasmResults = await parseFilesWasmForBackfill ( callerCandidateAbs , rootDir ) ;
708+ if ( callerWasmResults . size > 0 ) {
709+ mergedWasmResults = new Map ( [ ...wasmResults , ...callerWasmResults ] ) ;
710+ }
711+ } catch ( e ) {
712+ debug ( `runPostNativePrototypeMethods: caller-only WASM parse failed: ${ toErrorMessage ( e ) } ` ) ;
713+ }
714+ }
715+ }
716+
672717 // Build a name → node lookup from all DB nodes (including newly inserted ones).
673718 type NodeEntry = { id : number ; file : string ; kind : string } ;
674719 const byNameMap = new Map < string , NodeEntry [ ] > ( ) ;
@@ -716,14 +761,13 @@ async function runPostNativePrototypeMethods(
716761 // zero benefit and could OOM on large repositories.
717762 const seenByPair = new Set < string > ( ) ;
718763
719- // Resolve call edges in every file — not just those that define new func-prop
720- // methods. A caller in app.js calling a method defined in lib.js
721- // would be silently missed if we only scanned definition files.
722- // The newNodeIds guard inside the loop already prevents duplicate edges.
764+ // Resolve call edges across all parsed files (definition files + caller-only
765+ // files discovered in the second WASM pass above). The newNodeIds guard inside
766+ // the loop prevents emitting duplicate edges for nodes the Rust engine already built.
723767 const newEdgeRows : unknown [ ] [ ] = [ ] ;
724768 const fileNodeStmt = db . prepare ( `SELECT id FROM nodes WHERE kind = 'file' AND file = ?` ) ;
725769
726- for ( const [ relPath , symbols ] of wasmResults ) {
770+ for ( const [ relPath , symbols ] of mergedWasmResults ) {
727771 const fileNodeRow = fileNodeStmt . get ( relPath ) as { id : number } | undefined ;
728772 if ( ! fileNodeRow ) continue ;
729773
@@ -734,14 +778,32 @@ async function runPostNativePrototypeMethods(
734778
735779 const caller = findCaller ( lookup , call , symbols . definitions ?? [ ] , relPath , fileNodeRow ) ;
736780
737- const targets = resolveByMethodOrGlobal (
781+ let targets = resolveByMethodOrGlobal (
738782 lookup ,
739783 call ,
740784 relPath ,
741785 typeMap as Map < string , unknown > ,
742786 caller . callerName ,
743787 ) ;
744788
789+ // Direct receiver.method fallback: caller-only files often lack typeMap entries
790+ // for the receiver (e.g. `f.process()` where `f` isn't declared in the file).
791+ // Try qualified-name lookup scoped to newly-inserted nodes to avoid false positives.
792+ // Note: `call.receiver` is always truthy here — the `if (!call.receiver) continue`
793+ // guard above ensures we never reach this point with a falsy receiver.
794+ if (
795+ targets . length === 0 &&
796+ call . receiver !== 'this' &&
797+ call . receiver !== 'super' &&
798+ call . receiver !== 'self'
799+ ) {
800+ const qualifiedName = `${ call . receiver } .${ call . name } ` ;
801+ const direct = lookup
802+ . byName ( qualifiedName )
803+ . filter ( ( n ) => n . kind === 'method' && newNodeIds . has ( n . id ) ) ;
804+ if ( direct . length > 0 ) targets = direct ;
805+ }
806+
745807 for ( const t of targets ) {
746808 // Only emit edges to newly-inserted func-prop nodes to avoid
747809 // duplicating edges the Rust engine already built.
0 commit comments