@@ -709,6 +709,94 @@ function buildFnRefBindingsPtsPostPass(
709709 }
710710}
711711
712+ /**
713+ * Object.defineProperty accessor post-pass for the native call-edge path.
714+ *
715+ * When a function is registered as a getter/setter via
716+ * `Object.defineProperty(obj, "bar", { get: getter })`, calls to `this.X()`
717+ * inside `getter` need to resolve against `obj` (because `this === obj` when
718+ * the accessor is invoked). The native Rust engine has no knowledge of
719+ * `definePropertyReceivers`, so this JS post-pass adds the missing edges.
720+ */
721+ function buildDefinePropertyPostPass (
722+ ctx : PipelineContext ,
723+ getNodeIdStmt : NodeIdStmt ,
724+ allEdgeRows : EdgeRowTuple [ ] ,
725+ sharedLookup ?: CallNodeLookup ,
726+ ) : void {
727+ const filesWithReceivers = [ ...ctx . fileSymbols ] . filter (
728+ ( [ , symbols ] ) => symbols . definePropertyReceivers && symbols . definePropertyReceivers . size > 0 ,
729+ ) ;
730+ if ( filesWithReceivers . length === 0 ) return ;
731+
732+ const seenByPair = new Set < string > ( ) ;
733+ for ( const [ srcId , tgtId ] of allEdgeRows ) {
734+ seenByPair . add ( `${ srcId } |${ tgtId } ` ) ;
735+ }
736+
737+ const { barrelOnlyFiles, rootDir } = ctx ;
738+ const lookup = sharedLookup ?? makeContextLookup ( ctx , getNodeIdStmt ) ;
739+
740+ for ( const [ relPath , symbols ] of filesWithReceivers ) {
741+ if ( barrelOnlyFiles . has ( relPath ) ) continue ;
742+ const fileNodeRow = getNodeIdStmt . get ( relPath , 'file' , relPath , 0 ) ;
743+ if ( ! fileNodeRow ) continue ;
744+
745+ const importedNames = buildImportedNamesMap ( ctx , relPath , symbols , rootDir ) ;
746+ const typeMap : Map < string , TypeMapEntry | string > = symbols . typeMap || new Map ( ) ;
747+ const definePropertyReceivers = symbols . definePropertyReceivers ! ;
748+
749+ for ( const call of symbols . calls ) {
750+ if ( call . receiver !== 'this' ) continue ;
751+
752+ const caller = findCaller ( lookup , call , symbols . definitions , relPath , fileNodeRow ) ;
753+ if ( ! caller . callerName ) continue ;
754+
755+ const receiverVarName = definePropertyReceivers . get ( caller . callerName ) ;
756+ if ( ! receiverVarName ) continue ;
757+
758+ // Only add edges the native engine missed (no direct target already).
759+ const { targets : directTargets } = resolveCallTargets (
760+ lookup ,
761+ call ,
762+ relPath ,
763+ importedNames ,
764+ typeMap as Map < string , unknown > ,
765+ caller . callerName ,
766+ ) ;
767+ if ( directTargets . length > 0 ) continue ;
768+
769+ // Resolve via receiver type
770+ let targets : ReadonlyArray < { id : number ; file : string } > = [ ] ;
771+ const typeEntry = typeMap . get ( receiverVarName ) ;
772+ const typeName = typeEntry
773+ ? typeof typeEntry === 'string'
774+ ? typeEntry
775+ : ( typeEntry as { type ?: string } ) . type
776+ : null ;
777+ if ( typeName ) {
778+ const qualifiedName = `${ typeName } .${ call . name } ` ;
779+ targets = lookup . byNameAndFile ( qualifiedName , relPath ) ;
780+ }
781+ // Same-file fallback for plain object-literal methods
782+ if ( targets . length === 0 ) {
783+ targets = lookup . byNameAndFile ( call . name , relPath ) ;
784+ }
785+
786+ for ( const t of targets ) {
787+ const edgeKey = `${ caller . id } |${ t . id } ` ;
788+ if ( t . id !== caller . id && ! seenByPair . has ( edgeKey ) ) {
789+ const conf = computeConfidence ( relPath , t . file , null ) ;
790+ if ( conf > 0 ) {
791+ seenByPair . add ( edgeKey ) ;
792+ allEdgeRows . push ( [ caller . id , t . id , 'calls' , conf , 0 , 'ts-native' ] ) ;
793+ }
794+ }
795+ }
796+ }
797+ }
798+ }
799+
712800/**
713801 * Phase 8.5: CHA + RTA post-pass for the native call-edge path.
714802 *
@@ -1036,6 +1124,50 @@ function buildFileCallEdges(
10361124 }
10371125 }
10381126
1127+ // Object.defineProperty accessor fallback: when a function is registered as
1128+ // a getter/setter via `Object.defineProperty(obj, "bar", { get: getter })`,
1129+ // calls to `this.X()` inside `getter` resolve against `obj` (this === obj
1130+ // when the accessor is invoked). If the same-class fallback above found
1131+ // nothing, try treating `obj` as the receiver and look up `obj.X` in the
1132+ // typeMap, or fall back to a same-file lookup of any definition named X
1133+ // that belongs to the object literal or its type.
1134+ if (
1135+ targets . length === 0 &&
1136+ call . receiver === 'this' &&
1137+ caller . callerName != null &&
1138+ symbols . definePropertyReceivers
1139+ ) {
1140+ const receiverVarName = symbols . definePropertyReceivers . get ( caller . callerName ) ;
1141+ if ( receiverVarName ) {
1142+ // Try typeMap lookup for receiver.methodName
1143+ const typeEntry = typeMap . get ( receiverVarName ) ;
1144+ const typeName = typeEntry
1145+ ? typeof typeEntry === 'string'
1146+ ? typeEntry
1147+ : ( typeEntry as { type ?: string } ) . type
1148+ : null ;
1149+ if ( typeName ) {
1150+ const qualifiedName = `${ typeName } .${ call . name } ` ;
1151+ const qualified = lookup . byNameAndFile ( qualifiedName , relPath ) ;
1152+ if ( qualified . length > 0 ) {
1153+ targets = [ ...qualified ] ;
1154+ }
1155+ }
1156+ // If still no targets, search for any definition named `call.name` in
1157+ // the same file — handles plain object literals where the method isn't
1158+ // qualified (e.g. `const obj = { baz() {} }` defines `baz` directly).
1159+ // Note: this is intentionally broad — it matches any same-file definition
1160+ // with the called name, not just members of the receiver object. This is
1161+ // the same behaviour used by the native post-pass path (buildDefinePropertyPostPass).
1162+ if ( targets . length === 0 ) {
1163+ const sameFile = lookup . byNameAndFile ( call . name , relPath ) ;
1164+ if ( sameFile . length > 0 ) {
1165+ targets = [ ...sameFile ] ;
1166+ }
1167+ }
1168+ }
1169+ }
1170+
10391171 for ( const t of targets ) {
10401172 const edgeKey = `${ caller . id } |${ t . id } ` ;
10411173 if ( t . id !== caller . id ) {
@@ -1547,6 +1679,9 @@ export async function buildEdges(ctx: PipelineContext): Promise<void> {
15471679 // (e.g. `const f = fn.bind(ctx)`), so calls to bind-created aliases are
15481680 // not resolved to their original function on the native path.
15491681 buildFnRefBindingsPtsPostPass ( ctx , getNodeIdStmt , allEdgeRows , sharedLookup ) ;
1682+ // Object.defineProperty accessor post-pass: resolve this-dispatch inside
1683+ // getter/setter functions registered via Object.defineProperty.
1684+ buildDefinePropertyPostPass ( ctx , getNodeIdStmt , allEdgeRows , sharedLookup ) ;
15501685 // Phase 8.5 post-pass: augment native call edges with CHA-resolved dispatch.
15511686 // The native Rust engine has no knowledge of the CHA context, so this/self
15521687 // calls and interface dispatch are not expanded to concrete implementations.
0 commit comments