Skip to content

Commit fb05774

Browse files
committed
fix(native): prefer bare name over qualified in span-tie caller attribution
When an object-literal method (`const o1 = { f() { this.g() } }`) is parsed, the Rust extractor emits two definitions at the same line and span: `o1.f(function)` from `extract_object_literal_functions` (called eagerly inside `handle_var_decl`) and `f(method)` from `handle_method_def` (visited later during the AST child walk). The WASM extractor emits `f(method)` first (query captures run before the walk-phase helper), so its strict-`<` tie-break naturally picks the bare name. Native iterated them in the opposite order and therefore chose `o1.f`. Align `find_enclosing_caller` in the native engine with WASM by adding a tie-break: when two callable definitions have identical span, prefer the bare (unqualified) name over the dot-containing qualified one. Names with angle brackets (static-block synthetics like `B.<static:36:2>`) are excluded from this preference so they are never incorrectly preferred. Fixes the parity divergence reported in issue #1510 where native produced `o1.f(function) → g` while WASM produced `f(method) → g` for the receiver-callee-mixup jelly-micro fixture. Closes #1510
1 parent 6d242b4 commit fb05774

1 file changed

Lines changed: 24 additions & 2 deletions

File tree

crates/codegraph-core/src/domain/graph/builder/stages/build_edges.rs

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -669,13 +669,26 @@ fn is_top_level_binding_kind(kind: &str) -> bool {
669669

670670
/// Find the narrowest enclosing definition for a call at the given line.
671671
///
672-
/// Two-pass strategy (mirrors the updated `findCaller` in call-resolver.ts):
672+
/// Two-pass strategy (mirrors `findCaller` in call-resolver.ts):
673673
/// Pass 1 — narrowest enclosing function/method. Local variable declarations
674674
/// inside a function body must not shadow the enclosing function.
675675
/// Pass 2 — widest (outermost) enclosing variable/constant binding. Used as
676676
/// fallback when no function/method encloses the call (e.g. Haskell
677677
/// top-level `main = do …` is a `bind` node with kind `variable`).
678678
///
679+
/// Tie-breaking in Pass 1: when two callable definitions have the same span,
680+
/// prefer the bare (unqualified) name over the dot-containing qualified name.
681+
/// Object-literal methods are extracted twice by the Rust extractor — once as
682+
/// `o1.f(function)` from `extract_object_literal_functions` (called eagerly
683+
/// inside `handle_var_decl`) and once as `f(method)` from `handle_method_def`
684+
/// (called later during the child walk). The WASM extractor emits `f(method)`
685+
/// first (query captures run before the walk-phase `extractObjectLiteralFunctions`),
686+
/// so WASM's strict-less-than tie-break naturally picks the bare name.
687+
/// Applying the same preference here aligns native attribution with WASM and with
688+
/// the jelly-micro ground-truth expected-edges (which use bare `f`/`g` names).
689+
/// Names with angle brackets (e.g. `B.<static:36:2>`) are synthetic static-block
690+
/// nodes excluded from the bare-preference rule.
691+
///
679692
/// Returns `(caller_id, caller_name)` — `caller_name` is `""` when the call
680693
/// falls back to file scope.
681694
fn find_enclosing_caller<'a>(defs: &[DefWithId<'a>], call_line: u32, file_node_id: u32) -> (u32, &'a str) {
@@ -698,7 +711,16 @@ fn find_enclosing_caller<'a>(defs: &[DefWithId<'a>], call_line: u32, file_node_i
698711
if def.line <= call_line && call_line <= def.end_line {
699712
let span = def.end_line.saturating_sub(def.line);
700713
if is_callable_kind(def.kind) {
701-
if span < fn_caller_span {
714+
// On a strict span improvement always take the new candidate.
715+
// On a tie, prefer bare names over qualified names (dot-containing, no angle
716+
// brackets) so native matches WASM: both pick `f(method)` over `o1.f(function)`
717+
// when an object-literal method is extracted under both names at the same line.
718+
let is_improvement = span < fn_caller_span;
719+
let is_tie_prefer_bare = span == fn_caller_span
720+
&& !def.name.contains('.')
721+
&& fn_caller_name.contains('.')
722+
&& !fn_caller_name.contains('<');
723+
if is_improvement || is_tie_prefer_bare {
702724
if let Some(id) = def.node_id {
703725
fn_caller_id = Some(id);
704726
fn_caller_name = def.name;

0 commit comments

Comments
 (0)