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}