ricecoder_research/dependency_analyzer/
dart_parser.rs

1//! Dart/Flutter dependency parser for pubspec.yaml
2
3use crate::error::ResearchError;
4use crate::models::Dependency;
5use std::path::Path;
6use tracing::debug;
7
8/// Parses Dart/Flutter dependencies from pubspec.yaml
9#[derive(Debug)]
10pub struct DartParser;
11
12impl DartParser {
13    /// Creates a new DartParser
14    pub fn new() -> Self {
15        DartParser
16    }
17
18    /// Parses dependencies from pubspec.yaml
19    pub fn parse(&self, root: &Path) -> Result<Vec<Dependency>, ResearchError> {
20        let pubspec_path = root.join("pubspec.yaml");
21
22        if !pubspec_path.exists() {
23            return Ok(Vec::new());
24        }
25
26        debug!("Parsing Dart/Flutter dependencies from {:?}", pubspec_path);
27
28        let content = std::fs::read_to_string(&pubspec_path).map_err(|e| {
29            ResearchError::DependencyParsingFailed {
30                language: "Dart".to_string(),
31                path: Some(pubspec_path.clone()),
32                reason: format!("Failed to read pubspec.yaml: {}", e),
33            }
34        })?;
35
36        let pubspec: serde_yaml::Value =
37            serde_yaml::from_str(&content).map_err(|e| ResearchError::DependencyParsingFailed {
38                language: "Dart".to_string(),
39                path: Some(pubspec_path.clone()),
40                reason: format!("Failed to parse pubspec.yaml: {}", e),
41            })?;
42
43        let mut dependencies = Vec::new();
44
45        // Parse dependencies
46        if let Some(deps) = pubspec.get("dependencies").and_then(|d| d.as_mapping()) {
47            for (key, value) in deps {
48                if let Some(name) = key.as_str() {
49                    if name != "flutter" {
50                        let version = if let Some(version_str) = value.as_str() {
51                            version_str.to_string()
52                        } else if let Some(mapping) = value.as_mapping() {
53                            if let Some(version) =
54                                mapping.get(serde_yaml::Value::String("version".to_string()))
55                            {
56                                version.as_str().unwrap_or("*").to_string()
57                            } else {
58                                "*".to_string()
59                            }
60                        } else {
61                            "*".to_string()
62                        };
63
64                        dependencies.push(Dependency {
65                            name: name.to_string(),
66                            version: version.clone(),
67                            constraints: Some(version),
68                            is_dev: false,
69                        });
70                    }
71                }
72            }
73        }
74
75        // Parse dev_dependencies
76        if let Some(deps) = pubspec.get("dev_dependencies").and_then(|d| d.as_mapping()) {
77            for (key, value) in deps {
78                if let Some(name) = key.as_str() {
79                    let version = if let Some(version_str) = value.as_str() {
80                        version_str.to_string()
81                    } else if let Some(mapping) = value.as_mapping() {
82                        if let Some(version) =
83                            mapping.get(serde_yaml::Value::String("version".to_string()))
84                        {
85                            version.as_str().unwrap_or("*").to_string()
86                        } else {
87                            "*".to_string()
88                        }
89                    } else {
90                        "*".to_string()
91                    };
92
93                    dependencies.push(Dependency {
94                        name: name.to_string(),
95                        version: version.clone(),
96                        constraints: Some(version),
97                        is_dev: true,
98                    });
99                }
100            }
101        }
102
103        Ok(dependencies)
104    }
105
106    /// Checks if pubspec.yaml exists
107    pub fn has_manifest(&self, root: &Path) -> bool {
108        root.join("pubspec.yaml").exists()
109    }
110}
111
112impl Default for DartParser {
113    fn default() -> Self {
114        Self::new()
115    }
116}
117
118#[cfg(test)]
119mod tests {
120    use super::*;
121    use std::fs;
122    use tempfile::TempDir;
123
124    #[test]
125    fn test_dart_parser_creation() {
126        let _parser = DartParser::new();
127        assert!(true);
128    }
129
130    #[test]
131    fn test_dart_parser_no_manifest() {
132        let _parser = DartParser::new();
133        let temp_dir = TempDir::new().unwrap();
134        let result = _parser.parse(temp_dir.path()).unwrap();
135        assert!(result.is_empty());
136    }
137
138    #[test]
139    fn test_dart_parser_simple_dependencies() {
140        let parser = DartParser::new();
141        let temp_dir = TempDir::new().unwrap();
142
143        let pubspec = r#"
144name: my_app
145version: 1.0.0
146
147dependencies:
148  flutter:
149    sdk: flutter
150  provider: ^6.0.0
151  http: ^0.13.0
152
153dev_dependencies:
154  flutter_test:
155    sdk: flutter
156  test: ^1.20.0
157"#;
158
159        fs::write(temp_dir.path().join("pubspec.yaml"), pubspec).unwrap();
160
161        let deps = parser.parse(temp_dir.path()).unwrap();
162        assert!(deps.len() >= 2);
163
164        let provider = deps.iter().find(|d| d.name == "provider").unwrap();
165        assert_eq!(provider.version, "^6.0.0");
166        assert!(!provider.is_dev);
167    }
168
169    #[test]
170    fn test_dart_parser_has_manifest() {
171        let parser = DartParser::new();
172        let temp_dir = TempDir::new().unwrap();
173
174        assert!(!parser.has_manifest(temp_dir.path()));
175
176        fs::write(temp_dir.path().join("pubspec.yaml"), "").unwrap();
177        assert!(parser.has_manifest(temp_dir.path()));
178    }
179}