normalize_manifest/
cabal_project.rs1use crate::{DeclaredDep, DepKind, ManifestError, ManifestParser, ParsedManifest};
9
10pub struct CabalProjectParser;
12
13impl ManifestParser for CabalProjectParser {
14 fn filename(&self) -> &'static str {
15 "cabal.project"
16 }
17
18 fn parse(&self, content: &str) -> Result<ParsedManifest, ManifestError> {
19 let mut deps: Vec<DeclaredDep> = Vec::new();
20
21 #[derive(PartialEq)]
25 enum Stanza {
26 Other,
27 SourceRepoPackage,
28 }
29
30 let mut stanza = Stanza::Other;
31 let mut current_location: Option<String> = None;
32 let mut current_tag: Option<String> = None;
33
34 let flush =
35 |loc: &mut Option<String>, tag: &mut Option<String>, deps: &mut Vec<DeclaredDep>| {
36 if let Some(url) = loc.take() {
37 let dep_name = derive_name_from_url(&url);
38 deps.push(DeclaredDep {
39 name: dep_name,
40 version_req: tag.take(),
41 kind: DepKind::Normal,
42 });
43 } else {
44 tag.take();
45 }
46 };
47
48 for line in content.lines() {
49 let trimmed = line.trim();
50
51 if trimmed.is_empty() || trimmed.starts_with("--") {
53 continue;
54 }
55
56 if !line.starts_with(' ') && !line.starts_with('\t') {
58 if stanza == Stanza::SourceRepoPackage {
60 flush(&mut current_location, &mut current_tag, &mut deps);
61 }
62
63 let lower = trimmed.to_lowercase();
64 if lower.starts_with("source-repository-package") {
65 stanza = Stanza::SourceRepoPackage;
66 } else {
67 stanza = Stanza::Other;
68 }
69 continue;
70 }
71
72 if stanza == Stanza::SourceRepoPackage {
74 if let Some(rest) = trimmed.strip_prefix("location:") {
75 current_location = Some(rest.trim().to_string());
76 } else if let Some(rest) = trimmed.strip_prefix("tag:") {
77 current_tag = Some(rest.trim().to_string());
78 }
79 }
80 }
81
82 if stanza == Stanza::SourceRepoPackage {
84 flush(&mut current_location, &mut current_tag, &mut deps);
85 }
86
87 Ok(ParsedManifest {
88 ecosystem: "cabal",
89 name: None,
91 version: None,
92 dependencies: deps,
93 })
94 }
95}
96
97fn derive_name_from_url(url: &str) -> String {
102 let url = url.trim_end_matches('/');
103 let last = url.rsplit('/').next().unwrap_or(url);
104 last.trim_end_matches(".git").to_string()
105}
106
107#[cfg(test)]
108mod tests {
109 use super::*;
110 use crate::ManifestParser;
111
112 const SAMPLE: &str = r#"packages: ./
113 ./lib
114
115source-repository-package
116 type: git
117 location: https://github.com/someone/something.git
118 tag: abc123
119
120source-repository-package
121 type: git
122 location: https://github.com/another/pkg.git
123 tag: v2.0.0
124
125constraints: text ==1.2.4.0,
126 bytestring >=0.11
127"#;
128
129 #[test]
130 fn test_parse_cabal_project() {
131 let m = CabalProjectParser.parse(SAMPLE).unwrap();
132 assert_eq!(m.ecosystem, "cabal");
133 assert!(m.name.is_none());
134 assert!(m.version.is_none());
135
136 let names: Vec<&str> = m.dependencies.iter().map(|d| d.name.as_str()).collect();
137 assert!(names.contains(&"something"), "{names:?}");
138 assert!(names.contains(&"pkg"), "{names:?}");
139
140 let something = m
141 .dependencies
142 .iter()
143 .find(|d| d.name == "something")
144 .unwrap();
145 assert_eq!(something.version_req.as_deref(), Some("abc123"));
146 assert_eq!(something.kind, DepKind::Normal);
147
148 let pkg = m.dependencies.iter().find(|d| d.name == "pkg").unwrap();
149 assert_eq!(pkg.version_req.as_deref(), Some("v2.0.0"));
150 }
151
152 #[test]
153 fn test_no_source_repos() {
154 let content = r#"packages: ./
155
156constraints: base >=4.14
157"#;
158 let m = CabalProjectParser.parse(content).unwrap();
159 assert!(m.dependencies.is_empty());
160 }
161
162 #[test]
163 fn test_derive_name_from_url() {
164 assert_eq!(
165 derive_name_from_url("https://github.com/foo/bar.git"),
166 "bar"
167 );
168 assert_eq!(derive_name_from_url("https://github.com/foo/bar"), "bar");
169 assert_eq!(derive_name_from_url("https://github.com/foo/bar/"), "bar");
170 }
171}