Jelajahi Sumber

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>
Colby Mchenry 20 jam lalu
induk
melakukan
cf86fe8198
3 mengubah file dengan 64 tambahan dan 3 penghapusan
  1. 1 1
      CHANGELOG.md
  2. 45 0
      __tests__/resolution.test.ts
  3. 18 2
      src/resolution/name-matcher.ts

+ 1 - 1
CHANGELOG.md

@@ -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)
 - 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)
-- 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
 

+ 45 - 0
__tests__/resolution.test.ts

@@ -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', () => {

+ 18 - 2
src/resolution/name-matcher.ts

@@ -1102,12 +1102,21 @@ function localReceiverTypePatterns(language: Language, r: string): RegExp[] {
     case 'rust':
       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}\\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':
       return [
         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
+        // 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':
       return [
@@ -1121,11 +1130,18 @@ function localReceiverTypePatterns(language: Language, r: string): RegExp[] {
     case 'dart':
       return [
         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':
       return [
         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 'luau':