1pub mod catalog;
6pub mod groovy;
7pub mod kotlin;
8pub mod properties;
9pub mod settings;
10
11use crate::error::Result;
12use crate::types::GradleDependency;
13use std::any::Any;
14use std::collections::HashMap;
15use tower_lsp_server::ls_types::{Position, Range, Uri};
16
17pub use deps_core::lsp_helpers::LineOffsetTable;
18
19pub struct GradleParseResult {
20 pub dependencies: Vec<GradleDependency>,
21 pub uri: Uri,
22}
23
24pub fn resolve_variables(deps: &mut [GradleDependency], properties: &HashMap<String, String>) {
30 for dep in deps.iter_mut() {
31 if let Some(ref ver) = dep.version_req
32 && let Some(resolved) = resolve_variable_ref(ver, properties)
33 {
34 dep.version_req = Some(resolved);
35 }
36 }
37}
38
39fn resolve_variable_ref(value: &str, properties: &HashMap<String, String>) -> Option<String> {
41 let trimmed = value.trim();
42 if let Some(name) = trimmed.strip_prefix("${").and_then(|s| s.strip_suffix('}')) {
43 properties.get(name).cloned()
44 } else if let Some(name) = trimmed.strip_prefix('$') {
45 properties.get(name).cloned()
46 } else {
47 None
48 }
49}
50
51pub fn parse_gradle(content: &str, uri: &Uri) -> Result<GradleParseResult> {
52 let path = uri.path().to_string();
53 let mut result = if path.ends_with("libs.versions.toml") {
54 catalog::parse_version_catalog(content, uri)?
55 } else if path.ends_with("settings.gradle.kts") || path.ends_with("settings.gradle") {
56 settings::parse_settings(content, uri)?
57 } else if path.ends_with(".gradle.kts") {
58 kotlin::parse_kotlin_dsl(content, uri)?
59 } else if path.ends_with(".gradle") {
60 groovy::parse_groovy_dsl(content, uri)?
61 } else {
62 return Ok(GradleParseResult {
63 dependencies: vec![],
64 uri: uri.clone(),
65 });
66 };
67
68 if (path.ends_with("build.gradle.kts") || path.ends_with("build.gradle"))
70 && let Some(dir) = std::path::Path::new(&path).parent()
71 {
72 let props = properties::load_gradle_properties(dir);
73 if !props.is_empty() {
74 resolve_variables(&mut result.dependencies, &props);
75 }
76 }
77
78 Ok(result)
79}
80
81impl deps_core::ParseResult for GradleParseResult {
82 fn dependencies(&self) -> Vec<&dyn deps_core::Dependency> {
83 self.dependencies
84 .iter()
85 .map(|d| d as &dyn deps_core::Dependency)
86 .collect()
87 }
88
89 fn workspace_root(&self) -> Option<&std::path::Path> {
90 None
91 }
92
93 fn uri(&self) -> &Uri {
94 &self.uri
95 }
96
97 fn as_any(&self) -> &dyn Any {
98 self
99 }
100}
101
102pub(crate) fn utf16_len(s: &str) -> usize {
104 s.chars().map(|c| c.len_utf16()).sum()
105}
106
107pub(crate) fn find_name_range(
109 line: &str,
110 line_idx: u32,
111 group_id: &str,
112 artifact_id: &str,
113) -> Range {
114 let search = format!("{group_id}:{artifact_id}");
115 if let Some(col) = line.find(&search) {
116 let col_u32 = utf16_len(&line[..col]) as u32;
117 let end_u32 = col_u32 + utf16_len(&search) as u32;
118 Range::new(
119 Position::new(line_idx, col_u32),
120 Position::new(line_idx, end_u32),
121 )
122 } else {
123 Range::default()
124 }
125}
126
127pub(crate) fn find_version_range(line: &str, line_idx: u32, version: &str) -> Range {
129 let second_colon = line
130 .char_indices()
131 .filter(|(_, c)| *c == ':')
132 .nth(1)
133 .map(|(i, _)| i);
134
135 if let Some(colon_pos) = second_colon {
136 let after_colon = &line[colon_pos + 1..];
137 if let Some(rel) = after_colon.find(version) {
138 let abs_start = colon_pos + 1 + rel;
139 let col_start = utf16_len(&line[..abs_start]) as u32;
140 let col_end = col_start + utf16_len(version) as u32;
141 return Range::new(
142 Position::new(line_idx, col_start),
143 Position::new(line_idx, col_end),
144 );
145 }
146 }
147 Range::default()
148}
149
150#[cfg(test)]
151mod tests {
152 use super::*;
153
154 fn make_uri(path: &str) -> Uri {
155 Uri::from_file_path(path).unwrap()
156 }
157
158 #[test]
159 fn test_dispatch_catalog() {
160 let content = "[versions]\nspring = \"3.2.0\"\n\n[libraries]\nspring-boot = { module = \"org.springframework.boot:spring-boot-starter\", version.ref = \"spring\" }\n";
161 let uri = make_uri("/project/gradle/libs.versions.toml");
162 let result = parse_gradle(content, &uri).unwrap();
163 assert!(!result.dependencies.is_empty());
164 }
165
166 #[test]
167 fn test_dispatch_kotlin() {
168 let content = "dependencies {\n implementation(\"org.springframework.boot:spring-boot-starter:3.2.0\")\n}\n";
169 let uri = make_uri("/project/build.gradle.kts");
170 let result = parse_gradle(content, &uri).unwrap();
171 assert_eq!(result.dependencies.len(), 1);
172 }
173
174 #[test]
175 fn test_dispatch_groovy() {
176 let content = "dependencies {\n implementation 'org.springframework.boot:spring-boot-starter:3.2.0'\n}\n";
177 let uri = make_uri("/project/build.gradle");
178 let result = parse_gradle(content, &uri).unwrap();
179 assert_eq!(result.dependencies.len(), 1);
180 }
181
182 #[test]
183 fn test_dispatch_settings_gradle() {
184 let content = "pluginManagement {\n plugins {\n id \"org.jetbrains.kotlin.jvm\" version \"2.1.10\"\n }\n}\n";
185 let uri = make_uri("/project/settings.gradle");
186 let result = parse_gradle(content, &uri).unwrap();
187 assert_eq!(result.dependencies.len(), 1);
188 }
189
190 #[test]
191 fn test_dispatch_settings_gradle_kts() {
192 let content = "pluginManagement {\n plugins {\n id(\"org.springframework.boot\") version \"3.2.0\"\n }\n}\n";
193 let uri = make_uri("/project/settings.gradle.kts");
194 let result = parse_gradle(content, &uri).unwrap();
195 assert_eq!(result.dependencies.len(), 1);
196 }
197
198 #[test]
199 fn test_dispatch_unknown() {
200 let uri = make_uri("/project/something.xml");
201 let result = parse_gradle("", &uri).unwrap();
202 assert!(result.dependencies.is_empty());
203 }
204
205 #[test]
206 fn test_resolve_variables_dollar_brace() {
207 let props: HashMap<String, String> =
208 [("kotlinVersion".to_string(), "2.1.10".to_string())].into();
209 let mut deps = vec![GradleDependency {
210 group_id: "org.jetbrains.kotlin".into(),
211 artifact_id: "kotlin-stdlib".into(),
212 name: "org.jetbrains.kotlin:kotlin-stdlib".into(),
213 name_range: Range::default(),
214 version_req: Some("${kotlinVersion}".into()),
215 version_range: None,
216 configuration: "implementation".into(),
217 }];
218 resolve_variables(&mut deps, &props);
219 assert_eq!(deps[0].version_req, Some("2.1.10".into()));
220 }
221
222 #[test]
223 fn test_resolve_variables_dollar_plain() {
224 let props: HashMap<String, String> =
225 [("springVersion".to_string(), "3.2.0".to_string())].into();
226 let mut deps = vec![GradleDependency {
227 group_id: "org.springframework.boot".into(),
228 artifact_id: "spring-boot-starter".into(),
229 name: "org.springframework.boot:spring-boot-starter".into(),
230 name_range: Range::default(),
231 version_req: Some("$springVersion".into()),
232 version_range: None,
233 configuration: "implementation".into(),
234 }];
235 resolve_variables(&mut deps, &props);
236 assert_eq!(deps[0].version_req, Some("3.2.0".into()));
237 }
238
239 #[test]
240 fn test_resolve_variables_not_found_keeps_raw() {
241 let props: HashMap<String, String> = HashMap::new();
242 let mut deps = vec![GradleDependency {
243 group_id: "com.example".into(),
244 artifact_id: "lib".into(),
245 name: "com.example:lib".into(),
246 name_range: Range::default(),
247 version_req: Some("$unknownVar".into()),
248 version_range: None,
249 configuration: "implementation".into(),
250 }];
251 resolve_variables(&mut deps, &props);
252 assert_eq!(deps[0].version_req, Some("$unknownVar".into()));
253 }
254
255 #[test]
256 fn test_resolve_variables_literal_version_unchanged() {
257 let props: HashMap<String, String> = [("v".to_string(), "9.9.9".to_string())].into();
258 let mut deps = vec![GradleDependency {
259 group_id: "com.example".into(),
260 artifact_id: "lib".into(),
261 name: "com.example:lib".into(),
262 name_range: Range::default(),
263 version_req: Some("1.2.3".into()),
264 version_range: None,
265 configuration: "implementation".into(),
266 }];
267 resolve_variables(&mut deps, &props);
268 assert_eq!(deps[0].version_req, Some("1.2.3".into()));
269 }
270
271 #[test]
272 fn test_parse_result_trait() {
273 use deps_core::ParseResult;
274
275 let uri = make_uri("/project/build.gradle");
276 let result = parse_gradle("", &uri).unwrap();
277 assert!(result.dependencies().is_empty());
278 assert!(result.workspace_root().is_none());
279 assert!(result.as_any().is::<GradleParseResult>());
280 }
281
282 #[test]
283 fn test_line_offset_table() {
284 let content = "line0\nline1\nline2";
285 let table = LineOffsetTable::new(content);
286 let pos = table.byte_offset_to_position(content, 6);
287 assert_eq!(pos.line, 1);
288 assert_eq!(pos.character, 0);
289
290 let pos = table.byte_offset_to_position(content, 8);
291 assert_eq!(pos.line, 1);
292 assert_eq!(pos.character, 2);
293 }
294
295 #[test]
296 fn test_find_name_range() {
297 let line = " implementation(\"com.example:lib:1.0.0\")";
298 let range = find_name_range(line, 5, "com.example", "lib");
299 assert_eq!(range.start.line, 5);
300 assert!(range.start.character > 0);
301 }
302
303 #[test]
304 fn test_find_version_range() {
305 let line = " implementation(\"com.example:lib:1.0.0\")";
306 let range = find_version_range(line, 5, "1.0.0");
307 assert_eq!(range.start.line, 5);
308 assert_eq!(range.end.character - range.start.character, 5);
310 }
311
312 #[test]
313 fn test_utf16_len_ascii() {
314 assert_eq!(utf16_len("hello"), 5);
315 }
316}