fix(resolution): preserve callback registration scope (#1355) (#2015)

Field-channel synthesis discarded receiver and binding identity when selecting handlers globally by name.
Reuse resolved function-reference edges at the registration site, retaining class, inheritance, and import resolution without global fallback.
Add regression coverage for target identity, unknown handlers, and explore registration metadata.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-09-27 13:07:56 +00:00
committed by GitHub
parent 7b219a71c9
commit ce97ebc8e5
2 changed files with 114 additions and 3 deletions
+10 -3
View File
@@ -168,7 +168,7 @@ async function fieldChannelEdges(queries: QueryBuilder, ctx: ResolutionContext,
(d) => d.node.filePath === reg.node.filePath && d.field === reg.field
);
if (chDispatchers.length === 0) continue;
const argRe = new RegExp(`${reg.node.name}\\s*\\(\\s*(?:this\\.)?(\\w+)`);
const argRe = new RegExp(`${reg.node.name}\\s*\\(\\s*(this\\.\\w+|\\w+)\\s*(?=[,)])`);
let added = 0;
for (const e of queries.getIncomingEdges(reg.node.id, ['calls'])) {
if (added >= MAX_CALLBACKS_PER_CHANNEL) break;
@@ -178,8 +178,15 @@ async function fieldChannelEdges(queries: QueryBuilder, ctx: ResolutionContext,
const line = ctx.readFile(caller.filePath)?.split('\n')[e.line - 1];
const am = line?.match(argRe);
if (!am) continue;
const fn = ctx.getNodesByName(am[1]!).find((n) => n.kind === 'method' || n.kind === 'function');
if (!fn) continue;
// Reuse the resolved value at this registration site: it retains the
// receiver's class/inheritance and import binding, unlike a name lookup.
const refs = queries.getOutgoingEdges(caller.id, ['references']).filter(
(r) => r.line === e.line && r.metadata?.fnRef === true && r.metadata.refName === am[1]
);
if (refs.length !== 1) continue;
const fn = queries.getNodeById(refs[0]!.target);
if (!fn || (fn.kind !== 'method' && fn.kind !== 'function')) continue;
if (!am[1]!.startsWith('this.') && fn.filePath !== caller.filePath && refs[0]!.metadata?.resolvedBy !== 'import') continue;
for (const disp of chDispatchers) {
if (disp.node.id === fn.id) continue;
const key = `${disp.node.id}>${fn.id}`;