deps_gradle/parser/
kotlin.rs1use 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_VERSION: OnceLock<Regex> = OnceLock::new();
14static RE_NO_VERSION: OnceLock<Regex> = OnceLock::new();
16
17const CONFIGURATIONS: &[&str] = &[
18 "implementation",
19 "api",
20 "compileOnly",
21 "runtimeOnly",
22 "testImplementation",
23 "testRuntimeOnly",
24 "annotationProcessor",
25 "kapt",
26 "classpath",
27 "ksp",
28 "testCompileOnly",
29];
30
31fn re_with_version() -> &'static Regex {
32 RE_WITH_VERSION
33 .get_or_init(|| Regex::new(r#"(\w+)\(\s*"([^:"\s]+):([^:"\s]+):([^"]+)"\s*\)"#).unwrap())
34}
35
36fn re_no_version() -> &'static Regex {
37 RE_NO_VERSION.get_or_init(|| Regex::new(r#"(\w+)\(\s*"([^:"\s]+):([^:"\s"]+)"\s*\)"#).unwrap())
38}
39
40pub fn parse_kotlin_dsl(content: &str, uri: &Uri) -> Result<GradleParseResult> {
41 let mut dependencies = Vec::new();
42
43 let mut brace_depth: i32 = 0;
45 let mut in_dependencies_block = false;
46 let mut deps_brace_depth: i32 = 0;
47
48 for (line_idx, line) in content.lines().enumerate() {
49 let trimmed = line.trim();
50
51 if !in_dependencies_block
53 && (trimmed == "dependencies {" || trimmed.starts_with("dependencies {"))
54 {
55 in_dependencies_block = true;
56 deps_brace_depth = brace_depth + 1;
57 }
58
59 for ch in line.chars() {
61 match ch {
62 '{' => brace_depth += 1,
63 '}' => {
64 brace_depth -= 1;
65 if in_dependencies_block && brace_depth < deps_brace_depth {
66 in_dependencies_block = false;
67 }
68 }
69 _ => {}
70 }
71 }
72
73 if !in_dependencies_block && !line.trim_start().starts_with("dependencies") {
74 continue;
75 }
76
77 let line_u32 = line_idx as u32;
78
79 for caps in re_with_version().captures_iter(line) {
81 let config = caps.get(1).map_or("", |m| m.as_str());
82 if !CONFIGURATIONS.contains(&config) {
83 continue;
84 }
85
86 let group_id = caps.get(2).map_or("", |m| m.as_str()).to_string();
87 let artifact_id = caps.get(3).map_or("", |m| m.as_str()).to_string();
88 let version = caps.get(4).map_or("", |m| m.as_str()).trim().to_string();
89 let name = format!("{group_id}:{artifact_id}");
90
91 let name_range = find_name_range(line, line_u32, &group_id, &artifact_id);
93 let version_range = find_version_range(line, line_u32, &version);
94
95 dependencies.push(GradleDependency {
96 group_id,
97 artifact_id,
98 name,
99 name_range,
100 version_req: Some(version),
101 version_range: Some(version_range),
102 configuration: config.to_string(),
103 });
104 }
105
106 let already_matched: Vec<_> = re_with_version()
109 .captures_iter(line)
110 .filter_map(|c| {
111 let config = c.get(1)?.as_str();
112 CONFIGURATIONS
113 .contains(&config)
114 .then_some(c.get(0)?.start())
115 })
116 .collect();
117
118 for caps in re_no_version().captures_iter(line) {
119 let config = caps.get(1).map_or("", |m| m.as_str());
120 if !CONFIGURATIONS.contains(&config) {
121 continue;
122 }
123 let match_start = caps.get(0).map_or(0, |m| m.start());
125 if already_matched.contains(&match_start) {
126 continue;
127 }
128
129 let group_id = caps.get(2).map_or("", |m| m.as_str()).to_string();
130 let artifact_id = caps.get(3).map_or("", |m| m.as_str()).to_string();
131 let name = format!("{group_id}:{artifact_id}");
132 let name_range = find_name_range(line, line_u32, &group_id, &artifact_id);
133
134 dependencies.push(GradleDependency {
135 group_id,
136 artifact_id,
137 name,
138 name_range,
139 version_req: None,
140 version_range: None,
141 configuration: config.to_string(),
142 });
143 }
144 }
145
146 Ok(GradleParseResult {
147 dependencies,
148 uri: uri.clone(),
149 })
150}
151
152#[cfg(test)]
153mod tests {
154 use super::*;
155
156 fn make_uri() -> Uri {
157 Uri::from_file_path("/project/build.gradle.kts").unwrap()
158 }
159
160 #[test]
161 fn test_parse_simple_kotlin() {
162 let content = r#"dependencies {
163 implementation("org.springframework.boot:spring-boot-starter:3.2.0")
164 testImplementation("junit:junit:4.13.2")
165}
166"#;
167 let result = parse_kotlin_dsl(content, &make_uri()).unwrap();
168 assert_eq!(result.dependencies.len(), 2);
169
170 let spring = &result.dependencies[0];
171 assert_eq!(spring.name, "org.springframework.boot:spring-boot-starter");
172 assert_eq!(spring.version_req, Some("3.2.0".into()));
173 assert_eq!(spring.configuration, "implementation");
174
175 let junit = &result.dependencies[1];
176 assert_eq!(junit.name, "junit:junit");
177 assert_eq!(junit.configuration, "testImplementation");
178 }
179
180 #[test]
181 fn test_parse_no_version() {
182 let content = r#"dependencies {
183 implementation("org.springframework.boot:spring-boot-starter")
184}
185"#;
186 let result = parse_kotlin_dsl(content, &make_uri()).unwrap();
187 assert_eq!(result.dependencies.len(), 1);
188 assert!(result.dependencies[0].version_req.is_none());
189 }
190
191 #[test]
192 fn test_ignore_non_dependency_configurations() {
193 let content = r#"dependencies {
194 implementation("a:b:1.0")
195 unknown("c:d:2.0")
196}
197"#;
198 let result = parse_kotlin_dsl(content, &make_uri()).unwrap();
199 assert_eq!(result.dependencies.len(), 1);
200 assert_eq!(result.dependencies[0].name, "a:b");
201 }
202
203 #[test]
204 fn test_parse_multiple_configurations() {
205 let content = r#"dependencies {
206 api("com.google.guava:guava:33.0.0-jre")
207 compileOnly("org.projectlombok:lombok:1.18.30")
208 runtimeOnly("mysql:mysql-connector-java:8.0.33")
209 kapt("com.google.dagger:dagger-compiler:2.51")
210}
211"#;
212 let result = parse_kotlin_dsl(content, &make_uri()).unwrap();
213 assert_eq!(result.dependencies.len(), 4);
214 assert_eq!(result.dependencies[0].configuration, "api");
215 assert_eq!(result.dependencies[1].configuration, "compileOnly");
216 assert_eq!(result.dependencies[2].configuration, "runtimeOnly");
217 assert_eq!(result.dependencies[3].configuration, "kapt");
218 }
219
220 #[test]
221 fn test_empty_dependencies_block() {
222 let content = "dependencies {\n}\n";
223 let result = parse_kotlin_dsl(content, &make_uri()).unwrap();
224 assert!(result.dependencies.is_empty());
225 }
226
227 #[test]
228 fn test_no_dependencies_block() {
229 let content = "plugins {\n id(\"java\")\n}\n";
230 let result = parse_kotlin_dsl(content, &make_uri()).unwrap();
231 assert!(result.dependencies.is_empty());
232 }
233
234 #[test]
235 fn test_position_tracking() {
236 let content = "dependencies {\n implementation(\"com.example:lib:1.0.0\")\n}\n";
237 let result = parse_kotlin_dsl(content, &make_uri()).unwrap();
238 assert_eq!(result.dependencies.len(), 1);
239 let dep = &result.dependencies[0];
240 assert_eq!(dep.name_range.start.line, 1);
242 assert!(dep.version_range.is_some());
243 assert_eq!(dep.version_range.unwrap().start.line, 1);
244 }
245}