Skip to content

fix(rust): resolve bare calls through exact use bindings - #2308

Open
danusha2345 wants to merge 1 commit into
colbymchenry:mainfrom
danusha2345:codex/rust-exact-use-bindings
Open

danusha2345 wants to merge 1 commit into
colbymchenry:mainfrom
danusha2345:codex/rust-exact-use-bindings

Conversation

@danusha2345

@danusha2345 danusha2345 commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

Problem

A bare Rust call never looked at the use that brought its name in. With

// src/util.rs
pub struct Buf;
impl Buf { pub fn take(&self) {} }
pub fn take() {}

// src/a.rs
pub fn take() {}
pub fn consume() {}

// src/caller.rs
use crate::util::take;
pub fn imported() { take(); }

// src/alias.rs
use crate::util::take as consume;
pub fn aliased() { consume(); }

imported() was linked to a::take, and aliased() to the unrelated a::consume, because name matching only sees the local name. The same happened with nested groups (use crate::{util::{take as selected}, a::take as other}), self:: / super:: imports and use inside a function body.

A call written as a module path had a related problem (this is #2259): crate::util::take(3), super::util::take(3) or util::take(3) mapped the module prefix to the right file, then took the first node named take in it. Methods were an accepted kind, so impl Buf { fn take } above the free pub fn take caught every such call. Paths that end in the caller's own file (self::take(), crate::this_module::f() from inside it) and super::Item from a submodule file were not resolved by the module walk at all.

Fix

  • A new per-file view of named use bindings (rust-use-bindings.ts), parsed once per file per resolution pass and dropped with the other import-resolver memos. It knows which use is nearest to a source position (block and module scopes, nested groups, as, self, raw identifiers), and hides comments, strings and char literals. Globs bind no name and are skipped.
  • resolveRustImportedCall runs before name matching for a bare Rust call. When a use binds the name, the call goes to the item that path names: the module's free function, a crate-root item, an enum variant through its own enum, or a tuple-struct constructor. A parameter, a let already in effect, or a nested fn of the same name still shadows the import.
  • A module path names the module's own item: never a method, never an associated type or const of an impl or trait body, never a fn-local item. A module-level item wins; with none, an item of an inline module reached through the module's re-export (#[cfg(…)] mod imp { … } plus pub use imp::*;, a common platform pattern). Two module-level items of one name are #[cfg] variants, and the first one stands for both.
  • The module walk now also follows super:: to the parent module's file, ends in the caller's own file for self::, and goes through an inline module (mod support { pub mod panic; }) to a file-backed submodule, including in Cargo's standalone tests/, examples/ and benches/ files.
  • EXTRACTION_VERSION 27 → 28, since existing Rust indexes change.

This PR includes #2259 (its source change and its whole test file) and supersedes it.

Deliberately left as before

A binding this resolver cannot follow keeps today's resolution, so nothing that resolves now goes unresolved:

  • another crate of the workspace named by its package (use my_core::partition::read_table;),
  • a pub use re-export in the module the path names,
  • a relative use inside an inline module (mod tests { use super::parse; }),
  • two imports of one name in one scope.

The exceptions are an alias whose path cannot be followed (use external::take as consume;) and an enum without that variant: name matching on the local name could only find an unrelated item, so those calls stay unresolved. Glob imports (use super::*) and #[path] modules are left alone; #1892 covers that case and should compose with this.

Tests

  • __tests__/rust-use-bindings.test.ts (17 cases): the binding parser by itself. Groups, aliases, self, raw identifiers, nearest-scope lookup, inline modules, comments and string kinds, let shadowing, UTF-16 columns and CRLF, and inline-module items told apart from impl/trait/macro bodies. It also covers impl Trait + use<'a> (precise capturing), which must not read as an import.
  • __tests__/rust-use-call.test.ts (15 cases) and __tests__/rust-module-path-call.test.ts (5 cases, fix(rust): a module path names the module's free function, not a method #2259's 4 plus an associated-type decoy). Each one indexes a real temporary Cargo project into SQLite: imported vs same-named decoys, aliases, groups, self/super, block-local imports, shadowing, enum variants, crate-root and same-file imports, a workspace crate by package name, mod tests { use super::…; }, #[cfg] variants, an inline module's re-export, Unicode columns, and refresh after edit + sync. 17 of the 20 call-level cases fail on main.
  • Rust, resolution and import tests (24 files, 552 tests) pass with the native kernel and with CODEGRAPH_KERNEL=0, as do the Rust cases of extraction.test.ts.

Real repositories (Rust edges, main → this branch)

  • tokio 1.53.1 (555 files): 14 new calls, all correct. That is 10 super::unowned(…) reaching the test helper through its inline module's re-export, two crate::sync::watch::channel(…) from inside watch.rs, and two support::mpsc_stream::… from an integration test. 4 SendError(value) constructions in mpsc moved from broadcast::SendError to the mpsc::error::SendError their use names. There are also 42 new use import edges, all checked, and 0 lost edges.
  • A GNSS simulator crate (161 files): 13 crate::gnsstime::utc_to_gps_time(…)-style calls moved from the same-named GnssTimeConverter methods to the module's free functions, plus 7 import edges. All correct.
  • Two other application projects (33 and 71 files): calls unchanged, plus 8 correct use super::… import edges.
  • Node counts are unchanged everywhere. Indexing time on tokio is within noise. Peak RSS is about 10% higher (≈1.0 GB vs ≈0.9 GB), because each resolver worker keeps the per-file binding view for the pass. The view is about 2× the Rust source it covers.

🤖 Generated with Claude Code

A bare call bound by a `use` (`use crate::util::take; take(3)`, also
through `as` aliases, nested groups, `self`/`super`/`crate` paths and
block-local imports) now resolves to the item that path names before
name matching runs. A parameter, `let` or nested fn of that name still
shadows the import. A binding the resolver cannot follow (another
workspace crate, a `pub use` re-export, a relative path inside an
inline module) keeps the previous resolution.

A module path (`crate::util::take(3)`) names the module's own item:
never a method, an associated item of an impl or trait body, or a
fn-local item. A module-level item wins over an inline module's item
reached through a re-export. Includes and supersedes colbymchenry#2259.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
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