deps_gradle/parser/
settings.rs1use crate::error::Result;
6use crate::parser::{GradleParseResult, utf16_len};
7use crate::types::GradleDependency;
8use regex::Regex;
9use std::sync::OnceLock;
10use tower_lsp_server::ls_types::{Position, Range, Uri};
11
12static RE_PLUGIN: OnceLock<Regex> = OnceLock::new();
14
15fn re_plugin() -> &'static Regex {
16 RE_PLUGIN.get_or_init(|| {
17 Regex::new(r#"id\s*\(?\s*['"]([^'"]+)['"]\s*\)?\s+version\s+['"]([^'"]+)['"]"#).unwrap()
18 })
19}
20
21fn find_plugin_name_range(line: &str, line_idx: u32, plugin_id: &str) -> Range {
23 if let Some(col) = line.find(plugin_id) {
24 let col_u32 = utf16_len(&line[..col]) as u32;
25 let end_u32 = col_u32 + utf16_len(plugin_id) as u32;
26 Range::new(
27 Position::new(line_idx, col_u32),
28 Position::new(line_idx, end_u32),
29 )
30 } else {
31 Range::default()
32 }
33}
34
35fn find_plugin_version_range(line: &str, line_idx: u32, version: &str) -> Range {
37 if let Some(kw_pos) = line.find("version") {
39 let after_kw = &line[kw_pos + "version".len()..];
40 if let Some(rel) = after_kw.find(version) {
41 let abs_start = kw_pos + "version".len() + rel;
42 let col_start = utf16_len(&line[..abs_start]) as u32;
43 let col_end = col_start + utf16_len(version) as u32;
44 return Range::new(
45 Position::new(line_idx, col_start),
46 Position::new(line_idx, col_end),
47 );
48 }
49 }
50 Range::default()
51}
52
53pub fn parse_settings(content: &str, uri: &Uri) -> Result<GradleParseResult> {
55 let mut dependencies = Vec::new();
56 let mut brace_depth: i32 = 0;
57 let mut in_plugin_management = false;
58 let mut pm_depth: i32 = 0;
59 let mut in_plugins = false;
60 let mut plugins_depth: i32 = 0;
61
62 for (line_idx, line) in content.lines().enumerate() {
63 let trimmed = line.trim();
64
65 if !in_plugin_management && trimmed.starts_with("pluginManagement") && trimmed.contains('{')
67 {
68 in_plugin_management = true;
69 pm_depth = brace_depth + 1;
70 }
71
72 if in_plugin_management
74 && !in_plugins
75 && trimmed.starts_with("plugins")
76 && trimmed.contains('{')
77 {
78 in_plugins = true;
79 plugins_depth = brace_depth + 1;
80 }
81
82 for ch in line.chars() {
84 match ch {
85 '{' => brace_depth += 1,
86 '}' => {
87 brace_depth -= 1;
88 if in_plugins && brace_depth < plugins_depth {
89 in_plugins = false;
90 }
91 if in_plugin_management && brace_depth < pm_depth {
92 in_plugin_management = false;
93 }
94 }
95 _ => {}
96 }
97 }
98
99 if !in_plugins {
100 continue;
101 }
102
103 let line_u32 = line_idx as u32;
104
105 for caps in re_plugin().captures_iter(line) {
106 let plugin_id = caps.get(1).map_or("", |m| m.as_str()).to_string();
107 let version = caps.get(2).map_or("", |m| m.as_str()).trim().to_string();
108
109 let artifact_id = format!("{plugin_id}.gradle.plugin");
111 let name = format!("{plugin_id}:{artifact_id}");
112
113 let name_range = find_plugin_name_range(line, line_u32, &plugin_id);
114 let version_range = find_plugin_version_range(line, line_u32, &version);
115
116 dependencies.push(GradleDependency {
117 group_id: plugin_id,
118 artifact_id,
119 name,
120 name_range,
121 version_req: Some(version),
122 version_range: Some(version_range),
123 configuration: "plugin".to_string(),
124 });
125 }
126 }
127
128 Ok(GradleParseResult {
129 dependencies,
130 uri: uri.clone(),
131 })
132}
133
134#[cfg(test)]
135mod tests {
136 use super::*;
137
138 fn make_uri(name: &str) -> Uri {
139 Uri::from_file_path(format!("/project/{name}")).unwrap()
140 }
141
142 #[test]
143 fn test_parse_groovy_plugin() {
144 let content = r#"pluginManagement {
145 plugins {
146 id "org.jetbrains.kotlin.jvm" version "2.1.10"
147 id 'com.google.devtools.ksp' version '2.1.10-1.0.31'
148 }
149}
150"#;
151 let result = parse_settings(content, &make_uri("settings.gradle")).unwrap();
152 assert_eq!(result.dependencies.len(), 2);
153
154 let dep = &result.dependencies[0];
155 assert_eq!(dep.group_id, "org.jetbrains.kotlin.jvm");
156 assert_eq!(dep.artifact_id, "org.jetbrains.kotlin.jvm.gradle.plugin");
157 assert_eq!(
158 dep.name,
159 "org.jetbrains.kotlin.jvm:org.jetbrains.kotlin.jvm.gradle.plugin"
160 );
161 assert_eq!(dep.version_req, Some("2.1.10".into()));
162 assert_eq!(dep.configuration, "plugin");
163
164 let dep2 = &result.dependencies[1];
165 assert_eq!(dep2.group_id, "com.google.devtools.ksp");
166 assert_eq!(dep2.version_req, Some("2.1.10-1.0.31".into()));
167 }
168
169 #[test]
170 fn test_parse_kotlin_dsl_plugin() {
171 let content = r#"pluginManagement {
172 plugins {
173 id("org.springframework.boot") version "3.2.0"
174 }
175}
176"#;
177 let result = parse_settings(content, &make_uri("settings.gradle.kts")).unwrap();
178 assert_eq!(result.dependencies.len(), 1);
179 assert_eq!(result.dependencies[0].group_id, "org.springframework.boot");
180 assert_eq!(result.dependencies[0].version_req, Some("3.2.0".into()));
181 }
182
183 #[test]
184 fn test_no_plugin_management_block() {
185 let content = "rootProject.name = \"my-project\"\n";
186 let result = parse_settings(content, &make_uri("settings.gradle")).unwrap();
187 assert!(result.dependencies.is_empty());
188 }
189
190 #[test]
191 fn test_plugin_without_version_skipped() {
192 let content = r#"pluginManagement {
193 plugins {
194 id "org.jetbrains.kotlin.jvm"
195 }
196}
197"#;
198 let result = parse_settings(content, &make_uri("settings.gradle")).unwrap();
199 assert!(result.dependencies.is_empty());
200 }
201
202 #[test]
203 fn test_position_tracking() {
204 let content = r#"pluginManagement {
205 plugins {
206 id "org.jetbrains.kotlin.jvm" version "2.1.10"
207 }
208}
209"#;
210 let result = parse_settings(content, &make_uri("settings.gradle")).unwrap();
211 assert_eq!(result.dependencies.len(), 1);
212 let dep = &result.dependencies[0];
213 assert_eq!(dep.name_range.start.line, 2);
214 assert!(dep.version_range.is_some());
215 let vr = dep.version_range.unwrap();
216 assert_eq!(vr.start.line, 2);
217 }
218
219 #[test]
220 fn test_empty_content() {
221 let result = parse_settings("", &make_uri("settings.gradle")).unwrap();
222 assert!(result.dependencies.is_empty());
223 }
224}