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 node, unresolved reason) for every call edge leaving `source`. fn calls<'a>(model: &'a Building, source: &Node) -> Vec<(&'a Node, Option)> { 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 reason = e .attributes .get("source.unresolved-reason") .and_then(|v| v.as_str()) .map(str::to_owned); (target, reason) }) .collect() } fn single_call<'a>(model: &'a Building, source: &Node) -> (&'a Node, Option) { let mut found = calls(model, source); assert_eq!(found.len(), 1, "calls from {}", source.name); found.remove(0) } #[test] fn header_prototype_resolves_to_the_paired_definition_across_files() { let model = scan_files(&[ ("src/util.h", "int helper(int x);\n"), ( "src/util.cpp", "#include \"util.h\"\nint helper(int x) { return x; }\n", ), ( "src/main.cpp", "#include \"util.h\"\nint main() { return helper(1); }\n", ), // A second program's same-named definition never includes util.h. ("tools/dup.cpp", "int helper(int x) { return x + 1; }\n"), ]); let (target, reason) = single_call(&model, function(&model, "src/main.cpp", "main")); assert_eq!(target.path.as_deref(), Some("src/util.cpp")); assert_eq!(reason, None); let dup = function(&model, "tools/dup.cpp", "helper"); assert!( !model .edges .iter() .any(|e| e.kind == EdgeKind::Calls && e.target == dup.id) ); } #[test] fn undeclared_and_internal_definitions_in_other_files_stay_unresolved() { let model = scan_files(&[ ( "src/util.cpp", "static int twice(int x) { return 2 * x; }\nint helper(int x) { return twice(x); }\n", ), ( "src/other.cpp", "static int twice(int x) { return x + x; }\nint local() { return twice(1); }\nint caller() { return helper(1); }\n", ), ]); let (local, _) = single_call(&model, function(&model, "src/other.cpp", "local")); assert_eq!(local.path.as_deref(), Some("src/other.cpp")); assert_eq!( local .attributes .get("source.linkage") .and_then(|v| v.as_str()), Some("internal") ); let (helper, _) = single_call(&model, function(&model, "src/util.cpp", "helper")); assert_eq!(helper.path.as_deref(), Some("src/util.cpp")); // No prototype for helper is visible from other.cpp. let (target, reason) = single_call(&model, function(&model, "src/other.cpp", "caller")); assert_eq!(target.kind, NodeKind::External); assert_eq!(reason.as_deref(), Some("not-visible")); } #[test] fn out_of_line_methods_use_leaf_names_and_resolve_implicit_and_explicit_this() { let model = scan_files(&[ ( "include/geo/shape.h", "namespace geo {\nclass Shape {\npublic:\n double area() const;\n int& counter();\nprivate:\n double scale(double x) const;\n};\n}\n", ), ( "src/shape.cpp", "#include \"geo/shape.h\"\nnamespace geo {\ndouble Shape::area() const {\n return this->scale(2.0)\n + scale(1.0);\n}\ndouble Shape::scale(double x) const { return x; }\nint& Shape::counter() { static int n = 0; return n; }\n}\n", ), ]); let area = function(&model, "src/shape.cpp", "area"); assert_eq!( area.attributes .get("source.qualifier") .and_then(|v| v.as_str()), Some("Shape") ); assert_eq!( area.attributes .get("source.qualified-name") .and_then(|v| v.as_str()), Some("geo::Shape::area") ); // Both call lines merge into one edge with two pieces of provenance. let (scale, reason) = single_call(&model, area); assert_eq!(scale.name, "scale"); assert_eq!(scale.path.as_deref(), Some("src/shape.cpp")); assert_eq!(reason, None); let edge = model .edges .iter() .find(|e| e.source == area.id && e.target == scale.id) .unwrap(); assert_eq!(edge.evidence.len(), 2); // A reference return type no longer leaks into the declaration name. function(&model, "src/shape.cpp", "counter"); } #[test] fn macro_ambiguous_external_and_untyped_receivers_are_classified_not_guessed() { let model = scan_files(&[ ("src/log.h", "#define LOG_TWICE(x) ((x) * 2)\n"), ( "src/main.cpp", "#include \"log.h\"\n#include \nstruct Box { int size() { return 1; } };\nBox make();\nvoid emit(int) {}\nvoid emit(double) {}\nint main() {\n std::string text;\n emit(1);\n text.size();\n make().size();\n return static_cast(LOG_TWICE(2));\n}\n", ), ]); let main = function(&model, "src/main.cpp", "main"); let mut reasons: Vec<_> = calls(&model, main) .into_iter() .map(|(target, reason)| { assert_eq!(target.kind, NodeKind::External); (target.name.clone(), reason.unwrap()) }) .collect(); reasons.sort(); assert_eq!( reasons, [ ("LOG_TWICE".to_owned(), "macro".to_owned()), ("emit".to_owned(), "ambiguous".to_owned()), ("make".to_owned(), "declared-only".to_owned()), ("size".to_owned(), "receiver-type-external".to_owned()), ("size".to_owned(), "receiver-type-unknown".to_owned()), ] ); } #[test] fn parenthesized_local_object_initialization_resolves_member_calls() { let model = scan_files(&[ ( "include/daemon.h", "namespace yams::daemon { struct YamsDaemon { void runLoop(); }; }\n", ), ( "src/daemon.cpp", "#include \"daemon.h\"\nnamespace yams::daemon { void YamsDaemon::runLoop() {} }\n", ), ( "src/main.cpp", "#include \"daemon.h\"\nint main() {\n int config = 0;\n yams::daemon::YamsDaemon daemon(config);\n daemon.runLoop();\n}\n", ), ]); let main = function(&model, "src/main.cpp", "main"); let (target, reason) = single_call(&model, main); assert_eq!(target.id, function(&model, "src/daemon.cpp", "runLoop").id); assert_eq!(reason, None); let edge = model .edges .iter() .find(|e| e.kind == EdgeKind::Calls && e.source == main.id) .unwrap(); assert_eq!( edge.attributes["source.resolution"], "declared-receiver-type" ); assert_eq!(edge.evidence[0].lane, brick::model::EvidenceLane::Heuristic); assert_eq!( edge.evidence[0].confidence, Some(brick::model::Confidence::Score(0.65)) ); assert_eq!(edge.evidence[0].provenance, "src/main.cpp:5"); } #[test] fn inner_value_can_hide_a_type_alias_for_initializer_recovery() { let model = scan_files(&[( "src/main.cpp", "struct YamsDaemon { void runLoop() {} };\nvoid use() { using config = int; { int config = 0; YamsDaemon daemon(config); daemon.runLoop(); } }\n", )]); let (target, reason) = single_call(&model, function(&model, "src/main.cpp", "use")); assert_eq!(target.id, function(&model, "src/main.cpp", "runLoop").id); assert_eq!(reason, None); } #[test] fn local_prototypes_and_out_of_scope_values_do_not_supply_receiver_types() { for body in [ "struct Config {}; YamsDaemon daemon(Config); daemon.runLoop();", "YamsDaemon daemon(); daemon.runLoop();", "int config = 0; YamsDaemon daemon(config); { YamsDaemon daemon(); daemon.runLoop(); }", "int config = 0; YamsDaemon daemon(int config); daemon.runLoop();", "{ int config = 0; } YamsDaemon daemon(config); daemon.runLoop();", "{ int config = 0; YamsDaemon daemon(config); } daemon.runLoop();", "auto hook = [] { int config = 0; }; YamsDaemon daemon(config); daemon.runLoop();", "void hook(int config); YamsDaemon daemon(config); daemon.runLoop();", "int config = 0; { using config = int; YamsDaemon daemon(config); daemon.runLoop(); }", "int config = 0; { typedef int config; YamsDaemon daemon(config); daemon.runLoop(); }", "int config = 0; { typedef int Other, config; YamsDaemon daemon(config); daemon.runLoop(); }", "int items[1]; for (int config : items) {} YamsDaemon daemon(config); daemon.runLoop();", ] { let source = format!("struct YamsDaemon {{ void runLoop() {{}} }};\nvoid use() {{ {body} }}\n"); let model = scan_files(&[("src/main.cpp", &source)]); let use_fn = function(&model, "src/main.cpp", "use"); let (target, reason) = single_call(&model, use_fn); assert_eq!(target.kind, NodeKind::External, "{body}"); assert_eq!(reason.as_deref(), Some("receiver-type-unknown"), "{body}"); } } #[test] fn declared_receivers_fields_and_bases_resolve_members_with_a_label() { let model = scan_files(&[ ( "src/shape.h", "#include \nstruct Base { int id() const; };\nstruct Shape : Base {\n int area() const;\n int total();\n std::unique_ptr next_;\n Shape* raw_;\n};\n", ), ( "src/shape.cpp", "#include \"shape.h\"\nint Base::id() const { return 0; }\nint Shape::area() const { return id(); }\nint Shape::total() {\n return next_->area()\n + this->raw_->area();\n}\n", ), ( "src/main.cpp", "#include \"shape.h\"\nint use(const Shape& ref, Shape* ptr) {\n auto owned = std::make_unique();\n return ref.area()\n + ptr->id()\n + owned->area();\n}\n", ), ]); let targets = |source: &Node| { let mut found: Vec<_> = calls(&model, source) .into_iter() .filter(|(target, _)| !target.name.starts_with("std::")) .map(|(target, reason)| { assert_eq!(reason, None, "{} -> {}", source.name, target.name); (target.path.clone().unwrap(), target.name.clone()) }) .collect(); found.sort(); found.dedup(); found }; let area = ("src/shape.cpp".to_owned(), "area".to_owned()); let id = ("src/shape.cpp".to_owned(), "id".to_owned()); assert_eq!( targets(function(&model, "src/main.cpp", "use")), [area.clone(), id.clone()] ); assert_eq!( targets(function(&model, "src/shape.cpp", "total")), std::slice::from_ref(&area) ); // Unqualified lookup inside a class scope reaches in-repository bases. assert_eq!(targets(function(&model, "src/shape.cpp", "area")), [id]); let use_fn = function(&model, "src/main.cpp", "use"); assert!( model .edges .iter() .filter(|e| e.kind == EdgeKind::Calls && e.source == use_fn.id) .filter(|e| model .nodes .iter() .any(|n| n.id == e.target && n.kind == NodeKind::Function)) .all(|e| e .attributes .get("source.resolution") .and_then(|v| v.as_str()) == Some("declared-receiver-type")) ); } #[test] fn conversion_operators_are_declarations_matching_their_prototypes() { let model = scan_files(&[( "src/flag.cpp", "struct Flag {\n explicit operator bool() const;\n};\nFlag::operator bool() const { return true; }\n", )]); let operator = function(&model, "src/flag.cpp", "operator bool"); assert_eq!( operator .attributes .get("source.qualified-name") .and_then(|v| v.as_str()), Some("Flag::operator bool") ); } #[test] fn resolution_metrics_count_call_sites_by_outcome() { let model = scan_files(&[( "src/main.cpp", "struct Box { int get() { return 1; } };\nBox make();\nint one() { return 1; }\nint main() {\n return one()\n + one()\n + make().get();\n}\n", )]); let cpp = &model.metrics.resolution["cpp"]; assert_eq!(cpp.call_site_count, 4); assert_eq!(cpp.resolved_call_site_count, 2); assert_eq!( cpp.unresolved_call_sites_by_reason["receiver-type-unknown"], 1 ); assert_eq!(cpp.unresolved_call_sites_by_reason["declared-only"], 1); assert_eq!(cpp.function_count, 3); // main has no callers and Box::get is only reachable through an untyped receiver. assert_eq!(cpp.functions_without_resolved_callers, 2); } #[test] fn using_directives_and_declarations_extend_lookup_only_where_visible() { let model = scan_files(&[ ( "include/lib/tune.h", "namespace lib::daemon {\nstruct Tune { static int ticks(); };\nint helper();\n}\n", ), ( "src/tune.cpp", "#include \"lib/tune.h\"\nnamespace lib::daemon {\nint Tune::ticks() { return 1; }\nint helper() { return 2; }\n}\n", ), ( "tests/tune_test.cpp", "#include \"lib/tune.h\"\nusing namespace lib::daemon;\nint check() {\n Tune tune;\n return Tune::ticks()\n + tune.ticks();\n}\n", ), ( "tests/alias_test.cpp", "#include \"lib/tune.h\"\nusing lib::daemon::helper;\nint alias() { return helper(); }\n", ), // Clean negative: the directive lives in a file this caller never includes. ("tests/other_using.cpp", "using namespace lib::daemon;\n"), ( "tests/negative_test.cpp", "#include \"lib/tune.h\"\nint negative() { return helper(); }\n", ), ]); let (ticks, reason) = single_call(&model, function(&model, "tests/tune_test.cpp", "check")); assert_eq!(ticks.path.as_deref(), Some("src/tune.cpp")); assert_eq!(ticks.name, "ticks"); assert_eq!(reason, None); let (helper, reason) = single_call(&model, function(&model, "tests/alias_test.cpp", "alias")); assert_eq!(helper.path.as_deref(), Some("src/tune.cpp")); assert_eq!(reason, None); let (target, reason) = single_call( &model, function(&model, "tests/negative_test.cpp", "negative"), ); assert_eq!(target.kind, NodeKind::External); assert_eq!(reason.as_deref(), Some("not-visible")); } #[test] fn brace_default_arguments_do_not_hide_prototypes_or_report_errors() { let model = scan_files(&[ ( "src/api.h", "struct Config {};\nint store(const Config& config = {});\n", ), ( "src/api.cpp", "#include \"api.h\"\nint store(const Config& config) { return 0; }\n", ), ( "src/main.cpp", "#include \"api.h\"\nint main() { return store(); }\n", ), ]); let (target, reason) = single_call(&model, function(&model, "src/main.cpp", "main")); assert_eq!(target.path.as_deref(), Some("src/api.cpp")); assert_eq!(reason, None); assert!(model.diagnostics.is_empty(), "{:?}", model.diagnostics); } #[test] fn two_using_directives_naming_the_same_function_are_ambiguous() { // With `using namespace a; using namespace b;` and both `a::run` and `b::run` // defined, the call resolved to whichever directive came first. C++ makes that // ambiguous (or an overload choice Brick cannot make), so it must stay external. let model = scan_files(&[ ("src/a.h", "namespace a { int run(int x); }\n"), ("src/b.h", "namespace b { int run(int x); }\n"), ( "src/defs.cpp", "#include \"a.h\"\n#include \"b.h\"\nnamespace a { int run(int x) { return x; } }\nnamespace b { int run(int x) { return -x; } }\n", ), ( "src/main.cpp", "#include \"a.h\"\n#include \"b.h\"\nusing namespace a;\nusing namespace b;\nint main() { return run(1); }\n", ), // Clean negative: one directive still resolves with its label. ( "src/only_a.cpp", "#include \"a.h\"\nusing namespace a;\nint only() { return run(1); }\n", ), ]); let (target, reason) = single_call(&model, function(&model, "src/main.cpp", "main")); assert_eq!(target.kind, NodeKind::External); assert_eq!(reason.as_deref(), Some("ambiguous")); let (target, reason) = single_call(&model, function(&model, "src/only_a.cpp", "only")); assert_eq!(target.path.as_deref(), Some("src/defs.cpp")); assert_eq!(reason, None); } #[test] fn preprocessor_alternative_branches_are_not_call_targets() { // An `#else` fallback (yams' spdlog stubs in InternalEventBus.h) was indexed as // live, so every spdlog::warn call site in the repository resolved to the stub and // the stubs became false hubs. Brick cannot know which branch is active, so only // the first branch may be a resolution target. let model = scan_files(&[ ( "include/log.h", "#if __has_include()\n#include \n#elif defined(ALT)\nnamespace spdlog { inline void info(const char*) {} }\n#else\nnamespace spdlog { inline void warn(const char*) {} }\n#endif\n#ifndef LOG_H\n#define LOG_H\nnamespace app { inline int level() { return 1; } }\n#endif\n", ), ( "src/main.cpp", "#include \"log.h\"\nint main() { spdlog::warn(\"x\"); spdlog::info(\"y\"); return app::level(); }\n", ), ]); let found = calls(&model, function(&model, "src/main.cpp", "main")); let mut by_name: Vec<(String, NodeKind, Option)> = found .iter() .map(|(node, reason)| (node.name.clone(), node.kind, reason.clone())) .collect(); by_name.sort_by(|a, b| a.0.cmp(&b.0)); // First-branch (include-guard) code still resolves; the alternatives do not. assert!( by_name .iter() .any(|(name, kind, _)| name == "level" && *kind == NodeKind::Function) ); for (name, kind, _) in &by_name { if name.ends_with("warn") || name.ends_with("info") { assert_eq!( *kind, NodeKind::External, "{name} resolved into an #else/#elif branch" ); } } let stub = model .nodes .iter() .find(|n| n.kind == NodeKind::Function && n.name == "warn") .expect("alternative branches stay visible as structure"); assert_eq!( stub.attributes .get("source.preprocessor-branch") .and_then(|v| v.as_str()), Some("alternative") ); } #[test] fn c_sources_parse_with_the_c_grammar() { // `.c` files went through the C++ grammar, so valid C using C++ keywords as // identifiers produced grammar-error diagnostics and damaged extraction. let model = scan_files(&[ ("src/util.h", "int twice(int new);\n"), ( "src/util.c", "#include \"util.h\"\nint twice(int new) { int delete = new; return delete * 2; }\n", ), ( "src/main.c", "#include \"util.h\"\nint main(void) { int class = 1; return twice(class); }\n", ), ]); let grammar_errors: Vec<_> = model .diagnostics .iter() .filter(|d| d.path.as_deref().is_some_and(|p| p.ends_with(".c"))) .map(|d| d.message.clone()) .collect(); assert!(grammar_errors.is_empty(), "{grammar_errors:?}"); let (target, reason) = single_call(&model, function(&model, "src/main.c", "main")); assert_eq!(target.path.as_deref(), Some("src/util.c")); assert_eq!(reason, None); }