mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-11-14 22:35:47 +08:00
A bare Rust name took the file's `use`s all at once: a `use` inside one
function (`fn first() { use crate::util::take; take() }`) bound `take` for
every other function too, and for the file's own `fn take`; a renamed one
(`use crate::util::take as consume; consume()`) linked nothing, since no
item carries the alias; a parameter or closure of the function's name
(`fn run(take: fn()) { take() }`, `let take = || {};`) linked to the
imported function; and `use crate::models::E::Good; Good(1)` took any
`E::Good`. Paths through modules (`resolveRustPathReference`) walked the
directories instead of the modules: `super::helper()` and `crate::config()`
with no module between linked nothing, `super` inside an inline
`mod tests { }` went to the parent directory's module, and an item a module
passes on (`pub use`, a glob) or another workspace crate's was not found.
The fix keeps one `use` reader, rustUsesOf:
- each `use` keeps the block it is written in (a function's body or a
block; an inline `mod` and a macro's braces, `cfg_if!`'s nested ones
included, are the module's), and std `use`s are kept by position;
- a reference reads the binding of its innermost scope (rustVisibleUse);
isRustNameInScope uses it, so a block's `use` shadows the file's own item
and a variant `use` names its enum's variant (rustUsePathItems' enum tail);
- matchRustUseBoundName, ahead of the pre-filter, resolves a rename to the
item its path names, and a call to a name a local binds (rust-scopes'
bindings, nearer than any `use`) to nothing, unless the local is a `let`
holding a function by its path (serde's `let test = test::<i8>;`);
- resolveRustPathReference is rustWalkModules + rustModuleItems from the
module the path is written in (rustPathTarget), with today's crate-root
fallback; resolveRustModuleFile, collectRustUseBindings and
rustItemPlacement are gone, and isBoundToOutOfRepoImport reads
rustUsesOf's std `use`s;
- `use`s are read with strings blanked (blankRustCode), so a string's
"use crate::x;" binds nothing;
- the crate graph reads a package's name by its section (axum-core writes
`categories = [..]` first) and a `[lib]`/`[[bin]]` path of its own
(ripgrep's `crates/core/main.rs`), which `crate::` then starts from; a
file no manifest covers starts it from the nearest lib.rs or main.rs
above it, as the directory walk did.
Validation:
- rust-use-call.test.ts (PR 2308's 15 cases), rust-use-scopes.test.ts,
rust-crate-roots.test.ts and 5 new rust-module-path-call cases: on main's
source 18 of 32 fail; all pass. Every rust-* suite passes.
- A/B on main 01e0ac3a, 7 repos, the same sites on the kernel and on wasm:
tokio +2,434 sites (1,354 calls, 1,053 imports), 159 removed (152 locals
called or passed as values, 6 `Result`s std's, 1 import through a path
attribute module); ripgrep +248 / -149; axum +710 / -60; clap +730 / -112;
serde +155 / -207 (192 calls to a closure named `test`); dioxus
+2,950 / -272; gleam +1,573 / -359. Groups and samples inspected, every
changed site's crate checked against the manifests' dependencies;
express and flask byte-identical.
- Cost (kernel, medians of 3 interleaved): tokio CPU 34.5 -> 33.0 s, peak
RSS 991 -> 1,007 MB; gleam 32.5 -> 30.6 s, 1,022 -> 1,012 MB; dioxus
45.4 -> 45.4 s, 1,101 -> 1,107 MB.
- Original PR #2308 by @danusha2345; its module-path part landed as PR #2955.
Co-authored-by: danusha2345 <ewidusoc498@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
318 lines
10 KiB
TypeScript
318 lines
10 KiB
TypeScript
/**
|
|
* A Rust call written through a module path — `crate::util::take(3)`,
|
|
* `super::util::take(3)`, `self::util::take(3)`, `util::take(3)` after
|
|
* `use crate::util;` — is the free function that module declares, and so is
|
|
* a `use crate::util::take;`. A path never names a method (that takes
|
|
* `Type::method`), nor a function local to another function's body, even when
|
|
* the module's file declares one of the same name above the free function.
|
|
*/
|
|
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';
|
|
|
|
let root = '';
|
|
let cg: CodeGraph;
|
|
|
|
beforeAll(async () => {
|
|
root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-rust-module-path-'));
|
|
const files: Record<string, string> = {
|
|
'Cargo.toml': '[package]\nname = "app"\nversion = "0.1.0"\n',
|
|
'src/lib.rs': `mod util;
|
|
mod only;
|
|
mod a;
|
|
mod caller;
|
|
mod importer;
|
|
mod handle;
|
|
mod worker;
|
|
mod twice;
|
|
mod inline_reexport;
|
|
mod fs;
|
|
mod uses_std;
|
|
mod b;
|
|
mod x;
|
|
mod types;
|
|
mod type_;
|
|
mod decoy_list;
|
|
mod uses_config;
|
|
mod walks;
|
|
mod plat;
|
|
|
|
pub struct Config;
|
|
|
|
pub fn root_fn() -> usize {
|
|
0
|
|
}
|
|
|
|
pub fn via_self() -> usize {
|
|
self::util::take(1)
|
|
}
|
|
`,
|
|
// `super::x` and `crate::x`, with no module between them and the item.
|
|
'src/b/mod.rs': 'pub mod c;\npub fn helper() -> usize {\n 1\n}\n',
|
|
'src/b/c.rs': 'pub fn helper() -> usize {\n 2\n}\npub fn via_bare_super() -> usize {\n super::helper()\n}\npub fn via_bare_crate() -> usize {\n crate::root_fn()\n}\n',
|
|
// `super` in an inline `mod tests { }` is the file's own module, not its directory's parent.
|
|
'src/x/mod.rs': 'pub mod tested;\npub mod shared;\npub struct Config;\npub fn root_fn() -> usize {\n 9\n}\n',
|
|
'src/x/shared.rs': 'pub fn helper() -> usize {\n 3\n}\n',
|
|
'src/x/tested.rs': `mod shared;
|
|
|
|
pub fn tested_helper() -> usize {
|
|
4
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::tested_helper;
|
|
|
|
fn via_inline_super() -> usize {
|
|
super::shared::helper() + tested_helper()
|
|
}
|
|
}
|
|
`,
|
|
'src/x/tested/shared.rs': 'pub fn helper() -> usize {\n 5\n}\n',
|
|
// A module's item it passes on: a `pub use`, a `pub use …::*`.
|
|
'src/types/mod.rs': 'mod list;\npub use self::list::make_list;\n',
|
|
'src/types/list.rs': 'pub fn make_list() -> usize {\n 6\n}\n',
|
|
'src/decoy_list.rs': 'pub fn make_list() -> usize {\n 7\n}\n',
|
|
'src/type_/mod.rs': 'pub mod prelude;\npub use prelude::*;\n\npub struct Type;\n\nimpl Type {\n pub fn list() -> usize {\n 8\n }\n}\n',
|
|
'src/type_/prelude.rs': 'pub fn list() -> usize {\n 9\n}\n',
|
|
'src/uses_config.rs': 'use crate::{Config};\n',
|
|
// A re-export written in a macro's braces, as `cfg_if!` takes them, is the module's.
|
|
'src/plat/mod.rs': 'cfg_if::cfg_if! {\n if #[cfg(unix)] {\n mod unix;\n pub use self::unix::open;\n }\n}\n',
|
|
'src/plat/unix.rs': 'pub fn open() -> usize {\n 10\n}\n',
|
|
'src/decoy_open.rs': 'pub fn open() -> usize {\n 11\n}\n',
|
|
'src/walks.rs': `pub fn via_reexport() -> usize {
|
|
crate::types::make_list()
|
|
}
|
|
|
|
pub fn via_glob() -> usize {
|
|
crate::type_::list()
|
|
}
|
|
|
|
pub fn via_macro_reexport() -> usize {
|
|
crate::plat::open()
|
|
}
|
|
|
|
pub fn via_block_std() {
|
|
use std::fs;
|
|
let _ = fs::read("config.toml");
|
|
}
|
|
`,
|
|
// A namesake in an inline module, before the module's own item.
|
|
'src/twice.rs': `#[cfg(test)]
|
|
mod tests {
|
|
pub fn pick() -> usize {
|
|
0
|
|
}
|
|
}
|
|
|
|
pub fn pick() -> usize {
|
|
1
|
|
}
|
|
|
|
pub fn via_twice() -> usize {
|
|
crate::twice::pick()
|
|
}
|
|
`,
|
|
// The item lives in an inline module the file passes on with a glob.
|
|
'src/inline_reexport.rs': `#[cfg(unix)]
|
|
mod imp {
|
|
pub fn lone() {}
|
|
}
|
|
pub use imp::*;
|
|
|
|
pub fn via_inline() {
|
|
crate::inline_reexport::lone();
|
|
}
|
|
`,
|
|
// A project module named like a module of the standard library.
|
|
'src/fs/mod.rs': 'pub fn read(path: &str) -> usize {\n path.len()\n}\n',
|
|
'src/uses_std.rs': `use std::fs;
|
|
|
|
pub fn loads() {
|
|
let _ = fs::read("config.toml");
|
|
}
|
|
`,
|
|
'src/util.rs': `pub struct Buf;
|
|
|
|
impl Buf {
|
|
pub fn take(&self, n: usize) -> usize {
|
|
n
|
|
}
|
|
}
|
|
|
|
pub trait Take {
|
|
fn take(&self) -> usize;
|
|
}
|
|
|
|
fn outer() -> usize {
|
|
fn take() -> usize {
|
|
0
|
|
}
|
|
take()
|
|
}
|
|
|
|
pub fn take(n: usize) -> usize {
|
|
n
|
|
}
|
|
`,
|
|
'src/only.rs': `pub struct Only;
|
|
|
|
impl Only {
|
|
pub fn grab(&self) -> usize {
|
|
0
|
|
}
|
|
}
|
|
`,
|
|
'src/a/mod.rs': 'pub mod b;\n',
|
|
'src/a/b.rs': `pub struct Deep;
|
|
|
|
impl Deep {
|
|
pub fn get(&self) -> usize {
|
|
0
|
|
}
|
|
}
|
|
|
|
pub fn get() -> usize {
|
|
1
|
|
}
|
|
`,
|
|
'src/importer.rs': 'use crate::util::take;\n',
|
|
'src/handle.rs': 'pub struct Handle;\n',
|
|
'src/worker.rs': `pub use crate::handle::Handle;
|
|
mod dump;
|
|
|
|
pub trait Lock {
|
|
type Handle;
|
|
}
|
|
|
|
pub struct Guard;
|
|
|
|
impl Lock for Guard {
|
|
type Handle = u8;
|
|
}
|
|
`,
|
|
'src/worker/dump.rs': 'use super::Handle;\n',
|
|
'src/caller.rs': `use crate::util;
|
|
|
|
pub fn via_crate() -> usize {
|
|
crate::util::take(1)
|
|
}
|
|
|
|
pub fn via_super() -> usize {
|
|
super::util::take(1)
|
|
}
|
|
|
|
pub fn via_module() -> usize {
|
|
util::take(1)
|
|
}
|
|
|
|
pub fn via_nested() -> usize {
|
|
crate::a::b::get()
|
|
}
|
|
|
|
pub fn via_method_only() -> usize {
|
|
crate::only::grab()
|
|
}
|
|
|
|
pub fn via_missing() -> usize {
|
|
crate::nowhere::take(1)
|
|
}
|
|
`,
|
|
};
|
|
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);
|
|
}
|
|
cg = await CodeGraph.init(root, { index: true });
|
|
});
|
|
|
|
afterAll(() => {
|
|
cg?.close();
|
|
if (root) fs.rmSync(root, { recursive: true, force: true });
|
|
});
|
|
|
|
/** What the function `name` in `file` calls, as `kind qualifiedName file:line`. */
|
|
const callsFrom = (file: string, name: string): string[] => {
|
|
const fn = cg.getNodesInFile(file).find((n) => n.name === name)!;
|
|
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}`);
|
|
};
|
|
|
|
describe('Rust calls through a module path', () => {
|
|
it('reach the free function the module declares, never a same-named method or local fn', () => {
|
|
const take = 'function take src/util.rs:20';
|
|
expect(callsFrom('src/caller.rs', 'via_crate')).toEqual([take]);
|
|
expect(callsFrom('src/caller.rs', 'via_super')).toEqual([take]);
|
|
expect(callsFrom('src/caller.rs', 'via_module')).toEqual([take]);
|
|
expect(callsFrom('src/lib.rs', 'via_self')).toEqual([take]);
|
|
});
|
|
|
|
it('bind a use of the function to it', () => {
|
|
const ids = cg.getNodesInFile('src/importer.rs').map((n) => n.id);
|
|
const targets = cg.getOutgoingEdgesFrom(ids).filter((e) => e.kind === 'imports')
|
|
.map((e) => cg.getNode(e.target)!).map((t) => `${t.kind} ${t.qualifiedName} ${t.filePath}:${t.startLine}`);
|
|
expect(targets).toEqual(['function take src/util.rs:20']);
|
|
});
|
|
|
|
it('never bind a use to an associated type of the module\'s impl or trait', () => {
|
|
const ids = cg.getNodesInFile('src/worker/dump.rs').map((n) => n.id);
|
|
const targets = cg.getOutgoingEdgesFrom(ids).filter((e) => e.kind === 'imports')
|
|
.map((e) => cg.getNode(e.target)!).map((t) => `${t.kind} ${t.qualifiedName} ${t.filePath}:${t.startLine}`);
|
|
expect(targets.filter((t) => t.includes('src/worker.rs'))).toEqual([]);
|
|
});
|
|
|
|
it('walk a nested module path to its file', () => {
|
|
expect(callsFrom('src/caller.rs', 'via_nested')).toEqual(['function get src/a/b.rs:9']);
|
|
});
|
|
|
|
it('link nothing when the module declares no such free function, or is not in the index', () => {
|
|
expect(callsFrom('src/caller.rs', 'via_method_only')).toEqual([]);
|
|
expect(callsFrom('src/caller.rs', 'via_missing')).toEqual([]);
|
|
});
|
|
|
|
it('prefer the module\'s own item to a namesake in a module it declares inline', () => {
|
|
expect(callsFrom('src/twice.rs', 'via_twice')).toEqual(['function pick src/twice.rs:8']);
|
|
});
|
|
|
|
it('reach an inline module\'s item the module passes on, from the module\'s own file', () => {
|
|
expect(callsFrom('src/inline_reexport.rs', 'via_inline')).toEqual(['function lone src/inline_reexport.rs:3']);
|
|
});
|
|
|
|
it('leave a path through a module a `use` takes from the standard library to it', () => {
|
|
// `fs::read` under `use std::fs;` is std's, not `src/fs/mod.rs`'s `read`.
|
|
expect(callsFrom('src/uses_std.rs', 'loads')).toEqual([]);
|
|
expect(callsFrom('src/walks.rs', 'via_block_std')).toEqual([]);
|
|
});
|
|
|
|
it('reach the item of the module `super` and `crate` name, with no module between', () => {
|
|
expect(callsFrom('src/b/c.rs', 'via_bare_super')).toEqual(['function helper src/b/mod.rs:2']);
|
|
expect(callsFrom('src/b/c.rs', 'via_bare_crate')).toEqual(['function root_fn src/lib.rs:23']);
|
|
});
|
|
|
|
it('read `super` in an inline module as the module of its file', () => {
|
|
expect(callsFrom('src/x/tested.rs', 'via_inline_super').sort()).toEqual([
|
|
'function helper src/x/tested/shared.rs:1',
|
|
'function tested_helper src/x/tested.rs:3',
|
|
]);
|
|
const ids = cg.getNodesInFile('src/x/tested.rs').map((n) => n.id);
|
|
const imported = cg.getOutgoingEdgesFrom(ids).filter((e) => e.kind === 'imports')
|
|
.map((e) => cg.getNode(e.target)!).map((t) => `${t.kind} ${t.qualifiedName} ${t.filePath}:${t.startLine}`);
|
|
expect(imported).toEqual(['function tested_helper src/x/tested.rs:3']);
|
|
});
|
|
|
|
it('reach an item the module passes on with a `pub use`, or a glob of one', () => {
|
|
expect(callsFrom('src/walks.rs', 'via_reexport')).toEqual(['function make_list src/types/list.rs:1']);
|
|
expect(callsFrom('src/walks.rs', 'via_glob')).toEqual(['function list src/type_/prelude.rs:1']);
|
|
expect(callsFrom('src/walks.rs', 'via_macro_reexport')).toEqual(['function open src/plat/unix.rs:1']);
|
|
});
|
|
|
|
it('bind a `use crate::{Item}` to the crate root\'s item', () => {
|
|
const ids = cg.getNodesInFile('src/uses_config.rs').map((n) => n.id);
|
|
const imported = cg.getOutgoingEdgesFrom(ids).filter((e) => e.kind === 'imports')
|
|
.map((e) => cg.getNode(e.target)!).map((t) => `${t.kind} ${t.qualifiedName} ${t.filePath}:${t.startLine}`);
|
|
expect(imported).toEqual(['struct Config src/lib.rs:21']);
|
|
});
|
|
});
|