@@ -206,9 +206,11 @@ impl<'a> EdgeContext<'a> {
206206/// (a cross-file consumer added in an untouched file won't be seen until the
207207/// next full rebuild) — the same scoping trade-off already accepted
208208/// elsewhere in this codebase's incremental classification
209- /// (`has_active_file_siblings`, exported-via-reexport, and median fan-in/out
210- /// all recompute from the affected file set only, not the whole graph, in
211- /// `graph/classifiers/roles.rs`'s incremental path). Mirrors
209+ /// (`has_active_file_siblings` and exported-via-reexport both recompute from
210+ /// the affected file set only, not the whole graph, in
211+ /// `graph/classifiers/roles.rs`'s incremental path — median fan-in/out is a
212+ /// separate case, deliberately kept as a whole-graph statistic even on the
213+ /// incremental path, for classification-threshold consistency). Mirrors
212214/// `collectInvokedPropertyNames` in `src/domain/graph/builder/call-resolver.ts`.
213215fn collect_invoked_property_names ( files : & [ FileEdgeInput ] ) -> HashSet < & str > {
214216 let mut names = HashSet :: new ( ) ;
@@ -831,13 +833,14 @@ fn process_file<'a>(
831833 // of these positions is as likely to be a plain data reference
832834 // (`{ name: SOME_CONSTANT }`) as a real function/class, so drop any
833835 // other-kind match rather than fabricating a "calls" edge to a
834- // constant. `class` is included alongside function/method because
835- // `instanceof`'s right operand is always a class/constructor
836- // (#1784) — unlike the original #1771 object-literal case, which is
837- // function/method only. Applied once here (after all
838- // resolve_call_targets tiers), mirroring the
839- // `dynamicKind === 'value-ref'` filter in resolveFallbackTargets
840- // (stages/build-edges.ts).
836+ // constant. `class` was added because `instanceof`'s right operand
837+ // is always a class/constructor (#1784). The filter is keyed on
838+ // `dynamic_kind`, not on which site produced the call, so the #1771
839+ // object-literal and #1776 Lua sites also gain class-kind
840+ // resolution as a side effect — not because either idiom commonly
841+ // names a class. Applied once here (after all resolve_call_targets
842+ // tiers), mirroring the `dynamicKind === 'value-ref'` filter in
843+ // resolveFallbackTargets (stages/build-edges.ts).
841844 if call. dynamic_kind . as_deref ( ) == Some ( "value-ref" ) {
842845 targets. retain ( |t| t. kind == "function" || t. kind == "method" || t. kind == "class" ) ;
843846 // #1895: object-literal-property value-refs (key_expr set — see
@@ -888,7 +891,7 @@ fn process_file<'a>(
888891 }
889892 }
890893
891- emit_hierarchy_edges ( ctx, file_input, fc. rel_path , & fc. imported_names , edges) ;
894+ emit_hierarchy_edges ( ctx, file_input, fc. rel_path , & fc. imported_names , & fc . imported_original_names , edges) ;
892895}
893896
894897/// Callable definition kinds — only function/method bodies act as enclosing
@@ -1622,7 +1625,11 @@ fn emit_receiver_edge(
16221625/// the graph regardless of file or language, producing false cross-file
16231626/// (even cross-language) hierarchy edges for common type names. Priority:
16241627/// 1. Same-file declaration, when `name` is not itself an import artifact.
1625- /// 2. The file's actually-resolved import for `name` (barrel-traced).
1628+ /// 2. The file's actually-resolved import for `name` (barrel-traced). For a
1629+ /// renamed import (`import { Base as MyBase }`), the imported file stores
1630+ /// the symbol under its original exported name, not the local alias — so
1631+ /// `imported_original_name` resolves `MyBase` back to `Base` before the
1632+ /// lookup, mirroring `resolve_call_targets` (#1730).
16261633/// 3. Last resort: a same-language-family global-by-name match (#1783),
16271634/// first candidate only — a heritage clause names exactly one type.
16281635fn resolve_hierarchy_targets < ' a > (
@@ -1631,6 +1638,7 @@ fn resolve_hierarchy_targets<'a>(
16311638 rel_path : & str ,
16321639 imported_names : & HashMap < & str , & str > ,
16331640 target_kinds : & [ & str ] ,
1641+ imported_original_names : & HashMap < & str , & str > ,
16341642) -> Vec < & ' a NodeInfo > {
16351643 let samefile_all: Vec < & NodeInfo > = ctx. nodes_by_name_and_file
16361644 . get ( & ( name, rel_path) )
@@ -1643,8 +1651,9 @@ fn resolve_hierarchy_targets<'a>(
16431651 }
16441652
16451653 if let Some ( imported_from) = imported_names. get ( name) {
1654+ let target_name = imported_original_names. get ( name) . copied ( ) . unwrap_or ( name) ;
16461655 let imported_candidates: Vec < & NodeInfo > = ctx. nodes_by_name_and_file
1647- . get ( & ( name , * imported_from) )
1656+ . get ( & ( target_name , * imported_from) )
16481657 . cloned ( ) . unwrap_or_default ( )
16491658 . into_iter ( )
16501659 . filter ( |n| target_kinds. contains ( & n. kind . as_str ( ) ) )
@@ -1665,6 +1674,7 @@ fn resolve_hierarchy_targets<'a>(
16651674fn emit_hierarchy_edges (
16661675 ctx : & EdgeContext , file_input : & FileEdgeInput , rel_path : & str ,
16671676 imported_names : & HashMap < & str , & str > ,
1677+ imported_original_names : & HashMap < & str , & str > ,
16681678 edges : & mut Vec < ComputedEdge > ,
16691679) {
16701680 for cls in & file_input. classes {
@@ -1675,7 +1685,7 @@ fn emit_hierarchy_edges(
16751685 let Some ( source) = source_row else { continue } ;
16761686
16771687 if let Some ( ref extends_name) = cls. extends {
1678- let targets = resolve_hierarchy_targets ( ctx, extends_name, rel_path, imported_names, EXTENDS_TARGET_KINDS ) ;
1688+ let targets = resolve_hierarchy_targets ( ctx, extends_name, rel_path, imported_names, EXTENDS_TARGET_KINDS , imported_original_names ) ;
16791689 for t in targets {
16801690 edges. push ( ComputedEdge {
16811691 source_id : source. id , target_id : t. id ,
@@ -1685,7 +1695,7 @@ fn emit_hierarchy_edges(
16851695 }
16861696 }
16871697 if let Some ( ref implements_name) = cls. implements {
1688- let targets = resolve_hierarchy_targets ( ctx, implements_name, rel_path, imported_names, IMPLEMENTS_TARGET_KINDS ) ;
1698+ let targets = resolve_hierarchy_targets ( ctx, implements_name, rel_path, imported_names, IMPLEMENTS_TARGET_KINDS , imported_original_names ) ;
16891699 for t in targets {
16901700 edges. push ( ComputedEdge {
16911701 source_id : source. id , target_id : t. id ,
@@ -1697,6 +1707,7 @@ fn emit_hierarchy_edges(
16971707 }
16981708}
16991709
1710+
17001711// ── Import edges (native) ──────────────────────────────────────────────
17011712
17021713#[ napi( object) ]
@@ -1945,6 +1956,16 @@ fn is_named_reexport(imp: &ImportInfo) -> bool {
19451956 imp. reexport && !imp. wildcard_reexport
19461957}
19471958
1959+ /// True for a genuine wildcard re-export (`export * from 'Y'`). Emitted as a
1960+ /// distinct file-level marker edge (`reexports-wildcard`) alongside the
1961+ /// generic `reexports` edge so the query layer can tell a target reached
1962+ /// only by named specifiers apart from one that's also reached by a
1963+ /// wildcard — even when a *different* statement in the same file names
1964+ /// specific symbols from that exact target (#1849 review).
1965+ fn is_wildcard_reexport ( imp : & ImportInfo ) -> bool {
1966+ imp. reexport && imp. wildcard_reexport
1967+ }
1968+
19481969/// For a `type` import or a named re-export targeting a barrel or resolved
19491970/// file, emit one symbol-level edge per named symbol so the target symbols
19501971/// receive fan-in credit and aren't misclassified as dead code
@@ -2113,6 +2134,15 @@ fn process_single_import(
21132134 }
21142135 if is_named_reexport ( imp) {
21152136 emit_named_symbol_edges ( edges, file_input, imp, resolved_path, "reexports" , ctx) ;
2137+ } else if is_wildcard_reexport ( imp) {
2138+ edges. push ( ComputedEdge {
2139+ source_id : file_input. file_node_id ,
2140+ target_id : target_node_id,
2141+ kind : "reexports-wildcard" . to_string ( ) ,
2142+ confidence : 1.0 ,
2143+ dynamic : 0 ,
2144+ dynamic_kind : None ,
2145+ } ) ;
21162146 }
21172147 emit_barrel_through_edges ( edges, file_input, imp, resolved_path, edge_kind, ctx) ;
21182148}
@@ -2269,9 +2299,12 @@ mod import_edge_tests {
22692299
22702300 #[ test]
22712301 fn wildcard_reexport_emits_no_symbol_level_edge ( ) {
2272- // `export * from './utils'` carries no specific names, so only the
2273- // file-level `reexports` edge is emitted — the query layer falls
2274- // back to the target's full export list for genuine wildcards.
2302+ // `export * from './utils'` carries no specific names, so no
2303+ // symbol-level edge is emitted. It does get the dedicated
2304+ // `reexports-wildcard` file-level marker (alongside the generic
2305+ // `reexports` edge) so the query layer can always apply full-export
2306+ // semantics for genuine wildcards, even when a *different* statement
2307+ // to the same target also names specific symbols (#1849 review).
22752308 let files = vec ! [ make_file( "src/index.ts" , 1 , vec![
22762309 ImportInfo {
22772310 source: "./utils" . to_string( ) ,
@@ -2302,9 +2335,64 @@ mod import_edge_tests {
23022335 "/root" . to_string ( ) ,
23032336 Some ( symbol_nodes) ,
23042337 ) ;
2305- assert_eq ! ( edges. len( ) , 1 ) ;
2338+ assert_eq ! ( edges. len( ) , 2 ) ;
23062339 assert_eq ! ( edges[ 0 ] . kind, "reexports" ) ;
23072340 assert_eq ! ( edges[ 0 ] . target_id, 2 ) ;
2341+ assert_eq ! ( edges[ 1 ] . kind, "reexports-wildcard" ) ;
2342+ assert_eq ! ( edges[ 1 ] . target_id, 2 ) ;
2343+ }
2344+
2345+ #[ test]
2346+ fn named_and_wildcard_reexport_of_same_target_both_marked ( ) {
2347+ // `export { foo } from './utils'` AND `export * from './utils'` in
2348+ // the same file, both targeting utils.ts. The wildcard's full-export
2349+ // semantics must stay independently signalled (via the dedicated
2350+ // `reexports-wildcard` marker) rather than being suppressed by the
2351+ // named specifier's symbol-level edge — otherwise the query layer
2352+ // would report only `foo` and silently drop every other export of
2353+ // utils.ts that the wildcard was meant to surface (#1849 review).
2354+ let files = vec ! [ make_file( "src/index.ts" , 1 , vec![
2355+ make_import( "./utils" , vec![ "foo" ] , true , false , false ) ,
2356+ ImportInfo {
2357+ source: "./utils" . to_string( ) ,
2358+ names: vec![ ] ,
2359+ reexport: true ,
2360+ type_only: false ,
2361+ dynamic_import: false ,
2362+ wildcard_reexport: true ,
2363+ type_only_names: vec![ ] ,
2364+ renamed_imports: vec![ ] ,
2365+ } ,
2366+ ] , vec![ ] ) ] ;
2367+ let resolved = vec ! [ make_resolved( "/root/src/index.ts" , "./utils" , "src/utils.ts" ) ] ;
2368+ let node_ids = vec ! [ make_node_entry( "src/index.ts" , 1 ) , make_node_entry( "src/utils.ts" , 2 ) ] ;
2369+ let symbol_nodes = vec ! [ SymbolNodeEntry {
2370+ name: "foo" . to_string( ) ,
2371+ file: "src/utils.ts" . to_string( ) ,
2372+ node_id: 99 ,
2373+ kind: "function" . to_string( ) ,
2374+ } ] ;
2375+
2376+ let edges = build_import_edges (
2377+ files,
2378+ resolved,
2379+ vec ! [ ] ,
2380+ node_ids,
2381+ vec ! [ ] ,
2382+ "/root" . to_string ( ) ,
2383+ Some ( symbol_nodes) ,
2384+ ) ;
2385+ assert_eq ! ( edges. len( ) , 4 ) ;
2386+ // Named statement: file-level `reexports` + symbol-level `reexports` to foo.
2387+ assert_eq ! ( edges[ 0 ] . kind, "reexports" ) ;
2388+ assert_eq ! ( edges[ 0 ] . target_id, 2 ) ;
2389+ assert_eq ! ( edges[ 1 ] . kind, "reexports" ) ;
2390+ assert_eq ! ( edges[ 1 ] . target_id, 99 ) ;
2391+ // Wildcard statement: file-level `reexports` + the `reexports-wildcard` marker.
2392+ assert_eq ! ( edges[ 2 ] . kind, "reexports" ) ;
2393+ assert_eq ! ( edges[ 2 ] . target_id, 2 ) ;
2394+ assert_eq ! ( edges[ 3 ] . kind, "reexports-wildcard" ) ;
2395+ assert_eq ! ( edges[ 3 ] . target_id, 2 ) ;
23082396 }
23092397
23102398 #[ test]
0 commit comments