use crate::core::error::WorkshopError;
use crate::source::Span;
use super::table::{self, KeyKind};
use super::{PathPart, Settings, SettingsNode, suggest};
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct SettingsDiagnostic {
pub error: WorkshopError,
pub suggestion: Option<String>,
}
pub fn check_emission_diagnostics(settings: &Settings) -> Vec<SettingsDiagnostic> {
let mut diagnostics = Vec::new();
for child in &settings.children {
match child {
SettingsNode::Workshop { children, .. } => {
check_workshop_children(children, &mut diagnostics)
}
SettingsNode::Group { name, children, .. } => match name.as_str() {
"main" | "lobby" => {
for member in children {
check_member(member, &[PathPart::Part(name)], &mut diagnostics);
}
}
"gamemodes" => check_modes(children, &mut diagnostics),
"heroes" => check_heroes(children, &mut diagnostics),
"extensions" => {
for member in children {
check_member(member, &[PathPart::Part("extensions")], &mut diagnostics);
}
}
_ => check_opaque_children(children, &mut diagnostics),
},
other => diagnostics.push(rejected(
other.span(),
"settings block children must be groups".to_string(),
None,
)),
}
}
diagnostics
}
pub fn check_emission(settings: &Settings) -> Vec<WorkshopError> {
check_emission_diagnostics(settings)
.into_iter()
.map(|diagnostic| diagnostic.error)
.collect()
}
fn rejected(span: Option<Span>, message: String, suggestion: Option<String>) -> SettingsDiagnostic {
let message = suggest::with_suggestion_text(message, suggestion.as_deref());
SettingsDiagnostic {
error: WorkshopError::malformed(message, span),
suggestion,
}
}
fn check_workshop_children(children: &[SettingsNode], errors: &mut Vec<SettingsDiagnostic>) {
for child in children {
match child {
SettingsNode::Group { children, .. } | SettingsNode::Workshop { children, .. } => {
check_workshop_children(children, errors);
}
SettingsNode::Raw { .. } => {}
other => errors.push(rejected(
other.span(),
"settings.workshop contains a typed builtin setting".to_string(),
None,
)),
}
}
}
fn check_modes(modes: &[SettingsNode], errors: &mut Vec<SettingsDiagnostic>) {
for mode in modes {
let SettingsNode::Group { name, children, .. } = mode else {
errors.push(rejected(
mode.span(),
"mode entries must be groups".to_string(),
None,
));
continue;
};
for member in children {
if matches!(member, SettingsNode::Bool { name, .. } if name == "enabled") {
continue;
}
check_member(
member,
&[PathPart::Part("gamemodes"), PathPart::Part(name)],
errors,
);
}
}
}
fn check_heroes(teams: &[SettingsNode], errors: &mut Vec<SettingsDiagnostic>) {
for team in teams {
let SettingsNode::Group { name, children, .. } = team else {
errors.push(rejected(
team.span(),
"team entries must be groups".to_string(),
None,
));
continue;
};
if table::team_name(name).is_none() {
errors.push(rejected(
team.span(),
format!("unknown team '{name}'"),
suggest::suggest(name, table::team_spellings()),
));
continue;
}
for member in children {
match member {
SettingsNode::Group { name, children, .. } => {
if table::hero_name(name).is_none() {
errors.push(rejected(
member.span(),
format!("unknown hero '{name}'"),
suggest::suggest(name, table::hero_spellings()),
));
continue;
}
for inner in children {
check_member(
inner,
&[PathPart::Part("heroes"), PathPart::Team, PathPart::Hero],
errors,
);
}
}
other => check_member(other, &[PathPart::Part("heroes"), PathPart::Team], errors),
}
}
}
}
fn check_opaque_children(children: &[SettingsNode], errors: &mut Vec<SettingsDiagnostic>) {
for child in children {
match child {
SettingsNode::Group { children, .. } => check_opaque_children(children, errors),
other => check_member(other, &[], errors),
}
}
}
fn check_member(node: &SettingsNode, path: &[PathPart<'_>], errors: &mut Vec<SettingsDiagnostic>) {
if matches!(node, SettingsNode::Raw { .. }) {
return;
}
let name = node.name();
let mut full = path.to_vec();
full.push(PathPart::Part(name));
let Some(entry) = table::lookup(&full) else {
errors.push(rejected(
node.span(),
format!(
"settings key '{}' is outside the emission table",
table::path_string(&full)
),
suggest::suggest(name, table::key_spellings(path).into_iter()),
));
return;
};
match (node, &entry.kind) {
(SettingsNode::Flag { .. }, KeyKind::Flag)
| (SettingsNode::String { .. }, KeyKind::String)
| (SettingsNode::Number { .. }, KeyKind::Number | KeyKind::Percent)
| (SettingsNode::Bool { .. }, KeyKind::Bool | KeyKind::YesNo) => {}
(SettingsNode::String { value, .. }, KeyKind::Enum(domain)) => {
if table::enum_name(domain, value).is_none() {
errors.push(rejected(
node.span(),
format!("unknown value '{value}' for settings key '{name}'"),
suggest::suggest(value, table::enum_spellings(domain)),
));
}
}
(SettingsNode::Bool { value, .. }, KeyKind::BoolEnum(domain)) => {
if !*value {
errors.push(rejected(
node.span(),
format!("unsupported false value for settings key '{name}'"),
None,
));
} else if table::enum_name(domain, "enabled").is_none() {
errors.push(rejected(
node.span(),
format!("unknown value 'enabled' for settings key '{name}'"),
None,
));
}
}
(SettingsNode::List { elements, .. }, KeyKind::ListMap) => {
for element in elements {
if table::map_name(&element.value).is_none() {
errors.push(rejected(
element.span,
format!("unknown map '{}' in settings list '{name}'", element.value),
suggest::suggest(&element.value, table::map_spellings()),
));
}
}
}
(SettingsNode::List { elements, .. }, KeyKind::ListHero) => {
for element in elements {
if table::hero_name(&element.value).is_none() {
errors.push(rejected(
element.span,
format!("unknown hero '{}' in settings list '{name}'", element.value),
suggest::suggest(&element.value, table::hero_spellings()),
));
}
}
}
_ => errors.push(rejected(
node.span(),
format!("settings key '{name}' does not match its table kind"),
None,
)),
}
}