probe_code/path_resolver/
mod.rs

1//! Module for resolving special path formats to filesystem paths.
2//!
3//! This module provides functionality to resolve special path formats like
4//! "go:github.com/user/repo", "js:express", or "rust:serde" to actual filesystem paths.
5
6mod go;
7mod javascript;
8mod rust;
9
10use std::path::{Path, PathBuf};
11
12pub use go::GoPathResolver;
13pub use javascript::JavaScriptPathResolver;
14pub use rust::RustPathResolver;
15
16/// A trait for language-specific path resolvers.
17///
18/// Implementations of this trait provide language-specific logic for resolving
19/// package/module names to filesystem paths.
20pub trait PathResolver {
21    /// The prefix used to identify paths for this resolver (e.g., "go:", "js:", "rust:").
22    fn prefix(&self) -> &'static str;
23
24    /// Splits the path string (after the prefix) into the core module/package
25    /// identifier and an optional subpath.
26    ///
27    /// For example, for Go:
28    /// - "fmt" -> Ok(("fmt", None))
29    /// - "net/http" -> Ok(("net/http", None)) // Stdlib multi-segment
30    /// - "github.com/gin-gonic/gin" -> Ok(("github.com/gin-gonic/gin", None))
31    /// - "github.com/gin-gonic/gin/examples/basic" -> Ok(("github.com/gin-gonic/gin", Some("examples/basic")))
32    ///
33    /// For JavaScript:
34    /// - "lodash" -> Ok(("lodash", None))
35    /// - "lodash/get" -> Ok(("lodash", Some("get")))
36    /// - "@types/node" -> Ok(("@types/node", None))
37    /// - "@types/node/fs" -> Ok(("@types/node", Some("fs")))
38    ///
39    /// # Arguments
40    /// * `full_path_after_prefix` - The portion of the input path string that comes *after* the resolver's prefix.
41    ///
42    /// # Returns
43    /// * `Ok((String, Option<String>))` - A tuple containing the resolved module name and an optional subpath string.
44    /// * `Err(String)` - An error message if the path format is invalid for this resolver.
45    fn split_module_and_subpath(
46        &self,
47        full_path_after_prefix: &str,
48    ) -> Result<(String, Option<String>), String>;
49
50    /// Resolves a package/module name to its filesystem location.
51    ///
52    /// # Arguments
53    ///
54    /// * `module_name` - The package/module name to resolve (without any subpath)
55    ///
56    /// # Returns
57    ///
58    /// * `Ok(PathBuf)` - The filesystem path where the package is located
59    /// * `Err(String)` - An error message if resolution fails
60    fn resolve(&self, module_name: &str) -> Result<PathBuf, String>;
61}
62
63/// Resolves a path that might contain special prefixes to an actual filesystem path.
64///
65/// Currently supported formats:
66/// - "go:github.com/user/repo" - Resolves to the Go module's filesystem path
67/// - "js:express" - Resolves to the JavaScript/Node.js package's filesystem path
68/// - "rust:serde" - Resolves to the Rust crate's filesystem path
69/// - "/dep/go/fmt" - Alternative notation for "go:fmt"
70/// - "/dep/js/express" - Alternative notation for "js:express"
71/// - "/dep/rust/serde" - Alternative notation for "rust:serde"
72///
73/// # Arguments
74///
75/// * `path` - The path to resolve, which might contain special prefixes
76///
77/// # Returns
78///
79/// * `Ok(PathBuf)` - The resolved filesystem path
80/// * `Err(String)` - An error message if resolution fails
81pub fn resolve_path(path: &str) -> Result<PathBuf, String> {
82    // Create instances of all resolvers
83    let resolvers: Vec<Box<dyn PathResolver>> = vec![
84        Box::new(GoPathResolver::new()),
85        Box::new(JavaScriptPathResolver::new()),
86        Box::new(RustPathResolver::new()),
87    ];
88
89    // Check for /dep/ prefix notation
90    if let Some(dep_path) = path.strip_prefix("/dep/") {
91        // Extract the language identifier (e.g., "go", "js", "rust")
92        let parts: Vec<&str> = dep_path.splitn(2, '/').collect();
93        if parts.is_empty() {
94            return Err("Invalid /dep/ path: missing language identifier".to_string());
95        }
96
97        let lang_id = parts[0];
98        let remainder = parts.get(1).unwrap_or(&"");
99
100        // Map language identifier to resolver prefix
101        let prefix = match lang_id {
102            "go" => "go:",
103            "js" => "js:",
104            "rust" => "rust:",
105            _ => {
106                return Err(format!(
107                    "Unknown language identifier in /dep/ path: {lang_id}"
108                ))
109            }
110        };
111
112        // Find the appropriate resolver
113        for resolver in &resolvers {
114            if resolver.prefix() == prefix {
115                // 1. Split the path into module name and optional subpath
116                let (module_name, subpath_opt) =
117                    resolver.split_module_and_subpath(remainder).map_err(|e| {
118                        format!("Failed to parse path '{remainder}' for prefix '{prefix}': {e}")
119                    })?;
120
121                // 2. Resolve the base directory of the module
122                let module_base_path = resolver.resolve(&module_name).map_err(|e| {
123                    format!("Failed to resolve module '{module_name}' for prefix '{prefix}': {e}")
124                })?;
125
126                // 3. Combine base path with subpath if it exists
127                let final_path = match subpath_opt {
128                    Some(sub) if !sub.is_empty() => {
129                        // Ensure subpath is treated as relative
130                        let relative_subpath = Path::new(&sub)
131                            .strip_prefix("/")
132                            .unwrap_or_else(|_| Path::new(&sub));
133                        module_base_path.join(relative_subpath)
134                    }
135                    _ => module_base_path, // No subpath or empty subpath
136                };
137
138                return Ok(final_path);
139            }
140        }
141
142        // This should not happen if all language identifiers are properly mapped
143        return Err(format!("No resolver found for language: {lang_id}"));
144    }
145
146    // Find the appropriate resolver based on the path prefix
147    for resolver in resolvers {
148        let prefix = resolver.prefix();
149        if !prefix.ends_with(':') {
150            // Internal sanity check
151            eprintln!("Warning: PathResolver prefix '{prefix}' does not end with ':'");
152            continue;
153        }
154
155        if let Some(full_path_after_prefix) = path.strip_prefix(prefix) {
156            // 1. Split the path into module name and optional subpath
157            let (module_name, subpath_opt) = resolver
158                .split_module_and_subpath(full_path_after_prefix)
159                .map_err(|e| {
160                    format!(
161                        "Failed to parse path '{full_path_after_prefix}' for prefix '{prefix}': {e}"
162                    )
163                })?;
164
165            // 2. Resolve the base directory of the module
166            let module_base_path = resolver.resolve(&module_name).map_err(|e| {
167                format!("Failed to resolve module '{module_name}' for prefix '{prefix}': {e}")
168            })?;
169
170            // 3. Combine base path with subpath if it exists
171            let final_path = match subpath_opt {
172                Some(sub) if !sub.is_empty() => {
173                    // Ensure subpath is treated as relative
174                    let relative_subpath = Path::new(&sub)
175                        .strip_prefix("/")
176                        .unwrap_or_else(|_| Path::new(&sub));
177                    module_base_path.join(relative_subpath)
178                }
179                _ => module_base_path, // No subpath or empty subpath
180            };
181
182            return Ok(final_path);
183        }
184    }
185
186    // If no special prefix, return the path as is
187    Ok(PathBuf::from(path))
188}
189
190#[cfg(test)]
191mod tests {
192    use super::*;
193    use std::process::Command;
194
195    #[test]
196    fn test_resolve_path_regular() {
197        let path = "/some/regular/path";
198        let result = resolve_path(path);
199        assert!(result.is_ok());
200        assert_eq!(result.unwrap(), PathBuf::from(path));
201    }
202
203    #[test]
204    fn test_resolve_path_dep_prefix() {
205        // Skip this test if go is not installed
206        if Command::new("go").arg("version").output().is_err() {
207            println!("Skipping test_resolve_path_dep_prefix: Go is not installed");
208            return;
209        }
210
211        // Test with standard library package using /dep/go prefix
212        let result = resolve_path("/dep/go/fmt");
213
214        // Compare with traditional go: prefix
215        let traditional_result = resolve_path("go:fmt");
216
217        assert!(
218            result.is_ok(),
219            "Failed to resolve '/dep/go/fmt': {result:?}"
220        );
221        assert!(
222            traditional_result.is_ok(),
223            "Failed to resolve 'go:fmt': {traditional_result:?}"
224        );
225
226        // Both paths should resolve to the same location
227        assert_eq!(result.unwrap(), traditional_result.unwrap());
228    }
229
230    #[test]
231    fn test_invalid_dep_path() {
232        // Test with invalid /dep/ path (missing language identifier)
233        let result = resolve_path("/dep/");
234        assert!(result.is_err());
235
236        // Test with unknown language identifier
237        let result = resolve_path("/dep/unknown/package");
238        assert!(result.is_err());
239    }
240}