fix(rust): resolve chained associated-function calls Foo::new().bar() (#750) (#757)

A Rust call through a chained associated function — `Foo::new().bar()`,
`Foo::with(cfg).build()` — dropped the receiver to a bare method name, which
then attached to a same-named method on an unrelated type (a wrong edge) or
didn't resolve. Ports the #645/#608 mechanism for Rust's `::` receivers:

- Part 1: capture Rust return types; `-> Self` yields the `self` marker (resolved
  to the impl's own type, like PHP), references/generics are unwrapped/reduced.
- Part 2: encode an associated-function chain (`Foo::new().bar`), gated to a
  scoped_identifier receiver so instance chains (`x.foo().bar()`) keep bare-name.
- Part 3: resolve via matchScopedCallChain (PHP's `::` resolver, generalized),
  validated by resolveMethodOnType. Wire Rust into the conformance second pass
  (matchScopedCallChain variant) so a chained method provided by a trait the type
  implements (`impl Trait for Type` → existing implements edges) resolves too.

Validated: synthetic decoy + args + Self + trait-default-conformance + absent
safety tests; full suite green (lone failure is the known-flaky #662 daemon test,
passes in isolation). Real-repo A/B vs main: clap (329 .rs) a net precision win —
**+937 added (96% correct builder methods), 622 wrong->right retargets**
(`Command::new().arg()` was mis-resolving to `ArgGroup::arg`, now `Command::arg`),
+162 net unique edges; the pure-drops are largely wrong bare-name edges the fix
correctly stops emitting. tokio-rs/bytes 0/0 (no regression). Known limit: the
single-hop mechanism re-encodes only the first hop of a chain (deeper hops keep
bare-name) — clap's unusually deep builder chains are partly covered.
EXTRACTION_VERSION 10 -> 11.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-06-09 02:41:59 -04:00
committed by GitHub
co-authored by Claude Opus 4.8
parent 7c7f0dd56f
commit 5805f01957
7 changed files with 163 additions and 28 deletions
+16 -13
View File
@@ -570,15 +570,17 @@ export function matchCppCallChain(
}
/**
* Resolve a PHP fluent static-factory chain whose receiver is a static call —
* `Cls::for($x)->method()`, encoded by the extractor as `Cls::for().method`
* (#608, the per-credential Laravel client idiom). The receiver's type is what
* `Cls::for` returns: a `: self` / `: static` resolves to `Cls` itself, a
* concrete `: Type` to that type. The outer method is then resolved and
* VALIDATED on it (resolveMethodOnType requires the method to exist), so a
* wrong inference yields no edge rather than a wrong one.
* Resolve a `::`-scoped factory chain whose receiver is a scoped/static call —
* PHP `Cls::for($x)->method()` (#608, the per-credential Laravel client idiom) or
* Rust `Foo::new().bar()` (an associated-function call) — both encoded by the
* extractor as `Cls::factory().method`. The receiver's type is what `Cls::factory`
* returns: a `self` marker (PHP `: self`/`: static`, Rust `-> Self`) resolves to
* the factory's own type, a concrete return type to that type. The outer method is
* then resolved and VALIDATED on it (resolveMethodOnType requires the method to
* exist on the type or a supertype it conforms to), so a wrong inference yields no
* edge rather than a wrong one. Shared by the `::`-receiver languages (PHP, Rust).
*/
export function matchPhpCallChain(
export function matchScopedCallChain(
ref: UnresolvedRef,
context: ResolutionContext,
): ResolvedRef | null {
@@ -1080,11 +1082,12 @@ export function matchReference(
if (result) return result;
}
// 1c. PHP fluent static-factory chain — `Cls::for($x)->method()` encoded as
// `Cls::for().method` (#608). Same idea as 1b: the receiver's type is the
// factory's `: self` / `: Type` return.
if (ref.language === 'php') {
result = matchPhpCallChain(ref, context);
// 1c. `::`-scoped factory chain — PHP `Cls::for($x)->method()` (#608) or Rust
// `Foo::new().bar()`, both encoded as `Cls::factory().method`. The receiver's
// type is the factory's `self` (PHP `: self`/`: static`, Rust `-> Self`) or
// concrete return type.
if (ref.language === 'php' || ref.language === 'rust') {
result = matchScopedCallChain(ref, context);
if (result) return result;
}