Skip to main content

rust_relations_explorer/graph/
resolver.rs

1use std::collections::HashMap;
2use std::path::{Path, PathBuf};
3use std::sync::Arc;
4
5use crate::graph::{Item, ItemId, ItemType, KnowledgeGraph};
6
7pub struct Resolver<'a> {
8    graph: &'a KnowledgeGraph,
9    // name -> items (functions, types, etc.)
10    name_index: HashMap<Arc<str>, Vec<ItemId>>,
11    // module (file stem) -> file-level module item id
12    module_index: HashMap<Arc<str>, ItemId>,
13    // item -> file mapping
14    item_to_file: HashMap<ItemId, PathBuf>,
15    // alias (from pub use ... as Alias) -> fully-qualified target segments
16    alias_map: HashMap<Arc<str>, Vec<Arc<str>>>,
17    // per-file exposure of names via non-aliased re-exports: exposed name -> fully-qualified target segments
18    exposure_map: HashMap<PathBuf, HashMap<Arc<str>, Vec<Arc<str>>>>,
19}
20
21impl Resolver<'_> {
22    // Compute module segments relative to src/ for a given file path.
23    fn module_segments_for(&self, path: &Path) -> Vec<String> {
24        // Use cached precomputed segments when available
25        if let Some(segs) = self.graph.module_segments.get(path) {
26            return segs.clone();
27        }
28        // Fallback to on-the-fly computation (should be rare)
29        let mut src_idx: Option<usize> = None;
30        let comps: Vec<_> = path.components().collect();
31        for (i, c) in comps.iter().enumerate() {
32            if let std::path::Component::Normal(os) = c {
33                if os.to_str() == Some("src") {
34                    src_idx = Some(i);
35                    break;
36                }
37            }
38        }
39        let mut segs: Vec<String> = Vec::new();
40        if let Some(i) = src_idx {
41            for c in &comps[i + 1..comps.len().saturating_sub(1)] {
42                if let std::path::Component::Normal(os) = c {
43                    if let Some(s) = os.to_str() {
44                        segs.push(s.to_string());
45                    }
46                }
47            }
48            if let Some(file_os) = path.file_name() {
49                let file = file_os.to_string_lossy();
50                if file != "mod.rs" && file != "lib.rs" {
51                    if let Some(stem) = path.file_stem().and_then(|s| s.to_str()) {
52                        segs.push(stem.to_string());
53                    }
54                }
55            }
56        }
57        segs
58    }
59}
60
61impl<'a> Resolver<'a> {
62    /// Find items by simple name using the prebuilt name index.
63    #[must_use]
64    pub fn find_by_name(&self, name: &str) -> Vec<ItemId> {
65        if let Some(ids) = self.name_index.get(&Arc::<str>::from(name)) {
66            return ids.clone();
67        }
68        // Fallback: try looking up using the shared string pool if present
69        // (Most names will hit the Arc<str>::from path above already.)
70        let key: Arc<str> = {
71            let pool =
72                self.graph.string_pool.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
73            if let Some(a) = pool.get(name) {
74                a.clone()
75            } else {
76                Arc::from(name)
77            }
78        };
79        self.name_index.get(&key).cloned().unwrap_or_default()
80    }
81
82    /// Get the file path where an item is defined.
83    #[must_use]
84    pub fn item_path(&self, id: &ItemId) -> Option<&PathBuf> {
85        self.item_to_file.get(id)
86    }
87
88    /// Get the ItemType (kind) for a given item id.
89    #[must_use]
90    pub fn item_kind(&self, id: &ItemId) -> Option<ItemType> {
91        let path = self.item_to_file.get(id)?;
92        let file = self.graph.files.get(path)?;
93        for it in &file.items {
94            if &it.id == id {
95                return Some(it.item_type.clone());
96            }
97        }
98        None
99    }
100
101    #[must_use]
102    pub fn new(graph: &'a KnowledgeGraph) -> Self {
103        // Pre-size maps based on graph characteristics to reduce rehashing/allocations
104        let files_len = graph.files.len();
105        let mut approx_items = 0usize;
106        let mut approx_imports = 0usize;
107        for f in graph.files.values() {
108            approx_items += f.items.len();
109            approx_imports += f.imports.len();
110        }
111
112        // Global string interner via graph.string_pool to deduplicate hot strings
113        let intern_str = |s: &str| -> Arc<str> {
114            let mut pool =
115                graph.string_pool.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
116            if let Some(a) = pool.get(s) {
117                return a.clone();
118            }
119            let a: Arc<str> = Arc::from(s);
120            pool.insert(s.to_string(), a.clone());
121            a
122        };
123
124        let mut name_index: HashMap<Arc<str>, Vec<ItemId>> =
125            HashMap::with_capacity(approx_items * 2);
126        let mut module_index: HashMap<Arc<str>, ItemId> =
127            HashMap::with_capacity(files_len.saturating_mul(2));
128        let mut item_to_file: HashMap<ItemId, PathBuf> = HashMap::with_capacity(approx_items);
129        let mut alias_map: HashMap<Arc<str>, Vec<Arc<str>>> =
130            HashMap::with_capacity(approx_imports);
131        let mut exposure_map: HashMap<PathBuf, HashMap<Arc<str>, Vec<Arc<str>>>> =
132            HashMap::with_capacity(files_len);
133
134        for (path, file) in &graph.files {
135            // Ensure an entry exists for this file in exposure_map to avoid repeated reallocation of inner map
136            if !file.imports.is_empty() {
137                exposure_map
138                    .entry(path.clone())
139                    .or_insert_with(|| HashMap::with_capacity(file.imports.len()));
140            }
141            for (idx, it) in file.items.iter().enumerate() {
142                item_to_file.insert(it.id.clone(), path.clone());
143                let nm = intern_str(it.name.as_ref());
144                name_index.entry(nm).or_default().push(it.id.clone());
145                if idx == 0 {
146                    if let Some(stem) = path.file_stem().and_then(|s| s.to_str()) {
147                        let st = intern_str(stem);
148                        module_index.insert(st, it.id.clone());
149                    }
150                }
151            }
152            // Prefer precomputed import segments if available
153            if let Some(pre) = graph.import_segments.get(path) {
154                for (segments, alias_arc) in pre {
155                    if segments.is_empty() {
156                        continue;
157                    }
158                    if let Some(k) = alias_arc.clone() {
159                        alias_map.insert(k, segments.clone());
160                    } else if let Some(last) = segments.last().cloned() {
161                        exposure_map
162                            .entry(path.clone())
163                            .or_default()
164                            .insert(last, segments.clone());
165                    }
166                }
167            } else {
168                for imp in &file.imports {
169                    let segments: Vec<Arc<str>> =
170                        imp.path.split("::").filter(|s| !s.is_empty()).map(intern_str).collect();
171                    if let Some(alias) = &imp.alias {
172                        // Ignore underscore imports: `use path as _;` doesn't bind a name
173                        if alias.as_ref() == "_" {
174                            continue;
175                        }
176                        if !alias.is_empty() && !segments.is_empty() {
177                            let k = intern_str(alias.as_ref());
178                            alias_map.insert(k, segments);
179                        }
180                    } else if let Some(last) = segments.last().cloned() {
181                        // Non-aliased re-export exposes the last segment under the same name within this file/module
182                        exposure_map.entry(path.clone()).or_default().insert(last, segments);
183                    }
184                }
185            }
186        }
187        Self { graph, name_index, module_index, item_to_file, alias_map, exposure_map }
188    }
189
190    // Resolve an import path relative to a given file.
191    // Returns a list because globs or ambiguous names can map to multiple targets.
192    pub fn resolve_import(&self, from_file: &Path, raw_path: &str) -> Vec<ItemId> {
193        // Strip aliasing `as X`
194        let path = raw_path.split(" as ").next().unwrap_or(raw_path).trim();
195        let mut parts: Vec<Arc<str>> =
196            path.split("::").filter(|s| !s.is_empty()).map(Arc::<str>::from).collect();
197        if parts.is_empty() {
198            return Vec::new();
199        }
200
201        // Best-effort normalization of crate/self/super using filesystem layout under src/
202        let mut scope: Vec<String> = self.module_segments_for(from_file);
203        loop {
204            match parts.first().map(std::convert::AsRef::as_ref) {
205                Some("crate") => {
206                    parts.remove(0);
207                    scope.clear();
208                }
209                Some("self") => {
210                    parts.remove(0); /* stay in same scope */
211                }
212                Some("super") => {
213                    parts.remove(0);
214                    if !scope.is_empty() {
215                        scope.pop();
216                    }
217                }
218                _ => break,
219            }
220        }
221        if parts.is_empty() {
222            return Vec::new();
223        }
224
225        // Apply alias mapping on the first segment, if any
226        if let Some(first) = parts.first().cloned() {
227            if let Some(mapped) = self.alias_map.get(&first) {
228                parts.remove(0);
229                let mut new_parts = mapped.clone();
230                new_parts.extend(parts);
231                parts = new_parts;
232            }
233        }
234
235        // Apply per-file exposure mapping (re-exports without alias)
236        if let Some(first) = parts.first().cloned() {
237            if let Some(map) = self.exposure_map.get(from_file) {
238                if let Some(mapped) = map.get(&first) {
239                    parts.remove(0);
240                    let mut new_parts = mapped.clone();
241                    new_parts.extend(parts);
242                    parts = new_parts;
243                }
244            }
245        }
246
247        // Try to resolve using scoped module chain based on filesystem under src/
248        // Prepare a borrowable slice of &str for scoped chain
249        let parts_str: Vec<&str> = parts.iter().map(Arc::<str>::as_ref).collect();
250        if let Some(ids) = self.resolve_scoped_chain(from_file, &scope, &parts_str) {
251            return ids;
252        }
253
254        // Fallback: Try exact item name match on the last segment
255        let Some(last) = parts.last() else {
256            return Vec::new();
257        };
258        if let Some(ids) = self.name_index.get(last) {
259            return ids.clone();
260        }
261
262        // Fallback: map segment to a module (file-level) item
263        if let Some(mid) = self.module_index.get(last) {
264            return vec![mid.clone()];
265        }
266
267        // If there are multiple segments, try mapping first to a module and last to a symbol
268        if parts.len() >= 2 {
269            let first = parts[0].as_ref();
270            if let Some(_m0) = self.module_index.get(first) {
271                if let Some(ids) = self.name_index.get(last) {
272                    return ids.clone();
273                }
274            }
275            // Try combining scope head with parts
276            if let Some(scope_head) = scope.first() {
277                if let Some(_m) = self.module_index.get(scope_head.as_str()) {
278                    if let Some(ids) = self.name_index.get(last) {
279                        return ids.clone();
280                    }
281                }
282            }
283        }
284
285        Vec::new()
286    }
287
288    #[must_use]
289    pub fn is_item_function(&self, id: &ItemId) -> bool {
290        if let Some(file) = self.item_to_file.get(id).and_then(|p| self.graph.files.get(p)) {
291            if let Some(Item { item_type, .. }) = file.items.iter().find(|it| &it.id == id) {
292                return matches!(item_type, ItemType::Function { .. });
293            }
294        }
295        false
296    }
297
298    #[must_use]
299    pub fn is_file_level_module(&self, id: &ItemId) -> bool {
300        if let Some(file_path) = self.item_to_file.get(id) {
301            if let Some(file) = self.graph.files.get(file_path) {
302                if let Some(first) = file.items.first() {
303                    return &first.id == id;
304                }
305            }
306        }
307        false
308    }
309
310    // Attempt to walk modules using the scope and parts to find the target file/module and then resolve the final item.
311    // Returns Some(vec) on success; None if chain cannot be mapped.
312    fn resolve_scoped_chain(
313        &self,
314        from_file: &Path,
315        scope: &[String],
316        parts: &[&str],
317    ) -> Option<Vec<ItemId>> {
318        if parts.is_empty() {
319            return None;
320        }
321        let (base_src, _src_idx) = Self::base_src_dir(from_file)?;
322        // Build starting module path from scope
323        let mut dir = base_src.clone();
324        let mut scope_dirs: Vec<&str> = scope.iter().map(std::string::String::as_str).collect();
325        // If from_file is a leaf file (not mod.rs/lib.rs), drop last scope segment (file stem)
326        let is_leaf = from_file
327            .file_name()
328            .and_then(|s| s.to_str())
329            .is_some_and(|f| f != "mod.rs" && f != "lib.rs");
330        if is_leaf && !scope_dirs.is_empty() {
331            scope_dirs.pop();
332        }
333        for seg in scope_dirs {
334            dir.push(seg);
335        }
336        // Walk all segments except the last as module directories/files
337        for seg in &parts[..parts.len().saturating_sub(1)] {
338            // Try directory seg
339            dir.push(seg);
340            // Accept if there is either dir/mod.rs or dir/lib.rs in graph
341            let has_mod = self.graph.files.contains_key(&dir.join("mod.rs"));
342            let has_lib = !has_mod && self.graph.files.contains_key(&dir.join("lib.rs"));
343            let found_dir = has_mod || has_lib;
344            if !found_dir {
345                // Try sibling file: parent/<seg>.rs
346                dir.pop();
347                let file_rs = dir.join(format!("{seg}.rs"));
348                if self.graph.files.contains_key(&file_rs) {
349                    // Now move into that file's dir scope for next segments
350                    dir.push(seg);
351                } else {
352                    return None;
353                }
354            }
355        }
356        // Now resolve the last segment inside current dir/module
357        let last = parts[parts.len() - 1];
358        // First, try a file in this dir named last.rs
359        let file_rs = dir.join(format!("{last}.rs"));
360        if let Some(fnode) = self.graph.files.get(&file_rs) {
361            // Prefer concrete items named `last` inside that file
362            let mut ids: Vec<ItemId> = Vec::with_capacity(fnode.items.len());
363            for it in &fnode.items {
364                if it.name.as_ref() == last {
365                    ids.push(it.id.clone());
366                }
367            }
368            if !ids.is_empty() {
369                return Some(ids);
370            }
371            // Else return the file-level module id if known
372            if let Some(mid) = self.module_index.get(last) {
373                return Some(vec![mid.clone()]);
374            }
375        }
376        // Next, try dir/mod.rs or dir/lib.rs containing an item named `last`
377        let mod_path = dir.join("mod.rs");
378        let lib_path = dir.join("lib.rs");
379        for cand in [mod_path, lib_path] {
380            if let Some(fnode) = self.graph.files.get(&cand) {
381                let mut ids: Vec<ItemId> = Vec::with_capacity(fnode.items.len());
382                for it in &fnode.items {
383                    if it.name.as_ref() == last {
384                        ids.push(it.id.clone());
385                    }
386                }
387                if !ids.is_empty() {
388                    return Some(ids);
389                }
390            }
391        }
392        None
393    }
394
395    // Returns (base_src_dir, index_of_src_component) if src is found in the path
396    fn base_src_dir(path: &Path) -> Option<(PathBuf, usize)> {
397        let comps: Vec<_> = path.components().collect();
398        let mut src_idx: Option<usize> = None;
399        for (i, c) in comps.iter().enumerate() {
400            if let std::path::Component::Normal(os) = c {
401                if os.to_str() == Some("src") {
402                    src_idx = Some(i);
403                    break;
404                }
405            }
406        }
407        let i = src_idx?;
408        let mut base = PathBuf::new();
409        for c in &comps[..=i] {
410            base.push(c.as_os_str());
411        }
412        Some((base, i))
413    }
414}