use smol_str::SmolStr;
use crate::warnings::{WarnLevel, WarningCode, WarningSettings};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AutoloadEntry {
pub name: SmolStr,
pub path: SmolStr,
pub is_singleton: bool,
}
#[must_use]
pub fn parse_autoloads(text: &str) -> Vec<AutoloadEntry> {
let mut entries = Vec::new();
let mut in_autoload = false;
for raw_line in text.lines() {
let line = raw_line.trim();
if line.is_empty() || line.starts_with(';') || line.starts_with('#') {
continue;
}
if let Some(inner) = line.strip_prefix('[').and_then(|s| s.strip_suffix(']')) {
in_autoload = inner.trim() == "autoload";
continue;
}
if !in_autoload {
continue;
}
let Some((name, value)) = line.split_once('=') else {
continue;
};
let name = name.trim();
if name.is_empty() {
continue;
}
let value = dequote(value.trim());
let (is_singleton, path) = match value.strip_prefix('*') {
Some(rest) => (true, rest),
None => (false, value),
};
if path.is_empty() {
continue;
}
entries.push(AutoloadEntry {
name: SmolStr::new(name),
path: SmolStr::new(path),
is_singleton,
});
}
entries
}
#[must_use]
pub fn parse_engine_version(text: &str) -> Option<(u32, u32)> {
let mut in_application = false;
for raw_line in text.lines() {
let line = raw_line.trim();
if line.is_empty() || line.starts_with(';') || line.starts_with('#') {
continue;
}
if let Some(inner) = line.strip_prefix('[').and_then(|s| s.strip_suffix(']')) {
in_application = inner.trim() == "application";
continue;
}
if !in_application {
continue;
}
let Some((key, value)) = line.split_once('=') else {
continue;
};
if key.trim() != "config/features" {
continue;
}
let value = value.trim();
let inner = value
.strip_prefix("PackedStringArray(")
.or_else(|| value.strip_prefix("PoolStringArray("))
.and_then(|s| s.strip_suffix(')'))
.unwrap_or(value);
return inner
.split(',')
.find_map(|part| parse_major_minor(dequote(part.trim())));
}
None
}
#[must_use]
pub fn parse_warning_settings(text: &str, engine: (u32, u32)) -> WarningSettings {
let mut settings = WarningSettings::engine_default(engine);
let mut in_debug = false;
for raw_line in text.lines() {
let line = raw_line.trim();
if line.is_empty() || line.starts_with(';') || line.starts_with('#') {
continue;
}
if let Some(inner) = line.strip_prefix('[').and_then(|s| s.strip_suffix(']')) {
in_debug = inner.trim() == "debug";
continue;
}
if !in_debug {
continue;
}
let Some((key, value)) = line.split_once('=') else {
continue;
};
let Some(tail) = key.trim().strip_prefix("gdscript/warnings/") else {
continue;
};
let value = dequote(value.trim());
match tail {
"enable" => {
if let Some(b) = parse_bool(value) {
settings.enabled = b;
}
}
"treat_warnings_as_errors" => {
if let Some(b) = parse_bool(value) {
settings.treat_as_errors = b;
}
}
"exclude_addons" => {
if let Some(b) = parse_bool(value) {
settings.exclude_addons = b;
}
}
_ => {
if let Some(code) = WarningCode::from_setting_name(tail)
&& let Some(level) = parse_warn_level(value)
{
settings.per_code.insert(code, level);
}
}
}
}
settings
}
fn parse_bool(s: &str) -> Option<bool> {
match s.trim() {
"true" => Some(true),
"false" => Some(false),
_ => None,
}
}
fn parse_warn_level(s: &str) -> Option<WarnLevel> {
let s = s.trim();
if let Ok(n) = s.parse::<u32>() {
return WarnLevel::from_int(n);
}
match s {
"true" => Some(WarnLevel::Warn),
"false" => Some(WarnLevel::Ignore),
_ => None,
}
}
fn parse_major_minor(s: &str) -> Option<(u32, u32)> {
let mut parts = s.split('.');
let major = parts.next()?.parse::<u32>().ok()?;
let minor = parts.next()?.parse::<u32>().ok()?;
Some((major, minor))
}
fn dequote(s: &str) -> &str {
let bytes = s.as_bytes();
if bytes.len() >= 2
&& (bytes[0] == b'"' || bytes[0] == b'\'')
&& bytes[bytes.len() - 1] == bytes[0]
{
&s[1..s.len() - 1]
} else {
s
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_singleton_and_strips_star() {
let e = parse_autoloads("[autoload]\nGame=\"*res://game.gd\"\n");
assert_eq!(e.len(), 1);
assert_eq!(e[0].name, "Game");
assert_eq!(e[0].path, "res://game.gd");
assert!(e[0].is_singleton);
}
#[test]
fn non_star_is_not_a_singleton() {
let e = parse_autoloads("[autoload]\nHelper=\"res://helper.gd\"\n");
assert_eq!(e.len(), 1);
assert_eq!(e[0].path, "res://helper.gd");
assert!(!e[0].is_singleton, "no leading * → loaded-but-not-global");
}
#[test]
fn only_the_autoload_section_is_read() {
let src = "config_version=5\n\
[application]\n\
config/name=\"Demo\"\n\
config/features=PackedStringArray(\"4.6\")\n\
\n\
[autoload]\n\
; a comment\n\
Log=\"*res://utils/system_log.gd\"\n\
Music=\"*res://music.tscn\"\n\
\n\
[rendering]\n\
renderer/rendering_method=\"gl_compatibility\"\n";
let e = parse_autoloads(src);
assert_eq!(e.len(), 2);
assert_eq!(e[0].name, "Log");
assert_eq!(e[0].path, "res://utils/system_log.gd");
assert!(e[0].is_singleton);
assert_eq!(e[1].name, "Music");
assert_eq!(e[1].path, "res://music.tscn");
}
#[test]
fn empty_or_no_autoload_section_is_empty() {
assert!(parse_autoloads("").is_empty());
assert!(parse_autoloads("[application]\nconfig/name=\"X\"\n").is_empty());
}
#[test]
fn parses_engine_version_from_config_features() {
let src = "config_version=5\n\
[application]\n\
config/name=\"Demo\"\n\
config/features=PackedStringArray(\"4.3\", \"Forward Plus\")\n";
assert_eq!(parse_engine_version(src), Some((4, 3)));
}
#[test]
fn engine_version_picks_the_version_shaped_entry_anywhere_in_the_array() {
let src = "[application]\nconfig/features=PackedStringArray(\"Forward Plus\", \"4.6\", \"Mobile\")\n";
assert_eq!(parse_engine_version(src), Some((4, 6)));
}
#[test]
fn engine_version_ignores_patch_and_tolerates_bare_value() {
assert_eq!(
parse_engine_version("[application]\nconfig/features=PackedStringArray(\"4.2.1\")\n"),
Some((4, 2)),
);
assert_eq!(
parse_engine_version("[application]\nconfig/features=\"4.5\"\n"),
Some((4, 5)),
);
}
#[test]
fn engine_version_none_when_absent_or_no_version_entry() {
assert_eq!(parse_engine_version(""), None);
assert_eq!(
parse_engine_version("[application]\nconfig/name=\"X\"\n"),
None
);
assert_eq!(
parse_engine_version("[rendering]\nconfig/features=PackedStringArray(\"4.6\")\n"),
None,
);
assert_eq!(
parse_engine_version("[application]\nconfig/features=PackedStringArray(\"Vulkan\")\n"),
None,
);
}
}