Skip to content

fix(rust): resolve self.field.method() on the field's declared type instead of a same-named method (#1585) - #1599

Open
colbymchenry wants to merge 1 commit into
fix/1588-rust-impl-type-qualificationfrom
fix/1585-rust-self-field-receivers
Open

fix(rust): resolve self.field.method() on the field's declared type instead of a same-named method (#1585)#1599
colbymchenry wants to merge 1 commit into
fix/1588-rust-impl-type-qualificationfrom
fix/1585-rust-self-field-receivers

Conversation

@colbymchenry

Copy link
Copy Markdown
Owner

Fixes #1585. Stacked on #1596 (the base branch is fix/1588-rust-impl-type-qualification; this PR's own diff is the second commit). Merge #1596 first, then retarget/merge this one.

What was wrong

impl Outer {
    pub fn run(&mut self) {
        self.inner.run();      // inner: Inner
    }
}

produced Outer::run -> Outer::run — recursion the source doesn't contain. The extractor collapsed every self.<field>.<method>() receiver to the bare method name (run), so the resolver only ever saw run and exact-matched the nearest same-named method — the calling method itself, or a method of an unrelated type. Nothing marked the edge as a guess, and no row stayed in unresolved_refs, so a consumer had no way to tell.

The same happened when the field's type isn't a project type at all (its: std::vec::IntoIter<_>self.its.next(), matcher: Regexself.matcher.is_match()): the bare next / is_match attached to whatever local method shared the name. ripgrep had 279 self-edges on main; the issue lists three sites, all of this shape.

(The issue's C++ control — "Outer::run -> Inner::run resolves correctly" — doesn't actually hold on main: inner.h is classified as C by the .h heuristic, so Inner::run never exists and the C++ repro self-edges too. That's #1592, fixed separately.)

What this does

Rust struct fields are not graph nodes, so the field's type can only come from the struct's declaration text. This follows the Go 2-hop precedent exactly (matchGoFieldChainCall, #1276), including its exclusivity rule:

  1. Extraction (TS walker + native kernel, identical, parity-tested): a call whose receiver is self.<field> keeps the owner-field shape — self.inner.run() is emitted as self.inner.run. Deeper chains (self.a.b.m()), call receivers (self.f().m()), parenthesized receivers and bare self keep the bare name, exactly as before.
  2. Resolution (matchRustSelfFieldCall): owner type = the calling method's qualified-name prefix (Outer::runOuter); the field's declared type is read from the owner struct's own declaration lines (comment-stripped, line by line — same discipline as the Go helper); the method is resolved and validated on that type by resolveMethodOnType (confidence 0.85, instance-method).
  3. Exclusive: when the field is declared with an external type, a generic parameter (T), a container that doesn't auto-deref (Option/Vec/Mutex/…), or can't be found, the ref stays unresolved — it never falls through to the bare-name strategies. That is the safe behaviour the issue asks for, and it is what Go: external receiver calls resolve to unrelated local interface methods #1276 already chose for Go.

rustFieldTypeName looks through exactly the layers Rust's method-call auto-deref looks through: references (&, &'a mut) and the owning smart pointers Box/Rc/Arc. Box<dyn Source> yields the trait, whose method node the interface-impl synthesizer then fans out to every implementation. Option<Inner> is left alone — self.inner.take() is Option's method and must not become Inner::take.

Why it stacks on #1596: the owner is taken from the method's qualified name, which for a generic/lifetime impl was the trait's name before that fix.

Measured on ripgrep (110 .rs files, #1596 build vs this branch)

#1596 this PR
nodes 4029 4029
calls self-edges 279 146 (none of the self.<field> shape remain — 116 bare-receiver, 30 other dotted)
self.<field>.m() calls resolved through a validated field type 292 (DecompressionMatcher::command -> GlobSet::matches, Parser::find_long -> FlagMap::find, Haystack::path -> DirEntry::path, …)
self.<field>.m() calls left unresolved 417 — every sampled one is a std/container method: self.commands.push, self.child.wait, self.pre.is_some, self.colors.clone, self.path_terminator.unwrap_or
calls edges total 9150 8878 (the 272 removed are the former bare-name guesses for those 417)

The issue's three sites: walk.rs:824 now resolves to IgnoreBuilder::add_custom_ignore_filename (was a self-edge); walk.rs:1195 (self.its.next, IntoIter) and globset/lib.rs:983 (self.matcher.is_match, Regex) are parked as unresolved instead of guessed.

The issue's repro gives Outer::run -> Inner::run (instance-method, confidence 0.85) on both the kernel path and CODEGRAPH_KERNEL=0.

Tests

  • __tests__/extraction.test.ts: only the single-hop self.<field>.<method>() call keeps the prefix; deeper / call / parenthesized / bare-self receivers and a local receiver are unchanged.
  • __tests__/resolution.test.ts (end-to-end, Cargo layout): the issue's repro → Outer::run -> Inner::run, no self-edge; an external field type (std::vec::IntoIter) with a local next decoy → no edge at all; Box<Inner> and &'a mut Inner resolve, Option<Inner> does not (even though Inner declares the method); a generic T field → no edge; genuine self.run() recursion keeps its self-edge; the Rust: methods of a generic impl are qualified by the trait, not by the implementing type #1588 repro's UsesFile::go / UsesBuf::go resolve to FileSource::read / BufSource::read, and a Box<dyn Source> field lands on Source::read with the synthesizer fanning out to both impls.
  • __tests__/fixtures/kernel-parity/torture.rs grows the receiver shapes; all 15 kernel parity suites pass against the rebuilt kernel (147 tests).
  • Full npm test on this branch: 189 files, 3187 passed, 9 skipped, 0 failed.

Re-index after upgrading.

🤖 Generated with Claude Code

https://claude.ai/code/session_01LxZj6W6Y1SHXwvpT3uwJpK

… instead of a same-named method (#1585)

`impl Outer { fn run(&mut self) { self.inner.run(); } }` with `inner: Inner`
produced `Outer::run -> Outer::run`. The extractor collapsed every
`self.<field>.<method>()` receiver to the bare method name, so the resolver
only saw `run` and exact-matched the nearest same-named method — the calling
method itself, or one on an unrelated type — with nothing left in
unresolved_refs to show the edge was a guess. The same happened whenever the
field's type was a std or third-party type (`self.its.next()`,
`self.matcher.is_match()`): ripgrep carried 279 such self-edges.

Rust struct fields are not graph nodes, so the field's type comes from the
struct's own declaration text — the Go 2-hop precedent (#1276), including its
exclusivity rule:

- Extraction (TS walker + kernel, parity-tested): a `self.<field>` receiver
  keeps the owner-field shape (`self.inner.run`); deeper chains, call and
  parenthesized receivers, and bare `self` keep the bare name as before.
- Resolution: the owner type is the calling method's qualified-name prefix,
  the field's type is read from the owner struct's declaration lines, and the
  method is resolved AND validated on that type (instance-method, 0.85).
  References and `Box`/`Rc`/`Arc` are looked through (method-call auto-deref);
  `Option`/`Vec`/`Mutex`-style containers are not.
- Exclusive: an external, generic, container, or undeclared field type leaves
  the ref unresolved — it never falls through to the bare-name strategies.

Stacks on #1588: the owner is the method's qualified-name prefix, which for a
generic/lifetime impl was the trait's name before that fix.

ripgrep: self-edges 279 → 146 (none of the `self.<field>` shape remain);
292 field calls now resolve through a validated type, 417 are left
unresolved — every sampled one a std/container method (`push`, `is_some`,
`clone`, `unwrap_or`); nodes unchanged. walk.rs:824 now resolves to
`IgnoreBuilder::add_custom_ignore_filename`; walk.rs:1195 (`IntoIter`) and
globset/lib.rs:983 (`Regex`) are parked instead of guessed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LxZj6W6Y1SHXwvpT3uwJpK
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant