diff --git a/CHANGELOG.md b/CHANGELOG.md index 238ff6659..9a7432a9a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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::;` 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) diff --git a/__tests__/rust-crate-roots.test.ts b/__tests__/rust-crate-roots.test.ts new file mode 100644 index 000000000..ddf656a2f --- /dev/null +++ b/__tests__/rust-crate-roots.test.ts @@ -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): Promise { + 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']); + }); +}); diff --git a/__tests__/rust-module-path-call.test.ts b/__tests__/rust-module-path-call.test.ts index e822516df..6a8be7849 100644 --- a/__tests__/rust-module-path-call.test.ts +++ b/__tests__/rust-module-path-call.test.ts @@ -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']); }); }); diff --git a/__tests__/rust-use-call.test.ts b/__tests__/rust-use-call.test.ts new file mode 100644 index 000000000..c10cabd80 --- /dev/null +++ b/__tests__/rust-use-call.test.ts @@ -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 = { + '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(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]); + }); +}); diff --git a/__tests__/rust-use-scopes.test.ts b/__tests__/rust-use-scopes.test.ts new file mode 100644 index 000000000..7b9c8a667 --- /dev/null +++ b/__tests__/rust-use-scopes.test.ts @@ -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 = { + '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) { if let Some(take) = v { take(); } } +pub fn after_if_let(v: Option) { if let Some(take) = v { } take(); } +pub fn holds_fn() { let take = take; take(); } +pub fn holds_generic() { let take = take::; 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::; 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']); + }); +}); diff --git a/src/resolution/frameworks/cargo-workspace.ts b/src/resolution/frameworks/cargo-workspace.ts index 2a81284bd..6c47f5be0 100644 --- a/src/resolution/frameworks/cargo-workspace.ts +++ b/src/resolution/frameworks/cargo-workspace.ts @@ -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, crateName: string, memberPath: string): void { const normalized = crateName.replace(/-/g, '_'); map.set(crateName, memberPath); diff --git a/src/resolution/import-resolver.ts b/src/resolution/import-resolver.ts index 20f783683..c9a918914 100644 --- a/src/resolution/import-resolver.ts +++ b/src/resolution/import-resolver.ts @@ -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 = new Set([ '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` → `/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 - // `.rs` or `/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 { - const out = new Map(); - - // 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)) { diff --git a/src/resolution/index.ts b/src/resolution/index.ts index 9430776a5..6bcdaf213 100644 --- a/src/resolution/index.ts +++ b/src/resolution/index.ts @@ -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 } diff --git a/src/resolution/name-matcher.ts b/src/resolution/name-matcher.ts index 03ab98b31..1ca88a596 100644 --- a/src/resolution/name-matcher.ts +++ b/src/resolution/name-matcher.ts @@ -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>; /** Each name a project `use` binds → the paths that bind it (`Receiver` → `crate::sync::watch::Receiver`). */ paths: Map; + /** Each name a `use` from the standard library binds → those paths (`fs` → `std::fs`). */ + stdPaths: Map; /** 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; /** The project crates its `[dependencies]` name, by directory: the ones it can re-export from. */ deps: Set; + /** 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 { if (hit) return hit; const crates = new Set(); // 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, 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(); 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): Array | null { + const out: Array = []; + const open: Array = []; + 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): 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 | 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 | null = paths ? new Set() : null; - for (const p of paths ?? []) { + 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 | null { + const ids = new Set(); + 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); + 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 | 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 | 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 | 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> = { + 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::;` 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>(); + +/** + * 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(`(?= 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 = new Set(['function', 'struct', 'union', 'enum', 'trait', 'type_alias', 'constant']); + +const RUST_PATH_TARGETS = new WeakMap>(); + +/** + * 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);