use std::collections::{HashMap, HashSet};
use rayon::prelude::*;
use crate::db::Database;
use crate::types::*;
fn lang_from_path(path: &str) -> &'static str {
match path.rsplit('.').next().unwrap_or("") {
"rs" => "rust",
"go" => "go",
"py" | "pyi" => "python",
"js" | "jsx" | "mjs" | "cjs" => "javascript",
"ts" | "tsx" | "mts" | "cts" => "typescript",
"java" => "java",
"kt" | "kts" => "kotlin",
"swift" => "swift",
"c" | "h" => "c",
"cpp" | "cc" | "cxx" | "hpp" | "hxx" | "hh" => "cpp",
"cs" => "csharp",
"rb" => "ruby",
"php" => "php",
"scala" | "sc" => "scala",
"dart" => "dart",
"lua" => "lua",
"pl" | "pm" => "perl",
"sh" | "bash" => "bash",
"nix" => "nix",
"zig" => "zig",
"proto" => "proto",
_ => "unknown",
}
}
fn path_proximity(a: &str, b: &str) -> i64 {
let seg_a: Vec<&str> = a.split('/').collect();
let seg_b: Vec<&str> = b.split('/').collect();
let shared = seg_a
.iter()
.zip(seg_b.iter())
.take_while(|(x, y)| x == y)
.count();
(shared as i64 * 5).min(40)
}
pub struct ReferenceResolver<'a> {
#[allow(dead_code)]
db: &'a Database,
name_cache: HashMap<String, Vec<Node>>,
qualified_name_cache: HashMap<String, Vec<Node>>,
suffix_cache: HashMap<String, Vec<String>>,
known_names: HashSet<String>,
}
impl<'a> ReferenceResolver<'a> {
pub async fn new(db: &'a Database) -> Self {
let all_nodes = db.get_all_nodes().await.unwrap_or_default();
Self::from_nodes(db, &all_nodes)
}
pub fn from_nodes(db: &'a Database, all_nodes: &[Node]) -> Self {
let mut name_cache: HashMap<String, Vec<Node>> = HashMap::new();
let mut qualified_name_cache: HashMap<String, Vec<Node>> = HashMap::new();
let mut suffix_cache: HashMap<String, Vec<String>> = HashMap::new();
for node in all_nodes {
name_cache
.entry(node.name.clone())
.or_default()
.push(node.clone());
let qn = &node.qualified_name;
qualified_name_cache
.entry(qn.clone())
.or_default()
.push(node.clone());
let mut pos = 0;
while let Some(idx) = qn[pos..].find("::") {
let suffix = &qn[pos + idx + 2..];
if !suffix.is_empty() {
suffix_cache
.entry(suffix.to_string())
.or_default()
.push(qn.clone());
}
pos += idx + 2;
}
}
for entries in suffix_cache.values_mut() {
entries.sort_unstable();
entries.dedup();
}
let mut known_names: HashSet<String> = HashSet::new();
for key in name_cache.keys() {
known_names.insert(key.clone());
}
for key in qualified_name_cache.keys() {
known_names.insert(key.clone());
}
for key in suffix_cache.keys() {
known_names.insert(key.clone());
}
Self {
db,
name_cache,
qualified_name_cache,
suffix_cache,
known_names,
}
}
pub fn resolve_one(&self, uref: &UnresolvedRef) -> Option<ResolvedRef> {
if uref.reference_name.contains("::") {
if let Some(resolved) = self.try_qualified_match(uref) {
return Some(resolved);
}
}
self.try_exact_name_match(uref)
}
fn is_known_name(&self, name: &str) -> bool {
self.known_names.contains(name)
}
pub fn resolve_all(&self, refs: &[UnresolvedRef]) -> ResolutionResult {
let total = refs.len();
let (candidates, hopeless): (Vec<_>, Vec<_>) = refs
.iter()
.partition(|uref| self.is_known_name(&uref.reference_name));
let results: Vec<_> = candidates
.par_iter()
.map(|uref| (*uref, self.resolve_one(uref)))
.collect();
let mut resolved = Vec::new();
let mut unresolved: Vec<UnresolvedRef> = hopeless.into_iter().cloned().collect();
for (uref, res) in results {
match res {
Some(r) => resolved.push(r),
None => unresolved.push(uref.clone()),
}
}
let resolved_count = resolved.len();
ResolutionResult {
resolved,
unresolved,
total,
resolved_count,
}
}
pub fn create_edges(&self, resolved: &[ResolvedRef]) -> Vec<Edge> {
resolved
.iter()
.map(|r| Edge {
source: r.original.from_node_id.clone(),
target: r.target_node_id.clone(),
kind: r.original.reference_kind.clone(),
line: Some(r.original.line),
})
.collect()
}
fn try_qualified_match(&self, uref: &UnresolvedRef) -> Option<ResolvedRef> {
if let Some(candidates) = self.qualified_name_cache.get(&uref.reference_name) {
if let Some(node) = candidates.first() {
return Some(ResolvedRef {
original: uref.clone(),
target_node_id: node.id.clone(),
confidence: 0.95,
resolved_by: "qualified-match".to_string(),
});
}
}
if let Some(full_names) = self.suffix_cache.get(&uref.reference_name) {
for full_name in full_names {
if let Some(candidates) = self.qualified_name_cache.get(full_name) {
if let Some(node) = candidates.first() {
return Some(ResolvedRef {
original: uref.clone(),
target_node_id: node.id.clone(),
confidence: 0.95,
resolved_by: "qualified-match".to_string(),
});
}
}
}
}
None
}
fn try_exact_name_match(&self, uref: &UnresolvedRef) -> Option<ResolvedRef> {
let candidates = self.name_cache.get(&uref.reference_name)?;
if candidates.len() == 1 {
let ref_lang = lang_from_path(&uref.file_path);
let candidate_lang = lang_from_path(&candidates[0].file_path);
let confidence = if ref_lang != "unknown"
&& candidate_lang != "unknown"
&& ref_lang != candidate_lang
{
0.5
} else {
0.9
};
return Some(ResolvedRef {
original: uref.clone(),
target_node_id: candidates[0].id.clone(),
confidence,
resolved_by: "exact-match".to_string(),
});
}
let best = self.find_best_match(uref, candidates)?;
Some(ResolvedRef {
original: uref.clone(),
target_node_id: best.id.clone(),
confidence: 0.7,
resolved_by: "exact-match".to_string(),
})
}
fn find_best_match(&self, uref: &UnresolvedRef, candidates: &[Node]) -> Option<Node> {
if candidates.is_empty() {
return None;
}
let ref_lang = lang_from_path(&uref.file_path);
let mut best_score = i64::MIN;
let mut best_node: Option<&Node> = None;
for node in candidates {
let mut score: i64 = 0;
if node.file_path == uref.file_path {
score += 100;
let distance = node.start_line.abs_diff(uref.line);
let proximity = 20_i64.saturating_sub(i64::from(distance) / 10);
score += proximity.max(0);
} else {
score += path_proximity(&uref.file_path, &node.file_path);
}
let candidate_lang = lang_from_path(&node.file_path);
if ref_lang != "unknown" && candidate_lang != "unknown" {
if ref_lang == candidate_lang {
score += 50;
} else {
score -= 80;
}
}
if node.visibility == Visibility::Pub {
score += 10;
}
if uref.reference_kind == EdgeKind::Calls
&& matches!(
node.kind,
NodeKind::Function
| NodeKind::Method
| NodeKind::StructMethod
| NodeKind::Constructor
| NodeKind::AbstractMethod
)
{
score += 25;
}
if score > best_score {
best_score = score;
best_node = Some(node);
}
}
best_node.cloned()
}
}