Skip to content

Tracking: chained factory/singleton call resolution across statically-typed languages #750

@colbymchenry

Description

@colbymchenry

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions