normalize_manifest/
cabal.rs1use crate::{DeclaredDep, DepKind, ManifestError, ManifestParser, ParsedManifest};
7
8pub struct CabalParser;
14
15impl ManifestParser for CabalParser {
16 fn filename(&self) -> &'static str {
17 "*.cabal"
18 }
19
20 fn parse(&self, content: &str) -> Result<ParsedManifest, ManifestError> {
21 let mut name = None;
22 let mut version = None;
23 let mut deps: Vec<DeclaredDep> = Vec::new();
24 let mut in_build_depends = false;
25 let mut is_test = false;
26
27 for line in content.lines() {
28 let trimmed = line.trim();
29
30 if trimmed.is_empty() {
31 in_build_depends = false;
32 continue;
33 }
34 if trimmed.starts_with("--") {
35 continue;
36 }
37
38 let lower = trimmed.to_ascii_lowercase();
39
40 if lower.starts_with("name:") && name.is_none() {
42 name = Some(trimmed["name:".len()..].trim().to_string());
43 continue;
44 }
45 if lower.starts_with("version:") && version.is_none() {
46 version = Some(trimmed["version:".len()..].trim().to_string());
47 continue;
48 }
49
50 if lower.starts_with("test-suite") || lower.starts_with("benchmark") {
52 is_test = true;
53 }
54 if lower.starts_with("library") || lower.starts_with("executable") {
55 is_test = false;
56 }
57
58 if lower.starts_with("build-depends:") {
60 in_build_depends = true;
61 let rest = &trimmed["build-depends:".len()..];
62 extract_cabal_deps(rest, is_test, &mut deps);
63 continue;
64 }
65
66 if in_build_depends {
67 if line.starts_with([' ', '\t']) || trimmed.starts_with(',') {
69 extract_cabal_deps(trimmed, is_test, &mut deps);
70 } else {
71 in_build_depends = false;
72 }
73 }
74 }
75
76 deps.dedup_by(|a, b| a.name == b.name && a.kind == b.kind);
78
79 Ok(ParsedManifest {
80 ecosystem: "cabal",
81 name,
82 version,
83 dependencies: deps,
84 })
85 }
86}
87
88fn extract_cabal_deps(line: &str, is_test: bool, out: &mut Vec<DeclaredDep>) {
89 let kind = if is_test {
90 DepKind::Dev
91 } else {
92 DepKind::Normal
93 };
94
95 for part in line.split(',') {
96 let part = part.trim().trim_start_matches(',').trim();
97 if part.is_empty() || part.starts_with("--") {
98 continue;
99 }
100
101 let mut tokens = part.splitn(2, ['>', '<', '=', '&', '!']);
104 let name_part = tokens.next().unwrap_or("").trim();
105
106 let name = name_part
108 .trim_end_matches(['>', '<', '=', '~', ' '])
109 .to_string();
110
111 if name.is_empty() || name == "base" {
112 continue;
114 }
115
116 let version_req = part
118 .find(['>', '<', '='])
119 .map(|idx| part[idx..].trim().to_string());
120
121 out.push(DeclaredDep {
122 name,
123 version_req,
124 kind,
125 });
126 }
127}
128
129#[cfg(test)]
130mod tests {
131 use super::*;
132 use crate::ManifestParser;
133
134 #[test]
135 fn test_parse_cabal() {
136 let content = r#"cabal-version: 2.4
137name: my-package
138version: 0.1.0.0
139license: MIT
140
141library
142 exposed-modules: MyLib
143 build-depends:
144 base >= 4.14 && < 5,
145 text >= 1.2 && < 2.1,
146 aeson >= 2.0
147
148test-suite my-test
149 type: exitcode-stdio-1.0
150 build-depends:
151 base,
152 hspec >= 2.11
153"#;
154 let m = CabalParser.parse(content).unwrap();
155 assert_eq!(m.ecosystem, "cabal");
156 assert_eq!(m.name.as_deref(), Some("my-package"));
157 assert_eq!(m.version.as_deref(), Some("0.1.0.0"));
158
159 assert!(!m.dependencies.iter().any(|d| d.name == "base"));
161
162 let text = m.dependencies.iter().find(|d| d.name == "text").unwrap();
163 assert_eq!(text.kind, DepKind::Normal);
164
165 let hspec = m.dependencies.iter().find(|d| d.name == "hspec").unwrap();
166 assert_eq!(hspec.kind, DepKind::Dev);
167 }
168}