@@ -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 *
@@ -1020,6 +1108,50 @@ function buildFileCallEdges(
10201108 }
10211109 }
10221110
1111+ // Object.defineProperty accessor fallback: when a function is registered as
1112+ // a getter/setter via `Object.defineProperty(obj, "bar", { get: getter })`,
1113+ // calls to `this.X()` inside `getter` resolve against `obj` (this === obj
1114+ // when the accessor is invoked). If the same-class fallback above found
1115+ // nothing, try treating `obj` as the receiver and look up `obj.X` in the
1116+ // typeMap, or fall back to a same-file lookup of any definition named X
1117+ // that belongs to the object literal or its type.
1118+ if (
1119+ targets . length === 0 &&
1120+ call . receiver === 'this' &&
1121+ caller . callerName != null &&
1122+ symbols . definePropertyReceivers
1123+ ) {
1124+ const receiverVarName = symbols . definePropertyReceivers . get ( caller . callerName ) ;
1125+ if ( receiverVarName ) {
1126+ // Try typeMap lookup for receiver.methodName
1127+ const typeEntry = typeMap . get ( receiverVarName ) ;
1128+ const typeName = typeEntry
1129+ ? typeof typeEntry === 'string'
1130+ ? typeEntry
1131+ : ( typeEntry as { type ?: string } ) . type
1132+ : null ;
1133+ if ( typeName ) {
1134+ const qualifiedName = `${ typeName } .${ call . name } ` ;
1135+ const qualified = lookup . byNameAndFile ( qualifiedName , relPath ) ;
1136+ if ( qualified . length > 0 ) {
1137+ targets = [ ...qualified ] ;
1138+ }
1139+ }
1140+ // If still no targets, search for any definition named `call.name` in
1141+ // the same file — handles plain object literals where the method isn't
1142+ // qualified (e.g. `const obj = { baz() {} }` defines `baz` directly).
1143+ // Note: this is intentionally broad — it matches any same-file definition
1144+ // with the called name, not just members of the receiver object. This is
1145+ // the same behaviour used by the native post-pass path (buildDefinePropertyPostPass).
1146+ if ( targets . length === 0 ) {
1147+ const sameFile = lookup . byNameAndFile ( call . name , relPath ) ;
1148+ if ( sameFile . length > 0 ) {
1149+ targets = [ ...sameFile ] ;
1150+ }
1151+ }
1152+ }
1153+ }
1154+
10231155 for ( const t of targets ) {
10241156 const edgeKey = `${ caller . id } |${ t . id } ` ;
10251157 if ( t . id !== caller . id ) {
@@ -1482,6 +1614,9 @@ export async function buildEdges(ctx: PipelineContext): Promise<void> {
14821614 // (e.g. `const f = fn.bind(ctx)`), so calls to bind-created aliases are
14831615 // not resolved to their original function on the native path.
14841616 buildFnRefBindingsPtsPostPass ( ctx , getNodeIdStmt , allEdgeRows , sharedLookup ) ;
1617+ // Object.defineProperty accessor post-pass: resolve this-dispatch inside
1618+ // getter/setter functions registered via Object.defineProperty.
1619+ buildDefinePropertyPostPass ( ctx , getNodeIdStmt , allEdgeRows , sharedLookup ) ;
14851620 // Phase 8.5 post-pass: augment native call edges with CHA-resolved dispatch.
14861621 // The native Rust engine has no knowledge of the CHA context, so this/self
14871622 // calls and interface dispatch are not expanded to concrete implementations.
0 commit comments