Skip to main content

denise_forms/
error.rs

1//! What went wrong, and where in the file.
2//!
3//! A form is something a person typed, so every failure here carries a line, a
4//! column, and — where there is a finite set of right answers — the whole set. A
5//! misspelled property names the property, the widget, and everything that widget
6//! *does* accept; there is no error in this crate that leaves somebody grepping.
7
8use std::fmt;
9
10use denise_ui::widgets::Property;
11
12/// Where in the source something is, counted from one as an editor counts.
13#[derive(Clone, Copy, Debug, PartialEq, Eq)]
14pub struct At {
15    /// Line, from 1.
16    pub line: usize,
17    /// Column, from 1.
18    pub column: usize,
19}
20
21impl At {
22    /// The position of a byte offset in `source`.
23    ///
24    /// Computed on demand rather than carried around: the parser hands back byte
25    /// spans, and a form that loads without complaint should not have paid for
26    /// counting newlines.
27    ///
28    /// ```
29    /// # use denise_forms::At;
30    /// let source = "form \"F\" version=1\n    label \"æøå\" x=0\n";
31    ///
32    /// assert_eq!(At::of(source, 0), At { line: 1, column: 1 });
33    /// // Characters, not bytes, so a column points where a caret would be even
34    /// // on a line with an `æ` in it.
35    /// assert_eq!(At::of(source, source.len() - 1).line, 2);
36    /// ```
37    pub fn of(source: &str, offset: usize) -> Self {
38        let offset = offset.min(source.len());
39        let consumed = &source[..offset];
40        let line = consumed.bytes().filter(|&b| b == b'\n').count() + 1;
41        let column = consumed
42            .rfind('\n')
43            .map_or(offset, |nl| offset - nl - 1)
44            // Count characters rather than bytes, so a column in a line with an
45            // `æ` in it points where the editor's cursor would be.
46            .min(consumed.len());
47        let column = consumed[consumed.len() - column..].chars().count() + 1;
48        Self { line, column }
49    }
50
51    /// The start of the file, for a complaint about the file as a whole.
52    pub const START: Self = Self { line: 1, column: 1 };
53}
54
55impl fmt::Display for At {
56    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
57        write!(f, "{}:{}", self.line, self.column)
58    }
59}
60
61/// Everything that can be wrong with a form file.
62#[derive(Clone, Debug, PartialEq)]
63#[non_exhaustive]
64pub enum Reason {
65    /// The file is not KDL at all.
66    Syntax(String),
67    /// The top level is not a single `form` node.
68    NotAForm {
69        /// What was found there instead.
70        found: String,
71    },
72    /// `version` is missing, or is not a number.
73    Version,
74    /// The file was written for a later version of this crate.
75    FromTheFuture {
76        /// What the file asks for.
77        wanted: u64,
78        /// The highest this crate understands.
79        understood: u64,
80    },
81    /// A required property is not there.
82    Missing {
83        /// The node's kind.
84        kind: String,
85        /// What was needed.
86        name: &'static str,
87    },
88    /// No widget goes by that name.
89    UnknownWidget {
90        /// What the file said.
91        found: String,
92    },
93    /// The widget has no such property.
94    UnknownProperty {
95        /// The node's kind.
96        kind: &'static str,
97        /// What the file said.
98        found: String,
99        /// Everything that widget does accept.
100        accepted: &'static [Property],
101    },
102    /// A property the `form` node does not carry — often one that belongs to a
103    /// different kind of form.
104    ///
105    /// Separate from [`Reason::UnknownProperty`] because what a form accepts
106    /// depends on its kind, so the list has to be built rather than pointed at.
107    UnknownFormProperty {
108        /// What kind of form it said it was.
109        kind: &'static str,
110        /// What the file said.
111        found: String,
112        /// Everything a form of that kind does accept.
113        accepted: Vec<&'static str>,
114    },
115    /// The property exists; the value was not the shape it takes.
116    WrongType {
117        /// The node's kind.
118        kind: &'static str,
119        /// The property.
120        name: String,
121        /// What it takes, in words.
122        wanted: &'static str,
123    },
124    /// A name that should have been one of a fixed set was not.
125    NotAName {
126        /// The property, or the thing being named.
127        name: String,
128        /// What the file said.
129        found: String,
130        /// Every name that would have worked.
131        accepted: &'static [&'static str],
132    },
133    /// A child node that the parent has no use for.
134    UnexpectedChild {
135        /// The parent's kind.
136        parent: String,
137        /// The child's kind.
138        found: String,
139    },
140    /// A designer's placeholder content written where the engine would load it.
141    ///
142    /// A `table`'s `row`s and a `timeline`'s `event`s belong in that node's
143    /// `design { … }` block, where every build but a designer's skips them. Left
144    /// outside, they would ship to a kiosk — so this is an error rather than a
145    /// thing quietly ignored, for the same reason an unknown property is.
146    PlaceholderOutside {
147        /// The widget's kind.
148        kind: String,
149        /// The child node's name.
150        found: String,
151    },
152    /// One message name is used with two different payload shapes, so no single
153    /// enum variant can serve both.
154    ///
155    /// Only [`codegen`](crate::codegen) raises this. The engine is happy to
156    /// resolve one name twice, because the application's `match` answers each
157    /// call separately; a generated enum cannot, because `Greet` is either a
158    /// variant or a `fn(bool) -> M` and not both.
159    PayloadClash {
160        /// The name used twice.
161        found: String,
162        /// What it was first seen as.
163        first: &'static str,
164        /// And then as.
165        then: &'static str,
166    },
167    /// A name in the file cannot be turned into a Rust identifier.
168    ///
169    /// Only [`codegen`](crate::codegen). A form loads perfectly well with a node
170    /// called `2`; a struct field cannot be called that.
171    NotAnIdentifier {
172        /// What the file called it.
173        found: String,
174        /// Why it will not do.
175        because: &'static str,
176    },
177    /// Two names in the file become one Rust identifier.
178    ///
179    /// Only [`codegen`](crate::codegen). `full-name` and `full_name` are two
180    /// nodes to a form file and one field to Rust.
181    Collides {
182        /// The second name to arrive.
183        found: String,
184        /// The one already there.
185        with: String,
186        /// What they both became.
187        spelled: String,
188    },
189    /// The application's resolver did not know a message name.
190    UnknownMessage {
191        /// The name in the file.
192        found: String,
193    },
194    /// The resolver knew the name but gave back the wrong shape of message.
195    WrongMessage {
196        /// The name in the file.
197        found: String,
198        /// What the widget needs.
199        wanted: &'static str,
200    },
201    /// A picture the application's loader would not load.
202    Asset {
203        /// The path in the file.
204        path: String,
205    },
206    /// Two nodes claim the same name.
207    DuplicateName {
208        /// The name.
209        name: String,
210    },
211    /// More than one node asked for the caret.
212    TwoFocuses,
213    /// The tree refused a node, which it does when its parent is gone.
214    TreeRefused,
215    /// The file nests deeper than this crate will follow.
216    TooDeep {
217        /// The limit.
218        limit: usize,
219    },
220    /// The file is larger than this crate will read.
221    TooLarge {
222        /// The limit, in bytes.
223        limit: usize,
224    },
225    /// A brace has no partner.
226    ///
227    /// Refused by the same byte scan that counts depth, and for the same
228    /// reason: a file whose braces do not balance cannot parse whatever the
229    /// parser does with it, and `kdl` can spend an unbounded amount of time
230    /// discovering that. Saying it up front costs one pass and gives a better
231    /// position than the recovery would.
232    Unbalanced {
233        /// `true` for a `{` that is never closed, `false` for a `}` that
234        /// closes nothing.
235        open: bool,
236    },
237    /// The file nests commented-out children blocks past what the parser can
238    /// be asked to read.
239    ///
240    /// `kdl` takes time that doubles with every commented-out block nested
241    /// inside another: a hundred bytes of them is twenty seconds, and three
242    /// hundred is longer than anyone will wait. So this is refused by a byte
243    /// scan before the parser is handed the file at all — the same treatment,
244    /// and for the same reason, as nesting past
245    /// [`MAX_DEPTH`](crate::MAX_DEPTH), which overflows its stack. See
246    /// [`MAX_COMMENTED_DEPTH`](crate::MAX_COMMENTED_DEPTH).
247    CommentedTooDeep {
248        /// The limit, in levels.
249        limit: usize,
250    },
251    /// The parse was still running when the caller's deadline passed, so it
252    /// was abandoned and the file was not read.
253    ///
254    /// Only [`Form::parse_within`](crate::Form::parse_within) raises this, and
255    /// only a caller who asked for a deadline can get it. It says nothing about
256    /// the file beyond how long it was taking: a hostile one that has found a
257    /// corner of `kdl` that takes exponential time looks exactly like a
258    /// legitimate one on a machine that is too slow for the number chosen. The
259    /// position is the top of the file, because nothing in it has been read.
260    TooSlow {
261        /// The time the caller allowed.
262        limit: core::time::Duration,
263    },
264    /// A bounded parse could not be started, so the file was not read at all.
265    ///
266    /// [`Form::parse_within`](crate::Form::parse_within) works on a thread it
267    /// can walk away from, and this says there was no such thread to be had:
268    /// either [`MAX_ABANDONED`](crate::MAX_ABANDONED) earlier parses are still
269    /// running past their deadlines — a wedged thread each, and the point of
270    /// the limit is that a machine does not fill up with them — or the system
271    /// refused a thread outright.
272    NoThread {
273        /// How many earlier parses are still running past their deadline.
274        abandoned: usize,
275    },
276    /// The parser could not keep the file byte-for-byte.
277    ///
278    /// Everything this crate does — undo, the designer's save, a text editor
279    /// alongside — stands on [`Form::text`](crate::Form::text) reproducing what
280    /// was opened, and a file that cannot be reproduced would silently lose
281    /// bytes on the first save. Refusing it is the honest alternative.
282    ///
283    /// The known causes are all one thing — kdl dropping the trivia between a
284    /// closing brace and the next node, whether that is trailing whitespace or
285    /// a comment written on the brace's line — and all of it is put back before
286    /// this is ever raised. So reaching this means a shape nobody has seen yet,
287    /// which is what the fuzz target `parse_form` is hunting for.
288    NotPreserved,
289    /// An edit named a node that is not there.
290    NoSuchNode {
291        /// The child path that went nowhere.
292        path: Vec<usize>,
293    },
294    /// A move would have put a node inside itself.
295    ///
296    /// Or would have moved the form, which is the document rather than a node in
297    /// it. Both are the same mistake — a tree cannot contain its own root — and
298    /// both come from a drag that ended where it should not have been allowed to.
299    IntoItself {
300        /// The node the move named.
301        path: Vec<usize>,
302    },
303    /// An edit set the positional argument of a node written without one.
304    ///
305    /// An argument comes before every property, and no edit puts something at
306    /// the front of a line without rewriting the line. Setting the property
307    /// that argument stands for is what to do instead.
308    NoArgument,
309    /// An edit would have put a number where a string lives, or the reverse.
310    ///
311    /// Not a matter of reversibility — an edit's inverse restores the text that
312    /// was there, so any value can be put back. It is a matter of what the file
313    /// would become: `placeholder=70` parses and then will not build, and an
314    /// editor holding the widget's own descriptor already knows better. So the
315    /// door refuses it rather than the loader, three steps later.
316    ///
317    /// A number replacing a number is not this, whichever way it is written:
318    /// `value=70` becoming `value=70.5` is an ordinary edit.
319    WrongKind {
320        /// The property.
321        name: String,
322        /// What it holds now.
323        holds: &'static str,
324        /// What the edit offered.
325        given: &'static str,
326    },
327}
328
329/// A failure, and where in the file it is.
330#[derive(Clone, Debug, PartialEq)]
331pub struct Error {
332    /// Where.
333    pub at: At,
334    /// What.
335    pub reason: Reason,
336}
337
338impl Error {
339    pub(crate) const fn new(at: At, reason: Reason) -> Self {
340        Self { at, reason }
341    }
342}
343
344/// Renders a list, Oxford-comma-free and bounded, for an "expected one of" line.
345fn listing(names: impl Iterator<Item = impl fmt::Display>) -> String {
346    let mut all: Vec<String> = names.map(|n| n.to_string()).collect();
347    all.sort_unstable();
348    // A widget with thirty properties should not print thirty of them at the top
349    // of a diagnostic; the point is to jog a memory, not to be documentation.
350    const SHOWN: usize = 12;
351    if all.len() > SHOWN {
352        let rest = all.len() - SHOWN;
353        all.truncate(SHOWN);
354        format!("{}, and {rest} more", all.join(", "))
355    } else {
356        all.join(", ")
357    }
358}
359
360impl fmt::Display for Error {
361    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
362        write!(f, "{}: ", self.at)?;
363        match &self.reason {
364            Reason::Syntax(message) => write!(f, "{message}"),
365            Reason::NotAForm { found } => write!(
366                f,
367                "a form file holds one `form` node and nothing else; found `{found}`"
368            ),
369            Reason::Version => write!(
370                f,
371                "`form` needs a `version`, as a whole number — this crate reads version {}",
372                crate::VERSION
373            ),
374            Reason::FromTheFuture { wanted, understood } => write!(
375                f,
376                "this file is version {wanted} and this crate reads version {understood}; \
377                 a newer denise-forms will open it"
378            ),
379            Reason::Missing { kind, name } => {
380                write!(f, "`{kind}` needs a `{name}` and there is none")
381            }
382            Reason::UnknownWidget { found } => {
383                let kinds = listing(denise_ui::widgets::all().iter().map(|w| w.kind));
384                write!(f, "there is no widget called `{found}`; there is {kinds}")
385            }
386            Reason::UnknownProperty {
387                kind,
388                found,
389                accepted,
390            } => {
391                let names = listing(accepted.iter().map(|p| p.name));
392                write!(f, "`{kind}` has no property `{found}`; it accepts {names}")
393            }
394            Reason::UnknownFormProperty {
395                kind,
396                found,
397                accepted,
398            } => {
399                let names = listing(accepted.iter());
400                write!(
401                    f,
402                    "a `{kind}` form has no property `{found}`; it accepts {names}"
403                )
404            }
405            Reason::WrongType { kind, name, wanted } => {
406                write!(f, "`{name}` on `{kind}` takes {wanted}")
407            }
408            Reason::NotAName {
409                name,
410                found,
411                accepted,
412            } => {
413                let names = listing(accepted.iter());
414                write!(f, "`{found}` is not a {name}; try {names}")
415            }
416            Reason::UnexpectedChild { parent, found } => {
417                write!(f, "a `{parent}` has no use for a `{found}` inside it")
418            }
419            Reason::PayloadClash { found, first, then } => write!(
420                f,
421                "`{found}` is used as {first} and as {then}; one name cannot generate both"
422            ),
423            Reason::NotAnIdentifier { found, because } => {
424                write!(f, "`{found}` cannot name anything in Rust: {because}")
425            }
426            Reason::Collides {
427                found,
428                with,
429                spelled,
430            } => write!(f, "`{found}` and `{with}` are both `{spelled}` in Rust"),
431            Reason::UnknownMessage { found } => write!(
432                f,
433                "the application does not know a message called `{found}`"
434            ),
435            Reason::WrongMessage { found, wanted } => write!(
436                f,
437                "`{found}` resolved to the wrong kind of message; this one needs {wanted}"
438            ),
439            Reason::Asset { path } => {
440                write!(f, "the application could not load `{path}`")
441            }
442            Reason::DuplicateName { name } => {
443                write!(f, "two nodes are called `{name}`; a name identifies one")
444            }
445            Reason::TwoFocuses => write!(
446                f,
447                "two nodes ask for the caret with `focus=#true`; only one can have it"
448            ),
449            Reason::TreeRefused => write!(f, "the tree would not take this node"),
450            Reason::TooDeep { limit } => write!(
451                f,
452                "this form nests more than {limit} deep, which is past what a \
453                 form is and into what a stack overflow is"
454            ),
455            Reason::PlaceholderOutside { kind, found } => write!(
456                f,
457                "a `{found}` is placeholder content, so it belongs in this \
458                 {kind}'s `design {{ … }}` block; written here it would be \
459                 loaded on a panel that has its own"
460            ),
461            Reason::Unbalanced { open } => {
462                if *open {
463                    write!(f, "this `{{` is never closed")
464                } else {
465                    write!(f, "this `}}` closes nothing")
466                }
467            }
468            Reason::CommentedTooDeep { limit } => write!(
469                f,
470                "this nests commented-out blocks more than {limit} deep, and \
471                 every level of that doubles what reading the file costs"
472            ),
473            Reason::TooSlow { limit } => write!(
474                f,
475                "this form was taking longer than {limit:?} to parse, so it \
476                 was abandoned unread"
477            ),
478            Reason::NoThread { abandoned } => {
479                if *abandoned >= crate::MAX_ABANDONED {
480                    write!(
481                        f,
482                        "{abandoned} earlier parses are still running past \
483                         their deadline and cannot be stopped, so this form \
484                         was not started; restart to clear them"
485                    )
486                } else {
487                    write!(
488                        f,
489                        "this form needs a thread of its own to be parsed \
490                         under a deadline, and the system would not give it one"
491                    )
492                }
493            }
494            Reason::NotPreserved => write!(
495                f,
496                "the parser cannot keep this file byte-for-byte, so saving it \
497                 would corrupt it; the difference starts here"
498            ),
499            Reason::TooLarge { limit } => {
500                write!(
501                    f,
502                    "a form file is at most {limit} bytes; this one is larger"
503                )
504            }
505            Reason::NoSuchNode { path } => write!(f, "there is no node at {path:?}"),
506            Reason::IntoItself { path } => {
507                write!(f, "a node cannot go inside itself; {path:?} was asked to")
508            }
509            Reason::NoArgument => write!(
510                f,
511                "this node is written without an argument; set the property instead"
512            ),
513            Reason::WrongKind { name, holds, given } => write!(
514                f,
515                "`{name}` holds {holds}; putting {given} there would make a form that will not load"
516            ),
517        }
518    }
519}
520
521impl std::error::Error for Error {}
522
523#[cfg(test)]
524mod tests {
525    use super::*;
526
527    #[test]
528    fn a_position_counts_from_one_the_way_an_editor_does() {
529        let source = "abc\ndef\n";
530        assert_eq!(At::of(source, 0), At { line: 1, column: 1 });
531        assert_eq!(At::of(source, 2), At { line: 1, column: 3 });
532        assert_eq!(At::of(source, 4), At { line: 2, column: 1 });
533        assert_eq!(At::of(source, 6), At { line: 2, column: 3 });
534    }
535
536    #[test]
537    fn a_column_counts_characters_rather_than_bytes() {
538        // Four bytes before the `x`, but two characters.
539        let source = "æø x";
540        assert_eq!(At::of(source, 5), At { line: 1, column: 4 });
541    }
542
543    #[test]
544    fn a_position_past_the_end_lands_at_the_end() {
545        let source = "ab";
546        assert_eq!(At::of(source, 99), At { line: 1, column: 3 });
547    }
548
549    #[test]
550    fn a_long_list_of_names_is_cut_short_rather_than_dumped() {
551        let many: Vec<String> = (0..40).map(|n| format!("name{n:02}")).collect();
552        let rendered = listing(many.iter());
553        assert!(rendered.contains("and 28 more"), "{rendered}");
554        assert!(rendered.len() < 200, "{rendered}");
555    }
556}