feat: resolve C/C++ and Rust calls across files and typed receivers #3

Merged
trevon merged 2 commits from feat/cpp-rust-resolution into main 2026-09-22 21:14:53 +00:00
25 changed files with 3358 additions and 102 deletions
Showing only changes of commit 48436f35b7 - Show all commits

feat: resolve C/C++ and Rust calls across files and typed receivers

C++ calls now resolve through the include closure: header prototypes pair
with definitions in other translation units, lookup walks class (including
in-repository bases), namespace, and `using` scopes outward, and static or
anonymous-namespace definitions stay file-local. Member calls resolve only
through explicitly declared receiver types (parameters, locals, fields,
this, smart pointers, aliases); template parameters stay unknown. Calls
hidden by the most vexing parse are recovered, casts are no longer calls,
reference-returning and conversion-operator declarations are named
correctly, and benign `= {}` default-argument recoveries no longer drop
prototypes or flag files.

Rust method calls resolve through a crate-wide impl index for self, typed
parameters and lets, struct fields, Self-returning constructors, dyn
traits, and Type::/Self:: associated calls, with foreign imports and
generics never guessed. Calls inside expression macros are re-parsed.

Every unresolved call records source.unresolved-reason, weaker resolutions
record source.resolution/source.extraction, and metrics.resolution reports
per-language call-site coverage. Declarations gain facts consumed by
caller-status queries (qualified names, linkage, virtual, trait impls,
attributes, name references, parse-incomplete). C++/Rust External targets
include the reason in their identity; out-of-line C++ methods keep their
IDs.

Linear node/edge scans and per-node ancestor walks are replaced with
indexes, cutting a YAMS scan from 575 s to 23 s.

New labeled cpp_xtu and rust_methods corpora run three times in the
quality gate.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
trvon 2026-09-22 15:11:04 -06:00
Signed by: trevon
GPG key ID: 083EB1C25C575E81

View file

@ -11,6 +11,8 @@ python3 -m unittest discover -s tests/benchmarks -p 'test_*.py'
python3 tests/benchmarks/runner.py self-test
python3 tests/benchmarks/runner.py run source_languages
python3 tests/benchmarks/runner.py run rust_imports
python3 tests/benchmarks/runner.py run cpp_xtu
python3 tests/benchmarks/runner.py run rust_methods
python3 tests/benchmarks/diff_runner.py self-test
python3 tests/benchmarks/policy_runner.py self-test
python3 tests/benchmarks/mcp_runner.py self-test

View file

@ -1,8 +1,9 @@
use crate::model::{
Building, Diagnostic, Edge, EdgeKind, Node, NodeKind, P4Metrics, StructuralHealth,
Building, Diagnostic, Edge, EdgeKind, EvidenceLane, Node, NodeKind, P4Metrics,
ResolutionMetrics, StructuralHealth,
};
use serde_json::Value;
use std::collections::{HashMap, HashSet, VecDeque};
use std::collections::{BTreeMap, HashMap, HashSet, VecDeque};
const P4_DIAGNOSTIC_PREFIX: &str = "P4 structural observation (not verifier proof):";
@ -87,6 +88,7 @@ pub fn calculate(building: &mut Building) {
building.metrics.max_fan_in = max_fan_in;
building.metrics.max_fan_out = max_fan_out;
building.metrics.p4 = p4_metrics;
building.metrics.resolution = calculate_resolution(building);
building.metrics.structural_health = StructuralHealth {
score: round(score),
experimental: true,
@ -104,6 +106,68 @@ pub fn calculate(building: &mut Building) {
building.diagnostics.extend(p4_diagnostics);
}
/// Counts syntactic call sites (heuristic evidence entries), not deduplicated edges.
fn calculate_resolution(building: &Building) -> BTreeMap<String, ResolutionMetrics> {
let nodes: HashMap<&str, &Node> = building
.nodes
.iter()
.map(|node| (node.id.as_str(), node))
.collect();
let mut result: BTreeMap<String, ResolutionMetrics> = BTreeMap::new();
let mut called = HashSet::new();
for edge in building
.edges
.iter()
.filter(|edge| edge.kind == EdgeKind::Calls)
{
let (Some(source), Some(target)) = (
nodes.get(edge.source.as_str()),
nodes.get(edge.target.as_str()),
) else {
continue;
};
let Some(language) = source.language.as_deref() else {
continue;
};
let sites = edge
.evidence
.iter()
.filter(|evidence| evidence.lane == EvidenceLane::Heuristic)
.count();
let metrics = result.entry(language.into()).or_default();
metrics.call_site_count += sites;
if target.kind == NodeKind::External {
let reason = edge
.attributes
.get("source.unresolved-reason")
.and_then(Value::as_str)
.unwrap_or("unclassified");
*metrics
.unresolved_call_sites_by_reason
.entry(reason.into())
.or_default() += sites;
} else {
metrics.resolved_call_site_count += sites;
called.insert(target.id.as_str());
}
}
for node in building
.nodes
.iter()
.filter(|node| node.kind == NodeKind::Function)
{
let Some(language) = node.language.as_deref() else {
continue;
};
let metrics = result.entry(language.into()).or_default();
metrics.function_count += 1;
if !called.contains(node.id.as_str()) {
metrics.functions_without_resolved_callers += 1;
}
}
result
}
fn calculate_p4(building: &Building) -> (P4Metrics, Vec<Diagnostic>) {
let nodes: HashMap<&str, &Node> = building
.nodes

View file

@ -436,9 +436,24 @@ pub struct Metrics {
pub max_fan_in: usize,
pub max_fan_out: usize,
pub p4: P4Metrics,
/// Per-language call-resolution coverage. Absent in models from older builds.
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub resolution: BTreeMap<String, ResolutionMetrics>,
pub structural_health: StructuralHealth,
}
/// How much of a language's call graph resolved to in-repository symbols.
/// A symbol without resolved callers is not evidence of dead code unless the
/// unresolved call sites that could reach it are accounted for.
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct ResolutionMetrics {
pub call_site_count: usize,
pub resolved_call_site_count: usize,
pub unresolved_call_sites_by_reason: BTreeMap<String, usize>,
pub function_count: usize,
pub functions_without_resolved_callers: usize,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct P4Metrics {
pub parser_state_count: usize,

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,14 @@
{
"schema_version": 1,
"name": "cpp_xtu",
"corpus": "tests/fixtures/cpp_xtu/cpp",
"repeats": 3,
"direct_binary": true,
"measure_rss": true,
"expect": {
"min_symbols": 23,
"min_dependency_edges": 24,
"cycles": 0
},
"source_labels": "labels.json"
}

View file

@ -0,0 +1,14 @@
{
"schema_version": 1,
"name": "rust_methods",
"corpus": "tests/fixtures/rust_methods/rust",
"repeats": 3,
"direct_binary": true,
"measure_rss": true,
"expect": {
"min_symbols": 21,
"min_dependency_edges": 23,
"cycles": 0
},
"source_labels": "labels.json"
}

View file

@ -412,7 +412,8 @@ def source_language_quality(model: dict, labels: dict) -> dict:
if current["id"] in visited:
raise AssertionError("cyclic declaration ownership")
visited.add(current["id"])
names.append(current["name"])
qualifier = current.get("attributes", {}).get("source.qualifier")
names.append(f"{qualifier}::{current['name']}" if qualifier else current["name"])
current = nodes[current["parent"]]
scope = node.get("attributes", {}).get("source.scope")
return "::".join([node["path"], *([f"scope={scope}"] if scope else []), *reversed(names)])

328
tests/cpp_xtu.rs Normal file
View file

@ -0,0 +1,328 @@
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 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);
}

View file

@ -0,0 +1,21 @@
#pragma once
namespace geo {
class Shape {
public:
Shape();
double area() const;
static Shape unit();
int& counter();
private:
double scale(double x) const;
};
class Circle : public Shape {
public:
double radius() const;
};
double total(const Shape& shape);
} // namespace geo

223
tests/fixtures/cpp_xtu/cpp/labels.json vendored Normal file
View file

@ -0,0 +1,223 @@
{
"language_prefixes": {
"headers": "include/",
"sources": "src/",
"tools": "tools/"
},
"symbols": [
[
"include/geo/shape.h::scope=geo::Shape",
"class"
],
[
"src/shape.cpp::scope=geo::Shape::Shape",
"function"
],
[
"src/shape.cpp::scope=geo::Shape::area",
"function"
],
[
"src/shape.cpp::scope=geo::Shape::scale",
"function"
],
[
"src/shape.cpp::scope=geo::Shape::unit",
"function"
],
[
"src/shape.cpp::scope=geo::Shape::counter",
"function"
],
[
"src/shape.cpp::scope=geo::total",
"function"
],
[
"src/util.cpp::twice",
"function"
],
[
"src/util.cpp::helper",
"function"
],
[
"src/main.cpp::main",
"function"
],
[
"src/other.cpp::twice",
"function"
],
[
"src/other.cpp::orphan",
"function"
],
[
"src/other.cpp::undeclared_caller",
"function"
],
[
"src/overload.cpp::log_value",
"function"
],
[
"src/overload.cpp::log_value",
"function"
],
[
"src/overload.cpp::use_log",
"function"
],
[
"src/legacy.c::add",
"function"
],
[
"src/legacy.c::legacy_sum",
"function"
],
[
"tools/dup.cpp::helper",
"function"
],
[
"include/geo/shape.h::scope=geo::Circle",
"class"
],
[
"src/shape.cpp::scope=geo::Circle::radius",
"function"
],
[
"src/buffer.h::Buffer",
"class"
],
[
"src/buffer.h::Buffer::size",
"function"
]
],
"relationships": [
[
"src/shape.cpp",
"imports",
"include/geo/shape.h"
],
[
"src/util.cpp",
"imports",
"src/util.h"
],
[
"src/main.cpp",
"imports",
"include/geo/shape.h"
],
[
"src/main.cpp",
"imports",
"src/util.h"
],
[
"src/shape.cpp::scope=geo::Shape::area",
"calls",
"src/shape.cpp::scope=geo::Shape::scale"
],
[
"src/shape.cpp::scope=geo::Shape::unit",
"calls",
"src/shape.cpp::scope=geo::Shape::Shape"
],
[
"src/util.cpp::helper",
"calls",
"src/util.cpp::twice"
],
[
"src/main.cpp::main",
"calls",
"src/shape.cpp::scope=geo::Shape::unit"
],
[
"src/main.cpp::main",
"calls",
"src/util.cpp::helper"
],
[
"src/main.cpp::main",
"calls",
"src/shape.cpp::scope=geo::total"
],
[
"src/main.cpp::main",
"calls",
"external:cpp:LOG_TWICE"
],
[
"src/other.cpp::orphan",
"calls",
"src/other.cpp::twice"
],
[
"src/other.cpp::undeclared_caller",
"calls",
"external:cpp:helper"
],
[
"src/overload.cpp::use_log",
"calls",
"external:cpp:log_value"
],
[
"src/legacy.c::legacy_sum",
"calls",
"src/legacy.c::add"
],
[
"src/main.cpp",
"imports",
"src/buffer.h"
],
[
"src/main.cpp",
"imports",
"external:cpp:string"
],
[
"include/geo/shape.h::scope=geo::Circle",
"inherits",
"include/geo/shape.h::scope=geo::Shape"
],
[
"src/shape.cpp::scope=geo::Circle::radius",
"calls",
"src/shape.cpp::scope=geo::Shape::area"
],
[
"src/shape.cpp::scope=geo::total",
"calls",
"src/shape.cpp::scope=geo::Shape::area"
],
[
"src/main.cpp::main",
"calls",
"src/shape.cpp::scope=geo::Shape::counter"
],
[
"src/main.cpp::main",
"calls",
"external:cpp:size"
],
[
"src/main.cpp::main",
"calls",
"external:cpp:size"
],
[
"src/main.cpp::main",
"calls",
"external:cpp:make_buffer"
]
]
}

View file

@ -0,0 +1,6 @@
#pragma once
// Clean negative: a repository size() that std::string and untyped receivers must not reach.
struct Buffer {
int size() const { return 0; }
};

View file

@ -0,0 +1,3 @@
static int add(int a, int b) { return a + b; }
int legacy_sum(void) { return add(1, 2); }

16
tests/fixtures/cpp_xtu/cpp/src/main.cpp vendored Normal file
View file

@ -0,0 +1,16 @@
#include "geo/shape.h"
#include "buffer.h"
#include "util.h"
#include <string>
Buffer make_buffer();
int main() {
std::string text;
text.size();
make_buffer().size();
geo::Shape shape = geo::Shape::unit();
int n = helper(3);
shape.counter();
return static_cast<int>(geo::total(shape)) + n + LOG_TWICE(n);
}

View file

@ -0,0 +1,6 @@
// Clean negative: same-named internal function and an undeclared cross-TU call.
static int twice(int x) { return x + x; }
int orphan() { return twice(1); }
int undeclared_caller() { return helper(1); }

View file

@ -0,0 +1,4 @@
void log_value(int) {}
void log_value(double) {}
void use_log() { log_value(1); }

View file

@ -0,0 +1,20 @@
#include "geo/shape.h"
namespace geo {
Shape::Shape() {}
double Shape::area() const { return this->scale(2.0) + scale(1.0); }
double Shape::scale(double x) const { return x; }
Shape Shape::unit() { return Shape(); }
int& Shape::counter() {
static int count = 0;
return count;
}
double Circle::radius() const { return area(); }
double total(const Shape& shape) { return shape.area(); }
} // namespace geo

View file

@ -0,0 +1,5 @@
#include "util.h"
static int twice(int x) { return x * 2; }
int helper(int x) { return twice(x); }

4
tests/fixtures/cpp_xtu/cpp/src/util.h vendored Normal file
View file

@ -0,0 +1,4 @@
#pragma once
int helper(int x);
#define LOG_TWICE(x) ((x) * 2)

View file

@ -0,0 +1,2 @@
// Clean negative: a second program's helper that never includes util.h.
int helper(int x) { return x; }

View file

@ -0,0 +1,51 @@
{
"language_prefixes": {"rust": "src/"},
"symbols": [
["src/lib.rs::run", "function"],
["src/lib.rs::describe", "function"],
["src/store.rs::Engine", "class"],
["src/store.rs::Engine::commit", "function"],
["src/store.rs::Store", "class"],
["src/store.rs::Store::open", "function"],
["src/store.rs::Store::build", "function"],
["src/store.rs::Store::flush", "function"],
["src/store.rs::Store::name", "function"],
["src/store.rs::Store::is_ready", "function"],
["src/store.rs::Store::log", "function"],
["src/store.rs::Named", "class"],
["src/store.rs::Named::label", "function"],
["src/store.rs::Store::label", "function"],
["src/store.rs::Engine::label", "function"],
["src/store.rs::show", "function"],
["src/store.rs::show_generic", "function"],
["src/negative.rs::File", "class"],
["src/negative.rs::File::open", "function"],
["src/negative.rs::File::sync_all", "function"],
["src/negative.rs::unrelated", "function"]
],
"relationships": [
["src/lib.rs", "imports", "src/store.rs"],
["src/lib.rs", "imports", "external:rust:std::fs::File"],
["src/store.rs", "imports", "external:rust:std::sync::Arc"],
["src/lib.rs::run", "calls", "src/store.rs::Store::open"],
["src/lib.rs::run", "calls", "src/store.rs::Store::flush"],
["src/lib.rs::run", "calls", "src/store.rs::Store::is_ready"],
["src/lib.rs::run", "calls", "src/lib.rs::describe"],
["src/lib.rs::run", "calls", "external:rust:File::open"],
["src/lib.rs::run", "calls", "external:rust:sync_all"],
["src/lib.rs::run", "calls", "external:rust:Ok"],
["src/lib.rs::describe", "calls", "src/store.rs::Store::name"],
["src/store.rs::Store::open", "calls", "external:rust:Ok"],
["src/store.rs::Store::open", "calls", "src/store.rs::Store::build"],
["src/store.rs::Store::build", "calls", "external:rust:Arc::new"],
["src/store.rs::Store::build", "calls", "external:rust:into"],
["src/store.rs::Store::flush", "calls", "src/store.rs::Engine::commit"],
["src/store.rs::Store::flush", "calls", "src/store.rs::Store::log"],
["src/store.rs::Store::label", "calls", "src/store.rs::Store::name"],
["src/store.rs::Store::label", "calls", "external:rust:to_owned"],
["src/store.rs::Engine::label", "calls", "external:rust:String::new"],
["src/store.rs::show", "calls", "src/store.rs::Named::label"],
["src/store.rs::show_generic", "calls", "external:rust:label"],
["src/negative.rs::unrelated", "calls", "external:rust:len"]
]
}

View file

@ -0,0 +1,19 @@
mod negative;
mod store;
use crate::store::Store;
use std::fs::File;
pub fn run(path: &str) -> anyhow::Result<()> {
let store = Store::open(path)?;
store.flush();
assert!(store.is_ready(), "not ready: {}", describe(&store));
// Clean negative: std's File must not reach the repository's negative::File.
let file = File::open(path)?;
file.sync_all()?;
Ok(())
}
fn describe(store: &Store) -> String {
format!("{}", store.name())
}

View file

@ -0,0 +1,14 @@
// Clean negative: same names as std::fs::File; nothing in lib.rs may call these.
pub struct File;
impl File {
pub fn open(_path: &str) -> Self {
File
}
pub fn sync_all(&self) {}
}
pub fn unrelated(value: &str) -> usize {
value.len()
}

View file

@ -0,0 +1,65 @@
use std::sync::Arc;
pub struct Engine;
impl Engine {
pub fn commit(&self) {}
}
pub struct Store {
inner: Arc<Engine>,
name: String,
}
impl Store {
pub fn open(path: &str) -> anyhow::Result<Self> {
Ok(Self::build(path))
}
fn build(path: &str) -> Self {
Store {
inner: Arc::new(Engine),
name: path.into(),
}
}
pub fn flush(&self) {
self.inner.commit();
self.log();
}
pub fn name(&self) -> &str {
&self.name
}
pub fn is_ready(&self) -> bool {
true
}
fn log(&self) {}
}
pub trait Named {
fn label(&self) -> String;
}
impl Named for Store {
fn label(&self) -> String {
self.name().to_owned()
}
}
impl Named for Engine {
fn label(&self) -> String {
String::new()
}
}
pub fn show(item: &dyn Named) -> String {
item.label()
}
// Clean negative: a generic receiver names no concrete implementation.
pub fn show_generic<T: Named>(item: &T) -> String {
item.label()
}

155
tests/rust_methods.rs Normal file
View file

@ -0,0 +1,155 @@
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 parent name, target name, attribute value) for calls leaving `source`.
fn calls(model: &Building, source: &Node, attribute: &str) -> Vec<(String, String, String)> {
let mut found: Vec<_> = 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 parent = target
.parent
.as_ref()
.and_then(|id| model.nodes.iter().find(|n| &n.id == id))
.map(|n| n.name.clone())
.unwrap_or_default();
let value = e
.attributes
.get(attribute)
.and_then(|v| v.as_str())
.unwrap_or("")
.to_owned();
(parent, target.name.clone(), value)
})
.collect();
found.sort();
found
}
#[test]
fn expression_macro_bodies_yield_calls_and_other_macros_stay_opaque() {
let model = scan_files(&[(
"src/lib.rs",
"fn check() -> bool { true }\nfn name() -> u8 { 0 }\nfn hidden() {}\nmacro_rules! custom { ($e:expr) => { $e } }\nfn run() {\n assert!(check(), \"{}\", format!(\"{}\", name()));\n custom!(hidden());\n}\n",
)]);
let run = function(&model, "src/lib.rs", "run");
assert_eq!(
calls(&model, run, "source.extraction"),
[
("lib.rs".into(), "check".into(), "macro-reparse".into()),
("lib.rs".into(), "name".into(), "macro-reparse".into()),
]
);
let hidden = function(&model, "src/lib.rs", "hidden");
assert!(
!model
.edges
.iter()
.any(|e| e.target == hidden.id && e.kind == EdgeKind::Calls)
);
}
#[test]
fn receivers_resolve_through_self_fields_constructors_and_dyn_traits() {
let model = scan_files(&[(
"src/lib.rs",
"use std::sync::Arc;\npub struct Engine;\nimpl Engine { pub fn commit(&self) {} }\npub trait Named { fn label(&self) -> String; }\npub struct Store { inner: Arc<Engine> }\nimpl Store {\n pub fn open() -> Result<Self, ()> { Ok(Self::build()) }\n fn build() -> Self { Store { inner: Arc::new(Engine) } }\n fn flush(&self) {\n self.inner.commit();\n self.log();\n }\n fn log(&self) {}\n}\nfn run(item: &dyn Named) -> Result<(), ()> {\n let store = Store::open()?;\n store.flush();\n item.label();\n Ok(())\n}\n",
)]);
let resolved = |source: &Node| {
calls(&model, source, "source.resolution")
.into_iter()
.filter(|(parent, _, _)| !parent.is_empty())
.collect::<Vec<_>>()
};
assert_eq!(
resolved(function(&model, "src/lib.rs", "flush")),
[
(
"Engine".into(),
"commit".into(),
"declared-receiver-type".into()
),
("Store".into(), "log".into(), "self-receiver".into()),
]
);
assert_eq!(
resolved(function(&model, "src/lib.rs", "open")),
[("Store".into(), "build".into(), "impl-type".into())]
);
assert_eq!(
resolved(function(&model, "src/lib.rs", "run")),
[
(
"Named".into(),
"label".into(),
"declared-receiver-type".into()
),
(
"Store".into(),
"flush".into(),
"declared-receiver-type".into()
),
// Same-file `Type::f` already resolves by qualified lookup, unlabeled.
("Store".into(), "open".into(), "".into()),
]
);
}
#[test]
fn foreign_imports_generics_and_competing_trait_impls_never_guess() {
let model = scan_files(&[
(
"src/lib.rs",
"mod local;\nuse std::fs::File;\npub trait Named { fn label(&self) -> String; }\nstruct A;\nstruct B;\nstruct C;\nimpl Named for A { fn label(&self) -> String { String::new() } }\nimpl Named for C { fn label(&self) -> String { String::new() } }\ntrait Other { fn label(&self) -> String; }\nimpl Other for A { fn label(&self) -> String { String::new() } }\nfn run<T: Named>(generic: &T, a: A) {\n let file = File::open(\"x\").unwrap();\n file.sync_all();\n generic.label();\n a.label();\n}\n",
),
(
"src/local.rs",
"pub struct File;\nimpl File {\n pub fn open(_p: &str) -> Self { File }\n pub fn sync_all(&self) {}\n}\n",
),
]);
let run = function(&model, "src/lib.rs", "run");
let reasons: Vec<_> = calls(&model, run, "source.unresolved-reason")
.into_iter()
.map(|(_, name, reason)| (name, reason))
.collect();
assert_eq!(
reasons,
[
("File::open".into(), "qualifier-unknown".into()),
("label".into(), "ambiguous".into()),
("label".into(), "receiver-type-unknown".into()),
("sync_all".into(), "receiver-type-unknown".into()),
("unwrap".into(), "receiver-type-unknown".into()),
]
);
for name in ["open", "sync_all"] {
let local = function(&model, "src/local.rs", name);
assert!(
!model
.edges
.iter()
.any(|e| e.target == local.id && e.kind == EdgeKind::Calls)
);
}
}