From 6321cfcc65b702f3f274dbce8f4407195aa7d862 Mon Sep 17 00:00:00 2001 From: "bob-codedaptive.com" Date: Sun, 1 Nov 2026 00:22:16 -0500 Subject: [PATCH] fix(rust): a call through a type a path names reaches that type's own function, not a namesake's MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A call written through a type's path, `crate::a::Cache::open()`, took the function of any type of that name in the crate: rustTypeMember matched by qualified name and crate alone. A namesake type in another inline module of the same file, one a function declares in its own body, and an `impl` for a reference to the type (`impl Open for &Cache`, the reference's, not the type's) could each be linked instead of the type the path walks to. The walk itself also took a head wrongly: a `use` in a function's body that binds it (`use crate::b as a;`) was ignored, and so was a `mod a { … }` the function declares, which rustSubmodule also took for the module's own `a` when it was written before the module's `pub mod a`. - rustTypeMember takes a function, `const` or variant of a struct, enum or union only from an `impl` whose header names that type (rustImplSelfItems, walked from the module the `impl` is in); a trait's own declaration is still the trait body's. - rustImplSelfItems reads the `impl` header with the new rustImplSelfType, which rustImplSelfArgs shares; a header naming a reference to the type, or a bare name in an `impl` inside a function's body, names no type here. - rustSubmodule skips a `mod` that sits in a function's body or another block (RustModBlock.scope, read with rustUseScopes), which is no item of its module. - rustPathTarget reads a head that a `use` in the body binds as the path that `use` names, and declines one a `mod` in the body declares. --- CHANGELOG.md | 1 + __tests__/rust-path-call-edge-cases.test.ts | 493 ++++++++++++++++++++ src/resolution/name-matcher.ts | 179 ++++++- src/resolution/rust-scopes.ts | 27 +- 4 files changed, 678 insertions(+), 22 deletions(-) create mode 100644 __tests__/rust-path-call-edge-cases.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index f9854bde30..0823a677b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -192,6 +192,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - 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) - In Rust, a call through a type that a path or a `use` names, like `sim::items::ItemPool::for_def(…)` into another crate of your workspace, `ItemPool::for_def(…)` under `use sim::items::ItemPool;` or `crate::models::Mode::Fast(1)`, now links to that type's own function or variant, even when a type of the same name in another crate or module has one too, and a `use` of a `pub const` or `static`, like `use sim::RANK_COUNT;`, links to it. A type written on the line below its module path, as rustfmt wraps a long signature (`scheduler::` then `Handle`), now links by that path rather than by a `use` of its name. A workspace member listed outside the project, like `../shared`, is no longer read. Re-index Rust projects after upgrading. Thanks @BGamboa13. (#2631) +- In Rust, a call through a type that a path names, like `crate::a::Cache::open()`, now links only to that type's own function. Before, it could land on the function of a type of the same name in another module of the same file, on one a function declares inside its own body, or on an `impl` written for a reference to the type like `impl Open for &Cache`, and a path through a name that a `use` inside a function binds, like `use crate::b as a;`, or through a module declared inside a function could land on a module of that name elsewhere in the file. Re-index Rust projects after upgrading. - In Rust, a module loaded with `#[path = "…"]`, like tests kept in another folder as `#[path = "../../../tests/unit/scaling_tests.rs"] mod tests;`, is now part of the module that declares it: its `use super::*` brings in that module's items, private ones included, `super::` and `crate::` start from there, and the path is read as Rust reads it, from the declaring file's folder or an inline module's own. A call through such a module, like `support::make()` in each test that loads a shared helper, reaches the file it names. A module whose `#[cfg]` alternatives each name a file of their own, or whose path a `cfg_attr` sets, depends on the build and is left alone. An example, test or bench that `Cargo.toml` places at a path of its own, like `examples/07-fullstack/handling_errors.rs`, is now its own crate, so its `crate::` starts from it. A name only one of a module's `cfg` alternatives holds now links through it, like `crate::process::unix::GlobalOrphanQueue` through tokio's `#[path]` modules for Unix and Windows. Re-index Rust projects after upgrading. Thanks @seonghobae. (#1892) - 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. diff --git a/__tests__/rust-path-call-edge-cases.test.ts b/__tests__/rust-path-call-edge-cases.test.ts new file mode 100644 index 0000000000..5ab9aa8432 --- /dev/null +++ b/__tests__/rust-path-call-edge-cases.test.ts @@ -0,0 +1,493 @@ +/** + * A Rust call through a type a path names — `crate::a::Cache::open()` — is to + * the associated function of the type that path walks to, and no other's: + * - not a namesake type's, in another inline module of the same file or one a + * function declares in its own body; + * - not an `impl` for a reference to the type (`impl Open for &Cache`), which + * is the reference type's. + * A path whose head a `use` in the function's body binds (`use crate::b as + * a;`) is the path that `use` names, not the module `a` beside it; a `mod a { + * … }` the function declares is no module of the file, so neither its call + * nor a later `pub mod a` takes the other's items. Chained local imports + * resolve at their import sites, not where the call is written. An impl's + * callback bounds, including `->` and nested return types, keep its methods. + * A `mod` a block declares nearer the call than a `use` of the same name is + * the module the head names, not that `use`'s path. An impl whose type takes + * a const-generic block argument, in the type or in the impl's generics list, + * keeps its associated functions, whatever operator the block uses. + */ +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-path-edge-')); + const files: Record = { + 'Cargo.toml': '[package]\nname = "app"\nversion = "0.1.0"\nedition = "2021"\n', + 'src/lib.rs': 'pub mod inline;\npub mod local;\npub mod shadow;\npub mod order;\npub mod refimpl;\npub mod traits;\npub mod shapes;\npub mod store;\npub mod split;\npub mod callback;\npub mod near;\npub mod constarg;\n', + // a's Cache has only a trait's provided `open`; b's has an inherent one. + 'src/inline.rs': `pub mod a { + pub struct Cache; + pub trait Open { + fn open() -> usize { + 1 + } + } + impl Open for Cache {} +} + +pub mod b { + pub struct Cache; + impl Cache { + pub fn open() -> usize { + 2 + } + } +} + +pub fn through_a() -> usize { + crate::inline::a::Cache::open() +} + +pub fn through_b() -> usize { + crate::inline::b::Cache::open() +} +`, + // The root's Cache has only the trait's provided `open`; a function + // declares a Cache of its own, whose inherent `open` is not the root's. + 'src/local.rs': `pub struct Cache; + +pub trait Open { + fn open() -> usize { + 1 + } +} + +impl Open for Cache {} + +pub fn build_local() -> usize { + struct Cache; + impl Cache { + pub fn open() -> usize { + 2 + } + } + 0 +} + +pub fn through_root() -> usize { + crate::local::Cache::open() +} +`, + 'src/shadow.rs': `pub mod a { + pub struct Cache; + impl Cache { + pub fn open() -> usize { + 1 + } + } + pub fn make() -> usize { + 1 + } +} + +pub mod b { + pub struct Cache; + impl Cache { + pub fn open() -> usize { + 2 + } + } + pub fn make() -> usize { + 2 + } +} + +pub fn through_module() -> usize { + a::Cache::open() +} + +pub fn through_use() -> usize { + use crate::shadow::b as a; + a::Cache::open() +} + +pub fn through_use_fn() -> usize { + use crate::shadow::b as a; + a::make() +} + +pub fn through_local_mod() -> usize { + mod a { + pub use crate::shadow::b::Cache; + } + a::Cache::open() +} + +pub fn through_local_mod_fn() -> usize { + mod a { + pub fn make() -> usize { + 3 + } + } + a::make() +} + +pub fn through_chained_use() -> usize { + use crate::shadow::a as b; + { + use b as a; + a::make() + } +} + +pub fn through_import_scope() -> usize { + use crate::shadow::a as b; + { + use b as a; + { + use crate::shadow::b as b; + a::make() + } + } +} +`, + // b comes first in the file, and a function before `pub mod a` declares a + // `mod a` of its own: the module's own `a` is still the one after it. + 'src/order.rs': `pub mod b { + pub struct Cache; + impl Cache { + pub fn open() -> usize { + 2 + } + } +} + +pub fn decoy() { + mod a { + pub use crate::order::b::Cache; + } +} + +pub mod a { + pub struct Cache; + impl Cache { + pub fn open() -> usize { + 1 + } + } +} + +pub fn via_order() -> usize { + a::Cache::open() +} +`, + // Cache takes the trait's provided `open`; the impl for `&Cache` writes its own. + 'src/refimpl.rs': `pub struct Cache; + +pub trait Open { + fn open() -> usize { + 1 + } +} + +impl Open for Cache {} + +impl Open for &crate::refimpl::Cache { + fn open() -> usize { + 3 + } +} + +pub fn through_cache() -> usize { + crate::refimpl::Cache::open() +} +`, + // A trait, and a struct of the same name in another module with an inherent method of the trait's method's name. + 'src/traits.rs': `pub trait Region { + fn size() -> usize; +} +`, + 'src/shapes.rs': `pub struct Region; + +impl Region { + pub fn size() -> usize { + 0 + } +} + +pub fn through_trait() -> usize { + crate::traits::Region::size() +} +`, + // An impl written in another file than its type: by path, and by a bare name a `use` brings in. + 'src/store.rs': `pub struct Store; +pub struct Vault; +`, + 'src/split.rs': `use crate::store::Vault; + +impl crate::store::Store { + pub fn drop_all() -> usize { + 1 + } +} + +impl Vault { + pub fn seal() -> usize { + 2 + } +} + +pub fn through_path() -> usize { + crate::store::Store::drop_all() +} + +pub fn through_use() -> usize { + crate::store::Vault::seal() +} +`, + 'src/callback.rs': `pub struct Cache(F); + +impl usize> Cache { + pub fn open(f: F) -> Self { Self(f) } +} + +pub fn through_callback() { + crate::callback::Cache::open(|| 1); +} + +pub struct Nested(F); + +impl Option>> Nested { + pub fn open(f: F) -> Self { Self(f) } +} + +pub fn through_nested_callback() { + crate::callback::Nested::open(|| Some(Ok(1))); +} +`, + // `near`'s block declares a `mod b` nearer the call than the `use` that binds `b`, so + // `b::make` is that module's. `far` has no such block: its `b` is `a`. + 'src/near.rs': `pub mod a { + pub fn make() -> usize { + 1 + } +} + +pub fn near() -> usize { + use crate::near::a as b; + { + mod b { + pub fn make() -> usize { + 2 + } + } + b::make() + } +} + +pub fn far() -> usize { + use crate::near::a as b; + { + b::make() + } +} +`, + // Module `a`: the impl's type takes a const-generic block argument, `Cache<{ 1 + 1 }>`; + // its `open` is still `Cache`'s, reached as `Cache::<2>::open` and as `Cache::open`. + // Module `b`: the block sits in the impl's own generics list, `impl>`; + // its `open` is still `Cache`'s, reached as `Cache::<()>::open`, which satisfies the bound. + // Modules `c` to `f`: the const-generic block holds `<<`, `>>` or `>`, or is the first of two + // type arguments (`Pair<{ 1 << 1 }, u8>`); each impl's `open` is still its own type's. + 'src/constarg.rs': `pub mod a { + pub struct Cache; + impl Cache<{ 1 + 1 }> { + pub fn open() -> usize { + 2 + } + } +} + +pub fn through_turbofish() -> usize { + crate::constarg::a::Cache::<2>::open() +} + +pub fn through_path() -> usize { + crate::constarg::a::Cache::open() +} + +pub mod b { + pub trait Bound {} + pub struct Cache(T); + impl> Cache { + pub fn open() -> usize { + 3 + } + } + impl Bound<{ 1 + 1 }> for () {} +} + +pub fn through_bound() -> usize { + crate::constarg::b::Cache::<()>::open() +} + +pub mod c { + pub struct Cache; + impl Cache<{ 1 << 1 }> { + pub fn open() -> usize { + 5 + } + } +} + +pub fn through_shl() -> usize { + crate::constarg::c::Cache::open() +} + +pub mod d { + pub struct Cache; + impl Cache<{ 4 >> 1 }> { + pub fn open() -> usize { + 6 + } + } +} + +pub fn through_shr() -> usize { + crate::constarg::d::Cache::open() +} + +pub mod e { + pub struct Flag; + impl Flag<{ 2 > 1 }> { + pub fn open() -> usize { + 7 + } + } +} + +pub fn through_flag() -> usize { + crate::constarg::e::Flag::open() +} + +pub mod f { + pub struct Pair(T); + impl Pair<{ 1 << 1 }, u8> { + pub fn open() -> usize { + 8 + } + } +} + +pub fn through_pair() -> usize { + crate::constarg::f::Pair::open() +} +`, + }; + 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 }); +}); + +/** What the function `name` in `file` calls, as `kind qualifiedName file:line`. */ +const calls = (file: string, name: string): string[] => { + const fn = cg.getNodesInFile(file).find((n) => n.name === name && n.kind === 'function'); + expect(fn, `${name} in ${file}`).toBeDefined(); + return cg.getOutgoingEdgesFrom([fn!.id]).filter((e) => e.kind === 'calls') + .map((e) => cg.getNode(e.target)!).map((t) => `${t.kind} ${t.qualifiedName} ${t.filePath}:${t.startLine}`).sort(); +}; + +describe('Rust calls through a type a path names', () => { + it('never reach a namesake type\'s function in another inline module of the file', () => { + expect(calls('src/inline.rs', 'through_a')).toEqual([]); + expect(calls('src/inline.rs', 'through_b')).toEqual(['method Cache::open src/inline.rs:14']); + // Controls: an impl written in another file than its type is still the + // type's, by its path or by a name a `use` brings in; and a trait's own + // declaration is still reached through the trait, not a namesake struct's. + expect(calls('src/split.rs', 'through_path')).toEqual(['method Store::drop_all src/split.rs:4']); + expect(calls('src/split.rs', 'through_use')).toEqual(['method Vault::seal src/split.rs:10']); + expect(calls('src/shapes.rs', 'through_trait')).toEqual(['method Region::size src/traits.rs:2']); + }); + + it('never reach the function of a namesake type a function declares in its own body', () => { + expect(calls('src/local.rs', 'through_root')).toEqual([]); + }); + + it('never reach the function of an impl for a reference to the type', () => { + expect(calls('src/refimpl.rs', 'through_cache')).toEqual([]); + }); + + it('read a head a `use` in the function\'s body binds as the path that `use` names', () => { + expect(calls('src/shadow.rs', 'through_use')).toEqual(['method Cache::open src/shadow.rs:16']); + expect(calls('src/shadow.rs', 'through_use_fn')).toEqual(['function make src/shadow.rs:20']); + // Where no `use` in the body binds `a`, it is the module `a` (control). + expect(calls('src/shadow.rs', 'through_module')).toEqual(['method Cache::open src/shadow.rs:4']); + }); + + it('never reach module a\'s item through a module `a` the function declares in its own body', () => { + expect(calls('src/shadow.rs', 'through_local_mod')).not.toContain('method Cache::open src/shadow.rs:4'); + expect(calls('src/shadow.rs', 'through_local_mod_fn')).not.toContain('function make src/shadow.rs:8'); + }); + + it('keep the module\'s own `mod a` when a function before it declares a `mod a` of its own', () => { + expect(calls('src/order.rs', 'via_order')).toEqual(['method Cache::open src/order.rs:19']); + }); + + it('follow a block import through an outer local alias before walking modules', () => { + expect(calls('src/shadow.rs', 'through_chained_use')).toEqual(['function make src/shadow.rs:8']); + }); + + it('expand an imported alias in its import scope, not the call scope', () => { + expect(calls('src/shadow.rs', 'through_import_scope')).toEqual(['function make src/shadow.rs:8']); + }); + + it('keep associated functions on impls with callback bounds and nested return types', () => { + expect(calls('src/callback.rs', 'through_callback')).toEqual(['method Cache::open src/callback.rs:4']); + expect(calls('src/callback.rs', 'through_nested_callback')).toEqual(['method Nested::open src/callback.rs:14']); + }); + + it('read a head a `mod` declared in a nearer block as that module, not as the `use` binding the same name', () => { + expect(calls('src/near.rs', 'near')).not.toContain('function make src/near.rs:2'); + }); + + it('expand a head a `use` binds when no nearer block declares a `mod` of that name', () => { + expect(calls('src/near.rs', 'far')).toEqual(['function make src/near.rs:2']); + }); + + it('keep associated functions on an impl whose type takes a const-generic block argument', () => { + expect(calls('src/constarg.rs', 'through_turbofish')).toEqual(['method Cache::open src/constarg.rs:4']); + expect(calls('src/constarg.rs', 'through_path')).toEqual(['method Cache::open src/constarg.rs:4']); + }); + + it('keep associated functions on an impl whose generics list takes a const-generic block argument', () => { + expect(calls('src/constarg.rs', 'through_bound')).toEqual(['method Cache::open src/constarg.rs:22']); + }); + + it('keep associated functions when a const-generic block in an impl header shifts left', () => { + expect(calls('src/constarg.rs', 'through_shl')).toEqual(['method Cache::open src/constarg.rs:36']); + }); + + it('keep associated functions when a const-generic block in an impl header shifts right', () => { + expect(calls('src/constarg.rs', 'through_shr')).toEqual(['method Cache::open src/constarg.rs:49']); + }); + + it('keep associated functions when a const-generic block in an impl header compares with >', () => { + expect(calls('src/constarg.rs', 'through_flag')).toEqual(['method Flag::open src/constarg.rs:62']); + }); + + it('keep associated functions when a const-generic block is the first of two type arguments in an impl header', () => { + expect(calls('src/constarg.rs', 'through_pair')).toEqual(['method Pair::open src/constarg.rs:75']); + }); +}); diff --git a/src/resolution/name-matcher.ts b/src/resolution/name-matcher.ts index 5d4e0cc8ee..52f24367d6 100644 --- a/src/resolution/name-matcher.ts +++ b/src/resolution/name-matcher.ts @@ -9511,6 +9511,13 @@ interface RustModBlock { close: number; /** The inline module it is declared in, or null for the file's own top level. */ parent: RustModBlock | null; + /** + * The braces of the function body, or other block, it is declared in + * (`fn f() { mod a { … } }`): the module is `a` there alone and is no item + * of the module around it. Undefined for a module's own `mod`, in a macro's + * braces too. + */ + scope?: RustUseScope; } const RUST_CRATES = new WeakMap>(); @@ -9923,9 +9930,16 @@ 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, 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 kws: number[] = []; + 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]!); + kws.push(m.index!); + } const blocks: RustModBlock[] = []; if (heads.size === 0) return { blocks, balanced: true }; + // The block each `mod` is written in, if it is in a block rather than a module (rustUseScopes). + const scopes = rustUseScopes(text, kws, codeBraces(), new Set(heads.keys())); + const scopeOf = new Map([...heads.keys()].map((at, i) => [at, scopes?.[i]])); const open: Array = []; let inner: RustModBlock | null = null; let balanced = true; @@ -9933,7 +9947,9 @@ function rustModBlocks(text: string, codeBraces: () => readonly number[] = () => 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 }; + const scope = scopeOf.get(at); + const block: RustModBlock | null = name === undefined ? null + : scope ? { name, open: at, close: text.length, parent: inner, scope } : { name, open: at, close: text.length, parent: inner }; if (block) { blocks.push(block); inner = block; @@ -10354,7 +10370,8 @@ function rustLibModule(crateDir: string, context: ResolutionContext): RustModule } /** - * The module `name` that `m` declares: inline (`mod name { … }`), else + * The module `name` that `m` declares: inline (`mod name { … }`, other than + * one a function declares in its own body), else * `.rs` or `/mod.rs` in its directory, else one it passes on under * that name — dioxus's `pub use dioxus_hooks as hooks;`. One a `#[path]` * puts elsewhere is the file it names when `m` declares it once, whatever @@ -10376,7 +10393,8 @@ function rustSubmodule(m: RustModule, name: string, context: ResolutionContext, } return rustCfgChoice([...files].map((file) => ({ file, dir: rustParentDir(file), block: null, root: false }))); } - const block = layout.blocks.find((b) => b.name === name && b.parent === m.block); + // (A `mod` a function declares in its body is no item of `m`.) + const block = layout.blocks.find((b) => b.name === name && b.parent === m.block && !b.scope); if (block) return { file: m.file, dir, block, root: false }; for (const file of [`${dir}.rs`, `${dir}/mod.rs`]) { if (context.fileExists(file)) return { file, dir, block: null, root: false }; @@ -11120,10 +11138,15 @@ const RUST_PATH_TARGETS = new WeakMap /^[A-Za-z_]\w*$/.test(s))) return null; const variants = ref.referenceKind === 'imports'; const offset = rustRefOffset(ref, context); @@ -11137,11 +11160,41 @@ export function rustPathTarget(ref: UnresolvedRef, context: ResolutionContext): const ownerUse = typed && segs.length === 2 ? rustVisibleUse(owner, ref.filePath, offset, context) : undefined; if (typed && (ref.referenceKind !== 'calls' || owner === 'Self' || (segs.length === 2 && !ownerUse?.paths.length))) return null; const from = rustModuleAt(ref.filePath, offset, context); - const head = segs[0]!; - const bare = head !== 'crate' && head !== 'self' && head !== 'super'; + let head = segs[0]!; + let bare = head !== 'crate' && head !== 'self' && head !== 'super'; if (bare) { - const bound = rustVisibleUse(head, ref.filePath, offset, context); + let bound = rustVisibleUse(head, ref.filePath, offset, context); if (bound && bound.std.length > 0 && bound.paths.length === 0) return null; + // A local `use`'s head can itself be a local alias. Read each at its own + // import site: a binding nearer the call does not change what that import + // names. Module-level imports and a lone imported type keep their walk. + // A `mod` of the head's name declared in a block around the position read, + // inside the `use`'s own block, is nearer than the `use`: the walk stops + // at that head, which the check below then declines. + let at = offset; + const modAround = (after: number): boolean => rustUsesOf(ref.filePath, context).blocks.some((b) => b.name === head && + b.scope !== undefined && b.parent === from.block && b.scope.open > after && b.scope.open < at && at < b.scope.close); + if (!variants && !(typed && segs.length === 2)) { + const seen = new Set(); + while (bound && bound.block !== -1) { + if (modAround(bound.block)) break; + if (bound.paths.length === 0) return null; + const via = bound.paths[0]!; + const key = `${head}\0${via.at}`; + if (seen.has(key) || bound.paths.some((p) => p.segs.join('::') !== via.segs.join('::'))) return null; + seen.add(key); + segs = [...via.segs, ...segs.slice(1)]; + at = via.at; + head = segs[0]!; + bare = head !== 'crate' && head !== 'self' && head !== 'super'; + if (!bare) break; + bound = rustVisibleUse(head, ref.filePath, at, context); + if (bound && bound.std.length > 0 && bound.paths.length === 0) return null; + } + } + // A `mod` in the block around the reference (or the expanded import) is + // that block's alone; rustSubmodule reads only a module's own `mod`s. + if (modAround(-1)) return null; } let memo = RUST_PATH_TARGETS.get(context); if (!memo) RUST_PATH_TARGETS.set(context, (memo = new Map())); @@ -11189,8 +11242,11 @@ const RUST_TYPE_MEMBERS = new WeakMap type.kind !== 'trait' || - (n.filePath === type.filePath && n.startLine >= type.startLine && n.startLine <= (type.endLine ?? type.startLine)); + // A trait's own declaration is written in the trait's body. Any other + // type's item is written in an `impl` whose header names that type, walked + // from the module the `impl` is in: a namesake type's (in another inline + // module of the file, or one a function declares for itself) does not. + const within = (n: Node): boolean => type.kind === 'trait' + ? n.filePath === type.filePath && n.startLine >= type.startLine && n.startLine <= (type.endLine ?? type.startLine) + : rustImplSelfItems(n, context)?.has(type.id) === true; const all = context.getNodesByName(member).filter((n) => n.language === 'rust' && RUST_MEMBER_KINDS.has(n.kind) && n.qualifiedName === `${type.name}::${member}` && rustCrateDirOf(n.filePath, graph) === crate && within(n)); const inherent = all.filter((n) => !isRustTraitImplMethod(n, context)); @@ -11941,13 +12002,26 @@ const RUST_PRIMITIVE_TYPES: ReadonlySet = new Set([ ]); const RUST_OWNER_KINDS: ReadonlySet = new Set(['struct', 'enum', 'union']); -/** The offset of the `>` closing the `<` at `open` in a type's text, or -1. */ +/** + * The offset of the `>` closing the `<` at `open`, or -1 if it is missing or + * a brace block is unbalanced. Balanced `{ … }` blocks are skipped, so const + * arguments and array-length blocks contribute no angle brackets. A return + * arrow's `>` closes no angle bracket. + */ function rustAngleEnd(text: string, open: number): number { let depth = 0; for (let i = open; i < text.length; i++) { const c = text[i]; if (c === '<') depth++; else if (c === '>' && text[i - 1] !== '-' && --depth === 0) return i; + else if (c === '{') { + let braces = 1; + while (braces > 0 && ++i < text.length) { + if (text[i] === '{') braces++; + else if (text[i] === '}') braces--; + } + if (braces !== 0) return -1; + } } return -1; } @@ -13007,17 +13081,86 @@ function rustOwnedMethods(name: string, method: string, lookup: RustTypeLookup, return matches.filter((m) => files.has(m.filePath) || rustTypeLookup(name, [], m.filePath, context, rustNodeOffset(m, context)).nodes.some((n) => ids.has(n.id))); } -/** The text an `impl` header above a Rust method gives its implementing type's arguments: `BuildArgs` of `impl Overrides`. */ -function rustImplSelfArgs(method: Node, context: ResolutionContext): { args: string; generics: string; byRef: boolean } | null { +/** + * The ids of the items named by the type in the header of the `impl` around + * a Rust method (`impl Cache`, `impl Open for Cache`; rustImplSelfType), + * walked from the module the `impl` is written in. A path from `crate`, + * `self` or `super` names the same item wherever the `impl` is written: + * `impl crate::store::Cache` is store's. Any other path (a bare `impl + * Cache`) is walked only from an `impl` written directly in a module's body, + * the file's top level or an inline `mod`'s braces, where it is that + * module's own `Cache` (or the one its `use` brings in), never a namesake's + * in another inline module of the file. Null when the method is in no `impl` + * (a trait's own body, a macro the scopes don't read), when the header's + * type is no path or its path can't be walked, when the header names a + * reference to the type (`impl Open for &Cache`, `&'a mut Cache`): that + * `impl` is the reference type's, and its method is no method of `Cache` + * itself, which may take the trait's provided one instead; and for a path + * not from `crate`, `self` or `super` in an `impl` inside a function's body + * or another block: the block may declare a type of that name for itself + * (`fn f() { struct Cache; impl Cache { … } }`) or `use` one, which the + * module's walk does not see. + */ +function rustImplSelfItems(method: Node, context: ResolutionContext): Set | null { const scopes = rustScopesOf(method.filePath, context); if (!scopes) return null; const block = rustImplAt(scopes, rustNodeOffset(method, context)); if (!block || block.kind !== 'impl') return null; + const self = rustImplSelfType(scopes, block); + // `impl Open for &Cache`: the reference type's method, never `Cache`'s own. + if (!self || self.byRef) return null; + const type = self.type; + const head = type.segments[0]; + if (head !== 'crate' && head !== 'self' && head !== 'super') { + // In a module's body, the innermost braces around the `impl` are none + // (the file's top level) or that inline `mod`'s own. + const inner = rustInnerBlockAt(scopes, block.at); + const mod = rustBlockAt(rustUsesOf(method.filePath, context).blocks, block.at); + if (inner !== null && inner[0] !== mod?.open) return null; + } + // A bare name is looked up in the impl's own module, as `self::Cache`. + const segs = type.segments.length > 0 ? [...type.segments, type.name] : ['self', type.name]; + return rustWalkedItems(segs, method.filePath, block.at, context); +} + +/** + * The type an `impl` header names as the one it implements, as rustPathHead + * reads it — `crate::store::Cache` of `impl crate::store::Cache`, + * `Overrides` of `impl Overrides`, `Cache` of + * `impl Open for &'a mut Cache` — and whether the header names a + * reference to it. Read from the `impl` keyword at `block.at` to the body's + * `{` at `block.from`: its `where` clause and balanced `impl<…>` generics + * dropped (a callback bound's `->` closes no generic), the text after its + * first `for` when it has one, a leading `&'a mut` dropped. Null when that + * type is no path (a tuple, a slice, `dyn Trait`, a fn pointer). + */ +function rustImplSelfType(scopes: RustScopes, block: { at: number; from: number }): { type: { segments: string[]; name: string; args: string }; byRef: boolean } | null { const header = scopes.code.slice(block.at, block.from).replace(/\bwhere\b[\s\S]*$/, ''); - const head = header.replace(/^impl\s*(?:<(?:[^<>]|<(?:[^<>]|<[^<>]*>)*>)*>)?/, ''); + let head = header.slice('impl'.length).trimStart(); + if (head.startsWith('<')) { + const end = rustAngleEnd(head, 0); + if (end === -1) return null; + head = head.slice(end + 1).trimStart(); + } const self = /\bfor\s+([\s\S]+)$/.exec(head)?.[1] ?? head; const type = rustPathHead(self.trim().replace(/^&\s*(?:'\w+\s+)?(?:mut\s+)?/, '')); - return type ? { args: type.args, generics: block.generics, byRef: self.trim().startsWith('&') } : null; + return type ? { type, byRef: self.trim().startsWith('&') } : null; +} + +/** + * What the `impl` header above a Rust method says of its implementing type + * (rustImplSelfType): the type's arguments (`BuildArgs` of + * `impl Overrides`), the `impl<…>` generics, and whether the + * header names a reference to the type. Null when the method's file cannot + * be read, the method is in no `impl`, or the type is no path. + */ +function rustImplSelfArgs(method: Node, context: ResolutionContext): { args: string; generics: string; byRef: boolean } | null { + const scopes = rustScopesOf(method.filePath, context); + if (!scopes) return null; + const block = rustImplAt(scopes, rustNodeOffset(method, context)); + if (!block || block.kind !== 'impl') return null; + const self = rustImplSelfType(scopes, block); + return self ? { args: self.type.args, generics: block.generics, byRef: self.byRef } : null; } /** diff --git a/src/resolution/rust-scopes.ts b/src/resolution/rust-scopes.ts index 5e8215f3b8..753d73bf83 100644 --- a/src/resolution/rust-scopes.ts +++ b/src/resolution/rust-scopes.ts @@ -202,14 +202,23 @@ function identAt(code: string, at: number): string | null { return IDENT_AT.exec(code)?.[0] ?? null; } -/** The offset of the `>` closing the `<` at `at`, or -1. `->` and `=>` close nothing. */ -function angleClose(code: string, at: number, limit = code.length): number { +/** + * The offset of the `>` closing the `<` at `at`, or -1. `->` and `=>` close nothing. + * With `blocks` (the brackets' `close` map), a balanced `{ … }` inside the + * angle brackets, a const-generic block argument, is jumped. Otherwise a `{` + * ends the search. + */ +function angleClose(code: string, at: number, limit = code.length, blocks?: Map): number { let depth = 0; for (let i = at; i < limit; i++) { const c = code[i]; if (c === '<') depth++; else if (c === '>' && code[i - 1] !== '-' && code[i - 1] !== '=') { if (--depth === 0) return i; + } else if (c === '{' && blocks) { + const end = blocks.get(i); + if (end === undefined) return -1; + i = end; } else if (c === ';' || c === '{' || c === '}') return -1; } return -1; @@ -448,6 +457,9 @@ export function readRustScopes(source: string): RustScopes { // `impl … {` and `trait Name: Super where … {` — an item, not // the `impl Trait` of a type, which follows a `:`, `(`, `,`, `<`, `&` or `->`. + // The body's `{` is the first one outside the header's `<…>` arguments, and + // the generics list reads past one inside it: a const-generic block argument + // (`impl Cache<{ 1 + 1 }> {`, `impl> …`) is jumped whole. const impls: RustImplBlock[] = []; for (const m of code.matchAll(/\b(impl|trait)\b/g)) { const kind = m[1] as 'impl' | 'trait'; @@ -464,16 +476,23 @@ export function readRustScopes(source: string): RustScopes { } let generics = ''; if (code[p] === '<') { - const end = angleClose(code, p); + const end = angleClose(code, p, code.length, close); if (end < 0) continue; generics = code.slice(p + 1, end); p = end + 1; } let where = -1; let open = -1; + let angle = 0; for (let q = p; q < n; q++) { const c = code[q]!; - if (c === '{') { open = q; break; } + if (c === '<') angle++; + else if (c === '>' && angle > 0 && code[q - 1] !== '-' && code[q - 1] !== '=') angle--; + if (c === '{') { + if (angle > 0) { q = close.get(q) ?? n; continue; } + open = q; + break; + } if (c === ';' || c === '}') break; if (c === '(' || c === '[') { q = close.get(q) ?? n; continue; } if (where < 0 && c === 'w' && wordAt(code, q, 'where')) where = q + 5;