Skip to main content

ryo_symbol/
use_resolver.rs

1//! Use statement resolution
2//!
3//! Provides unified handling of Rust `use` statements for cross-crate symbol resolution.
4//!
5//! # Overview
6//!
7//! - **ImportMap**: Per-module mapping from local names to full symbol paths
8//! - **UseResolver**: Central hub for workspace-wide use resolution
9//!
10//! # Example
11//!
12//! ```ignore
13//! // Given: use std::collections::HashMap;
14//! // In module: my_crate::handlers
15//!
16//! let resolver = UseResolver::new();
17//! // ... build import maps for all modules ...
18//!
19//! // Resolve "HashMap" in my_crate::handlers
20//! let id = resolver.resolve(&module_path, "HashMap", &registry);
21//! // Returns: SymbolId for std::collections::HashMap
22//! ```
23
24use std::collections::HashMap;
25
26use crate::path::SymbolPath;
27use crate::registry::SymbolRegistry;
28use crate::SymbolId;
29
30/// Per-module import mapping
31///
32/// Maps local names (as used in code) to their full symbol paths.
33/// Each module (file) has its own ImportMap built from its `use` statements.
34#[derive(Debug, Clone, Default)]
35pub struct ImportMap {
36    /// Direct imports: local_name → full_path
37    /// e.g., "HashMap" → "std::collections::HashMap"
38    imports: HashMap<String, SymbolPath>,
39
40    /// Glob imports: modules whose contents are all available
41    /// e.g., use std::prelude::rust_2021::*;
42    glob_imports: Vec<SymbolPath>,
43
44    /// Renamed imports: alias → original_path
45    /// e.g., "Map" → "std::collections::HashMap" (from `use ... as Map`)
46    renames: HashMap<String, SymbolPath>,
47}
48
49impl ImportMap {
50    /// Create an empty ImportMap
51    pub fn new() -> Self {
52        Self::default()
53    }
54
55    /// Add a direct import
56    ///
57    /// # Arguments
58    /// - `local_name`: The name as used in code (e.g., "HashMap")
59    /// - `full_path`: The full symbol path (e.g., "std::collections::HashMap")
60    pub fn add_import(&mut self, local_name: impl Into<String>, full_path: SymbolPath) {
61        self.imports.insert(local_name.into(), full_path);
62    }
63
64    /// Add a renamed import (use X as Y)
65    ///
66    /// # Arguments
67    /// - `alias`: The local alias name
68    /// - `full_path`: The full symbol path being aliased
69    pub fn add_rename(&mut self, alias: impl Into<String>, full_path: SymbolPath) {
70        self.renames.insert(alias.into(), full_path);
71    }
72
73    /// Add a glob import (use X::*)
74    ///
75    /// # Arguments
76    /// - `module_path`: The module whose contents are glob-imported
77    pub fn add_glob(&mut self, module_path: SymbolPath) {
78        self.glob_imports.push(module_path);
79    }
80
81    /// Resolve a name to its full path (without glob resolution)
82    ///
83    /// Checks direct imports and renames only.
84    /// For glob resolution, use `resolve_with_registry`.
85    pub fn resolve(&self, name: &str) -> Option<&SymbolPath> {
86        // Check direct imports first
87        if let Some(path) = self.imports.get(name) {
88            return Some(path);
89        }
90        // Check renames
91        self.renames.get(name)
92    }
93
94    /// Resolve a name considering glob imports
95    ///
96    /// # Arguments
97    /// - `name`: The local name to resolve
98    /// - `registry`: SymbolRegistry for glob resolution
99    ///
100    /// # Returns
101    /// The SymbolId if found, None otherwise
102    pub fn resolve_with_registry(&self, name: &str, registry: &SymbolRegistry) -> Option<SymbolId> {
103        // 1. Try direct imports and renames
104        if let Some(path) = self.resolve(name) {
105            return registry.lookup(path);
106        }
107
108        // 2. Try glob imports
109        for glob_module in &self.glob_imports {
110            // Construct potential full path: glob_module::name
111            if let Ok(candidate) = glob_module.child(name) {
112                if let Some(id) = registry.lookup(&candidate) {
113                    return Some(id);
114                }
115            }
116        }
117
118        None
119    }
120
121    /// Get all direct imports
122    pub fn imports(&self) -> &HashMap<String, SymbolPath> {
123        &self.imports
124    }
125
126    /// Get all renamed imports
127    pub fn renames(&self) -> &HashMap<String, SymbolPath> {
128        &self.renames
129    }
130
131    /// Get all glob imports
132    pub fn glob_imports(&self) -> &[SymbolPath] {
133        &self.glob_imports
134    }
135
136    /// Check if the map is empty
137    pub fn is_empty(&self) -> bool {
138        self.imports.is_empty() && self.renames.is_empty() && self.glob_imports.is_empty()
139    }
140
141    /// Get the total number of imports (direct + renamed)
142    pub fn len(&self) -> usize {
143        self.imports.len() + self.renames.len()
144    }
145}
146
147/// Workspace-wide use resolver
148///
149/// Manages ImportMaps for all modules in the workspace and provides
150/// unified name resolution across crate boundaries.
151#[derive(Debug, Clone, Default)]
152pub struct UseResolver {
153    /// module_path → ImportMap
154    /// Key is the full module path (e.g., "ryo_cli::commands::graph")
155    import_maps: HashMap<SymbolPath, ImportMap>,
156}
157
158impl UseResolver {
159    /// Create an empty UseResolver
160    pub fn new() -> Self {
161        Self::default()
162    }
163
164    /// Register an ImportMap for a module
165    pub fn register(&mut self, module_path: SymbolPath, import_map: ImportMap) {
166        self.import_maps.insert(module_path, import_map);
167    }
168
169    /// Get the ImportMap for a module
170    pub fn get(&self, module_path: &SymbolPath) -> Option<&ImportMap> {
171        self.import_maps.get(module_path)
172    }
173
174    /// Get mutable ImportMap for a module
175    pub fn get_mut(&mut self, module_path: &SymbolPath) -> Option<&mut ImportMap> {
176        self.import_maps.get_mut(module_path)
177    }
178
179    /// Resolve a name in a specific module context
180    ///
181    /// Resolution order:
182    /// 1. Module's ImportMap (direct imports, renames)
183    /// 2. Glob imports from the module
184    /// 3. Walk up the module hierarchy
185    /// 4. Primitives are skipped (handled by caller)
186    ///
187    /// # Arguments
188    /// - `module_path`: The module where the name is used
189    /// - `name`: The local name to resolve
190    /// - `registry`: SymbolRegistry for lookups
191    ///
192    /// # Returns
193    /// The SymbolId if found, None otherwise
194    pub fn resolve(
195        &self,
196        module_path: &SymbolPath,
197        name: &str,
198        registry: &SymbolRegistry,
199    ) -> Option<SymbolId> {
200        // 1. Try the module's own ImportMap
201        if let Some(import_map) = self.import_maps.get(module_path) {
202            if let Some(id) = import_map.resolve_with_registry(name, registry) {
203                return Some(id);
204            }
205        }
206
207        // 2. Try qualified path as-is (e.g., "std::io::Read")
208        if name.contains("::") {
209            if let Ok(path) = SymbolPath::parse(name) {
210                if let Some(id) = registry.lookup(&path) {
211                    return Some(id);
212                }
213            }
214        }
215
216        // 3. Try in current module
217        if let Ok(qualified) = module_path.child(name) {
218            if let Some(id) = registry.lookup(&qualified) {
219                return Some(id);
220            }
221        }
222
223        // 4. Walk up the module hierarchy
224        let mut current = module_path.clone();
225        while let Some(parent) = current.parent() {
226            if let Ok(qualified) = parent.child(name) {
227                if let Some(id) = registry.lookup(&qualified) {
228                    return Some(id);
229                }
230            }
231            current = parent;
232        }
233
234        // 5. Try as top-level (crate root)
235        if let Ok(path) = SymbolPath::parse(name) {
236            return registry.lookup(&path);
237        }
238
239        None
240    }
241
242    /// Check if there's an ImportMap for a module
243    pub fn contains(&self, module_path: &SymbolPath) -> bool {
244        self.import_maps.contains_key(module_path)
245    }
246
247    /// Get the number of modules with ImportMaps
248    pub fn len(&self) -> usize {
249        self.import_maps.len()
250    }
251
252    /// Check if empty
253    pub fn is_empty(&self) -> bool {
254        self.import_maps.is_empty()
255    }
256
257    /// Iterate over all module paths and their ImportMaps
258    pub fn iter(&self) -> impl Iterator<Item = (&SymbolPath, &ImportMap)> {
259        self.import_maps.iter()
260    }
261}
262
263#[cfg(test)]
264mod tests {
265    use super::*;
266    use crate::SymbolKind;
267
268    fn make_path(s: &str) -> SymbolPath {
269        SymbolPath::parse(s).unwrap()
270    }
271
272    #[test]
273    fn test_import_map_basic() {
274        let mut map = ImportMap::new();
275
276        // Add direct import
277        let hashmap_path = make_path("std::collections::HashMap");
278        map.add_import("HashMap", hashmap_path.clone());
279
280        // Resolve
281        assert_eq!(map.resolve("HashMap"), Some(&hashmap_path));
282        assert_eq!(map.resolve("Vec"), None);
283    }
284
285    #[test]
286    fn test_import_map_rename() {
287        let mut map = ImportMap::new();
288
289        // Add renamed import: use std::collections::HashMap as Map
290        let hashmap_path = make_path("std::collections::HashMap");
291        map.add_rename("Map", hashmap_path.clone());
292
293        // Resolve alias
294        assert_eq!(map.resolve("Map"), Some(&hashmap_path));
295        // Original name not available
296        assert_eq!(map.resolve("HashMap"), None);
297    }
298
299    #[test]
300    fn test_import_map_with_registry() {
301        let mut map = ImportMap::new();
302        let mut registry = SymbolRegistry::new();
303
304        // Register symbol in registry
305        let hashmap_path = make_path("std::collections::HashMap");
306        let id = registry
307            .register(hashmap_path.clone(), SymbolKind::Struct)
308            .unwrap();
309
310        // Add to import map
311        map.add_import("HashMap", hashmap_path);
312
313        // Resolve with registry
314        assert_eq!(map.resolve_with_registry("HashMap", &registry), Some(id));
315    }
316
317    #[test]
318    fn test_import_map_glob() {
319        let mut map = ImportMap::new();
320        let mut registry = SymbolRegistry::new();
321
322        // Register symbols
323        let foo_bar_path = make_path("foo::bar::Baz");
324        let id = registry.register(foo_bar_path, SymbolKind::Struct).unwrap();
325
326        // Add glob import: use foo::bar::*
327        map.add_glob(make_path("foo::bar"));
328
329        // Resolve via glob
330        assert_eq!(map.resolve_with_registry("Baz", &registry), Some(id));
331    }
332
333    #[test]
334    fn test_use_resolver_basic() {
335        let mut resolver = UseResolver::new();
336        let mut registry = SymbolRegistry::new();
337
338        // Register symbol
339        let hashmap_path = make_path("std::collections::HashMap");
340        let id = registry
341            .register(hashmap_path.clone(), SymbolKind::Struct)
342            .unwrap();
343
344        // Create import map for a module
345        let module_path = make_path("my_crate::handlers");
346        let mut import_map = ImportMap::new();
347        import_map.add_import("HashMap", hashmap_path);
348
349        // Register in resolver
350        resolver.register(module_path.clone(), import_map);
351
352        // Resolve
353        assert_eq!(
354            resolver.resolve(&module_path, "HashMap", &registry),
355            Some(id)
356        );
357    }
358
359    #[test]
360    fn test_use_resolver_hierarchy_fallback() {
361        let mut resolver = UseResolver::new();
362        let mut registry = SymbolRegistry::new();
363
364        // Register symbol in parent module
365        let config_path = make_path("my_crate::Config");
366        let id = registry.register(config_path, SymbolKind::Struct).unwrap();
367
368        // No ImportMap for the child module, should fallback to hierarchy
369        let module_path = make_path("my_crate::handlers::create");
370
371        // Register empty import map
372        resolver.register(module_path.clone(), ImportMap::new());
373
374        // Should find Config by walking up hierarchy
375        assert_eq!(
376            resolver.resolve(&module_path, "Config", &registry),
377            Some(id)
378        );
379    }
380
381    #[test]
382    fn test_use_resolver_qualified_path() {
383        let resolver = UseResolver::new();
384        let mut registry = SymbolRegistry::new();
385
386        // Register symbol
387        let path = make_path("std::io::Read");
388        let id = registry.register(path, SymbolKind::Trait).unwrap();
389
390        // Resolve qualified path directly
391        let module_path = make_path("my_crate");
392        assert_eq!(
393            resolver.resolve(&module_path, "std::io::Read", &registry),
394            Some(id)
395        );
396    }
397}