brick/tests/callers.rs
trvon d292b9f3f2
feat: add brick_callers and brick_uncalled caller-status queries
Agents asking "is this function used?" get resolved, unknown, or
unreferenced per function instead of a bare zero in-degree. Unknown lists
why callers may exist: same-name unresolved call sites (excluding foreign
receivers and macros), name references (callbacks, address-taken, macro
bodies), virtual/trait-impl/external-base/outside-class members, framework
attributes, implicit protocol invocations, entry points, test code,
incomplete parses, and, unless assume_closed_world is set, exported or
external-linkage functions. Unreferenced is a review candidate with an
explicit heuristic/compiler evidence basis, never a dead-code proof.

The queries are read-only over validated schema-4 models and return
complete canonical nodes and edges plus metrics.resolution coverage.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-22 15:12:13 -06:00

219 lines
8.5 KiB
Rust

use brick::Building;
use brick::mcp::Catalog;
use brick::scan;
use serde_json::{Value, json};
use std::collections::BTreeMap;
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()
}
/// function name -> (status, reason kinds) for every function in the model.
fn statuses(model: Building, closed_world: bool) -> BTreeMap<String, (String, Vec<String>)> {
let catalog = Catalog::new(model).unwrap();
let result = catalog
.call(
"brick_uncalled",
json!({"assume_closed_world": closed_world, "include_unknown": true, "limit": 1000}),
)
.unwrap();
let mut found = BTreeMap::new();
for (status, list) in [("unreferenced", "unreferenced"), ("unknown", "unknown")] {
for report in result[list].as_array().unwrap() {
let reasons = report["reasons"]
.as_array()
.unwrap()
.iter()
.map(|r| r["kind"].as_str().unwrap().to_owned())
.collect();
found.insert(
report["node"]["name"].as_str().unwrap().to_owned(),
(status.to_owned(), reasons),
);
}
}
found
}
fn cpp_model() -> Building {
scan_files(&[
(
"src/app.h",
"#include <string>\nstruct Base { virtual void hook(); };\nstruct Widget : Base {\n void hook() override;\n int size() const;\n Widget();\n};\nint shared();\n",
),
(
"src/app.cpp",
"#include \"app.h\"\n#include <mutex>\nWidget::Widget() {}\nvoid Base::hook() {}\nvoid Widget::hook() {}\nint Widget::size() const { return 0; }\nstatic std::mutex& lockObject() { static std::mutex m; return m; }\nstatic int unusedHelper() { return 1; }\nstatic int callback(int x) { return x; }\nint shared() { return 2; }\nWidget make();\nint main() {\n std::lock_guard<std::mutex> guard(lockObject());\n std::string text;\n text.size();\n make().size();\n int (*fp)(int) = callback;\n return fp(1) + shared();\n}\n",
),
])
}
#[test]
fn cpp_statuses_separate_dead_code_from_unresolvable_uses() {
let found = statuses(cpp_model(), false);
// Only resolved callers are omitted: shared() and lockObject() (via the vexing parse).
assert!(!found.contains_key("shared"));
assert!(!found.contains_key("lockObject"));
assert_eq!(found["unusedHelper"], ("unreferenced".into(), vec![]));
let reasons = |name: &str| found[name].1.clone();
assert_eq!(found["size"].0, "unknown");
// make().size() may reach Widget::size; std::string::size cannot.
assert!(reasons("size").contains(&"unresolved-call-sites".to_owned()));
assert!(reasons("callback").contains(&"referenced-by-name".to_owned()));
assert!(reasons("hook").contains(&"virtual".to_owned()));
assert!(reasons("Widget").contains(&"implicit-invocation".to_owned()));
assert!(reasons("main").contains(&"entry-point".to_owned()));
}
#[test]
fn closed_world_drops_linkage_but_keeps_evidence_of_indirect_use() {
let open = statuses(cpp_model(), false);
let closed = statuses(cpp_model(), true);
assert!(open["size"].1.contains(&"external-linkage".to_owned()));
assert!(!closed["size"].1.contains(&"external-linkage".to_owned()));
assert_eq!(closed["size"].0, "unknown");
assert_eq!(closed["unusedHelper"].0, "unreferenced");
}
#[test]
fn rust_attributes_attribute_strings_and_trait_impls_are_uses() {
let model = scan_files(&[(
"src/lib.rs",
"use std::fmt;\nstruct Point;\nimpl fmt::Display for Point {\n fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { Ok(()) }\n}\n#[derive(serde::Deserialize)]\nstruct Config {\n #[serde(deserialize_with = \"crate::parse_flag\")]\n flag: bool,\n}\nfn parse_flag() {}\nfn orphan() {}\n#[test]\nfn checks() {}\n#[cfg(test)]\nmod tests {\n fn helper() {}\n}\n",
)]);
let found = statuses(model, true);
assert_eq!(found["orphan"], ("unreferenced".into(), vec![]));
assert!(found["fmt"].1.contains(&"trait-impl".to_owned()));
assert!(
found["parse_flag"]
.1
.contains(&"referenced-by-name".to_owned())
);
assert!(
found["checks"]
.1
.contains(&"framework-attribute".to_owned())
);
assert!(found["helper"].1.contains(&"test-code".to_owned()));
}
#[test]
fn callers_query_reports_resolved_callers_basis_and_rejects_non_functions() {
let model = cpp_model();
let shared = model
.nodes
.iter()
.find(|n| n.name == "shared" && n.path.as_deref() == Some("src/app.cpp"))
.unwrap()
.id
.clone();
let file = model
.nodes
.iter()
.find(|n| {
n.path.as_deref() == Some("src/app.cpp") && n.kind == brick::model::NodeKind::File
})
.unwrap()
.id
.clone();
let catalog = Catalog::new(model).unwrap();
let result = catalog
.call("brick_callers", json!({"node_id": shared}))
.unwrap();
let report = &result["report"];
assert_eq!(report["status"], "resolved");
assert_eq!(report["basis"], "heuristic");
assert_eq!(report["callers"].as_array().unwrap().len(), 1);
assert!(
result["coverage"]["cpp"]["call_site_count"]
.as_u64()
.unwrap()
> 0
);
assert!(result["caveat"].as_str().unwrap().contains("not proof"));
assert!(
catalog
.call("brick_callers", json!({"node_id": file}))
.is_err()
);
assert!(
catalog
.call(
"brick_callers",
json!({"node_id": shared, "extra": Value::Null})
)
.is_err()
);
}
#[test]
fn macro_bodies_and_syntax_errors_make_functions_unknown_not_dead() {
let model = scan_files(&[
(
"src/plugin.cpp",
"static void* open_impl(const char* name) { return nullptr; }\n#define open_library open_impl\nstatic void* unused_impl() { return nullptr; }\nvoid* load() { return open_library(\"x\"); }\n",
),
(
"src/broken.cpp",
"namespace {\n#if defined(A)\n}\n#endif\nstatic int lost() { return 1; }\n",
),
]);
let found = statuses(model, true);
assert!(
found["open_impl"]
.1
.contains(&"referenced-by-name".to_owned())
);
assert_eq!(found["unused_impl"], ("unreferenced".into(), vec![]));
assert!(
found["lost"].1.contains(&"parse-incomplete".to_owned()),
"{found:?}"
);
}
#[test]
fn template_parameters_aliases_and_protocol_methods_are_not_dead() {
let model = scan_files(&[
(
"src/repair.h",
"#include <memory>\nnamespace app {\nstruct Repair { void enqueue(); void drain(); void unused(); };\nusing RepairPtr = std::shared_ptr<Repair>;\nstruct Awaiter { bool await_ready() const noexcept; };\ntemplate <class T> struct Box { Box(); };\ntemplate <> struct Box<void> { Box() {} };\n}\n",
),
(
"src/repair.cpp",
"#include \"repair.h\"\n#include <lib/fsm.h>\nnamespace app {\nvoid Repair::enqueue() {}\nvoid Repair::drain() {}\nvoid Repair::unused() {}\nbool Awaiter::await_ready() const noexcept { return true; }\n}\ntemplate <> void lib::Fsm<int>::start() {}\n",
),
(
"src/use.cpp",
"#include \"repair.h\"\nnamespace app {\ntemplate <typename Ptr>\nvoid poke(Ptr service) { service->enqueue(); }\nvoid run(RepairPtr repair) { repair->drain(); }\n}\n",
),
]);
let found = statuses(model, true);
// Alias through shared_ptr resolves; the templated receiver keeps enqueue possible.
assert!(!found.contains_key("drain"), "{found:?}");
assert_eq!(found["enqueue"].0, "unknown");
assert!(
found["enqueue"]
.1
.contains(&"unresolved-call-sites".to_owned())
);
assert!(
found["await_ready"]
.1
.contains(&"implicit-invocation".to_owned())
);
assert!(
found["start"]
.1
.contains(&"outside-class-member".to_owned())
);
assert!(found["Box"].1.contains(&"implicit-invocation".to_owned()));
// Clean negative: nothing can reach unused().
assert_eq!(found["unused"], ("unreferenced".into(), vec![]));
}