@@ -17,6 +17,7 @@ import type {
1717 ClassRelation ,
1818 Definition ,
1919 ExtractorOutput ,
20+ FnRefBinding ,
2021 Import ,
2122 NativeAddon ,
2223 NodeRow ,
@@ -709,6 +710,78 @@ function buildFnRefBindingsPtsPostPass(
709710 }
710711}
711712
713+ /**
714+ * this-rebinding post-pass for the native call-edge path.
715+ *
716+ * When `fn.call(namedCtx, ...)` or `fn.apply(namedCtx, ...)` is extracted by the
717+ * WASM layer, `thisCallBindings` records `{ callee: 'fn', thisArg: 'namedCtx' }`.
718+ * The native Rust engine has no knowledge of these bindings, so `this()` calls
719+ * inside `fn` remain unresolved. This JS post-pass adds the missing edges by
720+ * resolving `this()` calls inside each `fn` that has a thisCallBinding.
721+ */
722+ function buildThisCallBindingsPtsPostPass (
723+ ctx : PipelineContext ,
724+ getNodeIdStmt : NodeIdStmt ,
725+ allEdgeRows : EdgeRowTuple [ ] ,
726+ sharedLookup ?: CallNodeLookup ,
727+ ) : void {
728+ const filesWithBindings = [ ...ctx . fileSymbols ] . filter (
729+ ( [ , symbols ] ) => symbols . thisCallBindings && symbols . thisCallBindings . length > 0 ,
730+ ) ;
731+ if ( filesWithBindings . length === 0 ) return ;
732+
733+ const seenByPair = new Set < string > ( ) ;
734+ for ( const [ srcId , tgtId ] of allEdgeRows ) {
735+ seenByPair . add ( `${ srcId } |${ tgtId } ` ) ;
736+ }
737+
738+ const { barrelOnlyFiles, rootDir } = ctx ;
739+ const lookup = sharedLookup ?? makeContextLookup ( ctx , getNodeIdStmt ) ;
740+
741+ for ( const [ relPath , symbols ] of filesWithBindings ) {
742+ if ( barrelOnlyFiles . has ( relPath ) ) continue ;
743+ const fileNodeRow = getNodeIdStmt . get ( relPath , 'file' , relPath , 0 ) ;
744+ if ( ! fileNodeRow ) continue ;
745+
746+ const importedNames = buildImportedNamesMap ( ctx , relPath , symbols , rootDir ) ;
747+ const typeMap : Map < string , TypeMapEntry | string > = symbols . typeMap || new Map ( ) ;
748+ const ptsMap = buildPointsToMapForFile ( symbols , importedNames ) ;
749+ if ( ! ptsMap ) continue ;
750+
751+ // Only process calls named 'this' (callee-not-receiver usage)
752+ for ( const call of symbols . calls ) {
753+ if ( call . name !== 'this' || call . receiver ) continue ;
754+
755+ const caller = findCaller ( lookup , call , symbols . definitions , relPath , fileNodeRow ) ;
756+ if ( caller . callerName == null ) continue ;
757+
758+ const scopedKey = `${ caller . callerName } ::this` ;
759+ if ( ! ptsMap . has ( scopedKey ) ) continue ;
760+
761+ for ( const alias of resolveViaPointsTo ( scopedKey , ptsMap ) ) {
762+ const { targets : aliasTargets , importedFrom : aliasFrom } = resolveCallTargets (
763+ lookup ,
764+ { name : alias } ,
765+ relPath ,
766+ importedNames ,
767+ typeMap as Map < string , unknown > ,
768+ ) ;
769+ for ( const t of aliasTargets ) {
770+ const edgeKey = `${ caller . id } |${ t . id } ` ;
771+ if ( t . id !== caller . id && ! seenByPair . has ( edgeKey ) ) {
772+ const conf =
773+ computeConfidence ( relPath , t . file , aliasFrom ?? null ) - PROPAGATION_HOP_PENALTY ;
774+ if ( conf > 0 ) {
775+ seenByPair . add ( edgeKey ) ;
776+ allEdgeRows . push ( [ caller . id , t . id , 'calls' , conf , 0 , 'points-to' ] ) ;
777+ }
778+ }
779+ }
780+ }
781+ }
782+ }
783+ }
784+
712785/**
713786 * Phase 8.3f post-pass for the native call-edge path.
714787 *
@@ -1144,6 +1217,7 @@ function buildPointsToMapForFile(
11441217 symbols : ExtractorOutput ,
11451218 importedNames : Map < string , string > ,
11461219) : PointsToMap | null {
1220+ const hasThisCallBindings = ! ! symbols . thisCallBindings ?. length ;
11471221 if (
11481222 ! symbols . fnRefBindings ?. length &&
11491223 ! symbols . paramBindings ?. length &&
@@ -1152,7 +1226,8 @@ function buildPointsToMapForFile(
11521226 ! symbols . forOfBindings ?. length &&
11531227 ! symbols . arrayCallbackBindings ?. length &&
11541228 ! symbols . objectRestParamBindings ?. length &&
1155- ! symbols . objectPropBindings ?. length
1229+ ! symbols . objectPropBindings ?. length &&
1230+ ! hasThisCallBindings
11561231 )
11571232 return null ;
11581233 const defNames = new Set (
@@ -1161,8 +1236,21 @@ function buildPointsToMapForFile(
11611236 . map ( ( d ) => d . name ) ,
11621237 ) ;
11631238 const definitionParams = buildDefinitionParamsMap ( symbols . definitions ) ;
1239+
1240+ // Convert thisCallBindings into scoped fnRefBindings: `fn::this → namedCtx`.
1241+ // The scoped key `fn::this` is looked up when `this()` calls are resolved inside
1242+ // function `fn` — caller.callerName='fn', call.name='this' → scopedPtsKey='fn::this'.
1243+ let allFnRefBindings : readonly FnRefBinding [ ] = symbols . fnRefBindings ?? [ ] ;
1244+ if ( hasThisCallBindings ) {
1245+ const extra : FnRefBinding [ ] = ( symbols . thisCallBindings ?? [ ] ) . map ( ( b ) => ( {
1246+ lhs : `${ b . callee } ::this` ,
1247+ rhs : b . thisArg ,
1248+ } ) ) ;
1249+ allFnRefBindings = [ ...allFnRefBindings , ...extra ] ;
1250+ }
1251+
11641252 return buildPointsToMap (
1165- symbols . fnRefBindings ?? [ ] ,
1253+ allFnRefBindings ,
11661254 defNames ,
11671255 importedNames ,
11681256 symbols . paramBindings ,
@@ -1835,6 +1923,9 @@ export async function buildEdges(ctx: PipelineContext): Promise<void> {
18351923 // (e.g. `const f = fn.bind(ctx)`), so calls to bind-created aliases are
18361924 // not resolved to their original function on the native path.
18371925 buildFnRefBindingsPtsPostPass ( ctx , getNodeIdStmt , allEdgeRows , sharedLookup ) ;
1926+ // this-rebinding post-pass: resolve `this()` calls inside functions that
1927+ // were invoked via `.call(namedCtx, ...)` / `.apply(namedCtx, ...)`.
1928+ buildThisCallBindingsPtsPostPass ( ctx , getNodeIdStmt , allEdgeRows , sharedLookup ) ;
18381929 // Phase 8.3f post-pass: augment native call edges with object rest-param
18391930 // receiver resolution — typeMap[restName] → argName → typeMap[argName.method].
18401931 buildObjectRestParamPostPass ( ctx , getNodeIdStmt , allEdgeRows , sharedLookup ) ;
0 commit comments