fix(extraction): never fabricate an edge from a call-result receiver (#1748)

A member call whose receiver is itself a call — `d.setdefault(k, []).append(v)`,
`make().run()` — used to drop the receiver at extraction time, degrade to the
bare method name, and exact-match any top-level project symbol of that name
(Python and JavaScript/TypeScript). Keep the inner callee encoded as
`<inner>().<method>` in the TS extractor and native kernel; the name-matcher
refuses to guess for that shape (store-accessor exception only). Based on
#1692, rebased onto main after #1746. Fixes #1683.

Co-authored-by: Colby McHenry <colbymchenry@users.noreply.github.com>
This commit is contained in:
Colby Mchenry
2026-09-08 00:23:08 -05:00
committed by GitHub
co-authored by Colby McHenry
parent bffd50e4f1
commit bb1d3093eb
10 changed files with 226 additions and 3 deletions
+23
View File
@@ -608,6 +608,13 @@ impl<'t> Walker<'t> {
} else {
callee_name = method_name.to_string();
}
} else if let Some(r) = receiver.filter(|r| r.kind() == "call") {
// Call receiver — `d.setdefault(k, []).append(v)` (#1683):
// `<inner>().<method>`, or nothing when the inner callee
// is not a plain name / attribute chain. Mirrors
// TreeSitterExtractor.extractCall.
let Some(inner) = self.plain_inner_callee(r) else { return };
callee_name = format!("{inner}().{method_name}");
} else {
callee_name = method_name.to_string();
}
@@ -626,6 +633,22 @@ impl<'t> Walker<'t> {
}
}
/// The callee of a call receiver when it is a plain identifier or attribute
/// chain (`make`, `d.setdefault`), whitespace stripped (#1683).
fn plain_inner_callee(&self, call: Node<'t>) -> Option<String> {
let inner = call.child_by_field_name("function")?;
let text: String = self.text(inner).chars().filter(|c| !c.is_whitespace()).collect();
if text.is_empty() {
return None;
}
let ok = text.split('.').all(|seg| {
let mut chars = seg.chars();
matches!(chars.next(), Some(c) if c.is_ascii_alphabetic() || c == '_')
&& chars.all(|c| c.is_ascii_alphanumeric() || c == '_')
});
if ok { Some(text) } else { None }
}
/// extractDecoratorsFor — python decorators are PRECEDING SIBLINGS inside
/// decorated_definition. Only bare-identifier decorators yield a target
/// (python's `call` kind isn't `call_expression`, and `attribute` isn't in
+23 -2
View File
@@ -1103,9 +1103,14 @@ impl<'t> Walker<'t> {
} else {
callee_name = method_name.to_string();
}
} else if let Some(r) = receiver.filter(|r| r.kind() == "call_expression") {
// Call receiver — `make().run()` (#1683): keep the inner
// callee as `<inner>().<method>`, or emit nothing when it
// is not a plain name / member chain. Mirrors
// TreeSitterExtractor.extractCall.
let Some(inner) = self.plain_inner_callee(r) else { return };
callee_name = format!("{inner}().{method_name}");
} else {
// (the call-receiver re-encode branches are other
// languages'; TS/JS keeps the bare method name)
callee_name = method_name.to_string();
}
}
@@ -1128,6 +1133,22 @@ impl<'t> Walker<'t> {
// --- extractInstantiation -----------------------------------------------------------
/// The callee of a call-expression receiver when it is a plain identifier
/// or member chain (`make`, `d.setdefault`), whitespace stripped (#1683).
fn plain_inner_callee(&self, call: Node<'t>) -> Option<String> {
let inner = call.child_by_field_name("function")?;
let text: String = self.text(inner).chars().filter(|c| !c.is_whitespace()).collect();
if text.is_empty() {
return None;
}
let ok = text.split('.').all(|seg| {
let mut chars = seg.chars();
matches!(chars.next(), Some(c) if c.is_ascii_alphabetic() || c == '_' || c == '$')
&& chars.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '$')
});
if ok { Some(text) } else { None }
}
pub(super) fn extract_instantiation(&mut self, node: Node<'t>) {
if self.stack.is_empty() {
return;