feat: resolve C/C++ and Rust calls across files and typed receivers #3
25 changed files with 3358 additions and 102 deletions
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>
commit
48436f35b7
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
15
src/model.rs
15
src/model.rs
|
|
@ -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,
|
||||
|
|
|
|||
952
src/scan.rs
952
src/scan.rs
File diff suppressed because it is too large
Load diff
1450
src/source.rs
1450
src/source.rs
File diff suppressed because it is too large
Load diff
14
tests/benchmarks/plans/cpp_xtu.json
Normal file
14
tests/benchmarks/plans/cpp_xtu.json
Normal 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"
|
||||
}
|
||||
14
tests/benchmarks/plans/rust_methods.json
Normal file
14
tests/benchmarks/plans/rust_methods.json
Normal 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"
|
||||
}
|
||||
|
|
@ -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
328
tests/cpp_xtu.rs
Normal 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);
|
||||
}
|
||||
21
tests/fixtures/cpp_xtu/cpp/include/geo/shape.h
vendored
Normal file
21
tests/fixtures/cpp_xtu/cpp/include/geo/shape.h
vendored
Normal 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
223
tests/fixtures/cpp_xtu/cpp/labels.json
vendored
Normal 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"
|
||||
]
|
||||
]
|
||||
}
|
||||
6
tests/fixtures/cpp_xtu/cpp/src/buffer.h
vendored
Normal file
6
tests/fixtures/cpp_xtu/cpp/src/buffer.h
vendored
Normal 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; }
|
||||
};
|
||||
3
tests/fixtures/cpp_xtu/cpp/src/legacy.c
vendored
Normal file
3
tests/fixtures/cpp_xtu/cpp/src/legacy.c
vendored
Normal 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
16
tests/fixtures/cpp_xtu/cpp/src/main.cpp
vendored
Normal 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);
|
||||
}
|
||||
6
tests/fixtures/cpp_xtu/cpp/src/other.cpp
vendored
Normal file
6
tests/fixtures/cpp_xtu/cpp/src/other.cpp
vendored
Normal 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); }
|
||||
4
tests/fixtures/cpp_xtu/cpp/src/overload.cpp
vendored
Normal file
4
tests/fixtures/cpp_xtu/cpp/src/overload.cpp
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
void log_value(int) {}
|
||||
void log_value(double) {}
|
||||
|
||||
void use_log() { log_value(1); }
|
||||
20
tests/fixtures/cpp_xtu/cpp/src/shape.cpp
vendored
Normal file
20
tests/fixtures/cpp_xtu/cpp/src/shape.cpp
vendored
Normal 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
|
||||
5
tests/fixtures/cpp_xtu/cpp/src/util.cpp
vendored
Normal file
5
tests/fixtures/cpp_xtu/cpp/src/util.cpp
vendored
Normal 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
4
tests/fixtures/cpp_xtu/cpp/src/util.h
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
#pragma once
|
||||
|
||||
int helper(int x);
|
||||
#define LOG_TWICE(x) ((x) * 2)
|
||||
2
tests/fixtures/cpp_xtu/cpp/tools/dup.cpp
vendored
Normal file
2
tests/fixtures/cpp_xtu/cpp/tools/dup.cpp
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
// Clean negative: a second program's helper that never includes util.h.
|
||||
int helper(int x) { return x; }
|
||||
51
tests/fixtures/rust_methods/rust/labels.json
vendored
Normal file
51
tests/fixtures/rust_methods/rust/labels.json
vendored
Normal 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"]
|
||||
]
|
||||
}
|
||||
19
tests/fixtures/rust_methods/rust/src/lib.rs
vendored
Normal file
19
tests/fixtures/rust_methods/rust/src/lib.rs
vendored
Normal 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())
|
||||
}
|
||||
14
tests/fixtures/rust_methods/rust/src/negative.rs
vendored
Normal file
14
tests/fixtures/rust_methods/rust/src/negative.rs
vendored
Normal 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()
|
||||
}
|
||||
65
tests/fixtures/rust_methods/rust/src/store.rs
vendored
Normal file
65
tests/fixtures/rust_methods/rust/src/store.rs
vendored
Normal 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
155
tests/rust_methods.rs
Normal 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)
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue