normalize_manifest/
pubspec.rs1use crate::{DeclaredDep, DepKind, ManifestError, ManifestParser, ParsedManifest};
7
8pub struct PubspecParser;
10
11impl ManifestParser for PubspecParser {
12 fn filename(&self) -> &'static str {
13 "pubspec.yaml"
14 }
15
16 fn parse(&self, content: &str) -> Result<ParsedManifest, ManifestError> {
17 let mut name = None;
18 let mut version = None;
19 let mut deps = Vec::new();
20
21 #[derive(PartialEq, Clone, Copy)]
22 enum Section {
23 None,
24 Deps,
25 DevDeps,
26 }
27
28 let mut section = Section::None;
29 let mut section_indent = 0usize;
30 let mut dep_name: Option<(String, DepKind)> = None;
31
32 for line in content.lines() {
33 if line.trim().is_empty() || line.trim().starts_with('#') {
34 continue;
35 }
36
37 let indent = leading_spaces(line);
38 let trimmed = line.trim();
39
40 if indent == 0 {
42 if let Some((dname, dkind)) = dep_name.take() {
44 deps.push(DeclaredDep {
45 name: dname,
46 version_req: None,
47 kind: dkind,
48 });
49 }
50
51 if let Some(rest) = trimmed.strip_prefix("name:") {
52 name = rest
53 .trim()
54 .trim_matches('"')
55 .trim_matches('\'')
56 .to_string()
57 .into();
58 section = Section::None;
59 } else if let Some(rest) = trimmed.strip_prefix("version:") {
60 version = rest
61 .trim()
62 .trim_matches('"')
63 .trim_matches('\'')
64 .to_string()
65 .into();
66 section = Section::None;
67 } else if trimmed == "dependencies:" {
68 section = Section::Deps;
69 section_indent = 0;
70 } else if trimmed == "dev_dependencies:" {
71 section = Section::DevDeps;
72 section_indent = 0;
73 } else {
74 section = Section::None;
75 }
76 continue;
77 }
78
79 if section == Section::None {
80 continue;
81 }
82
83 let kind = if section == Section::DevDeps {
84 DepKind::Dev
85 } else {
86 DepKind::Normal
87 };
88
89 if section_indent == 0 || indent == section_indent {
91 if let Some((dname, dkind)) = dep_name.take() {
93 deps.push(DeclaredDep {
94 name: dname,
95 version_req: None,
96 kind: dkind,
97 });
98 }
99
100 section_indent = indent;
101
102 if let Some(colon_idx) = trimmed.find(':') {
104 let pkg_name = trimmed[..colon_idx].trim().to_string();
105 let after_colon = trimmed[colon_idx + 1..].trim();
106
107 if after_colon == "sdk: flutter"
109 || after_colon == "flutter"
110 || after_colon.starts_with("sdk:")
111 {
112 continue;
113 }
114
115 if after_colon.is_empty() {
116 dep_name = Some((pkg_name, kind));
118 } else {
119 deps.push(DeclaredDep {
121 name: pkg_name,
122 version_req: Some(after_colon.to_string()),
123 kind,
124 });
125 }
126 }
127 } else if let Some((ref dname, dkind)) = dep_name {
128 if trimmed.starts_with("sdk:") {
130 dep_name = None;
132 } else if let Some(ver_rest) = trimmed.strip_prefix("version:") {
133 let ver = ver_rest.trim().to_string();
134 deps.push(DeclaredDep {
135 name: dname.clone(),
136 version_req: if ver.is_empty() { None } else { Some(ver) },
137 kind: dkind,
138 });
139 dep_name = None;
140 }
141 }
143 }
144
145 if let Some((dname, dkind)) = dep_name.take() {
147 deps.push(DeclaredDep {
148 name: dname,
149 version_req: None,
150 kind: dkind,
151 });
152 }
153
154 Ok(ParsedManifest {
155 ecosystem: "pub",
156 name,
157 version,
158 dependencies: deps,
159 })
160 }
161}
162
163fn leading_spaces(line: &str) -> usize {
164 line.len() - line.trim_start().len()
165}
166
167#[cfg(test)]
168mod tests {
169 use super::*;
170 use crate::ManifestParser;
171
172 #[test]
173 fn test_parse_pubspec_yaml() {
174 let content = r#"name: my_flutter_app
175version: 1.0.0+1
176description: A new Flutter project.
177
178dependencies:
179 flutter:
180 sdk: flutter
181 http: ^0.13.0
182 provider:
183 version: ^6.0.0
184 path_provider: ^2.0.0
185
186dev_dependencies:
187 flutter_test:
188 sdk: flutter
189 mockito: ^5.0.0
190"#;
191 let m = PubspecParser.parse(content).unwrap();
192 assert_eq!(m.ecosystem, "pub");
193 assert_eq!(m.name.as_deref(), Some("my_flutter_app"));
194 assert_eq!(m.version.as_deref(), Some("1.0.0+1"));
195
196 let normal: Vec<_> = m
197 .dependencies
198 .iter()
199 .filter(|d| d.kind == DepKind::Normal)
200 .collect();
201 assert_eq!(normal.len(), 3);
203 assert!(normal.iter().any(|d| d.name == "http"));
204
205 let http = m.dependencies.iter().find(|d| d.name == "http").unwrap();
206 assert_eq!(http.version_req.as_deref(), Some("^0.13.0"));
207
208 let dev: Vec<_> = m
209 .dependencies
210 .iter()
211 .filter(|d| d.kind == DepKind::Dev)
212 .collect();
213 assert_eq!(dev.len(), 1);
215 assert_eq!(dev[0].name, "mockito");
216 }
217}