Skip to content

Commit b08c2b2

Browse files
fix: add receiver field to call sites to eliminate false positive edges
Dogfooding revealed ~52% of call edges were false positives because obj.method() and standalone() both produced identical call records, causing the global fallback to match ANY function with that name. Add an optional receiver field to call site extraction across all 11 language extractors (WASM + Rust native). The builder's global fallback now only fires for standalone calls or this/self/super — method calls on a receiver skip it entirely. Graph edges on self-analysis dropped from ~1742 to 1321 (24% reduction), all removed edges being false positives like insertNode.run() resolving to f run in cli.test.js.
1 parent 33f562e commit b08c2b2

22 files changed

Lines changed: 172 additions & 280 deletions

File tree

crates/codegraph-core/src/extractors/csharp.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,14 +199,18 @@ fn walk_node(node: &Node, source: &[u8], symbols: &mut FileSymbols) {
199199
name: node_text(&fn_node, source).to_string(),
200200
line: start_line(node),
201201
dynamic: None,
202+
receiver: None,
202203
});
203204
}
204205
"member_access_expression" => {
205206
if let Some(name) = fn_node.child_by_field_name("name") {
207+
let receiver = fn_node.child_by_field_name("expression")
208+
.map(|expr| node_text(&expr, source).to_string());
206209
symbols.calls.push(Call {
207210
name: node_text(&name, source).to_string(),
208211
line: start_line(node),
209212
dynamic: None,
213+
receiver,
210214
});
211215
}
212216
}
@@ -219,6 +223,7 @@ fn walk_node(node: &Node, source: &[u8], symbols: &mut FileSymbols) {
219223
name: node_text(&name, source).to_string(),
220224
line: start_line(node),
221225
dynamic: None,
226+
receiver: None,
222227
});
223228
}
224229
}
@@ -242,6 +247,7 @@ fn walk_node(node: &Node, source: &[u8], symbols: &mut FileSymbols) {
242247
name,
243248
line: start_line(node),
244249
dynamic: None,
250+
receiver: None,
245251
});
246252
}
247253
}

crates/codegraph-core/src/extractors/go.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,14 +158,18 @@ fn walk_node(node: &Node, source: &[u8], symbols: &mut FileSymbols) {
158158
name: node_text(&fn_node, source).to_string(),
159159
line: start_line(node),
160160
dynamic: None,
161+
receiver: None,
161162
});
162163
}
163164
"selector_expression" => {
164165
if let Some(field) = fn_node.child_by_field_name("field") {
166+
let receiver = fn_node.child_by_field_name("operand")
167+
.map(|op| node_text(&op, source).to_string());
165168
symbols.calls.push(Call {
166169
name: node_text(&field, source).to_string(),
167170
line: start_line(node),
168171
dynamic: None,
172+
receiver,
169173
});
170174
}
171175
}

crates/codegraph-core/src/extractors/java.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,10 +192,13 @@ fn walk_node(node: &Node, source: &[u8], symbols: &mut FileSymbols) {
192192

193193
"method_invocation" => {
194194
if let Some(name_node) = node.child_by_field_name("name") {
195+
let receiver = node.child_by_field_name("object")
196+
.map(|obj| node_text(&obj, source).to_string());
195197
symbols.calls.push(Call {
196198
name: node_text(&name_node, source).to_string(),
197199
line: start_line(node),
198200
dynamic: None,
201+
receiver,
199202
});
200203
}
201204
}
@@ -212,6 +215,7 @@ fn walk_node(node: &Node, source: &[u8], symbols: &mut FileSymbols) {
212215
name,
213216
line: start_line(node),
214217
dynamic: None,
218+
receiver: None,
215219
});
216220
}
217221
}

crates/codegraph-core/src/extractors/javascript.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -388,6 +388,7 @@ fn extract_call_info(fn_node: &Node, call_node: &Node, source: &[u8]) -> Option<
388388
name: node_text(fn_node, source).to_string(),
389389
line: start_line(call_node),
390390
dynamic: None,
391+
receiver: None,
391392
}),
392393
"member_expression" => {
393394
let obj = fn_node.child_by_field_name("object");
@@ -402,6 +403,7 @@ fn extract_call_info(fn_node: &Node, call_node: &Node, source: &[u8]) -> Option<
402403
name: node_text(obj, source).to_string(),
403404
line: start_line(call_node),
404405
dynamic: Some(true),
406+
receiver: None,
405407
});
406408
}
407409
if obj.kind() == "member_expression" {
@@ -410,6 +412,7 @@ fn extract_call_info(fn_node: &Node, call_node: &Node, source: &[u8]) -> Option<
410412
name: node_text(&inner_prop, source).to_string(),
411413
line: start_line(call_node),
412414
dynamic: Some(true),
415+
receiver: None,
413416
});
414417
}
415418
}
@@ -419,18 +422,24 @@ fn extract_call_info(fn_node: &Node, call_node: &Node, source: &[u8]) -> Option<
419422
if prop.kind() == "string" || prop.kind() == "string_fragment" {
420423
let method_name = node_text(&prop, source).replace(&['\'', '"'][..], "");
421424
if !method_name.is_empty() {
425+
let receiver = fn_node.child_by_field_name("object")
426+
.map(|obj| node_text(&obj, source).to_string());
422427
return Some(Call {
423428
name: method_name,
424429
line: start_line(call_node),
425430
dynamic: Some(true),
431+
receiver,
426432
});
427433
}
428434
}
429435

436+
let receiver = fn_node.child_by_field_name("object")
437+
.map(|obj| node_text(&obj, source).to_string());
430438
Some(Call {
431439
name: prop_text.to_string(),
432440
line: start_line(call_node),
433441
dynamic: None,
442+
receiver,
434443
})
435444
}
436445
"subscript_expression" => {
@@ -440,10 +449,13 @@ fn extract_call_info(fn_node: &Node, call_node: &Node, source: &[u8]) -> Option<
440449
let method_name = node_text(&index, source)
441450
.replace(&['\'', '"', '`'][..], "");
442451
if !method_name.is_empty() && !method_name.contains('$') {
452+
let receiver = fn_node.child_by_field_name("object")
453+
.map(|obj| node_text(&obj, source).to_string());
443454
return Some(Call {
444455
name: method_name,
445456
line: start_line(call_node),
446457
dynamic: Some(true),
458+
receiver,
447459
});
448460
}
449461
}

crates/codegraph-core/src/extractors/php.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,7 @@ fn walk_node(node: &Node, source: &[u8], symbols: &mut FileSymbols) {
212212
name: node_text(&fn_node, source).to_string(),
213213
line: start_line(node),
214214
dynamic: None,
215+
receiver: None,
215216
});
216217
}
217218
"qualified_name" => {
@@ -221,6 +222,7 @@ fn walk_node(node: &Node, source: &[u8], symbols: &mut FileSymbols) {
221222
name: last.to_string(),
222223
line: start_line(node),
223224
dynamic: None,
225+
receiver: None,
224226
});
225227
}
226228
_ => {}
@@ -230,20 +232,26 @@ fn walk_node(node: &Node, source: &[u8], symbols: &mut FileSymbols) {
230232

231233
"member_call_expression" => {
232234
if let Some(name) = node.child_by_field_name("name") {
235+
let receiver = node.child_by_field_name("object")
236+
.map(|obj| node_text(&obj, source).to_string());
233237
symbols.calls.push(Call {
234238
name: node_text(&name, source).to_string(),
235239
line: start_line(node),
236240
dynamic: None,
241+
receiver,
237242
});
238243
}
239244
}
240245

241246
"scoped_call_expression" => {
242247
if let Some(name) = node.child_by_field_name("name") {
248+
let receiver = node.child_by_field_name("scope")
249+
.map(|s| node_text(&s, source).to_string());
243250
symbols.calls.push(Call {
244251
name: node_text(&name, source).to_string(),
245252
line: start_line(node),
246253
dynamic: None,
254+
receiver,
247255
});
248256
}
249257
}
@@ -258,6 +266,7 @@ fn walk_node(node: &Node, source: &[u8], symbols: &mut FileSymbols) {
258266
name: last.to_string(),
259267
line: start_line(node),
260268
dynamic: None,
269+
receiver: None,
261270
});
262271
}
263272
}

crates/codegraph-core/src/extractors/python.rs

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -85,18 +85,24 @@ fn walk_node(node: &Node, source: &[u8], symbols: &mut FileSymbols) {
8585

8686
"call" => {
8787
if let Some(fn_node) = node.child_by_field_name("function") {
88-
let call_name = match fn_node.kind() {
89-
"identifier" => Some(node_text(&fn_node, source).to_string()),
90-
"attribute" => fn_node
91-
.child_by_field_name("attribute")
92-
.map(|a| node_text(&a, source).to_string()),
93-
_ => None,
88+
let (call_name, receiver) = match fn_node.kind() {
89+
"identifier" => (Some(node_text(&fn_node, source).to_string()), None),
90+
"attribute" => {
91+
let name = fn_node
92+
.child_by_field_name("attribute")
93+
.map(|a| node_text(&a, source).to_string());
94+
let recv = fn_node.child_by_field_name("object")
95+
.map(|obj| node_text(&obj, source).to_string());
96+
(name, recv)
97+
}
98+
_ => (None, None),
9499
};
95100
if let Some(name) = call_name {
96101
symbols.calls.push(Call {
97102
name,
98103
line: start_line(node),
99104
dynamic: None,
105+
receiver,
100106
});
101107
}
102108
}

crates/codegraph-core/src/extractors/ruby.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,10 +143,13 @@ fn walk_node(node: &Node, source: &[u8], symbols: &mut FileSymbols) {
143143
}
144144
}
145145
} else {
146+
let receiver = node.child_by_field_name("receiver")
147+
.map(|r| node_text(&r, source).to_string());
146148
symbols.calls.push(Call {
147149
name: method_text.to_string(),
148150
line: start_line(node),
149151
dynamic: None,
152+
receiver,
150153
});
151154
}
152155
}

crates/codegraph-core/src/extractors/rust_lang.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,23 +138,30 @@ fn walk_node(node: &Node, source: &[u8], symbols: &mut FileSymbols) {
138138
name: node_text(&fn_node, source).to_string(),
139139
line: start_line(node),
140140
dynamic: None,
141+
receiver: None,
141142
});
142143
}
143144
"field_expression" => {
144145
if let Some(field) = fn_node.child_by_field_name("field") {
146+
let receiver = fn_node.child_by_field_name("value")
147+
.map(|v| node_text(&v, source).to_string());
145148
symbols.calls.push(Call {
146149
name: node_text(&field, source).to_string(),
147150
line: start_line(node),
148151
dynamic: None,
152+
receiver,
149153
});
150154
}
151155
}
152156
"scoped_identifier" => {
153157
if let Some(name) = fn_node.child_by_field_name("name") {
158+
let receiver = fn_node.child_by_field_name("path")
159+
.map(|p| node_text(&p, source).to_string());
154160
symbols.calls.push(Call {
155161
name: node_text(&name, source).to_string(),
156162
line: start_line(node),
157163
dynamic: None,
164+
receiver,
158165
});
159166
}
160167
}
@@ -169,6 +176,7 @@ fn walk_node(node: &Node, source: &[u8], symbols: &mut FileSymbols) {
169176
name: format!("{}!", node_text(&macro_node, source)),
170177
line: start_line(node),
171178
dynamic: None,
179+
receiver: None,
172180
});
173181
}
174182
}

crates/codegraph-core/src/types.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ pub struct Call {
1818
pub name: String,
1919
pub line: u32,
2020
pub dynamic: Option<bool>,
21+
pub receiver: Option<String>,
2122
}
2223

2324
#[napi(object)]

0 commit comments

Comments
 (0)