A call whose receiver is itself a call — a factory / singleton / builder that returns an object — should produce a calls edge to the chained method:
Foo.getInstance().bar(); // bar() should resolve to Foo::bar
This was fixed for C++ (#645) and PHP (#608). A probe of the other statically-typed README languages found the same gap in all of them — and in 7 of the 9 the chained method currently resolves to the wrong class (a same-named method on an unrelated type), which is a correctness bug, not just missing coverage.
The fix is the 3-part #645/#608 mechanism, per language: capture the factory's declared return type, preserve the chained receiver at extraction, then resolve and validate the chained method on the inferred type — so a wrong inference produces no edge, never a wrong one.
| Language |
Probe result (Foo.factory().method()) |
Status |
| Java |
edge missing |
✅ #751 |
| Kotlin |
wrong edge → same-named decoy |
✅ #752 |
| C# |
edge missing |
✅ #753 |
| Swift |
wrong edge → same-named decoy |
✅ #755 |
| Rust |
wrong edge → same-named decoy |
✅ #757 |
| Go |
wrong edge → same-named decoy (same-package New().Method(); cross-package pkg.New() was #469) |
✅ #760 |
| Scala |
wrong edge → same-named decoy |
✅ #761 |
| Dart |
wrong edge → same-named decoy (factory-constructor call also unresolved) |
✅ #762 |
| TypeScript |
wrong edge → same-named decoy |
⏭️ evaluated, skipped (see note) |
Dynamically-typed languages can't use this approach (no declared return types to read): Ruby and plain JS are out of scope; Python is partial (optional -> T hints, see #578).
Each language is validated the same way before merge: synthetic decoy + absent-method safety tests, then a real-repo A/B on a public OSS repo (node count unchanged, 0 edges lost, +N chained edges recovered, precision spot-checked).
TypeScript — evaluated and consciously skipped
The deterministic chain mechanism was fully implemented and probe-validated for TS (5 synthetic tests passed, including decoy-collision precision and a no-regression instance-chain guard), but real-repo A/B showed a net recall regression, so it was not shipped:
| Repo |
A/B (unique calls edges) |
| typeorm (495 files) |
+0 / −6 |
| nestjs/nest (600 files) |
+0 / −164 |
Why TS is different from the other 8 languages. The mechanism resolves a chained call from the factory's declared return type. TS code leans heavily on type inference — e.g. NestJS's Test.createTestingModule(metadata) { return new TestingModuleBuilder(...) } has no : TestingModuleBuilder annotation — so the factory's return type can't be recovered, the re-encoded chain can't resolve, and it drops the bare-name edge the existing resolver found. A bare-name fallback (the Go-fallback pattern, runaway-safe) recovered most but not all of them; ambiguous same-named methods across files (compile on both ModuleCompiler and TestingModuleBuilder) still resolved differently than baseline. The removed edges were mostly wrong (baseline mis-resolved .compile() to ModuleCompiler::compile), so it's precision-positive but recall-negative — the opposite of the recall-first invariant.
Zero added edges on both real repos confirms the marginal value is near-nil on real TS: method names are unique enough that the existing bare-name resolution already lands them, and the strict return-type annotations the mechanism needs are too often absent. The synthetic decoy win is real but doesn't occur in practice.
Conclusion: 8 of 9 statically-typed languages + conformance shipped (#751, #752, #753, #754, #755, #757, #760, #761, #762). TypeScript is the one language where gradual typing makes the deterministic chain mechanism a net loss, so it is intentionally not shipped.
The three typed README languages the original enumeration missed
The "9 statically-typed languages" list above was incomplete — the README also supports Objective-C, Pascal/Delphi, and Luau, all of which declare return types. Probed and addressed:
| Language |
Outcome |
| Objective-C |
✅ Shipped (#786). Same wrong-edge gap ([[Foo create] doIt] → a same-named decoy). Ported the mechanism over message_expression; getReturnType skips nullability qualifiers (nonnull instancetype); a class-message factory returns the receiver class by convention, so [[X alloc] init] / singleton chains resolve. SDWebImage A/B +35 / −75 — all corrections (the −75 are wrong init mis-matches retargeted to the right class). |
| Luau |
⏭️ Evaluated, not shipped — additive-safe but +0 on real repos. The mechanism works for annotated factory chains (Foo.create(): Bar then :doIt(); 5 synthetic-style checks pass) and its gap is missing-edge, so unlike TypeScript it's additive (no regression). But Luau is gradually typed and idiomatic code rarely annotates factory returns (Fusion's Expectation.new(value) has no : Expectation; typeof(Bar) returns don't even parse), so real-repo A/B was +0 / −0 on both Fusion AND matter. It doesn't meet the real-repo-win bar, so it was left unshipped (same gradual-typing conclusion as TS, minus the regression). |
| Pascal/Delphi |
🚧 Blocked by a larger prerequisite, out of scope for this mechanism. Pascal is statically typed (so the mechanism would genuinely pay off), but its method-call extraction from procedure bodies is broadly incomplete: paren-less calls (TFoo.GetInstance.DoIt) parse as a bare exprDot (not in the extractor's callTypes), and even paren'd method calls (f.Regular()) produce no edge (no receiver-type tracking for Pascal locals). Building Pascal's call graph is a substantial standalone extractor effort; the chained-call port is a small part of it and a half-fix would be worse than none. Documented as a separate follow-up. |
Final tally: the chained static-factory mechanism is shipped for 11 languages — C++, C, PHP, Java, Kotlin, C#, Swift, Rust, Go, Scala, Dart, Objective-C (+ conformance). TypeScript and Luau were evaluated and consciously skipped (gradual typing → the mechanism is +0 / regresses on real code). Pascal is blocked on prerequisite extractor work. Full design record: docs/design/chained-call-resolution.md.
Correction: Pascal/Delphi is shipped (#791), not blocked
My earlier "blocked" assessment was wrong — I'd probed only the paren-less form (TFoo.GetInstance.DoIt, which the extractor doesn't emit as a call). The paren'd form (TFoo.GetInstance().DoIt()) extracts fine and hit the chained-call gap like every other language (with a decoy it mis-resolved to a same-named method on an unrelated class). It's a clean port: getReturnType from the typeref; re-encode the chained exprCall-receiver, gated on the Delphi TFoo/IFoo type convention so capitalized variable chains stay bare; a constructor (TFoo.Create().…) or typecast (TFoo(x).…) resolves on the class itself. PascalCoin A/B +19 / −18 (15 of the −18 are correct class→interface retargets — GetInstance(): IAsn1OctetString now resolves on the declared interface). EXTRACTION_VERSION 16.
Revised final tally: 13 languages shipped — C++, C, PHP, Java, Kotlin, C#, Swift, Rust, Go, Scala, Dart, Objective-C, Pascal/Delphi (+ conformance). TypeScript and Luau evaluated and skipped (gradual typing → +0 / regression on real code). The only remaining out-of-scope languages are the genuinely dynamically-typed ones (JavaScript, Ruby, Lua, Svelte, Vue, Liquid; Python is partial via #578). Full design record: docs/design/chained-call-resolution.md.
A call whose receiver is itself a call — a factory / singleton / builder that returns an object — should produce a
callsedge to the chained method:This was fixed for C++ (#645) and PHP (#608). A probe of the other statically-typed README languages found the same gap in all of them — and in 7 of the 9 the chained method currently resolves to the wrong class (a same-named method on an unrelated type), which is a correctness bug, not just missing coverage.
The fix is the 3-part #645/#608 mechanism, per language: capture the factory's declared return type, preserve the chained receiver at extraction, then resolve and validate the chained method on the inferred type — so a wrong inference produces no edge, never a wrong one.
Foo.factory().method())New().Method(); cross-packagepkg.New()was #469)Dynamically-typed languages can't use this approach (no declared return types to read): Ruby and plain JS are out of scope; Python is partial (optional
-> Thints, see #578).Each language is validated the same way before merge: synthetic decoy + absent-method safety tests, then a real-repo A/B on a public OSS repo (node count unchanged, 0 edges lost, +N chained edges recovered, precision spot-checked).
TypeScript — evaluated and consciously skipped
The deterministic chain mechanism was fully implemented and probe-validated for TS (5 synthetic tests passed, including decoy-collision precision and a no-regression instance-chain guard), but real-repo A/B showed a net recall regression, so it was not shipped:
callsedges)Why TS is different from the other 8 languages. The mechanism resolves a chained call from the factory's declared return type. TS code leans heavily on type inference — e.g. NestJS's
Test.createTestingModule(metadata) { return new TestingModuleBuilder(...) }has no: TestingModuleBuilderannotation — so the factory's return type can't be recovered, the re-encoded chain can't resolve, and it drops the bare-name edge the existing resolver found. A bare-name fallback (the Go-fallback pattern, runaway-safe) recovered most but not all of them; ambiguous same-named methods across files (compileon bothModuleCompilerandTestingModuleBuilder) still resolved differently than baseline. The removed edges were mostly wrong (baseline mis-resolved.compile()toModuleCompiler::compile), so it's precision-positive but recall-negative — the opposite of the recall-first invariant.Zero added edges on both real repos confirms the marginal value is near-nil on real TS: method names are unique enough that the existing bare-name resolution already lands them, and the strict return-type annotations the mechanism needs are too often absent. The synthetic decoy win is real but doesn't occur in practice.
Conclusion: 8 of 9 statically-typed languages + conformance shipped (#751, #752, #753, #754, #755, #757, #760, #761, #762). TypeScript is the one language where gradual typing makes the deterministic chain mechanism a net loss, so it is intentionally not shipped.
The three typed README languages the original enumeration missed
The "9 statically-typed languages" list above was incomplete — the README also supports Objective-C, Pascal/Delphi, and Luau, all of which declare return types. Probed and addressed:
[[Foo create] doIt]→ a same-named decoy). Ported the mechanism overmessage_expression; getReturnType skips nullability qualifiers (nonnull instancetype); a class-message factory returns the receiver class by convention, so[[X alloc] init]/ singleton chains resolve. SDWebImage A/B +35 / −75 — all corrections (the −75 are wronginitmis-matches retargeted to the right class).Foo.create(): Barthen:doIt(); 5 synthetic-style checks pass) and its gap is missing-edge, so unlike TypeScript it's additive (no regression). But Luau is gradually typed and idiomatic code rarely annotates factory returns (Fusion'sExpectation.new(value)has no: Expectation;typeof(Bar)returns don't even parse), so real-repo A/B was +0 / −0 on both Fusion AND matter. It doesn't meet the real-repo-win bar, so it was left unshipped (same gradual-typing conclusion as TS, minus the regression).TFoo.GetInstance.DoIt) parse as a bareexprDot(not in the extractor'scallTypes), and even paren'd method calls (f.Regular()) produce no edge (no receiver-type tracking for Pascal locals). Building Pascal's call graph is a substantial standalone extractor effort; the chained-call port is a small part of it and a half-fix would be worse than none. Documented as a separate follow-up.Final tally: the chained static-factory mechanism is shipped for 11 languages — C++, C, PHP, Java, Kotlin, C#, Swift, Rust, Go, Scala, Dart, Objective-C (+ conformance). TypeScript and Luau were evaluated and consciously skipped (gradual typing → the mechanism is +0 / regresses on real code). Pascal is blocked on prerequisite extractor work. Full design record:
docs/design/chained-call-resolution.md.Correction: Pascal/Delphi is shipped (#791), not blocked
My earlier "blocked" assessment was wrong — I'd probed only the paren-less form (
TFoo.GetInstance.DoIt, which the extractor doesn't emit as a call). The paren'd form (TFoo.GetInstance().DoIt()) extracts fine and hit the chained-call gap like every other language (with a decoy it mis-resolved to a same-named method on an unrelated class). It's a clean port:getReturnTypefrom thetyperef; re-encode the chainedexprCall-receiver, gated on the DelphiTFoo/IFootype convention so capitalized variable chains stay bare; a constructor (TFoo.Create().…) or typecast (TFoo(x).…) resolves on the class itself. PascalCoin A/B +19 / −18 (15 of the −18 are correct class→interface retargets —GetInstance(): IAsn1OctetStringnow resolves on the declared interface). EXTRACTION_VERSION 16.Revised final tally: 13 languages shipped — C++, C, PHP, Java, Kotlin, C#, Swift, Rust, Go, Scala, Dart, Objective-C, Pascal/Delphi (+ conformance). TypeScript and Luau evaluated and skipped (gradual typing → +0 / regression on real code). The only remaining out-of-scope languages are the genuinely dynamically-typed ones (JavaScript, Ruby, Lua, Svelte, Vue, Liquid; Python is partial via #578). Full design record:
docs/design/chained-call-resolution.md.