diff --git a/tests/benchmarks/resolution/fixtures/rust/expected-edges.json b/tests/benchmarks/resolution/fixtures/rust/expected-edges.json index 00acd907f..85c07ce4e 100644 --- a/tests/benchmarks/resolution/fixtures/rust/expected-edges.json +++ b/tests/benchmarks/resolution/fixtures/rust/expected-edges.json @@ -100,6 +100,76 @@ "kind": "calls", "mode": "same-file", "notes": "Same-file associated function call (UserService::new)" + }, + { + "source": { "name": "main", "file": "main.rs" }, + "target": { "name": "build_service", "file": "service.rs" }, + "kind": "calls", + "mode": "module-function", + "notes": "Cross-module function call via use crate::service::build_service" + }, + { + "source": { "name": "main", "file": "main.rs" }, + "target": { "name": "UserService.add_user", "file": "service.rs" }, + "kind": "calls", + "mode": "receiver-typed", + "notes": "service.add_user() — service is typed as UserService (returned by build_service)" + }, + { + "source": { "name": "main", "file": "main.rs" }, + "target": { "name": "UserService.get_user", "file": "service.rs" }, + "kind": "calls", + "mode": "receiver-typed", + "notes": "service.get_user() — service is typed as UserService (returned by build_service)" + }, + { + "source": { "name": "main", "file": "main.rs" }, + "target": { "name": "User.display_name", "file": "models.rs" }, + "kind": "calls", + "mode": "receiver-typed", + "notes": "user.display_name() — user is typed as User (returned by UserService::get_user)" + }, + { + "source": { "name": "main", "file": "main.rs" }, + "target": { "name": "UserService.remove_user", "file": "service.rs" }, + "kind": "calls", + "mode": "receiver-typed", + "notes": "service.remove_user() — service is typed as UserService (returned by build_service)" + }, + { + "source": { "name": "main", "file": "main.rs" }, + "target": { "name": "direct_repo_access", "file": "main.rs" }, + "kind": "calls", + "mode": "same-file", + "notes": "Same-file private function call" + }, + { + "source": { "name": "direct_repo_access", "file": "main.rs" }, + "target": { "name": "UserRepository.new", "file": "repository.rs" }, + "kind": "calls", + "mode": "constructor", + "notes": "UserRepository::new() — cross-module associated function call via use crate::repository::UserRepository" + }, + { + "source": { "name": "direct_repo_access", "file": "main.rs" }, + "target": { "name": "create_user", "file": "models.rs" }, + "kind": "calls", + "mode": "module-function", + "notes": "Cross-module function call via use crate::models::create_user" + }, + { + "source": { "name": "direct_repo_access", "file": "main.rs" }, + "target": { "name": "validate_all", "file": "validator.rs" }, + "kind": "calls", + "mode": "module-function", + "notes": "Cross-module function call via use crate::validator::validate_all" + }, + { + "source": { "name": "direct_repo_access", "file": "main.rs" }, + "target": { "name": "UserRepository.save", "file": "repository.rs" }, + "kind": "calls", + "mode": "receiver-typed", + "notes": "repo.save() — repo is typed as UserRepository" } ] } diff --git a/tests/benchmarks/resolution/fixtures/rust/main.rs b/tests/benchmarks/resolution/fixtures/rust/main.rs new file mode 100644 index 000000000..3bfe7fdd7 --- /dev/null +++ b/tests/benchmarks/resolution/fixtures/rust/main.rs @@ -0,0 +1,35 @@ +mod models; +mod repository; +mod service; +mod validator; + +use crate::models::{create_user, Repository}; +use crate::repository::UserRepository; +use crate::service::build_service; +use crate::validator::validate_all; + +fn main() { + let service = build_service(); + + match service.add_user(1, "Alice", "alice@example.com") { + Ok(()) => println!("Added user 1"), + Err(e) => println!("Failed to add user: {}", e), + } + + if let Some(user) = service.get_user(1) { + println!("Found: {}", user.display_name()); + } + + let removed = service.remove_user(1); + println!("Removed: {}", removed); + + direct_repo_access(); +} + +fn direct_repo_access() { + let repo = UserRepository::new(); + let user = create_user(2, "Bob", "bob@example.com"); + if validate_all(&user).is_ok() { + let _ = repo.save(&user); + } +} diff --git a/tests/benchmarks/resolution/fixtures/rust/models.rs b/tests/benchmarks/resolution/fixtures/rust/models.rs index 9bd189a6f..a2b81d453 100644 --- a/tests/benchmarks/resolution/fixtures/rust/models.rs +++ b/tests/benchmarks/resolution/fixtures/rust/models.rs @@ -1,3 +1,4 @@ +#[derive(Clone)] pub struct User { pub id: u64, pub name: String, diff --git a/tests/benchmarks/resolution/fixtures/rust/service.rs b/tests/benchmarks/resolution/fixtures/rust/service.rs index 53942d58a..8c82be6e2 100644 --- a/tests/benchmarks/resolution/fixtures/rust/service.rs +++ b/tests/benchmarks/resolution/fixtures/rust/service.rs @@ -1,4 +1,4 @@ -use crate::models::{create_user, User}; +use crate::models::{create_user, Repository, User}; use crate::repository::{create_repository, UserRepository}; use crate::validator::validate_all; diff --git a/tests/benchmarks/resolution/tracer/native-tracer.sh b/tests/benchmarks/resolution/tracer/native-tracer.sh index 602b5268e..e83d3276d 100644 --- a/tests/benchmarks/resolution/tracer/native-tracer.sh +++ b/tests/benchmarks/resolution/tracer/native-tracer.sh @@ -385,13 +385,18 @@ thread_local! { pub fn trace_call(name: &str, file: &str) -> TraceGuard { TRACER.with(|t| { let mut t = t.borrow_mut(); - if let Some(caller) = t.stack.last() { - let key = format!("{}@{}->{}@{}", caller.name, caller.file, name, file); + // Clone the caller's name/file out (rather than holding the &Frame + // borrow from t.stack.last() across the mutations below) so the + // subsequent t.seen / t.edges mutable accesses don't overlap with an + // outstanding immutable borrow of t. + let caller = t.stack.last().map(|f| (f.name.clone(), f.file.clone())); + if let Some((caller_name, caller_file)) = caller { + let key = format!("{}@{}->{}@{}", caller_name, caller_file, name, file); if !t.seen.contains(&key) { t.seen.insert(key); t.edges.push(Edge { - source_name: caller.name.clone(), - source_file: caller.file.clone(), + source_name: caller_name, + source_file: caller_file, target_name: name.to_string(), target_file: file.to_string(), }); @@ -432,18 +437,26 @@ RSTRACE # Inject trace_call into every fn body, tracking impl blocks for qualnames. # Rust's Drop-guard RAII pattern means only entry needs injecting — the # guard's Drop impl fires trace_support's exit hook automatically. + # The context regex's leading optional group absorbs "Trait for " so group + # 2 always lands on the concrete type — plain `impl Type {` and trait impls + # `impl Trait for Type {` both must qualname as the type, not the trait + # (methods are defined per-type, and expected-edges.json's trait-dispatch + # entries name the implementing type, e.g. EmailValidator.validate). inject_trace_calls \ "$TMP_DIR/src/*.rs" \ "trace_support.rs" \ - '^impl[[:space:]]+([A-Za-z_][A-Za-z0-9_]*)' 1 \ + '^impl[[:space:]]+([A-Za-z_][A-Za-z0-9_]*[[:space:]]+for[[:space:]]+)?([A-Za-z_][A-Za-z0-9_]*)' 2 \ 'fn[[:space:]]+([a-z_][a-z0-9_]*)' 1 \ '' \ raii \ ' let _tg = crate::trace_support::trace_call("%s", "%s");' # Inject dump_trace() at end of main() + # The i\text form (rather than a GNU-only inline i\text) is required + # for BSD sed (macOS) to accept this — see #1759. sedi '/^fn main/,/^\}/ { - /^\}/ i\ crate::trace_support::dump_trace(); + /^\}/ i\ + crate::trace_support::dump_trace(); }' "$TMP_DIR/src/main.rs" # Redirect eprintln/println in fixture code to stderr to keep stdout clean for JSON