Repository navigation
TypeScript: x.m() links to an imported function x, even when a parameter or local shadows it #2953
Description
A member call x.m() links to an imported function named x, as though x itself were called. This happens even when x at the call is a parameter or local that shadows the import.
// streaming.ts
export interface StreamApi { onAbort(f: () => void): void; write(s: string): void; }
export function stream(c: unknown, cb: (s: StreamApi) => Promise<void>): Response { return null as any; }
// app.ts
import { stream } from './streaming';
export function handlerA(c: unknown) {
return stream(c, async (stream) => { // line 3
stream.onAbort(() => {}); // line 4
stream.write('x'); // line 5
});
}
export function handlerC(stream: { onAbort(f: () => void): void }) {
stream.onAbort(() => {}); // line 14
}
export function localConst() {
const stream = { onAbort(f: () => void) {} };
stream.onAbort(() => {});
}| call | edge on main (resolvedBy: import) |
expected |
|---|---|---|
line 3 stream(c, …) |
stream ✓ |
stream |
line 4 stream.onAbort(…) (arrow parameter) |
stream ✗ |
none, or StreamApi.onAbort |
line 5 stream.write(…) |
stream ✗ |
none, or StreamApi.write |
line 14 stream.onAbort(…) (function parameter) |
stream ✗ |
none |
localConst: stream.onAbort(…) (local const) |
stream ✗ |
none |
The edge's refName is stream.onAbort, but it resolves through the import binding of the receiver's first segment, so the callee is the receiver's import. The same parameter named s gives s.onAbort → StreamApi.onAbort. Even without shadowing, (stream as any).onAbort() links to stream, but a property of a function object isn't the function.
Reproduced on main at 2577601 with CODEGRAPH_KERNEL=0. Linux/WSL2, Node 22.23.2.
Where it shows up. This is hono's documented streaming API: stream(c, async (stream) => { stream.write(…); stream.onAbort(…) }). In hono (bac5c4a) indexed on 2577601, 45 calls edges have a refName of <name>.<member> and resolve by import to a function named <name>. One is runtime-tests/bun/index.test.tsx:164 stream.onAbort(…) → src/helper/streaming/stream.ts stream. So stream's callers list every line that uses the stream handle. (Another 56 edges in hono resolve <name>.<member>() to an imported constant named <name>, e.g. routes.map(…). I've left those out, since linking a member call to the object it's called on may be intended.)
CommonJS too. The same thing happens through require, where a module exports a function and hangs more functions off it (module.exports = Reply; module.exports.buildReply = buildReply):
// lib/reply.js
function Reply (res) { this.res = res }
function buildReply (R) { return R }
module.exports = Reply
module.exports.buildReply = buildReply
// app.js
const Reply = require('./lib/reply')
function build () { return Reply.buildReply(Reply) } // → Reply ✗ (expected buildReply)fastify (5df959f) is written this way. 397 calls edges there resolve X.member() by import to the function X. For example, fastify.js:187 Reply.buildReply(Reply) links to Reply in lib/reply.js rather than to buildReply.
Expected. A member call should not resolve to the receiver's own binding when that binding is a function import. It certainly shouldn't when a parameter or local between the call and the import shadows the name, as with the bare-call shadow rules (#2855, #2732, #2949).
Activity