1use smol_str::SmolStr;
15
16use crate::warnings::{WarnLevel, WarningCode, WarningSettings};
17
18#[derive(Debug, Clone, PartialEq, Eq)]
20pub struct AutoloadEntry {
21 pub name: SmolStr,
23 pub path: SmolStr,
25 pub is_singleton: bool,
28}
29
30#[must_use]
33pub fn parse_autoloads(text: &str) -> Vec<AutoloadEntry> {
34 let mut entries = Vec::new();
35 let mut in_autoload = false;
36 for raw_line in text.lines() {
37 let line = raw_line.trim();
38 if line.is_empty() || line.starts_with(';') || line.starts_with('#') {
40 continue;
41 }
42 if let Some(inner) = line.strip_prefix('[').and_then(|s| s.strip_suffix(']')) {
44 in_autoload = inner.trim() == "autoload";
45 continue;
46 }
47 if !in_autoload {
48 continue;
49 }
50 let Some((name, value)) = line.split_once('=') else {
52 continue;
53 };
54 let name = name.trim();
55 if name.is_empty() {
56 continue;
57 }
58 let value = dequote(value.trim());
60 let (is_singleton, path) = match value.strip_prefix('*') {
61 Some(rest) => (true, rest),
62 None => (false, value),
63 };
64 if path.is_empty() {
65 continue;
66 }
67 entries.push(AutoloadEntry {
68 name: SmolStr::new(name),
69 path: SmolStr::new(path),
70 is_singleton,
71 });
72 }
73 entries
74}
75
76#[must_use]
82pub fn parse_engine_version(text: &str) -> Option<(u32, u32)> {
83 let mut in_application = false;
84 for raw_line in text.lines() {
85 let line = raw_line.trim();
86 if line.is_empty() || line.starts_with(';') || line.starts_with('#') {
87 continue;
88 }
89 if let Some(inner) = line.strip_prefix('[').and_then(|s| s.strip_suffix(']')) {
90 in_application = inner.trim() == "application";
91 continue;
92 }
93 if !in_application {
94 continue;
95 }
96 let Some((key, value)) = line.split_once('=') else {
97 continue;
98 };
99 if key.trim() != "config/features" {
100 continue;
101 }
102 let value = value.trim();
105 let inner = value
106 .strip_prefix("PackedStringArray(")
107 .or_else(|| value.strip_prefix("PoolStringArray("))
108 .and_then(|s| s.strip_suffix(')'))
109 .unwrap_or(value);
110 return inner
111 .split(',')
112 .find_map(|part| parse_major_minor(dequote(part.trim())));
113 }
114 None
115}
116
117#[must_use]
125pub fn parse_warning_settings(text: &str, engine: (u32, u32)) -> WarningSettings {
126 let mut settings = WarningSettings::engine_default(engine);
127 let mut in_debug = false;
128 for raw_line in text.lines() {
129 let line = raw_line.trim();
130 if line.is_empty() || line.starts_with(';') || line.starts_with('#') {
131 continue;
132 }
133 if let Some(inner) = line.strip_prefix('[').and_then(|s| s.strip_suffix(']')) {
134 in_debug = inner.trim() == "debug";
135 continue;
136 }
137 if !in_debug {
138 continue;
139 }
140 let Some((key, value)) = line.split_once('=') else {
141 continue;
142 };
143 let Some(tail) = key.trim().strip_prefix("gdscript/warnings/") else {
144 continue;
145 };
146 let value = dequote(value.trim());
147 match tail {
148 "enable" => {
149 if let Some(b) = parse_bool(value) {
150 settings.enabled = b;
151 }
152 }
153 "treat_warnings_as_errors" => {
154 if let Some(b) = parse_bool(value) {
155 settings.treat_as_errors = b;
156 }
157 }
158 "exclude_addons" => {
159 if let Some(b) = parse_bool(value) {
160 settings.exclude_addons = b;
161 }
162 }
163 _ => {
164 if let Some(code) = WarningCode::from_setting_name(tail)
165 && let Some(level) = parse_warn_level(value)
166 {
167 settings.per_code.insert(code, level);
168 }
169 }
170 }
171 }
172 settings
173}
174
175fn parse_bool(s: &str) -> Option<bool> {
177 match s.trim() {
178 "true" => Some(true),
179 "false" => Some(false),
180 _ => None,
181 }
182}
183
184fn parse_warn_level(s: &str) -> Option<WarnLevel> {
186 let s = s.trim();
187 if let Ok(n) = s.parse::<u32>() {
188 return WarnLevel::from_int(n);
189 }
190 match s {
191 "true" => Some(WarnLevel::Warn),
192 "false" => Some(WarnLevel::Ignore),
193 _ => None,
194 }
195}
196
197fn parse_major_minor(s: &str) -> Option<(u32, u32)> {
200 let mut parts = s.split('.');
201 let major = parts.next()?.parse::<u32>().ok()?;
202 let minor = parts.next()?.parse::<u32>().ok()?;
203 Some((major, minor))
204}
205
206fn dequote(s: &str) -> &str {
208 let bytes = s.as_bytes();
209 if bytes.len() >= 2
210 && (bytes[0] == b'"' || bytes[0] == b'\'')
211 && bytes[bytes.len() - 1] == bytes[0]
212 {
213 &s[1..s.len() - 1]
214 } else {
215 s
216 }
217}
218
219#[cfg(test)]
220mod tests {
221 use super::*;
222
223 #[test]
224 fn parses_singleton_and_strips_star() {
225 let e = parse_autoloads("[autoload]\nGame=\"*res://game.gd\"\n");
226 assert_eq!(e.len(), 1);
227 assert_eq!(e[0].name, "Game");
228 assert_eq!(e[0].path, "res://game.gd");
229 assert!(e[0].is_singleton);
230 }
231
232 #[test]
233 fn non_star_is_not_a_singleton() {
234 let e = parse_autoloads("[autoload]\nHelper=\"res://helper.gd\"\n");
235 assert_eq!(e.len(), 1);
236 assert_eq!(e[0].path, "res://helper.gd");
237 assert!(!e[0].is_singleton, "no leading * → loaded-but-not-global");
238 }
239
240 #[test]
241 fn only_the_autoload_section_is_read() {
242 let src = "config_version=5\n\
243 [application]\n\
244 config/name=\"Demo\"\n\
245 config/features=PackedStringArray(\"4.6\")\n\
246 \n\
247 [autoload]\n\
248 ; a comment\n\
249 Log=\"*res://utils/system_log.gd\"\n\
250 Music=\"*res://music.tscn\"\n\
251 \n\
252 [rendering]\n\
253 renderer/rendering_method=\"gl_compatibility\"\n";
254 let e = parse_autoloads(src);
255 assert_eq!(e.len(), 2);
256 assert_eq!(e[0].name, "Log");
257 assert_eq!(e[0].path, "res://utils/system_log.gd");
258 assert!(e[0].is_singleton);
259 assert_eq!(e[1].name, "Music");
261 assert_eq!(e[1].path, "res://music.tscn");
262 }
264
265 #[test]
266 fn empty_or_no_autoload_section_is_empty() {
267 assert!(parse_autoloads("").is_empty());
268 assert!(parse_autoloads("[application]\nconfig/name=\"X\"\n").is_empty());
269 }
270
271 #[test]
272 fn parses_engine_version_from_config_features() {
273 let src = "config_version=5\n\
274 [application]\n\
275 config/name=\"Demo\"\n\
276 config/features=PackedStringArray(\"4.3\", \"Forward Plus\")\n";
277 assert_eq!(parse_engine_version(src), Some((4, 3)));
278 }
279
280 #[test]
281 fn engine_version_picks_the_version_shaped_entry_anywhere_in_the_array() {
282 let src = "[application]\nconfig/features=PackedStringArray(\"Forward Plus\", \"4.6\", \"Mobile\")\n";
284 assert_eq!(parse_engine_version(src), Some((4, 6)));
285 }
286
287 #[test]
288 fn engine_version_ignores_patch_and_tolerates_bare_value() {
289 assert_eq!(
290 parse_engine_version("[application]\nconfig/features=PackedStringArray(\"4.2.1\")\n"),
291 Some((4, 2)),
292 );
293 assert_eq!(
294 parse_engine_version("[application]\nconfig/features=\"4.5\"\n"),
295 Some((4, 5)),
296 );
297 }
298
299 #[test]
300 fn engine_version_none_when_absent_or_no_version_entry() {
301 assert_eq!(parse_engine_version(""), None);
302 assert_eq!(
303 parse_engine_version("[application]\nconfig/name=\"X\"\n"),
304 None
305 );
306 assert_eq!(
308 parse_engine_version("[rendering]\nconfig/features=PackedStringArray(\"4.6\")\n"),
309 None,
310 );
311 assert_eq!(
313 parse_engine_version("[application]\nconfig/features=PackedStringArray(\"Vulkan\")\n"),
314 None,
315 );
316 }
317}