fix(extraction): preserve receivers for method values (#1820) (#2034)

* fix(extraction): record callers for methods passed as values (#1820)

pool.submit(obj.method), thread_pool_exec(self.store.fetch), and Go
Submit(c.store.Fetch) produced no callers/impact edge because only
obj.method(...) call expressions were captured. Member values are now
emitted as *.method function-refs and resolve unique-or-drop
(same-file first). Two methods of the same name still produce no edge.

Go go c.store.Fetch(ids) remains a calls edge (it is a call expression).

EXTRACTION_VERSION 27: MCP daemon catch-up rebuilds a stale index on
start so the watcher does not keep serving the hole after upgrade.

* fix(resolution): do not let test mocks veto #1820 method-value callers

A unique production method plus a unit-test double of the same name
was treated as ambiguous, so retrieve_vision_vectors still looked
callback-invisible on a real repo. Prefer the non-test file when
exactly one production target remains.

* fix(resolution): prefer a real override over a NotImplementedError base

#1820 unique-or-drop treated DocStoreConnection.delete_payload_fields
(the ABC stub) and QdrantConnection.delete_payload_fields as ambiguous,
so gc_stale_member_stamps stayed invisible. Skip short not-implemented
stubs when exactly one real implementation remains.

* fix(extraction): preserve receivers for method values (#1820)

Python attributes and Go selectors used as callback values were dropped or reduced to bare method names.
Preserve receiver paths in native and WASM extraction, resolve through type/import scope, and reject ambiguous or noncallable targets.
Cover callback, assignment, collection, and query behavior, with extraction version 27 signaling the required reindex.

Co-authored-by: Josh66 <info@aska-systems.de>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Josh66 <info@aska-systems.de>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-09-27 13:42:27 +00:00
committed by GitHub
co-authored by Josh66 Claude Opus 5.5
parent 2a2f71ec41
commit 5eaa6fedd5
9 changed files with 644 additions and 33 deletions
+26
View File
@@ -1009,6 +1009,31 @@ impl<'t> Walker<'t> {
row: p.row,
});
}
// #1820: preserve the receiver of a method value.
"selector_expression" => {
let field = v
.child_by_field_name("field")
.or_else(|| v.named_child(v.named_child_count().saturating_sub(1)));
let Some(field) = field else { return };
let name = self.text(field);
if name.is_empty() || is_stoplisted(name) {
return;
}
let value = self.text(v);
if !value.split('.').all(|part| {
!part.is_empty() && part.chars().enumerate().all(|(i, c)| {
c == '_' || c.is_ascii_alphabetic() || (i > 0 && c.is_ascii_digit())
})
}) { return; }
let p = field.start_position();
self.fn_ref_cands.push(Cand {
from,
name: value.to_string(),
line: p.row as u32 + 1,
column_byte: field.start_byte(),
row: p.row,
});
}
"literal_element" | "expression_list" => {
for i in 0..v.named_child_count() {
if let Some(c) = v.named_child(i) {
@@ -1050,6 +1075,7 @@ impl<'t> Walker<'t> {
let mut seen: HashSet<(String, String)> = HashSet::new();
for c in cands {
if !c.name.starts_with("this.")
&& !c.name.contains('.')
&& !c.name.contains("::")
&& !self.defined_fn_names.contains(&c.name)
&& !self.imported_names.contains(&c.name)
+12 -12
View File
@@ -6,7 +6,8 @@
//! decorators (`@staticmethod` yes, `@app.route(...)` no — python's `call`
//! kind isn't `call_expression`), module-level assignments always extract as
//! `variable` (no isConst hook), and `self.method` fn-ref candidates carry the
//! BARE attribute name. Python is not a TYPE_ANNOTATION language — no type
//! full receiver path (#1820).
//! Python is not a TYPE_ANNOTATION language — no type
//! refs anywhere. Files with parse errors defer to wasm.
use crate::buffers::{
@@ -903,18 +904,16 @@ impl<'t> Walker<'t> {
for v in values {
let (name, anchor) = match v.kind() {
"identifier" => (self.text(v).to_string(), v),
// `self.handle_click` — object EXACTLY `self`; BARE attr name.
// Preserve only statically named member chains, as on WASM.
"attribute" => {
let obj = v.child_by_field_name("object");
let attr = v.child_by_field_name("attribute");
match (obj, attr) {
(Some(o), Some(a))
if o.kind() == "identifier" && self.text(o) == "self" =>
{
(self.text(a).to_string(), a)
}
_ => continue,
}
let Some(attr) = v.child_by_field_name("attribute") else { continue };
let name = self.text(v);
if !name.split('.').all(|part| {
!part.is_empty() && part.chars().enumerate().all(|(i, c)| {
c == '_' || c.is_ascii_alphabetic() || (i > 0 && c.is_ascii_digit())
})
}) { continue; }
(name.to_string(), attr)
}
_ => continue,
};
@@ -964,6 +963,7 @@ impl<'t> Walker<'t> {
let mut seen: HashSet<(String, String)> = HashSet::new();
for c in cands {
if !c.name.starts_with("this.")
&& !c.name.contains('.')
&& !c.name.contains("::")
&& !self.defined_fn_names.contains(&c.name)
&& !self.imported_names.contains(&c.name)