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>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
7c7514f43f
commit
385001398b
@@ -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
|
||||
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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 [
|
||||
|
||||
Reference in New Issue
Block a user