codemem_engine/index/
resolver.rs1use crate::index::symbol::{Reference, ReferenceKind, Symbol};
7use codemem_core::RelationshipType;
8use std::collections::{HashMap, HashSet};
9
10#[derive(Debug, Clone)]
12pub struct ResolvedEdge {
13 pub source_qualified_name: String,
15 pub target_qualified_name: String,
17 pub relationship: RelationshipType,
19 pub file_path: String,
21 pub line: usize,
23 pub resolution_confidence: f64,
25}
26
27pub struct ReferenceResolver {
29 symbol_index: HashMap<String, Symbol>,
31 name_index: HashMap<String, Vec<String>>,
33 file_imports: HashMap<String, HashSet<String>>,
35}
36
37impl ReferenceResolver {
38 pub fn new() -> Self {
40 Self {
41 symbol_index: HashMap::new(),
42 name_index: HashMap::new(),
43 file_imports: HashMap::new(),
44 }
45 }
46
47 pub fn add_symbols(&mut self, symbols: &[Symbol]) {
49 for sym in symbols {
50 self.symbol_index
51 .insert(sym.qualified_name.clone(), sym.clone());
52
53 self.name_index
54 .entry(sym.name.clone())
55 .or_default()
56 .push(sym.qualified_name.clone());
57 }
58 }
59
60 pub fn add_imports(&mut self, references: &[Reference]) {
62 for r in references {
63 if r.kind == ReferenceKind::Import {
64 self.file_imports
65 .entry(r.file_path.clone())
66 .or_default()
67 .insert(r.target_name.clone());
68 }
69 }
70 }
71
72 pub fn resolve_with_confidence(&self, reference: &Reference) -> Option<(&Symbol, f64)> {
80 if let Some(sym) = self.symbol_index.get(&reference.target_name) {
82 return Some((sym, 1.0));
83 }
84
85 if reference.target_name.starts_with("crate::") {
87 let stripped = &reference.target_name["crate::".len()..];
88 if let Some(sym) = self.symbol_index.get(stripped) {
89 return Some((sym, 0.95));
90 }
91 for (qn, sym) in &self.symbol_index {
93 if qn.ends_with(stripped) {
94 let prefix_len = qn.len() - stripped.len();
95 if prefix_len == 0 || qn.as_bytes()[prefix_len - 1] == b':' {
96 return Some((sym, 0.85));
97 }
98 }
99 }
100 }
101
102 if reference.target_name.contains("::") {
104 let with_crate = format!("crate::{}", reference.target_name);
105 if let Some(sym) = self.symbol_index.get(&with_crate) {
106 return Some((sym, 0.9));
107 }
108 for (qn, sym) in &self.symbol_index {
110 if qn.ends_with(&reference.target_name) {
111 let prefix_len = qn.len() - reference.target_name.len();
112 if prefix_len == 0 || qn.as_bytes()[prefix_len - 1] == b':' {
113 return Some((sym, 0.8));
114 }
115 }
116 }
117 }
118
119 let simple_name = reference
121 .target_name
122 .rsplit("::")
123 .next()
124 .unwrap_or(&reference.target_name);
125
126 if let Some(candidates) = self.name_index.get(simple_name) {
127 if candidates.len() == 1 {
128 let confidence = if simple_name == reference.target_name {
130 0.9 } else {
132 0.7 };
134 return self
135 .symbol_index
136 .get(&candidates[0])
137 .map(|s| (s, confidence));
138 }
139
140 let file_imports = self.file_imports.get(&reference.file_path);
142 let mut best: Option<(&Symbol, f64)> = None;
143
144 for qn in candidates {
145 if let Some(sym) = self.symbol_index.get(qn) {
146 let mut score: f64 = 0.0;
147
148 if let Some(imports) = file_imports {
150 if imports.contains(&sym.qualified_name)
151 || imports.iter().any(|imp| imp.ends_with(&sym.name))
152 {
153 score += 0.4;
154 }
155 }
156
157 if sym.file_path == reference.file_path {
159 score += 0.3;
160 }
161
162 if sym.name == reference.target_name {
164 score += 0.2;
165 }
166
167 let ref_module = extract_module_path(&reference.file_path);
169 let sym_module = extract_module_path(&sym.file_path);
170 if ref_module == sym_module {
171 score += 0.1;
172 }
173
174 if best.is_none() || score > best.unwrap().1 {
175 best = Some((sym, score));
176 }
177 }
178 }
179
180 if let Some((sym, score)) = best {
181 let confidence = 0.3 + (score.min(1.0) * 0.5);
183 return Some((sym, confidence));
184 }
185 }
186
187 None
188 }
189
190 pub fn resolve_all(&self, references: &[Reference]) -> Vec<ResolvedEdge> {
194 references
195 .iter()
196 .filter_map(|r| {
197 let (target, confidence) = self.resolve_with_confidence(r)?;
198 let relationship = match r.kind {
199 ReferenceKind::Call => RelationshipType::Calls,
200 ReferenceKind::Import => RelationshipType::Imports,
201 ReferenceKind::Inherits => RelationshipType::Inherits,
202 ReferenceKind::Implements => RelationshipType::Implements,
203 ReferenceKind::TypeUsage => RelationshipType::DependsOn,
204 };
205
206 Some(ResolvedEdge {
207 source_qualified_name: r.source_qualified_name.clone(),
208 target_qualified_name: target.qualified_name.clone(),
209 relationship,
210 file_path: r.file_path.clone(),
211 line: r.line,
212 resolution_confidence: confidence,
213 })
214 })
215 .collect()
216 }
217}
218
219fn extract_module_path(file_path: &str) -> &str {
222 file_path.rsplit_once('/').map(|(dir, _)| dir).unwrap_or("")
223}
224
225impl Default for ReferenceResolver {
226 fn default() -> Self {
227 Self::new()
228 }
229}
230
231#[cfg(test)]
232#[path = "tests/resolver_tests.rs"]
233mod tests;