Skip to main content

ryo_analysis/
import_map_builder.rs

1//! ImportMap builder from PureUseTree
2//!
3//! Converts Rust `use` statements (represented as PureUseTree) into ImportMap.
4
5use ryo_source::pure::{PureFile, PureItem, PureUseTree};
6use ryo_symbol::{CrateName, ImportMap, SymbolPath};
7
8/// Build ImportMap from a PureFile's use statements
9///
10/// # Arguments
11/// - `file`: The PureFile to extract use statements from
12/// - `crate_name`: The crate containing this file
13/// - `module_path`: The module path of this file (e.g., "my_crate::handlers")
14///
15/// # Returns
16/// An ImportMap containing all imports from the file's use statements
17pub fn build_import_map(
18    file: &PureFile,
19    crate_name: &CrateName,
20    module_path: &SymbolPath,
21) -> ImportMap {
22    let mut import_map = ImportMap::new();
23
24    for item in &file.items {
25        if let PureItem::Use(use_stmt) = item {
26            process_use_tree(&use_stmt.tree, "", crate_name, module_path, &mut import_map);
27        }
28    }
29
30    import_map
31}
32
33/// Process a PureUseTree and add imports to the ImportMap
34fn process_use_tree(
35    tree: &PureUseTree,
36    prefix: &str,
37    crate_name: &CrateName,
38    module_path: &SymbolPath,
39    import_map: &mut ImportMap,
40) {
41    match tree {
42        PureUseTree::Path { path, tree: inner } => {
43            // Resolve path segment (handle crate, self, super)
44            let resolved = resolve_path_segment(path, prefix, crate_name, module_path);
45            process_use_tree(inner, &resolved, crate_name, module_path, import_map);
46        }
47
48        PureUseTree::Name(name) => {
49            // use foo::bar::Name;
50            let full_path = if prefix.is_empty() {
51                name.clone()
52            } else {
53                format!("{}::{}", prefix, name)
54            };
55
56            if let Ok(symbol_path) = SymbolPath::parse(&full_path) {
57                import_map.add_import(name.clone(), symbol_path);
58            }
59        }
60
61        PureUseTree::Rename { name, rename } => {
62            // use foo::bar::Name as Alias;
63            let full_path = if prefix.is_empty() {
64                name.clone()
65            } else {
66                format!("{}::{}", prefix, name)
67            };
68
69            if let Ok(symbol_path) = SymbolPath::parse(&full_path) {
70                import_map.add_rename(rename.clone(), symbol_path);
71            }
72        }
73
74        PureUseTree::Glob => {
75            // use foo::bar::*;
76            if !prefix.is_empty() {
77                if let Ok(symbol_path) = SymbolPath::parse(prefix) {
78                    import_map.add_glob(symbol_path);
79                }
80            }
81        }
82
83        PureUseTree::Group(trees) => {
84            // use foo::bar::{A, B, C};
85            for inner_tree in trees {
86                process_use_tree(inner_tree, prefix, crate_name, module_path, import_map);
87            }
88        }
89    }
90}
91
92/// A public re-export entry: `pub use some::path::Name;` in a module.
93pub struct ReExportEntry {
94    /// The local name introduced by the use statement (e.g., "Mutex")
95    pub local_name: String,
96    /// The resolved full path of the imported symbol (e.g., "tokio::sync::mutex::Mutex")
97    pub full_path: SymbolPath,
98}
99
100/// Collect public re-exports from a PureFile.
101///
102/// Extracts `pub use` statements and returns (local_name, resolved_full_path) pairs.
103/// Only `pub` and `pub(crate)` visibility are included.
104pub fn collect_public_reexports(
105    file: &PureFile,
106    crate_name: &CrateName,
107    module_path: &SymbolPath,
108) -> Vec<ReExportEntry> {
109    let mut entries = Vec::new();
110
111    for item in &file.items {
112        if let PureItem::Use(use_stmt) = item {
113            if matches!(
114                use_stmt.vis,
115                ryo_source::pure::PureVis::Public | ryo_source::pure::PureVis::Crate
116            ) {
117                collect_reexport_entries(&use_stmt.tree, "", crate_name, module_path, &mut entries);
118            }
119        }
120    }
121
122    entries
123}
124
125/// Recursively extract (local_name, full_path) pairs from a PureUseTree.
126fn collect_reexport_entries(
127    tree: &PureUseTree,
128    prefix: &str,
129    crate_name: &CrateName,
130    module_path: &SymbolPath,
131    entries: &mut Vec<ReExportEntry>,
132) {
133    match tree {
134        PureUseTree::Path { path, tree: inner } => {
135            let resolved = resolve_path_segment(path, prefix, crate_name, module_path);
136            collect_reexport_entries(inner, &resolved, crate_name, module_path, entries);
137        }
138        PureUseTree::Name(name) => {
139            let full_path = if prefix.is_empty() {
140                name.clone()
141            } else {
142                format!("{}::{}", prefix, name)
143            };
144            if let Ok(symbol_path) = SymbolPath::parse(&full_path) {
145                entries.push(ReExportEntry {
146                    local_name: name.clone(),
147                    full_path: symbol_path,
148                });
149            }
150        }
151        PureUseTree::Rename { name, rename } => {
152            let full_path = if prefix.is_empty() {
153                name.clone()
154            } else {
155                format!("{}::{}", prefix, name)
156            };
157            if let Ok(symbol_path) = SymbolPath::parse(&full_path) {
158                entries.push(ReExportEntry {
159                    local_name: rename.clone(),
160                    full_path: symbol_path,
161                });
162            }
163        }
164        PureUseTree::Glob => {
165            // Glob re-exports can't be statically enumerated without full registry scan;
166            // skip for now.
167        }
168        PureUseTree::Group(trees) => {
169            for inner_tree in trees {
170                collect_reexport_entries(inner_tree, prefix, crate_name, module_path, entries);
171            }
172        }
173    }
174}
175
176/// Resolve special path segments (crate, self, super)
177fn resolve_path_segment(
178    segment: &str,
179    prefix: &str,
180    crate_name: &CrateName,
181    module_path: &SymbolPath,
182) -> String {
183    match segment {
184        "crate" => {
185            // `crate::` → current crate name
186            crate_name.as_str().to_string()
187        }
188
189        "self" => {
190            // `self::` → current module path
191            if prefix.is_empty() {
192                module_path.to_string()
193            } else {
194                prefix.to_string()
195            }
196        }
197
198        "super" => {
199            // `super::` → parent module
200            if prefix.is_empty() {
201                module_path
202                    .parent()
203                    .map(|p| p.to_string())
204                    .unwrap_or_else(|| crate_name.as_str().to_string())
205            } else {
206                // super within an already-resolved prefix
207                if let Ok(path) = SymbolPath::parse(prefix) {
208                    path.parent()
209                        .map(|p| p.to_string())
210                        .unwrap_or_else(|| prefix.to_string())
211                } else {
212                    prefix.to_string()
213                }
214            }
215        }
216
217        _ => {
218            // Normal path segment
219            if prefix.is_empty() {
220                segment.to_string()
221            } else {
222                format!("{}::{}", prefix, segment)
223            }
224        }
225    }
226}
227
228#[cfg(test)]
229mod tests {
230    use super::*;
231    use ryo_source::pure::{PureUse, PureVis};
232
233    fn make_file_with_uses(uses: Vec<PureUse>) -> PureFile {
234        PureFile {
235            items: uses.into_iter().map(PureItem::Use).collect(),
236            attrs: vec![],
237        }
238    }
239
240    fn make_use(tree: PureUseTree) -> PureUse {
241        PureUse {
242            vis: PureVis::Private,
243            tree,
244        }
245    }
246
247    fn make_crate_name() -> CrateName {
248        CrateName::new_for_test("my_crate")
249    }
250
251    fn make_module_path() -> SymbolPath {
252        SymbolPath::parse("my_crate::handlers").unwrap()
253    }
254
255    #[test]
256    fn test_simple_import() {
257        // use std::collections::HashMap;
258        let tree = PureUseTree::Path {
259            path: "std".to_string(),
260            tree: Box::new(PureUseTree::Path {
261                path: "collections".to_string(),
262                tree: Box::new(PureUseTree::Name("HashMap".to_string())),
263            }),
264        };
265
266        let file = make_file_with_uses(vec![make_use(tree)]);
267        let import_map = build_import_map(&file, &make_crate_name(), &make_module_path());
268
269        let expected = SymbolPath::parse("std::collections::HashMap").unwrap();
270        assert_eq!(import_map.resolve("HashMap"), Some(&expected));
271    }
272
273    #[test]
274    fn test_rename_import() {
275        // use std::collections::HashMap as Map;
276        let tree = PureUseTree::Path {
277            path: "std".to_string(),
278            tree: Box::new(PureUseTree::Path {
279                path: "collections".to_string(),
280                tree: Box::new(PureUseTree::Rename {
281                    name: "HashMap".to_string(),
282                    rename: "Map".to_string(),
283                }),
284            }),
285        };
286
287        let file = make_file_with_uses(vec![make_use(tree)]);
288        let import_map = build_import_map(&file, &make_crate_name(), &make_module_path());
289
290        let expected = SymbolPath::parse("std::collections::HashMap").unwrap();
291        assert_eq!(import_map.resolve("Map"), Some(&expected));
292        assert_eq!(import_map.resolve("HashMap"), None);
293    }
294
295    #[test]
296    fn test_glob_import() {
297        // use std::collections::*;
298        let tree = PureUseTree::Path {
299            path: "std".to_string(),
300            tree: Box::new(PureUseTree::Path {
301                path: "collections".to_string(),
302                tree: Box::new(PureUseTree::Glob),
303            }),
304        };
305
306        let file = make_file_with_uses(vec![make_use(tree)]);
307        let import_map = build_import_map(&file, &make_crate_name(), &make_module_path());
308
309        let expected = SymbolPath::parse("std::collections").unwrap();
310        assert!(import_map.glob_imports().contains(&expected));
311    }
312
313    #[test]
314    fn test_group_import() {
315        // use std::collections::{HashMap, HashSet};
316        let tree = PureUseTree::Path {
317            path: "std".to_string(),
318            tree: Box::new(PureUseTree::Path {
319                path: "collections".to_string(),
320                tree: Box::new(PureUseTree::Group(vec![
321                    PureUseTree::Name("HashMap".to_string()),
322                    PureUseTree::Name("HashSet".to_string()),
323                ])),
324            }),
325        };
326
327        let file = make_file_with_uses(vec![make_use(tree)]);
328        let import_map = build_import_map(&file, &make_crate_name(), &make_module_path());
329
330        let hashmap = SymbolPath::parse("std::collections::HashMap").unwrap();
331        let hashset = SymbolPath::parse("std::collections::HashSet").unwrap();
332        assert_eq!(import_map.resolve("HashMap"), Some(&hashmap));
333        assert_eq!(import_map.resolve("HashSet"), Some(&hashset));
334    }
335
336    #[test]
337    fn test_crate_import() {
338        // use crate::models::User;
339        let tree = PureUseTree::Path {
340            path: "crate".to_string(),
341            tree: Box::new(PureUseTree::Path {
342                path: "models".to_string(),
343                tree: Box::new(PureUseTree::Name("User".to_string())),
344            }),
345        };
346
347        let file = make_file_with_uses(vec![make_use(tree)]);
348        let import_map = build_import_map(&file, &make_crate_name(), &make_module_path());
349
350        let expected = SymbolPath::parse("my_crate::models::User").unwrap();
351        assert_eq!(import_map.resolve("User"), Some(&expected));
352    }
353
354    #[test]
355    fn test_super_import() {
356        // use super::Config;
357        // In module my_crate::handlers → resolves to my_crate::Config
358        let tree = PureUseTree::Path {
359            path: "super".to_string(),
360            tree: Box::new(PureUseTree::Name("Config".to_string())),
361        };
362
363        let file = make_file_with_uses(vec![make_use(tree)]);
364        let import_map = build_import_map(&file, &make_crate_name(), &make_module_path());
365
366        let expected = SymbolPath::parse("my_crate::Config").unwrap();
367        assert_eq!(import_map.resolve("Config"), Some(&expected));
368    }
369
370    #[test]
371    fn test_self_import() {
372        // use self::utils::Helper;
373        // In module my_crate::handlers → resolves to my_crate::handlers::utils::Helper
374        let tree = PureUseTree::Path {
375            path: "self".to_string(),
376            tree: Box::new(PureUseTree::Path {
377                path: "utils".to_string(),
378                tree: Box::new(PureUseTree::Name("Helper".to_string())),
379            }),
380        };
381
382        let file = make_file_with_uses(vec![make_use(tree)]);
383        let import_map = build_import_map(&file, &make_crate_name(), &make_module_path());
384
385        let expected = SymbolPath::parse("my_crate::handlers::utils::Helper").unwrap();
386        assert_eq!(import_map.resolve("Helper"), Some(&expected));
387    }
388
389    // ========== collect_public_reexports tests ==========
390
391    fn make_pub_use(tree: PureUseTree) -> PureUse {
392        PureUse {
393            vis: PureVis::Public,
394            tree,
395        }
396    }
397
398    #[test]
399    fn test_pub_reexport_collected() {
400        // pub use crate::sync::mutex::Mutex;
401        let tree = PureUseTree::Path {
402            path: "crate".to_string(),
403            tree: Box::new(PureUseTree::Path {
404                path: "sync".to_string(),
405                tree: Box::new(PureUseTree::Path {
406                    path: "mutex".to_string(),
407                    tree: Box::new(PureUseTree::Name("Mutex".to_string())),
408                }),
409            }),
410        };
411
412        let crate_name = CrateName::new_for_test("tokio");
413        let module_path = SymbolPath::parse("tokio::sync").unwrap();
414        let file = make_file_with_uses(vec![make_pub_use(tree)]);
415        let reexports = collect_public_reexports(&file, &crate_name, &module_path);
416
417        assert_eq!(reexports.len(), 1);
418        assert_eq!(reexports[0].local_name, "Mutex");
419        assert_eq!(
420            reexports[0].full_path,
421            SymbolPath::parse("tokio::sync::mutex::Mutex").unwrap()
422        );
423    }
424
425    #[test]
426    fn test_private_use_not_collected() {
427        // use crate::sync::mutex::Mutex;  (private)
428        let tree = PureUseTree::Path {
429            path: "crate".to_string(),
430            tree: Box::new(PureUseTree::Path {
431                path: "sync".to_string(),
432                tree: Box::new(PureUseTree::Name("Mutex".to_string())),
433            }),
434        };
435
436        let crate_name = CrateName::new_for_test("tokio");
437        let module_path = SymbolPath::parse("tokio::sync").unwrap();
438        let file = make_file_with_uses(vec![make_use(tree)]); // private
439        let reexports = collect_public_reexports(&file, &crate_name, &module_path);
440
441        assert!(reexports.is_empty());
442    }
443
444    #[test]
445    fn test_pub_reexport_rename() {
446        // pub use parking_lot::Mutex as ParkingMutex;
447        let tree = PureUseTree::Path {
448            path: "parking_lot".to_string(),
449            tree: Box::new(PureUseTree::Rename {
450                name: "Mutex".to_string(),
451                rename: "ParkingMutex".to_string(),
452            }),
453        };
454
455        let crate_name = CrateName::new_for_test("my_crate");
456        let module_path = SymbolPath::parse("my_crate").unwrap();
457        let file = make_file_with_uses(vec![make_pub_use(tree)]);
458        let reexports = collect_public_reexports(&file, &crate_name, &module_path);
459
460        assert_eq!(reexports.len(), 1);
461        assert_eq!(reexports[0].local_name, "ParkingMutex");
462        assert_eq!(
463            reexports[0].full_path,
464            SymbolPath::parse("parking_lot::Mutex").unwrap()
465        );
466    }
467
468    #[test]
469    fn test_pub_reexport_group() {
470        // pub use crate::types::{Config, State};
471        let tree = PureUseTree::Path {
472            path: "crate".to_string(),
473            tree: Box::new(PureUseTree::Path {
474                path: "types".to_string(),
475                tree: Box::new(PureUseTree::Group(vec![
476                    PureUseTree::Name("Config".to_string()),
477                    PureUseTree::Name("State".to_string()),
478                ])),
479            }),
480        };
481
482        let crate_name = CrateName::new_for_test("my_crate");
483        let module_path = SymbolPath::parse("my_crate").unwrap();
484        let file = make_file_with_uses(vec![make_pub_use(tree)]);
485        let reexports = collect_public_reexports(&file, &crate_name, &module_path);
486
487        assert_eq!(reexports.len(), 2);
488        let names: Vec<&str> = reexports.iter().map(|e| e.local_name.as_str()).collect();
489        assert!(names.contains(&"Config"));
490        assert!(names.contains(&"State"));
491    }
492}