ryo-symbol 0.1.0

Symbol system for Rust codebase - unique identifiers and file path management
Documentation
//! Use statement resolution
//!
//! Provides unified handling of Rust `use` statements for cross-crate symbol resolution.
//!
//! # Overview
//!
//! - **ImportMap**: Per-module mapping from local names to full symbol paths
//! - **UseResolver**: Central hub for workspace-wide use resolution
//!
//! # Example
//!
//! ```ignore
//! // Given: use std::collections::HashMap;
//! // In module: my_crate::handlers
//!
//! let resolver = UseResolver::new();
//! // ... build import maps for all modules ...
//!
//! // Resolve "HashMap" in my_crate::handlers
//! let id = resolver.resolve(&module_path, "HashMap", &registry);
//! // Returns: SymbolId for std::collections::HashMap
//! ```

use std::collections::HashMap;

use crate::path::SymbolPath;
use crate::registry::SymbolRegistry;
use crate::SymbolId;

/// Per-module import mapping
///
/// Maps local names (as used in code) to their full symbol paths.
/// Each module (file) has its own ImportMap built from its `use` statements.
#[derive(Debug, Clone, Default)]
pub struct ImportMap {
    /// Direct imports: local_name → full_path
    /// e.g., "HashMap" → "std::collections::HashMap"
    imports: HashMap<String, SymbolPath>,

    /// Glob imports: modules whose contents are all available
    /// e.g., use std::prelude::rust_2021::*;
    glob_imports: Vec<SymbolPath>,

    /// Renamed imports: alias → original_path
    /// e.g., "Map" → "std::collections::HashMap" (from `use ... as Map`)
    renames: HashMap<String, SymbolPath>,
}

impl ImportMap {
    /// Create an empty ImportMap
    pub fn new() -> Self {
        Self::default()
    }

    /// Add a direct import
    ///
    /// # Arguments
    /// - `local_name`: The name as used in code (e.g., "HashMap")
    /// - `full_path`: The full symbol path (e.g., "std::collections::HashMap")
    pub fn add_import(&mut self, local_name: impl Into<String>, full_path: SymbolPath) {
        self.imports.insert(local_name.into(), full_path);
    }

    /// Add a renamed import (use X as Y)
    ///
    /// # Arguments
    /// - `alias`: The local alias name
    /// - `full_path`: The full symbol path being aliased
    pub fn add_rename(&mut self, alias: impl Into<String>, full_path: SymbolPath) {
        self.renames.insert(alias.into(), full_path);
    }

    /// Add a glob import (use X::*)
    ///
    /// # Arguments
    /// - `module_path`: The module whose contents are glob-imported
    pub fn add_glob(&mut self, module_path: SymbolPath) {
        self.glob_imports.push(module_path);
    }

    /// Resolve a name to its full path (without glob resolution)
    ///
    /// Checks direct imports and renames only.
    /// For glob resolution, use `resolve_with_registry`.
    pub fn resolve(&self, name: &str) -> Option<&SymbolPath> {
        // Check direct imports first
        if let Some(path) = self.imports.get(name) {
            return Some(path);
        }
        // Check renames
        self.renames.get(name)
    }

    /// Resolve a name considering glob imports
    ///
    /// # Arguments
    /// - `name`: The local name to resolve
    /// - `registry`: SymbolRegistry for glob resolution
    ///
    /// # Returns
    /// The SymbolId if found, None otherwise
    pub fn resolve_with_registry(&self, name: &str, registry: &SymbolRegistry) -> Option<SymbolId> {
        // 1. Try direct imports and renames
        if let Some(path) = self.resolve(name) {
            return registry.lookup(path);
        }

        // 2. Try glob imports
        for glob_module in &self.glob_imports {
            // Construct potential full path: glob_module::name
            if let Ok(candidate) = glob_module.child(name) {
                if let Some(id) = registry.lookup(&candidate) {
                    return Some(id);
                }
            }
        }

        None
    }

    /// Get all direct imports
    pub fn imports(&self) -> &HashMap<String, SymbolPath> {
        &self.imports
    }

    /// Get all renamed imports
    pub fn renames(&self) -> &HashMap<String, SymbolPath> {
        &self.renames
    }

    /// Get all glob imports
    pub fn glob_imports(&self) -> &[SymbolPath] {
        &self.glob_imports
    }

    /// Check if the map is empty
    pub fn is_empty(&self) -> bool {
        self.imports.is_empty() && self.renames.is_empty() && self.glob_imports.is_empty()
    }

    /// Get the total number of imports (direct + renamed)
    pub fn len(&self) -> usize {
        self.imports.len() + self.renames.len()
    }
}

/// Workspace-wide use resolver
///
/// Manages ImportMaps for all modules in the workspace and provides
/// unified name resolution across crate boundaries.
#[derive(Debug, Clone, Default)]
pub struct UseResolver {
    /// module_path → ImportMap
    /// Key is the full module path (e.g., "ryo_cli::commands::graph")
    import_maps: HashMap<SymbolPath, ImportMap>,
}

impl UseResolver {
    /// Create an empty UseResolver
    pub fn new() -> Self {
        Self::default()
    }

    /// Register an ImportMap for a module
    pub fn register(&mut self, module_path: SymbolPath, import_map: ImportMap) {
        self.import_maps.insert(module_path, import_map);
    }

    /// Get the ImportMap for a module
    pub fn get(&self, module_path: &SymbolPath) -> Option<&ImportMap> {
        self.import_maps.get(module_path)
    }

    /// Get mutable ImportMap for a module
    pub fn get_mut(&mut self, module_path: &SymbolPath) -> Option<&mut ImportMap> {
        self.import_maps.get_mut(module_path)
    }

    /// Resolve a name in a specific module context
    ///
    /// Resolution order:
    /// 1. Module's ImportMap (direct imports, renames)
    /// 2. Glob imports from the module
    /// 3. Walk up the module hierarchy
    /// 4. Primitives are skipped (handled by caller)
    ///
    /// # Arguments
    /// - `module_path`: The module where the name is used
    /// - `name`: The local name to resolve
    /// - `registry`: SymbolRegistry for lookups
    ///
    /// # Returns
    /// The SymbolId if found, None otherwise
    pub fn resolve(
        &self,
        module_path: &SymbolPath,
        name: &str,
        registry: &SymbolRegistry,
    ) -> Option<SymbolId> {
        // 1. Try the module's own ImportMap
        if let Some(import_map) = self.import_maps.get(module_path) {
            if let Some(id) = import_map.resolve_with_registry(name, registry) {
                return Some(id);
            }
        }

        // 2. Try qualified path as-is (e.g., "std::io::Read")
        if name.contains("::") {
            if let Ok(path) = SymbolPath::parse(name) {
                if let Some(id) = registry.lookup(&path) {
                    return Some(id);
                }
            }
        }

        // 3. Try in current module
        if let Ok(qualified) = module_path.child(name) {
            if let Some(id) = registry.lookup(&qualified) {
                return Some(id);
            }
        }

        // 4. Walk up the module hierarchy
        let mut current = module_path.clone();
        while let Some(parent) = current.parent() {
            if let Ok(qualified) = parent.child(name) {
                if let Some(id) = registry.lookup(&qualified) {
                    return Some(id);
                }
            }
            current = parent;
        }

        // 5. Try as top-level (crate root)
        if let Ok(path) = SymbolPath::parse(name) {
            return registry.lookup(&path);
        }

        None
    }

    /// Check if there's an ImportMap for a module
    pub fn contains(&self, module_path: &SymbolPath) -> bool {
        self.import_maps.contains_key(module_path)
    }

    /// Get the number of modules with ImportMaps
    pub fn len(&self) -> usize {
        self.import_maps.len()
    }

    /// Check if empty
    pub fn is_empty(&self) -> bool {
        self.import_maps.is_empty()
    }

    /// Iterate over all module paths and their ImportMaps
    pub fn iter(&self) -> impl Iterator<Item = (&SymbolPath, &ImportMap)> {
        self.import_maps.iter()
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::SymbolKind;

    fn make_path(s: &str) -> SymbolPath {
        SymbolPath::parse(s).unwrap()
    }

    #[test]
    fn test_import_map_basic() {
        let mut map = ImportMap::new();

        // Add direct import
        let hashmap_path = make_path("std::collections::HashMap");
        map.add_import("HashMap", hashmap_path.clone());

        // Resolve
        assert_eq!(map.resolve("HashMap"), Some(&hashmap_path));
        assert_eq!(map.resolve("Vec"), None);
    }

    #[test]
    fn test_import_map_rename() {
        let mut map = ImportMap::new();

        // Add renamed import: use std::collections::HashMap as Map
        let hashmap_path = make_path("std::collections::HashMap");
        map.add_rename("Map", hashmap_path.clone());

        // Resolve alias
        assert_eq!(map.resolve("Map"), Some(&hashmap_path));
        // Original name not available
        assert_eq!(map.resolve("HashMap"), None);
    }

    #[test]
    fn test_import_map_with_registry() {
        let mut map = ImportMap::new();
        let mut registry = SymbolRegistry::new();

        // Register symbol in registry
        let hashmap_path = make_path("std::collections::HashMap");
        let id = registry
            .register(hashmap_path.clone(), SymbolKind::Struct)
            .unwrap();

        // Add to import map
        map.add_import("HashMap", hashmap_path);

        // Resolve with registry
        assert_eq!(map.resolve_with_registry("HashMap", &registry), Some(id));
    }

    #[test]
    fn test_import_map_glob() {
        let mut map = ImportMap::new();
        let mut registry = SymbolRegistry::new();

        // Register symbols
        let foo_bar_path = make_path("foo::bar::Baz");
        let id = registry.register(foo_bar_path, SymbolKind::Struct).unwrap();

        // Add glob import: use foo::bar::*
        map.add_glob(make_path("foo::bar"));

        // Resolve via glob
        assert_eq!(map.resolve_with_registry("Baz", &registry), Some(id));
    }

    #[test]
    fn test_use_resolver_basic() {
        let mut resolver = UseResolver::new();
        let mut registry = SymbolRegistry::new();

        // Register symbol
        let hashmap_path = make_path("std::collections::HashMap");
        let id = registry
            .register(hashmap_path.clone(), SymbolKind::Struct)
            .unwrap();

        // Create import map for a module
        let module_path = make_path("my_crate::handlers");
        let mut import_map = ImportMap::new();
        import_map.add_import("HashMap", hashmap_path);

        // Register in resolver
        resolver.register(module_path.clone(), import_map);

        // Resolve
        assert_eq!(
            resolver.resolve(&module_path, "HashMap", &registry),
            Some(id)
        );
    }

    #[test]
    fn test_use_resolver_hierarchy_fallback() {
        let mut resolver = UseResolver::new();
        let mut registry = SymbolRegistry::new();

        // Register symbol in parent module
        let config_path = make_path("my_crate::Config");
        let id = registry.register(config_path, SymbolKind::Struct).unwrap();

        // No ImportMap for the child module, should fallback to hierarchy
        let module_path = make_path("my_crate::handlers::create");

        // Register empty import map
        resolver.register(module_path.clone(), ImportMap::new());

        // Should find Config by walking up hierarchy
        assert_eq!(
            resolver.resolve(&module_path, "Config", &registry),
            Some(id)
        );
    }

    #[test]
    fn test_use_resolver_qualified_path() {
        let resolver = UseResolver::new();
        let mut registry = SymbolRegistry::new();

        // Register symbol
        let path = make_path("std::io::Read");
        let id = registry.register(path, SymbolKind::Trait).unwrap();

        // Resolve qualified path directly
        let module_path = make_path("my_crate");
        assert_eq!(
            resolver.resolve(&module_path, "std::io::Read", &registry),
            Some(id)
        );
    }
}