use std::collections::{HashMap, HashSet};
use crate::graph::types::{
CodeGraph, Confidence, Edge, FileFacts, Provenance, RefRole, Symbol, SymbolKind,
};
use crate::symbol::SymbolId;
use super::Resolver;
use super::enclosing_symbol_index;
#[derive(Debug, Default, Clone, Copy)]
pub struct ConformanceResolver;
fn member_of_type(sym: &Symbol) -> Option<(String /* type */, String /* member */)> {
if !matches!(
sym.kind,
SymbolKind::Method | SymbolKind::Const | SymbolKind::Static
) {
return None;
}
let mut second_last: Option<&str> = None;
let mut last: Option<&str> = None;
for name in sym.id.descriptor_names_iter() {
second_last = last;
last = Some(name);
}
match (second_last, last) {
(Some(type_name), Some(member)) => Some((type_name.to_owned(), member.to_owned())),
_ => None,
}
}
impl Resolver for ConformanceResolver {
fn resolve(&self, files: &[FileFacts]) -> CodeGraph {
let symbols: Vec<Symbol> = files
.iter()
.flat_map(|f| f.symbols.iter().cloned())
.collect();
let mut by_file: HashMap<&str, Vec<usize>> = HashMap::new();
for (i, s) in symbols.iter().enumerate() {
by_file.entry(s.file.as_str()).or_default().push(i);
}
let mut members: HashMap<String, HashMap<String, SymbolId>> = HashMap::new();
for s in &symbols {
if let Some((type_name, member)) = member_of_type(s) {
members
.entry(type_name)
.or_default()
.entry(member)
.or_insert_with(|| s.id.clone());
}
}
let mut supertypes: HashMap<String, Vec<String>> = HashMap::new();
for f in files {
let file_syms = by_file.get(f.file.as_str());
for r in &f.references {
if r.role != RefRole::IsImplementation {
continue;
}
let Some(from_idx) =
file_syms.and_then(|idxs| enclosing_symbol_index(&symbols, idxs, r.occ.byte))
else {
continue;
};
let Some(impl_type) = symbols[from_idx].id.leaf_name().map(|s| s.to_owned()) else {
continue;
};
supertypes
.entry(impl_type)
.or_default()
.push(r.name.clone());
}
}
let mut edges: Vec<Edge> = Vec::new();
for f in files {
let file_syms = by_file.get(f.file.as_str());
for r in &f.references {
if !matches!(r.role, RefRole::Call | RefRole::TypeRef) {
continue;
}
let Some(qualifier) = r.qualifier.as_deref() else {
continue; };
let Some(type_name) = qualifier.split(['.', '/', ':']).rfind(|s| {
!s.is_empty() && !matches!(*s, "." | ".." | "crate" | "self" | "super")
}) else {
continue;
};
let member = r.name.as_str();
if members
.get(type_name)
.is_some_and(|m| m.contains_key(member))
{
continue;
}
let Some(inherited) = find_inherited(type_name, member, &members, &supertypes)
else {
continue;
};
let Some(from_idx) =
file_syms.and_then(|idxs| enclosing_symbol_index(&symbols, idxs, r.occ.byte))
else {
continue;
};
edges.push(Edge {
from: symbols[from_idx].id.clone(),
to: inherited,
role: r.role,
confidence: Confidence::Scoped,
provenance: Provenance::Conformance,
occ: r.occ.clone(),
});
}
}
CodeGraph { symbols, edges }
}
}
fn find_inherited(
type_name: &str,
member: &str,
members: &HashMap<String, HashMap<String, SymbolId>>,
supertypes: &HashMap<String, Vec<String>>,
) -> Option<SymbolId> {
let mut visited: HashSet<String> = HashSet::new();
visited.insert(type_name.to_owned());
let mut stack: Vec<String> = supertypes
.get(type_name)
.map(|v| v.iter().rev().cloned().collect())
.unwrap_or_default();
while let Some(ancestor) = stack.pop() {
if !visited.insert(ancestor.clone()) {
continue;
}
if let Some(id) = members.get(&ancestor).and_then(|m| m.get(member)) {
return Some(id.clone());
}
if let Some(parents) = supertypes.get(&ancestor) {
for p in parents.iter().rev() {
if !visited.contains(p) {
stack.push(p.clone());
}
}
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
use crate::extract::{Extractor, JavaExtractor, RustExtractor};
use crate::graph::types::{Occurrence, Reference};
fn qualified_call(name: &str, qualifier: &str, file: &str, byte: usize) -> Reference {
Reference {
name: name.to_owned(),
occ: Occurrence {
file: file.to_owned(),
line: 1,
col: 0,
byte,
},
role: RefRole::Call,
source_module: None,
from_path: None,
qualifier: Some(qualifier.to_owned()),
scope: None,
type_ref_ctx: None,
}
}
#[test]
fn java_inherited_method_resolves_via_conformance() {
let base = JavaExtractor
.extract(
"package p; public class Base { public void process() {} }",
"src/p/Base.java",
)
.unwrap();
let sub = JavaExtractor
.extract(
"package p; public class Sub extends Base {}",
"src/p/Sub.java",
)
.unwrap();
let mut caller = JavaExtractor
.extract(
"package p; public class Caller { public void run() {} }",
"src/p/Caller.java",
)
.unwrap();
let run = caller
.symbols
.iter()
.find(|s| s.name == "run")
.expect("run method symbol");
let byte = run.span.start;
caller
.references
.push(qualified_call("process", "Sub", "src/p/Caller.java", byte));
let graph = ConformanceResolver.resolve(&[base, sub, caller]);
let conf_edges: Vec<_> = graph
.edges
.iter()
.filter(|e| e.provenance == Provenance::Conformance)
.collect();
assert_eq!(
conf_edges.len(),
1,
"expected exactly one conformance edge, got {:?}",
conf_edges
.iter()
.map(|e| e.to.to_scip_string())
.collect::<Vec<_>>()
);
let e = conf_edges[0];
assert_eq!(e.role, RefRole::Call);
assert_eq!(e.confidence, Confidence::Scoped);
assert_eq!(e.provenance, Provenance::Conformance);
assert!(
e.to.to_scip_string().ends_with("Base#process()."),
"edge `to` should be the inherited Base#process(), got: {}",
e.to.to_scip_string()
);
assert!(
e.from.to_scip_string().ends_with("Caller#run()."),
"edge `from` should be the enclosing caller method, got: {}",
e.from.to_scip_string()
);
}
#[test]
fn java_multi_level_inheritance_walks_chain() {
let root = JavaExtractor
.extract(
"package p; public class Root { public void process() {} }",
"src/p/Root.java",
)
.unwrap();
let base = JavaExtractor
.extract(
"package p; public class Base extends Root {}",
"src/p/Base.java",
)
.unwrap();
let sub = JavaExtractor
.extract(
"package p; public class Sub extends Base {}",
"src/p/Sub.java",
)
.unwrap();
let mut caller = JavaExtractor
.extract(
"package p; public class Caller { public void run() {} }",
"src/p/Caller.java",
)
.unwrap();
let byte = caller
.symbols
.iter()
.find(|s| s.name == "run")
.expect("run symbol")
.span
.start;
caller
.references
.push(qualified_call("process", "Sub", "src/p/Caller.java", byte));
let graph = ConformanceResolver.resolve(&[root, base, sub, caller]);
let conf_edges: Vec<_> = graph
.edges
.iter()
.filter(|e| e.provenance == Provenance::Conformance)
.collect();
assert_eq!(conf_edges.len(), 1, "expected one conformance edge");
assert!(
conf_edges[0]
.to
.to_scip_string()
.ends_with("Root#process()."),
"should climb two levels to Root#process(), got: {}",
conf_edges[0].to.to_scip_string()
);
}
#[test]
fn direct_member_does_not_emit_conformance_edge() {
let base = JavaExtractor
.extract(
"package p; public class Base { public void process() {} }",
"src/p/Base.java",
)
.unwrap();
let mut caller = JavaExtractor
.extract(
"package p; public class Caller { public void run() {} }",
"src/p/Caller.java",
)
.unwrap();
let byte = caller
.symbols
.iter()
.find(|s| s.name == "run")
.expect("run symbol")
.span
.start;
caller
.references
.push(qualified_call("process", "Base", "src/p/Caller.java", byte));
let graph = ConformanceResolver.resolve(&[base, caller]);
let conf_edges: Vec<_> = graph
.edges
.iter()
.filter(|e| e.provenance == Provenance::Conformance)
.collect();
assert!(
conf_edges.is_empty(),
"direct member must not yield a conformance edge, got {:?}",
conf_edges
.iter()
.map(|e| e.to.to_scip_string())
.collect::<Vec<_>>()
);
}
#[test]
fn conformance_resolves_rust_inherited_trait_method_end_to_end() {
let greet = RustExtractor
.extract("pub trait Greet { fn hello(&self); }", "src/greet.rs")
.unwrap();
let person = RustExtractor
.extract(
"pub struct Person; impl crate::greet::Greet for Person { fn hello(&self) {} }",
"src/person.rs",
)
.unwrap();
let main = RustExtractor
.extract(
"pub fn run(p: &Person) { Person::hello(p); }",
"src/main.rs",
)
.unwrap();
let graph = ConformanceResolver.resolve(&[greet, person, main]);
let conf_edges: Vec<_> = graph
.edges
.iter()
.filter(|e| e.provenance == Provenance::Conformance)
.collect();
let to_hello: Vec<_> = conf_edges
.iter()
.filter(|e| e.to.to_scip_string().ends_with("Greet#hello()."))
.collect();
assert!(
!to_hello.is_empty(),
"expected a conformance edge to Greet#hello()., got conformance edges: {:?}",
conf_edges
.iter()
.map(|e| format!("{} -> {}", e.from.to_scip_string(), e.to.to_scip_string()))
.collect::<Vec<_>>()
);
let e = to_hello[0];
assert_eq!(
e.role,
RefRole::Call,
"edge role should be Call, got {:?}",
e.role
);
assert_eq!(
e.confidence,
Confidence::Scoped,
"edge confidence should be Scoped, got {:?}",
e.confidence
);
assert_eq!(
e.provenance,
Provenance::Conformance,
"edge provenance should be Conformance, got {:?}",
e.provenance
);
assert!(
e.from.to_scip_string().ends_with("run()."),
"edge `from` should end with 'run().', got: {}",
e.from.to_scip_string()
);
}
#[test]
fn unqualified_reference_is_deferred() {
let base = JavaExtractor
.extract(
"package p; public class Base { public void process() {} }",
"src/p/Base.java",
)
.unwrap();
let sub = JavaExtractor
.extract(
"package p; public class Sub extends Base {}",
"src/p/Sub.java",
)
.unwrap();
let mut caller = JavaExtractor
.extract(
"package p; public class Caller { public void run() {} }",
"src/p/Caller.java",
)
.unwrap();
let byte = caller
.symbols
.iter()
.find(|s| s.name == "run")
.expect("run symbol")
.span
.start;
let mut unq = qualified_call("process", "Sub", "src/p/Caller.java", byte);
unq.qualifier = None;
caller.references.push(unq);
let graph = ConformanceResolver.resolve(&[base, sub, caller]);
assert!(
graph
.edges
.iter()
.all(|e| e.provenance != Provenance::Conformance),
"unqualified ref must not produce a conformance edge"
);
}
}