normalize_manifest/
r_description.rs1use crate::{DeclaredDep, DepKind, ManifestError, ManifestParser, ParsedManifest};
13
14pub struct RDescriptionParser;
16
17impl ManifestParser for RDescriptionParser {
18 fn filename(&self) -> &'static str {
19 "DESCRIPTION"
20 }
21
22 fn parse(&self, content: &str) -> Result<ParsedManifest, ManifestError> {
23 let mut name: Option<String> = None;
24 let mut version: Option<String> = None;
25 let mut deps: Vec<DeclaredDep> = Vec::new();
26
27 #[derive(Clone, Copy, PartialEq)]
28 enum Field {
29 None,
30 Imports,
31 Suggests,
32 Depends,
33 LinkingTo,
34 }
35
36 let mut current_field = Field::None;
37 let mut field_value = String::new();
39
40 let flush = |field: Field, value: &str, deps: &mut Vec<DeclaredDep>| {
41 if field == Field::None || value.is_empty() {
42 return;
43 }
44 let kind = match field {
45 Field::Imports => DepKind::Normal,
46 Field::Depends => DepKind::Normal,
47 Field::Suggests => DepKind::Dev,
48 Field::LinkingTo => DepKind::Build,
49 Field::None => return,
50 };
51 for entry in value.split(',') {
52 let entry = entry.trim();
53 if entry.is_empty() {
54 continue;
55 }
56 let dep_entry = parse_r_dep_entry(entry);
57 if dep_entry.pkg_name.is_empty() || dep_entry.pkg_name == "R" {
58 continue;
59 }
60 deps.push(DeclaredDep {
61 name: dep_entry.pkg_name,
62 version_req: dep_entry.version_req,
63 kind,
64 });
65 }
66 };
67
68 for line in content.lines() {
69 if line.starts_with(' ') || line.starts_with('\t') {
71 if current_field != Field::None {
72 if !field_value.is_empty() {
73 field_value.push(',');
74 }
75 field_value.push_str(line.trim());
76 }
77 continue;
78 }
79
80 flush(current_field, &field_value, &mut deps);
82 field_value.clear();
83 current_field = Field::None;
84
85 let trimmed = line.trim();
86 if trimmed.is_empty() {
87 continue;
88 }
89
90 if let Some(colon) = trimmed.find(':') {
91 let key = trimmed[..colon].trim();
92 let val = trimmed[colon + 1..].trim();
93
94 match key {
95 "Package" => name = Some(val.to_string()),
96 "Version" => version = Some(val.to_string()),
97 "Imports" => {
98 current_field = Field::Imports;
99 field_value = val.to_string();
100 }
101 "Suggests" => {
102 current_field = Field::Suggests;
103 field_value = val.to_string();
104 }
105 "Depends" => {
106 current_field = Field::Depends;
107 field_value = val.to_string();
108 }
109 "LinkingTo" => {
110 current_field = Field::LinkingTo;
111 field_value = val.to_string();
112 }
113 _ => {}
114 }
115 }
116 }
117
118 flush(current_field, &field_value, &mut deps);
120
121 Ok(ParsedManifest {
122 ecosystem: "cran",
123 name,
124 version,
125 dependencies: deps,
126 })
127 }
128}
129
130struct RDepEntry {
131 pkg_name: String,
132 version_req: Option<String>,
133}
134
135fn parse_r_dep_entry(s: &str) -> RDepEntry {
138 if let Some(paren) = s.find('(') {
139 let pkg_name = s[..paren].trim().to_string();
140 let rest = s[paren + 1..].trim();
141 let ver = rest.trim_end_matches(')').trim().to_string();
142 let version_req = if ver.is_empty() { None } else { Some(ver) };
143 RDepEntry {
144 pkg_name,
145 version_req,
146 }
147 } else {
148 RDepEntry {
149 pkg_name: s.trim().to_string(),
150 version_req: None,
151 }
152 }
153}
154
155#[cfg(test)]
156mod tests {
157 use super::*;
158 use crate::ManifestParser;
159
160 #[test]
161 fn test_parse_r_description() {
162 let content = r#"Package: mypackage
163Title: My R Package
164Version: 1.0.0
165Authors@R: person("Alice", "Smith", role = c("aut", "cre"))
166Description: Does things.
167Imports:
168 dplyr (>= 1.0.0),
169 ggplot2,
170 stringr (>= 1.4.0)
171Suggests:
172 testthat (>= 3.0.0),
173 knitr,
174 rmarkdown
175Depends:
176 R (>= 4.0.0)
177LinkingTo:
178 Rcpp
179License: MIT
180"#;
181 let m = RDescriptionParser.parse(content).unwrap();
182 assert_eq!(m.ecosystem, "cran");
183 assert_eq!(m.name.as_deref(), Some("mypackage"));
184 assert_eq!(m.version.as_deref(), Some("1.0.0"));
185
186 let dplyr = m.dependencies.iter().find(|d| d.name == "dplyr").unwrap();
187 assert_eq!(dplyr.kind, DepKind::Normal);
188 assert_eq!(dplyr.version_req.as_deref(), Some(">= 1.0.0"));
189
190 let ggplot = m.dependencies.iter().find(|d| d.name == "ggplot2").unwrap();
191 assert_eq!(ggplot.kind, DepKind::Normal);
192 assert!(ggplot.version_req.is_none());
193
194 let testthat = m
195 .dependencies
196 .iter()
197 .find(|d| d.name == "testthat")
198 .unwrap();
199 assert_eq!(testthat.kind, DepKind::Dev);
200 assert_eq!(testthat.version_req.as_deref(), Some(">= 3.0.0"));
201
202 let rcpp = m.dependencies.iter().find(|d| d.name == "Rcpp").unwrap();
203 assert_eq!(rcpp.kind, DepKind::Build);
204
205 assert!(!m.dependencies.iter().any(|d| d.name == "R"));
207 }
208
209 #[test]
210 fn test_inline_imports() {
211 let content = "Package: tiny\nVersion: 0.0.1\nImports: data.table, jsonlite\n";
213 let m = RDescriptionParser.parse(content).unwrap();
214 assert_eq!(m.dependencies.len(), 2);
215 assert!(m.dependencies.iter().any(|d| d.name == "data.table"));
216 assert!(m.dependencies.iter().any(|d| d.name == "jsonlite"));
217 }
218}