context_creator/core/semantic/
resolver.rs

1//! Module resolution for converting import strings to file paths
2
3use crate::utils::error::ContextCreatorError;
4use std::path::{Path, PathBuf};
5
6/// A resolved module path
7#[derive(Debug, Clone, PartialEq)]
8pub struct ResolvedPath {
9    /// The resolved file path
10    pub path: PathBuf,
11    /// Whether this is a third-party module (not in project)
12    pub is_external: bool,
13    /// Confidence in the resolution (0.0 to 1.0)
14    pub confidence: f32,
15}
16
17/// Trait for language-specific module resolution
18pub trait ModuleResolver: Send + Sync {
19    /// Resolve a module import to a file path
20    fn resolve_import(
21        &self,
22        module_path: &str,
23        from_file: &Path,
24        base_dir: &Path,
25    ) -> Result<ResolvedPath, ContextCreatorError>;
26
27    /// Get common file extensions for this language
28    fn get_file_extensions(&self) -> Vec<&'static str>;
29
30    /// Check if a module is likely external/third-party
31    fn is_external_module(&self, module_path: &str) -> bool {
32        // Default heuristics - languages can override
33        module_path.starts_with('@') || // npm scoped packages
34        !module_path.starts_with('.') || // relative imports usually start with .
35        module_path.contains("node_modules") ||
36        module_path.contains("site-packages") ||
37        module_path.contains("vendor")
38    }
39}
40
41/// Common module resolution utilities
42pub struct ResolverUtils;
43
44impl ResolverUtils {
45    /// Try to find a file with different extensions
46    pub fn find_with_extensions(base_path: &Path, extensions: &[&str]) -> Option<PathBuf> {
47        // Try exact path first
48        if base_path.exists() && base_path.is_file() {
49            return Some(base_path.to_path_buf());
50        }
51
52        // Try with each extension
53        for ext in extensions {
54            let with_ext = base_path.with_extension(ext);
55            if with_ext.exists() && with_ext.is_file() {
56                return Some(with_ext);
57            }
58        }
59
60        // Try as directory with index file
61        if base_path.exists() && base_path.is_dir() {
62            for index_name in &["index", "mod", "__init__"] {
63                for ext in extensions {
64                    let index_path = base_path.join(format!("{index_name}.{ext}"));
65                    if index_path.exists() && index_path.is_file() {
66                        return Some(index_path);
67                    }
68                }
69            }
70        }
71
72        None
73    }
74
75    /// Convert module path separators to file path separators
76    pub fn module_to_path(module_path: &str) -> PathBuf {
77        PathBuf::from(module_path.replace('.', "/").replace("::", "/"))
78    }
79
80    /// Resolve a relative import path
81    pub fn resolve_relative(
82        import_path: &str,
83        from_file: &Path,
84        extensions: &[&str],
85    ) -> Option<PathBuf> {
86        let from_dir = from_file.parent()?;
87
88        // Handle different relative import styles
89        let clean_path = import_path
90            .trim_start_matches("./")
91            .trim_start_matches("../");
92
93        let mut current_dir = from_dir.to_path_buf();
94
95        // Count leading ../ to go up directories
96        let up_count = import_path.matches("../").count();
97        for _ in 0..up_count {
98            current_dir = current_dir.parent()?.to_path_buf();
99        }
100
101        let target = current_dir.join(clean_path);
102        Self::find_with_extensions(&target, extensions)
103    }
104
105    /// Check if a path is within the project directory
106    pub fn is_within_project(path: &Path, base_dir: &Path) -> bool {
107        path.canonicalize()
108            .ok()
109            .and_then(|p| base_dir.canonicalize().ok().map(|b| p.starts_with(b)))
110            .unwrap_or(false)
111    }
112}