Skip to main content

foch_test/
diagnostic.rs

1//! Problems found before a game is launched.
2
3use crate::source::SourceSpan;
4use serde::{Deserialize, Serialize};
5use std::fmt;
6
7pub use foch_annotation::Severity;
8
9#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)]
10#[serde(rename_all = "snake_case")]
11#[non_exhaustive]
12pub enum DiagnosticCode {
13	/// The script itself does not parse.
14	Parse,
15	/// A malformed annotation, test block or fixture.
16	Annotation,
17	/// A comment that looks like an annotation but uses an unknown name.
18	UnknownAnnotation,
19	/// An annotation that is not followed by what it applies to.
20	OrphanAnnotation,
21	/// An annotation inside a definition rather than before it.
22	NestedAnnotation,
23	DuplicateCase,
24	DuplicateFixture,
25	UnknownFixture,
26	FixtureCycle,
27	/// The target cannot be invoked by the framework.
28	UnsupportedTarget,
29	TooManyCases,
30	InvalidMod,
31	UnknownTag,
32	UnknownEvent,
33	/// A trigger used where an effect is required.
34	TriggerAsEffect,
35	/// An effect used where a condition is required.
36	EffectAsTrigger,
37}
38
39/// `bypassable` separates two kinds of findings. A malformed test cannot be
40/// compiled, so it always blocks. A finding from Foch's own script model may
41/// be wrong about the game, so the user may insist on running the real game.
42#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
43pub struct Diagnostic {
44	pub code: DiagnosticCode,
45	pub severity: Severity,
46	pub span: Option<SourceSpan>,
47	pub message: String,
48	pub bypassable: bool,
49}
50
51impl Diagnostic {
52	pub(crate) fn error(
53		code: DiagnosticCode,
54		span: Option<SourceSpan>,
55		message: impl Into<String>,
56	) -> Self {
57		Self {
58			code,
59			severity: Severity::Error,
60			span,
61			message: message.into(),
62			bypassable: false,
63		}
64	}
65
66	pub(crate) fn model_error(
67		code: DiagnosticCode,
68		span: Option<SourceSpan>,
69		message: impl Into<String>,
70	) -> Self {
71		Self {
72			bypassable: true,
73			..Self::error(code, span, message)
74		}
75	}
76}
77
78impl From<foch_annotation::Diagnostic> for Diagnostic {
79	fn from(diagnostic: foch_annotation::Diagnostic) -> Self {
80		use foch_annotation::Code;
81		let code = match diagnostic.code {
82			Code::Parse => DiagnosticCode::Parse,
83			Code::UnknownAnnotation => DiagnosticCode::UnknownAnnotation,
84			Code::Orphan => DiagnosticCode::OrphanAnnotation,
85			Code::Nested => DiagnosticCode::NestedAnnotation,
86			Code::UnsupportedTarget => DiagnosticCode::UnsupportedTarget,
87			_ => DiagnosticCode::Annotation,
88		};
89		Self {
90			code,
91			severity: diagnostic.severity,
92			span: Some(diagnostic.span),
93			message: diagnostic.message,
94			bypassable: false,
95		}
96	}
97}
98
99impl fmt::Display for Diagnostic {
100	fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
101		if let Some(span) = &self.span {
102			write!(formatter, "{span}: ")?;
103		}
104		let severity = match self.severity {
105			Severity::Error => "error",
106			Severity::Warning => "warning",
107		};
108		write!(formatter, "{severity}: {}", self.message)
109	}
110}
111
112/// Whether diagnostics prevent launching the game. Warnings never block;
113/// errors block unless every error is bypassable and the user insisted.
114pub fn blocks_run(diagnostics: &[Diagnostic], run_anyway: bool) -> bool {
115	diagnostics.iter().any(|diagnostic| {
116		diagnostic.severity == Severity::Error && !(run_anyway && diagnostic.bypassable)
117	})
118}
119
120#[cfg(test)]
121mod tests {
122	use super::*;
123
124	#[test]
125	fn only_model_findings_can_be_overridden() {
126		let model = Diagnostic::model_error(DiagnosticCode::UnknownTag, None, "x");
127		let malformed = Diagnostic::error(DiagnosticCode::Annotation, None, "x");
128		let warning = Diagnostic {
129			severity: Severity::Warning,
130			..Diagnostic::error(DiagnosticCode::UnknownEvent, None, "x")
131		};
132		assert!(!blocks_run(std::slice::from_ref(&warning), false));
133		assert!(blocks_run(std::slice::from_ref(&model), false));
134		assert!(!blocks_run(std::slice::from_ref(&model), true));
135		assert!(blocks_run(&[model, malformed], true));
136	}
137}