use std::collections::{HashMap, HashSet};
use crate::graph::types::{CodeGraph, Confidence, Edge, FileFacts, Provenance, RefRole, Symbol};
use super::{
ConformanceResolver, ExternalResolver, FfiBridgeResolver, NormalizedNameResolver, Resolver,
ScopeGraphResolver, SymbolTableResolver,
};
pub struct LayeredResolver {
layers: Vec<Box<dyn Resolver>>,
}
impl LayeredResolver {
pub fn new(layers: Vec<Box<dyn Resolver>>) -> Self {
Self { layers }
}
pub fn default_dense() -> Self {
Self::new(vec![
Box::new(SymbolTableResolver),
Box::new(ScopeGraphResolver),
Box::new(FfiBridgeResolver),
Box::new(ConformanceResolver),
Box::new(ExternalResolver),
Box::new(NormalizedNameResolver),
])
}
}
impl Resolver for LayeredResolver {
fn resolve(&self, files: &[FileFacts]) -> CodeGraph {
let graphs: Vec<CodeGraph> = self.layers.iter().map(|r| r.resolve(files)).collect();
let mut seen_syms: HashSet<String> = HashSet::new();
let mut symbols: Vec<Symbol> = Vec::new();
for g in &graphs {
for sym in &g.symbols {
let key = sym.id.to_scip_string();
if seen_syms.insert(key) {
symbols.push(sym.clone());
}
}
}
type EdgeKey = (String, String, RefRole, String, usize);
let all_edges: Vec<_> = graphs.iter().flat_map(|g| g.edges.iter()).collect();
let keys: Vec<EdgeKey> = all_edges
.iter()
.map(|e| {
(
e.from.to_scip_string(),
e.to.to_scip_string(),
e.role,
e.occ.file.clone(),
e.occ.byte,
)
})
.collect();
let mut max_conf: HashMap<EdgeKey, Confidence> = HashMap::new();
for (key, e) in keys.iter().zip(all_edges.iter()) {
if let Some(c) = max_conf.get_mut(key) {
*c = (*c).max(e.confidence);
} else {
max_conf.insert(key.clone(), e.confidence);
}
}
let mut seen_key_prov: HashSet<(EdgeKey, Provenance)> = HashSet::new();
let mut edges: Vec<Edge> = Vec::new();
for (e, key) in all_edges.into_iter().zip(keys) {
let Some(&max) = max_conf.get(&key) else {
continue;
};
if e.confidence < max {
continue;
}
if seen_key_prov.insert((key, e.provenance)) {
edges.push(e.clone());
}
}
CodeGraph { symbols, edges }
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::graph::types::{
ByteSpan, CodeGraph, Confidence, Edge, FileFacts, Occurrence, Provenance, RefRole, Symbol,
SymbolKind, Visibility,
};
use crate::symbol::{Descriptor, SymbolId};
fn make_id(ns: &str, name: &str) -> SymbolId {
SymbolId::global(
"rust",
vec![
Descriptor::Namespace(ns.into()),
Descriptor::Term(name.into()),
],
)
}
fn make_symbol(ns: &str, name: &str) -> Symbol {
Symbol {
id: make_id(ns, name),
name: name.into(),
kind: SymbolKind::Function,
visibility: Visibility::Public,
entry_points: Vec::new(),
file: format!("src/{ns}.rs"),
line: 1,
span: ByteSpan { start: 0, end: 10 },
signature: format!("pub fn {name}()"),
}
}
fn make_edge(
from_ns: &str,
from_name: &str,
to_ns: &str,
to_name: &str,
confidence: Confidence,
provenance: Provenance,
byte: usize,
) -> Edge {
Edge {
from: make_id(from_ns, from_name),
to: make_id(to_ns, to_name),
role: RefRole::Call,
confidence,
provenance,
occ: Occurrence {
file: "src/caller.rs".into(),
line: 1,
col: 0,
byte,
},
}
}
struct StubResolver(CodeGraph);
impl Resolver for StubResolver {
fn resolve(&self, _files: &[FileFacts]) -> CodeGraph {
self.0.clone()
}
}
fn stub(graph: CodeGraph) -> Box<dyn Resolver> {
Box::new(StubResolver(graph))
}
#[test]
fn layered_is_superset_of_scope_graph() {
use crate::extract::{Extractor, RustExtractor};
use crate::resolve::ScopeGraphResolver;
let lib = RustExtractor
.extract("pub fn helper() -> u32 { 1 }", "src/util.rs")
.unwrap();
let main = RustExtractor
.extract("pub fn run() -> u32 { helper() }", "src/main.rs")
.unwrap();
let files = [lib, main];
let scope_graph = ScopeGraphResolver.resolve(&files);
let layered = LayeredResolver::default_dense().resolve(&files);
for sg_edge in &scope_graph.edges {
let sg_from = sg_edge.from.to_scip_string();
let sg_to = sg_edge.to.to_scip_string();
let found = layered.edges.iter().any(|le| {
le.from.to_scip_string() == sg_from
&& le.to.to_scip_string() == sg_to
&& le.role == sg_edge.role
});
assert!(
found,
"layered graph is missing ScopeGraph edge: {} → {} ({:?})",
sg_from, sg_to, sg_edge.role
);
}
}
#[test]
fn higher_confidence_wins_lower_is_dropped() {
let low_edge = make_edge(
"a",
"run",
"b",
"helper",
Confidence::NameOnly,
Provenance::SymbolTable,
10,
);
let high_edge = make_edge(
"a",
"run",
"b",
"helper",
Confidence::Exact,
Provenance::ScopeGraph,
10,
);
let g1 = CodeGraph {
symbols: vec![make_symbol("a", "run"), make_symbol("b", "helper")],
edges: vec![low_edge],
};
let g2 = CodeGraph {
symbols: vec![],
edges: vec![high_edge],
};
let resolver = LayeredResolver::new(vec![stub(g1), stub(g2)]);
let merged = resolver.resolve(&[]);
let call_edges: Vec<_> = merged
.edges
.iter()
.filter(|e| e.role == RefRole::Call)
.collect();
assert_eq!(
call_edges.len(),
1,
"expected 1 edge after confidence dedup, got {}: {:?}",
call_edges.len(),
call_edges
.iter()
.map(|e| format!("{:?}/{:?}", e.confidence, e.provenance))
.collect::<Vec<_>>()
);
assert_eq!(
call_edges[0].confidence,
Confidence::Exact,
"surviving edge must be the Exact one"
);
let has_name_only = merged
.edges
.iter()
.any(|e| e.confidence == Confidence::NameOnly);
assert!(
!has_name_only,
"strictly-lower-confidence NameOnly edge should be dropped"
);
}
#[test]
fn same_confidence_different_provenance_both_kept() {
let e1 = make_edge(
"a",
"run",
"b",
"helper",
Confidence::Exact,
Provenance::ScopeGraph,
20,
);
let e2 = make_edge(
"a",
"run",
"b",
"helper",
Confidence::Exact,
Provenance::FfiBridge,
20,
);
let g1 = CodeGraph {
symbols: vec![],
edges: vec![e1],
};
let g2 = CodeGraph {
symbols: vec![],
edges: vec![e2],
};
let resolver = LayeredResolver::new(vec![stub(g1), stub(g2)]);
let merged = resolver.resolve(&[]);
let call_edges: Vec<_> = merged
.edges
.iter()
.filter(|e| e.role == RefRole::Call)
.collect();
assert_eq!(
call_edges.len(),
2,
"expected 2 edges (distinct provenance at same confidence); got {}: {:?}",
call_edges.len(),
call_edges
.iter()
.map(|e| format!("{:?}/{:?}", e.confidence, e.provenance))
.collect::<Vec<_>>()
);
let provenances: HashSet<Provenance> = call_edges.iter().map(|e| e.provenance).collect();
assert!(
provenances.contains(&Provenance::ScopeGraph),
"ScopeGraph provenance must be present"
);
assert!(
provenances.contains(&Provenance::FfiBridge),
"FfiBridge provenance must be present"
);
}
#[test]
fn symbols_deduplicated_across_layers() {
let sym_a = make_symbol("util", "helper");
let sym_b = make_symbol("main", "run");
let g1 = CodeGraph {
symbols: vec![sym_a.clone(), sym_b.clone()],
edges: vec![],
};
let sym_c = make_symbol("extra", "other");
let g2 = CodeGraph {
symbols: vec![sym_a.clone(), sym_c.clone()],
edges: vec![],
};
let resolver = LayeredResolver::new(vec![stub(g1), stub(g2)]);
let merged = resolver.resolve(&[]);
assert_eq!(
merged.symbols.len(),
3,
"expected 3 unique symbols, got {}: {:?}",
merged.symbols.len(),
merged
.symbols
.iter()
.map(|s| s.id.to_scip_string())
.collect::<Vec<_>>()
);
let scip_strings: HashSet<String> = merged
.symbols
.iter()
.map(|s| s.id.to_scip_string())
.collect();
assert!(
scip_strings.contains(&sym_a.id.to_scip_string()),
"helper must be present"
);
assert!(
scip_strings.contains(&sym_b.id.to_scip_string()),
"run must be present"
);
assert!(
scip_strings.contains(&sym_c.id.to_scip_string()),
"other must be present"
);
}
#[test]
fn exact_duplicate_edge_collapsed_to_one() {
let e = make_edge(
"a",
"run",
"b",
"helper",
Confidence::Scoped,
Provenance::SymbolTable,
5,
);
let g1 = CodeGraph {
symbols: vec![],
edges: vec![e.clone()],
};
let g2 = CodeGraph {
symbols: vec![],
edges: vec![e],
};
let resolver = LayeredResolver::new(vec![stub(g1), stub(g2)]);
let merged = resolver.resolve(&[]);
assert_eq!(
merged.edges.len(),
1,
"exact duplicate edge must be collapsed to one"
);
}
}