mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-11-15 03:08:20 +08:00
A Rust path into a module, `crate::util::take(1)`, `super::util::take(1)`,
`util::take(1)` or a `use crate::util::take;`, went to the first node of
that name in the module's file, methods included, so an `impl Buf { fn
take }` above `pub fn take` caught every such call. A function written
inside another function, or in a `mod tests { ... }` above the module's
own item, could take it too, a path into the module's own file linked
nothing, and a path through a module a `use` takes from the standard
library (`fs::read` under `use std::fs;`) went to the project's own `fs`
module.
The path's item is now the module's own: never a method, an impl's or a
trait's associated item, or an item of a function body; one written at
the file's top level before one in a module the file declares inline,
which the path reaches through the module's re-export (`#[cfg(unix)] mod
imp { ... }` + `pub use imp::*;`). A path whose head a `use` binds from
the standard library is left to it (main's isBoundToOutOfRepoImport rule
for bare names), and a path into the module's own file resolves.
Validation:
- rust-module-path-call.test.ts: 8/8; on main 7 fail (method Buf::take,
Deep::get, Only::grab, no import edge, the inline-module namesake, the
re-export from its own file, std `fs::read` -> src/fs/mod.rs).
- A/B on main 7cceba52, 6 Rust repos: 162 changed sites, all inspected and
right. gleam: 133 calls like `fs::write(...)` / `crate::fs::mkdir(...)` move
from `impl ... for ProjectIO`'s methods to compiler-cli's free functions,
4 wrong `type_::list(...)` -> `Type::list` edges are gone, 15 `use`s link;
tokio: `crate::io::uring::open::open(...)` moves from `Op::open` to the free
`open`, +4; dioxus +7, 1 more; ripgrep +1; clap and serde unchanged.
express and flask byte-identical.
- Cost (medians of 3 interleaved runs): tokio CPU 36.6 -> 36.0 s, gleam
34.3 -> 35.2 s, both within the runs' spread; peak RSS unchanged. In a
CPU profile of tokio the changed functions take under 0.1 s. The
standard-library check reads the file's `use`s, so it runs only when the
path would otherwise link a project item (asked for every path, it cost
0.3 s on tokio).
This is the separable part of PR #2308. Its bare-call bindings (`as`
aliases, block-local `use`, shadowing by locals, enum variants through a
`use`) and the second `use` parser it adds are left out: main's `use` walk
(rustUsesOf, rustWalkModules, rustModuleItems) and scope reader
(rust-scopes.ts) cover the same ground, and folding those cases into them
is proposed separately.
Original PR #2308 by @danusha2345.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
224 lines
6.1 KiB
TypeScript
224 lines
6.1 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;
|
|
|
|
pub fn via_self() -> usize {
|
|
self::util::take(1)
|
|
}
|
|
`,
|
|
// 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([]);
|
|
});
|
|
});
|