Selaa lähdekoodia

fix(resolution): infer typed-parameter receivers in TS/JS (#1125) (#1129)

The local-variable receiver-type inference from #1108/#1110 covered typed
parameters for every language except TypeScript/JavaScript (+ TSX/JSX). The
TS/JS `:`-annotation pattern required a leading `const|let|var`, so it only
matched a local's own annotation (`const lg: Logger`) and never a bare
parameter (`function use(lg: Logger)` / `(lg: Logger) =>`). With a second
class sharing the method name — the case where a same-name fallback can't
paper over it — `lg.log()` resolved to no edge, dropping it from callers and
impact/blast-radius. TS/JS is the most common language pair in the userbase,
so this was a real precision gap.

Replace the keyword-anchored pattern with the keyword-free
`\b${r}\b\s*:\s*([A-Z][\w.$]*)`, mirroring Kotlin/Swift/Scala. It's a strict
superset (still matches `const lg: Logger`) plus the typed-parameter case,
and the capture stops at `<` so a generic-typed param
(`repo: Repository<User>`) still yields `Repository`. resolveMethodOnType
already validates the inferred type declares the method, so the looser match
produces no edge on a mis-inference — the same safety net the other
languages rely on; Swift already ships this identical bare-colon pattern with
the same theoretical ternary/dict-literal exposure.

Adds a regression test using two ambiguous classes + typed params, asserting
each call routes to its OWN class's method (verified to fail without the fix
and pass with it — a single-class version would pass either way via the
same-name fallback, which is why the collision is load-bearing).

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Colby Mchenry 15 tuntia sitten
vanhempi
sitoutus
385001398b
3 muutettua tiedostoa jossa 50 lisäystä ja 1 poistoa
  1. 1 0
      CHANGELOG.md
  2. 41 0
      __tests__/resolution.test.ts
  3. 8 1
      src/resolution/name-matcher.ts

+ 1 - 0
CHANGELOG.md

@@ -31,6 +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)
 
 ## [1.1.6] - 2026-06-30
 

+ 41 - 0
__tests__/resolution.test.ts

@@ -1734,6 +1734,47 @@ func main() {
       // Logger.new is still an instantiation of the class.
       expect(out.some((e) => e.kind === 'instantiates' && e.target === logger.id)).toBe(true);
     });
+
+    it('TypeScript: infers a typed-parameter receiver, disambiguating same-named methods (#1125)', async () => {
+      // A typed function parameter used as a receiver — `function use(lg: Logger)`
+      // — never matched the old TS/JS pattern (it required a const|let|var
+      // prefix), so `lg.log()` fell through to no edge once a second class shared
+      // the method name. Two ambiguous classes are load-bearing here: a
+      // single-class version resolves via a same-name fallback even without
+      // inference, so only the collision proves type inference actually fired.
+      fs.writeFileSync(
+        path.join(tempDir, 'svc.ts'),
+        `class Logger { log() { return 1; } }\n` +
+          `class Other { log() { return 2; } }\n` +
+          `export function use(lg: Logger) { return lg.log(); }\n` +
+          `export function useOther(o: Other) { return o.log(); }\n`,
+      );
+      cg = await CodeGraph.init(tempDir, { index: true });
+      cg.resolveReferences();
+
+      const classes = cg.getNodesByKind('class');
+      const logger = classes.find((n) => n.name === 'Logger')!;
+      const other = classes.find((n) => n.name === 'Other')!;
+      const logs = cg.getNodesByKind('method').filter((n) => n.name === 'log');
+      expect(logs.length, 'both log methods should be indexed').toBe(2);
+
+      // Associate each same-named `log` with its class by line containment.
+      const inClass = (m: (typeof logs)[number], c: typeof logger) =>
+        m.startLine >= c.startLine && m.startLine <= (c.endLine ?? c.startLine);
+      const loggerLog = logs.find((m) => inClass(m, logger))!;
+      const otherLog = logs.find((m) => inClass(m, other))!;
+      expect(loggerLog, "Logger's log").toBeDefined();
+      expect(otherLog, "Other's log").toBeDefined();
+
+      const loggerCallers = cg.getCallers(loggerLog.id).map((x) => x.node.name);
+      const otherCallers = cg.getCallers(otherLog.id).map((x) => x.node.name);
+
+      // Each typed-param call routes to its OWN class's method, not the other's.
+      expect(loggerCallers).toContain('use');
+      expect(loggerCallers).not.toContain('useOther');
+      expect(otherCallers).toContain('useOther');
+      expect(otherCallers).not.toContain('use');
+    });
   });
 
   describe('Name Matcher: kind bias for new ref kinds', () => {

+ 8 - 1
src/resolution/name-matcher.ts

@@ -1065,7 +1065,14 @@ function localReceiverTypePatterns(language: Language, r: string): RegExp[] {
     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
+        // No keyword requirement, so this matches BOTH a local annotation
+        // (`const lg: Logger`) and a typed parameter (`function use(lg: Logger)`
+        // / `(lg: Logger) =>`) — the parameter case the old `const|let|var`
+        // prefix excluded (#1125). Mirrors Kotlin/Swift/Scala; the capture stops
+        // at `<` so a generic-typed param (`repo: Repository<User>`) still yields
+        // `Repository`. resolveMethodOnType validates the type actually declares
+        // the method, so the looser match produces no edge on a mis-inference.
+        new RegExp(`\\b${r}\\b\\s*:\\s*([A-Z][\\w.$]*)`), // lg: Logger  (annotation or typed param)
       ];
     case 'python':
       return [