Repository navigation
Description
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
- Checked all 213 currently-open PRs (file lists pulled via GraphQL) — the only PR touching this file's TS/JS path is feat(resolution): resolve this.<field>.method() on the field's declared type (TS/JS) #920 (
this.<field>.method()resolution), which is a different mechanism (this-field receivers, not local/param receivers) and doesn't touchlocalReceiverTypePatterns. - Checked open issues — none reference this.
@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.
Activity
inth3shadows commentedon Jul 2, 2026
Follow-up: instrumented
inferLocalReceiverTypedirectly (tempconsole.errorin the compiled resolver, reverted immediately after — no repo changes) to confirm the mechanism rather than just the symptom. For the repro above:Confirms neither TS/JS pattern matches
export function use(lg: Logger) { ... }at all — inference returnsnulloutright for a typed-parameter receiver, which is why it falls through to no edge once there's a second class with the same method name.fix(resolution): infer typed-parameter receivers in TS/JS (#1125) (#1129
colbymchenry commentedon Jul 2, 2026
Fixed in #1129 (merged to
main, ships in the next release).Your diagnosis was exactly right, down to the instrumentation —
inferLocalReceiverTypereturningnullfor a typed-param receiver because theconst|let|varprefix excluded the parameter shape. Went with the keyword-free form mirroring Kotlin/Swift/Scala:A strict superset of the old pattern, so
const lg: Loggerstill resolves and the typed-param case now does too. It also keeps generics working (repo: Repository<User>→Repository, since the capture stops at<), which is why I didn't go the trailing-[,)]-delimiter route — that would've missed generic-typed params, and those are everywhere in TS.resolveMethodOnTyperemains the safety net (no edge on a mis-inference), and Swift already ships this identical bare-colon pattern, so this just brings TS/JS in line with the rest of the set.The two-ambiguous-classes framing in your repro was the key detail — I built the regression test around it and confirmed it fails without the fix and passes with it (a single-class version passes either way through the same-name fallback, exactly as you called out).
Thank you again for the consistently excellent reports — isolated repro, a byte-identical control in another language to pin the gap, PR/issue scan, and honest scoping of what you did vs. didn't verify. It makes these trivial to action and genuinely raises the bar for the whole project. Hugely appreciated. 🙏
fix(resolution): extend typed-parameter receiver inference to Rust/Go…
colbymchenry commentedon Jul 2, 2026
Follow-up: your report generalized, so I audited the rest of the supported languages for the same shape and found four more with the identical gap — any language whose receiver-type patterns only matched keyword-anchored locals (
let/var/:=/= new) but not the bare parameter form. Fixed all of them in #1130:fn use(lg: &Logger)(dropped theletanchor; also covers closures|lg: T|)func use(lg Logger)/func (l Logger) M()(PascalCase-guarded, scope-bound)int use(Logger lg)(widened the trailing delimiter to include)/,)function use(Logger $lg)(also?Logger,\App\Logger,&$lg,catch (E $e))The rest needed nothing: Python, Java, C#, Kotlin, Swift, Scala, Pascal, Luau already handled params; Ruby, Lua, R, and plain JS/JSX have no typed params; C/C++/Objective-C use separate receiver mechanisms. So the typed-parameter case is now covered across every supported language that has typed parameters. Verified each with a deterministic two-ambiguous-classes probe (Java + Kotlin as passing controls) and a parameterized regression test.
Your one isolated repro ended up closing a gap in six languages. Thank you again. 🙏
inth3shadows commentedon Jul 2, 2026
Really appreciate that — and the follow-up audit is honestly the more valuable half of this: catching the same gap in Rust/Go/Dart/PHP before anyone had to file each one separately. Those are all tough finds — the indexer I use myself only covers a handful of languages, so I'm glad what it could surface was enough to point you at the rest.
And codegraph itself has genuinely changed how I work day to day — this report came out of using it for real, not synthetic bug-hunting. Great tool, genuinely glad you built it. 🙏