Summary
The local-variable receiver-type inference added in #1108/#1110 covers typed parameters for every supported language except TypeScript/JavaScript (and TSX/JSX, which share the same pattern set). A method call through a typed function parameter — function use(lg: Logger) { lg.log(); } — never resolves via type inference in TS/JS, unlike the identical shape in Java, C#, Kotlin, Swift, and Scala, all of which explicitly support it.
Root cause
// src/resolution/name-matcher.ts:1062-1069
case 'typescript':
case 'javascript':
case 'tsx':
case 'jsx':
return [
new RegExp(`\\b${r}\\b\\s*=\\s*new\\s+([A-Za-z_$][\\w.$]*)`), // = new Logger()
new RegExp(`\\b(?:const|let|var)\\s+${r}\\s*:\\s*([A-Z][\\w.$]*)`), // lg: Logger
];
The second pattern requires a leading const|let|var, so it only matches a local variable's own type annotation (const lg: Logger = ...) — never a bare parameter declaration (function use(lg: Logger)), since a parameter has no const/let/var keyword. Compare to Java's equivalent (line ~1078): \b([A-Z][\w.]*)\s+${r}\b\s*[=;,)] — the trailing ) in the character class is exactly what lets it also match a parameter. Kotlin/Swift/Scala go further and drop any keyword requirement at all (\b${r}\b\s*:\s*([A-Z][\w.]*)), matching both val-declarations and typed params uniformly. TS/JS is the only language in this set that excludes the parameter case.
Repro (built v1.2.0, CodeGraph.init + resolveReferences())
// svc.ts
class Logger { log() { return 1; } }
class Other { log() { return 2; } }
export function use(lg: Logger) { return lg.log(); }
export function useOther(o: Other) { return o.log(); }
Callers of both log methods come back empty.
Control: the byte-identical shape in Java —
class Logger { void log() { int a = 1; } }
class Other { void log() { int a = 2; } }
class Use {
void run(Logger lg) { lg.log(); }
void runOther(Other o) { o.log(); }
}
— resolves correctly: run → Logger.log, runOther → Other.log. Same construct, same ambiguity (two classes sharing the method name, so no single-candidate fallback can paper over it), only the language differs. This isolates the gap to the TS/JS pattern set specifically.
(Note: a single-class, unambiguous version of the TS repro does resolve — through a same-name fallback strategy elsewhere in the resolver, not through type inference. That fallback is exactly what breaks down once there's a second class with the same method name, which is when type inference is supposed to take over and doesn't.)
Impact
TypeScript/JavaScript is the most common language pair in this project's own userbase, so this is a real precision gap wherever a typed parameter is used as a receiver and its method name collides with another class's method of the same name (a common shape for e.g. handler/service interfaces — process(), handle(), execute()).
Suggested fix direction
Add a parameter-shape pattern mirroring Java's, e.g. \b${r}\s*:\s*([A-Z][\w.$]*)\s*[,)] (or simply drop the const|let|var requirement the way Kotlin/Swift/Scala do) — resolveMethodOnType already validates the inferred type actually declares the method, so loosening this is safe against false positives the same way it is for the other languages.
Verification / scope
@colbymchenry — happy to send a PR for the fix (with a regression test pinning the two-ambiguous-classes/typed-param case), or just leave this filed if you'd rather pick it up yourself. Let me know which you'd prefer.
Summary
The local-variable receiver-type inference added in #1108/#1110 covers typed parameters for every supported language except TypeScript/JavaScript (and TSX/JSX, which share the same pattern set). A method call through a typed function parameter —
function use(lg: Logger) { lg.log(); }— never resolves via type inference in TS/JS, unlike the identical shape in Java, C#, Kotlin, Swift, and Scala, all of which explicitly support it.Root cause
The second pattern requires a leading
const|let|var, so it only matches a local variable's own type annotation (const lg: Logger = ...) — never a bare parameter declaration (function use(lg: Logger)), since a parameter has noconst/let/varkeyword. Compare to Java's equivalent (line ~1078):\b([A-Z][\w.]*)\s+${r}\b\s*[=;,)]— the trailing)in the character class is exactly what lets it also match a parameter. Kotlin/Swift/Scala go further and drop any keyword requirement at all (\b${r}\b\s*:\s*([A-Z][\w.]*)), matching bothval-declarations and typed params uniformly. TS/JS is the only language in this set that excludes the parameter case.Repro (built v1.2.0,
CodeGraph.init+resolveReferences())Callers of both
logmethods come back empty.Control: the byte-identical shape in Java —
— resolves correctly:
run→Logger.log,runOther→Other.log. Same construct, same ambiguity (two classes sharing the method name, so no single-candidate fallback can paper over it), only the language differs. This isolates the gap to the TS/JS pattern set specifically.(Note: a single-class, unambiguous version of the TS repro does resolve — through a same-name fallback strategy elsewhere in the resolver, not through type inference. That fallback is exactly what breaks down once there's a second class with the same method name, which is when type inference is supposed to take over and doesn't.)
Impact
TypeScript/JavaScript is the most common language pair in this project's own userbase, so this is a real precision gap wherever a typed parameter is used as a receiver and its method name collides with another class's method of the same name (a common shape for e.g. handler/service interfaces —
process(),handle(),execute()).Suggested fix direction
Add a parameter-shape pattern mirroring Java's, e.g.
\b${r}\s*:\s*([A-Z][\w.$]*)\s*[,)](or simply drop theconst|let|varrequirement the way Kotlin/Swift/Scala do) —resolveMethodOnTypealready validates the inferred type actually declares the method, so loosening this is safe against false positives the same way it is for the other languages.Verification / scope
this.<field>.method()resolution), which is a different mechanism (this-field receivers, not local/param receivers) and doesn't touchlocalReceiverTypePatterns.@colbymchenry — happy to send a PR for the fix (with a regression test pinning the two-ambiguous-classes/typed-param case), or just leave this filed if you'd rather pick it up yourself. Let me know which you'd prefer.