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>
219 lines
8.5 KiB
Rust
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![]));
|
|
}
|