mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-11-15 03:08:20 +08:00
fix(rust): calls through a type a path or a use names reach that type's own function (#3025)
A Rust call through a type, `sim::items::ItemPool::for_def(…)` into another
crate of the workspace or `ItemPool::for_def(…)` under `use
sim::items::ItemPool;`, was matched by its qualified name alone, so a
namesake type's function in another crate or module could take it (a
workspace's `client` crate's `ItemPool::for_def`). The module walk stopped
at the type, a `use` of a `pub const` or `static` (`use sim::RANK_COUNT;`)
linked nothing, and a type rustfmt wrapped below its path (`h:
&scheduler::\n Handle`) linked by the file's `use` of its last name.
The fix, on the module walk:
- rustPathTarget reads `Type::member` calls: the type is the item the
path's modules name, or the one the `use` that binds its name names
(rustUseItemsAt), and rustTypeMember finds its member: an enum's variant;
else the type's own (inherent) function or `const` in the type's crate,
a trait impl's only when it is the one of that name; a trait's own
declared method. A type alias, `Self`, a type the module declares itself
and a type path in any other position are left as they were.
- Module items include `variable`s (a module-level `pub const`/`static`).
- matchRustUseBoundName reads a qualifier written on the line above a
type (rust-scopes' blanked code, so a commented-out path is none) and
resolves the whole path.
- Memoized per (type, member) and per path.
- Manifest paths stay inside the project: a workspace member that leaves
it (`../sibling`, an absolute path) is neither read nor listed
(escapesRoot, in expandMembers), and rustCrateGraph drops a
`[lib]`/`[[bin]]` path that leaves it, as it already did a dependency's.
Validation:
- rust-cross-crate.test.ts (PR 2631's 9 cases, credited),
rust-type-member-paths.test.ts and rust-manifest-paths.test.ts: on main
10 of 15 fail; all pass. Every rust-* suite passes. On main the workspace
map read `../outside/Cargo.toml`, `../vendor/x/Cargo.toml` and
`/abs/crate/Cargo.toml` and listed `../vendor`; now only the project's.
- A/B against main, native kernel, 7 Rust repos (every site the same
as against PR #3014's own commit; re-checked on 60b0d7a9): tokio +205
links / 139 retargeted / 230 chains gaining their head call, ripgrep
+155 / 13 / 1, axum +25, clap +179 / 0 / 295, serde +28, dioxus +142 /
6 / 31, gleam +114 / 47 / 0; 0 removed. Retargets go to the type the
code imports or its own crate's (tokio's Semaphore::new and loom mocks
to tokio::sync, ripgrep's Error::from, gleam's Printer::new); express
and flask byte-identical.
- Cost, native kernel, medians of 3 interleaved runs (dioxus of 7):
tokio CPU 32.1 -> 29.7 s, peak RSS 1,000 -> 1,010 MB; gleam 29.1 ->
28.1 s, 1,010 -> 1,004 MB; dioxus 38.7 -> 40.1 s, 1,111 -> 1,115 MB
(timed on one thread, rustPathTarget 1.24 -> 1.32 s over 17,198 calls).
Builds on PR #2631 by @BGamboa13.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
cdfa31be54
commit
7021de5c30
@@ -130,6 +130,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
- In Rust, code that uses an enum only through its variants, like `mode::Mode::A` in an expression, a `Mode::B =>` match arm or `Self::A` inside the enum's own `impl`, now shows up in that enum's callers and impact, linked to the enum that actually declares the variant rather than to a same-named type elsewhere. Standard-library variants like `Ordering::Less` or `Option::Some` and associated items like `Foo::new()` or `Foo::MAX` don't count as using a project enum. Re-index Rust projects after upgrading. Thanks @mg-mg-mg for the report and @danusha2345. (#2328)
|
||||
- In Rust, a call written through a module path, like `crate::util::take(3)`, `super::util::take(3)` or `util::take(3)`, and a `use` of a function, like `use crate::util::take;`, now link to the function that module declares. Before, they could land on a method of the same name declared above it in that file, like an `impl Buf`'s `take`, or on a function written inside another function, and a call through a module the standard library provides, like `fs::read(…)` under `use std::fs;`, could link to your own `fs` module's `read`. Re-index Rust projects after upgrading. Thanks @danusha2345. (#2308)
|
||||
- In Rust, a name a `use` renames, like `consume()` after `use crate::util::take as consume;`, now links to the item its path names, and a `use` written inside a function or a block counts only there, ahead of the file's own `use`s and items, so `fn first() { use crate::util::take; take() }` reaches util's `take` and the next function's own `use` its own. A call to a parameter, a closure or a local that shares a function's name, like `take()` inside `fn run(take: fn())` or after `let take = || {};`, no longer links to that function, while `let test = test::<i8>;` still calls `test`, and a `use` of an enum's variant, like `use crate::models::Mode::Fast;`, links `Fast(…)` to that enum's variant. A path through a module, like `super::helper()`, `crate::config()`, `super::shared::helper()` inside a `mod tests { … }` or `tokio::task::yield_now()`, now reaches the item that module declares or passes on with a `pub use`, in your own crate or another crate of the workspace, and its `use` links to it; that includes binaries whose manifest gives them a path of their own, like ripgrep's, and packages whose manifest lists `name` after other fields, like axum-core's. A `use` written inside a string no longer counts. Re-index Rust projects after upgrading. Thanks @danusha2345. (#2308)
|
||||
- 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)
|
||||
- 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 Spring projects, a route now links to the method its mapping annotation is written on when that method has no access modifier, like `@GetMapping List<Article> list()` in a package-private controller, and a method-level `@RequestMapping(value = "/feed", method = GET)` on such a method now makes a route. Before, such a handler was skipped, so its route linked the next method declared `public`, `private` or `protected`, or nothing, and the handler looked unused. An overloaded handler under a tall stack of annotations, like `@Operation` and `@ApiResponses` above `@GetMapping`, now links the overload it annotates rather than its neighbour. Re-index Spring projects after upgrading. Thanks @Liurchen.
|
||||
|
||||
@@ -0,0 +1,242 @@
|
||||
/**
|
||||
* Cross-crate references in a Cargo workspace: a path written from one member
|
||||
* crate into another (`sim::items::ItemPool::for_def(…)`), through a re-export
|
||||
* (`pub use rng::Rng`), through a renamed or non-member path dependency, and
|
||||
* the `crate::` / `super::` paths inside a member crate. Cases from PR #2631
|
||||
* by @BGamboa13.
|
||||
*/
|
||||
import { afterEach, beforeEach, expect, it } from 'vitest';
|
||||
import * as fs from 'node:fs';
|
||||
import * as os from 'node:os';
|
||||
import * as path from 'node:path';
|
||||
import { CodeGraph } from '../src';
|
||||
|
||||
let root: string;
|
||||
let cg: CodeGraph | undefined;
|
||||
beforeEach(() => { root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-rust-cross-crate-')); });
|
||||
afterEach(() => { cg?.close(); cg = undefined; fs.rmSync(root, { recursive: true, force: true }); });
|
||||
|
||||
async function index(files: Record<string, string>) {
|
||||
for (const [file, text] of Object.entries(files)) {
|
||||
fs.mkdirSync(path.dirname(path.join(root, file)), { recursive: true });
|
||||
fs.writeFileSync(path.join(root, file), text);
|
||||
}
|
||||
cg = await CodeGraph.init(root, { index: true });
|
||||
}
|
||||
|
||||
/** `file:qualifiedName` of everything that has an edge of `kind` into the named symbol. */
|
||||
function sources(file: string, qualifiedName: string, kind = 'calls'): string[] {
|
||||
const target = cg!.getNodesInFile(file).find((n) => n.qualifiedName === qualifiedName);
|
||||
expect(target, `${file}:${qualifiedName}`).toBeDefined();
|
||||
return cg!.getIncomingEdges(target!.id).filter((e) => e.kind === kind).map((e) => {
|
||||
const src = cg!.getNode(e.source)!;
|
||||
return `${src.filePath}:${src.qualifiedName}`;
|
||||
}).sort();
|
||||
}
|
||||
|
||||
const WORKSPACE = `[workspace]
|
||||
resolver = "2"
|
||||
members = ["crates/sim", "crates/server", "crates/client"]
|
||||
|
||||
[workspace.dependencies]
|
||||
sim = { path = "crates/sim" }
|
||||
`;
|
||||
const SIM_TOML = '[package]\nname = "sim"\nversion = "0.1.0"\nedition = "2021"\n';
|
||||
const SIM_LIB = `pub mod items;
|
||||
mod rng;
|
||||
pub use rng::Rng;
|
||||
pub const RANK_COUNT: usize = 6;
|
||||
pub fn median_u32(v: &[u32]) -> u32 { v[0] }
|
||||
`;
|
||||
const SIM_ITEMS = `pub struct ItemPool { pub n: u32 }
|
||||
impl ItemPool {
|
||||
pub fn for_def(def: u32) -> ItemPool { ItemPool { n: def } }
|
||||
}
|
||||
pub enum Drop { Gold(u32), Nothing }
|
||||
pub fn roll_item(p: &ItemPool) -> u32 { p.n }
|
||||
`;
|
||||
const SIM_RNG = 'pub struct Rng { s: u64 }\nimpl Rng {\n pub fn new(s: u64) -> Rng { Rng { s } }\n}\n';
|
||||
|
||||
const base = (server: Record<string, string>, serverToml = '[package]\nname = "server"\nversion = "0.1.0"\nedition = "2021"\n\n[dependencies]\nsim.workspace = true\n') => ({
|
||||
'Cargo.toml': WORKSPACE,
|
||||
'crates/sim/Cargo.toml': SIM_TOML,
|
||||
'crates/sim/src/lib.rs': SIM_LIB,
|
||||
'crates/sim/src/items.rs': SIM_ITEMS,
|
||||
'crates/sim/src/rng.rs': SIM_RNG,
|
||||
// A decoy: the same type and method names in another member crate.
|
||||
'crates/client/Cargo.toml': '[package]\nname = "client"\nversion = "0.1.0"\nedition = "2021"\n',
|
||||
'crates/client/src/lib.rs': 'pub mod items;\n',
|
||||
'crates/client/src/items.rs': 'pub struct ItemPool;\nimpl ItemPool {\n pub fn for_def(def: u32) -> ItemPool { ItemPool }\n}\npub fn roll_item() -> u32 { 0 }\n',
|
||||
'crates/server/Cargo.toml': serverToml,
|
||||
...Object.fromEntries(Object.entries(server).map(([f, t]) => [`crates/server/src/${f}`, t])),
|
||||
});
|
||||
|
||||
it('resolves a fully qualified call into another workspace crate to that crate', async () => {
|
||||
await index(base({
|
||||
'lib.rs': 'pub mod rift;\npub mod econ;\n',
|
||||
'rift.rs': 'pub fn open() -> u32 {\n let pool = sim::items::ItemPool::for_def(3);\n sim::items::roll_item(&pool)\n}\n',
|
||||
'econ/mod.rs': 'pub mod offline;\n',
|
||||
'econ/offline.rs': 'pub fn tick() {\n let _ = sim::items::ItemPool::for_def(\n 4,\n );\n}\n',
|
||||
}));
|
||||
expect(sources('crates/sim/src/items.rs', 'ItemPool::for_def')).toEqual([
|
||||
'crates/server/src/econ/offline.rs:tick',
|
||||
'crates/server/src/rift.rs:open',
|
||||
]);
|
||||
expect(sources('crates/sim/src/items.rs', 'roll_item')).toEqual(['crates/server/src/rift.rs:open']);
|
||||
expect(sources('crates/client/src/items.rs', 'ItemPool::for_def')).toEqual([]);
|
||||
});
|
||||
|
||||
it('resolves a crate-root item, a re-export, a constant, and an enum variant across crates', async () => {
|
||||
await index(base({
|
||||
'lib.rs': 'pub mod rift;\n',
|
||||
'rift.rs': `use sim::RANK_COUNT;
|
||||
pub fn open(v: &[u32]) -> usize {
|
||||
let _r = sim::Rng::new(7);
|
||||
let _m = sim::median_u32(v);
|
||||
let _d = sim::items::Drop::Gold(3);
|
||||
RANK_COUNT
|
||||
}
|
||||
`,
|
||||
}));
|
||||
expect(sources('crates/sim/src/rng.rs', 'Rng::new')).toEqual(['crates/server/src/rift.rs:open']);
|
||||
expect(sources('crates/sim/src/lib.rs', 'median_u32')).toEqual(['crates/server/src/rift.rs:open']);
|
||||
expect(sources('crates/sim/src/items.rs', 'Drop::Gold')).toEqual(['crates/server/src/rift.rs:open']);
|
||||
expect(sources('crates/sim/src/lib.rs', 'RANK_COUNT', 'imports')).toEqual(['crates/server/src/rift.rs:crates/server/src/rift.rs']);
|
||||
});
|
||||
|
||||
it('resolves `use` imports of another crate and the calls through them', async () => {
|
||||
await index(base({
|
||||
'lib.rs': 'pub mod rift;\n',
|
||||
'rift.rs': `use sim::items::{roll_item, ItemPool};
|
||||
use sim::Rng;
|
||||
pub fn open() -> u32 {
|
||||
let _r = Rng::new(1);
|
||||
roll_item(&ItemPool::for_def(2))
|
||||
}
|
||||
`,
|
||||
}));
|
||||
expect(sources('crates/sim/src/items.rs', 'ItemPool::for_def')).toEqual(['crates/server/src/rift.rs:open']);
|
||||
expect(sources('crates/sim/src/items.rs', 'roll_item')).toEqual(['crates/server/src/rift.rs:open']);
|
||||
expect(sources('crates/sim/src/rng.rs', 'Rng::new')).toEqual(['crates/server/src/rift.rs:open']);
|
||||
expect(sources('crates/sim/src/items.rs', 'ItemPool', 'imports').length).toBeGreaterThan(0);
|
||||
expect(sources('crates/sim/src/rng.rs', 'Rng', 'imports').length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('resolves a renamed path dependency and a path dependency outside the workspace members', async () => {
|
||||
await index({
|
||||
...base(
|
||||
{ 'lib.rs': 'pub fn open() -> u32 {\n let p = core_sim::items::ItemPool::for_def(1);\n vendored::helper(p.n)\n}\n' },
|
||||
'[package]\nname = "server"\nversion = "0.1.0"\nedition = "2021"\n\n[dependencies]\ncore_sim = { path = "../sim", package = "sim" }\nvendored = { path = "../../libs/vendored" }\n',
|
||||
),
|
||||
'libs/vendored/Cargo.toml': '[package]\nname = "vendored"\nversion = "0.1.0"\nedition = "2021"\n',
|
||||
'libs/vendored/src/lib.rs': 'pub fn helper(n: u32) -> u32 { n }\n',
|
||||
});
|
||||
expect(sources('crates/sim/src/items.rs', 'ItemPool::for_def')).toEqual(['crates/server/src/lib.rs:open']);
|
||||
expect(sources('libs/vendored/src/lib.rs', 'helper')).toEqual(['crates/server/src/lib.rs:open']);
|
||||
});
|
||||
|
||||
it('resolves `crate::` and `super::` paths inside a member crate, constants included', async () => {
|
||||
await index(base({
|
||||
'lib.rs': 'pub mod ledger;\npub mod econ;\n',
|
||||
'ledger.rs': 'pub const KEY_RANK_COUNT: usize = 6;\npub static LIMIT: u32 = 9;\npub fn persist() {}\n',
|
||||
'econ/mod.rs': 'pub mod sinks;\npub fn shared() {}\n',
|
||||
'econ/sinks.rs': `use crate::ledger::{KEY_RANK_COUNT, LIMIT};
|
||||
pub fn drain() -> usize {
|
||||
crate::ledger::persist();
|
||||
super::shared();
|
||||
KEY_RANK_COUNT + LIMIT as usize
|
||||
}
|
||||
`,
|
||||
}));
|
||||
expect(sources('crates/server/src/ledger.rs', 'persist')).toEqual(['crates/server/src/econ/sinks.rs:drain']);
|
||||
expect(sources('crates/server/src/econ/mod.rs', 'shared')).toEqual(['crates/server/src/econ/sinks.rs:drain']);
|
||||
expect(sources('crates/server/src/ledger.rs', 'KEY_RANK_COUNT', 'imports').length).toBeGreaterThan(0);
|
||||
expect(sources('crates/server/src/ledger.rs', 'LIMIT', 'imports').length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('resolves a module-qualified type by its own path, not through a same-name `use` import', async () => {
|
||||
// tokio shape: `scheduler::Handle` / `driver::Handle` in a file that also
|
||||
// `use`s a third `Handle` — the qualifier decides, never the file-level binding.
|
||||
await index(base({
|
||||
'lib.rs': 'pub mod scheduler;\npub mod driver;\npub mod multi;\n',
|
||||
'scheduler.rs': 'pub enum Handle { A, B }\n',
|
||||
'driver.rs': 'pub struct Handle { pub n: u32 }\n',
|
||||
'multi/mod.rs': 'mod handle;\npub(crate) use handle::Handle;\n',
|
||||
'multi/handle.rs': 'pub struct Handle { pub n: u32 }\n',
|
||||
'user.rs': `use crate::multi::Handle;
|
||||
use crate::{driver, scheduler};
|
||||
pub fn sched(h: &scheduler::Handle) -> u32 { match h { scheduler::Handle::A => 1, _ => 2 } }
|
||||
pub fn drv(h: &driver::Handle) -> u32 { h.n }
|
||||
pub fn own(h: &Handle) -> u32 { h.n }
|
||||
`,
|
||||
}));
|
||||
const refs = (file: string, qn: string) => sources(file, qn, 'references');
|
||||
expect(refs('crates/server/src/scheduler.rs', 'Handle')).toContain('crates/server/src/user.rs:sched');
|
||||
expect(refs('crates/server/src/driver.rs', 'Handle')).toEqual(['crates/server/src/user.rs:drv']);
|
||||
expect(refs('crates/server/src/multi/handle.rs', 'Handle')).not.toContain('crates/server/src/user.rs:sched');
|
||||
expect(refs('crates/server/src/multi/handle.rs', 'Handle')).not.toContain('crates/server/src/user.rs:drv');
|
||||
expect(refs('crates/server/src/multi/handle.rs', 'Handle')).toContain('crates/server/src/user.rs:own');
|
||||
});
|
||||
|
||||
it('reads a module qualifier written across lines, instead of falling back to the same-name `use` import', async () => {
|
||||
await index(base({
|
||||
'lib.rs': 'pub mod scheduler;\npub mod multi;\n',
|
||||
'scheduler.rs': 'pub enum Handle { A, B }\n',
|
||||
'multi/mod.rs': 'mod handle;\npub(crate) use handle::Handle;\n',
|
||||
'multi/handle.rs': 'pub struct Handle { pub n: u32 }\n',
|
||||
'user.rs': `use crate::multi::Handle;
|
||||
use crate::scheduler;
|
||||
pub fn sched(
|
||||
h: &scheduler::
|
||||
Handle,
|
||||
) -> u32 { 1 }
|
||||
`,
|
||||
}));
|
||||
expect(sources('crates/server/src/scheduler.rs', 'Handle', 'references')).toContain('crates/server/src/user.rs:sched');
|
||||
expect(sources('crates/server/src/multi/handle.rs', 'Handle', 'references')).not.toContain('crates/server/src/user.rs:sched');
|
||||
});
|
||||
|
||||
it('does not read a commented-out path above a leaf as its qualifier', async () => {
|
||||
await index(base({
|
||||
'lib.rs': 'pub mod scheduler;\npub mod multi;\n',
|
||||
'scheduler.rs': 'pub struct Handle { pub n: u32 }\nimpl Handle { pub fn new() -> Handle { Handle { n: 0 } } }\n',
|
||||
'multi/mod.rs': 'mod handle;\npub(crate) use handle::Handle;\n',
|
||||
'multi/handle.rs': 'pub struct Handle { pub n: u32 }\nimpl Handle { pub fn new() -> Handle { Handle { n: 1 } } }\n',
|
||||
'user.rs': `use crate::multi::Handle;
|
||||
pub fn make() -> Handle {
|
||||
// crate::scheduler::
|
||||
Handle { n: 3 }
|
||||
}
|
||||
pub fn make_block() -> Handle {
|
||||
/* crate::scheduler:: */
|
||||
Handle { n: 4 }
|
||||
}
|
||||
`,
|
||||
}));
|
||||
/** Every file:function with any edge into the file's `Handle` type or its `new`. */
|
||||
const touches = (file: string) => cg!.getNodesInFile(file)
|
||||
.filter((n) => n.name === 'Handle' || n.name === 'new')
|
||||
.flatMap((n) => cg!.getIncomingEdges(n.id).map((e) => cg!.getNode(e.source)!))
|
||||
.map((src) => `${src.filePath}:${src.qualifiedName}`);
|
||||
for (const fn of ['make', 'make_block']) {
|
||||
expect(touches('crates/server/src/scheduler.rs')).not.toContain(`crates/server/src/user.rs:${fn}`);
|
||||
expect(touches('crates/server/src/multi/handle.rs')).toContain(`crates/server/src/user.rs:${fn}`);
|
||||
}
|
||||
});
|
||||
|
||||
it('leaves a path-dependency key unresolved when two crates bind it to different directories', async () => {
|
||||
const pkg = (name: string, dep = '') => `[package]\nname = "${name}"\nversion = "0.1.0"\nedition = "2021"\n${dep}`;
|
||||
await index({
|
||||
'Cargo.toml': '[workspace]\nmembers = ["crates/server", "crates/client"]\n',
|
||||
'crates/server/Cargo.toml': pkg('server', '\n[dependencies]\nutil = { path = "../../libs/util_a", package = "util_a" }\n'),
|
||||
'crates/server/src/lib.rs': 'pub fn s() -> u32 { util::unique_a(1) }\n',
|
||||
'crates/client/Cargo.toml': pkg('client', '\n[dependencies]\nutil = { path = "../../libs/util_b", package = "util_b" }\n'),
|
||||
'crates/client/src/lib.rs': 'pub fn c() -> u32 { util::unique_b(2) }\n',
|
||||
'libs/util_a/Cargo.toml': pkg('util_a'),
|
||||
'libs/util_a/src/lib.rs': 'pub fn unique_a(n: u32) -> u32 { n }\npub fn unique_b(n: u32) -> u32 { n + 1 }\n',
|
||||
'libs/util_b/Cargo.toml': pkg('util_b'),
|
||||
'libs/util_b/src/lib.rs': 'pub fn unique_b(n: u32) -> u32 { n }\n',
|
||||
});
|
||||
// `client`'s `util` is util_b — it must never be answered with util_a's `unique_b`.
|
||||
expect(sources('libs/util_a/src/lib.rs', 'unique_b')).not.toContain('crates/client/src/lib.rs:c');
|
||||
});
|
||||
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* A Cargo workspace's members are read only inside the project: a member a
|
||||
* manifest writes as `../sibling`, or as an absolute path, is neither read
|
||||
* nor listed, so the cross-crate paths built on them stay in the project.
|
||||
*/
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import * as path from 'node:path';
|
||||
import { getCargoWorkspaceCrateMap } from '../src/resolution/frameworks/cargo-workspace';
|
||||
import type { ResolutionContext } from '../src/resolution/types';
|
||||
import { escapesRoot } from '../src/utils';
|
||||
|
||||
function stubContext(files: Record<string, string>, dirs: Record<string, string[]>) {
|
||||
const read: string[] = [];
|
||||
const listed: string[] = [];
|
||||
const context = {
|
||||
readFile: (p: string) => {
|
||||
read.push(p);
|
||||
return files[p] ?? null;
|
||||
},
|
||||
listDirectories: (p: string) => {
|
||||
listed.push(p);
|
||||
return dirs[p] ?? [];
|
||||
},
|
||||
fileExists: (p: string) => p in files,
|
||||
} as unknown as ResolutionContext;
|
||||
return { context, read, listed };
|
||||
}
|
||||
|
||||
const outside = (p: string): boolean => escapesRoot(path.posix.normalize(p));
|
||||
|
||||
describe('Cargo workspace members stay inside the project', () => {
|
||||
it('reads and lists no member outside the project', () => {
|
||||
const { context, read, listed } = stubContext(
|
||||
{
|
||||
'Cargo.toml': '[workspace]\nmembers = ["crates/*", "app", "../outside", "../vendor/*", "/abs/crate", "crates/../../escape"]\n',
|
||||
'crates/core/Cargo.toml': '[package]\nname = "core-crate"\n',
|
||||
'app/Cargo.toml': '[package]\nname = "app"\n',
|
||||
'../outside/Cargo.toml': '[package]\nname = "outside"\n',
|
||||
'/abs/crate/Cargo.toml': '[package]\nname = "abs"\n',
|
||||
},
|
||||
{ crates: ['core'], '../vendor': ['x'] },
|
||||
);
|
||||
const map = getCargoWorkspaceCrateMap(context);
|
||||
expect([...map.keys()].sort()).toEqual(['app', 'core-crate', 'core_crate']);
|
||||
expect(read.filter(outside)).toEqual([]);
|
||||
expect(listed.filter(outside)).toEqual([]);
|
||||
});
|
||||
|
||||
it('reads a member whose folder only starts with two dots', () => {
|
||||
const { context, read } = stubContext(
|
||||
{
|
||||
'Cargo.toml': '[workspace]\nmembers = ["..generated/crate"]\n',
|
||||
'..generated/crate/Cargo.toml': '[package]\nname = "generated"\n',
|
||||
},
|
||||
{},
|
||||
);
|
||||
expect([...getCargoWorkspaceCrateMap(context).keys()]).toEqual(['generated']);
|
||||
expect(read).toContain('..generated/crate/Cargo.toml');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* A Rust call through a type a path or a `use` names — `Handle::current()`
|
||||
* under `use crate::runtime::Handle;`, `crate::models::Mode::Fast(1)`,
|
||||
* `Codec::encode(&x)` under `use crate::codec::Codec;` — is to that type's
|
||||
* own associated function, variant, or (for a trait) declared method, never a
|
||||
* namesake type's in another module. A type from the standard library or a
|
||||
* dependency links nothing, and a type alias is left as it was.
|
||||
*/
|
||||
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-type-member-'));
|
||||
const files: Record<string, string> = {
|
||||
'Cargo.toml': '[package]\nname = "app"\nversion = "0.1.0"\nedition = "2021"\n\n[dependencies]\nserde_json = "1"\n',
|
||||
'src/lib.rs': 'pub mod runtime;\npub mod driver;\npub mod models;\npub mod codec;\npub mod alias;\npub mod user;\n',
|
||||
'src/runtime/mod.rs': 'mod handle;\npub use handle::Handle;\n',
|
||||
'src/runtime/handle.rs': 'pub struct Handle;\n\nimpl Handle {\n pub fn current() -> Handle {\n Handle\n }\n}\n',
|
||||
'src/driver.rs': 'pub struct Handle;\n\nimpl Handle {\n pub fn current() -> Handle {\n Handle\n }\n}\n',
|
||||
'src/models.rs': 'pub enum Mode {\n Fast(u8),\n Slow,\n}\n\npub struct Fast(pub u8);\n',
|
||||
'src/codec.rs': 'pub trait Codec {\n fn encode(&self) -> u8;\n}\n\npub struct Bytes;\n\nimpl Codec for Bytes {\n fn encode(&self) -> u8 {\n 1\n }\n}\n',
|
||||
'src/alias.rs': 'pub type Shortcut = crate::driver::Handle;\n',
|
||||
'src/user.rs': `use std::collections::HashMap;
|
||||
use serde_json::Value;
|
||||
use crate::runtime::Handle;
|
||||
use crate::codec::{Bytes, Codec};
|
||||
use crate::alias::Shortcut;
|
||||
|
||||
pub fn current() {
|
||||
let _ = Handle::current();
|
||||
}
|
||||
|
||||
pub fn variant() {
|
||||
let _ = crate::models::Mode::Fast(1);
|
||||
}
|
||||
|
||||
pub fn through_trait() {
|
||||
let _ = Codec::encode(&Bytes);
|
||||
}
|
||||
|
||||
pub fn outside() {
|
||||
let _ = HashMap::<u8, u8>::new();
|
||||
let _ = Value::from(1);
|
||||
}
|
||||
|
||||
pub fn through_alias() {
|
||||
let _ = Shortcut::current();
|
||||
}
|
||||
`,
|
||||
};
|
||||
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 = (name: string): string[] => {
|
||||
const fn = cg.getNodesInFile('src/user.rs').find((n) => n.name === name && n.kind === 'function')!;
|
||||
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 or a use names', () => {
|
||||
it('reach that type\'s own function, not a namesake type\'s', () => {
|
||||
expect(calls('current')).toEqual(['method Handle::current src/runtime/handle.rs:4']);
|
||||
});
|
||||
|
||||
it('reach an enum\'s variant through a module path', () => {
|
||||
expect(calls('variant')).toEqual(['enum_member Mode::Fast src/models.rs:2']);
|
||||
});
|
||||
|
||||
it('reach a trait\'s own method through the trait', () => {
|
||||
expect(calls('through_trait')).toEqual(['method Codec::encode src/codec.rs:2']);
|
||||
});
|
||||
|
||||
it('link nothing for a standard library or dependency type', () => {
|
||||
expect(calls('outside')).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -7,8 +7,10 @@
|
||||
* into workspace member crates.
|
||||
*/
|
||||
|
||||
import * as path from 'path';
|
||||
import picomatch from 'picomatch';
|
||||
import { ResolutionContext } from '../types';
|
||||
import { escapesRoot } from '../../utils';
|
||||
|
||||
const GLOB_CHARS = /[*?[\]{}!]/;
|
||||
const SKIP_DIRS = new Set(['target', 'node_modules', '.git', 'dist', 'build']);
|
||||
@@ -224,6 +226,8 @@ function expandMembers(members: string[], context: ResolutionContext): string[]
|
||||
const expanded: string[] = [];
|
||||
const seen = new Set<string>();
|
||||
for (const member of members) {
|
||||
// A member outside the project (`../shared`, an absolute path) is neither read nor listed.
|
||||
if (escapesRoot(path.posix.normalize(member.replace(/\\/g, '/')))) continue;
|
||||
const candidates = GLOB_CHARS.test(member)
|
||||
? expandGlobMember(member, context)
|
||||
: [member];
|
||||
|
||||
+134
-22
@@ -7,7 +7,7 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { Language, Node, IMPLICIT_RECORD_ACCESSOR_DOC } from '../types';
|
||||
import { escapeRegExp } from '../utils';
|
||||
import { escapeRegExp, escapesRoot } from '../utils';
|
||||
import { UnresolvedRef, ResolvedRef, ResolutionContext, ImportMapping, isSupertypeTarget, CPP_DEFINE_SIGNATURE, isInheritanceRef, isImportableKind } from './types';
|
||||
import { blankStringContents, stripCommentsForRegex } from './strip-comments';
|
||||
import { isJsSuperCallSite } from './js-super-calls';
|
||||
@@ -8878,6 +8878,12 @@ function rustCrateGraph(context: ResolutionContext): RustCrateGraph {
|
||||
const crates = new Map<string, RustCrate>();
|
||||
const manifests = new Map<string, string>();
|
||||
const byName = new Map<string, string>();
|
||||
// A path a manifest in `dir` writes, as a project path; null when it leaves
|
||||
// the project, which nothing here reads or names.
|
||||
const join = (dir: string, rel: string): string | null => {
|
||||
const joined = path.posix.normalize(dir === '' ? rel : `${dir}/${rel}`).replace(/\/+$/, '');
|
||||
return joined === '.' ? '' : escapesRoot(joined) ? null : joined;
|
||||
};
|
||||
const add = (dir: string): boolean => {
|
||||
if (crates.has(dir)) return true;
|
||||
const text = context.readFile(dir === '' ? 'Cargo.toml' : `${dir}/Cargo.toml`);
|
||||
@@ -8885,21 +8891,20 @@ function rustCrateGraph(context: ResolutionContext): RustCrateGraph {
|
||||
const name = text ? parsePackageName(text)?.replace(/-/g, '_') : undefined;
|
||||
if (!name) return false;
|
||||
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) });
|
||||
const lib = targets.lib === undefined ? null : join(dir, targets.lib);
|
||||
const bins = targets.bins.map((bin) => join(dir, bin)).filter((bin): bin is string => bin !== null);
|
||||
crates.set(dir, { name, keys: new Map(), deps: new Set(), lib: lib ?? undefined, bins });
|
||||
manifests.set(dir, text!);
|
||||
if (!byName.has(name)) byName.set(name, dir);
|
||||
return true;
|
||||
};
|
||||
add('');
|
||||
for (const dir of getCargoWorkspaceCrateMap(context).values()) add(dir.replace(/^\.(?:\/|$)/, ''));
|
||||
for (const member of getCargoWorkspaceCrateMap(context).values()) {
|
||||
const dir = join('', member);
|
||||
if (dir !== null) add(dir);
|
||||
}
|
||||
const inherited = new Map(rustManifestDependencies(context.readFile('Cargo.toml') ?? '')
|
||||
.filter((dep) => dep.kind === 'workspace').map((dep) => [dep.key, dep]));
|
||||
// A path from `dir`, as a project directory; null when it leaves the project.
|
||||
const join = (dir: string, rel: string): string | null => {
|
||||
const joined = path.posix.normalize(dir === '' ? rel : `${dir}/${rel}`).replace(/\/+$/, '');
|
||||
return joined === '.' ? '' : joined === '..' || joined.startsWith('../') || path.posix.isAbsolute(joined) ? null : joined;
|
||||
};
|
||||
const queue = [...crates.keys()];
|
||||
for (let i = 0; i < queue.length; i++) {
|
||||
const dir = queue[i]!;
|
||||
@@ -9912,11 +9917,28 @@ export function matchRustUseBoundName(ref: UnresolvedRef, context: ResolutionCon
|
||||
// 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];
|
||||
const lines = context.getFileLines?.(ref.filePath) ?? context.readFile(ref.filePath)?.split('\n');
|
||||
const line = lines?.[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);
|
||||
// A type rustfmt wrapped below its path — `h: &scheduler::\n Handle` — is
|
||||
// the path's, not the `use`'s of its last name: read back over the line
|
||||
// break, comments blanked (`// crate::scheduler::` above it is no path).
|
||||
if (ref.referenceKind === 'references' && /^\s*$/.test(line.slice(0, ref.column)) && ref.line > 1 &&
|
||||
/::\s*(?:\/\/.*|\/\*.*)?$/.test(lines![ref.line - 2] ?? '')) {
|
||||
const scopes = rustScopesOf(ref.filePath, context);
|
||||
const qualifier = scopes ? rustWrittenQualifier(scopes.code, offset) : [];
|
||||
if (qualifier.length > 0) {
|
||||
const target = rustPathTarget({ ...ref, referenceName: [...qualifier, name].join('::') }, context);
|
||||
if (!target) return undefined;
|
||||
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' };
|
||||
}
|
||||
}
|
||||
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;
|
||||
@@ -9947,6 +9969,31 @@ export function matchRustUseBoundName(ref: UnresolvedRef, context: ResolutionCon
|
||||
/** References matchRustUseBoundName answered from the scope around them: a rename, a `let` that holds a function. */
|
||||
const RUST_SCOPED_REFS = new WeakMap<ResolutionContext, WeakSet<UnresolvedRef>>();
|
||||
|
||||
/**
|
||||
* The module path written right before `offset` in a Rust file's blanked
|
||||
* code, read back over whitespace and line breaks: `scheduler::` before
|
||||
* `Handle` gives `['scheduler']`. Empty when a `::` follows no plain name
|
||||
* (a turbofish's `>::`, a qualified self).
|
||||
*/
|
||||
function rustWrittenQualifier(code: string, offset: number): string[] {
|
||||
const segs: string[] = [];
|
||||
let i = offset;
|
||||
for (;;) {
|
||||
let j = i - 1;
|
||||
while (j >= 0 && /\s/.test(code[j]!)) j--;
|
||||
if (j < 1 || code[j] !== ':' || code[j - 1] !== ':') break;
|
||||
j -= 2;
|
||||
while (j >= 0 && /\s/.test(code[j]!)) j--;
|
||||
let k = j;
|
||||
while (k >= 0 && /\w/.test(code[k]!)) k--;
|
||||
const seg = code.slice(k + 1, j + 1);
|
||||
if (!/^[A-Za-z_]\w*$/.test(seg)) return [];
|
||||
segs.unshift(seg);
|
||||
i = k + 1;
|
||||
}
|
||||
return segs;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
@@ -9983,8 +10030,12 @@ function rustShadowingLocal(name: string, ref: UnresolvedRef, offset: number, vi
|
||||
return local;
|
||||
}
|
||||
|
||||
/** Kinds a Rust path to a module's item names: never a method, nor a variable (an impl's `const`, a `static`). */
|
||||
const RUST_PATH_ITEM_KINDS: ReadonlySet<string> = new Set(['function', 'struct', 'union', 'enum', 'trait', 'type_alias', 'constant']);
|
||||
/**
|
||||
* Kinds a Rust path to a module's item names: a module-level `pub const` or
|
||||
* `static` (extracted as a variable) too, never a method — the module's own
|
||||
* items only (rustDeclaredItems), so never an impl's `const`.
|
||||
*/
|
||||
const RUST_PATH_ITEM_KINDS: ReadonlySet<string> = new Set(['function', 'struct', 'union', 'enum', 'trait', 'type_alias', 'constant', 'variable']);
|
||||
|
||||
const RUST_PATH_TARGETS = new WeakMap<ResolutionContext, Map<string, string | null>>();
|
||||
|
||||
@@ -10005,11 +10056,16 @@ export function rustPathTarget(ref: UnresolvedRef, context: ResolutionContext):
|
||||
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);
|
||||
// `sim::items::ItemPool::for_def(…)`, `ItemPool::for_def(…)` under `use
|
||||
// sim::items::ItemPool;`: a call through a type its path or a `use` names
|
||||
// (Rust names its modules in snake_case, its types in CamelCase) is to that
|
||||
// type's associated function or variant. A type the module declares itself,
|
||||
// `Self`, and a type's path in any other position are left as they were.
|
||||
const owner = segs[segs.length - 2]!;
|
||||
const typed = !variants && /^[A-Z]/.test(owner);
|
||||
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';
|
||||
@@ -10019,19 +10075,74 @@ export function rustPathTarget(ref: UnresolvedRef, context: ResolutionContext):
|
||||
}
|
||||
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('::')}`;
|
||||
// (A type a `use` binds is that `use`'s, wherever it is written.)
|
||||
const scope = ownerUse ? `${ownerUse.block}\0${ownerUse.paths[0]!.at}` : '';
|
||||
const key = `${ref.filePath}\0${from.block?.open ?? -1}\0${variants ? 1 : 0}\0${scope}\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());
|
||||
// The items the path's segments name — up to its type's, for a typed one.
|
||||
const named = (path: readonly string[]): Node[] | null => {
|
||||
// A type a `use` binds, written alone: the item that `use` names.
|
||||
if (path.length === 1) {
|
||||
const ids = rustUseItemsAt(path[0]!, ref, context);
|
||||
return ids === null ? null : [...ids].map((id) => context.getNodeById?.(id)).filter((n): n is Node => !!n);
|
||||
}
|
||||
let items = rustUsePathItems(path, from, context, new Set());
|
||||
if (items === null && bare) {
|
||||
const root = rustCrateRootModule(ref.filePath, context);
|
||||
if (root) items = rustUsePathItems(path, root, context, new Set());
|
||||
}
|
||||
return items;
|
||||
};
|
||||
let target: Node | null = null;
|
||||
if (typed) {
|
||||
for (const type of named(segs.slice(0, -1)) ?? []) {
|
||||
target = rustTypeMember(type, segs[segs.length - 1]!, context);
|
||||
if (target) break;
|
||||
}
|
||||
} else {
|
||||
target = named(segs)?.find((n) => RUST_PATH_ITEM_KINDS.has(n.kind) || (variants && n.kind === 'enum_member')) ?? null;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
/** Kinds a type's associated item is extracted as: an impl's functions, a `const`, a variant. */
|
||||
const RUST_MEMBER_KINDS: ReadonlySet<string> = new Set(['method', 'function', 'variable', 'constant', 'enum_member']);
|
||||
|
||||
const RUST_TYPE_MEMBERS = new WeakMap<ResolutionContext, Map<string, string | null>>();
|
||||
|
||||
/**
|
||||
* The associated item `member` of a Rust type, as `Type::member` reaches it:
|
||||
* an enum's variant; else the type's own (inherent) function or `const`, a
|
||||
* trait impl's only when it is the one item of that name; a trait's own
|
||||
* declared method. An impl of the type is written in the type's crate, so a
|
||||
* namesake type's impl in another crate is never it. Null when none is.
|
||||
*/
|
||||
function rustTypeMember(type: Node, member: string, context: ResolutionContext): Node | null {
|
||||
if (type.language !== 'rust' || !RUST_DECLARED_TYPE_KINDS.has(type.kind) || type.kind === 'type_alias') return null;
|
||||
let memo = RUST_TYPE_MEMBERS.get(context);
|
||||
if (!memo) RUST_TYPE_MEMBERS.set(context, (memo = new Map()));
|
||||
const key = `${type.id}\0${member}`;
|
||||
const hit = memo.get(key);
|
||||
if (hit !== undefined) return hit === null ? null : context.getNodeById?.(hit) ?? null;
|
||||
let found: Node | null = null;
|
||||
if (type.kind === 'enum') found = rustEnumVariants(type, member, context)[0] ?? null;
|
||||
if (!found) {
|
||||
const graph = rustCrateGraph(context);
|
||||
const crate = rustCrateDirOf(type.filePath, graph);
|
||||
const within = (n: Node): boolean => type.kind !== 'trait' ||
|
||||
(n.filePath === type.filePath && n.startLine >= type.startLine && n.startLine <= (type.endLine ?? type.startLine));
|
||||
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));
|
||||
const pool = inherent.length > 0 ? inherent : all.length === 1 ? all : [];
|
||||
found = pool.find((n) => n.filePath === type.filePath) ?? pool[0] ?? null;
|
||||
}
|
||||
memo.set(key, found?.id ?? null);
|
||||
return found;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,
|
||||
@@ -20414,6 +20525,7 @@ export function clearNameMatcherMemos(context: ResolutionContext): void {
|
||||
RUST_USE_ITEMS.delete(context);
|
||||
RUST_VISIBLE_ITEMS.delete(context);
|
||||
RUST_PATH_TARGETS.delete(context);
|
||||
RUST_TYPE_MEMBERS.delete(context);
|
||||
RUST_SCOPED_REFS.delete(context);
|
||||
RUST_GLOB_ITEMS.delete(context);
|
||||
RUST_CRATES.delete(context);
|
||||
|
||||
Reference in New Issue
Block a user