ricecoder_research/dependency_analyzer/
rust_parser.rs

1//! Rust dependency parser for Cargo.toml
2
3use crate::error::ResearchError;
4use crate::models::Dependency;
5use std::path::Path;
6use tracing::debug;
7
8/// Parses Rust dependencies from Cargo.toml
9#[derive(Debug)]
10pub struct RustParser;
11
12impl RustParser {
13    /// Creates a new RustParser
14    pub fn new() -> Self {
15        RustParser
16    }
17
18    /// Parses dependencies from Cargo.toml
19    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        // Parse regular dependencies
46        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        // Parse dev dependencies
55        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        // Parse build dependencies
67        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        // Parse workspace dependencies if this is a workspace
79        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    /// Checks if Cargo.toml exists
93    pub fn has_manifest(&self, root: &Path) -> bool {
94        root.join("Cargo.toml").exists()
95    }
96
97    /// Parses a single dependency entry
98    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            // Simple version string
106            (version_str.to_string(), Some(version_str.to_string()))
107        } else if let Some(table) = value.as_table() {
108            // Complex dependency specification
109            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 dependency
113                ("path".to_string(), Some(format!("path: {}", path)))
114            } else if let Some(git) = table.get("git").and_then(|g| g.as_str()) {
115                // Git dependency
116                ("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        // Check serde
183        let serde = deps.iter().find(|d| d.name == "serde").unwrap();
184        assert_eq!(serde.version, "1.0");
185        assert!(!serde.is_dev);
186
187        // Check tokio
188        let tokio = deps.iter().find(|d| d.name == "tokio").unwrap();
189        assert_eq!(tokio.version, "1.0");
190        assert!(!tokio.is_dev);
191
192        // Check proptest
193        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}