Skip to content

fix(rust): a method call resolves by the type its receiver is written with, never by the method's name alone (#2614) - #2625

Closed
danusha2345 wants to merge 1 commit into
colbymchenry:mainfrom
danusha2345:fix/2614-rust-written-receiver-type
Closed

danusha2345 wants to merge 1 commit into
colbymchenry:mainfrom
danusha2345:fix/2614-rust-written-receiver-type

Conversation

@danusha2345

Copy link
Copy Markdown
Contributor

Fixes #2614.

What happens

A Rust method call whose receiver's type stands in the source was still matched by the method's name. The reporter's four shapes, all reproduced on main (c29caf2) with the crate in the issue:

call receiver's written type resolved to
names.contains(&k) names: &[&str] Obj::contains (instance-method 0.7)
vis.map.get("a") map: HashMap<String, i32> Ext::get (exact-match 0.9)
J::from(i) use serde_json::Value as J Fail::from (instance-method 0.7)
Xyz { x: 1.0 }.to_val() the literal's own type Stat::to_val (exact-match 0.4)

The first three are a same-named method of an unrelated project type; the fourth is a namesake of the right one. #1585 (self.field) and #1861 (self) read the written type for receivers on self; a parameter, a local, a field read on one, a struct literal and a use alias did not.

The change

The receiver's written type decides, read from the source and never guessed from the receiver's name (rustWrittenReceiverType):

  • a struct literal names its type (rustGoReceiverName now reads Xyz { … } as Xyz, as it already read Go's (JSON{data}));
  • a parameter or local by its declaration in the enclosing function — the annotation, let x: T, or a let x = T { … } / T::new(…) / T::default() that is the whole initializer (let matches = Command::new("x").arg(…).get_matches(); holds what the chain returns, so it is not read) — the last declaration before the call;
  • a field read on either, vis.map, by the field's declared type in the struct;
  • Self by the impl's type.

A project struct or enum calls its own method (resolveMethodOnType, 0.9) or nothing. A slice, tuple, array, raw pointer or primitive; a name a use binds from the standard library or a dependency (HashMap, J); or a name no Rust type of the project declares is outside the project and calls nothing of the project's — and so does a Type::method path through such a use alias. A trait (impl IntoResettable<T>), a type alias, a generic parameter, a closure's argument, a for binding or a value the declaration does not type stays unknown and resolves exactly as before.

Three sites consult it: matchMethodCall for the dotted (names.contains) and path (J::from) shapes, and the candidate filters of matchByExactName and matchFuzzy for the bare shape the extractor emits for a chained receiver (vis.map.get("a"), Xyz { x: 1.0 }.to_val() arrive as bare get / to_val).

Real-repo check

Indexed ripgrep and clap with main and with this branch, and diffed the calls edges to Rust methods and functions:

repo edges on main removed added
ripgrep 6,367 111 134
clap 14,583 79 52

Every removed edge checked was a method of the wrong type, and the added one is the written type's: glob_builder.build() on a GlobSetBuilder went to GlobBuilder::build and now to GlobSetBuilder::build; builder.add(…) on a GitignoreBuilder to GlobSetBuilder::add, now GitignoreBuilder::add; sunk.bytes() on a &SinkContext to Sunk::bytes, now SinkContext::bytes; seen.contains(…) on a HashSet to ByteSet::contains, OsStr::new(…) (std) to StyledStr::new, f.write_str(…) on a fmt::Formatter to StyledStr::write_str, raw.next() on a clap_lex::RawArgs to RawValues::next — now nothing, or the right type. The remaining removals are std iterator methods (filter, count, finish, as_bytes) that had landed on project namesakes.

Two shapes stay unresolved where they used to guess: an index into a slice of a project type (matches[i].end(), the slice is outside) and a Deref through a project wrapper (Sp<Kind>'s kind.name()).

Tests

__tests__/rust-receiver-written-type.test.ts indexes the issue's crate (with its Cargo.toml, so serde_json is a dependency) plus a project type on a parameter, a local and a field read, a Vec field, and an untyped closure argument:

  • slice_has, map_get, to_json, vec_field call nothing of the project's;
  • centroid calls Xyz::to_val;
  • typed_param, typed_local, through_field call Obj::contains; main keeps Obj::contains, Ext::get, Stat::to_val;
  • the closure's it.contains(…) resolves as before.

The first two cases fail on main. All ten Rust suites, resolution.test.ts and cross-language-resolution.test.ts pass (284 tests).

🤖 Generated with Claude Code

… with, never by the method's name alone (colbymchenry#2614)

A Rust method call whose receiver's type stood in the source was still
matched by the method's name: `names.contains(&k)` on a `&[&str]` parameter
went to `Obj::contains`, `vis.map.get("a")` on a `HashMap` field to
`Ext::get`, `J::from(i)` under `use serde_json::Value as J` to a local
`impl From<io::Error> for Fail`, and `Xyz { x: 1.0 }.to_val()` to
`Stat::to_val` — a same-named method of another type each time.

The receiver's written type now decides, read from the source and never
guessed from the receiver's name: a struct literal names its type; a
parameter or local by its declaration — the annotation, `let x: T`, or a
`let x = T { … }` / `T::new(…)` / `T::default()` that is the whole
initializer — the last one before the call; a field read on either by the
field's declared type; `Self` by the impl's type. A project struct or enum
calls its own method or nothing. A slice, tuple, array, primitive, a name a
`use` binds from the standard library or a dependency, or a name no Rust type
of the project declares is outside the project and calls nothing of the
project's; so does a path through such a `use` alias. A trait, an alias, a
generic parameter, a closure's argument or a value the declaration does not
type stays unknown and resolves as before.

The bare-name strategies see the same answer: `vis.map.get("a")` and
`Xyz { x: 1.0 }.to_val()` arrive as bare `get` / `to_val`, and the struct
literal now names its type to the receiver reader.

On ripgrep the change removes 111 call edges and adds 134, on clap removes 79
and adds 52; every removed edge checked was a method of the wrong type
(`glob_builder.build()` on a `GlobSetBuilder` went to `GlobBuilder::build`,
`seen.contains(…)` on a `HashSet` to `ByteSet::contains`, `OsStr::new(…)` to
`StyledStr::new`), and the added ones are the written type's
(`GlobSetBuilder::build`, `GitignoreBuilder::add`, `SinkContext::bytes`).
Not followed: an index into a slice of a project type (`matches[i].end()`)
and a `Deref` through a project wrapper (`Sp<Kind>`) — both resolve to
nothing where they used to guess.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@danusha2345

Copy link
Copy Markdown
Contributor Author

Closing: main fixed #2614 in 8fdc8a1 (#2831), built on the typed-receiver reading from #2715–#2719, with this PR's struct-literal and use-alias cases folded in (thanks for the note in its description). This PR's test (rust-receiver-written-type.test.ts) passes 4/4 on main 16a8e77.

danusha2345 pushed a commit to danusha2345/codegraph that referenced this pull request Oct 23, 2026
…mchenry#2834; superseded colbymchenry#2260/colbymchenry#2373/colbymchenry#2461/colbymchenry#2625/colbymchenry#2704/colbymchenry#2791 yield to main's versions)

Conflicts: main's versions taken where our PRs were re-implemented upstream
(static/instance override colbymchenry#2813, workspace package entries colbymchenry#2799, daemon lock
colbymchenry#2819, override-overload pairing colbymchenry#2820, Rust typed receivers colbymchenry#2831, explore
long-query perf colbymchenry#2804); both sides kept in resolution/index.ts and
name-matcher.ts (our Kotlin `kotlinCallResultType` renamed to
`kotlinQualifiedCallResultType` beside main's colbymchenry#2822 helper); the fuzzy search
keeps our hard-filter scope on top of main's memoized name list. Tests of the
superseded PRs dropped.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
danusha2345 pushed a commit to danusha2345/codegraph that referenced this pull request Oct 23, 2026
…per call in a class expression is not the enclosing class's member

The merge took main's colbymchenry#2831 for the conflicting Rust hunks but 167 lines of
the closed colbymchenry#2625 (rustWrittenReceiverType) auto-merged beside them and ruled
out main's own `Sink::flush` case; removed. With main's js-super-calls (colbymchenry#2818),
colbymchenry#1952's enclosing-scope pick read a class expression's `super.run()` as the
enclosing class's and sent it to its parent: a call written on `super` now
skips the pick (the same change goes to PR colbymchenry#1952). js-class-expressions pins
the 0.9 strong match colbymchenry#1952 gives `this.run()` in a class expression.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
danusha2345 pushed a commit to danusha2345/codegraph that referenced this pull request Oct 23, 2026
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.

Rust: method call on std/external or struct-literal receiver links to a same-named project method

1 participant