use brick::model::{EdgeKind, Node, NodeKind}; use brick::{Building, scan}; use std::fs; fn project(files: &[(&str, &str)]) -> (tempfile::TempDir, Building) { let dir = tempfile::tempdir().unwrap(); for (path, text) in files { let dest = dir.path().join(path); fs::create_dir_all(dest.parent().unwrap()).unwrap(); fs::write(dest, text).unwrap(); } let model = scan::scan(dir.path()).unwrap(); model.validate_current_schema().unwrap(); (dir, model) } fn symbol<'a>(model: &'a Building, path: &str, name: &str) -> &'a Node { let nodes: Vec<_> = model .nodes .iter() .filter(|n| { n.path.as_deref() == Some(path) && n.name == name && matches!(n.kind, NodeKind::Function | NodeKind::Class) }) .collect(); assert_eq!(nodes.len(), 1, "expected one {path}:{name}"); nodes[0] } fn calls(model: &Building, source: &Node, target: &Node) -> bool { model .edges .iter() .any(|e| e.kind == EdgeKind::Calls && e.source == source.id && e.target == target.id) } #[test] fn relative_imports_are_source_relative_and_never_guess_a_missing_path() { for (extension, import) in [ ("ts", "import { work } from"), ("js", "import { work } from"), ("dart", "import"), ] { let main = format!("a/main.{extension}"); let local = format!("a/util.{extension}"); let other = format!("b/util.{extension}"); let specifier = if extension == "dart" { "./util.dart" } else { "./util" }; let text = format!("{import} '{specifier}';\n{import} './missing/util';\n"); let (_, model) = project(&[(&main, &text), (&local, ""), (&other, "")]); let imports: Vec<_> = model .edges .iter() .filter(|e| e.kind == EdgeKind::Imports) .map(|e| model.nodes.iter().find(|n| n.id == e.target).unwrap()) .collect(); assert_eq!(imports.len(), 2); assert!(imports.iter().any(|n| n.path.as_deref() == Some(&local))); assert!( imports .iter() .any(|n| n.kind == NodeKind::External && n.name == "./missing/util") ); } let (_, model) = project(&[("a/main.ts", "import './missing/util';"), ("b/util.ts", "")]); assert!( model .edges .iter() .filter(|e| e.kind == EdgeKind::Imports) .all(|e| model .nodes .iter() .any(|n| n.id == e.target && n.kind == NodeKind::External)) ); } #[test] fn resolution_respects_language_and_lexical_owners() { let (_, model) = project(&[ ( "main.rs", "fn caller() { foreign(); local(); }\nfn local() {}", ), ("other.js", "function foreign() {}\nfunction local() {}"), ( "types.dart", "class A { void run() { helper(); } void helper() {} }\nclass B { void helper() {} }", ), ]); let caller = symbol(&model, "main.rs", "caller"); assert!(calls(&model, caller, symbol(&model, "main.rs", "local"))); assert!(!calls( &model, caller, symbol(&model, "other.js", "foreign") )); let a = symbol(&model, "types.dart", "A"); let helper = model .nodes .iter() .find(|n| n.name == "helper" && n.parent.as_deref() == Some(&a.id)) .unwrap(); assert!(calls(&model, symbol(&model, "types.dart", "run"), helper)); } #[test] fn cpp_inline_definitions_and_constructors_are_symbols_not_calls() { let (_, model) = project(&[( "main.cpp", "void helper() {}\nclass Worker {\npublic:\n Worker() { helper(); }\n void run() { helper(); }\n};\nint main() { helper(); return 0; }", )]); let class = model .nodes .iter() .find(|n| n.kind == NodeKind::Class && n.name == "Worker") .unwrap(); let constructor = model .nodes .iter() .find(|n| n.kind == NodeKind::Function && n.name == "Worker") .unwrap(); assert_eq!(constructor.parent.as_deref(), Some(class.id.as_str())); let helper = symbol(&model, "main.cpp", "helper"); for source in [ constructor, symbol(&model, "main.cpp", "run"), symbol(&model, "main.cpp", "main"), ] { assert!(calls(&model, source, helper)); } assert_eq!( model .edges .iter() .filter(|e| e.kind == EdgeKind::Calls) .count(), 3 ); } #[test] fn javascript_and_typescript_declarations_have_correct_call_owners() { for ext in ["js", "ts", "tsx"] { let path = format!("main.{ext}"); let (_, model) = project(&[( &path, "export const arrow = () => target();\nexport default function main() { target(); }\nclass Service { run() { target(); } }\nfunction target() {}", )]); let target = symbol(&model, &path, "target"); for name in ["arrow", "main", "run"] { assert!(calls(&model, symbol(&model, &path, name), target)); } assert_eq!( symbol(&model, &path, "run").parent.as_deref(), Some(symbol(&model, &path, "Service").id.as_str()) ); assert_eq!( model .edges .iter() .filter(|e| e.kind == EdgeKind::Calls) .count(), 3 ); } let (_, model) = project(&[( "main.ts", "export interface Service { run(): void; }\nexport function identity(x: T): T { return x; }\nexport const arrow = () => identity(1);", )]); symbol(&model, "main.ts", "Service"); assert!(calls( &model, symbol(&model, "main.ts", "arrow"), symbol(&model, "main.ts", "identity") )); assert_eq!( model .edges .iter() .filter(|e| e.kind == EdgeKind::Calls) .count(), 1 ); } #[test] fn dart_multiline_method_owns_its_body_calls() { let (_, model) = project(&[( "main.dart", "class Service {\n Future run(\n int value,\n ) async {\n helper();\n }\n void helper() {}\n}", )]); let run = symbol(&model, "main.dart", "run"); assert_eq!( run.parent.as_deref(), Some(symbol(&model, "main.dart", "Service").id.as_str()) ); assert!(calls(&model, run, symbol(&model, "main.dart", "helper"))); assert_eq!( model .edges .iter() .filter(|e| e.kind == EdgeKind::Calls) .count(), 1 ); } #[test] fn rust_impl_owners_and_const_functions_survive_unrelated_changes() { let (dir, before) = project(&[( "main.rs", "struct A;\nstruct B;\nimpl A { fn run(&self) {} }\nimpl B { fn run(&self) {} }\nconst fn constant() -> u32 { 1 }", )]); symbol(&before, "main.rs", "constant"); for owner in ["A", "B"] { let class = symbol(&before, "main.rs", owner); assert_eq!( before .nodes .iter() .filter(|n| n.name == "run" && n.parent.as_deref() == Some(&class.id)) .count(), 1 ); } assert!(!before.edges.iter().any(|e| e.kind == EdgeKind::Calls)); let path = dir.path().join("main.rs"); fs::write( &path, format!( "// comment\nstruct Unrelated;\n{}", fs::read_to_string(&path).unwrap() ), ) .unwrap(); let after = scan::scan(dir.path()).unwrap(); for node in before .nodes .iter() .filter(|n| matches!(n.kind, NodeKind::Class | NodeKind::Function)) { assert!(after.nodes.iter().any(|n| n.id == node.id)); } } #[test] fn block_comments_and_strings_cannot_create_symbols_or_calls() { for (ext, code) in [ ( "rs", "/* fn ghost() { phantom(); } */\nfn real() { let s = r#\"ghost();\"#; }", ), ( "cpp", "/* void ghost() { phantom(); } */\nvoid real() { auto s = R\"(ghost();)\"; }", ), ( "js", "/* function ghost() { phantom(); } */\nfunction real() { const s = `ghost();`; }", ), ( "ts", "/* function ghost() { phantom(); } */\nfunction real() { const s = 'https://example/ghost()'; }", ), ( "dart", "/* void ghost() { phantom(); } */\nvoid real() { final s = '''ghost();'''; }", ), ] { let path = format!("main.{ext}"); let (_, model) = project(&[(&path, code)]); symbol(&model, &path, "real"); assert!( !model .nodes .iter() .any(|n| ["ghost", "phantom"].contains(&n.name.as_str())), "{ext}" ); assert!( !model.edges.iter().any(|e| e.kind == EdgeKind::Calls), "{ext}" ); } } #[cfg(unix)] #[test] fn symlinks_are_skipped_with_diagnostics_including_cycles_and_source_files() { use std::os::unix::fs::symlink; let (dir, _) = project(&[("src/real.rs", "fn real() {}")]); let outside = tempfile::tempdir().unwrap(); fs::write(outside.path().join("secret.rs"), "fn secret() {}").unwrap(); symlink(outside.path(), dir.path().join("outside")).unwrap(); symlink(dir.path(), dir.path().join("cycle")).unwrap(); symlink( outside.path().join("secret.rs"), dir.path().join("secret.rs"), ) .unwrap(); symlink(dir.path().join("src"), dir.path().join("alias")).unwrap(); let model = scan::scan(dir.path()).unwrap(); assert_eq!( model .nodes .iter() .filter(|n| n.kind == NodeKind::File) .count(), 1 ); assert_eq!(model.diagnostics.len(), 4); assert!( model .diagnostics .iter() .all(|d| d.message.contains("symlink") && !d.path.as_deref().unwrap().starts_with('/')) ); } #[test] fn namespaces_do_not_cross_resolve_and_do_not_renumber_unrelated_ids() { let (dir, before) = project(&[( "main.cpp", "namespace a { void helper() {} void run() { helper(); } }\nnamespace b { void helper() {} }", )]); let run = symbol(&before, "main.cpp", "run"); let helper = before .nodes .iter() .find(|n| { n.name == "helper" && n.attributes.get("source.scope") == Some(&serde_json::json!("a")) }) .unwrap(); assert!(calls(&before, run, helper)); let path = dir.path().join("main.cpp"); fs::write( &path, format!( "namespace unrelated {{ void helper() {{}} }}\n{}", fs::read_to_string(&path).unwrap() ), ) .unwrap(); let after = scan::scan(dir.path()).unwrap(); assert!(after.nodes.iter().any(|n| n.id == helper.id)); } #[test] fn explicitly_imported_named_functions_resolve_without_global_name_guessing() { let (_, model) = project(&[ ( "app/main.ts", "import { work as execute } from './util';\nexport function run() { execute(); missing(); }", ), ("app/util.ts", "export function work() {}"), ( "unrelated/util.ts", "export function work() {}\nexport function missing() {}", ), ]); let run = symbol(&model, "app/main.ts", "run"); assert!(calls(&model, run, symbol(&model, "app/util.ts", "work"))); assert!(!calls( &model, run, symbol(&model, "unrelated/util.ts", "missing") )); } #[test] fn malformed_source_reports_errors_without_inventing_calls_from_recovery_text() { let (_, model) = project(&[( "main.ts", "function complete() {}\nfunction broken( { !!! phantom()", )]); symbol(&model, "main.ts", "complete"); assert!(!model.diagnostics.is_empty()); assert!(!model.nodes.iter().any(|n| n.name == "phantom")); } #[test] fn imports_never_resolve_to_nonexported_or_type_only_symbols() { let (_, model) = project(&[ ( "main.ts", "import { hidden } from './util';\nimport type { declared } from './util';\nfunction run() { hidden(); declared(); }", ), ( "util.ts", "function hidden() {}\nexport function declared() {}", ), ]); let run = symbol(&model, "main.ts", "run"); for name in ["hidden", "declared"] { assert!(!calls(&model, run, symbol(&model, "util.ts", name))); } } #[test] fn member_calls_stay_heuristic_and_unresolved_in_all_languages() { for (ext, code) in [ ("cpp", "void helper() {}\nvoid run() { object.helper(); }"), ( "js", "function helper() {}\nfunction run() { object.helper(); }", ), ( "ts", "function helper() {}\nfunction run() { object.helper(); }", ), ("dart", "void helper() {}\nvoid run() { object.helper(); }"), ("rs", "fn helper() {}\nfn run() { object.helper(); }"), ] { let path = format!("main.{ext}"); let (_, model) = project(&[(&path, code)]); assert!(!calls( &model, symbol(&model, &path, "run"), symbol(&model, &path, "helper") )); let edge = model .edges .iter() .find(|e| e.kind == EdgeKind::Calls) .unwrap(); assert!( edge.evidence .iter() .all(|e| e.lane == brick::model::EvidenceLane::Heuristic && e.confidence == Some(brick::model::Confidence::Score(0.65)) && e.provenance.starts_with(&path)) ); } } #[test] fn runtime_parameter_bindings_do_not_resolve_to_same_named_functions() { for (ext, code) in [ ( "js", "function helper() {}\nfunction run(helper) { helper(); }", ), ( "ts", "function helper() {}\nfunction run(helper: () => void) { helper(); }", ), ("rs", "fn helper() {}\nfn run(helper: fn()) { helper(); }"), ( "dart", "void helper() {}\nvoid run(void Function() helper) { helper(); }", ), ( "cpp", "void helper() {}\nvoid run(void (*helper)()) { helper(); }", ), ] { let path = format!("main.{ext}"); let (_, model) = project(&[(&path, code)]); assert!( !calls( &model, symbol(&model, &path, "run"), symbol(&model, &path, "helper") ), "{ext}" ); } } #[test] fn qualified_rust_module_calls_use_exact_paths_and_visibility() { let (_, model) = project(&[ ( "src/api.rs", "fn run() { crate::storage::query(); crate::other::query(); crate::storage::hidden(); }", ), ("src/storage.rs", "pub fn query() {}\nfn hidden() {}"), ("unrelated/storage.rs", "pub fn query() {}"), ]); let run = symbol(&model, "src/api.rs", "run"); assert!(calls( &model, run, symbol(&model, "src/storage.rs", "query") )); assert!(!calls( &model, run, symbol(&model, "src/storage.rs", "hidden") )); assert!(!calls( &model, run, symbol(&model, "unrelated/storage.rs", "query") )); } #[test] fn unresolved_impl_cannot_use_a_method_local_type_as_its_own_owner() { let (_, model) = project(&[("main.rs", "impl Missing { fn run() { struct Missing; } }")]); model.validate_current_schema().unwrap(); assert!( model .diagnostics .iter() .any(|d| d.message.contains("unresolved impl owner")) ); } #[test] fn cpp_parent_relative_includes_resolve_without_basename_fallback() { let (_, model) = project(&[ ( "src/main.cpp", "#include \"../include/base.h\"\n#include \"../missing/base.h\"\n", ), ("include/base.h", "struct Base {};"), ]); let imports: Vec<_> = model .edges .iter() .filter(|e| e.kind == EdgeKind::Imports) .map(|e| model.nodes.iter().find(|n| n.id == e.target).unwrap()) .collect(); assert_eq!(imports.len(), 2); assert!( imports .iter() .any(|n| n.path.as_deref() == Some("include/base.h")) ); assert!( imports .iter() .any(|n| n.kind == NodeKind::External && n.name == "../missing/base.h") ); } #[test] fn generic_type_arguments_are_not_base_classes() { // `extends Bloc` produced inherits edges to Ev and St: the base walker // descended into the type-argument list. On pipe 39 of 145 Dart inherits edges // (27%) pointed at type arguments. C++ `: public Base` shared the walker. let (_, model) = project(&[ ( "lib/bloc.dart", "class Ev {}\nclass St {}\nclass Bloc {}\nclass MyBloc extends Bloc {}\n", ), ( "src/base.cpp", "template class Base {};\nclass Arg {};\nclass Derived : public Base {};\n", ), ]); let inherits = |source: &Node| { let mut names: Vec = model .edges .iter() .filter(|e| e.kind == EdgeKind::Inherits && e.source == source.id) .map(|e| { model .nodes .iter() .find(|n| n.id == e.target) .unwrap() .name .clone() }) .collect(); names.sort(); names }; assert_eq!( inherits(symbol(&model, "lib/bloc.dart", "MyBloc")), ["Bloc"] ); assert_eq!( inherits(symbol(&model, "src/base.cpp", "Derived")), ["Base"] ); } #[test] fn python_calls_keep_their_function_and_resolve_only_what_is_visible() { let (_, model) = project(&[ ( "pkg/a.py", "def process(x):\n return x\n\ndef unrelated():\n pass\n", ), ( "pkg/b.py", "class Base:\n pass\n\ndef helper():\n pass\n", ), ( "pkg/c.py", // A blank line and a comment inside the body used to pop the function // scope, attributing the later calls to the file. "from pkg.b import Base\n\nclass Child(Base):\n def go(self):\n x = 1\n\n # comment\n helper()\n process(1)\n unrelated()\n", ), ( "pkg/d.py", "from pkg.a import process\n\ndef use():\n return process(2)\n", ), ]); let go = symbol(&model, "pkg/c.py", "go"); let outgoing = |source: &Node| { let mut found: Vec<(String, String)> = 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.name.clone(), target.path.clone().unwrap_or_default()) }) .collect(); found.sort(); found }; // helper comes from the imported pkg/b.py; process and unrelated are never // imported by c.py, so they stay external instead of resolving by unique name. assert_eq!( outgoing(go), [ ("helper".to_owned(), "pkg/b.py".to_owned()), ("process".to_owned(), String::new()), ("unrelated".to_owned(), String::new()), ] ); // An imported name still resolves across files. assert_eq!( outgoing(symbol(&model, "pkg/d.py", "use")), [("process".to_owned(), "pkg/a.py".to_owned())] ); // A class header is not a call. let file_c = model .nodes .iter() .find(|n| n.kind == NodeKind::File && n.path.as_deref() == Some("pkg/c.py")) .unwrap(); assert!( !model .edges .iter() .any(|e| e.kind == EdgeKind::Calls && e.source == file_c.id), "file-level call edges in pkg/c.py" ); } #[test] fn dart_calls_resolve_through_imported_libraries() { // Dart imports expose a library's public top-level names without a binding per // name, so no cross-file Dart call ever resolved even when its import did. let (_, model) = project(&[ ("pubspec.yaml", "name: demo\n"), ( "lib/util.dart", "int helper() => 1;\nint _secret() => 2;\nclass Tool { int use() => 3; }\n", ), ("lib/other.dart", "int shared() => 4;\n"), ("lib/twin.dart", "int shared() => 5;\n"), ( "lib/main.dart", "import 'util.dart';\nimport 'package:demo/other.dart';\nimport 'twin.dart';\nvoid run() {\n helper();\n _secret();\n shared();\n}\n", ), // Clean negative: no import, no edge. ("lib/stranger.dart", "void stray() {\n helper();\n}\n"), ]); let targets = |source: &Node| { let mut found: Vec<(String, String, String)> = 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.name.clone(), target.path.clone().unwrap_or_default(), e.attributes .get("source.unresolved-reason") .and_then(|v| v.as_str()) .unwrap_or("") .to_owned(), ) }) .collect(); found.sort(); found }; assert_eq!( targets(symbol(&model, "lib/main.dart", "run")), [ ("_secret".into(), String::new(), "not-visible".into()), ("helper".into(), "lib/util.dart".into(), String::new()), ("shared".into(), String::new(), "ambiguous".into()), ] ); assert_eq!( targets(symbol(&model, "lib/stranger.dart", "stray")), [("helper".into(), String::new(), "not-visible".into())] ); } #[test] fn a_non_utf8_byte_does_not_drop_the_whole_file() { // One Latin-1 `é` in a comment made the whole file (and its functions) vanish. let dir = tempfile::tempdir().unwrap(); let mut source = b"// caf".to_vec(); source.push(0xE9); source.extend_from_slice(b"\nfn kept() {}\nfn caller() { kept(); }\n"); fs::write(dir.path().join("latin1.rs"), source).unwrap(); fs::write(dir.path().join("blob.rs"), [0u8, 159, 146, 150]).unwrap(); let model = scan::scan(dir.path()).unwrap(); let caller = symbol(&model, "latin1.rs", "caller"); assert!(calls(&model, caller, symbol(&model, "latin1.rs", "kept"))); assert!( model .diagnostics .iter() .any(|d| d.path.as_deref() == Some("latin1.rs") && d.message.contains("non-UTF-8")) ); // A file with NUL bytes is binary, not source: still skipped with a diagnostic. assert!( !model .nodes .iter() .any(|n| n.path.as_deref() == Some("blob.rs")) ); }