use crate::index::manifest::ManifestResult;
use crate::index::symbol::{Symbol, Visibility};
#[derive(Debug, Clone)]
pub struct RegisteredPackage {
pub package_name: String,
pub namespace: String,
pub version: String,
pub manifest: String,
}
#[derive(Debug, Clone)]
pub struct PendingRef {
pub id: String,
pub namespace: String,
pub source_node: String,
pub target_name: String,
pub package_hint: Option<String>,
pub ref_kind: String,
pub file_path: Option<String>,
pub line: Option<usize>,
}
#[derive(Debug, Clone)]
pub struct CrossRepoEdge {
pub id: String,
pub source: String,
pub target: String,
pub relationship: String,
pub confidence: f64,
pub source_namespace: String,
pub target_namespace: String,
}
#[derive(Debug, Default)]
pub struct LinkResult {
pub packages_registered: usize,
pub forward_edges: Vec<CrossRepoEdge>,
pub backward_edges: Vec<CrossRepoEdge>,
pub resolved_ref_ids: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct SymbolMatch {
pub qualified_name: String,
pub visibility: Visibility,
pub kind: String,
}
pub fn extract_packages(manifests: &ManifestResult, namespace: &str) -> Vec<RegisteredPackage> {
manifests
.packages
.iter()
.map(|(name, manifest_path)| {
let version = manifests
.dependencies
.iter()
.find(|d| d.name == *name)
.map(|d| d.version.clone())
.unwrap_or_default();
RegisteredPackage {
package_name: name.clone(),
namespace: namespace.to_string(),
version,
manifest: manifest_path.clone(),
}
})
.collect()
}
pub fn forward_link(
namespace: &str,
pending_refs: &[PendingRef],
registry: &[RegisteredPackage],
resolve_fn: &dyn Fn(&str, &str) -> Vec<SymbolMatch>,
) -> LinkResult {
let mut result = LinkResult::default();
for pending_ref in pending_refs {
if pending_ref.namespace != namespace {
continue;
}
let package_hint = match &pending_ref.package_hint {
Some(hint) => hint,
None => continue,
};
let matching_entries: Vec<&RegisteredPackage> = registry
.iter()
.filter(|entry| entry.package_name == *package_hint && entry.namespace != namespace)
.collect();
let mut best_edge: Option<(CrossRepoEdge, f64)> = None;
for entry in matching_entries {
let matches = resolve_fn(&entry.namespace, &pending_ref.target_name);
if let Some(best) = pick_best_match(&matches) {
let confidence = match_confidence_for_symbol(best);
if best_edge.as_ref().is_none_or(|(_, c)| confidence > *c) {
best_edge = Some((
CrossRepoEdge {
id: make_edge_id(
namespace,
&pending_ref.source_node,
&entry.namespace,
&best.qualified_name,
),
source: pending_ref.source_node.clone(),
target: format!("sym:{}", best.qualified_name),
relationship: ref_kind_to_relationship(&pending_ref.ref_kind)
.to_string(),
confidence,
source_namespace: namespace.to_string(),
target_namespace: entry.namespace.clone(),
},
confidence,
));
}
}
}
if let Some((edge, _)) = best_edge {
result.forward_edges.push(edge);
result.resolved_ref_ids.push(pending_ref.id.clone());
}
}
result
}
pub fn backward_link(
namespace: &str,
package_names: &[String],
pending_refs_for_packages: &[PendingRef],
symbols: &[Symbol],
) -> LinkResult {
let mut result = LinkResult::default();
for pending_ref in pending_refs_for_packages {
if pending_ref.namespace == namespace {
continue;
}
let Some(ref hint) = pending_ref.package_hint else {
continue;
};
if !package_names.iter().any(|p| p == hint) {
continue;
}
if let Some((qualified_name, confidence)) = match_symbol(&pending_ref.target_name, symbols)
{
let edge = CrossRepoEdge {
id: make_edge_id(
&pending_ref.namespace,
&pending_ref.source_node,
namespace,
&qualified_name,
),
source: pending_ref.source_node.clone(),
target: format!("sym:{qualified_name}"),
relationship: ref_kind_to_relationship(&pending_ref.ref_kind).to_string(),
confidence,
source_namespace: pending_ref.namespace.clone(),
target_namespace: namespace.to_string(),
};
result.backward_edges.push(edge);
result.resolved_ref_ids.push(pending_ref.id.clone());
}
}
result
}
pub fn match_symbol(target_name: &str, symbols: &[Symbol]) -> Option<(String, f64)> {
if let Some(sym) = symbols.iter().find(|s| s.qualified_name == target_name) {
let boost = visibility_boost(sym.visibility);
return Some((sym.qualified_name.clone(), (1.0 + boost).min(1.0)));
}
let suffix_matches: Vec<&Symbol> = symbols
.iter()
.filter(|s| {
let qn = &s.qualified_name;
qn.ends_with(target_name)
&& (qn.len() == target_name.len()
|| qn[..qn.len() - target_name.len()].ends_with('.')
|| qn[..qn.len() - target_name.len()].ends_with("::"))
})
.collect();
if !suffix_matches.is_empty() {
let public_matches: Vec<&&Symbol> = suffix_matches
.iter()
.filter(|s| s.visibility == Visibility::Public)
.collect();
let best = if !public_matches.is_empty() {
public_matches
.iter()
.min_by_key(|s| s.qualified_name.len())
.unwrap()
} else {
suffix_matches
.iter()
.min_by_key(|s| s.qualified_name.len())
.unwrap()
};
let boost = visibility_boost(best.visibility);
return Some((best.qualified_name.clone(), (0.85 + boost).min(1.0)));
}
let simple_name = simple_name_of(target_name);
let name_matches: Vec<&Symbol> = symbols.iter().filter(|s| s.name == simple_name).collect();
if !name_matches.is_empty() {
let best = pick_best_by_visibility(&name_matches);
let boost = visibility_boost(best.visibility);
return Some((best.qualified_name.clone(), (0.7 + boost).min(1.0)));
}
None
}
fn make_edge_id(src_ns: &str, src_sym: &str, dst_ns: &str, dst_sym: &str) -> String {
format!("xref:{src_ns}/{src_sym}->{dst_ns}/{dst_sym}")
}
fn ref_kind_to_relationship(ref_kind: &str) -> &str {
match ref_kind {
"call" => "Calls",
"import" => "Imports",
"inherits" => "Inherits",
"implements" => "Implements",
"type_usage" => "DependsOn",
_ => "RelatesTo",
}
}
fn simple_name_of(name: &str) -> &str {
name.rsplit("::")
.next()
.unwrap_or(name)
.rsplit('.')
.next()
.unwrap_or(name)
}
fn visibility_boost(vis: Visibility) -> f64 {
match vis {
Visibility::Public => 0.05,
Visibility::Crate => 0.02,
Visibility::Protected => 0.01,
Visibility::Private => 0.0,
}
}
fn pick_best_by_visibility<'a>(candidates: &[&'a Symbol]) -> &'a Symbol {
candidates
.iter()
.max_by(|a, b| {
let vis_ord = visibility_rank(a.visibility).cmp(&visibility_rank(b.visibility));
vis_ord.then_with(|| b.qualified_name.len().cmp(&a.qualified_name.len()))
})
.unwrap()
}
fn visibility_rank(vis: Visibility) -> u8 {
match vis {
Visibility::Public => 4,
Visibility::Crate => 3,
Visibility::Protected => 2,
Visibility::Private => 1,
}
}
fn pick_best_match(matches: &[SymbolMatch]) -> Option<&SymbolMatch> {
matches.iter().max_by(|a, b| {
let va = visibility_rank(a.visibility);
let vb = visibility_rank(b.visibility);
va.cmp(&vb)
.then_with(|| b.qualified_name.len().cmp(&a.qualified_name.len()))
})
}
fn match_confidence_for_symbol(m: &SymbolMatch) -> f64 {
0.85 + visibility_boost(m.visibility)
}
#[cfg(test)]
#[path = "tests/linker_tests.rs"]
mod tests;