mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-11-15 07:15:37 +08:00
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>
243 lines
12 KiB
TypeScript
243 lines
12 KiB
TypeScript
/**
|
|
* 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');
|
|
});
|