Files
codegraph/__tests__/rust-use-call.test.ts
T
4955eac958 fix(rust): use bindings follow Rust's scopes, and module paths walk the crate's modules (#3014)
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>
2026-10-29 18:39:13 +00:00

221 lines
10 KiB
TypeScript

/**
* A bare Rust name a `use` binds: the item that `use`'s path names, where
* the `use` is in scope — a block's own `use` before its module's, a rename
* (`use crate::util::take as consume`), an enum's variant — and nothing when
* a local (a parameter, a `let`) binds the name there. Cases from PR #2308 by
* @danusha2345.
*/
import { afterAll, beforeAll, describe, expect, it } 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-use-call-'));
const files: Record<string, string> = {
'Cargo.toml': '[package]\nname = "app"\nversion = "0.1.0"\nedition = "2021"\n\n[dependencies]\nexternal = "1"\n',
'src/lib.rs': 'pub mod a;\npub mod util;\npub mod caller;\npub mod child;\npub mod method_only;\npub fn root_take() {}\npub enum Root { RootGood(i32) }\n',
'src/a.rs': 'pub fn take() {}\npub fn consume() {}\npub fn selected() {}\npub enum Noise { take }\n',
'src/util.rs': `pub struct Buf;
impl Buf { pub fn take(&self) {} }
pub fn outer() {
fn take() {}
take();
}
pub fn take() {}
`,
'src/method_only.rs': 'pub struct Buf;\nimpl Buf { pub fn take(&self) {} }\n',
'src/caller.rs': `use crate::util::take;
pub fn imported() { take(); }
pub fn shadowed() {
fn take() {}
take();
}
pub fn parameter(take: fn()) { take(); }
pub fn destructured((take, _): (fn(), i32)) { take(); }
pub fn type_label(callback: fn(take: i32)) { take(); }
pub fn generic<T: Fn(i32)>(callback: T) { take(); }
pub fn after_binding() {
take();
let take = || {};
take();
}
`,
'src/alias.rs': `use crate::util::take as consume;
pub fn aliased() { consume(); }
`,
'src/group.rs': `use crate::{util::{take as selected}, a::{take as other}};
pub fn grouped() { selected(); }
pub fn second() { other(); }
`,
'src/child/mod.rs': `use super::util::take;
use self::util::take as local_take;
pub fn parent() { take(); }
pub fn child() { local_take(); }
`,
'src/child/util.rs': 'pub fn take() {}\n',
'src/scoped.rs': `pub fn first() {
use crate::util::take;
take();
}
pub fn second() {
use crate::a::take;
take();
}
`,
'src/outer_shadow.rs': `fn take() {}
pub fn imported_in_block() {
use crate::util::take;
take();
}
pub fn inner_import(take: fn()) {
{
use crate::util::take;
take();
}
}
pub fn before_binding() {
use crate::util::take;
take();
let take = || {};
}
`,
'src/tests_mod.rs': 'pub fn tested_helper() {}\n#[cfg(test)]\nmod tests {\n use super::tested_helper;\n fn uses() { tested_helper(); }\n}\n',
'src/external.rs': 'use external::take as consume;\npub fn external_call() { consume(); }\n',
'src/models.rs': 'pub enum E { Good(i32) }\n',
'src/model_decoy.rs': 'pub enum E { Real(i32) }\nmod hidden { pub enum E { Good(i32) } }\n',
'src/model_decoy_call.rs': 'use crate::model_decoy::E::Good;\npub fn model_decoy() { Good(1); }\n',
'src/variant.rs': 'use crate::models::E::Good;\npub fn variant() { Good(1); }\n',
'src/variant_alias.rs': 'use crate::models::E::Good as Build;\npub fn alias_variant() { Build(1); }\n',
'src/root_call.rs': 'use crate::root_take as consume;\npub fn root_call() { consume(); }\n',
'src/root_variant.rs': 'use crate::Root::RootGood;\npub fn root_variant() { RootGood(1); }\n',
'src/same_file.rs': 'fn same_take() {}\nuse self::same_take as consume;\npub fn same_file() { consume(); }\n',
'src/inline_reexport.rs': '#[cfg(unix)]\nmod imp { pub fn lone() {} }\npub use imp::*;\nuse self::lone as aliased;\npub fn via_alias() { aliased(); }\npub fn via_path() { crate::inline_reexport::lone(); }\n',
'src/duplicate.rs': '#[cfg(unix)]\npub fn take() {}\n#[cfg(windows)]\npub fn take() {}\n',
'src/duplicate_call.rs': 'use crate::duplicate::take;\npub fn duplicate_call() { take(); }\n',
'tests/bad_inline.rs': 'mod hidden { mod support { pub mod panic; } }\nuse support::panic::test_panic;\npub fn bad_inline() { test_panic(); }\n',
'tests/support/panic.rs': 'pub fn test_panic() {}\n',
'src/sync.rs': 'use crate::a::take;\npub fn synced() { take(); }\n',
'src/unicode.rs': 'use crate::util::take;\npub fn unicode() { let crab = "🦀Я"; take(); }\n',
'src/unicode_scoped.rs': 'pub fn unicode_scoped() { let crab = "🦀Я"; { use crate::util::take as consume; consume(); } }\n',
'crates/one/Cargo.toml': '[package]\nname = "one"\nversion = "0.1.0"\n',
'crates/one/src/lib.rs': 'pub mod util;\n',
'crates/one/src/util.rs': 'pub fn take() {}\npub fn only_in_one() {}\n',
'crates/two/Cargo.toml': '[package]\nname = "two"\nversion = "0.1.0"\n',
'crates/two/src/lib.rs': 'pub mod util;\npub mod caller;\n',
'crates/two/src/util.rs': 'pub fn take() {}\n',
'crates/two/src/caller.rs': 'use crate::util::take;\nuse one::util::only_in_one;\npub fn workspace_call() { take(); }\npub fn cross_crate() { only_in_one(); }\n',
};
for (const [relative, content] of Object.entries(files)) {
fs.mkdirSync(path.dirname(path.join(root, relative)), { recursive: true });
fs.writeFileSync(path.join(root, relative), content);
}
cg = await CodeGraph.init(root, { index: true });
});
afterAll(() => {
cg?.close();
if (root) fs.rmSync(root, { recursive: true, force: true });
});
const calls = (file: string, name: string) => {
const caller = cg.getNodesInFile(file).find((n) => n.name === name && n.kind === 'function');
expect(caller).toBeDefined();
return cg.getOutgoingEdgesFrom([caller!.id]).filter((e) => e.kind === 'calls')
.map((e) => cg.getNode(e.target)!)
.map((n) => `${n.kind} ${n.qualifiedName} ${n.filePath}:${n.startLine}`);
};
const take = 'function take src/util.rs:7';
describe('Rust bare calls bound by use', () => {
it('chooses the imported free function among same-name methods and functions', () => {
expect(calls('src/caller.rs', 'imported')).toEqual([take]);
});
it('follows an alias even when no declaration carries its local name', () => {
expect(calls('src/alias.rs', 'aliased')).toEqual([take]);
expect(calls('src/group.rs', 'grouped')).toEqual([take]);
expect(calls('src/group.rs', 'second')).toEqual(['function take src/a.rs:1']);
});
it('anchors self and super imports on the current module', () => {
expect(calls('src/child/mod.rs', 'parent')).toEqual([take]);
expect(calls('src/child/mod.rs', 'child')).toEqual(['function take src/child/util.rs:1']);
});
it('keeps block-local imports in their own scopes', () => {
expect(calls('src/scoped.rs', 'first')).toEqual([take]);
expect(calls('src/scoped.rs', 'second')).toEqual(['function take src/a.rs:1']);
});
it('does not replace a local function or parameter with an imported function', () => {
expect(calls('src/caller.rs', 'shadowed')).toEqual(['function shadowed::take src/caller.rs:4']);
expect(calls('src/caller.rs', 'parameter')).toEqual([]);
expect(calls('src/caller.rs', 'destructured')).toEqual([]);
expect(calls('src/caller.rs', 'type_label')).toEqual([take]);
expect(calls('src/caller.rs', 'generic')).toEqual([take]);
expect(calls('src/caller.rs', 'after_binding')).toEqual([take]);
});
it('lets a closer use shadow an outer declaration, and ignores later let bindings', () => {
expect(calls('src/outer_shadow.rs', 'imported_in_block')).toEqual([take]);
expect(calls('src/outer_shadow.rs', 'inner_import')).toEqual([take]);
expect(calls('src/outer_shadow.rs', 'before_binding')).toEqual([take]);
});
it('does not guess a target for an alias or an enum variant it cannot follow', () => {
expect(calls('src/external.rs', 'external_call')).toEqual([]);
expect(calls('src/model_decoy_call.rs', 'model_decoy')).toEqual([]);
});
it('leaves an import it cannot follow to the general resolution, as before', () => {
// Another crate of the workspace, named by its package.
expect(calls('crates/two/src/caller.rs', 'cross_crate')).toEqual(['function only_in_one crates/one/src/util.rs:2']);
// A relative path inside an inline module, the usual `mod tests { use super::…; }`.
expect(calls('src/tests_mod.rs', 'uses')).toEqual(['function tested_helper src/tests_mod.rs:1']);
expect(calls('tests/bad_inline.rs', 'bad_inline')).toEqual(['function test_panic tests/support/panic.rs:1']);
});
it('reaches an inline module\'s function through the module\'s re-export', () => {
expect(calls('src/inline_reexport.rs', 'via_alias')).toEqual(['function lone src/inline_reexport.rs:2']);
expect(calls('src/inline_reexport.rs', 'via_path')).toEqual(['function lone src/inline_reexport.rs:2']);
});
it('takes the first of two #[cfg] variants of the imported function', () => {
expect(calls('src/duplicate_call.rs', 'duplicate_call')).toEqual(['function take src/duplicate.rs:2']);
});
it('keeps a variant attached to its imported enum, including a renamed variant', () => {
expect(calls('src/variant.rs', 'variant')).toEqual(['enum_member E::Good src/models.rs:1']);
expect(calls('src/variant_alias.rs', 'alias_variant')).toEqual(['enum_member E::Good src/models.rs:1']);
expect(calls('src/root_variant.rs', 'root_variant')).toEqual(['enum_member Root::RootGood src/lib.rs:7']);
});
it('binds crate-root functions and same-file aliases', () => {
expect(calls('src/root_call.rs', 'root_call')).toEqual(['function root_take src/lib.rs:6']);
expect(calls('src/same_file.rs', 'same_file')).toEqual(['function same_take src/same_file.rs:1']);
});
it('uses UTF-16 source columns after Cyrillic and emoji on the call line', () => {
expect(calls('src/unicode.rs', 'unicode')).toEqual([take]);
expect(calls('src/unicode_scoped.rs', 'unicode_scoped')).toEqual([take]);
});
it('resolves crate relative to the caller rather than another package', () => {
expect(calls('crates/two/src/caller.rs', 'workspace_call'))
.toEqual(['function take crates/two/src/util.rs:1']);
});
it('refreshes the exact binding after a source edit and sync', async () => {
expect(calls('src/sync.rs', 'synced')).toEqual(['function take src/a.rs:1']);
fs.writeFileSync(path.join(root, 'src/sync.rs'), 'use crate::util::take;\npub fn synced() { take(); }\n');
await cg.sync();
expect(calls('src/sync.rs', 'synced')).toEqual([take]);
});
});