diff --git a/CHANGELOG.md b/CHANGELOG.md index 141cf103db..8c62165f97 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -398,6 +398,8 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - The Claude Code prompt hook no longer runs on the task-notification messages Claude Code injects when a background agent finishes, removing a multi-second stall on every such turn. (#1832) +- C function-pointer calls no longer link to a Python or Rust function that happens to share the handler's name, and such a function no longer hides the real C handler. + - Rust calls on `self` now stay with the enclosing type instead of linking to an unrelated type’s same-named method. Thanks @L4XB. (#1861) - Turning telemetry off now resets its identity and stops running processes from recording, sending, or restoring unsent data. (#1869) diff --git a/__tests__/c-fnptr-synthesizer.test.ts b/__tests__/c-fnptr-synthesizer.test.ts index bba9fcadfc..77ea3f8772 100644 --- a/__tests__/c-fnptr-synthesizer.test.ts +++ b/__tests__/c-fnptr-synthesizer.test.ts @@ -179,6 +179,29 @@ int total(struct box *x) { return x->count + 1; } expect(edges.length).toBe(0); }); + it('links registrations only to C/C++ functions, never a same-named function in another language', async () => { + write('ops.h', `struct ops { int (*read)(int); int (*probe)(int); };\n`); + write('impl.c', `int impl_read(int fd) { return fd; }\n`); + write('ops.c', ` +#include "ops.h" +int impl_read(int fd); +static struct ops the_ops = { .read = impl_read, .probe = gen_probe }; +int use(struct ops *o, int fd) { return o->read(fd) + o->probe(fd); } +`); + write('aaa/gen.py', `def impl_read():\n return 1\n\ndef gen_probe():\n return 2\n`); + const cg = await CodeGraph.init(dir, { silent: true }); + await cg.indexAll(); + const rows: { tgt: string; file: string }[] = (cg as any).db.db + .prepare( + `SELECT t.name tgt, t.file_path file FROM edges e JOIN nodes t ON t.id = e.target + WHERE json_extract(e.metadata,'$.synthesizedBy') = 'fn-pointer-dispatch'` + ) + .all(); + cg.close?.(); + expect(rows.filter((r) => r.tgt === 'impl_read').map((r) => r.file)).toEqual(['impl.c']); + expect(rows.some((r) => r.file.endsWith('.py'))).toBe(false); + }); + it('is a no-op on a project with no C/C++ (clean control)', async () => { write('app.js', ` const handlers = { add: (x) => x + 1, rm: (x) => x - 1 }; diff --git a/src/resolution/c-fnptr-synthesizer.ts b/src/resolution/c-fnptr-synthesizer.ts index 568b782c9f..1585ae8ba7 100644 --- a/src/resolution/c-fnptr-synthesizer.ts +++ b/src/resolution/c-fnptr-synthesizer.ts @@ -879,7 +879,9 @@ export async function cFnPointerDispatchEdges( // ---- function-name → node resolution (prefer a function in the same file) ---- const resolveFn = (name: string, preferFile?: string): Node | null => { - const cands = ctx.getNodesByName(name).filter((n) => FN_KINDS.has(n.kind)); + // C and C++ functions only: a same-named Python or Rust function is never + // what a C table registers, and must not displace the C one. + const cands = ctx.getNodesByName(name).filter((n) => FN_KINDS.has(n.kind) && C_CPP_EXT.test(n.filePath)); if (cands.length === 0) return null; if (cands.length === 1) return cands[0]!; if (preferFile) {