fix(rust): a name a use brings in is the item of the module its path walks (#2702)

tokio's signal/mod.rs writes `use crate::sync::watch::Receiver;` and
tokio-stream `use tokio::sync::watch::Receiver;`, and both linked their
`Receiver<()>` to the `Receiver` of net/unix/pipe.rs. Every `Receiver` in
the crate read as in scope once the file imported one, and the first won.

isRustNameInScope now walks the modules a `use` path names, from the module
the `use` is written in: crate::, self::, super::, a module declared there
(2018 paths), the crate's own name, or a project crate its manifest depends
on. It looks the item up there as Rust does: declared in that module (not
inside a function or an inline module), else brought in by the module's own
`use` of that name, macro-wrapped ones included (tokio's `cfg_sync! { pub use
mutex::{Mutex, ...}; }`), else by its globs. A module declared inline, like
`pub mod futures { ... }`, is found by its braces. A candidate the path does
not name is out of scope, so every strategy, the framework name rule
included, lands on the item the path names.

What the walk can't follow says nothing, and name matching answers as
before: a module with no file, one a path attribute puts elsewhere, a glob
over an enum, a file whose braces don't balance. A module that holds no
such item the index knows, because a macro declares it or another crate
does, leaves nothing in scope. A `use` of the name that yields no item
doesn't hide the globs: clap's `pub use clap_derive::ValueEnum` is a derive
macro, and the trait comes in through `pub use clap_builder::*`.

A/B with fresh indexes, main 5530676f against this branch, every changed
edge triaged: tokio 233 links move to the item the path names and 78 wrong
links go (the real items are declared inside cfg_*! macros, or are std's
io::Result); axum 6 move; clap 20; ripgrep 49 move and 2 go (termcolor's
ColorChoice); gleam 404 move; dioxus 58 move. No right link is lost, and
527 more keep their target at a higher confidence. serde changes only
confidences; express, gin, flask and leveldb are byte-identical. The new
lookups cost about 0.2 s of CPU per full index of tokio or gleam.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-10-20 17:36:35 +00:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 5a7a4347a6
commit 5e72a43b3c
3 changed files with 664 additions and 3 deletions
+1
View File
@@ -229,6 +229,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- In Rust, a method called through `self: Pin<&mut Self>`, `self: Box<Self>`, `self: Rc<Self>` or `self: Arc<Self>`, or on a value declared `Pin<&mut …>`, now links the way Rust looks it up: to the type's own method, like `self.poll_flush(cx)` inside tokio's `impl AsyncWrite for File`, rather than to a catch-all `impl AsyncWrite for Pin<P>`, while `Pin`'s own methods, like `self.get_mut()`, stay unlinked. A type's own method now also comes before a trait's of the same name, so tokio's `Arc::from_raw(…)` calls are the standard library's rather than its internal `Link` trait's, and a method called on a tuple, like serde's `(self.ip(), self.port()).serialize(…)`, no longer links to an unrelated type's. Before, most calls made through such a `self` linked to nothing. Re-index Rust projects after upgrading.
- In Rust, a method called on what `.as_mut()` or `.as_ref()` hands back now links by the type its receiver is declared with: `self.delay.as_mut().reset(…)` on a `Pin<Box<Sleep>>` field reaches `Sleep::reset`, a field pin-project hands out reaches its own type's method, and a `Box` or `NonNull` lends what it holds. A method on an `Option` reached this way, like `self.child.as_mut().map(…)`, or on a generic type no longer links to an unrelated project method that shares its name. Before, these calls were matched by name alone, so tokio's `self.delay.as_mut().reset(…)` linked to the calling `Interval::reset`, and dioxus's `Option` calls to its own `map` and `filter` helpers. Re-index Rust projects after upgrading.
- In Rust, a type alias, struct, enum or function declared without `pub` now links only from its own module and the modules inside it, and a name a `use` brings in from one of your workspace's crates, like `use tokio_test::task::spawn` or `use crate::error::Error`, links only to an item of that crate or of a crate it depends on. Before, a file's `use crate::Result` could link its `Result<…>` types to another module's private `type Result`, as in dioxus's CLI, and a call to an imported function could link to another crate's function of the same name, such as tokio's test `spawn(…)` calls to one in its examples, or to nothing because a private function of that name was in the way. Re-index Rust projects after upgrading.
- In Rust, a name a `use` brings in, like `use crate::sync::watch::Receiver` or `use tokio::task::yield_now`, now links to the item of that name in the module its path leads to, found the way Rust finds it: declared there, in a module declared inline with `mod name { … }`, or passed on by that module's own `pub use`, including one written inside a macro like tokio's `cfg_sync! { … }`. Before, any item of that name in the crate could take it, so tokio's `Receiver<()>` linked to the `Receiver` of `net/unix/pipe.rs`, `crate::runtime::Handle` to the runtime driver's `Handle`, and `crate::runtime::task::JoinHandle` to a test mock. A name whose item is declared by a macro, which the index doesn't see, now links to nothing rather than to a same-named item elsewhere. Re-index Rust projects after upgrading.
- In Rust, a name written on its own, like an `Err(…)` call or a `Response` return type, or through a module, like `io::Error` or `use crate::de::Error`, no longer links to a `type` or `const` declared inside an `impl`, such as a `FromStr` impl's `type Err` or a `Service` impl's `type Response`: only a path like `Self::Err` names those, and they are now listed under their type, as `ValueHint::Err`. Before, serde's and ripgrep's `Ok(…)` and `Err(…)` calls linked to a serializer's or parser's `type Ok` or `type Err`, axum's handlers' `Response` to a `Service` impl's, and ripgrep's `impl Captures for RegexCaptures` to another impl's `type Captures` instead of the `Captures` trait. Re-index Rust projects after upgrading.
- In C++, every name a declaration lists now has the declaration's type, like `strings_found` in google/breakpad's `ByteBuffer symbols_found, strings_found;` or `data_` in a class's `Table *index_, *data_;`, so calls on it link to that class's methods. Before, only the first name had its type read, and a call on another name linked to nothing or to the class of an earlier variable with the same name.
- In C++, a call on a parameter, a local lambda or functor, a lambda's parameter or a range-`for` variable no longer links to a function or method elsewhere that only shares its name, like protocolbuffers/protobuf's code generators calling `format(…)` on their `Formatter& format` parameter, which linked to the conformance suite's `TestResult::format`, or facebook/rocksdb's tests calling a local `auto key = [](int i) {…};`. C++ finds the local first, so that is what the call calls.
+230
View File
@@ -0,0 +1,230 @@
/**
* A name a Rust `use` binds is the item its path names: the item of that name
* in the module the path walks to. tokio's `signal/mod.rs` writes `use
* crate::sync::watch::Receiver;` and tokio-stream `use
* tokio::sync::watch::Receiver;`, and both linked their `Receiver<()>` to the
* `Receiver` of `net/unix/pipe.rs`: every `Receiver` in the crate read as in
* scope, because the file imports a `Receiver`, and the first one won. The
* path's module now decides — its own items, then what it re-exports (`pub
* use mutex::{Mutex, …}`, macro-wrapped as tokio's `cfg_sync! { … }` are),
* and the modules declared inline (`pub mod futures { … }`). Name matching
* still answers when the path's module has no file to read.
*/
import { describe, it, expect, afterAll, beforeAll } from 'vitest';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { CodeGraph } from '../src';
/** Writes `files` into a fresh temp dir and indexes it. */
async function indexProject(prefix: string, files: Record<string, string>): Promise<{ root: string; cg: CodeGraph }> {
const root = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
for (const [rel, content] of Object.entries(files)) {
fs.mkdirSync(path.dirname(path.join(root, rel)), { recursive: true });
fs.writeFileSync(path.join(root, rel), content);
}
return { root, cg: await CodeGraph.init(root, { index: true }) };
}
/** What `name`, written in `file`, links to, as `file@line` of each target, sorted. */
const targetsOf = (cg: CodeGraph, file: string, name: string, kinds: string[]): string[] => {
const out: string[] = [];
for (const node of cg.getNodesInFile(file)) {
for (const edge of cg.getOutgoingEdges(node.id)) {
if (!kinds.includes(edge.kind)) continue;
const target = cg.getNode(edge.target)!;
if (target.name === name) out.push(`${target.filePath}@${target.startLine}`);
}
}
return [...new Set(out)].sort();
};
describe('a name a Rust `use` binds, by the module its path walks', () => {
let root = '';
let cg: CodeGraph;
beforeAll(async () => {
({ root, cg } = await indexProject('cg-rust-use-path-', {
'Cargo.toml': '[workspace]\nmembers = ["tokio", "tokio-stream"]\n',
'tokio/Cargo.toml': '[package]\nname = "tokio"\nversion = "0.1.0"\n',
'tokio/src/lib.rs': 'pub mod fs;\npub mod net;\npub mod signal;\npub mod sync;\nmod loom;\n',
// Namesakes in modules that sort ahead of `sync`.
'tokio/src/fs/mod.rs': 'pub struct Ready;\n\npub struct Notified;\n',
'tokio/src/loom/mod.rs': 'pub(crate) struct Mutex<T>(T);\n',
'tokio/src/net/mod.rs': 'pub mod unix;\n',
'tokio/src/net/unix/mod.rs': 'pub mod pipe;\n',
'tokio/src/net/unix/pipe.rs': [
'pub struct Receiver {',
' fd: i32,',
'}',
'',
'pub struct Sender {',
' fd: i32,',
'}',
'',
'pub fn channel() -> Receiver {',
' Receiver { fd: 0 }',
'}',
'',
].join('\n'),
'tokio/src/sync/mod.rs': [
'macro_rules! cfg_sync {',
' ($($item:item)*) => { $( $item )* }',
'}',
'',
'cfg_sync! {',
' mod mutex;',
' pub use mutex::{Mutex, MutexGuard};',
'',
' pub(crate) mod notify;',
'',
' pub mod broadcast;',
' pub mod watch;',
'}',
'',
'pub mod futures {',
' pub use super::notify::Notified;',
'',
' pub struct Ready;',
'}',
'',
].join('\n'),
'tokio/src/sync/mutex.rs': 'pub struct Mutex<T> {\n value: T,\n}\n\npub struct MutexGuard;\n',
'tokio/src/sync/notify.rs': 'pub struct Notified;\n',
'tokio/src/sync/broadcast.rs': 'use super::Mutex;\n\npub fn tail(lock: Mutex<u64>) {}\n',
'tokio/src/sync/watch.rs': [
'macro_rules! sender {',
' () => { pub struct Sender; }',
'}',
'',
'sender!();',
'',
'pub struct Receiver<T> {',
' shared: T,',
'}',
'',
'pub fn channel<T>(init: T) -> Receiver<T> {',
' Receiver { shared: init }',
'}',
'',
].join('\n'),
'tokio/src/signal/mod.rs': [
'use crate::sync::futures::{Notified, Ready};',
'use crate::sync::watch::{channel, Receiver, Sender};',
'use crate::sync::Mutex;',
'use crate::gen::Thing;',
'',
'pub fn make(lock: Mutex<u8>) -> Receiver<()> {',
' channel(())',
'}',
'',
'pub fn send(tx: Sender) {}',
'',
'pub fn wait(n: Notified, r: Ready, t: Thing) {}',
'',
].join('\n'),
// Generated at build time: the path's module has no file to read.
'tokio/src/signal/thing.rs': 'pub struct Thing;\n',
'tokio/tests/sync_watch.rs': 'use tokio::sync::watch::Receiver;\n\nfn rx(r: Receiver<u8>) -> Receiver<u8> {\n r\n}\n',
'tokio-stream/Cargo.toml': '[package]\nname = "tokio-stream"\nversion = "0.1.0"\n\n[dependencies]\ntokio = { path = "../tokio" }\n',
'tokio-stream/src/lib.rs': 'pub mod wrappers;\n',
'tokio-stream/src/wrappers/mod.rs': 'mod watch;\npub use watch::stream;\n',
'tokio-stream/src/wrappers/watch.rs': 'use tokio::sync::watch::Receiver;\n\npub fn stream<T>(rx: Receiver<T>) -> Receiver<T> {\n rx\n}\n',
}));
}, 60_000);
afterAll(() => {
cg?.close();
if (root) fs.rmSync(root, { recursive: true, force: true });
});
it('`use crate::sync::watch::Receiver` is watch.rs\'s Receiver, not a namesake elsewhere in the crate', () => {
expect(targetsOf(cg, 'tokio/src/signal/mod.rs', 'Receiver', ['references'])).toEqual(['tokio/src/sync/watch.rs@7']);
});
it('so is `use tokio::sync::watch::Receiver`, from tokio\'s own tests and from a crate that depends on it', () => {
expect(targetsOf(cg, 'tokio/tests/sync_watch.rs', 'Receiver', ['references'])).toEqual(['tokio/src/sync/watch.rs@7']);
expect(targetsOf(cg, 'tokio-stream/src/wrappers/watch.rs', 'Receiver', ['references'])).toEqual(['tokio/src/sync/watch.rs@7']);
});
it('follows what the module re-exports, inside a macro too', () => {
expect(targetsOf(cg, 'tokio/src/signal/mod.rs', 'Mutex', ['references'])).toEqual(['tokio/src/sync/mutex.rs@1']);
});
it('`use super::Mutex` starts from the parent module', () => {
expect(targetsOf(cg, 'tokio/src/sync/broadcast.rs', 'Mutex', ['references'])).toEqual(['tokio/src/sync/mutex.rs@1']);
});
it('reaches into a module declared inline, and what it re-exports', () => {
expect(targetsOf(cg, 'tokio/src/signal/mod.rs', 'Ready', ['references'])).toEqual(['tokio/src/sync/mod.rs@18']);
expect(targetsOf(cg, 'tokio/src/signal/mod.rs', 'Notified', ['references'])).toEqual(['tokio/src/sync/notify.rs@1']);
});
it('links a call the same way', () => {
expect(targetsOf(cg, 'tokio/src/signal/mod.rs', 'channel', ['calls'])).toEqual(['tokio/src/sync/watch.rs@11']);
});
it('links nothing when the module holds no such item the index knows', () => {
// watch.rs's `Sender` is a macro's: no node, and pipe.rs's is not it.
expect(targetsOf(cg, 'tokio/src/signal/mod.rs', 'Sender', ['references'])).toEqual([]);
});
it('falls back to name matching when the path\'s module has no file', () => {
expect(targetsOf(cg, 'tokio/src/signal/mod.rs', 'Thing', ['references'])).toEqual(['tokio/src/signal/thing.rs@1']);
});
it('reads an edited `use` afresh on sync', async () => {
fs.writeFileSync(path.join(root, 'tokio/src/sync/broadcast.rs'), 'use crate::loom::Mutex;\n\npub fn tail(lock: Mutex<u64>) {}\n');
await cg.sync();
expect(targetsOf(cg, 'tokio/src/sync/broadcast.rs', 'Mutex', ['references'])).toEqual(['tokio/src/loom/mod.rs@1']);
});
});
describe('what a Rust module re-exports, when its names have more than one meaning', () => {
let root = '';
let cg: CodeGraph;
beforeAll(async () => {
({ root, cg } = await indexProject('cg-rust-use-ns-', {
'Cargo.toml': '[workspace]\nmembers = ["app", "clap", "clap_builder", "clap_derive"]\n',
// clap re-exports its derive macros by name and its traits by a glob:
// `ValueEnum` is both, a macro and a trait.
'clap/Cargo.toml': '[package]\nname = "clap"\nversion = "0.1.0"\n\n[dependencies]\nclap_builder = { path = "../clap_builder" }\nclap_derive = { path = "../clap_derive" }\n',
'clap/src/lib.rs': 'pub use clap_builder::*;\n\n#[cfg(feature = "derive")]\npub use clap_derive::{Parser, ValueEnum};\n',
'clap_builder/Cargo.toml': '[package]\nname = "clap_builder"\nversion = "0.1.0"\n',
'clap_builder/src/lib.rs': 'mod derive;\n\npub use crate::derive::{Parser, ValueEnum};\n',
'clap_builder/src/derive.rs': 'pub trait Parser {}\n\npub trait ValueEnum {}\n',
'clap_derive/Cargo.toml': '[package]\nname = "clap_derive"\nversion = "0.1.0"\n\n[lib]\nproc-macro = true\n',
'clap_derive/src/lib.rs': 'use proc_macro::TokenStream;\n\n#[proc_macro_derive(ValueEnum)]\npub fn value_enum(input: TokenStream) -> TokenStream {\n input\n}\n',
'app/Cargo.toml': '[package]\nname = "app"\nversion = "0.1.0"\n\n[dependencies]\nclap = { path = "../clap" }\nglobal-hotkey = "0.6"\n',
'app/src/lib.rs': 'mod desktop;\nmod mobile_shortcut;\nmod shell;\nmod shortcut;\nmod sys;\nmod user;\n',
'app/src/shell.rs': 'use clap::ValueEnum;\n\npub enum Shell {\n Bash,\n}\n\nimpl ValueEnum for Shell {}\n',
// One cfg re-exports another crate's type, the other the project's.
'app/src/shortcut.rs': '#[cfg(not(target_os = "ios"))]\npub use global_hotkey::HotKey;\n\n#[cfg(target_os = "ios")]\npub use crate::mobile_shortcut::*;\n',
'app/src/mobile_shortcut.rs': 'pub struct HotKey;\n',
'app/src/desktop.rs': 'use crate::shortcut::HotKey;\n\npub fn register(key: HotKey) {}\n',
// A module a `#[path]` puts in another file on one target.
'app/src/sys/mod.rs': '#[cfg(target_os = "ios")]\n#[path = "ios.rs"]\nmod imp;\n\n#[cfg(not(target_os = "ios"))]\nmod imp;\n\npub use imp::Handle;\n',
'app/src/sys/imp.rs': 'pub use std::fs::File as Handle;\n',
'app/src/sys/ios.rs': 'pub struct Handle;\n',
'app/src/user.rs': 'use crate::sys::Handle;\n\npub fn open(handle: Handle) {}\n',
}));
}, 60_000);
afterAll(() => {
cg?.close();
if (root) fs.rmSync(root, { recursive: true, force: true });
});
it('a name `use`d as a macro still comes in as a trait through a glob', () => {
expect(targetsOf(cg, 'app/src/shell.rs', 'ValueEnum', ['implements'])).toEqual(['clap_builder/src/derive.rs@3']);
});
it('a re-export of another crate\'s type leaves the project\'s, another cfg\'s, in scope', () => {
expect(targetsOf(cg, 'app/src/desktop.rs', 'HotKey', ['references'])).toEqual(['app/src/mobile_shortcut.rs@1']);
});
it('a module whose file a `#[path]` names says nothing about its items', () => {
expect(targetsOf(cg, 'app/src/user.rs', 'Handle', ['references'])).toEqual(['app/src/sys/ios.rs@1']);
});
});
+433 -3
View File
@@ -7873,6 +7873,34 @@ interface RustUses {
bound: Set<string>;
/** Each bound item → the first segment of every path that binds it (`crate`, `super`, `tokio_test`). */
boundFrom: Map<string, Set<string>>;
/** Each name a project `use` binds → the paths that bind it (`Receiver` → `crate::sync::watch::Receiver`). */
paths: Map<string, RustUsePath[]>;
/** The modules its project `use … ::*` globs bring in, as paths. */
globPaths: RustUsePath[];
/** The modules the file declares inline (`mod tests { … }`), in source order. */
blocks: RustModBlock[];
/** Modules it declares with a `#[path = "…"]` of their own (often one per cfg), whose file can't be told from their name. */
pathMods: Set<string>;
/** Whether every brace closes, so `blocks` can be trusted to say what is inside them. */
balanced: boolean;
/** The offset each line starts at. */
lineStarts: number[];
}
interface RustUsePath {
/** The path as written, less any `as` alias: `['crate', 'sync', 'watch', 'Receiver']`. */
segs: string[];
/** The offset of its `use` in the file, which decides the module the path starts from. */
at: number;
}
/** An inline `mod name { … }`, by the offsets of its braces. */
interface RustModBlock {
name: string;
open: number;
close: number;
/** The inline module it is declared in, or null for the file's own top level. */
parent: RustModBlock | null;
}
const RUST_CRATES = new WeakMap<ResolutionContext, Set<string>>();
@@ -8092,7 +8120,10 @@ function rustUsesOf(filePath: string, context: ResolutionContext): RustUses {
}
const hit = memo.get(filePath);
if (hit) return hit;
const uses: RustUses = { names: new Set(), globs: new Set(), external: new Set(), bound: new Set(), boundFrom: new Map() };
const uses: RustUses = {
names: new Set(), globs: new Set(), external: new Set(), bound: new Set(), boundFrom: new Map(),
paths: new Map(), globPaths: [], blocks: [], pathMods: new Set(), balanced: true, lineStarts: [],
};
const leaves = (tree: string): string[] => [
...[...tree.matchAll(/([A-Za-z_]\w*)\s*(?=[,}]|$|\s+as\b)|\bas\s+([A-Za-z_]\w*)/g)]
.map((leaf) => leaf[2] ?? leaf[1]!).filter((id) => id !== 'self' && id !== 'as'),
@@ -8108,6 +8139,16 @@ function rustUsesOf(filePath: string, context: ResolutionContext): RustUses {
// Outside: the standard library or a crate the manifests depend on — not a
// module of the project's (`mod support { … }` inline in a test).
const outside = root === 'std' || root === 'core' || root === 'alloc' || (root !== '' && dependencies.has(root));
// Every path, outside ones too: walked, a path dependency on a crate the
// workspace doesn't list is the project's (a facade's `pub use shared::Thing`).
for (const { segs, local } of rustUseTreeBindings(tree)) {
// A macro's own `use $crate::…` names no module of this file's.
if (segs.length === 0 || segs[0]!.startsWith('$') || RUST_STD_ROOTS.has(segs[0]!)) continue;
const path: RustUsePath = { segs, at: m.index! };
if (local === null) uses.globPaths.push(path);
else if (uses.paths.has(local)) uses.paths.get(local)!.push(path);
else uses.paths.set(local, [path]);
}
// The items it binds: each leaf (`as` aliases by their alias), never the path it walks.
if (outside) {
for (const id of leaves(tree)) uses.external.add(id);
@@ -8122,10 +8163,394 @@ function rustUsesOf(filePath: string, context: ResolutionContext): RustUses {
for (const id of tree.matchAll(/[A-Za-z_]\w*/g)) uses.names.add(id[0]);
for (const g of tree.matchAll(/(\w+)\s*::\s*(?:\{[^}]*)?\*/g)) uses.globs.add(g[1]!);
}
const layout = rustModBlocks(text);
uses.blocks = layout.blocks;
uses.balanced = layout.balanced;
// `#[path = "atomic_u64_native.rs"] mod imp;`, `#[cfg_attr(docsrs, path = "…")] pub mod de;`
for (const m of text.matchAll(/#\[\s*(?:cfg_attr\s*\([^\]]*\bpath|path)\s*=[^\]]*\]\s*(?:#!?\[[^\]]*\]\s*)*(?:pub(?:\([^)]*\))?\s+)?mod\s+(?:r#)?([A-Za-z_]\w*)/g)) {
uses.pathMods.add(m[1]!);
}
for (let at = 0; at !== -1; at = text.indexOf('\n', at) + 1 || -1) uses.lineStarts.push(at);
memo.set(filePath, uses);
return uses;
}
/** Roots that name a crate of the standard library. */
const RUST_STD_ROOTS: ReadonlySet<string> = new Set(['std', 'core', 'alloc', 'proc_macro']);
/**
* What one `use` tree binds, by the path each binding names: `crate::sync::{
* watch::{self, Receiver as Rx}, *}` binds `watch` to `crate::sync::watch` and
* `Rx` to `crate::sync::watch::Receiver`, and globs `crate::sync` (local null).
*/
function rustUseTreeBindings(tree: string): Array<{ segs: string[]; local: string | null }> {
const tokens = tree.match(/(?:r#)?\$?[A-Za-z_]\w*|::|[{},*]/g) ?? [];
const out: Array<{ segs: string[]; local: string | null }> = [];
let i = 0;
// One tree under `prefix`: a path, then a `{ … }` group, a `*`, or an `as` alias.
const read = (prefix: string[]): void => {
const segs = [...prefix];
while (i < tokens.length) {
const t = tokens[i]!;
if (t === '::') {
i++;
continue;
}
if (t === '*') {
i++;
out.push({ segs, local: null });
return;
}
if (t === '{') {
i++;
while (i < tokens.length && tokens[i] !== '}') {
const before = i;
read(segs);
if (tokens[i] === ',' || i === before) i++;
}
i++;
return;
}
if (t === ',' || t === '}' || (t === 'as' && segs.length > prefix.length)) break;
segs.push(t.startsWith('r#') ? t.slice(2) : t);
i++;
if (tokens[i] !== '::') break;
}
if (segs.length === prefix.length) return;
let local: string | undefined;
if (tokens[i] === 'as') {
local = tokens[i + 1];
i += 2;
}
// `watch::{self, …}` binds the module itself.
if (segs[segs.length - 1] === 'self') segs.pop();
local ??= segs[segs.length - 1];
if (segs.length > 0 && local !== undefined && local !== '_' && /^[A-Za-z_]\w*$/.test(local)) out.push({ segs, local });
};
while (i < tokens.length) {
const before = i;
read([]);
if (i === before) i++;
}
return out;
}
/** The offsets of the braces in `text` (comments already blanked) that are code: none in a string or char literal. */
function rustCodeBraces(text: string): number[] {
const braces: number[] = [];
const scan = /[{}]|\b(?:b?r(#*)")|"|'/g;
for (let m = scan.exec(text); m; m = scan.exec(text)) {
const at = m.index;
const c = m[0];
if (c === '{' || c === '}') {
braces.push(at);
} else if (c === "'") {
// A char literal (`'{'`, `'\''`, `'\u{7f}'`) — any other quote starts a lifetime or a label.
const lit = /^'(?:\\(?:u\{[0-9A-Fa-f]{1,6}\}|x[0-9A-Fa-f]{2}|.)|[^\\'\n])'/.exec(text.slice(at, at + 12));
if (lit) scan.lastIndex = at + lit[0].length;
} else if (c === '"') {
// The next quote no odd run of backslashes escapes.
let end = at;
for (;;) {
end = text.indexOf('"', end + 1);
let slashes = 0;
while (end > 0 && text[end - 1 - slashes] === '\\') slashes++;
if (end < 0 || slashes % 2 === 0) break;
}
scan.lastIndex = end < 0 ? text.length : end + 1;
} else {
// A raw string: `r#"…"#` closes only at a quote followed by as many `#`s.
const close = text.indexOf(`"${m[1]}`, at + c.length);
scan.lastIndex = close < 0 ? text.length : close + 1 + m[1]!.length;
}
}
return braces;
}
/** The inline `mod name { … }` blocks of a file's text (comments already blanked), in source order. */
function rustModBlocks(text: string): { blocks: RustModBlock[]; balanced: boolean } {
const heads = new Map<number, string>();
for (const m of text.matchAll(/\bmod\s+(?:r#)?([A-Za-z_]\w*)\s*\{/g)) heads.set(m.index! + m[0].length - 1, m[1]!);
const blocks: RustModBlock[] = [];
if (heads.size === 0) return { blocks, balanced: true };
const open: Array<RustModBlock | null> = [];
let inner: RustModBlock | null = null;
let balanced = true;
// A head whose `{` is in a string is never among the braces.
for (const at of rustCodeBraces(text)) {
if (text[at] === '{') {
const name = heads.get(at);
const block: RustModBlock | null = name === undefined ? null : { name, open: at, close: text.length, parent: inner };
if (block) {
blocks.push(block);
inner = block;
}
open.push(block);
continue;
}
if (open.length === 0) {
balanced = false;
continue;
}
const closed = open.pop();
if (closed) {
closed.close = at;
inner = closed.parent;
}
}
return { blocks, balanced: balanced && open.length === 0 };
}
/** The innermost inline module whose braces hold `offset`, or null for the file's top level. */
function rustBlockAt(blocks: readonly RustModBlock[], offset: number): RustModBlock | null {
let at: RustModBlock | null = null;
for (const b of blocks) {
if (b.open > offset) break;
if (offset < b.close) at = b;
}
return at;
}
/** A Rust module, by where its items are written. */
interface RustModule {
/** The file its items are written in. */
file: string;
/** Where the files of the modules it declares live: `src/sync` for `src/sync/mod.rs` and `src/sync.rs` alike. */
dir: string;
/** Its block, for a module declared inline (`mod futures { … }`); null for the file's own. */
block: RustModBlock | null;
/** A crate's root — `lib.rs`, `main.rs`, a test target's file — which nothing is above. */
root: boolean;
/** The package's other root, which holds crate-root items too: the `main.rs` beside its `lib.rs`. */
alt?: string;
}
const rustJoin = (dir: string, name: string): string => (dir === '' ? name : `${dir}/${name}`);
const rustParentDir = (p: string): string => p.slice(0, Math.max(0, p.lastIndexOf('/')));
/** Whether a Rust file is a Cargo target's root: `src/lib.rs`, `src/main.rs`, `src/bin/x.rs`, `tests/x.rs`, `build.rs`, … */
function isRustTargetRoot(filePath: string, context: ResolutionContext): boolean {
const dir = rustCrateDirOf(filePath, rustCrateGraph(context));
if (dir === null) return /(?:^|\/)(?:lib|main)\.rs$/.test(filePath);
const rel = dir === '' ? filePath : filePath.slice(dir.length + 1);
return /^(?:src\/(?:lib|main)\.rs|src\/bin\/[^/]+(?:\.rs|\/main\.rs)|(?:tests|examples|benches)\/[^/]+(?:\.rs|\/main\.rs)|build\.rs)$/.test(rel);
}
/** The module a Rust file is, at its own top level. */
function rustFileModule(filePath: string, context: ResolutionContext): RustModule {
const root = isRustTargetRoot(filePath, context);
const dir = root || filePath.endsWith('/mod.rs') || filePath === 'mod.rs' ? rustParentDir(filePath) : filePath.replace(/\.rs$/, '');
return { file: filePath, dir, block: null, root };
}
/** The module a `use` at `offset` in `filePath` is written in: the file's, or the inline one around it. */
function rustModuleAt(filePath: string, offset: number, context: ResolutionContext): RustModule {
const own = rustFileModule(filePath, context);
const block = rustBlockAt(rustUsesOf(filePath, context).blocks, offset);
if (!block) return own;
let dir = own.dir;
const names: string[] = [];
for (let b: RustModBlock | null = block; b; b = b.parent) names.unshift(b.name);
for (const name of names) dir = rustJoin(dir, name);
return { file: filePath, dir, block, root: false };
}
/**
* The root of the crate `filePath` is in — what its `crate::` names: a test,
* example, bench or bin target's own file, else the package's `src/lib.rs` (or
* `src/main.rs`). null when no single root is the file's: a `tests/support/`
* module serves every test that declares it.
*/
function rustCrateRootModule(filePath: string, context: ResolutionContext): RustModule | null {
const dir = rustCrateDirOf(filePath, rustCrateGraph(context));
if (dir === null) return null;
const base = dir === '' ? '' : `${dir}/`;
const rel = filePath.slice(base.length);
const target = /^(src\/bin|tests|examples|benches)\/([^/]+?)(\.rs$|\/)/.exec(rel);
if (target) {
const file = target[3] === '.rs' ? filePath : `${base}${target[1]}/${target[2]}/main.rs`;
return context.fileExists(file) ? { file, dir: rustParentDir(file), block: null, root: true } : null;
}
if (!rel.startsWith('src/')) return null;
const lib = `${base}src/lib.rs`;
const main = `${base}src/main.rs`;
const hasLib = context.fileExists(lib);
const hasMain = context.fileExists(main);
if (!hasLib && !hasMain) return null;
const file = hasMain && (filePath === main || !hasLib) ? main : lib;
return { file, dir: `${base}src`, block: null, root: true, alt: hasLib && hasMain ? (file === lib ? main : lib) : undefined };
}
/** The root of a project crate's library: what a path that starts with the crate's name (`tokio::sync::…`) walks from. */
function rustLibModule(crateDir: string, context: ResolutionContext): RustModule | null {
const file = crateDir === '' ? 'src/lib.rs' : `${crateDir}/src/lib.rs`;
return context.fileExists(file) ? { file, dir: rustParentDir(file), block: null, root: true } : null;
}
/**
* The module `name` that `m` declares: inline (`mod name { … }`), else
* `<name>.rs` or `<name>/mod.rs` in its directory. null for one a `#[path]`
* puts elsewhere — tokio's `process::imp` is `unix/mod.rs` or `windows.rs` by
* cfg — as for one with no file.
*/
function rustSubmodule(m: RustModule, name: string, context: ResolutionContext): RustModule | null {
const dir = rustJoin(m.dir, name);
const layout = rustUsesOf(m.file, context);
if (layout.pathMods.has(name)) return null;
const block = layout.blocks.find((b) => b.name === name && b.parent === m.block);
if (block) return { file: m.file, dir, block, root: false };
for (const file of [`${dir}.rs`, `${dir}/mod.rs`]) {
if (context.fileExists(file)) return { file, dir, block: null, root: false };
}
return null;
}
/** The module `m` is declared in — what `super::` names — or null above a crate's root. */
function rustSuperModule(m: RustModule, context: ResolutionContext): RustModule | null {
if (m.block) {
return m.block.parent ? { file: m.file, dir: rustParentDir(m.dir), block: m.block.parent, root: false } : rustFileModule(m.file, context);
}
if (m.root) return null;
const dir = rustParentDir(m.dir);
for (const file of [rustJoin(dir, 'mod.rs'), ...(dir === '' ? [] : [`${dir}.rs`]), rustJoin(dir, 'lib.rs'), rustJoin(dir, 'main.rs')]) {
if (context.fileExists(file)) return rustFileModule(file, context);
}
return null;
}
/**
* The module the segments of a `use` path walk to from `from`, the module the
* `use` is written in: `crate::`, `self::` and `super::` as Rust reads them; a
* module `from` declares (2018 paths start from the current module); a project
* crate by the name its manifest gives the file's crate, or by its own.
* 'outside' for the standard library and every other crate; null when a step
* finds no module the project holds.
*/
function rustWalkModules(segs: readonly string[], from: RustModule, context: ResolutionContext): RustModule | 'outside' | null {
const [first, ...rest] = segs;
if (first === undefined) return from;
let at: RustModule | null;
if (first === 'crate') at = rustCrateRootModule(from.file, context);
else if (first === 'self') at = from;
else if (first === 'super') at = rustSuperModule(from, context);
else if (RUST_STD_ROOTS.has(first)) return 'outside';
else {
at = rustSubmodule(from, first, context);
if (at === null) {
const graph = rustCrateGraph(context);
const own = rustCrateDirOf(from.file, graph);
const crate = own === null ? undefined : graph.crates.get(own);
const dep = crate === undefined ? undefined : first === crate.name ? own! : crate.keys.get(first);
if (dep !== undefined) at = rustLibModule(dep, context);
else if (rustDependencyCrates(context).has(first)) return 'outside';
}
}
for (const seg of rest) {
if (at === null) return null;
at = seg === 'self' ? at : seg === 'super' ? rustSuperModule(at, context) : rustSubmodule(at, seg, context);
}
return at;
}
/** The items `m` declares as `name` at its own level: not in a module it declares inline, nor inside a function. */
function rustDeclaredItems(m: RustModule, name: string, context: ResolutionContext): Node[] {
const out: Node[] = [];
for (const file of m.alt ? [m.file, m.alt] : [m.file]) {
const layout = rustUsesOf(file, context);
const block = file === m.file ? m.block : null;
for (const n of context.getNodesInFileNamed?.(file, name) ?? context.getNodesInFile(file).filter((c) => c.name === name)) {
// An item in a function body is qualified by it (`outer::Inner`); an inline module adds nothing.
if (n.language !== 'rust' || n.qualifiedName !== name || !RUST_MODULE_ITEM_KINDS.has(n.kind)) continue;
const at = (layout.lineStarts[n.startLine - 1] ?? 0) + Math.max(0, n.startColumn ?? 0);
if (rustBlockAt(layout.blocks, at) === block) out.push(n);
}
}
return out;
}
/**
* The items named `name` in module `m`, as Rust looks a name up in a module:
* the ones it declares, else the ones its `use`s of that name bring in —
* `pub use mutex::{Mutex, …}`, macro-wrapped as tokio's `cfg_sync! { … }`
* are — else the ones its globs do. An outside crate's add nothing. null when
* one of them can't be followed. `seen` holds the lookups under way, so
* modules that glob each other (`use super::*` below a `pub use child::*`)
* end the walk.
*/
function rustModuleItems(m: RustModule, name: string, context: ResolutionContext, seen: Set<string>): Node[] | null {
const key = `${m.file}\0${m.block?.open ?? -1}\0${name}`;
if (seen.has(key)) return [];
if (seen.size >= 32) return null;
const uses = rustUsesOf(m.file, context);
if (!uses.balanced) return null;
seen.add(key);
try {
const declared = rustDeclaredItems(m, name, context);
if (declared.length > 0) return declared;
const here = (at: number): boolean => rustBlockAt(uses.blocks, at) === m.block;
const named = (uses.paths.get(name) ?? []).filter((p) => here(p.at));
const found: Node[] = [];
for (const p of named) {
const items = rustUsePathItems(p.segs, m, context, seen);
if (items === null) return null;
found.push(...items);
}
// A `use` of the name shadows the globs — in its own namespace: clap's
// `pub use clap_derive::ValueEnum` is a derive macro, and the trait
// `ValueEnum` still comes in through its `pub use clap_builder::*`.
if (found.length > 0) return found;
for (const g of uses.globPaths) {
if (!here(g.at)) continue;
const mod = rustWalkModules(g.segs, m, context);
if (mod === 'outside') continue;
const items = mod === null ? null : rustModuleItems(mod, name, context, seen);
if (items === null) return null;
found.push(...items);
}
return found;
} finally {
seen.delete(key);
}
}
/** The items a `use` path names from `from`: its last segment's, in the module the rest walks to. */
function rustUsePathItems(segs: readonly string[], from: RustModule, context: ResolutionContext, seen: Set<string>): Node[] | null {
if (segs.length < 2) return null;
const mod = rustWalkModules(segs.slice(0, -1), from, context);
if (mod === 'outside') return [];
return mod === null ? null : rustModuleItems(mod, segs[segs.length - 1]!, context, seen);
}
const RUST_USE_ITEMS = new WeakMap<ResolutionContext, Map<string, Set<string> | null>>();
/**
* The ids of what the `use`s that bind `name` in `filePath` name, through the
* project's modules: `use crate::sync::watch::Receiver` names watch.rs's
* `Receiver`, never pipe.rs's. Empty when the module holds nothing by that
* name the index knows (a macro's item); null when the file binds no such
* name, or one of its paths can't be followed — a module with no file to read.
*/
function rustUseItems(name: string, filePath: string, context: ResolutionContext): Set<string> | null {
let memo = RUST_USE_ITEMS.get(context);
if (!memo) RUST_USE_ITEMS.set(context, (memo = new Map()));
const key = `${filePath}\0${name}`;
const hit = memo.get(key);
if (hit !== undefined) return hit;
const uses = rustUsesOf(filePath, context);
const paths = uses.balanced ? uses.paths.get(name) : undefined;
let ids: Set<string> | null = paths ? new Set() : null;
for (const p of paths ?? []) {
const items = rustUsePathItems(p.segs, rustModuleAt(filePath, p.at, context), context, new Set());
if (items === null) {
ids = null;
break;
}
for (const n of items) ids!.add(n.id);
}
memo.set(key, ids);
return ids;
}
/** The module a Rust file is: `src/glob.rs` → `glob`, `src/walk/mod.rs` → `walk`. */
function rustModuleName(filePath: string): string {
const parts = filePath.split('/');
@@ -8185,7 +8610,9 @@ function rustGlobCovers(uses: RustUses, candidate: Node, ref: UnresolvedRef): bo
* project item of that name — serde's macro-hygiene tests declare `struct
* Ok`, `struct Result`, and serde bound its own `Ok(…)` and `Result<…>` to them.
* A private item is out of scope outside its module (isRustItemVisibleFrom),
* and an item a `use` binds is one its path's crate can reach (rustUseCanReach).
* and an item a `use` binds is one its path's crate can reach (rustUseCanReach)
* and, where the project holds the modules that path walks, the item it names
* there (rustUseItems).
*/
export function isRustNameInScope(candidate: Node, ref: UnresolvedRef, context: ResolutionContext): boolean {
if (!isRustItemVisibleFrom(candidate, ref)) return false;
@@ -8252,9 +8679,11 @@ export function isRustNameInScope(candidate: Node, ref: UnresolvedRef, context:
}
if (candidate.filePath === ref.filePath) return true;
const uses = rustUsesOf(ref.filePath, context);
// An item a `use` binds is one its path's crate can name.
// An item a `use` binds is one its path's crate can name — and, where the
// project holds the modules the path walks, the item of that module.
const from = RUST_MODULE_ITEM_KINDS.has(candidate.kind) ? uses.boundFrom.get(name) : undefined;
if (from && !rustUseCanReach(from, candidate, ref, context)) return false;
if (from && rustUseItems(name, ref.filePath, context)?.has(candidate.id) === false) return false;
// `use std::task::{Context, Poll}`: the file's `Context` is std's, not tokio's
// `runtime::context::Context`. (A method call `.env(…)` is no imported name.)
if (uses.external.has(name) && !uses.bound.has(name) && line !== undefined) {
@@ -14693,6 +15122,7 @@ export function clearNameMatcherMemos(context: ResolutionContext): void {
CPP_CALLER_HEADS.delete(context);
RUST_TRAIT_IMPL_MEMO.delete(context);
RUST_USES.delete(context);
RUST_USE_ITEMS.delete(context);
RUST_CRATES.delete(context);
RUST_DEPENDENCIES.delete(context);
RUST_CRATE_GRAPHS.delete(context);