fix(resolution): extend typed-parameter receiver inference to Rust/Go/Dart/PHP (#1125) (#1130)

Completes the #1125 fix. The same typed-parameter gap fixed for TS/JS existed
in every other language whose localReceiverTypePatterns only matched
keyword-anchored locals (let/var/:=/= new) and never the bare parameter form:

- Rust: the `:`-annotation pattern required `let`, so `fn use(lg: &Logger)`
  didn't match. Dropped the `let` anchor (still covers `let lg: Logger`),
  keeping the `&?mut?` handling — now covers params and closures `|lg: T|`.
- Go: only `lg := T{}` / `var lg T` matched; a parameter/method-receiver
  `func use(lg Logger)` / `func (l Logger) M()` (name-before-type, no keyword)
  didn't. Added a PascalCase-guarded `ident Type` pattern — the guard plus the
  existing enclosing-scope bound (excludes package-level struct fields) keep
  the keyword-free shape from matching unrelated pairs.
- Dart: the type-before-name pattern's trailing `[=;]` missed a parameter's
  `)`/`,`. Widened to `[=;,)]`, mirroring Java/C#.
- PHP: only `$lg = new T` matched; a typed param `function use(Logger $lg)`
  (also `?Logger`, `\App\Logger`, `&$lg`, `catch (E $e)`) didn't. Added a
  type-before-$var pattern. Reserved words can't be class names, so the
  looser lowercase-allowing capture yields no wrong edges.

Every pattern still relies on resolveMethodOnType validating the inferred type
actually declares the method (no edge on a mis-inference) — the same safety
net the already-covered languages use. Verified with a deterministic probe:
all four now disambiguate two same-named methods via the typed param (Java +
Kotlin as passing controls), full suite green (1930), no regressions.

Adds a parameterized regression test (Rust/Go/Dart/PHP), associating method to
type by qualifiedName so it holds where the method sits outside the type's
line range (Rust impl, Go decl).

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-07-02 12:17:23 -05:00
committed by GitHub
co-authored by Claude Opus 4.8
parent 385001398b
commit cf86fe8198
3 changed files with 64 additions and 3 deletions
+1 -1
View File
@@ -31,7 +31,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- Graph traversal and blast-radius results no longer drop or miscount relationships in a handful of edge cases. When a symbol could be reached by more than one path, an impact/blast-radius query could leave out a direct dependency between two symbols that were already linked another way; separately, the lower-level graph traversal used by the library API could keep only one of several relationships between the same pair of symbols (for example a symbol that both calls and references another), count a caller reached through two different call sites twice, or return slightly more results than the requested size limit on a very highly-connected symbol. These were long-standing and mostly masked by later de-duplication, so day-to-day query results were largely unaffected, but the traversal now returns the complete, correctly-bounded set. Thanks @inth3shadows for the precise, individually-traced reports. (#1086, #1087, #1088, #1089, #1090) - Graph traversal and blast-radius results no longer drop or miscount relationships in a handful of edge cases. When a symbol could be reached by more than one path, an impact/blast-radius query could leave out a direct dependency between two symbols that were already linked another way; separately, the lower-level graph traversal used by the library API could keep only one of several relationships between the same pair of symbols (for example a symbol that both calls and references another), count a caller reached through two different call sites twice, or return slightly more results than the requested size limit on a very highly-connected symbol. These were long-standing and mostly masked by later de-duplication, so day-to-day query results were largely unaffected, but the traversal now returns the complete, correctly-bounded set. Thanks @inth3shadows for the precise, individually-traced reports. (#1086, #1087, #1088, #1089, #1090)
- Method calls to same-named classes in different files now resolve to the right definition. If two files each declared, say, a `Logger` class with its own `log()` method, a call could be linked to whichever definition happened to be indexed first — so a call in one file wrongly pointed at the class in another, mixing up that method's callers and blast radius. This affected calls written as `obj.log()`, `Logger.log()`, and `Logger::log()` across many languages, including C++, Python, TypeScript, Java, C#, and Rust. When a method name is ambiguous, CodeGraph now prefers the definition in the calling file itself — the correct target in the common case — while Java/Kotlin calls that an `import` already pins to another file are unaffected. Thanks @inth3shadows for the minimal repro and root-cause analysis. (#1079) - Method calls to same-named classes in different files now resolve to the right definition. If two files each declared, say, a `Logger` class with its own `log()` method, a call could be linked to whichever definition happened to be indexed first — so a call in one file wrongly pointed at the class in another, mixing up that method's callers and blast radius. This affected calls written as `obj.log()`, `Logger.log()`, and `Logger::log()` across many languages, including C++, Python, TypeScript, Java, C#, and Rust. When a method name is ambiguous, CodeGraph now prefers the definition in the calling file itself — the correct target in the common case — while Java/Kotlin calls that an `import` already pins to another file are unaffected. Thanks @inth3shadows for the minimal repro and root-cause analysis. (#1079)
- Live auto-sync now gives up cleanly if indexing keeps failing, instead of retrying forever in the background. If a repeatable indexing error kept every sync from completing — for example a file that reliably crashes one language's parser, a full disk, or a corrupt database — a long-running server or daemon would retry every couple of seconds indefinitely, quietly wasting work and filling the logs while the graph silently stopped updating. Auto-sync now backs off after repeated failures and, if they persist, disables itself with an actionable message (naming the underlying error and pointing you to run `codegraph sync`), so the stalled index is surfaced rather than hidden — matching how prolonged file-lock contention and watch-limit exhaustion already degrade. A single successful sync resets everything, so an occasional transient hiccup is unaffected. Thanks @inth3shadows for the precise, code-traced report. (#1127) - Live auto-sync now gives up cleanly if indexing keeps failing, instead of retrying forever in the background. If a repeatable indexing error kept every sync from completing — for example a file that reliably crashes one language's parser, a full disk, or a corrupt database — a long-running server or daemon would retry every couple of seconds indefinitely, quietly wasting work and filling the logs while the graph silently stopped updating. Auto-sync now backs off after repeated failures and, if they persist, disables itself with an actionable message (naming the underlying error and pointing you to run `codegraph sync`), so the stalled index is surfaced rather than hidden — matching how prolonged file-lock contention and watch-limit exhaustion already degrade. A single successful sync resets everything, so an occasional transient hiccup is unaffected. Thanks @inth3shadows for the precise, code-traced report. (#1127)
- TypeScript/JavaScript method calls made through a typed function parameter now resolve to the right method. A call like `function use(lg: Logger) { lg.log(); }` wasn't linking to `Logger`'s `log()` — CodeGraph inferred a receiver's type from a local declaration (`const lg: Logger`) but not from a bare typed parameter, so when another class defined a method of the same name the call was dropped from callers and impact/blast-radius. Typed parameters are now recognized the same way, including generic types (`repo: Repository<User>`), matching how Java, C#, Kotlin, Swift, and Scala already handled it. Thanks @inth3shadows for the isolated repro and root-cause analysis. (#1125) - Method calls made through a typed function parameter now resolve to the right method in more languages. A call like `function use(lg: Logger) { lg.log(); }` wasn't linking to `Logger`'s `log()` — CodeGraph inferred a receiver's type from a local declaration but not from a bare typed parameter, so when another class defined a method of the same name the call was dropped from callers and impact/blast-radius. Typed parameters (and method receivers) are now recognized in TypeScript, JavaScript, Rust, Go, Dart, and PHP — including TypeScript generic types (`repo: Repository<User>`) matching how Java, C#, Kotlin, Swift, Scala, Python, and Pascal already handled it, so the parameter case is now covered across every language that has typed parameters. Thanks @inth3shadows for the isolated repro and root-cause analysis. (#1125)
## [1.1.6] - 2026-06-30 ## [1.1.6] - 2026-06-30
+45
View File
@@ -1775,6 +1775,51 @@ func main() {
expect(otherCallers).toContain('useOther'); expect(otherCallers).toContain('useOther');
expect(otherCallers).not.toContain('use'); expect(otherCallers).not.toContain('use');
}); });
// The same typed-parameter gap existed in every language whose pattern set
// only matched keyword-anchored locals (let/var/:=/= new), not the bare
// parameter form — Rust, Go, Dart, PHP (#1125). Each case: two classes
// sharing a method name + two functions taking one as a typed param; a
// correct fix routes each call to its OWN type's method (the collision is
// load-bearing — a single class resolves via the same-name fallback either
// way). Method↔type association is by qualifiedName, robust where the method
// lives outside the type's line range (Rust `impl`, Go method decl).
const typedParamCases: Array<{
lang: string; file: string; method: string; callerA: string; callerB: string; src: string;
}> = [
{ lang: 'Rust (fn f(x: &T))', file: 'svc.rs', method: 'log', callerA: 'use_it', callerB: 'use_other',
src: `pub struct Logger { n: i32 }\nimpl Logger { pub fn log(&self) -> i32 { self.n } }\npub struct Other { n: i32 }\nimpl Other { pub fn log(&self) -> i32 { self.n } }\npub fn use_it(lg: &Logger) -> i32 { lg.log() }\npub fn use_other(o: &Other) -> i32 { o.log() }\n` },
{ lang: 'Go (func f(x T))', file: 'svc.go', method: 'Log', callerA: 'UseIt', callerB: 'UseOther',
src: `package a\ntype Logger struct{}\nfunc (l Logger) Log() int { return 1 }\ntype Other struct{}\nfunc (o Other) Log() int { return 2 }\nfunc UseIt(lg Logger) int { return lg.Log() }\nfunc UseOther(o Other) int { return o.Log() }\n` },
{ lang: 'Dart (T f(U x))', file: 'svc.dart', method: 'log', callerA: 'useIt', callerB: 'useOther',
src: `class Logger { int log() { return 1; } }\nclass Other { int log() { return 2; } }\nint useIt(Logger lg) { return lg.log(); }\nint useOther(Other o) { return o.log(); }\n` },
{ lang: 'PHP (f(T $x))', file: 'svc.php', method: 'log', callerA: 'useIt', callerB: 'useOther',
src: `<?php\nclass Logger { function log() { return 1; } }\nclass Other { function log() { return 2; } }\nfunction useIt(Logger $lg) { return $lg->log(); }\nfunction useOther(Other $o) { return $o->log(); }\n` },
];
for (const c of typedParamCases) {
it(`infers a typed-parameter receiver, disambiguating same-named methods — ${c.lang} (#1125)`, async () => {
fs.writeFileSync(path.join(tempDir, c.file), c.src);
cg = await CodeGraph.init(tempDir, { index: true });
cg.resolveReferences();
const methods = cg.getNodesByKind('method').filter((n) => n.name === c.method);
expect(methods.length, `${c.lang}: both ${c.method} methods indexed`).toBe(2);
const loggerLog = methods.find((m) => /Logger/.test(m.qualifiedName ?? ''));
const otherLog = methods.find((m) => /Other/.test(m.qualifiedName ?? ''));
expect(loggerLog, `${c.lang}: Logger's ${c.method}`).toBeDefined();
expect(otherLog, `${c.lang}: Other's ${c.method}`).toBeDefined();
const loggerCallers = cg.getCallers(loggerLog!.id).map((x) => x.node.name);
const otherCallers = cg.getCallers(otherLog!.id).map((x) => x.node.name);
expect(loggerCallers, `${c.lang}: Logger callers`).toContain(c.callerA);
expect(loggerCallers, `${c.lang}: Logger callers`).not.toContain(c.callerB);
expect(otherCallers, `${c.lang}: Other callers`).toContain(c.callerB);
expect(otherCallers, `${c.lang}: Other callers`).not.toContain(c.callerA);
});
}
}); });
describe('Name Matcher: kind bias for new ref kinds', () => { describe('Name Matcher: kind bias for new ref kinds', () => {
+18 -2
View File
@@ -1102,12 +1102,21 @@ function localReceiverTypePatterns(language: Language, r: string): RegExp[] {
case 'rust': case 'rust':
return [ return [
new RegExp(`\\blet\\s+(?:mut\\s+)?${r}\\b(?:\\s*:[^=]+)?=\\s*&?(?:mut\\s+)?([A-Z][\\w]*)`), // let lg = Logger::new()/Logger{}/Logger new RegExp(`\\blet\\s+(?:mut\\s+)?${r}\\b(?:\\s*:[^=]+)?=\\s*&?(?:mut\\s+)?([A-Z][\\w]*)`), // let lg = Logger::new()/Logger{}/Logger
new RegExp(`\\blet\\s+(?:mut\\s+)?${r}\\s*:\\s*&?(?:mut\\s+)?([A-Z][\\w]*)`), // let lg: Logger // No `let`, so this covers a `let lg: Logger` binding AND a typed
// parameter (`fn use(lg: &Logger)`, a closure `|lg: Logger|`) — the
// parameter case the old `let`-anchored pattern excluded (#1125).
new RegExp(`\\b${r}\\s*:\\s*&?(?:mut\\s+)?([A-Z][\\w]*)`), // lg: Logger (binding or typed param)
]; ];
case 'go': case 'go':
return [ return [
new RegExp(`\\b${r}\\b\\s*:=\\s*&?([A-Za-z_][\\w.]*)\\s*{`), // lg := Logger{} / &Logger{} new RegExp(`\\b${r}\\b\\s*:=\\s*&?([A-Za-z_][\\w.]*)\\s*{`), // lg := Logger{} / &Logger{}
new RegExp(`\\bvar\\s+${r}\\s+\\*?([A-Za-z_][\\w.]*)`), // var lg Logger / *Logger new RegExp(`\\bvar\\s+${r}\\s+\\*?([A-Za-z_][\\w.]*)`), // var lg Logger / *Logger
// A typed parameter / method receiver (`func use(lg Logger)`,
// `func (l Logger) M()`) — name-before-type with no `var`/`:=` (#1125).
// PascalCase-guarded (unlike the anchored patterns above) to keep the
// keyword-free `ident Type` shape from matching unrelated pairs; the
// enclosing-scope bound already excludes package-level struct fields.
new RegExp(`\\b${r}\\s+\\*?([A-Z][\\w.]*)`), // func use(lg Logger) / (l Logger)
]; ];
case 'ruby': case 'ruby':
return [ return [
@@ -1121,11 +1130,18 @@ function localReceiverTypePatterns(language: Language, r: string): RegExp[] {
case 'dart': case 'dart':
return [ return [
new RegExp(`\\b${r}\\b\\s*=\\s*([A-Z][\\w.]*)\\s*\\(`), // var lg = Logger(...) new RegExp(`\\b${r}\\b\\s*=\\s*([A-Z][\\w.]*)\\s*\\(`), // var lg = Logger(...)
new RegExp(`\\b([A-Z][\\w.]*)\\s+${r}\\b\\s*[=;]`), // Logger lg = ... // Trailing `[=;,)]` (not just `[=;]`) so a typed parameter — `Logger lg)`
// / `Logger lg,` — matches too, not only `Logger lg = ...` / `Logger lg;`
// (#1125). Mirrors Java/C#.
new RegExp(`\\b([A-Z][\\w.]*)\\s+${r}\\b\\s*[=;,)]`), // Logger lg = ... / param
]; ];
case 'php': case 'php':
return [ return [
new RegExp(`\\$?${r}\\b\\s*=\\s*new\\s+([A-Za-z_\\\\][\\w\\\\]*)`), // $lg = new Logger() new RegExp(`\\$?${r}\\b\\s*=\\s*new\\s+([A-Za-z_\\\\][\\w\\\\]*)`), // $lg = new Logger()
// A typed parameter (`function use(Logger $lg)`, `?Logger $lg`,
// `\\App\\Logger $lg`, `&$lg` by-ref) and a typed `catch (E $e)` — the
// type sits before the `$`-variable (#1125). Namespace `\\` allowed.
new RegExp(`\\b([A-Za-z_\\\\][\\w\\\\]*)\\s+&?\\$${r}\\b`), // Logger $lg (typed param)
]; ];
case 'lua': case 'lua':
case 'luau': case 'luau':