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