use crate::models::{ClassInfo, FileInfo, InterfaceInfo, TypeRelationship};
use std::collections::{HashMap, HashSet};
pub struct ImplementsEdge {
pub type_name: String,
pub interface_name: String,
}
pub struct ExtendsEdge {
pub child_name: String,
pub parent_name: String,
}
pub struct HasMethodEdge {
pub owner: String,
pub method: String,
}
pub struct ExternalNode {
pub qualified_name: String,
pub name: String,
}
pub struct TypeEdgeOutput {
pub implements: Vec<ImplementsEdge>,
pub extends: Vec<ExtendsEdge>,
pub has_method: Vec<HasMethodEdge>,
pub external_traits: Vec<ExternalNode>,
pub external_classes: Vec<ExternalNode>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Kind {
Class,
Interface,
}
struct TypeIndex {
by_name: HashMap<String, Vec<(String, Kind)>>,
qname_to_file: HashMap<String, String>,
file_imports: HashMap<String, Vec<String>>,
qname_to_namespace: HashMap<String, String>,
}
impl TypeIndex {
fn build(files: &[FileInfo], classes: &[ClassInfo], interfaces: &[InterfaceInfo]) -> Self {
let mut by_name: HashMap<String, Vec<(String, Kind)>> = HashMap::new();
let mut qname_to_file: HashMap<String, String> = HashMap::new();
let mut qname_to_namespace: HashMap<String, String> = HashMap::new();
for c in classes {
let kind = Kind::Class;
by_name
.entry(c.name.clone())
.or_default()
.push((c.qualified_name.clone(), kind));
qname_to_file.insert(c.qualified_name.clone(), c.file_path.clone());
qname_to_namespace.insert(c.qualified_name.clone(), namespace_of(&c.qualified_name));
}
for i in interfaces {
let kind = Kind::Interface;
by_name
.entry(i.name.clone())
.or_default()
.push((i.qualified_name.clone(), kind));
qname_to_file.insert(i.qualified_name.clone(), i.file_path.clone());
qname_to_namespace.insert(i.qualified_name.clone(), namespace_of(&i.qualified_name));
}
let mut file_imports: HashMap<String, Vec<String>> = HashMap::new();
for f in files {
if !f.imports.is_empty() {
file_imports.insert(f.path.clone(), f.imports.clone());
}
}
TypeIndex {
by_name,
qname_to_file,
file_imports,
qname_to_namespace,
}
}
fn resolve(
&self,
name: &str,
source_qname: &str,
prefer: Option<Kind>,
) -> (String, Option<Kind>) {
if let Some(file) = self.qname_to_file.get(name) {
let bare = name.rsplit_once('.').map(|(_, n)| n).unwrap_or(name);
let kind = self
.by_name
.get(bare)
.and_then(|cands| cands.iter().find(|(q, _)| q == name).map(|(_, k)| *k));
let _ = file;
return (name.to_string(), kind);
}
let Some(candidates) = self.by_name.get(name) else {
return (name.to_string(), None);
};
if candidates.len() == 1 {
let (qn, k) = &candidates[0];
return (qn.clone(), Some(*k));
}
let source_file = self.qname_to_file.get(source_qname);
let source_ns = self.qname_to_namespace.get(source_qname).cloned();
let imports: Vec<String> = source_file
.and_then(|p| self.file_imports.get(p))
.cloned()
.unwrap_or_default();
let mut in_scope: Vec<String> = imports;
if let Some(ns) = source_ns {
if !ns.is_empty() && !in_scope.contains(&ns) {
in_scope.push(ns);
}
}
let score = |qn: &str, k: Kind| -> i32 {
let mut s = 0;
if let Some(p) = prefer {
if p == k {
s += 100;
}
}
let mut best_prefix = 0usize;
for scope in &in_scope {
if qn.len() > scope.len() + 1
&& qn.starts_with(scope.as_str())
&& qn.as_bytes()[scope.len()] == b'.'
&& scope.len() > best_prefix
{
best_prefix = scope.len();
}
}
s += best_prefix as i32;
s
};
let best = candidates
.iter()
.max_by_key(|(qn, k)| score(qn.as_str(), *k))
.expect("non-empty");
(best.0.clone(), Some(best.1))
}
}
fn namespace_of(qname: &str) -> String {
if let Some((ns, _)) = qname.rsplit_once('.') {
return ns.to_string();
}
if let Some((ns, _)) = qname.rsplit_once("::") {
return ns.to_string();
}
String::new()
}
fn resolve_owner(qualified_name: &str, name_to_qname: &HashMap<String, String>) -> String {
for sep in ["::", ".", "\\"] {
if let Some(idx) = qualified_name.rfind(sep) {
let owner = &qualified_name[..idx];
let simple = owner
.rfind(sep)
.map(|i| &owner[i + sep.len()..])
.unwrap_or(owner);
return name_to_qname
.get(simple)
.cloned()
.unwrap_or_else(|| owner.to_string());
}
}
qualified_name.to_string()
}
pub fn build_type_edges(
relationships: &[TypeRelationship],
files: &[FileInfo],
classes: &[ClassInfo],
interfaces: &[InterfaceInfo],
name_to_qname: &mut HashMap<String, String>,
) -> TypeEdgeOutput {
let known_interfaces: HashSet<String> = interfaces.iter().map(|i| i.name.clone()).collect();
let known_classes: HashSet<String> = classes.iter().map(|c| c.name.clone()).collect();
let index = TypeIndex::build(files, classes, interfaces);
let mut implements: Vec<ImplementsEdge> = Vec::new();
let mut extends: Vec<ExtendsEdge> = Vec::new();
let mut has_method: Vec<HasMethodEdge> = Vec::new();
let mut seen_impl: HashSet<(String, String)> = HashSet::new();
let mut seen_ext: HashSet<(String, String)> = HashSet::new();
let mut external_traits: HashMap<String, ExternalNode> = HashMap::new();
let mut external_classes: HashMap<String, ExternalNode> = HashMap::new();
for tr in relationships {
match tr.relationship.as_str() {
"inherent" => {
for method in &tr.methods {
has_method.push(HasMethodEdge {
owner: resolve_owner(&method.qualified_name, name_to_qname),
method: method.qualified_name.clone(),
});
}
}
"implements" => {
let Some(target) = tr.target_type.as_ref() else {
continue;
};
let key = (tr.source_type.clone(), target.clone());
if seen_impl.insert(key) {
let (resolved_target, _kind) =
index.resolve(target, &tr.source_type, Some(Kind::Interface));
let resolved_source = name_to_qname
.get(&tr.source_type)
.cloned()
.unwrap_or_else(|| tr.source_type.clone());
implements.push(ImplementsEdge {
type_name: resolved_source,
interface_name: resolved_target.clone(),
});
if !known_interfaces.contains(target) {
external_traits
.entry(resolved_target.clone())
.or_insert(ExternalNode {
qualified_name: resolved_target.clone(),
name: target.clone(),
});
name_to_qname
.entry(target.clone())
.or_insert(resolved_target);
}
}
for method in &tr.methods {
has_method.push(HasMethodEdge {
owner: resolve_owner(&method.qualified_name, name_to_qname),
method: method.qualified_name.clone(),
});
}
}
"extends" => {
let Some(target) = tr.target_type.as_ref() else {
continue;
};
let (resolved_target, kind) =
index.resolve(target, &tr.source_type, Some(Kind::Class));
if matches!(kind, Some(Kind::Interface)) {
let key = (tr.source_type.clone(), target.clone());
if seen_impl.insert(key) {
let resolved_source = name_to_qname
.get(&tr.source_type)
.cloned()
.unwrap_or_else(|| tr.source_type.clone());
implements.push(ImplementsEdge {
type_name: resolved_source,
interface_name: resolved_target.clone(),
});
if !known_interfaces.contains(target) {
external_traits.entry(resolved_target.clone()).or_insert(
ExternalNode {
qualified_name: resolved_target.clone(),
name: target.clone(),
},
);
name_to_qname
.entry(target.clone())
.or_insert(resolved_target);
}
}
continue;
}
let key = (tr.source_type.clone(), target.clone());
if seen_ext.insert(key) {
let resolved_source = name_to_qname
.get(&tr.source_type)
.cloned()
.unwrap_or_else(|| tr.source_type.clone());
extends.push(ExtendsEdge {
child_name: resolved_source.clone(),
parent_name: resolved_target.clone(),
});
if !known_classes.contains(&tr.source_type) {
external_classes
.entry(resolved_source.clone())
.or_insert(ExternalNode {
qualified_name: resolved_source,
name: tr.source_type.clone(),
});
}
if !known_classes.contains(target) {
external_classes
.entry(resolved_target.clone())
.or_insert(ExternalNode {
qualified_name: resolved_target,
name: target.clone(),
});
}
}
}
_ => {}
}
}
let mut external_traits: Vec<_> = external_traits.into_values().collect();
external_traits.sort_unstable_by(|a, b| {
a.qualified_name
.cmp(&b.qualified_name)
.then_with(|| a.name.cmp(&b.name))
});
let mut external_classes: Vec<_> = external_classes.into_values().collect();
external_classes.sort_unstable_by(|a, b| {
a.qualified_name
.cmp(&b.qualified_name)
.then_with(|| a.name.cmp(&b.name))
});
TypeEdgeOutput {
implements,
extends,
has_method,
external_traits,
external_classes,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn external_nodes_are_qualified_name_sorted() {
let relationships = vec![
TypeRelationship {
source_type: "Demo".into(),
target_type: Some("Default".into()),
relationship: "implements".into(),
..TypeRelationship::default()
},
TypeRelationship {
source_type: "Demo".into(),
target_type: Some("Clone".into()),
relationship: "implements".into(),
..TypeRelationship::default()
},
];
let classes = vec![ClassInfo {
name: "Demo".into(),
qualified_name: "crate::Demo".into(),
kind: "struct".into(),
..ClassInfo::default()
}];
let mut names = HashMap::from([("Demo".into(), "crate::Demo".into())]);
let output = build_type_edges(&relationships, &[], &classes, &[], &mut names);
let traits: Vec<_> = output
.external_traits
.iter()
.map(|node| node.qualified_name.as_str())
.collect();
assert_eq!(traits, vec!["Clone", "Default"]);
}
}