use crate::core::error::WorkshopError;
use crate::core::suggest;
use super::member::{self, Member, rejected};
use super::table;
use super::{PathPart, Settings, SettingsNode};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DiagnosticSeverity {
Error,
Warning,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct SettingsDiagnostic {
pub severity: DiagnosticSeverity,
pub error: WorkshopError,
pub suggestion: Option<String>,
}
pub fn check_emission_diagnostics(settings: &Settings) -> Vec<SettingsDiagnostic> {
let mut diagnostics = Vec::new();
walk(settings, &mut |visit| match visit {
Visit::Rejected(diagnostic) => diagnostics.push(diagnostic),
Visit::Member(node, path) => check_member(node, path, &mut diagnostics),
Visit::OpaqueLeaf(node) => check_member(node, &[], &mut diagnostics),
});
diagnostics
}
pub fn check_emission(settings: &Settings) -> Vec<WorkshopError> {
check_emission_diagnostics(settings)
.into_iter()
.filter(|diagnostic| diagnostic.severity == DiagnosticSeverity::Error)
.map(|diagnostic| diagnostic.error)
.collect()
}
pub(crate) struct UncataloguedMember<'a> {
pub(crate) name: &'a str,
pub(crate) span: Option<crate::source::Span>,
pub(crate) suggestion: Option<String>,
}
pub(crate) fn uncatalogued_members(settings: &Settings) -> Vec<UncataloguedMember<'_>> {
let mut members = Vec::new();
walk(settings, &mut |visit| match visit {
Visit::Rejected(_) => {}
Visit::Member(node, path) => {
let mut full = path.to_vec();
full.push(PathPart::Part(node.name()));
let catalogued = table::lookup(&full);
let member = match (node, catalogued) {
(SettingsNode::Raw { .. }, _) | (SettingsNode::Group { .. }, _) => {
UncataloguedMember {
name: node.name(),
span: node.span(),
suggestion: carried_suggestion(node, path),
}
}
(SettingsNode::RawValue { value, .. }, Some(_)) => UncataloguedMember {
name: value,
span: node.span(),
suggestion: carried_suggestion(node, path),
},
(SettingsNode::RawValue { .. }, None) => UncataloguedMember {
name: node.name(),
span: node.span(),
suggestion: carried_suggestion(node, path),
},
_ => return,
};
members.push(member);
}
Visit::OpaqueLeaf(node @ (SettingsNode::Raw { .. } | SettingsNode::RawValue { .. })) => {
members.push(UncataloguedMember {
name: node.name(),
span: node.span(),
suggestion: None,
});
}
Visit::OpaqueLeaf(_) => {}
});
members
}
pub(crate) fn carried_suggestion(node: &SettingsNode, path: &[PathPart<'_>]) -> Option<String> {
let mut full = path.to_vec();
full.push(PathPart::Part(node.name()));
match (node, table::lookup(&full)) {
(SettingsNode::Raw { .. }, _) | (SettingsNode::Group { .. }, None) => {
suggest::suggest(node.name(), table::key_spellings(path).into_iter())
}
(SettingsNode::RawValue { value, .. }, Some(entry)) => match entry.kind {
table::KeyKind::Enum(domain) => suggest::suggest(value, table::enum_spellings(domain)),
_ => None,
},
(SettingsNode::RawValue { .. }, None) => {
suggest::suggest(node.name(), table::key_spellings(path).into_iter())
}
_ => None,
}
}
enum Visit<'a, 'p> {
Rejected(SettingsDiagnostic),
Member(&'a SettingsNode, &'p [PathPart<'a>]),
OpaqueLeaf(&'a SettingsNode),
}
fn walk<'a>(settings: &'a Settings, visit: &mut dyn FnMut(Visit<'a, '_>)) {
for child in &settings.children {
match child {
SettingsNode::Workshop { children, .. } => walk_workshop(children, visit),
SettingsNode::Group { name, children, .. } => match name.as_str() {
"main" | "lobby" | "extensions" => {
for member in children {
visit(Visit::Member(member, &[PathPart::Part(name)]));
}
}
"gamemodes" => walk_modes(children, visit),
"heroes" => walk_heroes(children, visit),
_ => walk_opaque(children, visit),
},
other => {
visit(Visit::Rejected(rejected(
other.span(),
"settings block children must be groups".to_string(),
None,
)));
if matches!(
other,
SettingsNode::Raw { .. } | SettingsNode::RawValue { .. }
) {
visit(Visit::OpaqueLeaf(other));
}
}
}
}
}
fn walk_workshop<'a>(children: &'a [SettingsNode], visit: &mut dyn FnMut(Visit<'a, '_>)) {
for child in children {
match child {
SettingsNode::Group { children, .. } | SettingsNode::Workshop { children, .. } => {
walk_workshop(children, visit);
}
SettingsNode::Raw { .. } | SettingsNode::RawValue { .. } => {}
other => visit(Visit::Rejected(rejected(
other.span(),
"settings.workshop contains a typed builtin setting".to_string(),
None,
))),
}
}
}
fn walk_modes<'a>(modes: &'a [SettingsNode], visit: &mut dyn FnMut(Visit<'a, '_>)) {
for mode in modes {
let SettingsNode::Group { name, children, .. } = mode else {
visit(Visit::Rejected(rejected(
mode.span(),
"mode entries must be groups".to_string(),
None,
)));
continue;
};
let path = [PathPart::Part("gamemodes"), PathPart::Part(name)];
for member in children {
if matches!(member, SettingsNode::Bool { name, .. } if name == "enabled") {
continue;
}
visit(Visit::Member(member, &path));
}
}
}
fn walk_heroes<'a>(teams: &'a [SettingsNode], visit: &mut dyn FnMut(Visit<'a, '_>)) {
const TEAM: [PathPart<'static>; 2] = [PathPart::Part("heroes"), PathPart::Team];
const HERO: [PathPart<'static>; 3] = [PathPart::Part("heroes"), PathPart::Team, PathPart::Hero];
for team in teams {
let SettingsNode::Group { name, children, .. } = team else {
visit(Visit::Rejected(rejected(
team.span(),
"team entries must be groups".to_string(),
None,
)));
continue;
};
if table::team_name(name).is_none() {
visit(Visit::Rejected(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() {
visit(Visit::Rejected(rejected(
member.span(),
format!("unknown hero '{name}'"),
suggest::suggest(name, table::hero_spellings()),
)));
continue;
}
for inner in children {
visit(Visit::Member(inner, &HERO));
}
}
other => visit(Visit::Member(other, &TEAM)),
}
}
}
}
fn warn_near_miss(
node: &SettingsNode,
path: &[PathPart<'_>],
spelling: String,
) -> SettingsDiagnostic {
let mut full = path.to_vec();
full.push(PathPart::Part(node.name()));
let message = match node {
SettingsNode::RawValue { value, .. } if table::lookup(&full).is_some() => format!(
"settings value '{value}' for key '{}' is not declared",
table::path_string(&full)
),
other => format!("settings key '{}' is not declared", other.name()),
};
let message = suggest::with_suggestion_text(message, Some(&spelling));
SettingsDiagnostic {
severity: DiagnosticSeverity::Warning,
error: WorkshopError::warning(message, node.span()),
suggestion: Some(spelling),
}
}
fn walk_opaque<'a>(children: &'a [SettingsNode], visit: &mut dyn FnMut(Visit<'a, '_>)) {
for child in children {
match child {
SettingsNode::Group { children, .. } => walk_opaque(children, visit),
other => visit(Visit::OpaqueLeaf(other)),
}
}
}
fn check_member(node: &SettingsNode, path: &[PathPart<'_>], errors: &mut Vec<SettingsDiagnostic>) {
let name = node.name();
let mut full = path.to_vec();
full.push(PathPart::Part(name));
if !path.is_empty() {
if let Some(spelling) = carried_suggestion(node, path) {
errors.push(warn_near_miss(node, path, spelling));
}
}
match (node, table::lookup(&full)) {
(SettingsNode::Raw { .. } | SettingsNode::RawValue { .. }, _) => return,
(SettingsNode::Group { children, .. }, _) => {
for child in children {
check_member(child, &[], errors);
}
return;
}
_ => {}
}
match member::lookup(node, &full).and_then(|entry| member::accept(node, entry)) {
Ok(Member::List { elements, kind }) => {
for element in elements {
if let Err(diagnostic) = kind.resolve(element, name) {
errors.push(*diagnostic);
}
}
}
Ok(_) => {}
Err(diagnostic) => errors.push(*diagnostic),
}
}