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