Skip to content

fix(rust): a private item, or one another crate holds, no longer takes a name a file imports - #2692

Merged
colbymchenry merged 1 commit into
mainfrom
claude/rust-private-scope
Oct 20, 2026
Merged

colbymchenry merged 1 commit into
mainfrom
claude/rust-private-scope

Conversation

@colbymchenry

Copy link
Copy Markdown
Owner

Problem

Follow-up from #2687, which kept .rs out of the src/**/build/ rule. Indexing dioxus's packages/cli/src/build/ moved 158 Result type references off the crate's own alias. The cause is in Rust name matching, not in the build-folder rule:

  • src/build/patch.rs declares a private type Result<T, E = PatchError>. The crate's own alias is error.rs's pub(crate) type Result, which 21 files bring in with use crate::Result.
  • isRustNameInScope accepts any Result once a file uses one (Result is a prelude name), and the Rust extractors recorded no visibility for type aliases in either engine. Nothing said the private alias can't leave patch.rs.

#1731 already made isVisibleAcrossFiles veto a private Rust item, but only after the ranking has picked it, and the veto leaves the reference unresolved. That is the second half of the bug: wherever a private namesake outranks the item a file imports, the import never links. tokio's tests' use tokio::time::timeout calls, split(…), try_exists(…) and sleep(…) were all unresolved this way.

Fix

  1. Type aliases record their visibility in both engines. In the wasm walker (extractTypeAlias) and the kernel (extract_type_alias), private unless pub in any form, as functions and structs already do.
  2. A private module item is out of scope outside its module, ahead of the ranking. isRustItemVisibleFrom applies the Kotlin, Go, Rust: a private / unexported / non-pub definition is accepted as a cross-file name-match target #1731 veto's rule (its own module and the modules inside it, with a mod.rs/lib.rs/main.rs owning its directory) to functions, structs, enums, unions, type aliases, traits, constants and statics. The crate's own pub(crate) type Result can then win, instead of the reference going unresolved.
  3. A name a use binds is an item its path's crate can reach. Taking private items out ahead of the ranking let a runner-up from an unrelated crate win. tokio's tests' spawn(…) calls, under use tokio_test::task::spawn, went to the examples crate's pub fn spawn, which nothing depends on. So rustUsesOf now records the first segment of each use path that binds a name, and rustUseCanReach keeps a candidate only when it is in the crate that path starts from, or in a project crate that crate's [dependencies] name, transitively. A facade's re-exports therefore resolve: clap::Command is clap_builder's, and axum's Response is axum-core's.
    • crate::, self:: and super:: start from the file's own crate. A named root counts when it is the file's own crate or a dependency its own manifest lists, under any kind.
    • Dev and build dependencies can't be re-exported, so they don't extend the reach. tokio's own use crate::spawn (tokio's spawn sits in a cfg_rt! macro the index doesn't read) stays unresolved instead of going to tokio-test's, which tokio dev-depends on.
    • rustCrateGraph finds a dependency by its path: its own, or the workspace's for workspace = true. So renamed keys and crates outside the members land. It also reads every way Cargo writes a dependency: plain, inline table, dotted keys (helper.workspace = true) and [dependencies.foo] tables.
    • A file in no crate the manifests name, and a path from anything else (a module, an outside crate), are matched as before.

Test change. reference-target-kind.test.ts's sibling-crate fixture had app re-export pupil_core::ports without depending on it, which Cargo rejects. The fixture now declares the dependency, and the test still covers what it was written for: a trait reached through a re-exported module with no file of its own.

Related open PRs.

Verification

  • __tests__/rust-private-item-scope.test.ts, 7 tests, on both engines.
    • A dioxus-shaped crate (the private alias in codegen/patch.rs sorts before error.rs, as dioxus's build/ does):
      • use crate::Result and use crate::{Error, Result} reach error.rs's alias;
      • the private one stays in scope in its own file and in patch/jump.rs;
      • a private fn checksum stays in its module;
      • type aliases record public/private.
    • A tokio-shaped workspace:
      • use helper::task::spawn reaches helper's spawn, not the examples crate's, past a private namesake nearer the test. helper is a dev-dependency written helper.workspace = true.
      • use facade::Thing reaches the Thing the facade re-exports from a crate that it depends on under a package = "shared-types" rename, and that isn't a workspace member.
  • Red on main. Against origin/main's name-matcher.ts and tree-sitter.ts, 3 of the 7 fail: the visibility test, use crate::Result, and the helper spawn. Two mutants each fail the spawn test, which links examples/custom.rs exactly as tokio did: the crate rule switched off, and dotted keys not read.
  • Kernel/wasm parity. kernel-rustlang-parity passes. The new tests pass with CODEGRAPH_KERNEL=0 and with the kernel. With the kernel change reverted, the LF and CRLF torture parity tests fail (checked earlier on this branch).
  • tsc --noEmit is clean.
  • Full suite on origin/main 6c202bc plus this change (kernel built with scripts/build-kernel.sh, CODEGRAPH_KERNEL_EXPECT=1, --maxWorkers=4 --minWorkers=1): 7,637 tests, 7,557 passed, 0 failed, 80 skipped. Rebased since onto 40987d7 (fix(objc): a macro alone on its line no longer hides the declaration below it #2691, Objective-C only): tsc and the 15 Rust, Objective-C, resolution and framework test files (338 tests) pass.

A/B on real repos

Arms: origin/main 6c202bc, and this branch on it. Each arm carries its own kernel. 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.

Every changed edge was judged two ways:

  • by hand;
  • by a script that scores each changed edge against the module its use path names. The script can't judge a root-level path such as use crate::Match or pub use anyhow::Error; those were checked by hand.

No edge moved from a right target to a wrong one.

Repo Newly linked Moved to the right target Moved, still wrong Wrong links gone Same target, new metadata
tokio 194 77 57 6 293
ripgrep 0 30 0 0 76
serde 0 7 0 0 57
axum 0 91 7 3 117
clap 2 1 0 7 248
gleam (compiler-core) 27 0 0 49 30
dioxus (packages/cli) 0 0 0 12 0

tokio:

  • 127 spawn(…) calls under use tokio_test::task::spawn now link to tokio-test's spawn. 44 more, which main sent to the examples crate's spawn, move there.
  • 67 calls were unresolved before because a private namesake outranked them: 23 timeout, 10 split, 10 try_exists, 8 tokio-util write_all_vectored, 6 read, 5 sleep, 3 interval, 1 write and 1 create_dir. All now reach the function the file uses.
  • 33 more moves are corrections: 26 Handle, 4 Expiration, 2 Builder and 1 Elapsed. These are references in tokio's own crates that main sent to tokio-test's, tokio-util's or tokio-stream's namesakes, crates they don't depend on.
  • 57 move but stay wrong: 20 Handle, 19 Receiver, 9 Sender, 6 Wheel and 3 Expiration. They went to a crate the file can't reach, and now go to a reachable namesake in the wrong module. For example, tokio-stream's use tokio::sync::watch::Receiver went to tokio-util's simplex Receiver and now goes to tokio's net::unix::pipe::Receiver. The Rust framework rule takes the first in-scope namesake (see below).
  • Gone: 5 Error refs that went to tokio-util's copy_to_bytes.rs impl-associated type Error, and 1 Output that went to another test file's.

ripgrep: use crate::error::{Error, ErrorKind} in grep-regex (14) and grep-pcre2 (7) went to globset's Error, a crate neither depends on; they now go to their own error.rs. 9 Match in ignore now go to ignore's own enum Match instead of grep-matcher's struct Match.

serde: serde_core's impl SerializeSeq for Impossible<…> and its six siblings went to structs of those names in serde's private/ser.rs; serde_core depends on serde only as a dev-dependency. They now reach serde_core's own traits. The 15 synthesized trait→impl edges move with them: they now start at the traits' end and serialize_* methods instead of the structs'.

axum: 91 Response refs move from an impl-associated type Response to axum-core's pub type Response, which axum re-exports.

  • 3 are gone: 2 Item refs to cookie/private.rs's iterator type Item, and 1 Client in the mongodb example that went to the reverse-proxy example's private alias.
  • 7 refs inside axum/src/routing/ move from one impl-associated type Response to another (routing/mod.rs's), wrong before and after.

clap: impl Args for CliArgs under use clap::{…, Args, …} went to clap_bench's struct Args. It now reaches clap_builder's trait Args, and the trait→impl edges follow (the 2 new edges). 7 Err(…) calls in value_parser.rs no longer link to value_hint.rs's type Err of a FromStr impl.

gleam:

  • 22 TypeConstructor { … } constructions and 4 references now reach type_.rs's pub struct; before, docs.rs's private one outranked it and was vetoed.
  • bool() reaches prelude.rs's, through use crate::{type_::{…, bool}}.
  • Gone: 42 Err(…) calls that went to dependency.rs's type Err and 7 Item refs to io.rs's type Item, both impl-associated.

dioxus: 8 Err(…) calls and 4 Error refs no longer go to platform.rs's and proxy_ws.rs's impl-associated aliases. The crate's own Error is pub use anyhow::Error, from outside the project.

Same target, new metadata: the same target, now a unique exact match (confidence 0.9) where the Rust framework rule took it (0.7) or a ranking picked it (0.4), because the private candidates no longer compete.

Byte-identical controls: gin, excalidraw, serilog, pypa/build.

Not in this PR

  • The module a use path names, within a crate. This PR checks the crate a path reaches, not the module. The Rust framework rule still takes the first in-scope namesake, so use tokio::sync::watch::Receiver can land on another tokio Receiver: tokio's 57 still-wrong moves.
  • Associated types. An associated type (type Response = …; inside an impl) is never named bare; only Self::Response or a path reaches it. It now records private, so other modules no longer match it, which is where most of the removed wrong links came from. Inside its own module it can still match a bare name: axum's 7 routing/ refs, or an Err(…) call in the same file as an impl FromStr { type Err = … }.
  • Rust in the src/**/build/ rule (HAND_WRITTEN_SOURCE) comes next, re-measured on dioxus with this in place.

🤖 Generated with Claude Code

…s a name a file imports

A Rust type alias declared without pub recorded no visibility in either
engine, so name matching took dioxus's private type Result in
src/build/patch.rs for the Result<...> a file means through
use crate::Result. Type aliases now record it, as functions and structs
already do, and a private module item is out of scope outside its own
module ahead of the ranking. The pipeline's veto already rejected such
a pick after the ranking, which left the reference unresolved: that is
what kept tokio's timeout(...), split(...) and sleep(...) calls from
linking, since a private namesake outranked the function they import.

With the private items gone, a runner-up from an unrelated crate could
win instead: tokio's tests' spawn(...) calls, under
use tokio_test::task::spawn, went to the examples crate's pub fn spawn.
So a name a use binds is now an item of the crate its path starts from,
or of a project crate that one's [dependencies] name, in turn, which a
facade's re-exports need: clap::Command is clap_builder's. crate::,
self:: and super:: start from the file's own crate; a named root counts
when the file's own manifest lists it. Dependencies are found by path
(their own, or the workspace's for workspace = true), so renamed keys
and crates outside the members land, and dotted keys such as
helper.workspace = true are read. Dev and build dependencies are not
re-exported, so they don't count: tokio's own use crate::spawn, whose
spawn sits in a macro the index doesn't see, stays unresolved instead
of going to tokio-test's. A file in no crate the manifests name, or a
path from anything else, is matched as before.

reference-target-kind's sibling-crate fixture re-exported pupil_core
without depending on it, which Cargo rejects; it now declares it.

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