probe_code/path_resolver/
go.rs

1//! Go-specific path resolver implementation.
2
3use super::PathResolver;
4use serde_json::Value;
5use std::path::PathBuf;
6use std::process::Command;
7
8/// A path resolver for Go packages.
9pub struct GoPathResolver;
10
11impl Default for GoPathResolver {
12    fn default() -> Self {
13        Self::new()
14    }
15}
16
17impl GoPathResolver {
18    /// Creates a new Go path resolver.
19    pub fn new() -> Self {
20        GoPathResolver
21    }
22}
23
24impl PathResolver for GoPathResolver {
25    fn prefix(&self) -> &'static str {
26        "go:"
27    }
28
29    fn split_module_and_subpath(
30        &self,
31        full_path_after_prefix: &str,
32    ) -> Result<(String, Option<String>), String> {
33        if full_path_after_prefix.is_empty() {
34            return Err("Go path cannot be empty".to_string());
35        }
36        if full_path_after_prefix.contains("..") {
37            return Err("Go path cannot contain '..'".to_string());
38        }
39
40        // Trim potential trailing slash
41        let path = full_path_after_prefix.trim_end_matches('/');
42
43        // Common external host heuristic
44        let parts: Vec<&str> = path.split('/').collect();
45        let is_common_external = parts.len() >= 3
46            && (parts[0] == "github.com"
47                || parts[0] == "gitlab.com"
48                || parts[0] == "bitbucket.org"
49                || (parts[0] == "golang.org" && parts[1] == "x"));
50
51        if is_common_external {
52            // Assume module is host/user_or_x/repo_or_pkg (first 3 parts)
53            let module_name = parts[..3].join("/");
54            let subpath = if parts.len() > 3 {
55                Some(parts[3..].join("/")).filter(|s| !s.is_empty()) // Ensure subpath isn't just ""
56            } else {
57                None
58            };
59            Ok((module_name, subpath))
60        } else {
61            // For standard library packages, we need to handle file paths specially
62            // Check if the last part looks like a file (has an extension)
63            if parts.len() > 1 && parts.last().unwrap().contains('.') {
64                // Assume the last part is a file and everything before is the module
65                let file_part = parts.last().unwrap();
66                let module_parts = &parts[..parts.len() - 1];
67                let module_name = module_parts.join("/");
68                Ok((module_name, Some(file_part.to_string())))
69            } else {
70                // Fallback: Assume the *entire* path is the module identifier
71                // This covers:
72                // - Simple stdlib ("fmt")
73                // - Stdlib with slashes ("net/http", "net/http/pprof")
74                // - Less common external paths ("mycorp.com/internal/pkg")
75                Ok((path.to_string(), None))
76            }
77        }
78    }
79
80    fn resolve(&self, module_name: &str) -> Result<PathBuf, String> {
81        // Check if Go is installed before trying to run it
82        if Command::new("go").arg("version").output().is_err() {
83            return Err(
84                "Go command not found. Please ensure Go is installed and in your PATH.".to_string(),
85            );
86        }
87
88        // Run `go list -json <import-path>`
89        let output = Command::new("go")
90            .args(["list", "-json", module_name])
91            .output()
92            .map_err(|e| format!("Failed to execute 'go list': {e}"))?;
93
94        if !output.status.success() {
95            return Err(format!(
96                "Error running 'go list': {}",
97                String::from_utf8_lossy(&output.stderr)
98            ));
99        }
100
101        let json_str = String::from_utf8_lossy(&output.stdout);
102        let json: Value = serde_json::from_str(&json_str)
103            .map_err(|e| format!("Failed to parse JSON output from 'go list': {e}"))?;
104
105        // Extract the directory path
106        if let Some(dir) = json["Dir"].as_str() {
107            Ok(PathBuf::from(dir))
108        } else {
109            Err(format!("No directory found for Go package: {module_name}"))
110        }
111    }
112}
113
114#[cfg(test)]
115mod tests {
116    use super::*;
117
118    #[test]
119    fn test_go_path_resolver() {
120        // Skip this test if go is not installed
121        if Command::new("go").arg("version").output().is_err() {
122            println!("Skipping test_go_path_resolver: Go is not installed");
123            return;
124        }
125
126        let resolver = GoPathResolver::new();
127
128        // Test with a standard library package
129        let result = resolver.resolve("fmt");
130        assert!(
131            result.is_ok(),
132            "Failed to resolve 'fmt' package: {result:?}"
133        );
134
135        // The path should exist and contain the package
136        let path = result.unwrap();
137        assert!(path.exists(), "Path does not exist: {path:?}");
138    }
139}