ricecoder_research/dependency_analyzer/
go_parser.rs

1//! Go dependency parser for go.mod
2
3use crate::error::ResearchError;
4use crate::models::Dependency;
5use std::path::Path;
6use tracing::debug;
7
8/// Parses Go dependencies from go.mod
9#[derive(Debug)]
10pub struct GoParser;
11
12impl GoParser {
13    /// Creates a new GoParser
14    pub fn new() -> Self {
15        GoParser
16    }
17
18    /// Parses dependencies from go.mod
19    pub fn parse(&self, root: &Path) -> Result<Vec<Dependency>, ResearchError> {
20        let go_mod_path = root.join("go.mod");
21
22        if !go_mod_path.exists() {
23            return Ok(Vec::new());
24        }
25
26        debug!("Parsing Go dependencies from {:?}", go_mod_path);
27
28        let content = std::fs::read_to_string(&go_mod_path).map_err(|e| {
29            ResearchError::DependencyParsingFailed {
30                language: "Go".to_string(),
31                path: Some(go_mod_path.clone()),
32                reason: format!("Failed to read go.mod: {}", e),
33            }
34        })?;
35
36        let mut dependencies = Vec::new();
37        let mut in_require = false;
38
39        for line in content.lines() {
40            let line = line.trim();
41
42            // Skip empty lines and comments
43            if line.is_empty() || line.starts_with("//") {
44                continue;
45            }
46
47            // Check for require block
48            if line.starts_with("require") {
49                in_require = true;
50                if line == "require" {
51                    continue;
52                }
53                // Handle inline require: require (...)
54                if line.starts_with("require (") {
55                    continue;
56                }
57            }
58
59            // Check for end of require block
60            if in_require && line == ")" {
61                in_require = false;
62                continue;
63            }
64
65            // Parse require block entries
66            if in_require || (line.starts_with("require ") && !line.contains("(")) {
67                let line = if let Some(stripped) = line.strip_prefix("require ") {
68                    stripped
69                } else {
70                    line
71                };
72
73                if let Some(dep) = self.parse_require_line(line) {
74                    dependencies.push(dep);
75                }
76            }
77        }
78
79        Ok(dependencies)
80    }
81
82    /// Parses a single require line (e.g., "github.com/user/repo v1.2.3")
83    fn parse_require_line(&self, line: &str) -> Option<Dependency> {
84        let parts: Vec<&str> = line.split_whitespace().collect();
85
86        if parts.len() < 2 {
87            return None;
88        }
89
90        let name = parts[0];
91        let version = parts[1];
92
93        // Skip indirect marker if present
94        let version = if version == "indirect" && parts.len() > 2 {
95            parts[2]
96        } else {
97            version
98        };
99
100        Some(Dependency {
101            name: name.to_string(),
102            version: version.to_string(),
103            constraints: Some(version.to_string()),
104            is_dev: false,
105        })
106    }
107
108    /// Checks if go.mod exists
109    pub fn has_manifest(&self, root: &Path) -> bool {
110        root.join("go.mod").exists()
111    }
112}
113
114impl Default for GoParser {
115    fn default() -> Self {
116        Self::new()
117    }
118}
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123    use std::fs;
124    use tempfile::TempDir;
125
126    #[test]
127    fn test_go_parser_creation() {
128        let parser = GoParser::new();
129        assert!(true);
130    }
131
132    #[test]
133    fn test_go_parser_no_manifest() {
134        let parser = GoParser::new();
135        let temp_dir = TempDir::new().unwrap();
136        let result = parser.parse(temp_dir.path()).unwrap();
137        assert!(result.is_empty());
138    }
139
140    #[test]
141    fn test_go_parser_simple_dependencies() {
142        let parser = GoParser::new();
143        let temp_dir = TempDir::new().unwrap();
144
145        let go_mod = r#"module github.com/user/project
146
147go 1.21
148
149require (
150    github.com/gorilla/mux v1.8.0
151    github.com/lib/pq v1.10.9
152)
153"#;
154
155        fs::write(temp_dir.path().join("go.mod"), go_mod).unwrap();
156
157        let deps = parser.parse(temp_dir.path()).unwrap();
158        assert_eq!(deps.len(), 2);
159
160        let mux = deps
161            .iter()
162            .find(|d| d.name == "github.com/gorilla/mux")
163            .unwrap();
164        assert_eq!(mux.version, "v1.8.0");
165    }
166
167    #[test]
168    fn test_go_parser_has_manifest() {
169        let parser = GoParser::new();
170        let temp_dir = TempDir::new().unwrap();
171
172        assert!(!parser.has_manifest(temp_dir.path()));
173
174        fs::write(temp_dir.path().join("go.mod"), "module test").unwrap();
175        assert!(parser.has_manifest(temp_dir.path()));
176    }
177}