ricecoder_research/dependency_analyzer/
go_parser.rs1use crate::error::ResearchError;
4use crate::models::Dependency;
5use std::path::Path;
6use tracing::debug;
7
8#[derive(Debug)]
10pub struct GoParser;
11
12impl GoParser {
13 pub fn new() -> Self {
15 GoParser
16 }
17
18 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 if line.is_empty() || line.starts_with("//") {
44 continue;
45 }
46
47 if line.starts_with("require") {
49 in_require = true;
50 if line == "require" {
51 continue;
52 }
53 if line.starts_with("require (") {
55 continue;
56 }
57 }
58
59 if in_require && line == ")" {
61 in_require = false;
62 continue;
63 }
64
65 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 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 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 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}