1use crate::error::Result;
6use crate::parser::{GradleParseResult, find_name_range, find_version_range};
7use crate::types::GradleDependency;
8use regex::Regex;
9use std::sync::OnceLock;
10use tower_lsp_server::ls_types::Uri;
11
12static RE_WITH_PARENS: OnceLock<Regex> = OnceLock::new();
14static RE_WITHOUT_PARENS: OnceLock<Regex> = OnceLock::new();
16static RE_NO_VERSION_WITHOUT_PARENS: OnceLock<Regex> = OnceLock::new();
18static RE_NO_VERSION_WITH_PARENS: OnceLock<Regex> = OnceLock::new();
20
21const CONFIGURATIONS: &[&str] = &[
22 "implementation",
23 "api",
24 "compileOnly",
25 "runtimeOnly",
26 "testImplementation",
27 "testRuntimeOnly",
28 "annotationProcessor",
29 "kapt",
30 "classpath",
31 "ksp",
32 "testCompileOnly",
33 "compile",
34 "testCompile",
35 "provided",
36];
37
38fn re_with_parens() -> &'static Regex {
39 RE_WITH_PARENS.get_or_init(|| {
40 Regex::new(r#"(\w+)\(\s*['"]([^:'"]+):([^:'"]+):([^'"]+)['"]\s*\)"#).unwrap()
41 })
42}
43
44fn re_without_parens() -> &'static Regex {
45 RE_WITHOUT_PARENS
46 .get_or_init(|| Regex::new(r#"(\w+)\s+['"]([^:'"]+):([^:'"]+):([^'"]+)['"]"#).unwrap())
47}
48
49fn re_no_version_without_parens() -> &'static Regex {
50 RE_NO_VERSION_WITHOUT_PARENS
51 .get_or_init(|| Regex::new(r#"(\w+)\s+['"]([^:'"]+):([^:'"]+)['"]"#).unwrap())
52}
53
54fn re_no_version_with_parens() -> &'static Regex {
55 RE_NO_VERSION_WITH_PARENS
56 .get_or_init(|| Regex::new(r#"(\w+)\(\s*['"]([^:'"]+):([^:'"]+)['"]\s*\)"#).unwrap())
57}
58
59pub fn parse_groovy_dsl(content: &str, uri: &Uri) -> Result<GradleParseResult> {
60 let mut dependencies = Vec::new();
61
62 let mut brace_depth: i32 = 0;
63 let mut in_dependencies_block = false;
64 let mut deps_brace_depth: i32 = 0;
65
66 for (line_idx, line) in content.lines().enumerate() {
67 let trimmed = line.trim();
68
69 if !in_dependencies_block
70 && (trimmed == "dependencies {" || trimmed.starts_with("dependencies {"))
71 {
72 in_dependencies_block = true;
73 deps_brace_depth = brace_depth + 1;
74 }
75
76 for ch in line.chars() {
77 match ch {
78 '{' => brace_depth += 1,
79 '}' => {
80 brace_depth -= 1;
81 if in_dependencies_block && brace_depth < deps_brace_depth {
82 in_dependencies_block = false;
83 }
84 }
85 _ => {}
86 }
87 }
88
89 if !in_dependencies_block && !line.trim_start().starts_with("dependencies") {
90 continue;
91 }
92
93 let line_u32 = line_idx as u32;
94 let mut matched_positions: Vec<usize> = Vec::new();
95
96 for caps in re_with_parens().captures_iter(line) {
98 let config = caps.get(1).map_or("", |m| m.as_str());
99 if !CONFIGURATIONS.contains(&config) {
100 continue;
101 }
102 let start = caps.get(0).map_or(0, |m| m.start());
103 matched_positions.push(start);
104
105 let group_id = caps.get(2).map_or("", |m| m.as_str()).to_string();
106 let artifact_id = caps.get(3).map_or("", |m| m.as_str()).to_string();
107 let version = caps.get(4).map_or("", |m| m.as_str()).trim().to_string();
108 let name = format!("{group_id}:{artifact_id}");
109
110 let name_range = find_name_range(line, line_u32, &group_id, &artifact_id);
111 let version_range = find_version_range(line, line_u32, &version);
112
113 dependencies.push(GradleDependency {
114 group_id,
115 artifact_id,
116 name,
117 name_range,
118 version_req: Some(version),
119 version_range: Some(version_range),
120 configuration: config.to_string(),
121 });
122 }
123
124 for caps in re_without_parens().captures_iter(line) {
126 let config = caps.get(1).map_or("", |m| m.as_str());
127 if !CONFIGURATIONS.contains(&config) {
128 continue;
129 }
130 let start = caps.get(0).map_or(0, |m| m.start());
131 if matched_positions.contains(&start) {
132 continue;
133 }
134 matched_positions.push(start);
135
136 let group_id = caps.get(2).map_or("", |m| m.as_str()).to_string();
137 let artifact_id = caps.get(3).map_or("", |m| m.as_str()).to_string();
138 let version = caps.get(4).map_or("", |m| m.as_str()).trim().to_string();
139 let name = format!("{group_id}:{artifact_id}");
140
141 let name_range = find_name_range(line, line_u32, &group_id, &artifact_id);
142 let version_range = find_version_range(line, line_u32, &version);
143
144 dependencies.push(GradleDependency {
145 group_id,
146 artifact_id,
147 name,
148 name_range,
149 version_req: Some(version),
150 version_range: Some(version_range),
151 configuration: config.to_string(),
152 });
153 }
154
155 for caps in re_no_version_with_parens().captures_iter(line) {
157 let config = caps.get(1).map_or("", |m| m.as_str());
158 if !CONFIGURATIONS.contains(&config) {
159 continue;
160 }
161 let start = caps.get(0).map_or(0, |m| m.start());
162 if matched_positions.contains(&start) {
163 continue;
164 }
165 matched_positions.push(start);
166
167 let group_id = caps.get(2).map_or("", |m| m.as_str()).to_string();
168 let artifact_id = caps.get(3).map_or("", |m| m.as_str()).to_string();
169 let name = format!("{group_id}:{artifact_id}");
170 let name_range = find_name_range(line, line_u32, &group_id, &artifact_id);
171
172 dependencies.push(GradleDependency {
173 group_id,
174 artifact_id,
175 name,
176 name_range,
177 version_req: None,
178 version_range: None,
179 configuration: config.to_string(),
180 });
181 }
182
183 for caps in re_no_version_without_parens().captures_iter(line) {
185 let config = caps.get(1).map_or("", |m| m.as_str());
186 if !CONFIGURATIONS.contains(&config) {
187 continue;
188 }
189 let start = caps.get(0).map_or(0, |m| m.start());
190 if matched_positions.contains(&start) {
191 continue;
192 }
193
194 let group_id = caps.get(2).map_or("", |m| m.as_str()).to_string();
195 let artifact_id = caps.get(3).map_or("", |m| m.as_str()).to_string();
196 let name = format!("{group_id}:{artifact_id}");
197 let name_range = find_name_range(line, line_u32, &group_id, &artifact_id);
198
199 dependencies.push(GradleDependency {
200 group_id,
201 artifact_id,
202 name,
203 name_range,
204 version_req: None,
205 version_range: None,
206 configuration: config.to_string(),
207 });
208 }
209 }
210
211 Ok(GradleParseResult {
212 dependencies,
213 uri: uri.clone(),
214 })
215}
216
217#[cfg(test)]
218mod tests {
219 use super::*;
220
221 fn make_uri() -> Uri {
222 Uri::from_file_path("/project/build.gradle").unwrap()
223 }
224
225 #[test]
226 fn test_parse_single_quotes() {
227 let content = "dependencies {\n implementation 'org.springframework.boot:spring-boot-starter:3.2.0'\n}\n";
228 let result = parse_groovy_dsl(content, &make_uri()).unwrap();
229 assert_eq!(result.dependencies.len(), 1);
230 assert_eq!(
231 result.dependencies[0].name,
232 "org.springframework.boot:spring-boot-starter"
233 );
234 assert_eq!(result.dependencies[0].version_req, Some("3.2.0".into()));
235 }
236
237 #[test]
238 fn test_parse_double_quotes() {
239 let content =
240 "dependencies {\n implementation \"com.google.guava:guava:33.0.0-jre\"\n}\n";
241 let result = parse_groovy_dsl(content, &make_uri()).unwrap();
242 assert_eq!(result.dependencies.len(), 1);
243 assert_eq!(result.dependencies[0].name, "com.google.guava:guava");
244 assert_eq!(
245 result.dependencies[0].version_req,
246 Some("33.0.0-jre".into())
247 );
248 }
249
250 #[test]
251 fn test_parse_with_parens() {
252 let content = "dependencies {\n implementation('junit:junit:4.13.2')\n}\n";
253 let result = parse_groovy_dsl(content, &make_uri()).unwrap();
254 assert_eq!(result.dependencies.len(), 1);
255 assert_eq!(result.dependencies[0].name, "junit:junit");
256 }
257
258 #[test]
259 fn test_parse_multiple_configurations() {
260 let content = "dependencies {\n implementation 'org.springframework.boot:spring-boot-starter:3.2.0'\n testImplementation 'junit:junit:4.13.2'\n compileOnly 'org.projectlombok:lombok:1.18.30'\n runtimeOnly 'mysql:mysql-connector-java:8.0.33'\n}\n";
261 let result = parse_groovy_dsl(content, &make_uri()).unwrap();
262 assert_eq!(result.dependencies.len(), 4);
263 assert_eq!(result.dependencies[1].configuration, "testImplementation");
264 assert_eq!(result.dependencies[2].configuration, "compileOnly");
265 }
266
267 #[test]
268 fn test_ignore_unknown_configurations() {
269 let content = "dependencies {\n implementation 'a:b:1.0'\n unknown 'c:d:2.0'\n}\n";
270 let result = parse_groovy_dsl(content, &make_uri()).unwrap();
271 assert_eq!(result.dependencies.len(), 1);
272 }
273
274 #[test]
275 fn test_parse_no_version() {
276 let content = "dependencies {\n implementation 'org.springframework.boot:spring-boot-starter'\n}\n";
277 let result = parse_groovy_dsl(content, &make_uri()).unwrap();
278 assert_eq!(result.dependencies.len(), 1);
279 assert!(result.dependencies[0].version_req.is_none());
280 }
281
282 #[test]
283 fn test_empty_block() {
284 let content = "dependencies {\n}\n";
285 let result = parse_groovy_dsl(content, &make_uri()).unwrap();
286 assert!(result.dependencies.is_empty());
287 }
288
289 #[test]
290 fn test_position_tracking() {
291 let content = "dependencies {\n implementation 'com.example:lib:1.0.0'\n}\n";
292 let result = parse_groovy_dsl(content, &make_uri()).unwrap();
293 assert_eq!(result.dependencies.len(), 1);
294 let dep = &result.dependencies[0];
295 assert_eq!(dep.name_range.start.line, 1);
296 assert!(dep.version_range.is_some());
297 }
298
299 #[test]
300 fn test_no_dependencies_block() {
301 let content = "apply plugin: 'java'\n";
302 let result = parse_groovy_dsl(content, &make_uri()).unwrap();
303 assert!(result.dependencies.is_empty());
304 }
305}