Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
242 changes: 242 additions & 0 deletions __tests__/rust-cross-crate.test.ts
Original file line number Diff line number Diff line change
@@ -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');
});
60 changes: 60 additions & 0 deletions __tests__/rust-manifest-paths.test.ts
Original file line number Diff line number Diff line change
@@ -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');
});
});
Loading