use brick::model::{Building, EdgeKind, Node, NodeKind}; use brick::scan; use std::fs; fn scan_files(files: &[(&str, &str)]) -> Building { let root = tempfile::tempdir().unwrap(); for (path, content) in files { let file = root.path().join(path); fs::create_dir_all(file.parent().unwrap()).unwrap(); fs::write(file, content).unwrap(); } scan::scan(root.path()).unwrap() } fn function<'a>(model: &'a Building, path: &str, name: &str) -> &'a Node { model .nodes .iter() .find(|n| n.kind == NodeKind::Function && n.path.as_deref() == Some(path) && n.name == name) .unwrap_or_else(|| panic!("missing {path}::{name}")) } /// (target parent name, target name, attribute value) for calls leaving `source`. fn calls(model: &Building, source: &Node, attribute: &str) -> Vec<(String, String, String)> { let mut found: Vec<_> = model .edges .iter() .filter(|e| e.kind == EdgeKind::Calls && e.source == source.id) .map(|e| { let target = model.nodes.iter().find(|n| n.id == e.target).unwrap(); let parent = target .parent .as_ref() .and_then(|id| model.nodes.iter().find(|n| &n.id == id)) .map(|n| n.name.clone()) .unwrap_or_default(); let value = e .attributes .get(attribute) .and_then(|v| v.as_str()) .unwrap_or("") .to_owned(); (parent, target.name.clone(), value) }) .collect(); found.sort(); found } #[test] fn expression_macro_bodies_yield_calls_and_other_macros_stay_opaque() { let model = scan_files(&[( "src/lib.rs", "fn check() -> bool { true }\nfn name() -> u8 { 0 }\nfn hidden() {}\nmacro_rules! custom { ($e:expr) => { $e } }\nfn run() {\n assert!(check(), \"{}\", format!(\"{}\", name()));\n custom!(hidden());\n}\n", )]); let run = function(&model, "src/lib.rs", "run"); assert_eq!( calls(&model, run, "source.extraction"), [ ("lib.rs".into(), "check".into(), "macro-reparse".into()), ("lib.rs".into(), "name".into(), "macro-reparse".into()), ] ); let hidden = function(&model, "src/lib.rs", "hidden"); assert!( !model .edges .iter() .any(|e| e.target == hidden.id && e.kind == EdgeKind::Calls) ); } #[test] fn receivers_resolve_through_self_fields_constructors_and_dyn_traits() { let model = scan_files(&[( "src/lib.rs", "use std::sync::Arc;\npub struct Engine;\nimpl Engine { pub fn commit(&self) {} }\npub trait Named { fn label(&self) -> String; }\npub struct Store { inner: Arc }\nimpl Store {\n pub fn open() -> Result { Ok(Self::build()) }\n fn build() -> Self { Store { inner: Arc::new(Engine) } }\n fn flush(&self) {\n self.inner.commit();\n self.log();\n }\n fn log(&self) {}\n}\nfn run(item: &dyn Named) -> Result<(), ()> {\n let store = Store::open()?;\n store.flush();\n item.label();\n Ok(())\n}\n", )]); let resolved = |source: &Node| { calls(&model, source, "source.resolution") .into_iter() .filter(|(parent, _, _)| !parent.is_empty()) .collect::>() }; assert_eq!( resolved(function(&model, "src/lib.rs", "flush")), [ ( "Engine".into(), "commit".into(), "declared-receiver-type".into() ), ("Store".into(), "log".into(), "self-receiver".into()), ] ); assert_eq!( resolved(function(&model, "src/lib.rs", "open")), [("Store".into(), "build".into(), "impl-type".into())] ); assert_eq!( resolved(function(&model, "src/lib.rs", "run")), [ ( "Named".into(), "label".into(), "declared-receiver-type".into() ), ( "Store".into(), "flush".into(), "declared-receiver-type".into() ), // Same-file `Type::f` already resolves by qualified lookup, unlabeled. ("Store".into(), "open".into(), "".into()), ] ); } #[test] fn foreign_imports_generics_and_competing_trait_impls_never_guess() { let model = scan_files(&[ ( "src/lib.rs", "mod local;\nuse std::fs::File;\npub trait Named { fn label(&self) -> String; }\nstruct A;\nstruct B;\nstruct C;\nimpl Named for A { fn label(&self) -> String { String::new() } }\nimpl Named for C { fn label(&self) -> String { String::new() } }\ntrait Other { fn label(&self) -> String; }\nimpl Other for A { fn label(&self) -> String { String::new() } }\nfn run(generic: &T, a: A) {\n let file = File::open(\"x\").unwrap();\n file.sync_all();\n generic.label();\n a.label();\n}\n", ), ( "src/local.rs", "pub struct File;\nimpl File {\n pub fn open(_p: &str) -> Self { File }\n pub fn sync_all(&self) {}\n}\n", ), ]); let run = function(&model, "src/lib.rs", "run"); let reasons: Vec<_> = calls(&model, run, "source.unresolved-reason") .into_iter() .map(|(_, name, reason)| (name, reason)) .collect(); assert_eq!( reasons, [ ("File::open".into(), "qualifier-unknown".into()), ("label".into(), "ambiguous".into()), ("label".into(), "receiver-type-unknown".into()), ("sync_all".into(), "receiver-type-unknown".into()), ("unwrap".into(), "receiver-type-unknown".into()), ] ); for name in ["open", "sync_all"] { let local = function(&model, "src/local.rs", name); assert!( !model .edges .iter() .any(|e| e.target == local.id && e.kind == EdgeKind::Calls) ); } } #[test] fn same_named_types_in_different_modules_resolve_through_use_bindings() { // Types were keyed by leaf name, so `b::Config` (method from a trait impl) and // `a::Config` (inherent method) shared one key and the inherent one won: a call on // `b::Config` was attributed to `a::Config::load`. let model = scan_files(&[ ("src/lib.rs", "pub mod a;\npub mod b;\npub mod user;\n"), ( "src/a.rs", "pub struct Config;\nimpl Config {\n pub fn load(&self) -> u32 { 1 }\n}\n", ), ( "src/b.rs", "pub struct Config;\npub trait Loader { fn load(&self) -> u32; }\nimpl Loader for Config {\n fn load(&self) -> u32 { 2 }\n}\n", ), ( "src/user.rs", "use crate::b::Config;\nuse crate::b::Loader;\npub fn run(c: &Config) -> u32 { c.load() }\n", ), ( "src/other.rs", "pub fn guess(c: &Config) -> u32 { c.load() }\n", ), ]); let targets = |source: &Node| { model .edges .iter() .filter(|e| e.kind == EdgeKind::Calls && e.source == source.id) .map(|e| { let target = model.nodes.iter().find(|n| n.id == e.target).unwrap(); ( target.path.clone().unwrap_or_default(), e.attributes .get("source.unresolved-reason") .and_then(|v| v.as_str()) .unwrap_or("") .to_owned(), ) }) .collect::>() }; let run = function(&model, "src/user.rs", "run"); assert_eq!(targets(run), [("src/b.rs".to_owned(), String::new())]); // Without a binding, two same-named types leave the call ambiguous, not guessed. let guess = function(&model, "src/other.rs", "guess"); assert_eq!(targets(guess), [(String::new(), "ambiguous".to_owned())]); } #[test] fn an_impl_on_a_qualified_path_in_another_file_keeps_its_methods() { // With several files declaring `Foo`, an impl was kept only if its own file // resolved `Foo`; `impl crate::a::Foo` in a third file resolved nothing and its // methods were dropped, so the call read "member-not-found". let model = scan_files(&[ ( "src/lib.rs", "pub mod a;\npub mod c;\npub mod ext;\npub mod main_use;\n", ), ("src/a.rs", "pub struct Foo;\n"), ("src/c.rs", "pub struct Foo;\n"), ( "src/ext.rs", "impl crate::a::Foo {\n pub fn run(&self) {}\n}\n", ), ( "src/main_use.rs", "use crate::a::Foo;\npub fn f(x: Foo) {\n x.run();\n}\n", ), ]); let caller = function(&model, "src/main_use.rs", "f"); let targets = calls(&model, caller, "source.resolution"); assert!( targets.iter().any(|(_, name, _)| name == "run"), "{targets:?}" ); let run = function(&model, "src/ext.rs", "run"); assert!( model .edges .iter() .any(|e| e.kind == EdgeKind::Calls && e.source == caller.id && e.target == run.id) ); }