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
+23 -14
View File
@@ -25,9 +25,10 @@
*
* Deliberately NOT covered (resolving the *dispatch* — `o->cb(x)` → the
* registered function — needs data-flow through struct fields; a wrong edge
* is worse than none): indirect-call resolution and `obj.method` member
* values where `obj` isn't `this`/`self` (the receiver's type is statically
* unknowable without local data-flow).
* is worse than none): indirect-call resolution. Member values where the
* receiver isn't `this`/`self` (`pool.submit(obj.method)`, `Submit(c.store.Fetch)`)
* retain their receiver (#1820), so resolution can use type/import scope
* before considering a unique method name.
*/
import type { Node as SyntaxNode } from 'web-tree-sitter';
@@ -51,7 +52,8 @@ export interface FnRefCandidate {
* referenced cross-file WITHOUT imports (global namespace), so the gate
* can't see them — the strong positional prior (a string argument to
* `usort`/`array_map`/…) plus resolution's unique-or-drop rule carry the
* precision instead.
* precision instead. Python/Go member values also skip the name gate,
* retaining their receiver for scoped resolution.
*/
skipGate?: boolean;
}
@@ -231,6 +233,7 @@ const GO_SPEC: FnRefSpec = {
['literal_element', null],
['expression_list', null],
]),
special: new Set(['selector_expression']),
};
const RUST_SPEC: FnRefSpec = {
@@ -699,16 +702,23 @@ function normalizeSpecial(
return [];
}
// Swift `#selector(Holder.fire)` → fire. ObjC `@selector(storeImage:)` →
// `storeImage:` verbatim (ObjC method nodes keep their selector colons).
// Go `c.store.Fetch` (has a `field` child) vs Swift `#selector(...)` /
// ObjC `@selector(...)` (no `field` — inner identifier / selector text).
case 'selector_expression': {
const field = getChildByField(node, 'field');
if (field) {
const name = getNodeText(field, source);
const receiver = getChildByField(node, 'operand');
const value = receiver ? `${getNodeText(receiver, source)}.${name}` : '';
return /^[A-Za-z_]\w*(?:\.[A-Za-z_]\w*)+$/.test(value)
? [{ name: value, node: field, skipGate: true }] : [];
}
const inner = node.namedChild(0);
if (!inner) return [];
if (inner.type === 'identifier' || inner.type === 'simple_identifier') {
return [{ name: getNodeText(inner, source), node: inner }];
}
// Swift dotted form: rightmost simple_identifier. ObjC keyword selector:
// text as-is.
// Swift dotted form: rightmost simple_identifier; ObjC keeps selector text.
const last = lastNamedOfType(node, new Set(['simple_identifier']));
if (last) return [{ name: getNodeText(last, source), node: last }];
return [{ name: getNodeText(inner, source).trim(), node: inner }];
@@ -739,14 +749,13 @@ function normalizeSpecial(
return [];
}
// `self.handle_click` (Python) — object must be EXACTLY `self`.
// Keep the receiver on Python member values; calls/subscripts are not
// statically named receivers and must not collapse to a bare method.
case 'attribute': {
const obj = getChildByField(node, 'object');
const attr = getChildByField(node, 'attribute');
if (obj && attr && obj.type === 'identifier' && getNodeText(obj, source) === 'self') {
return [{ name: getNodeText(attr, source), node: attr }];
}
return [];
const name = getNodeText(node, source);
return attr && /^[A-Za-z_]\w*(?:\.[A-Za-z_]\w*)+$/.test(name)
? [{ name, node: attr, skipGate: true }] : [];
}
// `this.Run0` (C#) — receiver must be EXACTLY `this`. Two grammar shapes: