mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-11-14 16:52:09 +08:00
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:
co-authored by
danusha2345
Claude Opus 5.5
parent
e7c9ffd068
commit
4955eac958
@@ -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)
|
||||
- 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 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)
|
||||
- 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)
|
||||
|
||||
@@ -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']);
|
||||
});
|
||||
});
|
||||
@@ -30,10 +30,74 @@ mod twice;
|
||||
mod inline_reexport;
|
||||
mod fs;
|
||||
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 {
|
||||
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.
|
||||
'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', () => {
|
||||
// `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/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']);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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]);
|
||||
});
|
||||
});
|
||||
@@ -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']);
|
||||
});
|
||||
});
|
||||
@@ -147,13 +147,36 @@ function parseWorkspaceMembers(cargoToml: string): string[] {
|
||||
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');
|
||||
if (!packageSection) return null;
|
||||
const packageNameMatch = packageSection.match(/name\s*=\s*["']([^"'\n]+)["']/);
|
||||
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 {
|
||||
const normalized = crateName.replace(/-/g, '_');
|
||||
map.set(crateName, memberPath);
|
||||
|
||||
@@ -31,10 +31,9 @@ import {
|
||||
reExportSource,
|
||||
pythonFunctionModuleAlias,
|
||||
isPythonFromImportModule,
|
||||
isRustAssociatedItem,
|
||||
tsGlobalScope,
|
||||
isLexicallyReachable,
|
||||
rustItemPlacement,
|
||||
rustPathTarget,
|
||||
isRustNameFromStd,
|
||||
} from './name-matcher';
|
||||
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',
|
||||
]);
|
||||
|
||||
/** 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.)
|
||||
*
|
||||
@@ -3678,147 +3674,21 @@ function resolvePythonAbsoluteModule(
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a Rust qualified reference `A::B::C` by mapping the MODULE prefix
|
||||
* (`A::B`) to a file and finding the leaf symbol (`C`) in it. This is the Rust
|
||||
* analog of {@link resolvePythonModuleMember} / {@link resolveGoCrossPackageReference}
|
||||
* and the precise answer to common-name re-exports (`pub use self::read::read`)
|
||||
* that name-matching can't disambiguate. Returns null when the prefix isn't a
|
||||
* real module path (e.g. `Widget::new` — `Widget` is a struct, not a module),
|
||||
* so associated-function calls and enum-variant paths fall through untouched.
|
||||
* Resolve a Rust qualified reference `A::B::C` to the item module `A::B`
|
||||
* holds by the name `C` (rustPathTarget walks the modules as Rust does). This
|
||||
* is the Rust analog of {@link resolvePythonModuleMember} /
|
||||
* {@link resolveGoCrossPackageReference} and the precise answer to common-name
|
||||
* re-exports (`pub use self::read::read`) that name-matching can't
|
||||
* disambiguate. Returns null when the prefix isn't a module path the project
|
||||
* 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(
|
||||
ref: UnresolvedRef,
|
||||
context: ResolutionContext
|
||||
): ResolvedRef | null {
|
||||
const segments = ref.referenceName.split('::').filter((s) => s.length > 0);
|
||||
if (segments.length < 2) return 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)
|
||||
);
|
||||
const target = rustPathTarget(ref, context);
|
||||
return target ? { original: ref, targetNodeId: target.id, confidence: 0.9, resolvedBy: 'import' } : null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -4233,70 +4103,6 @@ function resolveStaticMember(
|
||||
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
|
||||
* OUTSIDE the repository?
|
||||
@@ -4335,17 +4141,9 @@ export function isBoundToOutOfRepoImport(
|
||||
const name = ref.referenceName;
|
||||
if (name.includes('::') || name.includes('.')) return false; // qualified refs resolve by path
|
||||
|
||||
if (ref.language === 'rust') {
|
||||
const content = context.readFile(ref.filePath);
|
||||
if (!content) return false;
|
||||
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;
|
||||
}
|
||||
// (A 2015-edition crate-relative path can shadow a stdlib root with a local
|
||||
// module of the same name: a `use core::…` with a `mod core;` is the crate's.)
|
||||
if (ref.language === 'rust') return isRustNameFromStd(ref, context);
|
||||
|
||||
if (!ESM_IMPORT_LANGUAGES.has(ref.language)) return false;
|
||||
for (const imp of context.getImportMappings(ref.filePath, ref.language)) {
|
||||
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
isImportableKind,
|
||||
CPP_DEFINE_SIGNATURE,
|
||||
} 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 { isCppConstructorRef, matchCppConstructor } from './cpp-constructor';
|
||||
import { isCppSupertypeRef, matchCppSupertype, clearCppSupertypeMemos } from './cpp-supertypes';
|
||||
@@ -1646,6 +1646,14 @@ export class ReferenceResolver {
|
||||
const rubyPath = matchRubyConstantPathRef(ref, this.context);
|
||||
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
|
||||
// AND the name doesn't match a local import. The import escape is
|
||||
// necessary because re-export rename chains (`import { login }
|
||||
|
||||
+439
-60
@@ -25,8 +25,8 @@ import { isCFeatureVariant, isOffByDefault } from './c-feature-variants';
|
||||
import { isTestFile, isTestPath } from '../search/query-utils';
|
||||
import { inBlock, isDeclarationFile, isMergedAmbientDeclaration, scanGlobalScope, type GlobalScopeScan } from './ambient-modules';
|
||||
import { isMinifiedContent } from '../extraction/generated-detection';
|
||||
import { getCargoWorkspaceCrateMap } from './frameworks/cargo-workspace';
|
||||
import { readRustScopes, rustBindingAt, rustInnerBlockAt, rustBoundsOf, rustFnAt, rustImplAt, splitRustTopLevel, type RustBinding, type RustPatternSite, type RustScopes } from './rust-scopes';
|
||||
import { getCargoWorkspaceCrateMap, parsePackageName, parseTargetPaths } from './frameworks/cargo-workspace';
|
||||
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
|
||||
* score. A name defined more times than this is "ubiquitous" — a method/symbol
|
||||
@@ -8255,6 +8255,8 @@ interface RustUses {
|
||||
boundFrom: Map<string, Set<string>>;
|
||||
/** Each name a project `use` binds → the paths that bind it (`Receiver` → `crate::sync::watch::Receiver`). */
|
||||
paths: Map<string, RustUsePath[]>;
|
||||
/** 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. */
|
||||
globPaths: RustUsePath[];
|
||||
/** The modules the file declares inline (`mod tests { … }`), in source order. */
|
||||
@@ -8274,6 +8276,19 @@ interface RustUsePath {
|
||||
at: number;
|
||||
/** A `pub use` (of any reach), which a glob over its module passes on. */
|
||||
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. */
|
||||
@@ -8364,6 +8379,10 @@ interface RustCrate {
|
||||
keys: Map<string, string>;
|
||||
/** The project crates its `[dependencies]` name, by directory: the ones it can re-export from. */
|
||||
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 {
|
||||
@@ -8393,9 +8412,12 @@ function rustCrateGraph(context: ResolutionContext): RustCrateGraph {
|
||||
const add = (dir: string): boolean => {
|
||||
if (crates.has(dir)) return true;
|
||||
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;
|
||||
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!);
|
||||
if (!byName.has(name)) byName.set(name, dir);
|
||||
return true;
|
||||
@@ -8486,7 +8508,7 @@ function rustProjectCrates(context: ResolutionContext): Set<string> {
|
||||
if (hit) return hit;
|
||||
const crates = new Set<string>();
|
||||
// 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, '_'));
|
||||
for (const name of getCargoWorkspaceCrateMap(context).keys()) crates.add(name.replace(/-/g, '_'));
|
||||
RUST_CRATES.set(context, crates);
|
||||
@@ -8504,7 +8526,7 @@ function rustUsesOf(filePath: string, context: ResolutionContext): RustUses {
|
||||
if (hit) return hit;
|
||||
const uses: RustUses = {
|
||||
names: new Set(), globs: new Set(), external: new Set(), bound: new Set(), boundFrom: new Map(),
|
||||
paths: new Map(), globPaths: [], blocks: [], pathMods: new Set(), balanced: true, lineStarts: [],
|
||||
paths: new Map(), stdPaths: new Map(), globPaths: [], blocks: [], pathMods: new Set(), balanced: true, lineStarts: [],
|
||||
};
|
||||
const leaves = (tree: string): string[] => [
|
||||
...[...tree.matchAll(/([A-Za-z_]\w*)\s*(?=[,}]|$|\s+as\b)|\bas\s+([A-Za-z_]\w*)/g)]
|
||||
@@ -8512,10 +8534,28 @@ function rustUsesOf(filePath: string, context: ResolutionContext): RustUses {
|
||||
// `use std::io::{self, Read}` binds `io` too.
|
||||
...[...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 text = stripCommentsForRegex(context.readFile(filePath) ?? '', 'rust');
|
||||
const bind = (map: Map<string, RustUsePath[]>, local: string, path: RustUsePath): void => {
|
||||
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);
|
||||
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 root = /^\s*(?:::)?([A-Za-z_]\w*)/.exec(tree)?.[1] ?? '';
|
||||
// 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));
|
||||
// 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`).
|
||||
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 scope = scopes?.[i];
|
||||
for (const { segs, local } of rustUseTreeBindings(tree)) {
|
||||
// A macro's own `use $crate::…` names no module of this file's.
|
||||
if (segs.length === 0 || segs[0]!.startsWith('$') || RUST_STD_ROOTS.has(segs[0]!)) continue;
|
||||
const path: RustUsePath = { segs, at: m.index!, pub };
|
||||
if (segs.length === 0 || segs[0]!.startsWith('$')) continue;
|
||||
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);
|
||||
else if (uses.paths.has(local)) uses.paths.get(local)!.push(path);
|
||||
else uses.paths.set(local, [path]);
|
||||
else bind(uses.paths, local, path);
|
||||
}
|
||||
// The items it binds: each leaf (`as` aliases by their alias), never the path it walks.
|
||||
if (outside) {
|
||||
for (const id of leaves(tree)) uses.external.add(id);
|
||||
continue;
|
||||
return;
|
||||
}
|
||||
for (const id of leaves(tree)) {
|
||||
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 g of tree.matchAll(/(\w+)\s*::\s*(?:\{[^}]*)?\*/g)) uses.globs.add(g[1]!);
|
||||
}
|
||||
const layout = rustModBlocks(text);
|
||||
uses.blocks = layout.blocks;
|
||||
uses.balanced = layout.balanced;
|
||||
});
|
||||
// `#[path = "atomic_u64_native.rs"] mod imp;`, `#[cfg_attr(docsrs, path = "…")] pub mod de;`
|
||||
for (const m of text.matchAll(/#\[\s*(?:cfg_attr\s*\([^\]]*\bpath|path)\s*=[^\]]*\]\s*(?:#!?\[[^\]]*\]\s*)*(?:pub(?:\([^)]*\))?\s+)?mod\s+(?:r#)?([A-Za-z_]\w*)/g)) {
|
||||
uses.pathMods.add(m[1]!);
|
||||
@@ -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. */
|
||||
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>();
|
||||
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[] = [];
|
||||
@@ -8661,7 +8703,7 @@ function rustModBlocks(text: string): { blocks: RustModBlock[]; balanced: boolea
|
||||
let inner: RustModBlock | null = null;
|
||||
let balanced = true;
|
||||
// A head whose `{` is in a string is never among the braces.
|
||||
for (const at of rustCodeBraces(text)) {
|
||||
for (const at of codeBraces()) {
|
||||
if (text[at] === '{') {
|
||||
const name = heads.get(at);
|
||||
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 };
|
||||
}
|
||||
|
||||
/**
|
||||
* 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. */
|
||||
function rustBlockAt(blocks: readonly RustModBlock[], offset: number): RustModBlock | 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`, … */
|
||||
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);
|
||||
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);
|
||||
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,
|
||||
* example, bench or bin target's own file, else the package's `src/lib.rs` (or
|
||||
* `src/main.rs`). null when no single root is the file's: a `tests/support/`
|
||||
* module serves every test that declares it.
|
||||
* `src/main.rs`); in no crate the manifests name, the nearest `lib.rs` or
|
||||
* `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 {
|
||||
const dir = rustCrateDirOf(filePath, rustCrateGraph(context));
|
||||
if (dir === null) return null;
|
||||
const graph = rustCrateGraph(context);
|
||||
const dir = rustCrateDirOf(filePath, graph);
|
||||
if (dir === null) return rustNearestRootModule(filePath, context);
|
||||
const base = dir === '' ? '' : `${dir}/`;
|
||||
const rel = filePath.slice(base.length);
|
||||
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`;
|
||||
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 main = `${base}src/main.rs`;
|
||||
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 };
|
||||
}
|
||||
|
||||
/**
|
||||
* 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. */
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -8789,7 +8911,7 @@ function rustSubmodule(m: RustModule, name: string, context: ResolutionContext,
|
||||
}
|
||||
if (depth >= 8) return null;
|
||||
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);
|
||||
if (passed !== null && passed !== 'outside') return passed;
|
||||
}
|
||||
@@ -8885,7 +9007,8 @@ function rustModuleItems(m: RustModule, name: string, context: ResolutionContext
|
||||
try {
|
||||
const declared = rustDeclaredItems(m, name, context, publicOnly);
|
||||
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 found: Node[] = [];
|
||||
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);
|
||||
}
|
||||
|
||||
/** 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 {
|
||||
if (segs.length < 2) return null;
|
||||
const mod = rustWalkModules(segs.slice(0, -1), from, context);
|
||||
if (mod === 'outside') return [];
|
||||
return mod === null ? null : rustModuleItems(mod, segs[segs.length - 1]!, context, seen);
|
||||
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>>();
|
||||
@@ -8969,19 +9108,87 @@ function rustUseItems(name: string, filePath: string, context: ResolutionContext
|
||||
if (hit !== undefined) return hit;
|
||||
const uses = rustUsesOf(filePath, context);
|
||||
const paths = uses.balanced ? uses.paths.get(name) : undefined;
|
||||
let ids: Set<string> | null = paths ? new Set() : null;
|
||||
for (const p of paths ?? []) {
|
||||
const items = rustUsePathItems(p.segs, rustModuleAt(filePath, p.at, context), context, new Set());
|
||||
if (items === null) {
|
||||
ids = null;
|
||||
break;
|
||||
}
|
||||
for (const n of items) ids!.add(n.id);
|
||||
}
|
||||
const ids = paths ? rustPathsItems(paths, filePath, context) : null;
|
||||
memo.set(key, 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>>();
|
||||
|
||||
/**
|
||||
@@ -9056,21 +9263,6 @@ export function isRustAssociatedItem(n: Node): boolean {
|
||||
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)? */
|
||||
function rustGlobCovers(uses: RustUses, candidate: Node, ref: UnresolvedRef): boolean {
|
||||
if (uses.globs.has(rustModuleName(candidate.filePath))) return true;
|
||||
@@ -9094,6 +9286,7 @@ function rustGlobCovers(uses: RustUses, candidate: Node, ref: UnresolvedRef): bo
|
||||
* there (rustUseItems).
|
||||
*/
|
||||
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;
|
||||
const name = ref.referenceName;
|
||||
// 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 cut = candidate.qualifiedName.lastIndexOf('::');
|
||||
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);
|
||||
// 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;
|
||||
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
|
||||
// `runtime::context::Context`. (A method call `.env(…)` is no imported name.)
|
||||
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);
|
||||
}
|
||||
|
||||
/** 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:
|
||||
* 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_USES.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_CRATES.delete(context);
|
||||
RUST_DEPENDENCIES.delete(context);
|
||||
|
||||
Reference in New Issue
Block a user