brick/tests/cpp_xtu.rs
trvon 40bea78f9a
Some checks failed
build / build-linux (push) Has been cancelled
build / build-macos (push) Has been cancelled
build / release (push) Has been cancelled
fix(cpp): recover receivers from local initialization
Recover parenthesized object declarations only when their apparent parameters resolve to earlier visible value bindings. Respect recovered scope, alias hiding and prototype shadowing instead of manufacturing receiver types.

Preserve heuristic confidence and source provenance. Add positive and negative regressions plus a three-repeat labeled initialization fixture to the quality gate.

Validation: cargo fmt --check and ./scripts/quality-gate.sh passed.
2026-10-11 18:19:08 -06:00

509 lines
20 KiB
Rust

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<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();
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<String>) {
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 <string>\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<int>(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 <memory>\nstruct Base { int id() const; };\nstruct Shape : Base {\n int area() const;\n int total();\n std::unique_ptr<Shape> 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<Shape>();\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(<spdlog/spdlog.h>)\n#include <spdlog/spdlog.h>\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<String>)> = 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);
}