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