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 @@ -229,6 +229,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- In Rust, a method called through `self: Pin<&mut Self>`, `self: Box<Self>`, `self: Rc<Self>` or `self: Arc<Self>`, or on a value declared `Pin<&mut …>`, now links the way Rust looks it up: to the type's own method, like `self.poll_flush(cx)` inside tokio's `impl AsyncWrite for File`, rather than to a catch-all `impl AsyncWrite for Pin<P>`, while `Pin`'s own methods, like `self.get_mut()`, stay unlinked. A type's own method now also comes before a trait's of the same name, so tokio's `Arc::from_raw(…)` calls are the standard library's rather than its internal `Link` trait's, and a method called on a tuple, like serde's `(self.ip(), self.port()).serialize(…)`, no longer links to an unrelated type's. Before, most calls made through such a `self` linked to nothing. Re-index Rust projects after upgrading.
- In Rust, a method called on what `.as_mut()` or `.as_ref()` hands back now links by the type its receiver is declared with: `self.delay.as_mut().reset(…)` on a `Pin<Box<Sleep>>` field reaches `Sleep::reset`, a field pin-project hands out reaches its own type's method, and a `Box` or `NonNull` lends what it holds. A method on an `Option` reached this way, like `self.child.as_mut().map(…)`, or on a generic type no longer links to an unrelated project method that shares its name. Before, these calls were matched by name alone, so tokio's `self.delay.as_mut().reset(…)` linked to the calling `Interval::reset`, and dioxus's `Option` calls to its own `map` and `filter` helpers. Re-index Rust projects after upgrading.
- In Rust, a type alias, struct, enum or function declared without `pub` now links only from its own module and the modules inside it, and a name a `use` brings in from one of your workspace's crates, like `use tokio_test::task::spawn` or `use crate::error::Error`, links only to an item of that crate or of a crate it depends on. Before, a file's `use crate::Result` could link its `Result<…>` types to another module's private `type Result`, as in dioxus's CLI, and a call to an imported function could link to another crate's function of the same name, such as tokio's test `spawn(…)` calls to one in its examples, or to nothing because a private function of that name was in the way. Re-index Rust projects after upgrading.
- In Rust, a name a `use` brings in, like `use crate::sync::watch::Receiver` or `use tokio::task::yield_now`, now links to the item of that name in the module its path leads to, found the way Rust finds it: declared there, in a module declared inline with `mod name { … }`, or passed on by that module's own `pub use`, including one written inside a macro like tokio's `cfg_sync! { … }`. Before, any item of that name in the crate could take it, so tokio's `Receiver<()>` linked to the `Receiver` of `net/unix/pipe.rs`, `crate::runtime::Handle` to the runtime driver's `Handle`, and `crate::runtime::task::JoinHandle` to a test mock. A name whose item is declared by a macro, which the index doesn't see, now links to nothing rather than to a same-named item elsewhere. Re-index Rust projects after upgrading.
- In Rust, a name written on its own, like an `Err(…)` call or a `Response` return type, or through a module, like `io::Error` or `use crate::de::Error`, no longer links to a `type` or `const` declared inside an `impl`, such as a `FromStr` impl's `type Err` or a `Service` impl's `type Response`: only a path like `Self::Err` names those, and they are now listed under their type, as `ValueHint::Err`. Before, serde's and ripgrep's `Ok(…)` and `Err(…)` calls linked to a serializer's or parser's `type Ok` or `type Err`, axum's handlers' `Response` to a `Service` impl's, and ripgrep's `impl Captures for RegexCaptures` to another impl's `type Captures` instead of the `Captures` trait. Re-index Rust projects after upgrading.
- In C++, every name a declaration lists now has the declaration's type, like `strings_found` in google/breakpad's `ByteBuffer symbols_found, strings_found;` or `data_` in a class's `Table *index_, *data_;`, so calls on it link to that class's methods. Before, only the first name had its type read, and a call on another name linked to nothing or to the class of an earlier variable with the same name.
- In C++, a call on a parameter, a local lambda or functor, a lambda's parameter or a range-`for` variable no longer links to a function or method elsewhere that only shares its name, like protocolbuffers/protobuf's code generators calling `format(…)` on their `Formatter& format` parameter, which linked to the conformance suite's `TestResult::format`, or facebook/rocksdb's tests calling a local `auto key = [](int i) {…};`. C++ finds the local first, so that is what the call calls.
Expand Down
230 changes: 230 additions & 0 deletions __tests__/rust-use-path-module.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
/**
* A name a Rust `use` binds is the item its path names: the item of that name
* in the module the path walks to. tokio's `signal/mod.rs` writes `use
* crate::sync::watch::Receiver;` and tokio-stream `use
* tokio::sync::watch::Receiver;`, and both linked their `Receiver<()>` to the
* `Receiver` of `net/unix/pipe.rs`: every `Receiver` in the crate read as in
* scope, because the file imports a `Receiver`, and the first one won. The
* path's module now decides — its own items, then what it re-exports (`pub
* use mutex::{Mutex, …}`, macro-wrapped as tokio's `cfg_sync! { … }` are),
* and the modules declared inline (`pub mod futures { … }`). Name matching
* still answers when the path's module has no file to read.
*/
import { describe, it, expect, afterAll, beforeAll } from 'vitest';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { CodeGraph } from '../src';

/** Writes `files` into a fresh temp dir and indexes it. */
async function indexProject(prefix: string, files: Record<string, string>): Promise<{ root: string; cg: CodeGraph }> {
const root = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
for (const [rel, content] of Object.entries(files)) {
fs.mkdirSync(path.dirname(path.join(root, rel)), { recursive: true });
fs.writeFileSync(path.join(root, rel), content);
}
return { root, cg: await CodeGraph.init(root, { index: true }) };
}

/** What `name`, written in `file`, links to, as `file@line` of each target, sorted. */
const targetsOf = (cg: CodeGraph, file: string, name: string, kinds: string[]): string[] => {
const out: string[] = [];
for (const node of cg.getNodesInFile(file)) {
for (const edge of cg.getOutgoingEdges(node.id)) {
if (!kinds.includes(edge.kind)) continue;
const target = cg.getNode(edge.target)!;
if (target.name === name) out.push(`${target.filePath}@${target.startLine}`);
}
}
return [...new Set(out)].sort();
};

describe('a name a Rust `use` binds, by the module its path walks', () => {
let root = '';
let cg: CodeGraph;

beforeAll(async () => {
({ root, cg } = await indexProject('cg-rust-use-path-', {
'Cargo.toml': '[workspace]\nmembers = ["tokio", "tokio-stream"]\n',
'tokio/Cargo.toml': '[package]\nname = "tokio"\nversion = "0.1.0"\n',
'tokio/src/lib.rs': 'pub mod fs;\npub mod net;\npub mod signal;\npub mod sync;\nmod loom;\n',
// Namesakes in modules that sort ahead of `sync`.
'tokio/src/fs/mod.rs': 'pub struct Ready;\n\npub struct Notified;\n',
'tokio/src/loom/mod.rs': 'pub(crate) struct Mutex<T>(T);\n',
'tokio/src/net/mod.rs': 'pub mod unix;\n',
'tokio/src/net/unix/mod.rs': 'pub mod pipe;\n',
'tokio/src/net/unix/pipe.rs': [
'pub struct Receiver {',
' fd: i32,',
'}',
'',
'pub struct Sender {',
' fd: i32,',
'}',
'',
'pub fn channel() -> Receiver {',
' Receiver { fd: 0 }',
'}',
'',
].join('\n'),
'tokio/src/sync/mod.rs': [
'macro_rules! cfg_sync {',
' ($($item:item)*) => { $( $item )* }',
'}',
'',
'cfg_sync! {',
' mod mutex;',
' pub use mutex::{Mutex, MutexGuard};',
'',
' pub(crate) mod notify;',
'',
' pub mod broadcast;',
' pub mod watch;',
'}',
'',
'pub mod futures {',
' pub use super::notify::Notified;',
'',
' pub struct Ready;',
'}',
'',
].join('\n'),
'tokio/src/sync/mutex.rs': 'pub struct Mutex<T> {\n value: T,\n}\n\npub struct MutexGuard;\n',
'tokio/src/sync/notify.rs': 'pub struct Notified;\n',
'tokio/src/sync/broadcast.rs': 'use super::Mutex;\n\npub fn tail(lock: Mutex<u64>) {}\n',
'tokio/src/sync/watch.rs': [
'macro_rules! sender {',
' () => { pub struct Sender; }',
'}',
'',
'sender!();',
'',
'pub struct Receiver<T> {',
' shared: T,',
'}',
'',
'pub fn channel<T>(init: T) -> Receiver<T> {',
' Receiver { shared: init }',
'}',
'',
].join('\n'),
'tokio/src/signal/mod.rs': [
'use crate::sync::futures::{Notified, Ready};',
'use crate::sync::watch::{channel, Receiver, Sender};',
'use crate::sync::Mutex;',
'use crate::gen::Thing;',
'',
'pub fn make(lock: Mutex<u8>) -> Receiver<()> {',
' channel(())',
'}',
'',
'pub fn send(tx: Sender) {}',
'',
'pub fn wait(n: Notified, r: Ready, t: Thing) {}',
'',
].join('\n'),
// Generated at build time: the path's module has no file to read.
'tokio/src/signal/thing.rs': 'pub struct Thing;\n',
'tokio/tests/sync_watch.rs': 'use tokio::sync::watch::Receiver;\n\nfn rx(r: Receiver<u8>) -> Receiver<u8> {\n r\n}\n',
'tokio-stream/Cargo.toml': '[package]\nname = "tokio-stream"\nversion = "0.1.0"\n\n[dependencies]\ntokio = { path = "../tokio" }\n',
'tokio-stream/src/lib.rs': 'pub mod wrappers;\n',
'tokio-stream/src/wrappers/mod.rs': 'mod watch;\npub use watch::stream;\n',
'tokio-stream/src/wrappers/watch.rs': 'use tokio::sync::watch::Receiver;\n\npub fn stream<T>(rx: Receiver<T>) -> Receiver<T> {\n rx\n}\n',
}));
}, 60_000);

afterAll(() => {
cg?.close();
if (root) fs.rmSync(root, { recursive: true, force: true });
});

it('`use crate::sync::watch::Receiver` is watch.rs\'s Receiver, not a namesake elsewhere in the crate', () => {
expect(targetsOf(cg, 'tokio/src/signal/mod.rs', 'Receiver', ['references'])).toEqual(['tokio/src/sync/watch.rs@7']);
});

it('so is `use tokio::sync::watch::Receiver`, from tokio\'s own tests and from a crate that depends on it', () => {
expect(targetsOf(cg, 'tokio/tests/sync_watch.rs', 'Receiver', ['references'])).toEqual(['tokio/src/sync/watch.rs@7']);
expect(targetsOf(cg, 'tokio-stream/src/wrappers/watch.rs', 'Receiver', ['references'])).toEqual(['tokio/src/sync/watch.rs@7']);
});

it('follows what the module re-exports, inside a macro too', () => {
expect(targetsOf(cg, 'tokio/src/signal/mod.rs', 'Mutex', ['references'])).toEqual(['tokio/src/sync/mutex.rs@1']);
});

it('`use super::Mutex` starts from the parent module', () => {
expect(targetsOf(cg, 'tokio/src/sync/broadcast.rs', 'Mutex', ['references'])).toEqual(['tokio/src/sync/mutex.rs@1']);
});

it('reaches into a module declared inline, and what it re-exports', () => {
expect(targetsOf(cg, 'tokio/src/signal/mod.rs', 'Ready', ['references'])).toEqual(['tokio/src/sync/mod.rs@18']);
expect(targetsOf(cg, 'tokio/src/signal/mod.rs', 'Notified', ['references'])).toEqual(['tokio/src/sync/notify.rs@1']);
});

it('links a call the same way', () => {
expect(targetsOf(cg, 'tokio/src/signal/mod.rs', 'channel', ['calls'])).toEqual(['tokio/src/sync/watch.rs@11']);
});

it('links nothing when the module holds no such item the index knows', () => {
// watch.rs's `Sender` is a macro's: no node, and pipe.rs's is not it.
expect(targetsOf(cg, 'tokio/src/signal/mod.rs', 'Sender', ['references'])).toEqual([]);
});

it('falls back to name matching when the path\'s module has no file', () => {
expect(targetsOf(cg, 'tokio/src/signal/mod.rs', 'Thing', ['references'])).toEqual(['tokio/src/signal/thing.rs@1']);
});

it('reads an edited `use` afresh on sync', async () => {
fs.writeFileSync(path.join(root, 'tokio/src/sync/broadcast.rs'), 'use crate::loom::Mutex;\n\npub fn tail(lock: Mutex<u64>) {}\n');
await cg.sync();
expect(targetsOf(cg, 'tokio/src/sync/broadcast.rs', 'Mutex', ['references'])).toEqual(['tokio/src/loom/mod.rs@1']);
});
});

describe('what a Rust module re-exports, when its names have more than one meaning', () => {
let root = '';
let cg: CodeGraph;

beforeAll(async () => {
({ root, cg } = await indexProject('cg-rust-use-ns-', {
'Cargo.toml': '[workspace]\nmembers = ["app", "clap", "clap_builder", "clap_derive"]\n',
// clap re-exports its derive macros by name and its traits by a glob:
// `ValueEnum` is both, a macro and a trait.
'clap/Cargo.toml': '[package]\nname = "clap"\nversion = "0.1.0"\n\n[dependencies]\nclap_builder = { path = "../clap_builder" }\nclap_derive = { path = "../clap_derive" }\n',
'clap/src/lib.rs': 'pub use clap_builder::*;\n\n#[cfg(feature = "derive")]\npub use clap_derive::{Parser, ValueEnum};\n',
'clap_builder/Cargo.toml': '[package]\nname = "clap_builder"\nversion = "0.1.0"\n',
'clap_builder/src/lib.rs': 'mod derive;\n\npub use crate::derive::{Parser, ValueEnum};\n',
'clap_builder/src/derive.rs': 'pub trait Parser {}\n\npub trait ValueEnum {}\n',
'clap_derive/Cargo.toml': '[package]\nname = "clap_derive"\nversion = "0.1.0"\n\n[lib]\nproc-macro = true\n',
'clap_derive/src/lib.rs': 'use proc_macro::TokenStream;\n\n#[proc_macro_derive(ValueEnum)]\npub fn value_enum(input: TokenStream) -> TokenStream {\n input\n}\n',
'app/Cargo.toml': '[package]\nname = "app"\nversion = "0.1.0"\n\n[dependencies]\nclap = { path = "../clap" }\nglobal-hotkey = "0.6"\n',
'app/src/lib.rs': 'mod desktop;\nmod mobile_shortcut;\nmod shell;\nmod shortcut;\nmod sys;\nmod user;\n',
'app/src/shell.rs': 'use clap::ValueEnum;\n\npub enum Shell {\n Bash,\n}\n\nimpl ValueEnum for Shell {}\n',
// One cfg re-exports another crate's type, the other the project's.
'app/src/shortcut.rs': '#[cfg(not(target_os = "ios"))]\npub use global_hotkey::HotKey;\n\n#[cfg(target_os = "ios")]\npub use crate::mobile_shortcut::*;\n',
'app/src/mobile_shortcut.rs': 'pub struct HotKey;\n',
'app/src/desktop.rs': 'use crate::shortcut::HotKey;\n\npub fn register(key: HotKey) {}\n',
// A module a `#[path]` puts in another file on one target.
'app/src/sys/mod.rs': '#[cfg(target_os = "ios")]\n#[path = "ios.rs"]\nmod imp;\n\n#[cfg(not(target_os = "ios"))]\nmod imp;\n\npub use imp::Handle;\n',
'app/src/sys/imp.rs': 'pub use std::fs::File as Handle;\n',
'app/src/sys/ios.rs': 'pub struct Handle;\n',
'app/src/user.rs': 'use crate::sys::Handle;\n\npub fn open(handle: Handle) {}\n',
}));
}, 60_000);

afterAll(() => {
cg?.close();
if (root) fs.rmSync(root, { recursive: true, force: true });
});

it('a name `use`d as a macro still comes in as a trait through a glob', () => {
expect(targetsOf(cg, 'app/src/shell.rs', 'ValueEnum', ['implements'])).toEqual(['clap_builder/src/derive.rs@3']);
});

it('a re-export of another crate\'s type leaves the project\'s, another cfg\'s, in scope', () => {
expect(targetsOf(cg, 'app/src/desktop.rs', 'HotKey', ['references'])).toEqual(['app/src/mobile_shortcut.rs@1']);
});

it('a module whose file a `#[path]` names says nothing about its items', () => {
expect(targetsOf(cg, 'app/src/user.rs', 'Handle', ['references'])).toEqual(['app/src/sys/ios.rs@1']);
});
});
Loading