@@ -649,7 +649,7 @@ fn process_file<'a>(
649649 }
650650 }
651651
652- emit_receiver_edge ( ctx, call, caller_id, rel_path, & type_map, & mut seen_edges, edges) ;
652+ emit_receiver_edge ( ctx, call, caller_id, rel_path, & type_map, & imported_names , & mut seen_edges, edges) ;
653653 }
654654
655655 emit_hierarchy_edges ( ctx, file_input, rel_path, edges) ;
@@ -1025,6 +1025,7 @@ fn emit_call_edges(
10251025fn emit_receiver_edge (
10261026 ctx : & EdgeContext , call : & CallInfo , caller_id : u32 , rel_path : & str ,
10271027 type_map : & HashMap < & str , ( & str , f64 ) > ,
1028+ imported_names : & HashMap < & str , & str > ,
10281029 seen_edges : & mut HashSet < u64 > , edges : & mut Vec < ComputedEdge > ,
10291030) {
10301031 let Some ( ref receiver) = call. receiver else { return } ;
@@ -1035,18 +1036,21 @@ fn emit_receiver_edge(
10351036 let type_entry = type_map. get ( receiver. as_str ( ) ) ;
10361037 let effective_receiver = type_entry. map ( |& ( t, _) | t) . unwrap_or ( receiver. as_str ( ) ) ;
10371038
1038- // Filter-before: apply receiver_kinds to same-file candidates first, then
1039- // fall back to global candidates (also filtered) only when same-file yields
1040- // nothing. This prevents an imported name emitted as kind='function' in the
1041- // importing file from blocking the fallback to the actual class/struct/etc.
1042- // node in the defining file .
1043- let samefile_candidates : Vec < & NodeInfo > = ctx. nodes_by_name_and_file
1039+ // Block global fallback only when the same-file node is a local definition,
1040+ // not when it's an import artifact (e.g. `const { C } = require(…)` seeds a
1041+ // kind="function" node in the importer but the real class lives elsewhere).
1042+ // A locally-defined `function C(){}` owns the name — no cross-file class
1043+ // should shadow it (issue #1539). Mirror of JS resolveReceiverEdge logic .
1044+ let samefile_all : Vec < & NodeInfo > = ctx. nodes_by_name_and_file
10441045 . get ( & ( effective_receiver, rel_path) )
1045- . cloned ( ) . unwrap_or_default ( )
1046- . into_iter ( )
1046+ . cloned ( ) . unwrap_or_default ( ) ;
1047+ let is_local_definition = !samefile_all. is_empty ( )
1048+ && !imported_names. contains_key ( effective_receiver) ;
1049+ let samefile_candidates: Vec < & NodeInfo > = samefile_all. iter ( )
1050+ . copied ( )
10471051 . filter ( |n| ctx. receiver_kinds . contains ( n. kind . as_str ( ) ) )
10481052 . collect ( ) ;
1049- let receiver_nodes: Vec < & NodeInfo > = if !samefile_candidates . is_empty ( ) {
1053+ let receiver_nodes: Vec < & NodeInfo > = if is_local_definition {
10501054 samefile_candidates
10511055 } else {
10521056 ctx. nodes_by_name . get ( effective_receiver) . cloned ( ) . unwrap_or_default ( )
@@ -1816,11 +1820,13 @@ mod call_edge_tests {
18161820 }
18171821
18181822 /// Regression: when the same file has a `kind="function"` node for the
1819- /// effective receiver (e.g. `Calculator` imported via destructuring), the
1820- /// same-file "function" node must NOT block the fallback to the global
1821- /// class node in another file. Filter-before semantics required.
1823+ /// effective receiver created by a destructured import (e.g.
1824+ /// `const { Calculator } = require('./utils')`), that import artifact must
1825+ /// NOT block the fallback to the global class node in another file.
1826+ /// The import must be listed in `imported_names` so the resolver knows it
1827+ /// is an import artifact, not a local function-constructor definition.
18221828 #[ test]
1823- fn receiver_edge_filter_before_skips_same_file_function_node ( ) {
1829+ fn receiver_edge_imported_function_node_falls_through_to_global_class ( ) {
18241830 let all_nodes = vec ! [
18251831 node( 1 , "main" , "function" , "index.js" , 3 ) ,
18261832 // Destructured import `const { Calculator } = require('./utils')` → kind "function" in index.js
@@ -1829,25 +1835,60 @@ mod call_edge_tests {
18291835 node( 3 , "compute" , "method" , "utils.js" , 3 ) ,
18301836 ] ;
18311837
1832- let files = vec ! [ make_file(
1838+ let mut file = make_file (
18331839 "index.js" ,
18341840 10 ,
18351841 vec ! [ def( "main" , "function" , 3 , 8 ) ] ,
18361842 vec ! [ call( "compute" , 7 , Some ( "calc" ) ) ] ,
18371843 vec ! [ type_map_entry( "calc" , "Calculator" , 1.0 ) ] ,
18381844 vec ! [ ] ,
1839- ) ] ;
1845+ ) ;
1846+ // Mark `Calculator` as an imported name so the resolver treats the
1847+ // same-file kind="function" node as an import artifact and falls through.
1848+ file. imported_names = vec ! [ ImportedName { name: "Calculator" . to_string( ) , file: "utils.js" . to_string( ) } ] ;
18401849
1841- let edges = build_call_edges ( files , all_nodes, vec ! [ ] ) ;
1850+ let edges = build_call_edges ( vec ! [ file ] , all_nodes, vec ! [ ] ) ;
18421851
18431852 let receiver_edge = edges. iter ( ) . find ( |e| e. kind == "receiver" ) ;
18441853 assert ! (
18451854 receiver_edge. is_some( ) ,
1846- "same-file 'function' node must not block fallback to global class; got: {:?}" ,
1855+ "imported 'function' node must not block fallback to global class; got: {:?}" ,
18471856 edges. iter( ) . map( |e| ( & e. kind, e. source_id, e. target_id) ) . collect:: <Vec <_>>( )
18481857 ) ;
18491858 let re = receiver_edge. unwrap ( ) ;
1850- assert_eq ! ( re. target_id, 2 , "receiver edge must point to Calculator class (id=2), not function (id=4)" ) ;
1859+ assert_eq ! ( re. target_id, 2 , "receiver edge must point to Calculator class (id=2), not import artifact (id=4)" ) ;
1860+ }
1861+
1862+ /// Issue #1539: `function C(){}` (function constructor) in the same file as
1863+ /// `var v = new C(); v.foo()` must block the global fallback to any cross-file
1864+ /// class `C`. A locally-defined function constructor owns the name in its
1865+ /// file — no cross-file class should win over it.
1866+ #[ test]
1867+ fn receiver_edge_local_function_ctor_blocks_global_class ( ) {
1868+ let all_nodes = vec ! [
1869+ node( 1 , "C" , "function" , "prototypes.js" , 1 ) , // local function constructor
1870+ node( 2 , "C.foo" , "method" , "prototypes.js" , 3 ) ,
1871+ node( 3 , "C" , "class" , "classes.js" , 1 ) , // cross-file class with same name
1872+ ] ;
1873+
1874+ // No imported_names — `C` is locally defined.
1875+ let files = vec ! [ make_file(
1876+ "prototypes.js" ,
1877+ 10 ,
1878+ vec![ def( "C" , "function" , 1 , 2 ) ] ,
1879+ vec![ call( "foo" , 8 , Some ( "v" ) ) ] ,
1880+ vec![ type_map_entry( "v" , "C" , 1.0 ) ] ,
1881+ vec![ ] ,
1882+ ) ] ;
1883+
1884+ let edges = build_call_edges ( files, all_nodes, vec ! [ ] ) ;
1885+
1886+ let receiver_edge = edges. iter ( ) . find ( |e| e. kind == "receiver" ) ;
1887+ assert ! (
1888+ receiver_edge. is_none( ) ,
1889+ "local function constructor must block global class fallback — no receiver edge expected; got: {:?}" ,
1890+ edges. iter( ) . map( |e| ( & e. kind, e. source_id, e. target_id) ) . collect:: <Vec <_>>( )
1891+ ) ;
18511892 }
18521893
18531894 /// Issue #1453: `this.logger.error()` inside `UserService.create` where the
0 commit comments