Skip to content

fix(rust): a Self:: call is the function of the type whose impl it is in - #2700

Merged
colbymchenry merged 1 commit into
mainfrom
claude/rust-self-path-calls
Oct 20, 2026
Merged

colbymchenry merged 1 commit into
mainfrom
claude/rust-self-path-calls

Conversation

@colbymchenry

Copy link
Copy Markdown
Owner

Problem

Found in #2695's A/B: once dioxus's build/android.rs was indexed, Self::current() inside impl Workspace stopped linking to Workspace::current. The new file adds AndroidTools::current as a namesake.

A Rust Self::x(…) call reaches matchMethodCall as a call on a receiver named Self. No type has that name, so it falls to the name-only guess (Strategy 3), which links only when nothing else shares the method's name.

A census of every Self::x(…) call across tokio, ripgrep, serde, axum, clap, gleam and dioxus found 0 wrong links. It did find many lost ones:

Repo Resolved (all right) Unresolved, though the index holds the impl type's x Unresolved, no such x (a trait default, std, a macro)
clap 10 120 41
tokio 31 56 7
axum 12 55 6
gleam (compiler-core) 14 26 11
serde 1 18 1
dioxus (packages/cli) 73 9 12
ripgrep 2 3 0

The unresolved ones include Self::new(…) inside impl Error while Error::new sits in the same file, Self::Text(…) inside impl Message, and Self::from_std(…).

Fix

matchRustSelfPath (name-matcher), tried by matchMethodCall for a Rust Self::x call before any guess:

  • Self is the type of the impl (or trait) the call is written in (rustSelfType, reading the header with fix(rust): a call reaches a project impl on a std type only as Rust would pick it #2697's parseRustImplHeader). The call reaches that type's x: a function, a method, or a variant for Self::Text(…), matching how an explicit Message::Text(…) already resolves. The type's own file comes first, then the nearest.
  • As Rust looks it up:
    • the type's inherent function comes first, with #[cfg] twins standing for one another;
    • a trait impl's is taken only when it is the only one;
    • several trait impls of one name are left unresolved: clap's Self::from(…) among OsStr's 13 From impls, or tokio's SpawnLocation's two.
  • Inside a trait impl, fn local_addr(&self) { Self::local_addr(self) } hands the call on to the type's own function, so it never links to the method it is written in. In the type's own impl, that shape is a recursion and links to itself as before, as in gleam's resolve_type_var_id.
  • A type from outside the project has only the project's impls written beside the call; another type of its name elsewhere is not it. Outside means a std or dependency path, an outside use, or a std type the project doesn't declare, like axum's impl Listener for tokio::net::TcpListener.
  • A trait's own method is the fallback only where nothing could shadow it (not for an outside type, and not when handing on to the type's function of the same name). It is also only one the trait's body declares: clap's impl<T: Parser> Parser for Box<T> calls Self::new, which is Box::new, not the new of a struct also named Parser.
  • When the type has no such x, the call stays unresolved instead of falling to a guess at another type's.

Verification

  • __tests__/rust-self-path-calls.test.ts, 5 tests. Every name involved also has a namesake elsewhere, so a guess by name can't pick it:
    • Self::new in impl Error and dioxus's Self::current in impl Workspace;
    • a recursion, and Self::Text(…);
    • Self::new() in impl Store for Config, and Self::open() in a trait's default method;
    • clap's Box<T> shape;
    • a trait impl delegating to the type's inherent function, and one on std's TcpListener that must link nothing.
  • Red on main. Against origin/main's name-matcher.ts, 4 of the 5 tests fail. The one that passes there is the Box<T> guard, which main also leaves unlinked.
  • Mutants. Dropping the caller exclusion, the outside-type restriction, the trait-fallback guard or the trait-membership check each fails a test.
  • tsc --noEmit is clean.
  • Full suite on origin/main 2043152 plus this change (kernel built with scripts/build-kernel.sh, CODEGRAPH_KERNEL_EXPECT=1, --maxWorkers=4 --minWorkers=1): 7,671 tests, 7,591 passed, 0 failed, 80 skipped.

A/B on real repos

Arms: origin/main 60b2f1e (#2697 merged), and this change on it, with one kernel. Main has since gained only #2698, a C++ fix. Each repo was indexed fresh with codegraph init -y, dumped with scripts/dump-graph.mjs, and edges were paired by kind, source, line, column and name.

Repo Self:: calls newly linked Same target, new metadata
clap 105 10
tokio 55 31
axum 51 12
gleam (compiler-core) 26 14
serde 18 1
dioxus (packages/cli) 9 (incl. Workspace::reload → Workspace::current) 73
ripgrep 3 2
  • Nothing moved and nothing was removed.
  • A re-run of the census finds no wrong Self:: link and no self-loop among the new edges. A sample read by hand: Error::argument_conflict → Error::new, BoolValueParser::default → BoolValueParser::new, StreamMap::from_iter → StreamMap::with_capacity, KeepAlive::default → KeepAlive::new and EventFlags::from_bits.
  • The "same target, new metadata" edges are the calls the name guess already got right, now resolved through their impl's type (0.9, qualified-name).
  • 20 calls are left unresolved on purpose:
    • axum's 4 delegations to tokio's own listener methods;
    • Self::from(…) among several From impls: clap 15 and tokio 1.
  • Byte-identical: tauri-plugin and the controls (gin, excalidraw, serilog, pypa/build).

🤖 Generated with Claude Code

A Rust Self::x(...) call reached the resolver as a method call on a
receiver named Self, which names no type. So it fell to a guess by the
name alone, and linked only when nothing else in the project shared
that name. clap's Self::new(...) calls in impl Error, ValueParser and
the like stayed unresolved, and so did tokio's, axum's and serde's.
dioxus's Self::current() stopped resolving once its Android tooling's
AndroidTools::current was indexed.

Self is now the type of the impl (or trait) the call is written in. The
call reaches that type's function, or its variant for Self::Text(...),
in the type's own file first, then the nearest. As in Rust, the type's
inherent function comes first, with cfg twins standing for one another.
A trait impl's function is taken only when it is the only one, and
several trait impls of one name (From<A>, From<B>) are left unresolved.
Inside a trait impl, a call to the function's own name hands on to the
type's own function, so it never links to itself there. In the type's
own impl such a call is a recursion and stays one. A trait's own method
is the fallback only where nothing could shadow it.

A type from outside the project, such as axum's impl Listener for
tokio::net::TcpListener, has only the impls written beside the call.
When the type has no such function, the call stays unresolved instead
of going to another type's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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