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
Conversation
… 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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
produced
Outer::run -> Outer::run— recursion the source doesn't contain. The extractor collapsed everyself.<field>.<method>()receiver to the bare method name (run), so the resolver only ever sawrunand 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 inunresolved_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: Regex→self.matcher.is_match()): the barenext/is_matchattached to whatever local method shared the name. ripgrep had 279 self-edges onmain; the issue lists three sites, all of this shape.(The issue's C++ control — "
Outer::run -> Inner::runresolves correctly" — doesn't actually hold onmain:inner.his classified as C by the.hheuristic, soInner::runnever 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:self.<field>keeps the owner-field shape —self.inner.run()is emitted asself.inner.run. Deeper chains (self.a.b.m()), call receivers (self.f().m()), parenthesized receivers and bareselfkeep the bare name, exactly as before.matchRustSelfFieldCall): owner type = the calling method's qualified-name prefix (Outer::run→Outer); 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 byresolveMethodOnType(confidence 0.85,instance-method).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.rustFieldTypeNamelooks through exactly the layers Rust's method-call auto-deref looks through: references (&,&'a mut) and the owning smart pointersBox/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 becomeInner::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
.rsfiles, #1596 build vs this branch)callsself-edgesself.<field>shape remain — 116 bare-receiver, 30 other dotted)self.<field>.m()calls resolved through a validated field typeDecompressionMatcher::command -> GlobSet::matches,Parser::find_long -> FlagMap::find,Haystack::path -> DirEntry::path, …)self.<field>.m()calls left unresolvedself.commands.push,self.child.wait,self.pre.is_some,self.colors.clone,self.path_terminator.unwrap_orcallsedges totalThe issue's three sites:
walk.rs:824now resolves toIgnoreBuilder::add_custom_ignore_filename(was a self-edge);walk.rs:1195(self.its.next,IntoIter) andglobset/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 andCODEGRAPH_KERNEL=0.Tests
__tests__/extraction.test.ts: only the single-hopself.<field>.<method>()call keeps the prefix; deeper / call / parenthesized / bare-selfreceivers 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 localnextdecoy → no edge at all;Box<Inner>and&'a mut Innerresolve,Option<Inner>does not (even thoughInnerdeclares the method); a genericTfield → no edge; genuineself.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'sUsesFile::go/UsesBuf::goresolve toFileSource::read/BufSource::read, and aBox<dyn Source>field lands onSource::readwith the synthesizer fanning out to both impls.__tests__/fixtures/kernel-parity/torture.rsgrows the receiver shapes; all 15 kernel parity suites pass against the rebuilt kernel (147 tests).npm teston this branch: 189 files, 3187 passed, 9 skipped, 0 failed.Re-index after upgrading.
🤖 Generated with Claude Code
https://claude.ai/code/session_01LxZj6W6Y1SHXwvpT3uwJpK