Skip to content

fix(rust): a name written through a path is what the path names, not a same-file namesake - #2711

Merged
colbymchenry merged 2 commits into
mainfrom
claude/rust-path-same-file
Oct 20, 2026
Merged

colbymchenry merged 2 commits into
mainfrom
claude/rust-path-same-file

Conversation

@colbymchenry

Copy link
Copy Markdown
Owner

Problem

Found while triaging the A/B for #2707. A Rust name written through a path is an item of what the path names: a module (ast::Pattern, io::Error), a type (Source::Record { … }) or a crate (mio::Interest, std::ffi::OsStr).

isRustNameInScope accepted any candidate in the file the path is written in, whatever the path said. It refused a std path's candidate only when the candidate was in another file. So a path linked to a same-named item that merely shared its file:

  • clap's std::ffi::OsStr linked to its own struct OsStr. In error/mod.rs, io::Error, fmt::Error, error::Error and fmt::Result linked to its own Error and Result.
  • tokio's mio::Interest linked to its own Interest, std::time::Instant to its own Instant, and std::fs::ReadDir to its own ReadDir.
  • serde's tests' Token::Struct { … }, Token::Seq, Token::Map and the like, all variants of serde_test's Token, linked to the test files' own Struct types and variants: 317 links. serde_derive's syn::Path { … } linked to a Default::Path variant.
  • gleam's ast::Pattern linked to the enum Pattern beside it. BinaryBuilderPosition::RecordAttribute { … } linked to ErlangSourceBuilderPosition::RecordAttribute, another enum's variant of that name, and the same happened to 90 more variant literals.

Fix

All three changes are in the path branch of isRustNameInScope.

  1. A path from std, core, alloc or a crate the manifests depend on names nothing of the project's, in any file. The exceptions are a project that is that crate and a file whose own use binds the root.
  2. The same-file shortcut is gone. A candidate is what the path names when one of these holds:
    • its file is the path's module;
    • its qualified name holds the segment, as a type's variant or method does;
    • it sits in an inline mod of that name in the file (isInRustInlineModule). An inline module adds nothing to its items' qualified names, and rustfmt closes the block at the indentation it opens at.
  3. Through a module a use binds, a candidate must be in a crate that use's path can reach. This is rustUseCanReach, as a bare name already gets. Without it, removing the same-file shortcut sent tokio's io::Handle (under use crate::runtime::io) to tokio-test's io.rs Handle, a dev-dependency's. It now reaches runtime/io/driver.rs's.

Verification

A/B on real repos

Arms: main b5553e2 (#2707), and this change on it. The later main commits change C, C++, TS and Express only. Both use the same kernel, since this change is in the resolver only. Every repo was indexed fresh with codegraph init -y, dumped with scripts/dump-graph.mjs, and edges were paired by kind, source, line and column. Every changed site was triaged: the written path was read from the source, and the old and new targets were checked against it. No edge was added.

Repo Wrong links gone Moved to the path's own target Same target, new metadata
axum 16 2 0
tokio 27 10 1
clap 33 0 0
serde 328 11 15
ripgrep 20 4 4
gleam 15 124 53
dioxus 31 41 4
  • Gone (470). Every removed link went through std (std::, io::, fmt::, fs::, error::, task::), a dependency (mio::, syn::, tungstenite::, walkdir::, lsp_server::, pubgrub::, backtrace::, anyhow::, proc_macro2::, axum-extra's cookie::) or an alias of one (ts::Message, TMessage::Close), or through serde_test's Token::. In each case it reached a same-named project item that the path doesn't lead to.
    • The serde tests account for 317 of these, and serde_derive's syn:: paths for 15.
    • tokio's sys::Blocking is crate::blocking::JoinHandle re-exported under that name from io/mod.rs's inline mod sys, not io/blocking.rs's own struct Blocking.
  • Moved (192). Each now reaches the item the path names:
    • gleam's 22 ast::Pattern reach ast.rs's enum;
    • gleam's 91 enum-variant literals, like BinaryBuilderPosition::RecordAttribute { … } or BitArrayOption::Utf16, reach the variant of the enum they name;
    • dioxus's and serde's variant literals do the same;
    • tokio's driver::Handle, io::Handle, io::Driver, list::Tx and super::task::trace::Trace reach the module the path walks.
  • Same target, new metadata (77). These now link at 0.9 as a unique match, because a same-file namesake no longer competes.
  • Controls. gin, serilog, flask and express are byte-identical.
  • Cost. Three interleaved rounds per arm, median CPU seconds: tokio 30.3 → 29.8, gleam 26.7 → 27.7, serde 6.1 → 6.5. That is within run-to-run noise.

🤖 Generated with Claude Code

colbymchenry and others added 2 commits October 20, 2026 12:34
…a same-file namesake

A Rust name written through a path is an item of what the path names: a
module (`ast::Pattern`, `io::Error`), a type (`Source::Record { … }`) or a
crate (`mio::Interest`, `std::ffi::OsStr`). isRustNameInScope accepted any
candidate in the file the path is written in, whatever the path said, and
a std path only when the candidate was in another file:
- clap's `std::ffi::OsStr` linked to its own `struct OsStr`, and
  `io::Error`, `fmt::Error` and `fmt::Result` in `error/mod.rs` to its own
  `Error` and `Result`;
- tokio's `mio::Interest` linked to its own `Interest`, and
  `std::time::Instant` to its own `Instant`;
- serde's tests' `Token::Struct { … }` literals linked to their own
  `Struct`s, and serde_derive's `syn::Path { … }` to a `Default::Path`
  variant;
- gleam's `ast::Pattern` linked to the `enum Pattern` beside it, and
  `BinaryBuilderPosition::RecordAttribute { … }` to another enum's
  variant of that name.

Fix, in the path branch of isRustNameInScope:
- A path from std, core, alloc or a crate the manifests depend on names
  nothing of the project's, in any file, unless the project is that crate
  or a `use` in the file binds the root.
- The same-file shortcut is gone. A candidate is the path's when its
  file is the path's module, its qualified name holds the segment (a
  type's variant or method), or it sits in an inline `mod` of that name
  in the file (isInRustInlineModule; rustfmt closes the block at the
  indentation it opens at).
- Through a module a `use` binds, a candidate must be in a crate that
  `use`'s path can reach (rustUseCanReach, as for a bare name): tokio's
  `io::Handle` under `use crate::runtime::io` is tokio's own, not
  tokio-test's `io.rs` `Handle` (a dev-dependency).

Tests: rust-path-same-file.test.ts, 6 tests, on both engines; 5 fail
on main (the sixth is the inline-module guard), and the crate-reach one
also fails with only that rule switched off.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@colbymchenry
colbymchenry merged commit 505c355 into main Oct 20, 2026
@colbymchenry
colbymchenry deleted the claude/rust-path-same-file branch October 20, 2026 17:38
colbymchenry added a commit that referenced this pull request Oct 20, 2026
…not its indentation (#2713)

PR #2711 and PR #2702 landed a way each to tell which inline `mod` a
place in a Rust file is in. #2711's isInRustInlineModule scanned lines
upward for `mod <name> {` and trusted rustfmt's indentation to find where
the block closes. #2702's rustModBlocks scans the braces, skipping
strings, raw strings, char literals and comments, and keeps the result in
rustUsesOf. isInRustInlineModule now asks rustBlockAt for the innermost
block around the candidate, as rustDeclaredItems does, so there is one
answer to that question.

What changes:
- An item written without indentation inside `mod flat { … }` is found
  through `flat::Flat`. The indentation rule rejected it.
- `outer::Shell` is only `outer`'s own `Shell`, not one declared in a
  `mod deep` inside it. The upward scan accepted any item below the
  `mod outer {` line.
- A file whose braces don't all close says nothing, as it does for the
  `use` walk of PR #2702.

The A/B (main 818e780 vs this change; axum, tokio, clap, serde,
ripgrep, gleam, dioxus, and the gin, serilog, flask and express
controls) is byte-identical. rust-path-same-file.test.ts gains the two
cases. The `flat::Flat` one fails on main.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
danusha2345 pushed a commit to danusha2345/codegraph that referenced this pull request Oct 21, 2026
…olbymchenry#2308 follow-up for colbymchenry#2711's test)

The same change went into the rebased colbymchenry#2308 branch.

Co-Authored-By: Claude Fable 5.1 <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