Skip to main content

mf2_model/
diagnostic.rs

1//! Error kinds shared by every crate, and how a frontend reports them.
2
3use alloc::vec::Vec;
4
5/// The kind of an MF2 error: the 13 error types of the WG test suite
6/// (`test/README.md`, "Error Codes") plus *Unsupported Operation* and the
7/// umbrella *Message Function Error* of `spec/errors.md`.
8#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
9#[repr(u8)]
10#[non_exhaustive]
11pub enum ErrorKind {
12    /// Syntax Error (`syntax-error`): the source is not well-formed.
13    Syntax,
14    /// Variant Key Mismatch (`variant-key-mismatch`).
15    VariantKeyMismatch,
16    /// Missing Fallback Variant (`missing-fallback-variant`).
17    MissingFallbackVariant,
18    /// Missing Selector Annotation (`missing-selector-annotation`).
19    MissingSelectorAnnotation,
20    /// Duplicate Declaration (`duplicate-declaration`).
21    DuplicateDeclaration,
22    /// Duplicate Option Name (`duplicate-option-name`).
23    DuplicateOptionName,
24    /// Duplicate Variant (`duplicate-variant`).
25    DuplicateVariant,
26    /// Unresolved Variable (`unresolved-variable`).
27    UnresolvedVariable,
28    /// Unknown Function (`unknown-function`).
29    UnknownFunction,
30    /// Bad Selector (`bad-selector`).
31    BadSelector,
32    /// Bad Operand (`bad-operand`).
33    BadOperand,
34    /// Bad Option (`bad-option`).
35    BadOption,
36    /// Bad Variant Key (`bad-variant-key`).
37    BadVariantKey,
38    /// Unsupported Operation — not in the suite's schema; used for an
39    /// operation a configuration does not provide (e.g. a locale-dependent
40    /// numeric option without the `fn-number` feature).
41    UnsupportedOperation,
42    /// Any other Message Function Error — the spec's umbrella category, not in
43    /// the suite's schema.
44    MessageFunctionError,
45}
46
47/// The category of an [`ErrorKind`] (`spec/errors.md`).
48#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
49#[non_exhaustive]
50pub enum ErrorClass {
51    /// The source is not well-formed.
52    Syntax,
53    /// The message is well-formed but not valid.
54    DataModel,
55    /// A value cannot be determined at runtime.
56    Resolution,
57    /// An error from, or specific to, a function handler.
58    MessageFunction,
59}
60
61impl ErrorKind {
62    /// Every kind, in declaration order.
63    pub const ALL: &'static [ErrorKind] = &[
64        ErrorKind::Syntax,
65        ErrorKind::VariantKeyMismatch,
66        ErrorKind::MissingFallbackVariant,
67        ErrorKind::MissingSelectorAnnotation,
68        ErrorKind::DuplicateDeclaration,
69        ErrorKind::DuplicateOptionName,
70        ErrorKind::DuplicateVariant,
71        ErrorKind::UnresolvedVariable,
72        ErrorKind::UnknownFunction,
73        ErrorKind::BadSelector,
74        ErrorKind::BadOperand,
75        ErrorKind::BadOption,
76        ErrorKind::BadVariantKey,
77        ErrorKind::UnsupportedOperation,
78        ErrorKind::MessageFunctionError,
79    ];
80
81    /// The category of this kind.
82    pub const fn class(self) -> ErrorClass {
83        match self {
84            ErrorKind::Syntax => ErrorClass::Syntax,
85            ErrorKind::VariantKeyMismatch
86            | ErrorKind::MissingFallbackVariant
87            | ErrorKind::MissingSelectorAnnotation
88            | ErrorKind::DuplicateDeclaration
89            | ErrorKind::DuplicateOptionName
90            | ErrorKind::DuplicateVariant => ErrorClass::DataModel,
91            ErrorKind::UnresolvedVariable | ErrorKind::UnknownFunction | ErrorKind::BadSelector => {
92                ErrorClass::Resolution
93            }
94            ErrorKind::BadOperand
95            | ErrorKind::BadOption
96            | ErrorKind::BadVariantKey
97            | ErrorKind::UnsupportedOperation
98            | ErrorKind::MessageFunctionError => ErrorClass::MessageFunction,
99        }
100    }
101
102    /// The error's name in the WG test suite (`"bad-option"`, …). The 13 kinds
103    /// of the suite's schema use its names; the other two use the spec's
104    /// names in the same style (`"unsupported-operation"`,
105    /// `"message-function-error"`), which never occur in the suite.
106    #[cfg(feature = "suite-names")]
107    #[doc(hidden)]
108    pub fn suite_name(self) -> &'static str {
109        match self {
110            ErrorKind::Syntax => "syntax-error",
111            ErrorKind::VariantKeyMismatch => "variant-key-mismatch",
112            ErrorKind::MissingFallbackVariant => "missing-fallback-variant",
113            ErrorKind::MissingSelectorAnnotation => "missing-selector-annotation",
114            ErrorKind::DuplicateDeclaration => "duplicate-declaration",
115            ErrorKind::DuplicateOptionName => "duplicate-option-name",
116            ErrorKind::DuplicateVariant => "duplicate-variant",
117            ErrorKind::UnresolvedVariable => "unresolved-variable",
118            ErrorKind::UnknownFunction => "unknown-function",
119            ErrorKind::BadSelector => "bad-selector",
120            ErrorKind::BadOperand => "bad-operand",
121            ErrorKind::BadOption => "bad-option",
122            ErrorKind::BadVariantKey => "bad-variant-key",
123            ErrorKind::UnsupportedOperation => "unsupported-operation",
124            ErrorKind::MessageFunctionError => "message-function-error",
125        }
126    }
127
128    /// The kind named `s` (the inverse of [`ErrorKind::suite_name`]).
129    #[cfg(feature = "suite-names")]
130    #[doc(hidden)]
131    pub fn from_suite_name(s: &str) -> Option<ErrorKind> {
132        ErrorKind::ALL.iter().copied().find(|k| k.suite_name() == s)
133    }
134}
135
136/// A byte range in the source: `start ≤ end ≤ len`, both on char boundaries.
137#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
138pub struct Span {
139    /// Offset of the first byte.
140    pub start: u32,
141    /// Offset one past the last byte.
142    pub end: u32,
143}
144
145/// One reported error. Made with [`Diagnostic::new`]; a later version may
146/// add fields.
147#[derive(Clone, PartialEq, Eq, Hash, Debug)]
148#[non_exhaustive]
149pub struct Diagnostic {
150    /// What kind of error.
151    pub kind: ErrorKind,
152    /// 0 = none; otherwise the frontend's stable detail code (`mf2-syntax`
153    /// documents its table).
154    pub code: u16,
155    /// Where, in the source; `None` for models built in code.
156    pub span: Option<Span>,
157}
158
159impl Diagnostic {
160    /// An error of `kind`, with the frontend's detail `code` (0 = none) at
161    /// `span`.
162    pub const fn new(kind: ErrorKind, code: u16, span: Option<Span>) -> Diagnostic {
163        Diagnostic { kind, code, span }
164    }
165}
166
167/// The errors a frontend or validator reported, in the order reported.
168#[derive(Clone, Default, PartialEq, Eq, Hash, Debug)]
169pub struct Diagnostics(Vec<Diagnostic>);
170
171impl Diagnostics {
172    /// No diagnostics (does not allocate).
173    pub const fn new() -> Self {
174        Diagnostics(Vec::new())
175    }
176
177    /// Appends `d`.
178    pub fn push(&mut self, d: Diagnostic) {
179        self.0.push(d);
180    }
181
182    /// The diagnostics, in the order reported.
183    pub fn iter(&self) -> core::slice::Iter<'_, Diagnostic> {
184        self.0.iter()
185    }
186
187    /// How many.
188    pub fn len(&self) -> usize {
189        self.0.len()
190    }
191
192    /// None reported.
193    pub fn is_empty(&self) -> bool {
194        self.0.is_empty()
195    }
196
197    /// Some diagnostic has kind `kind`.
198    pub fn has(&self, kind: ErrorKind) -> bool {
199        self.0.iter().any(|d| d.kind == kind)
200    }
201
202    /// Some diagnostic's kind is of class `class`.
203    pub fn has_class(&self, class: ErrorClass) -> bool {
204        self.0.iter().any(|d| d.kind.class() == class)
205    }
206
207    /// The diagnostics as a vector.
208    pub fn into_vec(self) -> Vec<Diagnostic> {
209        self.0
210    }
211}
212
213impl<'a> IntoIterator for &'a Diagnostics {
214    type Item = &'a Diagnostic;
215    type IntoIter = core::slice::Iter<'a, Diagnostic>;
216
217    fn into_iter(self) -> Self::IntoIter {
218        self.iter()
219    }
220}
221
222impl From<Vec<Diagnostic>> for Diagnostics {
223    fn from(v: Vec<Diagnostic>) -> Self {
224        Diagnostics(v)
225    }
226}
227
228impl Extend<Diagnostic> for Diagnostics {
229    fn extend<I: IntoIterator<Item = Diagnostic>>(&mut self, iter: I) {
230        self.0.extend(iter);
231    }
232}
233
234#[cfg(test)]
235mod tests {
236    use super::{Diagnostic, Diagnostics, ErrorClass, ErrorKind, Span};
237
238    #[test]
239    fn classes() {
240        use ErrorClass::{DataModel, MessageFunction, Resolution, Syntax};
241        let classes: alloc::vec::Vec<ErrorClass> =
242            ErrorKind::ALL.iter().map(|k| k.class()).collect();
243        assert_eq!(
244            classes,
245            [
246                Syntax,
247                DataModel,
248                DataModel,
249                DataModel,
250                DataModel,
251                DataModel,
252                DataModel,
253                Resolution,
254                Resolution,
255                Resolution,
256                MessageFunction,
257                MessageFunction,
258                MessageFunction,
259                MessageFunction,
260                MessageFunction,
261            ]
262        );
263    }
264
265    #[test]
266    fn discriminants_follow_declaration_order() {
267        for (i, k) in ErrorKind::ALL.iter().enumerate() {
268            assert_eq!(*k as u8 as usize, i);
269        }
270    }
271
272    #[cfg(feature = "suite-names")]
273    #[test]
274    fn suite_names_round_trip() {
275        for &k in ErrorKind::ALL {
276            assert_eq!(ErrorKind::from_suite_name(k.suite_name()), Some(k));
277        }
278        assert_eq!(
279            ErrorKind::from_suite_name("bad-option"),
280            Some(ErrorKind::BadOption)
281        );
282        assert_eq!(ErrorKind::from_suite_name("Bad-Option"), None);
283        assert_eq!(ErrorKind::from_suite_name(""), None);
284    }
285
286    #[test]
287    fn diagnostics_queries() {
288        let mut d = Diagnostics::new();
289        assert!(d.is_empty());
290        assert_eq!(d.len(), 0);
291        assert!(!d.has(ErrorKind::Syntax));
292        d.push(Diagnostic {
293            kind: ErrorKind::DuplicateVariant,
294            code: 0,
295            span: Some(Span { start: 1, end: 4 }),
296        });
297        assert_eq!(d.len(), 1);
298        assert!(d.has(ErrorKind::DuplicateVariant));
299        assert!(!d.has(ErrorKind::Syntax));
300        assert!(d.has_class(ErrorClass::DataModel));
301        assert!(!d.has_class(ErrorClass::Syntax));
302        assert_eq!(d.iter().count(), 1);
303        let v = d.into_vec();
304        assert_eq!(v[0].span, Some(Span { start: 1, end: 4 }));
305    }
306}