normalize_manifest/
sbt.rs1use crate::{DeclaredDep, DepKind, ManifestError, ManifestParser, ParsedManifest};
9
10pub struct SbtParser;
12
13impl ManifestParser for SbtParser {
14 fn filename(&self) -> &'static str {
15 "build.sbt"
16 }
17
18 fn parse(&self, content: &str) -> Result<ParsedManifest, ManifestError> {
19 let mut name = None;
20 let mut version = None;
21 let mut deps = Vec::new();
22
23 let mut accumulator = String::new();
26 let mut paren_depth: i32 = 0;
27
28 for line in content.lines() {
29 let trimmed = line.trim();
30
31 if trimmed.is_empty() || trimmed.starts_with("//") {
32 if paren_depth == 0 && !accumulator.trim().is_empty() {
33 process_sbt_statement(accumulator.trim(), &mut name, &mut version, &mut deps);
34 accumulator.clear();
35 }
36 continue;
37 }
38
39 accumulator.push(' ');
40 accumulator.push_str(trimmed);
41
42 for ch in trimmed.chars() {
43 match ch {
44 '(' => paren_depth += 1,
45 ')' => {
46 paren_depth -= 1;
47 if paren_depth < 0 {
48 paren_depth = 0;
49 }
50 }
51 _ => {}
52 }
53 }
54
55 if paren_depth == 0 {
56 process_sbt_statement(accumulator.trim(), &mut name, &mut version, &mut deps);
57 accumulator.clear();
58 }
59 }
60 if !accumulator.trim().is_empty() {
61 process_sbt_statement(accumulator.trim(), &mut name, &mut version, &mut deps);
62 }
63
64 Ok(ParsedManifest {
65 ecosystem: "sbt",
66 name,
67 version,
68 dependencies: deps,
69 })
70 }
71}
72
73fn process_sbt_statement(
74 stmt: &str,
75 name: &mut Option<String>,
76 version: &mut Option<String>,
77 deps: &mut Vec<DeclaredDep>,
78) {
79 let stmt = stmt.trim();
80
81 if stmt.starts_with("name") && stmt.contains(":=") {
83 if let Some(val) = extract_sbt_string(stmt) {
84 *name = Some(val);
85 }
86 return;
87 }
88 if stmt.starts_with("version") && stmt.contains(":=") {
90 if let Some(val) = extract_sbt_string(stmt) {
91 *version = Some(val);
92 }
93 return;
94 }
95
96 if !stmt.contains("libraryDependencies") {
98 return;
99 }
100
101 parse_sbt_deps(stmt, deps);
104}
105
106fn parse_sbt_deps(stmt: &str, out: &mut Vec<DeclaredDep>) {
107 let strings = collect_quoted_strings(stmt);
109
110 let mut i = 0;
113 while i + 2 < strings.len() {
114 let org = &strings[i];
115 let artifact = &strings[i + 1];
116 let ver = &strings[i + 2];
117
118 let name = format!("{}:{}", org, artifact);
120 let version_req = Some(ver.clone());
121
122 let quoted_ver = format!("\"{}\"", ver);
124 let rest_idx = stmt
125 .find(quoted_ver.as_str())
126 .map(|p| p + quoted_ver.len())
127 .unwrap_or(stmt.len());
128 let rest = stmt[rest_idx..].trim();
129 let kind = if rest.starts_with('%') {
130 let scope_part = rest.trim_start_matches('%').trim();
131 if scope_part.starts_with("Test")
132 || scope_part.starts_with('"') && scope_part.contains("test")
133 {
134 DepKind::Dev
135 } else if scope_part.starts_with("Provided") {
136 DepKind::Optional
137 } else {
138 DepKind::Normal
139 }
140 } else {
141 DepKind::Normal
142 };
143
144 out.push(DeclaredDep {
145 name,
146 version_req,
147 kind,
148 });
149
150 i += 3;
151 }
152}
153
154fn collect_quoted_strings(s: &str) -> Vec<String> {
155 let mut result = Vec::new();
156 let mut chars = s.chars().peekable();
157 while let Some(ch) = chars.next() {
158 if ch == '"' {
159 let mut buf = String::new();
160 for inner in chars.by_ref() {
161 if inner == '"' {
162 break;
163 }
164 buf.push(inner);
165 }
166 result.push(buf);
167 }
168 }
169 result
170}
171
172fn extract_sbt_string(stmt: &str) -> Option<String> {
173 let start = stmt.find('"')? + 1;
174 let end = stmt[start..].find('"')?;
175 Some(stmt[start..start + end].to_string())
176}
177
178#[cfg(test)]
179mod tests {
180 use super::*;
181 use crate::ManifestParser;
182
183 #[test]
184 fn test_parse_build_sbt() {
185 let content = r#"
186name := "my-project"
187version := "0.1.0"
188scalaVersion := "2.13.12"
189
190libraryDependencies += "org.typelevel" %% "cats-core" % "2.10.0"
191libraryDependencies += "com.typesafe.akka" %% "akka-http" % "10.5.0"
192libraryDependencies += "org.scalatest" %% "scalatest" % "3.2.17" % Test
193"#;
194 let m = SbtParser.parse(content).unwrap();
195 assert_eq!(m.ecosystem, "sbt");
196 assert_eq!(m.name.as_deref(), Some("my-project"));
197 assert_eq!(m.version.as_deref(), Some("0.1.0"));
198 assert_eq!(m.dependencies.len(), 3);
199
200 let cats = m
201 .dependencies
202 .iter()
203 .find(|d| d.name.contains("cats-core"))
204 .unwrap();
205 assert_eq!(cats.version_req.as_deref(), Some("2.10.0"));
206 assert_eq!(cats.kind, DepKind::Normal);
207
208 let scalatest = m
209 .dependencies
210 .iter()
211 .find(|d| d.name.contains("scalatest"))
212 .unwrap();
213 assert_eq!(scalatest.kind, DepKind::Dev);
214 }
215
216 #[test]
217 fn test_sbt_seq_deps() {
218 let content = r#"
219libraryDependencies ++= Seq(
220 "org.typelevel" %% "cats-core" % "2.10.0",
221 "io.circe" %% "circe-core" % "0.14.6"
222)
223"#;
224 let m = SbtParser.parse(content).unwrap();
225 assert_eq!(m.dependencies.len(), 2);
226 }
227}