1use crate::index::manifest::ManifestResult;
5use crate::index::symbol::{Symbol, Visibility};
6
7#[derive(Debug, Clone)]
9pub struct RegisteredPackage {
10 pub package_name: String,
11 pub namespace: String,
12 pub version: String,
13 pub manifest: String,
14}
15
16#[derive(Debug, Clone)]
18pub struct PendingRef {
19 pub id: String,
20 pub namespace: String,
21 pub source_node: String,
22 pub target_name: String,
23 pub package_hint: Option<String>,
24 pub ref_kind: String,
25 pub file_path: Option<String>,
26 pub line: Option<usize>,
27}
28
29#[derive(Debug, Clone)]
31pub struct CrossRepoEdge {
32 pub id: String,
34 pub source: String,
36 pub target: String,
38 pub relationship: String,
40 pub confidence: f64,
42 pub source_namespace: String,
44 pub target_namespace: String,
46}
47
48#[derive(Debug, Default)]
50pub struct LinkResult {
51 pub packages_registered: usize,
53 pub forward_edges: Vec<CrossRepoEdge>,
55 pub backward_edges: Vec<CrossRepoEdge>,
57 pub resolved_ref_ids: Vec<String>,
59}
60
61#[derive(Debug, Clone)]
63pub struct SymbolMatch {
64 pub qualified_name: String,
65 pub visibility: Visibility,
66 pub kind: String,
67}
68
69pub fn extract_packages(manifests: &ManifestResult, namespace: &str) -> Vec<RegisteredPackage> {
71 manifests
72 .packages
73 .iter()
74 .map(|(name, manifest_path)| {
75 let version = manifests
77 .dependencies
78 .iter()
79 .find(|d| d.name == *name)
80 .map(|d| d.version.clone())
81 .unwrap_or_default();
82 RegisteredPackage {
83 package_name: name.clone(),
84 namespace: namespace.to_string(),
85 version,
86 manifest: manifest_path.clone(),
87 }
88 })
89 .collect()
90}
91
92pub fn forward_link(
98 namespace: &str,
99 pending_refs: &[PendingRef],
100 registry: &[RegisteredPackage],
101 resolve_fn: &dyn Fn(&str, &str) -> Vec<SymbolMatch>,
102) -> LinkResult {
103 let mut result = LinkResult::default();
104
105 for pending_ref in pending_refs {
106 if pending_ref.namespace != namespace {
108 continue;
109 }
110
111 let package_hint = match &pending_ref.package_hint {
113 Some(hint) => hint,
114 None => continue,
115 };
116
117 let matching_entries: Vec<&RegisteredPackage> = registry
119 .iter()
120 .filter(|entry| entry.package_name == *package_hint && entry.namespace != namespace)
121 .collect();
122
123 let mut best_edge: Option<(CrossRepoEdge, f64)> = None;
126 for entry in matching_entries {
127 let matches = resolve_fn(&entry.namespace, &pending_ref.target_name);
128 if let Some(best) = pick_best_match(&matches) {
129 let confidence = match_confidence_for_symbol(best);
130 if best_edge.as_ref().is_none_or(|(_, c)| confidence > *c) {
131 best_edge = Some((
132 CrossRepoEdge {
133 id: make_edge_id(
134 namespace,
135 &pending_ref.source_node,
136 &entry.namespace,
137 &best.qualified_name,
138 ),
139 source: pending_ref.source_node.clone(),
140 target: format!("sym:{}", best.qualified_name),
141 relationship: ref_kind_to_relationship(&pending_ref.ref_kind)
142 .to_string(),
143 confidence,
144 source_namespace: namespace.to_string(),
145 target_namespace: entry.namespace.clone(),
146 },
147 confidence,
148 ));
149 }
150 }
151 }
152 if let Some((edge, _)) = best_edge {
153 result.forward_edges.push(edge);
154 result.resolved_ref_ids.push(pending_ref.id.clone());
155 }
156 }
157
158 result
159}
160
161pub fn backward_link(
166 namespace: &str,
167 package_names: &[String],
168 pending_refs_for_packages: &[PendingRef],
169 symbols: &[Symbol],
170) -> LinkResult {
171 let mut result = LinkResult::default();
172
173 for pending_ref in pending_refs_for_packages {
174 if pending_ref.namespace == namespace {
176 continue;
177 }
178
179 let Some(ref hint) = pending_ref.package_hint else {
183 continue;
184 };
185 if !package_names.iter().any(|p| p == hint) {
186 continue;
187 }
188
189 if let Some((qualified_name, confidence)) = match_symbol(&pending_ref.target_name, symbols)
190 {
191 let edge = CrossRepoEdge {
192 id: make_edge_id(
193 &pending_ref.namespace,
194 &pending_ref.source_node,
195 namespace,
196 &qualified_name,
197 ),
198 source: pending_ref.source_node.clone(),
199 target: format!("sym:{qualified_name}"),
200 relationship: ref_kind_to_relationship(&pending_ref.ref_kind).to_string(),
201 confidence,
202 source_namespace: pending_ref.namespace.clone(),
203 target_namespace: namespace.to_string(),
204 };
205 result.backward_edges.push(edge);
206 result.resolved_ref_ids.push(pending_ref.id.clone());
207 }
208 }
209
210 result
211}
212
213pub fn match_symbol(target_name: &str, symbols: &[Symbol]) -> Option<(String, f64)> {
221 if let Some(sym) = symbols.iter().find(|s| s.qualified_name == target_name) {
223 let boost = visibility_boost(sym.visibility);
224 return Some((sym.qualified_name.clone(), (1.0 + boost).min(1.0)));
225 }
226
227 let suffix_matches: Vec<&Symbol> = symbols
229 .iter()
230 .filter(|s| {
231 let qn = &s.qualified_name;
233 qn.ends_with(target_name)
234 && (qn.len() == target_name.len()
235 || qn[..qn.len() - target_name.len()].ends_with('.')
236 || qn[..qn.len() - target_name.len()].ends_with("::"))
237 })
238 .collect();
239
240 if !suffix_matches.is_empty() {
241 let public_matches: Vec<&&Symbol> = suffix_matches
243 .iter()
244 .filter(|s| s.visibility == Visibility::Public)
245 .collect();
246
247 let best = if !public_matches.is_empty() {
248 public_matches
249 .iter()
250 .min_by_key(|s| s.qualified_name.len())
251 .unwrap()
252 } else {
253 suffix_matches
254 .iter()
255 .min_by_key(|s| s.qualified_name.len())
256 .unwrap()
257 };
258
259 let boost = visibility_boost(best.visibility);
260 return Some((best.qualified_name.clone(), (0.85 + boost).min(1.0)));
261 }
262
263 let simple_name = simple_name_of(target_name);
265 let name_matches: Vec<&Symbol> = symbols.iter().filter(|s| s.name == simple_name).collect();
266
267 if !name_matches.is_empty() {
268 let best = pick_best_by_visibility(&name_matches);
269 let boost = visibility_boost(best.visibility);
270 return Some((best.qualified_name.clone(), (0.7 + boost).min(1.0)));
271 }
272
273 None
274}
275
276fn make_edge_id(src_ns: &str, src_sym: &str, dst_ns: &str, dst_sym: &str) -> String {
278 format!("xref:{src_ns}/{src_sym}->{dst_ns}/{dst_sym}")
279}
280
281fn ref_kind_to_relationship(ref_kind: &str) -> &str {
283 match ref_kind {
284 "call" => "Calls",
285 "import" => "Imports",
286 "inherits" => "Inherits",
287 "implements" => "Implements",
288 "type_usage" => "DependsOn",
289 _ => "RelatesTo",
290 }
291}
292
293fn simple_name_of(name: &str) -> &str {
297 name.rsplit("::")
299 .next()
300 .unwrap_or(name)
301 .rsplit('.')
302 .next()
303 .unwrap_or(name)
304}
305
306fn visibility_boost(vis: Visibility) -> f64 {
308 match vis {
309 Visibility::Public => 0.05,
310 Visibility::Crate => 0.02,
311 Visibility::Protected => 0.01,
312 Visibility::Private => 0.0,
313 }
314}
315
316fn pick_best_by_visibility<'a>(candidates: &[&'a Symbol]) -> &'a Symbol {
318 candidates
319 .iter()
320 .max_by(|a, b| {
321 let vis_ord = visibility_rank(a.visibility).cmp(&visibility_rank(b.visibility));
322 vis_ord.then_with(|| b.qualified_name.len().cmp(&a.qualified_name.len()))
324 })
325 .unwrap()
326}
327
328fn visibility_rank(vis: Visibility) -> u8 {
330 match vis {
331 Visibility::Public => 4,
332 Visibility::Crate => 3,
333 Visibility::Protected => 2,
334 Visibility::Private => 1,
335 }
336}
337
338fn pick_best_match(matches: &[SymbolMatch]) -> Option<&SymbolMatch> {
340 matches.iter().max_by(|a, b| {
341 let va = visibility_rank(a.visibility);
342 let vb = visibility_rank(b.visibility);
343 va.cmp(&vb)
344 .then_with(|| b.qualified_name.len().cmp(&a.qualified_name.len()))
345 })
346}
347
348fn match_confidence_for_symbol(m: &SymbolMatch) -> f64 {
350 0.85 + visibility_boost(m.visibility)
351}
352
353#[cfg(test)]
354#[path = "tests/linker_tests.rs"]
355mod tests;