fix(rust): use bindings follow Rust's scopes, and module paths walk the crate's modules (#3014)

A bare Rust name took the file's `use`s all at once: a `use` inside one
function (`fn first() { use crate::util::take; take() }`) bound `take` for
every other function too, and for the file's own `fn take`; a renamed one
(`use crate::util::take as consume; consume()`) linked nothing, since no
item carries the alias; a parameter or closure of the function's name
(`fn run(take: fn()) { take() }`, `let take = || {};`) linked to the
imported function; and `use crate::models::E::Good; Good(1)` took any
`E::Good`. Paths through modules (`resolveRustPathReference`) walked the
directories instead of the modules: `super::helper()` and `crate::config()`
with no module between linked nothing, `super` inside an inline
`mod tests { }` went to the parent directory's module, and an item a module
passes on (`pub use`, a glob) or another workspace crate's was not found.

The fix keeps one `use` reader, rustUsesOf:
- each `use` keeps the block it is written in (a function's body or a
  block; an inline `mod` and a macro's braces, `cfg_if!`'s nested ones
  included, are the module's), and std `use`s are kept by position;
- a reference reads the binding of its innermost scope (rustVisibleUse);
  isRustNameInScope uses it, so a block's `use` shadows the file's own item
  and a variant `use` names its enum's variant (rustUsePathItems' enum tail);
- matchRustUseBoundName, ahead of the pre-filter, resolves a rename to the
  item its path names, and a call to a name a local binds (rust-scopes'
  bindings, nearer than any `use`) to nothing, unless the local is a `let`
  holding a function by its path (serde's `let test = test::<i8>;`);
- resolveRustPathReference is rustWalkModules + rustModuleItems from the
  module the path is written in (rustPathTarget), with today's crate-root
  fallback; resolveRustModuleFile, collectRustUseBindings and
  rustItemPlacement are gone, and isBoundToOutOfRepoImport reads
  rustUsesOf's std `use`s;
- `use`s are read with strings blanked (blankRustCode), so a string's
  "use crate::x;" binds nothing;
- the crate graph reads a package's name by its section (axum-core writes
  `categories = [..]` first) and a `[lib]`/`[[bin]]` path of its own
  (ripgrep's `crates/core/main.rs`), which `crate::` then starts from; a
  file no manifest covers starts it from the nearest lib.rs or main.rs
  above it, as the directory walk did.

Validation:
- rust-use-call.test.ts (PR 2308's 15 cases), rust-use-scopes.test.ts,
  rust-crate-roots.test.ts and 5 new rust-module-path-call cases: on main's
  source 18 of 32 fail; all pass. Every rust-* suite passes.
- A/B on main 01e0ac3a, 7 repos, the same sites on the kernel and on wasm:
  tokio +2,434 sites (1,354 calls, 1,053 imports), 159 removed (152 locals
  called or passed as values, 6 `Result`s std's, 1 import through a path
  attribute module); ripgrep +248 / -149; axum +710 / -60; clap +730 / -112;
  serde +155 / -207 (192 calls to a closure named `test`); dioxus
  +2,950 / -272; gleam +1,573 / -359. Groups and samples inspected, every
  changed site's crate checked against the manifests' dependencies;
  express and flask byte-identical.
- Cost (kernel, medians of 3 interleaved): tokio CPU 34.5 -> 33.0 s, peak
  RSS 991 -> 1,007 MB; gleam 32.5 -> 30.6 s, 1,022 -> 1,012 MB; dioxus
  45.4 -> 45.4 s, 1,101 -> 1,107 MB.
- Original PR #2308 by @danusha2345; its module-path part landed as PR #2955.

Co-authored-by: danusha2345 <ewidusoc498@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-10-29 18:39:13 +00:00
committed by GitHub
co-authored by danusha2345 Claude Opus 5.5
parent e7c9ffd068
commit 4955eac958
9 changed files with 1026 additions and 279 deletions
+1
View File
@@ -123,6 +123,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- `codegraph_explore` and `codegraph context` now include some of the subtypes and implementors of a class or interface you ask about, and the type hierarchy the library returns for one (`getTypeHierarchy`) now lists all of them. Before, a bug in the hierarchy walk left every type without subtypes, so only what it extends or implements came back. Thanks @jocax for the report and @danusha2345 for the fix. (#2833) - `codegraph_explore` and `codegraph context` now include some of the subtypes and implementors of a class or interface you ask about, and the type hierarchy the library returns for one (`getTypeHierarchy`) now lists all of them. Before, a bug in the hierarchy walk left every type without subtypes, so only what it extends or implements came back. Thanks @jocax for the report and @danusha2345 for the fix. (#2833)
- In Rust, code that uses an enum only through its variants, like `mode::Mode::A` in an expression, a `Mode::B =>` match arm or `Self::A` inside the enum's own `impl`, now shows up in that enum's callers and impact, linked to the enum that actually declares the variant rather than to a same-named type elsewhere. Standard-library variants like `Ordering::Less` or `Option::Some` and associated items like `Foo::new()` or `Foo::MAX` don't count as using a project enum. Re-index Rust projects after upgrading. Thanks @mg-mg-mg for the report and @danusha2345. (#2328) - In Rust, code that uses an enum only through its variants, like `mode::Mode::A` in an expression, a `Mode::B =>` match arm or `Self::A` inside the enum's own `impl`, now shows up in that enum's callers and impact, linked to the enum that actually declares the variant rather than to a same-named type elsewhere. Standard-library variants like `Ordering::Less` or `Option::Some` and associated items like `Foo::new()` or `Foo::MAX` don't count as using a project enum. Re-index Rust projects after upgrading. Thanks @mg-mg-mg for the report and @danusha2345. (#2328)
- In Rust, a call written through a module path, like `crate::util::take(3)`, `super::util::take(3)` or `util::take(3)`, and a `use` of a function, like `use crate::util::take;`, now link to the function that module declares. Before, they could land on a method of the same name declared above it in that file, like an `impl Buf`'s `take`, or on a function written inside another function, and a call through a module the standard library provides, like `fs::read(…)` under `use std::fs;`, could link to your own `fs` module's `read`. Re-index Rust projects after upgrading. Thanks @danusha2345. (#2308) - In Rust, a call written through a module path, like `crate::util::take(3)`, `super::util::take(3)` or `util::take(3)`, and a `use` of a function, like `use crate::util::take;`, now link to the function that module declares. Before, they could land on a method of the same name declared above it in that file, like an `impl Buf`'s `take`, or on a function written inside another function, and a call through a module the standard library provides, like `fs::read(…)` under `use std::fs;`, could link to your own `fs` module's `read`. Re-index Rust projects after upgrading. Thanks @danusha2345. (#2308)
- In Rust, a name a `use` renames, like `consume()` after `use crate::util::take as consume;`, now links to the item its path names, and a `use` written inside a function or a block counts only there, ahead of the file's own `use`s and items, so `fn first() { use crate::util::take; take() }` reaches util's `take` and the next function's own `use` its own. A call to a parameter, a closure or a local that shares a function's name, like `take()` inside `fn run(take: fn())` or after `let take = || {};`, no longer links to that function, while `let test = test::<i8>;` still calls `test`, and a `use` of an enum's variant, like `use crate::models::Mode::Fast;`, links `Fast(…)` to that enum's variant. A path through a module, like `super::helper()`, `crate::config()`, `super::shared::helper()` inside a `mod tests { … }` or `tokio::task::yield_now()`, now reaches the item that module declares or passes on with a `pub use`, in your own crate or another crate of the workspace, and its `use` links to it; that includes binaries whose manifest gives them a path of their own, like ripgrep's, and packages whose manifest lists `name` after other fields, like axum-core's. A `use` written inside a string no longer counts. Re-index Rust projects after upgrading. Thanks @danusha2345. (#2308)
- Axum routes now link to their handler when rustfmt wraps the `.route(` call so the handler sits on a later line, and when the handler's name also appears elsewhere on the line, as in `delete(handlers::user::delete)` or `.route("/login", post(handlers::user::login))`; Actix's `web::resource(…)` and `.route(…)` registrations get the same fix. A call inside a closure handler is no longer mistaken for a route, and an Actix resource no longer takes the next route's handler as its own. Thanks @mg-mg-mg for the report and @danusha2345. (#2326) - Axum routes now link to their handler when rustfmt wraps the `.route(` call so the handler sits on a later line, and when the handler's name also appears elsewhere on the line, as in `delete(handlers::user::delete)` or `.route("/login", post(handlers::user::login))`; Actix's `web::resource(…)` and `.route(…)` registrations get the same fix. A call inside a closure handler is no longer mistaken for a route, and an Actix resource no longer takes the next route's handler as its own. Thanks @mg-mg-mg for the report and @danusha2345. (#2326)
- In Play projects, a route or a call like `MediaType.parse(...)` now links to a method of the class or object it names, and a call to an overloaded one to the overload its arguments fit. Before, it went to the first method of that name anywhere in the class's file, often a nested or sibling class's. When a Scala and a Java type share a name, as Play's own `RequestAuthority` does, each language's calls now reach their own. Thanks @danusha2345. - In Play projects, a route or a call like `MediaType.parse(...)` now links to a method of the class or object it names, and a call to an overloaded one to the overload its arguments fit. Before, it went to the first method of that name anywhere in the class's file, often a nested or sibling class's. When a Scala and a Java type share a name, as Play's own `RequestAuthority` does, each language's calls now reach their own. Thanks @danusha2345.
- In JavaScript and TypeScript, the functions written inside a named object literal are now symbols of their own even when the object isn't exported: a plain `const api = { load() {…} }`, an object declared inside an IIFE or a function, and a namespace hung on the page or on another object, like `window.App = { init() {…} }`, `App.utils = {…}` or `dw_page = {…}`. Calls such as `api.load()`, `window.App.init()`, `App.utils.pad()`, a sibling's `this.render()`, and `App.init()` from another script on the page now reach them, the calls made inside a member belong to that member instead of the object, and a bare `init()` is no longer taken for `App.init`, so script-tag apps no longer lose most of their code from `callers` and impact. A member's qualified name now carries its object, exported or not (`api::load`, `window.App::init`), so asking for `App.init` finds it. Re-index JavaScript and TypeScript projects after upgrading. Thanks @tkhoaaa for the report and @danusha2345. (#2300) - In JavaScript and TypeScript, the functions written inside a named object literal are now symbols of their own even when the object isn't exported: a plain `const api = { load() {…} }`, an object declared inside an IIFE or a function, and a namespace hung on the page or on another object, like `window.App = { init() {…} }`, `App.utils = {…}` or `dw_page = {…}`. Calls such as `api.load()`, `window.App.init()`, `App.utils.pad()`, a sibling's `this.render()`, and `App.init()` from another script on the page now reach them, the calls made inside a member belong to that member instead of the object, and a bare `init()` is no longer taken for `App.init`, so script-tag apps no longer lose most of their code from `callers` and impact. A member's qualified name now carries its object, exported or not (`api::load`, `window.App::init`), so asking for `App.init` finds it. Re-index JavaScript and TypeScript projects after upgrading. Thanks @tkhoaaa for the report and @danusha2345. (#2300)
+73
View File
@@ -0,0 +1,73 @@
/**
* The root a Rust path's `crate::` names, as the manifest says: a `[[bin]]`
* or `[lib]` with a `path` of its own (ripgrep's `crates/core/main.rs`), and
* a package whose `name` its manifest writes after an array (axum-core's
* `categories = [ … ]` first).
*/
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { CodeGraph } from '../src';
const roots: string[] = [];
const graphs: CodeGraph[] = [];
async function index(files: Record<string, string>): Promise<CodeGraph> {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-rust-crate-roots-'));
roots.push(root);
for (const [relative, content] of Object.entries(files)) {
fs.mkdirSync(path.dirname(path.join(root, relative)), { recursive: true });
fs.writeFileSync(path.join(root, relative), content);
}
const cg = await CodeGraph.init(root, { index: true });
graphs.push(cg);
return cg;
}
let bin: CodeGraph;
let member: CodeGraph;
beforeAll(async () => {
bin = await index({
'Cargo.toml': '[package]\nname = "ripgrep"\nversion = "0.1.0"\nedition = "2021"\n\n[[bin]]\nbench = false\npath = "crates/core/main.rs"\nname = "rg"\n',
'crates/core/main.rs': 'mod flags;\nmod messages;\n\nfn main() {\n let _ = messages::errored();\n}\n',
'crates/core/messages.rs': 'pub fn errored() -> bool {\n false\n}\n',
'crates/core/flags/mod.rs': 'pub mod parse;\npub mod doc;\n\npub enum Category {\n Input,\n}\n',
'crates/core/flags/parse.rs': 'pub fn lookup() -> usize {\n 1\n}\n',
'crates/core/flags/doc.rs': 'use crate::flags::Category;\n\npub fn generate() -> usize {\n crate::flags::parse::lookup()\n}\n',
// Namesakes the paths must not reach.
'crates/core/flags/doc/parse.rs': 'pub fn lookup() -> usize {\n 2\n}\n',
'crates/core/other.rs': 'pub fn errored() -> bool {\n true\n}\npub enum Category {\n Output,\n}\n',
});
member = await index({
'Cargo.toml': '[workspace]\nmembers = ["core-lib"]\n',
'core-lib/Cargo.toml': '[package]\ncategories = ["asynchronous", "network-programming"]\ndescription = "Core types"\nedition = "2021"\nname = "core-lib"\nversion = "0.1.0"\n',
'core-lib/src/lib.rs': 'pub mod body;\npub mod other;\n',
'core-lib/src/body.rs': 'pub struct Body;\n\n#[cfg(test)]\nmod tests {\n use crate::body::Body;\n}\n',
'core-lib/src/other.rs': 'pub struct Body;\n',
});
});
afterAll(() => {
for (const cg of graphs) cg.close();
for (const root of roots) fs.rmSync(root, { recursive: true, force: true });
});
const describeEdges = (cg: CodeGraph, file: string, kind: string, name?: string): string[] => {
const ids = cg.getNodesInFile(file).filter((n) => name === undefined || n.name === name).map((n) => n.id);
return cg.getOutgoingEdgesFrom(ids).filter((e) => e.kind === kind)
.map((e) => cg.getNode(e.target)!).map((t) => `${t.kind} ${t.qualifiedName} ${t.filePath}:${t.startLine}`);
};
describe('the crate a Rust path starts from', () => {
it('is the binary a manifest places with a path of its own', () => {
expect(describeEdges(bin, 'crates/core/flags/doc.rs', 'calls', 'generate')).toEqual(['function lookup crates/core/flags/parse.rs:1']);
expect(describeEdges(bin, 'crates/core/flags/doc.rs', 'imports')).toEqual(['enum Category crates/core/flags/mod.rs:4']);
expect(describeEdges(bin, 'crates/core/main.rs', 'calls', 'main')).toEqual(['function errored crates/core/messages.rs:1']);
});
it('is a package whose manifest writes its name after an array', () => {
expect(describeEdges(member, 'core-lib/src/body.rs', 'imports')).toEqual(['struct Body core-lib/src/body.rs:1']);
});
});
+94
View File
@@ -30,10 +30,74 @@ mod twice;
mod inline_reexport; mod inline_reexport;
mod fs; mod fs;
mod uses_std; mod uses_std;
mod b;
mod x;
mod types;
mod type_;
mod decoy_list;
mod uses_config;
mod walks;
mod plat;
pub struct Config;
pub fn root_fn() -> usize {
0
}
pub fn via_self() -> usize { pub fn via_self() -> usize {
self::util::take(1) self::util::take(1)
} }
`,
// `super::x` and `crate::x`, with no module between them and the item.
'src/b/mod.rs': 'pub mod c;\npub fn helper() -> usize {\n 1\n}\n',
'src/b/c.rs': 'pub fn helper() -> usize {\n 2\n}\npub fn via_bare_super() -> usize {\n super::helper()\n}\npub fn via_bare_crate() -> usize {\n crate::root_fn()\n}\n',
// `super` in an inline `mod tests { }` is the file's own module, not its directory's parent.
'src/x/mod.rs': 'pub mod tested;\npub mod shared;\npub struct Config;\npub fn root_fn() -> usize {\n 9\n}\n',
'src/x/shared.rs': 'pub fn helper() -> usize {\n 3\n}\n',
'src/x/tested.rs': `mod shared;
pub fn tested_helper() -> usize {
4
}
#[cfg(test)]
mod tests {
use super::tested_helper;
fn via_inline_super() -> usize {
super::shared::helper() + tested_helper()
}
}
`,
'src/x/tested/shared.rs': 'pub fn helper() -> usize {\n 5\n}\n',
// A module's item it passes on: a `pub use`, a `pub use …::*`.
'src/types/mod.rs': 'mod list;\npub use self::list::make_list;\n',
'src/types/list.rs': 'pub fn make_list() -> usize {\n 6\n}\n',
'src/decoy_list.rs': 'pub fn make_list() -> usize {\n 7\n}\n',
'src/type_/mod.rs': 'pub mod prelude;\npub use prelude::*;\n\npub struct Type;\n\nimpl Type {\n pub fn list() -> usize {\n 8\n }\n}\n',
'src/type_/prelude.rs': 'pub fn list() -> usize {\n 9\n}\n',
'src/uses_config.rs': 'use crate::{Config};\n',
// A re-export written in a macro's braces, as `cfg_if!` takes them, is the module's.
'src/plat/mod.rs': 'cfg_if::cfg_if! {\n if #[cfg(unix)] {\n mod unix;\n pub use self::unix::open;\n }\n}\n',
'src/plat/unix.rs': 'pub fn open() -> usize {\n 10\n}\n',
'src/decoy_open.rs': 'pub fn open() -> usize {\n 11\n}\n',
'src/walks.rs': `pub fn via_reexport() -> usize {
crate::types::make_list()
}
pub fn via_glob() -> usize {
crate::type_::list()
}
pub fn via_macro_reexport() -> usize {
crate::plat::open()
}
pub fn via_block_std() {
use std::fs;
let _ = fs::read("config.toml");
}
`, `,
// A namesake in an inline module, before the module's own item. // A namesake in an inline module, before the module's own item.
'src/twice.rs': `#[cfg(test)] 'src/twice.rs': `#[cfg(test)]
@@ -219,5 +283,35 @@ describe('Rust calls through a module path', () => {
it('leave a path through a module a `use` takes from the standard library to it', () => { it('leave a path through a module a `use` takes from the standard library to it', () => {
// `fs::read` under `use std::fs;` is std's, not `src/fs/mod.rs`'s `read`. // `fs::read` under `use std::fs;` is std's, not `src/fs/mod.rs`'s `read`.
expect(callsFrom('src/uses_std.rs', 'loads')).toEqual([]); expect(callsFrom('src/uses_std.rs', 'loads')).toEqual([]);
expect(callsFrom('src/walks.rs', 'via_block_std')).toEqual([]);
});
it('reach the item of the module `super` and `crate` name, with no module between', () => {
expect(callsFrom('src/b/c.rs', 'via_bare_super')).toEqual(['function helper src/b/mod.rs:2']);
expect(callsFrom('src/b/c.rs', 'via_bare_crate')).toEqual(['function root_fn src/lib.rs:23']);
});
it('read `super` in an inline module as the module of its file', () => {
expect(callsFrom('src/x/tested.rs', 'via_inline_super').sort()).toEqual([
'function helper src/x/tested/shared.rs:1',
'function tested_helper src/x/tested.rs:3',
]);
const ids = cg.getNodesInFile('src/x/tested.rs').map((n) => n.id);
const imported = cg.getOutgoingEdgesFrom(ids).filter((e) => e.kind === 'imports')
.map((e) => cg.getNode(e.target)!).map((t) => `${t.kind} ${t.qualifiedName} ${t.filePath}:${t.startLine}`);
expect(imported).toEqual(['function tested_helper src/x/tested.rs:3']);
});
it('reach an item the module passes on with a `pub use`, or a glob of one', () => {
expect(callsFrom('src/walks.rs', 'via_reexport')).toEqual(['function make_list src/types/list.rs:1']);
expect(callsFrom('src/walks.rs', 'via_glob')).toEqual(['function list src/type_/prelude.rs:1']);
expect(callsFrom('src/walks.rs', 'via_macro_reexport')).toEqual(['function open src/plat/unix.rs:1']);
});
it('bind a `use crate::{Item}` to the crate root\'s item', () => {
const ids = cg.getNodesInFile('src/uses_config.rs').map((n) => n.id);
const imported = cg.getOutgoingEdgesFrom(ids).filter((e) => e.kind === 'imports')
.map((e) => cg.getNode(e.target)!).map((t) => `${t.kind} ${t.qualifiedName} ${t.filePath}:${t.startLine}`);
expect(imported).toEqual(['struct Config src/lib.rs:21']);
}); });
}); });
+220
View File
@@ -0,0 +1,220 @@
/**
* A bare Rust name a `use` binds: the item that `use`'s path names, where
* the `use` is in scope — a block's own `use` before its module's, a rename
* (`use crate::util::take as consume`), an enum's variant — and nothing when
* a local (a parameter, a `let`) binds the name there. Cases from PR #2308 by
* @danusha2345.
*/
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { CodeGraph } from '../src';
let root = '';
let cg: CodeGraph;
beforeAll(async () => {
root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-rust-use-call-'));
const files: Record<string, string> = {
'Cargo.toml': '[package]\nname = "app"\nversion = "0.1.0"\nedition = "2021"\n\n[dependencies]\nexternal = "1"\n',
'src/lib.rs': 'pub mod a;\npub mod util;\npub mod caller;\npub mod child;\npub mod method_only;\npub fn root_take() {}\npub enum Root { RootGood(i32) }\n',
'src/a.rs': 'pub fn take() {}\npub fn consume() {}\npub fn selected() {}\npub enum Noise { take }\n',
'src/util.rs': `pub struct Buf;
impl Buf { pub fn take(&self) {} }
pub fn outer() {
fn take() {}
take();
}
pub fn take() {}
`,
'src/method_only.rs': 'pub struct Buf;\nimpl Buf { pub fn take(&self) {} }\n',
'src/caller.rs': `use crate::util::take;
pub fn imported() { take(); }
pub fn shadowed() {
fn take() {}
take();
}
pub fn parameter(take: fn()) { take(); }
pub fn destructured((take, _): (fn(), i32)) { take(); }
pub fn type_label(callback: fn(take: i32)) { take(); }
pub fn generic<T: Fn(i32)>(callback: T) { take(); }
pub fn after_binding() {
take();
let take = || {};
take();
}
`,
'src/alias.rs': `use crate::util::take as consume;
pub fn aliased() { consume(); }
`,
'src/group.rs': `use crate::{util::{take as selected}, a::{take as other}};
pub fn grouped() { selected(); }
pub fn second() { other(); }
`,
'src/child/mod.rs': `use super::util::take;
use self::util::take as local_take;
pub fn parent() { take(); }
pub fn child() { local_take(); }
`,
'src/child/util.rs': 'pub fn take() {}\n',
'src/scoped.rs': `pub fn first() {
use crate::util::take;
take();
}
pub fn second() {
use crate::a::take;
take();
}
`,
'src/outer_shadow.rs': `fn take() {}
pub fn imported_in_block() {
use crate::util::take;
take();
}
pub fn inner_import(take: fn()) {
{
use crate::util::take;
take();
}
}
pub fn before_binding() {
use crate::util::take;
take();
let take = || {};
}
`,
'src/tests_mod.rs': 'pub fn tested_helper() {}\n#[cfg(test)]\nmod tests {\n use super::tested_helper;\n fn uses() { tested_helper(); }\n}\n',
'src/external.rs': 'use external::take as consume;\npub fn external_call() { consume(); }\n',
'src/models.rs': 'pub enum E { Good(i32) }\n',
'src/model_decoy.rs': 'pub enum E { Real(i32) }\nmod hidden { pub enum E { Good(i32) } }\n',
'src/model_decoy_call.rs': 'use crate::model_decoy::E::Good;\npub fn model_decoy() { Good(1); }\n',
'src/variant.rs': 'use crate::models::E::Good;\npub fn variant() { Good(1); }\n',
'src/variant_alias.rs': 'use crate::models::E::Good as Build;\npub fn alias_variant() { Build(1); }\n',
'src/root_call.rs': 'use crate::root_take as consume;\npub fn root_call() { consume(); }\n',
'src/root_variant.rs': 'use crate::Root::RootGood;\npub fn root_variant() { RootGood(1); }\n',
'src/same_file.rs': 'fn same_take() {}\nuse self::same_take as consume;\npub fn same_file() { consume(); }\n',
'src/inline_reexport.rs': '#[cfg(unix)]\nmod imp { pub fn lone() {} }\npub use imp::*;\nuse self::lone as aliased;\npub fn via_alias() { aliased(); }\npub fn via_path() { crate::inline_reexport::lone(); }\n',
'src/duplicate.rs': '#[cfg(unix)]\npub fn take() {}\n#[cfg(windows)]\npub fn take() {}\n',
'src/duplicate_call.rs': 'use crate::duplicate::take;\npub fn duplicate_call() { take(); }\n',
'tests/bad_inline.rs': 'mod hidden { mod support { pub mod panic; } }\nuse support::panic::test_panic;\npub fn bad_inline() { test_panic(); }\n',
'tests/support/panic.rs': 'pub fn test_panic() {}\n',
'src/sync.rs': 'use crate::a::take;\npub fn synced() { take(); }\n',
'src/unicode.rs': 'use crate::util::take;\npub fn unicode() { let crab = "🦀Я"; take(); }\n',
'src/unicode_scoped.rs': 'pub fn unicode_scoped() { let crab = "🦀Я"; { use crate::util::take as consume; consume(); } }\n',
'crates/one/Cargo.toml': '[package]\nname = "one"\nversion = "0.1.0"\n',
'crates/one/src/lib.rs': 'pub mod util;\n',
'crates/one/src/util.rs': 'pub fn take() {}\npub fn only_in_one() {}\n',
'crates/two/Cargo.toml': '[package]\nname = "two"\nversion = "0.1.0"\n',
'crates/two/src/lib.rs': 'pub mod util;\npub mod caller;\n',
'crates/two/src/util.rs': 'pub fn take() {}\n',
'crates/two/src/caller.rs': 'use crate::util::take;\nuse one::util::only_in_one;\npub fn workspace_call() { take(); }\npub fn cross_crate() { only_in_one(); }\n',
};
for (const [relative, content] of Object.entries(files)) {
fs.mkdirSync(path.dirname(path.join(root, relative)), { recursive: true });
fs.writeFileSync(path.join(root, relative), content);
}
cg = await CodeGraph.init(root, { index: true });
});
afterAll(() => {
cg?.close();
if (root) fs.rmSync(root, { recursive: true, force: true });
});
const calls = (file: string, name: string) => {
const caller = cg.getNodesInFile(file).find((n) => n.name === name && n.kind === 'function');
expect(caller).toBeDefined();
return cg.getOutgoingEdgesFrom([caller!.id]).filter((e) => e.kind === 'calls')
.map((e) => cg.getNode(e.target)!)
.map((n) => `${n.kind} ${n.qualifiedName} ${n.filePath}:${n.startLine}`);
};
const take = 'function take src/util.rs:7';
describe('Rust bare calls bound by use', () => {
it('chooses the imported free function among same-name methods and functions', () => {
expect(calls('src/caller.rs', 'imported')).toEqual([take]);
});
it('follows an alias even when no declaration carries its local name', () => {
expect(calls('src/alias.rs', 'aliased')).toEqual([take]);
expect(calls('src/group.rs', 'grouped')).toEqual([take]);
expect(calls('src/group.rs', 'second')).toEqual(['function take src/a.rs:1']);
});
it('anchors self and super imports on the current module', () => {
expect(calls('src/child/mod.rs', 'parent')).toEqual([take]);
expect(calls('src/child/mod.rs', 'child')).toEqual(['function take src/child/util.rs:1']);
});
it('keeps block-local imports in their own scopes', () => {
expect(calls('src/scoped.rs', 'first')).toEqual([take]);
expect(calls('src/scoped.rs', 'second')).toEqual(['function take src/a.rs:1']);
});
it('does not replace a local function or parameter with an imported function', () => {
expect(calls('src/caller.rs', 'shadowed')).toEqual(['function shadowed::take src/caller.rs:4']);
expect(calls('src/caller.rs', 'parameter')).toEqual([]);
expect(calls('src/caller.rs', 'destructured')).toEqual([]);
expect(calls('src/caller.rs', 'type_label')).toEqual([take]);
expect(calls('src/caller.rs', 'generic')).toEqual([take]);
expect(calls('src/caller.rs', 'after_binding')).toEqual([take]);
});
it('lets a closer use shadow an outer declaration, and ignores later let bindings', () => {
expect(calls('src/outer_shadow.rs', 'imported_in_block')).toEqual([take]);
expect(calls('src/outer_shadow.rs', 'inner_import')).toEqual([take]);
expect(calls('src/outer_shadow.rs', 'before_binding')).toEqual([take]);
});
it('does not guess a target for an alias or an enum variant it cannot follow', () => {
expect(calls('src/external.rs', 'external_call')).toEqual([]);
expect(calls('src/model_decoy_call.rs', 'model_decoy')).toEqual([]);
});
it('leaves an import it cannot follow to the general resolution, as before', () => {
// Another crate of the workspace, named by its package.
expect(calls('crates/two/src/caller.rs', 'cross_crate')).toEqual(['function only_in_one crates/one/src/util.rs:2']);
// A relative path inside an inline module, the usual `mod tests { use super::…; }`.
expect(calls('src/tests_mod.rs', 'uses')).toEqual(['function tested_helper src/tests_mod.rs:1']);
expect(calls('tests/bad_inline.rs', 'bad_inline')).toEqual(['function test_panic tests/support/panic.rs:1']);
});
it('reaches an inline module\'s function through the module\'s re-export', () => {
expect(calls('src/inline_reexport.rs', 'via_alias')).toEqual(['function lone src/inline_reexport.rs:2']);
expect(calls('src/inline_reexport.rs', 'via_path')).toEqual(['function lone src/inline_reexport.rs:2']);
});
it('takes the first of two #[cfg] variants of the imported function', () => {
expect(calls('src/duplicate_call.rs', 'duplicate_call')).toEqual(['function take src/duplicate.rs:2']);
});
it('keeps a variant attached to its imported enum, including a renamed variant', () => {
expect(calls('src/variant.rs', 'variant')).toEqual(['enum_member E::Good src/models.rs:1']);
expect(calls('src/variant_alias.rs', 'alias_variant')).toEqual(['enum_member E::Good src/models.rs:1']);
expect(calls('src/root_variant.rs', 'root_variant')).toEqual(['enum_member Root::RootGood src/lib.rs:7']);
});
it('binds crate-root functions and same-file aliases', () => {
expect(calls('src/root_call.rs', 'root_call')).toEqual(['function root_take src/lib.rs:6']);
expect(calls('src/same_file.rs', 'same_file')).toEqual(['function same_take src/same_file.rs:1']);
});
it('uses UTF-16 source columns after Cyrillic and emoji on the call line', () => {
expect(calls('src/unicode.rs', 'unicode')).toEqual([take]);
expect(calls('src/unicode_scoped.rs', 'unicode_scoped')).toEqual([take]);
});
it('resolves crate relative to the caller rather than another package', () => {
expect(calls('crates/two/src/caller.rs', 'workspace_call'))
.toEqual(['function take crates/two/src/util.rs:1']);
});
it('refreshes the exact binding after a source edit and sync', async () => {
expect(calls('src/sync.rs', 'synced')).toEqual(['function take src/a.rs:1']);
fs.writeFileSync(path.join(root, 'src/sync.rs'), 'use crate::util::take;\npub fn synced() { take(); }\n');
await cg.sync();
expect(calls('src/sync.rs', 'synced')).toEqual([take]);
});
});
+151
View File
@@ -0,0 +1,151 @@
/**
* Where a Rust `use` binds its name, and where a local hides it: a `use` in a
* block binds the name in that block alone, a nested block's own `use` comes
* before its function's and its module's, and a parameter, a `let` (after its
* statement) or a closure's parameter is the local a call reaches — unless a
* `use` in a block inside it is nearer. A `use` written in a comment or a
* string binds nothing, and one in a macro's braces at the module's level is
* the module's. Cases from PR #2308 by @danusha2345, as call edges.
*/
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { CodeGraph } from '../src';
let root = '';
let cg: CodeGraph;
beforeAll(async () => {
root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-rust-use-scopes-'));
const take = 'pub fn take() {}\npub fn noop() {}\n';
const files: Record<string, string> = {
'Cargo.toml': '[package]\nname = "app"\nversion = "0.1.0"\nedition = "2021"\n',
'src/lib.rs': 'pub mod a;\npub mod b;\npub mod c;\npub mod other;\npub mod util;\npub mod real;\npub mod wrong;\n' +
'pub mod blocks;\npub mod params;\npub mod lets;\npub mod literals;\npub mod macros;\npub mod cfgs;\n',
'src/a.rs': take,
'src/b.rs': take,
'src/c.rs': take,
'src/other.rs': take,
'src/util.rs': take,
'src/real.rs': take,
'src/wrong.rs': take,
'src/blocks.rs': `use crate::a::take;
pub fn outer() { take(); }
pub fn inner() { { use crate::b::take; take(); } }
pub fn sibling() { { use crate::b::take; } { take(); } }
pub fn after() { { use crate::b::take; } take(); }
pub fn two() { use crate::c::take; take(); }
pub fn three() { take(); }
`,
'src/params.rs': `use crate::util::take;
pub fn root(take: fn()) { take(); }
pub fn body(take: fn()) { use crate::other::take; take(); }
pub fn block(take: fn()) { { use crate::other::take; take(); } }
pub fn closure() { let _ = |take: fn()| take(); }
pub fn nested(take: fn()) { fn inner() { take(); } }
`,
'src/lets.rs': `use crate::util::take;
use crate::util::noop;
pub fn outer_let() { let take = || {}; take(); }
pub fn initializer() { let take: fn() = { take(); noop }; }
pub fn inner_use() { let take = || {}; { use crate::other::take; take(); } }
pub fn inner_let() { { let take = || {}; take(); } }
pub fn if_let(v: Option<fn()>) { if let Some(take) = v { take(); } }
pub fn after_if_let(v: Option<fn()>) { if let Some(take) = v { } take(); }
pub fn holds_fn() { let take = take; take(); }
pub fn holds_generic() { let take = take::<u8>; take(); }
pub fn holds_path() { let take = crate::other::take; take(); }
`,
'src/literals.rs': `use crate::real::take;
// use crate::wrong::take;
/* use crate::wrong::take; */
const NORMAL: &str = "use crate::wrong::take; { }";
const RAW: &str = r##"use crate::wrong::take; mod fake {"##;
pub fn call() { take(); }
`,
'src/macros.rs': `macro_rules! items { ($($i:item)*) => { $($i)* } }
items! {
use crate::b::take;
}
pub fn call() { take(); }
pub fn local() { use crate::c::take; take(); }
`,
// The braces inside a macro's are its input too: `cfg_if!`'s `use`s are the module's.
'src/cfgs.rs': `cfg_if::cfg_if! {
if #[cfg(unix)] {
use crate::b::take;
} else {
use crate::b::take;
}
}
pub fn call() { take(); }
pub fn local() { use crate::c::take; take(); }
`,
};
for (const [relative, content] of Object.entries(files)) {
fs.mkdirSync(path.dirname(path.join(root, relative)), { recursive: true });
fs.writeFileSync(path.join(root, relative), content);
}
cg = await CodeGraph.init(root, { index: true });
});
afterAll(() => {
cg?.close();
if (root) fs.rmSync(root, { recursive: true, force: true });
});
/** The files of the functions named `take` that the function `name` in `file` calls. */
const takes = (file: string, name: string): string[] => {
const caller = cg.getNodesInFile(file).find((n) => n.name === name && n.kind === 'function');
expect(caller, `${file}:${name}`).toBeDefined();
return cg.getOutgoingEdgesFrom([caller!.id]).filter((e) => e.kind === 'calls')
.map((e) => cg.getNode(e.target)!).filter((n) => n.name === 'take').map((n) => n.filePath).sort();
};
describe('Rust `use` scopes at a call', () => {
it('lets a block\'s own use win where it is, and no further', () => {
expect(takes('src/blocks.rs', 'outer')).toEqual(['src/a.rs']);
expect(takes('src/blocks.rs', 'inner')).toEqual(['src/b.rs']);
expect(takes('src/blocks.rs', 'sibling')).toEqual(['src/a.rs']);
expect(takes('src/blocks.rs', 'after')).toEqual(['src/a.rs']);
expect(takes('src/blocks.rs', 'two')).toEqual(['src/c.rs']);
expect(takes('src/blocks.rs', 'three')).toEqual(['src/a.rs']);
});
it('calls a parameter, unless a use in the body or a block in it is nearer', () => {
expect(takes('src/params.rs', 'root')).toEqual([]);
expect(takes('src/params.rs', 'body')).toEqual(['src/other.rs']);
expect(takes('src/params.rs', 'block')).toEqual(['src/other.rs']);
expect(takes('src/params.rs', 'closure')).toEqual([]);
// A nested `fn` sees no parameter of the function around it.
expect(takes('src/params.rs', 'inner')).toEqual(['src/util.rs']);
});
it('calls a let from its statement on, in its own block', () => {
expect(takes('src/lets.rs', 'outer_let')).toEqual([]);
expect(takes('src/lets.rs', 'initializer')).toEqual(['src/util.rs']);
expect(takes('src/lets.rs', 'inner_use')).toEqual(['src/other.rs']);
expect(takes('src/lets.rs', 'inner_let')).toEqual([]);
expect(takes('src/lets.rs', 'if_let')).toEqual([]);
expect(takes('src/lets.rs', 'after_if_let')).toEqual(['src/util.rs']);
});
it('calls the function a let holds by its path', () => {
// serde's tests: `let test = test::<i8>; test(…)`.
expect(takes('src/lets.rs', 'holds_fn')).toEqual(['src/util.rs']);
expect(takes('src/lets.rs', 'holds_generic')).toEqual(['src/util.rs']);
expect(takes('src/lets.rs', 'holds_path')).toEqual(['src/other.rs']);
});
it('reads no use in a comment or a string', () => {
expect(takes('src/literals.rs', 'call')).toEqual(['src/real.rs']);
});
it('counts a use in a macro\'s braces at the module\'s level for the module', () => {
expect(takes('src/macros.rs', 'call')).toEqual(['src/b.rs']);
expect(takes('src/macros.rs', 'local')).toEqual(['src/c.rs']);
expect(takes('src/cfgs.rs', 'call')).toEqual(['src/b.rs']);
expect(takes('src/cfgs.rs', 'local')).toEqual(['src/c.rs']);
});
});
+24 -1
View File
@@ -147,13 +147,36 @@ function parseWorkspaceMembers(cargoToml: string): string[] {
return extractQuotedValues(membersValue); return extractQuotedValues(membersValue);
} }
function parsePackageName(cargoToml: string): string | null { /** A manifest's `[package]` name, wherever the section writes it (after a `categories = [...]` too). */
export function parsePackageName(cargoToml: string): string | null {
const packageSection = getSection(cargoToml, 'package'); const packageSection = getSection(cargoToml, 'package');
if (!packageSection) return null; if (!packageSection) return null;
const packageNameMatch = packageSection.match(/name\s*=\s*["']([^"'\n]+)["']/); const packageNameMatch = packageSection.match(/name\s*=\s*["']([^"'\n]+)["']/);
return packageNameMatch?.[1]?.trim() ?? null; return packageNameMatch?.[1]?.trim() ?? null;
} }
/**
* The target roots a manifest names with a `path` of their own: `[lib] path =
* "…"` and each `[[bin]] path = "…"` (ripgrep's `crates/core/main.rs`), as
* written — relative to the manifest's directory.
*/
export function parseTargetPaths(cargoToml: string): { lib?: string; bins: string[] } {
const out: { lib?: string; bins: string[] } = { bins: [] };
let table: 'lib' | 'bin' | null = null;
for (const raw of cargoToml.split(/\r?\n/)) {
const line = raw.replace(/#.*$/, '').trim();
if (line.startsWith('[')) {
table = line === '[lib]' ? 'lib' : line === '[[bin]]' ? 'bin' : null;
continue;
}
const at = table && /^path\s*=\s*["']([^"'\n]+)["']/.exec(line)?.[1]?.replace(/\\/g, '/');
if (!at) continue;
if (table === 'lib') out.lib = at;
else out.bins.push(at);
}
return out;
}
function addCrateAlias(map: Map<string, string>, crateName: string, memberPath: string): void { function addCrateAlias(map: Map<string, string>, crateName: string, memberPath: string): void {
const normalized = crateName.replace(/-/g, '_'); const normalized = crateName.replace(/-/g, '_');
map.set(crateName, memberPath); map.set(crateName, memberPath);
+15 -217
View File
@@ -31,10 +31,9 @@ import {
reExportSource, reExportSource,
pythonFunctionModuleAlias, pythonFunctionModuleAlias,
isPythonFromImportModule, isPythonFromImportModule,
isRustAssociatedItem,
tsGlobalScope, tsGlobalScope,
isLexicallyReachable, rustPathTarget,
rustItemPlacement, isRustNameFromStd,
} from './name-matcher'; } from './name-matcher';
import { JS_ARRAY_METHODS } from './js-builtins'; import { JS_ARRAY_METHODS } from './js-builtins';
@@ -1093,9 +1092,6 @@ export const ESM_IMPORT_LANGUAGES: ReadonlySet<Language> = new Set<Language>([
'typescript', 'tsx', 'javascript', 'jsx', 'arkts', 'svelte', 'vue', 'astro', 'typescript', 'tsx', 'javascript', 'jsx', 'arkts', 'svelte', 'vue', 'astro',
]); ]);
/** Rust path roots that always name a standard-library crate. */
const RUST_STDLIB_ROOTS = new Set(['std', 'core', 'alloc', 'proc_macro']);
/** /**
* Check if an import is external (npm package, etc.) * Check if an import is external (npm package, etc.)
* *
@@ -3678,147 +3674,21 @@ function resolvePythonAbsoluteModule(
} }
/** /**
* Resolve a Rust qualified reference `A::B::C` by mapping the MODULE prefix * Resolve a Rust qualified reference `A::B::C` to the item module `A::B`
* (`A::B`) to a file and finding the leaf symbol (`C`) in it. This is the Rust * holds by the name `C` (rustPathTarget walks the modules as Rust does). This
* analog of {@link resolvePythonModuleMember} / {@link resolveGoCrossPackageReference} * is the Rust analog of {@link resolvePythonModuleMember} /
* and the precise answer to common-name re-exports (`pub use self::read::read`) * {@link resolveGoCrossPackageReference} and the precise answer to common-name
* that name-matching can't disambiguate. Returns null when the prefix isn't a * re-exports (`pub use self::read::read`) that name-matching can't
* real module path (e.g. `Widget::new` — `Widget` is a struct, not a module), * disambiguate. Returns null when the prefix isn't a module path the project
* so associated-function calls and enum-variant paths fall through untouched. * holds (e.g. `Widget::new` — `Widget` is a struct, not a module), so
* associated-function calls and enum-variant paths fall through untouched.
*/ */
function resolveRustPathReference( function resolveRustPathReference(
ref: UnresolvedRef, ref: UnresolvedRef,
context: ResolutionContext context: ResolutionContext
): ResolvedRef | null { ): ResolvedRef | null {
const segments = ref.referenceName.split('::').filter((s) => s.length > 0); const target = rustPathTarget(ref, context);
if (segments.length < 2) return null; return target ? { original: ref, targetNodeId: target.id, confidence: 0.9, resolvedBy: 'import' } : null;
const leaf = segments[segments.length - 1]!;
const modSegs = segments.slice(0, -1);
const file = resolveRustModuleFile(modSegs, ref.filePath, context);
if (!file) return null;
// The module's own item. A path never names a method (that is
// `Type::method`), nor a fn local to another fn's body: the file's first
// same-named node was taken, so an `impl Buf { fn take }` above
// `pub fn take` caught every `crate::util::take(…)`. Two module-level items
// of one name are `#[cfg]` variants of it; the first one stands for both.
// With none, the path reaches an inline module's item through a re-export
// (`#[cfg(…)] mod imp { … }` + `pub use imp::*;`), never an associated item
// of an `impl` or `trait` body: serde's `use crate::de::Error` is not
// `de/mod.rs`'s `impl SeqAccess`'s `type Error`.
const candidates = context.getNodesInFile(file).filter(
(n) =>
n.name === leaf &&
!isRustAssociatedItem(n) &&
(n.kind === 'function' ||
n.kind === 'struct' ||
n.kind === 'union' ||
n.kind === 'enum' ||
n.kind === 'trait' ||
n.kind === 'type_alias' ||
n.kind === 'constant' ||
n.kind === 'class' ||
n.kind === 'interface') &&
isLexicallyReachable(n, ref, context)
);
const target =
candidates.find((n) => rustItemPlacement(n, context) === 'top') ??
candidates.find((n) => rustItemPlacement(n, context) === 'inline');
if (!target) return null;
// A path whose head a `use` binds from the standard library — `fs::read`
// under `use std::fs;`, `error::Error` under `use std::{error, io};` — is
// std's, whatever the module the project has of that name: `fs/mod.rs`'s own
// `read` is not `std::fs::read`. (Asked only here, where it changes the
// answer: the check reads the file's `use`s.)
const head = modSegs[0]!;
if (!/^(?:crate|self|super)$/.test(head) && isBoundToOutOfRepoImport({ ...ref, referenceName: head }, context)) return null;
return { original: ref, targetNodeId: target.id, confidence: 0.9, resolvedBy: 'import' };
}
/** The crate-root directory (holds `lib.rs`/`main.rs`), walking up from a file. */
function rustCrateRootDir(fromFileAbs: string, context: ResolutionContext): string | null {
const projectRoot = context.getProjectRoot();
const toRel = (p: string) => path.relative(projectRoot, p).replace(/\\/g, '/');
let dir = path.dirname(fromFileAbs);
for (let i = 0; i < 64; i++) {
if (context.fileExists(toRel(path.join(dir, 'lib.rs'))) ||
context.fileExists(toRel(path.join(dir, 'main.rs')))) {
return dir;
}
const parent = path.dirname(dir);
if (parent === dir) return null;
dir = parent;
}
return null;
}
/** Directory under which the current file's module declares its SUBMODULES. */
function rustSelfModuleDir(fromFileAbs: string): string {
const base = path.basename(fromFileAbs);
const dir = path.dirname(fromFileAbs);
// mod.rs / lib.rs / main.rs own their directory; `foo.rs`'s submodules live in `foo/`.
if (base === 'mod.rs' || base === 'lib.rs' || base === 'main.rs') return dir;
return path.join(dir, base.replace(/\.rs$/, ''));
}
/**
* Resolve a Rust module path (segments WITHOUT the leaf symbol) to the file of
* the last module segment — `crate::a::b` → `<crate>/a/b.rs` (or `.../b/mod.rs`).
* Anchors on `crate` / `self` / `super`; a bare path is tried crate-relative.
*/
function resolveRustModuleFile(
segments: string[],
fromFile: string,
context: ResolutionContext
): string | null {
if (segments.length === 0) return null;
const projectRoot = context.getProjectRoot();
const fromAbs = path.join(projectRoot, fromFile);
const toRel = (p: string) => path.relative(projectRoot, p).replace(/\\/g, '/');
// Walk a sequence of module segments down from `startDir`, mapping each to a
// `<seg>.rs` or `<seg>/mod.rs` file. Returns the leaf module's file, or null
// if `startDir` is null or any segment has no file on disk.
const resolveUnder = (startDir: string | null, rest: string[]): string | null => {
if (!startDir) return null;
let dir = startDir;
let targetFile: string | null = null;
for (const seg of rest) {
if (seg === 'self' || seg === 'crate' || seg === 'super') continue;
const asFile = toRel(path.join(dir, seg + '.rs'));
const asMod = toRel(path.join(dir, seg, 'mod.rs'));
if (context.fileExists(asFile)) targetFile = asFile;
else if (context.fileExists(asMod)) targetFile = asMod;
else return null;
dir = path.join(dir, seg);
}
return targetFile;
};
const first = segments[0]!;
if (first === 'crate') {
return resolveUnder(rustCrateRootDir(fromAbs, context), segments.slice(1));
}
if (first === 'self') {
return resolveUnder(rustSelfModuleDir(fromAbs), segments.slice(1));
}
if (first === 'super') {
let supers = 0;
while (segments[supers] === 'super') supers++;
let dir: string | null = rustSelfModuleDir(fromAbs);
for (let s = 0; s < supers && dir; s++) dir = path.dirname(dir);
return resolveUnder(dir, segments.slice(supers));
}
// Bare path. In expression position (`submodule::item()` — the router-assembly
// and general cross-module-call pattern) the prefix is a SUBMODULE of the
// current module, i.e. 2018 `self::`-relative — so try self-relative FIRST.
// Fall back to crate-relative for 2015-edition / crate-root items. External
// crate paths (`serde::de::Error`) miss both and fall through to name-matching.
return (
resolveUnder(rustSelfModuleDir(fromAbs), segments) ??
resolveUnder(rustCrateRootDir(fromAbs, context), segments)
);
} }
/** /**
@@ -4233,70 +4103,6 @@ function resolveStaticMember(
return candidates[0]; return candidates[0];
} }
/**
* Rust `use` declarations, flattened to `localName → full path`.
*
* Rust is the one supported language with NO `ImportMapping` extraction (see
* `extractImportMappings`), so this is the only channel that can tell whether
* a bare type name in a Rust file was brought in by a `use`. Handles nested
* groups (`use a::{b::C, d as E}`), globs (skipped — they bind no single
* name), and `as` aliases.
*/
function collectRustUseBindings(content: string): Map<string, string> {
const out = new Map<string, string>();
// Expand one level of `{...}` at a time so `a::{b::{C, D}, E}` flattens.
const expand = (spec: string): string[] => {
const open = spec.indexOf('{');
if (open === -1) return [spec.trim()];
const prefix = spec.slice(0, open);
let depth = 0;
let close = -1;
for (let i = open; i < spec.length; i++) {
if (spec[i] === '{') depth++;
else if (spec[i] === '}') {
depth--;
if (depth === 0) { close = i; break; }
}
}
if (close === -1) return [];
const suffix = spec.slice(close + 1);
const inner = spec.slice(open + 1, close);
const parts: string[] = [];
let depth2 = 0;
let start = 0;
for (let i = 0; i <= inner.length; i++) {
const ch = inner[i];
if (ch === '{') depth2++;
else if (ch === '}') depth2--;
if (i === inner.length || (ch === ',' && depth2 === 0)) {
const seg = inner.slice(start, i).trim();
if (seg) parts.push(seg);
start = i + 1;
}
}
return parts.flatMap((p) => expand(prefix + p + suffix));
};
// `use` items end at the first `;`. Attributes/visibility (`pub use`) are
// irrelevant to the binding itself.
const useRe = /(^|\n)\s*(?:pub(?:\([^)]*\))?\s+)?use\s+([^;]+);/g;
let m: RegExpExecArray | null;
while ((m = useRe.exec(content)) !== null) {
for (const spec of expand(m[2]!.replace(/\s+/g, ' '))) {
const aliasMatch = /^(.*?)\s+as\s+([A-Za-z_]\w*)$/.exec(spec);
const rawPath = (aliasMatch ? aliasMatch[1]! : spec).trim();
if (!rawPath || rawPath.endsWith('*')) continue;
const segments = rawPath.split('::').map((s) => s.trim()).filter(Boolean);
const leaf = segments[segments.length - 1];
if (!leaf) continue;
const local = aliasMatch ? aliasMatch[2]! : leaf;
out.set(local, segments.join('::'));
}
}
return out;
}
/** /**
* Is `name`, as used in `ref`'s file, bound by an import whose module lives * Is `name`, as used in `ref`'s file, bound by an import whose module lives
* OUTSIDE the repository? * OUTSIDE the repository?
@@ -4335,17 +4141,9 @@ export function isBoundToOutOfRepoImport(
const name = ref.referenceName; const name = ref.referenceName;
if (name.includes('::') || name.includes('.')) return false; // qualified refs resolve by path if (name.includes('::') || name.includes('.')) return false; // qualified refs resolve by path
if (ref.language === 'rust') { // (A 2015-edition crate-relative path can shadow a stdlib root with a local
const content = context.readFile(ref.filePath); // module of the same name: a `use core::…` with a `mod core;` is the crate's.)
if (!content) return false; if (ref.language === 'rust') return isRustNameFromStd(ref, context);
const usePath = collectRustUseBindings(content).get(name);
if (!usePath) return false;
const segments = usePath.split('::');
if (segments.length < 2 || !RUST_STDLIB_ROOTS.has(segments[0]!)) return false;
// 2015-edition crate-relative paths can shadow a stdlib root with a local
// module of the same name — if the path walks to a real file, it's local.
return resolveRustModuleFile(segments.slice(0, -1), ref.filePath, context) === null;
}
if (!ESM_IMPORT_LANGUAGES.has(ref.language)) return false; if (!ESM_IMPORT_LANGUAGES.has(ref.language)) return false;
for (const imp of context.getImportMappings(ref.filePath, ref.language)) { for (const imp of context.getImportMappings(ref.filePath, ref.language)) {
+9 -1
View File
@@ -21,7 +21,7 @@ import {
isImportableKind, isImportableKind,
CPP_DEFINE_SIGNATURE, CPP_DEFINE_SIGNATURE,
} from './types'; } from './types';
import { isPythonSelfCall, matchJsStoreBindingCall, isUnresolvedJsMemberCall, matchObjectPathCall, thisScopeCaller, isVisibleAcrossFiles, matchReference, matchByFilePath, matchFunctionRef, matchDottedCallChain, matchScopedCallChain, matchMethodCall, sameLanguageFamily, crossesCodeBoundary, gateLanguageMatch, dumpNameMatcherProfile, clearNameMatcherMemos, isRustNameInScope, isRustForeignImplCallable, isRustPathCallReachable, rustChainedCallByName, CASE_INSENSITIVE_LANGUAGES, isDartMemberRead, matchDartMemberRead, isDartChainLink, matchDartChainLink, isDartAnnotation, matchDartAnnotation, isStdMethodName, isGoUnknownQualified, isGoProjectLocalCall, GO_STDLIB_PACKAGES, isGoBareName, goTypePositionTarget, goDotImportedSupertype, GO_TYPE_KINDS, matchGoAssertedCall, matchGoIndexedCall, matchGoChainedCall, preferGoBuildTwin, isGoAssertedLocal, isGoLocallyBound, isPythonFromImportModule, pythonAttrRefResolves, matchPythonTypedLocalCall, isImportShadowedByJsBinding, isManualMockPath, isTestSuitePath, matchNewReceiverCall, newReceiverClass, NEW_RECEIVER_SHAPE, isGuessedRecordAccessor, matchKotlinReceiverChain } from './name-matcher'; import { isPythonSelfCall, matchJsStoreBindingCall, isUnresolvedJsMemberCall, matchObjectPathCall, thisScopeCaller, isVisibleAcrossFiles, matchReference, matchByFilePath, matchFunctionRef, matchDottedCallChain, matchScopedCallChain, matchMethodCall, sameLanguageFamily, crossesCodeBoundary, gateLanguageMatch, dumpNameMatcherProfile, clearNameMatcherMemos, isRustNameInScope, isRustForeignImplCallable, isRustPathCallReachable, rustChainedCallByName, matchRustUseBoundName, CASE_INSENSITIVE_LANGUAGES, isDartMemberRead, matchDartMemberRead, isDartChainLink, matchDartChainLink, isDartAnnotation, matchDartAnnotation, isStdMethodName, isGoUnknownQualified, isGoProjectLocalCall, GO_STDLIB_PACKAGES, isGoBareName, goTypePositionTarget, goDotImportedSupertype, GO_TYPE_KINDS, matchGoAssertedCall, matchGoIndexedCall, matchGoChainedCall, preferGoBuildTwin, isGoAssertedLocal, isGoLocallyBound, isPythonFromImportModule, pythonAttrRefResolves, matchPythonTypedLocalCall, isImportShadowedByJsBinding, isManualMockPath, isTestSuitePath, matchNewReceiverCall, newReceiverClass, NEW_RECEIVER_SHAPE, isGuessedRecordAccessor, matchKotlinReceiverChain } from './name-matcher';
import { isVisibleCppMacro, clearCppMacroVisibility } from './cpp-macro-visibility'; import { isVisibleCppMacro, clearCppMacroVisibility } from './cpp-macro-visibility';
import { isCppConstructorRef, matchCppConstructor } from './cpp-constructor'; import { isCppConstructorRef, matchCppConstructor } from './cpp-constructor';
import { isCppSupertypeRef, matchCppSupertype, clearCppSupertypeMemos } from './cpp-supertypes'; import { isCppSupertypeRef, matchCppSupertype, clearCppSupertypeMemos } from './cpp-supertypes';
@@ -1646,6 +1646,14 @@ export class ReferenceResolver {
const rubyPath = matchRubyConstantPathRef(ref, this.context); const rubyPath = matchRubyConstantPathRef(ref, this.context);
if (rubyPath !== undefined) return this.gateLanguage(rubyPath, ref); if (rubyPath !== undefined) return this.gateLanguage(rubyPath, ref);
// A bare Rust name a `use` renames (`use crate::util::take as consume;
// consume()`) is the item its path names, which no declaration carries by
// that name — the pre-filter below would drop it — and a call to a name a
// local binds where it is written (a parameter, a `let`, a closure's) is
// to that local, whatever the file imports (see matchRustUseBoundName).
const rustBound = matchRustUseBoundName(ref, this.context);
if (rustBound !== undefined) return this.gateLanguage(rustBound, ref);
// Fast pre-filter: skip if no symbol with this name exists anywhere // Fast pre-filter: skip if no symbol with this name exists anywhere
// AND the name doesn't match a local import. The import escape is // AND the name doesn't match a local import. The import escape is
// necessary because re-export rename chains (`import { login } // necessary because re-export rename chains (`import { login }
+439 -60
View File
@@ -25,8 +25,8 @@ import { isCFeatureVariant, isOffByDefault } from './c-feature-variants';
import { isTestFile, isTestPath } from '../search/query-utils'; import { isTestFile, isTestPath } from '../search/query-utils';
import { inBlock, isDeclarationFile, isMergedAmbientDeclaration, scanGlobalScope, type GlobalScopeScan } from './ambient-modules'; import { inBlock, isDeclarationFile, isMergedAmbientDeclaration, scanGlobalScope, type GlobalScopeScan } from './ambient-modules';
import { isMinifiedContent } from '../extraction/generated-detection'; import { isMinifiedContent } from '../extraction/generated-detection';
import { getCargoWorkspaceCrateMap } from './frameworks/cargo-workspace'; import { getCargoWorkspaceCrateMap, parsePackageName, parseTargetPaths } from './frameworks/cargo-workspace';
import { readRustScopes, rustBindingAt, rustInnerBlockAt, rustBoundsOf, rustFnAt, rustImplAt, splitRustTopLevel, type RustBinding, type RustPatternSite, type RustScopes } from './rust-scopes'; import { blankRustCode, readRustScopes, rustBindingAt, rustInnerBlockAt, rustBoundsOf, rustFnAt, rustImplAt, splitRustTopLevel, type RustBinding, type RustPatternSite, type RustScopes } from './rust-scopes';
/** /**
* Ceiling on how many same-named definitions a FUZZY name-match strategy will * Ceiling on how many same-named definitions a FUZZY name-match strategy will
* score. A name defined more times than this is "ubiquitous" — a method/symbol * score. A name defined more times than this is "ubiquitous" — a method/symbol
@@ -8255,6 +8255,8 @@ interface RustUses {
boundFrom: Map<string, Set<string>>; boundFrom: Map<string, Set<string>>;
/** Each name a project `use` binds → the paths that bind it (`Receiver` → `crate::sync::watch::Receiver`). */ /** Each name a project `use` binds → the paths that bind it (`Receiver` → `crate::sync::watch::Receiver`). */
paths: Map<string, RustUsePath[]>; paths: Map<string, RustUsePath[]>;
/** Each name a `use` from the standard library binds → those paths (`fs` → `std::fs`). */
stdPaths: Map<string, RustUsePath[]>;
/** The modules its project `use … ::*` globs bring in, as paths. */ /** The modules its project `use … ::*` globs bring in, as paths. */
globPaths: RustUsePath[]; globPaths: RustUsePath[];
/** The modules the file declares inline (`mod tests { … }`), in source order. */ /** The modules the file declares inline (`mod tests { … }`), in source order. */
@@ -8274,6 +8276,19 @@ interface RustUsePath {
at: number; at: number;
/** A `pub use` (of any reach), which a glob over its module passes on. */ /** A `pub use` (of any reach), which a glob over its module passes on. */
pub: boolean; pub: boolean;
/**
* The braces of the block it is written in — a function's body, a block in
* one — where alone it binds its name, rather than in its module. Undefined
* for a `use` of the module itself: at a file's top level, in an inline
* `mod … { }`, or in a macro's braces there (`cfg_rt! { use …; }`).
*/
scope?: RustUseScope;
}
/** A block a `use` is written in, by the offsets of its braces. */
interface RustUseScope {
open: number;
close: number;
} }
/** An inline `mod name { … }`, by the offsets of its braces. */ /** An inline `mod name { … }`, by the offsets of its braces. */
@@ -8364,6 +8379,10 @@ interface RustCrate {
keys: Map<string, string>; keys: Map<string, string>;
/** The project crates its `[dependencies]` name, by directory: the ones it can re-export from. */ /** The project crates its `[dependencies]` name, by directory: the ones it can re-export from. */
deps: Set<string>; deps: Set<string>;
/** The library root its manifest gives a `path` of its own (`[lib] path = …`), as a project file. */
lib?: string;
/** The binaries' roots its manifest gives a `path` (ripgrep's `crates/core/main.rs`), as project files. */
bins: string[];
} }
interface RustCrateGraph { interface RustCrateGraph {
@@ -8393,9 +8412,12 @@ function rustCrateGraph(context: ResolutionContext): RustCrateGraph {
const add = (dir: string): boolean => { const add = (dir: string): boolean => {
if (crates.has(dir)) return true; if (crates.has(dir)) return true;
const text = context.readFile(dir === '' ? 'Cargo.toml' : `${dir}/Cargo.toml`); const text = context.readFile(dir === '' ? 'Cargo.toml' : `${dir}/Cargo.toml`);
const name = text ? /\[package\][^[]*?\bname\s*=\s*"([^"]+)"/.exec(text)?.[1]?.replace(/-/g, '_') : undefined; // By its section: axum-core's manifest writes `categories = [ … ]` above its `name`.
const name = text ? parsePackageName(text)?.replace(/-/g, '_') : undefined;
if (!name) return false; if (!name) return false;
crates.set(dir, { name, keys: new Map(), deps: new Set() }); const targets = parseTargetPaths(text!);
const file = (rel: string): string => path.posix.normalize(dir === '' ? rel : `${dir}/${rel}`);
crates.set(dir, { name, keys: new Map(), deps: new Set(), lib: targets.lib && file(targets.lib), bins: targets.bins.map(file) });
manifests.set(dir, text!); manifests.set(dir, text!);
if (!byName.has(name)) byName.set(name, dir); if (!byName.has(name)) byName.set(name, dir);
return true; return true;
@@ -8486,7 +8508,7 @@ function rustProjectCrates(context: ResolutionContext): Set<string> {
if (hit) return hit; if (hit) return hit;
const crates = new Set<string>(); const crates = new Set<string>();
// The manifests are not indexed files: the root's package, and the workspace's members. // The manifests are not indexed files: the root's package, and the workspace's members.
const root = /\[package\][^[]*?\bname\s*=\s*"([^"]+)"/.exec(context.readFile('Cargo.toml') ?? '')?.[1]; const root = parsePackageName(context.readFile('Cargo.toml') ?? '');
if (root) crates.add(root.replace(/-/g, '_')); if (root) crates.add(root.replace(/-/g, '_'));
for (const name of getCargoWorkspaceCrateMap(context).keys()) crates.add(name.replace(/-/g, '_')); for (const name of getCargoWorkspaceCrateMap(context).keys()) crates.add(name.replace(/-/g, '_'));
RUST_CRATES.set(context, crates); RUST_CRATES.set(context, crates);
@@ -8504,7 +8526,7 @@ function rustUsesOf(filePath: string, context: ResolutionContext): RustUses {
if (hit) return hit; if (hit) return hit;
const uses: RustUses = { const uses: RustUses = {
names: new Set(), globs: new Set(), external: new Set(), bound: new Set(), boundFrom: new Map(), 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: [], paths: new Map(), stdPaths: new Map(), globPaths: [], blocks: [], pathMods: new Set(), balanced: true, lineStarts: [],
}; };
const leaves = (tree: string): string[] => [ const leaves = (tree: string): string[] => [
...[...tree.matchAll(/([A-Za-z_]\w*)\s*(?=[,}]|$|\s+as\b)|\bas\s+([A-Za-z_]\w*)/g)] ...[...tree.matchAll(/([A-Za-z_]\w*)\s*(?=[,}]|$|\s+as\b)|\bas\s+([A-Za-z_]\w*)/g)]
@@ -8512,10 +8534,28 @@ function rustUsesOf(filePath: string, context: ResolutionContext): RustUses {
// `use std::io::{self, Read}` binds `io` too. // `use std::io::{self, Read}` binds `io` too.
...[...tree.matchAll(/([A-Za-z_]\w*)\s*::\s*\{[^{}]*\bself\b/g)].map((m) => m[1]!), ...[...tree.matchAll(/([A-Za-z_]\w*)\s*::\s*\{[^{}]*\bself\b/g)].map((m) => m[1]!),
]; ];
// Comments first: a doc comment's prose ("…use the Option…") is not a `use`. const bind = (map: Map<string, RustUsePath[]>, local: string, path: RustUsePath): void => {
const text = stripCommentsForRegex(context.readFile(filePath) ?? '', 'rust'); const list = map.get(local);
if (list) list.push(path);
else map.set(local, [path]);
};
// Comments and strings blanked first: a doc comment's prose ("…use the
// Option…"), a string's `"use crate::x;"` is no `use`.
const text = blankRustCode(context.readFile(filePath) ?? '');
const dependencies = rustDependencyCrates(context); const dependencies = rustDependencyCrates(context);
for (const m of text.matchAll(/(?:^|[;{}\s])use\s+([^;]{1,2000});/g)) { const matches = [...text.matchAll(/(?:^|[;{}\s])use\s+([^;]{1,2000});/g)];
const kws = matches.map((m) => m.index! + m[0].indexOf('use'));
// The braces are read for inline modules, and for the block a `use` is in
// when one may be in a block: indented, or after a brace on its line.
let braces: number[] | undefined;
const codeBraces = (): number[] => (braces ??= rustCodeBraces(text));
const layout = rustModBlocks(text, codeBraces);
uses.blocks = layout.blocks;
uses.balanced = layout.balanced;
// (Braces that don't pair say nothing: every `use` then counts for its module.)
const nested = kws.some((kw) => /^\s|[{}]/.test(text.slice(text.lastIndexOf('\n', kw - 1) + 1, kw)));
const scopes = nested ? rustUseScopes(text, kws, codeBraces(), new Set(layout.blocks.map((b) => b.open))) : null;
matches.forEach((m, i) => {
const tree = m[1]!; const tree = m[1]!;
const root = /^\s*(?:::)?([A-Za-z_]\w*)/.exec(tree)?.[1] ?? ''; const root = /^\s*(?:::)?([A-Za-z_]\w*)/.exec(tree)?.[1] ?? '';
// Outside: the standard library or a crate the manifests depend on — not a // Outside: the standard library or a crate the manifests depend on — not a
@@ -8523,20 +8563,25 @@ function rustUsesOf(filePath: string, context: ResolutionContext): RustUses {
const outside = root === 'std' || root === 'core' || root === 'alloc' || (root !== '' && dependencies.has(root)); 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 // 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`). // workspace doesn't list is the project's (a facade's `pub use shared::Thing`).
const kw = m.index! + m[0].indexOf('use'); const kw = kws[i]!;
const pub = /\bpub(?:\s*\([^()]*\))?\s*$/.test(text.slice(Math.max(0, kw - 48), kw)); const pub = /\bpub(?:\s*\([^()]*\))?\s*$/.test(text.slice(Math.max(0, kw - 48), kw));
const scope = scopes?.[i];
for (const { segs, local } of rustUseTreeBindings(tree)) { for (const { segs, local } of rustUseTreeBindings(tree)) {
// A macro's own `use $crate::…` names no module of this file's. // 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; if (segs.length === 0 || segs[0]!.startsWith('$')) continue;
const path: RustUsePath = { segs, at: m.index!, pub }; const path: RustUsePath = scope ? { segs, at: m.index!, pub, scope } : { segs, at: m.index!, pub };
// The standard library's: what its name means where the `use` reaches.
if (RUST_STD_ROOTS.has(segs[0]!)) {
if (local !== null) bind(uses.stdPaths, local, path);
continue;
}
if (local === null) uses.globPaths.push(path); if (local === null) uses.globPaths.push(path);
else if (uses.paths.has(local)) uses.paths.get(local)!.push(path); else bind(uses.paths, local, path);
else uses.paths.set(local, [path]);
} }
// The items it binds: each leaf (`as` aliases by their alias), never the path it walks. // The items it binds: each leaf (`as` aliases by their alias), never the path it walks.
if (outside) { if (outside) {
for (const id of leaves(tree)) uses.external.add(id); for (const id of leaves(tree)) uses.external.add(id);
continue; return;
} }
for (const id of leaves(tree)) { for (const id of leaves(tree)) {
uses.bound.add(id); uses.bound.add(id);
@@ -8546,10 +8591,7 @@ 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 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]!); 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;` // `#[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)) { 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]!); uses.pathMods.add(m[1]!);
@@ -8652,7 +8694,7 @@ function rustCodeBraces(text: string): number[] {
} }
/** The inline `mod name { … }` blocks of a file's text (comments already blanked), in source order. */ /** The inline `mod name { … }` blocks of a file's text (comments already blanked), in source order. */
function rustModBlocks(text: string): { blocks: RustModBlock[]; balanced: boolean } { function rustModBlocks(text: string, codeBraces: () => readonly number[] = () => rustCodeBraces(text)): { blocks: RustModBlock[]; balanced: boolean } {
const heads = new Map<number, string>(); 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]!); 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[] = []; const blocks: RustModBlock[] = [];
@@ -8661,7 +8703,7 @@ function rustModBlocks(text: string): { blocks: RustModBlock[]; balanced: boolea
let inner: RustModBlock | null = null; let inner: RustModBlock | null = null;
let balanced = true; let balanced = true;
// A head whose `{` is in a string is never among the braces. // A head whose `{` is in a string is never among the braces.
for (const at of rustCodeBraces(text)) { for (const at of codeBraces()) {
if (text[at] === '{') { if (text[at] === '{') {
const name = heads.get(at); const name = heads.get(at);
const block: RustModBlock | null = name === undefined ? null : { name, open: at, close: text.length, parent: inner }; const block: RustModBlock | null = name === undefined ? null : { name, open: at, close: text.length, parent: inner };
@@ -8685,6 +8727,55 @@ function rustModBlocks(text: string): { blocks: RustModBlock[]; balanced: boolea
return { blocks, balanced: balanced && open.length === 0 }; return { blocks, balanced: balanced && open.length === 0 };
} }
/**
* The block each `use` at `kws` (ascending offsets) is written in, read from
* the file's code braces: the innermost one around it — a function's body, a
* block in one — that is no module's own. An inline `mod … {` is the module's,
* and a macro's braces (`cfg_rt! {`, `macro_rules! m {`), with every brace in
* them, are the scope around them: tokio's `cfg_sync! { pub use … }` and a
* `cfg_if! { if #[cfg(unix)] { pub use … } }` are the module's own `use`s.
* Undefined for a `use` of its module; null when the braces don't pair.
*/
function rustUseScopes(text: string, kws: readonly number[], braces: readonly number[], modOpens: ReadonlySet<number>): Array<RustUseScope | undefined> | null {
const out: Array<RustUseScope | undefined> = [];
const open: Array<RustUseScope | 'module' | 'macro'> = [];
const innermost = (): RustUseScope | undefined => {
for (let i = open.length - 1; i >= 0; i--) {
const e = open[i]!;
if (e !== 'macro') return e === 'module' ? undefined : e;
}
return undefined;
};
let k = 0;
for (const at of braces) {
while (k < kws.length && kws[k]! < at) out[k++] = innermost();
if (text.charCodeAt(at) === 123) {
open.push(modOpens.has(at) ? 'module'
: open[open.length - 1] === 'macro' || isRustMacroBrace(text, at) ? 'macro' : { open: at, close: text.length });
continue;
}
const closed = open.pop();
if (closed === undefined) return null;
if (typeof closed === 'object') closed.close = at;
}
if (open.length > 0) return null;
while (k < kws.length) out[k++] = innermost();
return out;
}
/** Whether the `{` at `at` opens a macro's input: `name! {`, `path::name! {`, `macro_rules! name {`. */
function isRustMacroBrace(text: string, at: number): boolean {
let p = at - 1;
while (p >= 0 && /\s/.test(text[p]!)) p--;
if (text[p] === '!') return true;
// `macro_rules! name {`: the name, then the `!` before it.
let q = p;
while (q >= 0 && /\w/.test(text[q]!)) q--;
if (q === p) return false;
while (q >= 0 && /\s/.test(text[q]!)) q--;
return text[q] === '!' && /\bmacro_rules\s*$/.test(text.slice(Math.max(0, q - 24), q));
}
/** The innermost inline module whose braces hold `offset`, or null for the file's top level. */ /** 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 { function rustBlockAt(blocks: readonly RustModBlock[], offset: number): RustModBlock | null {
let at: RustModBlock | null = null; let at: RustModBlock | null = null;
@@ -8714,8 +8805,11 @@ const rustParentDir = (p: string): string => p.slice(0, Math.max(0, p.lastIndexO
/** 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`, … */ /** 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 { function isRustTargetRoot(filePath: string, context: ResolutionContext): boolean {
const dir = rustCrateDirOf(filePath, rustCrateGraph(context)); const graph = rustCrateGraph(context);
const dir = rustCrateDirOf(filePath, graph);
if (dir === null) return /(?:^|\/)(?:lib|main)\.rs$/.test(filePath); if (dir === null) return /(?:^|\/)(?:lib|main)\.rs$/.test(filePath);
const crate = graph.crates.get(dir);
if (crate && (crate.lib === filePath || crate.bins.includes(filePath))) return true;
const rel = dir === '' ? filePath : filePath.slice(dir.length + 1); 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); return /^(?:src\/(?:lib|main)\.rs|src\/bin\/[^/]+(?:\.rs|\/main\.rs)|(?:tests|examples|benches)\/[^/]+(?:\.rs|\/main\.rs)|build\.rs)$/.test(rel);
} }
@@ -8742,12 +8836,14 @@ function rustModuleAt(filePath: string, offset: number, context: ResolutionConte
/** /**
* The root of the crate `filePath` is in — what its `crate::` names: a test, * 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 * 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/` * `src/main.rs`); in no crate the manifests name, the nearest `lib.rs` or
* module serves every test that declares it. * `main.rs` above it. 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 { function rustCrateRootModule(filePath: string, context: ResolutionContext): RustModule | null {
const dir = rustCrateDirOf(filePath, rustCrateGraph(context)); const graph = rustCrateGraph(context);
if (dir === null) return null; const dir = rustCrateDirOf(filePath, graph);
if (dir === null) return rustNearestRootModule(filePath, context);
const base = dir === '' ? '' : `${dir}/`; const base = dir === '' ? '' : `${dir}/`;
const rel = filePath.slice(base.length); const rel = filePath.slice(base.length);
const target = /^(src\/bin|tests|examples|benches)\/([^/]+?)(\.rs$|\/)/.exec(rel); const target = /^(src\/bin|tests|examples|benches)\/([^/]+?)(\.rs$|\/)/.exec(rel);
@@ -8755,7 +8851,15 @@ function rustCrateRootModule(filePath: string, context: ResolutionContext): Rust
const file = target[3] === '.rs' ? filePath : `${base}${target[1]}/${target[2]}/main.rs`; 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; return context.fileExists(file) ? { file, dir: rustParentDir(file), block: null, root: true } : null;
} }
if (!rel.startsWith('src/')) return null; if (!rel.startsWith('src/')) {
// A root its manifest places elsewhere (ripgrep's `[[bin]] path =
// "crates/core/main.rs"`): the nearest one whose directory holds the file.
const crate = graph.crates.get(dir);
const holds = (r: string | undefined): r is string =>
r !== undefined && (rustParentDir(r) === '' || filePath.startsWith(`${rustParentDir(r)}/`)) && context.fileExists(r);
const own = [crate?.lib, ...(crate?.bins ?? [])].filter(holds).sort((x, y) => y.length - x.length)[0];
return own ? { file: own, dir: rustParentDir(own), block: null, root: true } : null;
}
const lib = `${base}src/lib.rs`; const lib = `${base}src/lib.rs`;
const main = `${base}src/main.rs`; const main = `${base}src/main.rs`;
const hasLib = context.fileExists(lib); const hasLib = context.fileExists(lib);
@@ -8765,9 +8869,27 @@ function rustCrateRootModule(filePath: string, context: ResolutionContext): Rust
return { file, dir: `${base}src`, block: null, root: true, alt: hasLib && hasMain ? (file === lib ? main : lib) : undefined }; return { file, dir: `${base}src`, block: null, root: true, alt: hasLib && hasMain ? (file === lib ? main : lib) : undefined };
} }
/**
* For a file no manifest the project holds covers (sources indexed without
* their `Cargo.toml`), the crate root nearest above it: the first `lib.rs` or
* `main.rs` up its directories.
*/
function rustNearestRootModule(filePath: string, context: ResolutionContext): RustModule | null {
let dir = rustParentDir(filePath);
for (let i = 0; i < 64; i++) {
for (const name of ['lib.rs', 'main.rs']) {
const file = rustJoin(dir, name);
if (context.fileExists(file)) return { file, dir, block: null, root: true };
}
if (dir === '') return null;
dir = rustParentDir(dir);
}
return null;
}
/** The root of a project crate's library: what a path that starts with the crate's name (`tokio::sync::…`) walks from. */ /** 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 { function rustLibModule(crateDir: string, context: ResolutionContext): RustModule | null {
const file = crateDir === '' ? 'src/lib.rs' : `${crateDir}/src/lib.rs`; const file = rustCrateGraph(context).crates.get(crateDir)?.lib ?? (crateDir === '' ? 'src/lib.rs' : `${crateDir}/src/lib.rs`);
return context.fileExists(file) ? { file, dir: rustParentDir(file), block: null, root: true } : null; return context.fileExists(file) ? { file, dir: rustParentDir(file), block: null, root: true } : null;
} }
@@ -8789,7 +8911,7 @@ function rustSubmodule(m: RustModule, name: string, context: ResolutionContext,
} }
if (depth >= 8) return null; if (depth >= 8) return null;
for (const p of layout.paths.get(name) ?? []) { for (const p of layout.paths.get(name) ?? []) {
if (rustBlockAt(layout.blocks, p.at) !== m.block) continue; if (p.scope || rustBlockAt(layout.blocks, p.at) !== m.block) continue;
const passed = rustWalkModules(p.segs, m, context, depth + 1); const passed = rustWalkModules(p.segs, m, context, depth + 1);
if (passed !== null && passed !== 'outside') return passed; if (passed !== null && passed !== 'outside') return passed;
} }
@@ -8885,7 +9007,8 @@ function rustModuleItems(m: RustModule, name: string, context: ResolutionContext
try { try {
const declared = rustDeclaredItems(m, name, context, publicOnly); const declared = rustDeclaredItems(m, name, context, publicOnly);
if (declared.length > 0) return declared; if (declared.length > 0) return declared;
const here = (p: RustUsePath): boolean => (p.pub || !publicOnly) && rustBlockAt(uses.blocks, p.at) === m.block; // (A `use` in a function's body binds its name there alone.)
const here = (p: RustUsePath): boolean => (p.pub || !publicOnly) && !p.scope && rustBlockAt(uses.blocks, p.at) === m.block;
const named = (uses.paths.get(name) ?? []).filter(here); const named = (uses.paths.get(name) ?? []).filter(here);
const found: Node[] = []; const found: Node[] = [];
for (const p of named) { for (const p of named) {
@@ -8944,12 +9067,28 @@ function rustModuleWithin(inner: RustModule, outer: RustModule): boolean {
return inner.file.startsWith(outer.dir === '' ? '' : `${outer.dir}/`) && !(outer.dir === '' && outer.block !== null); return inner.file.startsWith(outer.dir === '' ? '' : `${outer.dir}/`) && !(outer.dir === '' && outer.block !== null);
} }
/** The items a `use` path names from `from`: its last segment's, in the module the rest walks to. */ /**
* The items a `use` path names from `from`: its last segment's, in the module
* the rest walks to — or, when that segment's parent is an enum the module
* holds, the enum's variant: `use crate::models::Mode::Fast` names `Fast` of
* that `Mode`, never a namesake's.
*/
function rustUsePathItems(segs: readonly string[], from: RustModule, context: ResolutionContext, seen: Set<string>): Node[] | null { function rustUsePathItems(segs: readonly string[], from: RustModule, context: ResolutionContext, seen: Set<string>): Node[] | null {
if (segs.length < 2) return null; if (segs.length < 2) return null;
const mod = rustWalkModules(segs.slice(0, -1), from, context); const mod = rustWalkModules(segs.slice(0, -1), from, context);
if (mod === 'outside') return []; if (mod === 'outside') return [];
return mod === null ? null : rustModuleItems(mod, segs[segs.length - 1]!, context, seen); if (mod !== null) return rustModuleItems(mod, segs[segs.length - 1]!, context, seen);
const owner = segs.length > 2 ? rustWalkModules(segs.slice(0, -2), from, context) : from;
if (owner === null || owner === 'outside') return owner === null ? null : [];
const enums = rustModuleItems(owner, segs[segs.length - 2]!, context, seen)?.filter((n) => n.kind === 'enum') ?? [];
return enums.length === 0 ? null : enums.flatMap((e) => rustEnumVariants(e, segs[segs.length - 1]!, context));
}
/** The variant `name` of a Rust enum: the member written inside its braces, not a namesake's in the same file. */
function rustEnumVariants(e: Node, name: string, context: ResolutionContext): Node[] {
const qualified = `${e.qualifiedName}::${name}`;
return (context.getNodesInFileNamed?.(e.filePath, name) ?? context.getNodesInFile(e.filePath)).filter((n) =>
n.kind === 'enum_member' && n.qualifiedName === qualified && n.startLine >= e.startLine && n.startLine <= (e.endLine ?? e.startLine));
} }
const RUST_USE_ITEMS = new WeakMap<ResolutionContext, Map<string, Set<string> | null>>(); const RUST_USE_ITEMS = new WeakMap<ResolutionContext, Map<string, Set<string> | null>>();
@@ -8969,19 +9108,87 @@ function rustUseItems(name: string, filePath: string, context: ResolutionContext
if (hit !== undefined) return hit; if (hit !== undefined) return hit;
const uses = rustUsesOf(filePath, context); const uses = rustUsesOf(filePath, context);
const paths = uses.balanced ? uses.paths.get(name) : undefined; const paths = uses.balanced ? uses.paths.get(name) : undefined;
let ids: Set<string> | null = paths ? new Set() : null; const ids = paths ? rustPathsItems(paths, filePath, context) : 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); memo.set(key, ids);
return ids; return ids;
} }
/** The ids of what `use` paths of `filePath` name, each walked from the module it is written in; null if one can't be followed. */
function rustPathsItems(paths: readonly RustUsePath[], filePath: string, context: ResolutionContext): Set<string> | null {
const ids = new Set<string>();
for (const p of paths) {
const items = rustUsePathItems(p.segs, rustModuleAt(filePath, p.at, context), context, new Set());
if (items === null) return null;
for (const n of items) ids.add(n.id);
}
return ids;
}
/** The `use`s that bind a name where a reference is (rustVisibleUse). */
interface RustVisibleUse {
/** The project's paths among them (`crate::util::take`, a dependency's). */
paths: RustUsePath[];
/** The standard library's (`use std::fs;`). */
std: RustUsePath[];
/** The braces' `{` of the block they are written in, or -1 for a `use` of the module itself. */
block: number;
}
/**
* The `use`s that bind `name` at `offset` of `filePath`, as Rust scopes them:
* those of the innermost block around it — a function's body, a block in one —
* else those of the module it is written in, at the file's top level or in an
* inline `mod`, whose `use`s neither the modules around it nor those inside it
* see. Undefined when none binds it there, or the file's braces don't close.
*/
function rustVisibleUse(name: string, filePath: string, offset: number, context: ResolutionContext): RustVisibleUse | undefined {
const uses = rustUsesOf(filePath, context);
const project = uses.paths.get(name);
const std = uses.stdPaths.get(name);
if ((!project && !std) || !uses.balanced) return undefined;
const module = rustBlockAt(uses.blocks, offset);
let best: RustVisibleUse | undefined;
const take = (p: RustUsePath, isStd: boolean): void => {
if (p.scope ? offset <= p.scope.open || offset >= p.scope.close : false) return;
if (rustBlockAt(uses.blocks, p.at) !== module) return;
const block = p.scope ? p.scope.open : -1;
if (!best || block > best.block) best = { paths: [], std: [], block };
else if (block < best.block) return;
(isStd ? best.std : best.paths).push(p);
};
for (const p of project ?? []) take(p, false);
for (const p of std ?? []) take(p, true);
return best;
}
const RUST_VISIBLE_ITEMS = new WeakMap<ResolutionContext, Map<string, Set<string> | null>>();
/**
* The ids of what the `use`s binding `name` where `ref` is name (rustVisibleUse):
* `fn first() { use crate::util::take; take() }` reaches util's `take`, the
* next function's `use crate::a::take;` a's. Empty for a name only the
* standard library's `use` binds there. Where no `use` binds it, those of the
* whole file (rustUseItems), as a reference no scope reads.
*/
function rustUseItemsAt(name: string, ref: UnresolvedRef, context: ResolutionContext): Set<string> | null {
const visible = rustVisibleUse(name, ref.filePath, rustRefOffset(ref, context), context);
if (!visible) return rustUseItems(name, ref.filePath, context);
if (visible.paths.length === 0) return new Set();
let memo = RUST_VISIBLE_ITEMS.get(context);
if (!memo) RUST_VISIBLE_ITEMS.set(context, (memo = new Map()));
const key = `${ref.filePath}\0${name}\0${visible.block}\0${visible.paths[0]!.at}`;
const hit = memo.get(key);
if (hit !== undefined) return hit;
const ids = rustPathsItems(visible.paths, ref.filePath, context);
memo.set(key, ids);
return ids;
}
/** The offset of a reference in its file's text (its line's start, plus its column). */
function rustRefOffset(ref: UnresolvedRef, context: ResolutionContext): number {
return (rustUsesOf(ref.filePath, context).lineStarts[ref.line - 1] ?? 0) + Math.max(0, ref.column);
}
const RUST_GLOB_ITEMS = new WeakMap<ResolutionContext, Map<string, Set<string> | null>>(); const RUST_GLOB_ITEMS = new WeakMap<ResolutionContext, Map<string, Set<string> | null>>();
/** /**
@@ -9056,21 +9263,6 @@ export function isRustAssociatedItem(n: Node): boolean {
return n.language === 'rust' && RUST_ASSOCIATED_KINDS.has(n.kind) && n.qualifiedName.includes('::'); return n.language === 'rust' && RUST_ASSOCIATED_KINDS.has(n.kind) && n.qualifiedName.includes('::');
} }
/**
* Where a Rust item sits in its file's module, as a path through the module
* reaches it: written at the file's top level ('top'), or in a module the
* file declares inline (`mod imp { … }`, 'inline'), which adds nothing to its
* qualified name. Null for an item a module path never names: a method, an
* `impl`'s or a trait's associated item, an item of a function body — each
* qualified by its owner.
*/
export function rustItemPlacement(n: Node, context: ResolutionContext): 'top' | 'inline' | null {
if (n.language !== 'rust' || n.qualifiedName !== n.name) return null;
const layout = rustUsesOf(n.filePath, context);
const at = (layout.lineStarts[n.startLine - 1] ?? 0) + Math.max(0, n.startColumn ?? 0);
return rustBlockAt(layout.blocks, at) === null ? 'top' : 'inline';
}
/** Does one of the file's globs bring in this candidate's module (or, for `use super::*`, its parent's)? */ /** Does one of the file's globs bring in this candidate's module (or, for `use super::*`, its parent's)? */
function rustGlobCovers(uses: RustUses, candidate: Node, ref: UnresolvedRef): boolean { function rustGlobCovers(uses: RustUses, candidate: Node, ref: UnresolvedRef): boolean {
if (uses.globs.has(rustModuleName(candidate.filePath))) return true; if (uses.globs.has(rustModuleName(candidate.filePath))) return true;
@@ -9094,6 +9286,7 @@ function rustGlobCovers(uses: RustUses, candidate: Node, ref: UnresolvedRef): bo
* there (rustUseItems). * there (rustUseItems).
*/ */
export function isRustNameInScope(candidate: Node, ref: UnresolvedRef, context: ResolutionContext): boolean { export function isRustNameInScope(candidate: Node, ref: UnresolvedRef, context: ResolutionContext): boolean {
if (RUST_SCOPED_REFS.get(context)?.has(ref)) return true;
if (!isRustItemVisibleFrom(candidate, ref)) return false; if (!isRustItemVisibleFrom(candidate, ref)) return false;
const name = ref.referenceName; const name = ref.referenceName;
// Bare in the SOURCE: the index keeps `crate::error::Result` by its last // Bare in the SOURCE: the index keeps `crate::error::Result` by its last
@@ -9185,15 +9378,27 @@ export function isRustNameInScope(candidate: Node, ref: UnresolvedRef, context:
const uses = rustUsesOf(ref.filePath, context); const uses = rustUsesOf(ref.filePath, context);
const cut = candidate.qualifiedName.lastIndexOf('::'); const cut = candidate.qualifiedName.lastIndexOf('::');
const owner = cut >= 0 ? candidate.qualifiedName.slice(0, cut).split('::').pop()! : ''; const owner = cut >= 0 ? candidate.qualifiedName.slice(0, cut).split('::').pop()! : '';
return (owner !== '' && uses.globs.has(owner)) || (uses.names.has(name) && uses.names.has(owner)); if (!((owner !== '' && uses.globs.has(owner)) || (uses.names.has(name) && uses.names.has(owner)))) return false;
// A `use` of the variant itself names that enum's: `use crate::models::E::Good`
// is `models`'s `E::Good`, not an `E::Good` another module declares.
return !uses.paths.has(name) || rustUseItemsAt(name, ref, context)?.has(candidate.id) !== false;
}
if (candidate.filePath === ref.filePath) {
// A `use` in the block around the reference shadows its module's own item
// of the name: `fn f() { use crate::util::take; take() }` is util's `take`,
// not the file's.
if (candidate.qualifiedName !== name || !RUST_MODULE_ITEM_KINDS.has(candidate.kind)) return true;
const visible = rustVisibleUse(name, ref.filePath, rustRefOffset(ref, context), context);
return !visible || visible.block < 0 || rustUseItemsAt(name, ref, context)?.has(candidate.id) !== false;
} }
if (candidate.filePath === ref.filePath) return true;
const uses = rustUsesOf(ref.filePath, context); const uses = rustUsesOf(ref.filePath, context);
// An item a `use` binds is one its path's crate can name — and, where the // 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. // project holds the modules the path walks, the item of that module: the
// `use` that binds the name where the reference is (a block's own, else its
// module's), else those of the whole file.
const from = RUST_MODULE_ITEM_KINDS.has(candidate.kind) ? uses.boundFrom.get(name) : undefined; 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 && !rustUseCanReach(from, candidate, ref, context)) return false;
if (from && rustUseItems(name, ref.filePath, context)?.has(candidate.id) === false) return false; if (from && rustUseItemsAt(name, ref, context)?.has(candidate.id) === false) return false;
// `use std::task::{Context, Poll}`: the file's `Context` is std's, not tokio's // `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.) // `runtime::context::Context`. (A method call `.env(…)` is no imported name.)
if (uses.external.has(name) && !uses.bound.has(name) && line !== undefined) { if (uses.external.has(name) && !uses.bound.has(name) && line !== undefined) {
@@ -9213,6 +9418,177 @@ export function isRustNameInScope(candidate: Node, ref: UnresolvedRef, context:
return uses.names.has(name) || rustGlobScope(name, candidate, uses, ref, context); return uses.names.has(name) || rustGlobScope(name, candidate, uses, ref, context);
} }
/** What a bare Rust name a `use` renames may be, by the reference's kind. */
const RUST_RENAMED_KINDS: Record<string, ReadonlySet<string>> = {
calls: new Set(['function', 'struct', 'union', 'enum_member', 'variable', 'constant']),
function_ref: new Set(['function']),
references: new Set([...RUST_MODULE_ITEM_KINDS, 'enum_member']),
};
/**
* A bare Rust name as the scope around it binds it, which a match by its name
* can't read:
* - a call to a name a local binds there — a parameter, a `let`, a closure's
* parameter, nearer than any `use` of the name — calls that local: null;
* - a name a `use` renames (`use crate::util::take as consume; consume()`)
* is the item its path names, which no declaration carries by that name,
* or nothing: a rename of a std item, of one the walk can't follow.
* Undefined otherwise.
*/
export function matchRustUseBoundName(ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null | undefined {
const kinds = RUST_RENAMED_KINDS[ref.referenceKind];
if (ref.language !== 'rust' || !kinds || !/^[A-Za-z_]\w*$/.test(ref.referenceName)) return undefined;
const name = ref.referenceName;
// Written bare where the reference is: no method's name, nor a path's first
// or last segment — and a call's own name, with its arguments after it: a
// chained call is recorded where its receiver starts, which may be a local
// of the method's name (`deadline.as_ref().deadline()`).
const line = context.getFileLines?.(ref.filePath)?.[ref.line - 1] ?? context.readFile(ref.filePath)?.split('\n')[ref.line - 1];
if (line === undefined || !line.startsWith(name, ref.column) || /(?:\.|::)\s*$/.test(line.slice(0, ref.column))) return undefined;
const after = line.slice(ref.column + name.length).trimStart();
if (ref.referenceKind === 'calls' ? !/^(?:\(|::\s*<)/.test(after) : /^(?:\.|::)/.test(after)) return undefined;
const offset = rustRefOffset(ref, context);
const visible = rustVisibleUse(name, ref.filePath, offset, context);
const local = ref.referenceKind === 'references' ? null : rustShadowingLocal(name, ref, offset, visible, context);
let target: Node | null | undefined;
if (local) {
// A `let` that holds a function by its path is that function: serde's `let
// test = test::<i8>;` before its `test(…)` calls is the `test` the name
// finds anyway; `let take = crate::other::take;` is other's.
const held = local.kind === 'let' ? local.init?.replace(/\s*::\s*<[\s\S]*>$/, '').replace(/\s+/g, '') : undefined;
if (held === undefined || !/^(?:[A-Za-z_]\w*::)*[A-Za-z_]\w*$/.test(held)) return null;
if (held === name) return undefined;
target = held.includes('::') ? rustPathTarget({ ...ref, referenceName: held }, context) : null;
} else {
// A rename: a path whose last segment is not the name it binds.
const renames = (p: RustUsePath): boolean => p.segs[p.segs.length - 1] !== name;
if (!visible || !(visible.paths.some(renames) || visible.std.some(renames))) return undefined;
if (visible.paths.length === 0) return null;
const ids = rustUseItemsAt(name, ref, context);
target = ids === null ? undefined : [...ids].map((id) => context.getNodeById?.(id)).find((n) => n && kinds.has(n.kind));
}
if (!target || !kinds.has(target.kind)) return null;
// What the scope says the name is needs no other look at the name (isRustNameInScope).
let scoped = RUST_SCOPED_REFS.get(context);
if (!scoped) RUST_SCOPED_REFS.set(context, (scoped = new WeakSet()));
scoped.add(ref);
return { original: ref, targetNodeId: target.id, confidence: 0.9, resolvedBy: 'import' };
}
/** References matchRustUseBoundName answered from the scope around them: a rename, a `let` that holds a function. */
const RUST_SCOPED_REFS = new WeakMap<ResolutionContext, WeakSet<UnresolvedRef>>();
/**
* The local a bare name at `offset` of `ref`'s function is there (rust-scopes.ts
* reads the bindings): a parameter, a `let`, a closure's, a pattern's — unless
* an item of the name is nearer, as a block's own `use` (`fn f(take: fn()) {
* { use crate::util::take; take() } }`, and one in the body itself, a scope
* inside the parameters') or a function the block declares. A nested `fn`
* sees no local of the function around it. Null for none.
*/
function rustShadowingLocal(name: string, ref: UnresolvedRef, offset: number, visible: RustVisibleUse | undefined, context: ResolutionContext): RustBinding | null {
const caller = context.getNodeById?.(ref.fromNodeId);
if (!caller || (caller.kind !== 'function' && caller.kind !== 'method')) return null;
// A local is written before the reference, in the function: the name's own text first.
const lines = context.getFileLines?.(ref.filePath) ?? context.readFile(ref.filePath)?.split('\n') ?? [];
const word = new RegExp(`(?<![\\w$])${escapeRegExp(name)}(?![\\w$])`);
let written = false;
for (let l = caller.startLine; l <= ref.line && !written; l++) {
const text = l === ref.line ? (lines[l - 1] ?? '').slice(0, ref.column) : lines[l - 1] ?? '';
written = word.test(text);
}
if (!written) return null;
const scopes = rustScopesOf(ref.filePath, context);
const found = scopes ? rustBindingAt(scopes, name, offset) : undefined;
if (!scopes || !found) return null;
const local = found.binding;
const fn = rustFnAt(scopes, offset);
if (fn && local.from < fn.from) return null;
if (visible && visible.block >= local.from) return null;
// `fn f(take: fn()) { fn take() {} take() }`: the block's own item.
for (const n of context.getNodesInFileNamed?.(ref.filePath, name) ?? []) {
if (n.kind !== 'function' || n.qualifiedName === name) continue;
const block = rustInnerBlockAt(scopes, (scopes.lineStarts[n.startLine - 1] ?? 0) + Math.max(0, n.startColumn ?? 0));
if (block && block[0] >= local.from && block[0] < offset && offset < block[1]) return null;
}
return local;
}
/** Kinds a Rust path to a module's item names: never a method, nor a variable (an impl's `const`, a `static`). */
const RUST_PATH_ITEM_KINDS: ReadonlySet<string> = new Set(['function', 'struct', 'union', 'enum', 'trait', 'type_alias', 'constant']);
const RUST_PATH_TARGETS = new WeakMap<ResolutionContext, Map<string, string | null>>();
/**
* The item a Rust path written in `ref`'s file names — `crate::util::take`,
* `super::Item` (from an inline `mod tests { }` too), `util::take` through a
* module a `use` binds, a project crate's by its name — walking the modules as
* Rust does from the one the path is written in (rustWalkModules), else, as a
* 2015 path, from its crate's root, to what the last module declares, passes
* on (`pub use`) or globs in (rustModuleItems): a function, a type, a trait —
* and in a `use`, an enum's variant. Never a method, an `impl`'s or a trait's
* associated item, nor a function's local item. Null when the path names no
* module the project holds, or one that holds nothing by that name — and for
* one whose head a `use` takes from the standard library (`fs::read` under
* `use std::fs;`), whatever module of that name the project has.
*/
export function rustPathTarget(ref: UnresolvedRef, context: ResolutionContext): Node | null {
const segs = ref.referenceName.replace(/^::/, '').split('::');
if (segs.length < 2 || !segs.every((s) => /^[A-Za-z_]\w*$/.test(s))) return null;
const variants = ref.referenceKind === 'imports';
// `Foo::new(…)`, `io::Error::new(…)`: through a type, by its name, to no
// module's item — only a `use` names a variant that way. (Rust names its
// modules in snake_case.)
if (!variants && /^[A-Z]/.test(segs[segs.length - 2]!)) return null;
const offset = rustRefOffset(ref, context);
const from = rustModuleAt(ref.filePath, offset, context);
const head = segs[0]!;
const bare = head !== 'crate' && head !== 'self' && head !== 'super';
if (bare) {
const bound = rustVisibleUse(head, ref.filePath, offset, context);
if (bound && bound.std.length > 0 && bound.paths.length === 0) return null;
}
let memo = RUST_PATH_TARGETS.get(context);
if (!memo) RUST_PATH_TARGETS.set(context, (memo = new Map()));
const key = `${ref.filePath}\0${from.block?.open ?? -1}\0${variants ? 1 : 0}\0${segs.join('::')}`;
const hit = memo.get(key);
if (hit !== undefined) return hit === null ? null : context.getNodeById?.(hit) ?? null;
let items = rustUsePathItems(segs, from, context, new Set());
if (items === null && bare) {
const root = rustCrateRootModule(ref.filePath, context);
if (root) items = rustUsePathItems(segs, root, context, new Set());
}
const target = items?.find((n) => RUST_PATH_ITEM_KINDS.has(n.kind) || (variants && n.kind === 'enum_member')) ?? null;
memo.set(key, target?.id ?? null);
return target;
}
/**
* Whether a bare Rust name where `ref` is was brought in from the standard
* library: by the `use` that binds it there (rustVisibleUse), or with none,
* the file's last `use` of it — unless the crate holds a module by the root's
* name, which a 2015 path names (`use core::Thing;` with a `mod core;`).
*/
export function isRustNameFromStd(ref: UnresolvedRef, context: ResolutionContext): boolean {
const name = ref.referenceName;
const uses = rustUsesOf(ref.filePath, context);
const visible = rustVisibleUse(name, ref.filePath, rustRefOffset(ref, context), context);
let std: RustUsePath | undefined;
if (visible) {
if (visible.paths.length > 0) return false;
std = visible.std[visible.std.length - 1];
} else {
const last = [...(uses.paths.get(name) ?? []), ...(uses.stdPaths.get(name) ?? [])].sort((a, b) => a.at - b.at).pop();
if (!last || !RUST_STD_ROOTS.has(last.segs[0]!)) return false;
std = last;
}
if (!std || std.segs.length < 2) return false;
const root = std.segs[0]!;
const crateRoot = rustCrateRootModule(ref.filePath, context);
return rustSubmodule(rustModuleAt(ref.filePath, std.at, context), root, context) === null &&
(crateRoot === null || rustSubmodule(crateRoot, root, context) === null);
}
/** /**
* Std types a crate can implement a trait for but never declare a method on: * Std types a crate can implement a trait for but never declare a method on:
* a method the index holds for one is the project's `impl SomeTrait for * a method the index holds for one is the project's `impl SomeTrait for
@@ -19388,6 +19764,9 @@ export function clearNameMatcherMemos(context: ResolutionContext): void {
RUST_TRAIT_IMPL_MEMO.delete(context); RUST_TRAIT_IMPL_MEMO.delete(context);
RUST_USES.delete(context); RUST_USES.delete(context);
RUST_USE_ITEMS.delete(context); RUST_USE_ITEMS.delete(context);
RUST_VISIBLE_ITEMS.delete(context);
RUST_PATH_TARGETS.delete(context);
RUST_SCOPED_REFS.delete(context);
RUST_GLOB_ITEMS.delete(context); RUST_GLOB_ITEMS.delete(context);
RUST_CRATES.delete(context); RUST_CRATES.delete(context);
RUST_DEPENDENCIES.delete(context); RUST_DEPENDENCIES.delete(context);