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
+45
View File
@@ -1775,6 +1775,51 @@ func main() {
expect(otherCallers).toContain('useOther');
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', () => {