ricecoder_research/dependency_analyzer/
rust_parser.rs1use crate::error::ResearchError;
4use crate::models::Dependency;
5use std::path::Path;
6use tracing::debug;
7
8#[derive(Debug)]
10pub struct RustParser;
11
12impl RustParser {
13 pub fn new() -> Self {
15 RustParser
16 }
17
18 pub fn parse(&self, root: &Path) -> Result<Vec<Dependency>, ResearchError> {
20 let cargo_toml_path = root.join("Cargo.toml");
21
22 if !cargo_toml_path.exists() {
23 return Ok(Vec::new());
24 }
25
26 debug!("Parsing Rust dependencies from {:?}", cargo_toml_path);
27
28 let content = std::fs::read_to_string(&cargo_toml_path).map_err(|e| {
29 ResearchError::DependencyParsingFailed {
30 language: "Rust".to_string(),
31 path: Some(cargo_toml_path.clone()),
32 reason: format!("Failed to read Cargo.toml: {}", e),
33 }
34 })?;
35
36 let cargo_toml: toml::Value =
37 toml::from_str(&content).map_err(|e| ResearchError::DependencyParsingFailed {
38 language: "Rust".to_string(),
39 path: Some(cargo_toml_path.clone()),
40 reason: format!("Failed to parse Cargo.toml: {}", e),
41 })?;
42
43 let mut dependencies = Vec::new();
44
45 if let Some(deps) = cargo_toml.get("dependencies").and_then(|d| d.as_table()) {
47 for (name, value) in deps {
48 if let Some(dep) = self.parse_dependency(name, value, false) {
49 dependencies.push(dep);
50 }
51 }
52 }
53
54 if let Some(deps) = cargo_toml
56 .get("dev-dependencies")
57 .and_then(|d| d.as_table())
58 {
59 for (name, value) in deps {
60 if let Some(dep) = self.parse_dependency(name, value, true) {
61 dependencies.push(dep);
62 }
63 }
64 }
65
66 if let Some(deps) = cargo_toml
68 .get("build-dependencies")
69 .and_then(|d| d.as_table())
70 {
71 for (name, value) in deps {
72 if let Some(dep) = self.parse_dependency(name, value, false) {
73 dependencies.push(dep);
74 }
75 }
76 }
77
78 if let Some(workspace) = cargo_toml.get("workspace") {
80 if let Some(deps) = workspace.get("dependencies").and_then(|d| d.as_table()) {
81 for (name, value) in deps {
82 if let Some(dep) = self.parse_dependency(name, value, false) {
83 dependencies.push(dep);
84 }
85 }
86 }
87 }
88
89 Ok(dependencies)
90 }
91
92 pub fn has_manifest(&self, root: &Path) -> bool {
94 root.join("Cargo.toml").exists()
95 }
96
97 fn parse_dependency(
99 &self,
100 name: &str,
101 value: &toml::Value,
102 is_dev: bool,
103 ) -> Option<Dependency> {
104 let (version, constraints) = if let Some(version_str) = value.as_str() {
105 (version_str.to_string(), Some(version_str.to_string()))
107 } else if let Some(table) = value.as_table() {
108 if let Some(version) = table.get("version").and_then(|v| v.as_str()) {
110 (version.to_string(), Some(version.to_string()))
111 } else if let Some(path) = table.get("path").and_then(|p| p.as_str()) {
112 ("path".to_string(), Some(format!("path: {}", path)))
114 } else if let Some(git) = table.get("git").and_then(|g| g.as_str()) {
115 ("git".to_string(), Some(format!("git: {}", git)))
117 } else {
118 return None;
119 }
120 } else {
121 return None;
122 };
123
124 Some(Dependency {
125 name: name.to_string(),
126 version,
127 constraints,
128 is_dev,
129 })
130 }
131}
132
133impl Default for RustParser {
134 fn default() -> Self {
135 Self::new()
136 }
137}
138
139#[cfg(test)]
140mod tests {
141 use super::*;
142 use std::fs;
143 use tempfile::TempDir;
144
145 #[test]
146 fn test_rust_parser_creation() {
147 let parser = RustParser::new();
148 assert!(true);
149 }
150
151 #[test]
152 fn test_rust_parser_no_manifest() {
153 let parser = RustParser::new();
154 let temp_dir = TempDir::new().unwrap();
155 let result = parser.parse(temp_dir.path()).unwrap();
156 assert!(result.is_empty());
157 }
158
159 #[test]
160 fn test_rust_parser_simple_dependencies() {
161 let parser = RustParser::new();
162 let temp_dir = TempDir::new().unwrap();
163
164 let cargo_toml = r#"
165[package]
166name = "test"
167version = "0.1.0"
168
169[dependencies]
170serde = "1.0"
171tokio = { version = "1.0", features = ["full"] }
172
173[dev-dependencies]
174proptest = "1.0"
175"#;
176
177 fs::write(temp_dir.path().join("Cargo.toml"), cargo_toml).unwrap();
178
179 let deps = parser.parse(temp_dir.path()).unwrap();
180 assert_eq!(deps.len(), 3);
181
182 let serde = deps.iter().find(|d| d.name == "serde").unwrap();
184 assert_eq!(serde.version, "1.0");
185 assert!(!serde.is_dev);
186
187 let tokio = deps.iter().find(|d| d.name == "tokio").unwrap();
189 assert_eq!(tokio.version, "1.0");
190 assert!(!tokio.is_dev);
191
192 let proptest = deps.iter().find(|d| d.name == "proptest").unwrap();
194 assert_eq!(proptest.version, "1.0");
195 assert!(proptest.is_dev);
196 }
197
198 #[test]
199 fn test_rust_parser_has_manifest() {
200 let parser = RustParser::new();
201 let temp_dir = TempDir::new().unwrap();
202
203 assert!(!parser.has_manifest(temp_dir.path()));
204
205 fs::write(
206 temp_dir.path().join("Cargo.toml"),
207 "[package]\nname = \"test\"",
208 )
209 .unwrap();
210 assert!(parser.has_manifest(temp_dir.path()));
211 }
212}