Skip to main content

facet_format/deserializer/
error.rs

1use facet_core::Shape;
2use facet_path::Path;
3use facet_reflect::{AllocError, ReflectError, ReflectErrorKind, ShapeMismatchError, Span};
4use std::borrow::Cow;
5use std::cell::Cell;
6use std::fmt;
7
8thread_local! {
9    /// Thread-local storage for the current span during deserialization.
10    /// This is set by SpanGuard before calling Partial methods,
11    /// allowing the From<ReflectError> impl to capture the span automatically.
12    static CURRENT_SPAN: Cell<Option<Span>> = const { Cell::new(None) };
13}
14
15/// RAII guard that sets the current span for error reporting.
16///
17/// When dropped, restores the previous span value.
18/// The `From<ReflectError>` impl will panic if no span is set.
19pub struct SpanGuard {
20    prev: Option<Span>,
21}
22
23impl SpanGuard {
24    /// Create a new span guard, setting the current span.
25    #[inline]
26    pub fn new(span: Span) -> Self {
27        let prev = CURRENT_SPAN.with(|cell| cell.replace(Some(span)));
28        Self { prev }
29    }
30}
31
32impl Drop for SpanGuard {
33    fn drop(&mut self) {
34        CURRENT_SPAN.with(|cell| cell.set(self.prev));
35    }
36}
37
38/// Get the current span for error reporting.
39/// Panics if no span is set (i.e., no SpanGuard is active).
40#[inline]
41fn current_span() -> Span {
42    CURRENT_SPAN.with(|cell| {
43        cell.get().expect(
44            "current_span called without an active SpanGuard - this is a bug in the deserializer",
45        )
46    })
47}
48
49/// Error produced by a format parser (JSON, TOML, etc.).
50///
51/// Parse errors always have a span (location in the input) but never have a path
52/// (location in the type structure) because parsers don't know about the target type.
53///
54/// When propagated through the deserializer, this is converted to a `DeserializeError`
55/// which can add path information.
56#[derive(Debug)]
57#[non_exhaustive]
58pub struct ParseError {
59    /// Source span where the error occurred.
60    pub span: Span,
61
62    /// The specific kind of error.
63    pub kind: DeserializeErrorKind,
64}
65
66impl ParseError {
67    /// Create a new parse error with the given span and kind.
68    #[inline]
69    pub const fn new(span: Span, kind: DeserializeErrorKind) -> Self {
70        Self { span, kind }
71    }
72}
73
74impl fmt::Display for ParseError {
75    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
76        write!(f, "{} at {:?}", self.kind, self.span)
77    }
78}
79
80impl std::error::Error for ParseError {}
81
82impl From<ParseError> for DeserializeError {
83    fn from(e: ParseError) -> Self {
84        DeserializeError {
85            span: Some(e.span),
86            path: None,
87            kind: e.kind,
88        }
89    }
90}
91
92/// Error produced by the format deserializer.
93///
94/// This struct contains span and path information at the top level,
95/// with a `kind` field describing the specific error.
96pub struct DeserializeError {
97    /// Source span where the error occurred (if available).
98    pub span: Option<Span>,
99
100    /// Path through the type structure where the error occurred.
101    pub path: Option<Path>,
102
103    /// The specific kind of error.
104    pub kind: DeserializeErrorKind,
105}
106
107impl fmt::Debug for DeserializeError {
108    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
109        // Show span as simple numbers instead of the verbose Span { offset: X, len: Y }
110        let span_str = match self.span {
111            Some(span) => format!("[{}..{})", span.offset, span.offset + span.len),
112            None => "none".to_string(),
113        };
114
115        // Use Display for path which is much more readable
116        let path_str = match &self.path {
117            Some(path) => format!("{path}"),
118            None => "none".to_string(),
119        };
120
121        // Use Display for kind which gives human-readable error messages
122        write!(
123            f,
124            "DeserializeError {{ span: {}, path: {}, kind: {} }}",
125            span_str, path_str, self.kind
126        )
127    }
128}
129
130/// Specific kinds of deserialization errors.
131///
132/// Uses `Cow<'static, str>` to avoid allocations when possible while still
133/// supporting owned strings when needed (e.g., field names from input).
134#[derive(Debug)]
135#[non_exhaustive]
136pub enum DeserializeErrorKind {
137    // ============================================================
138    // Parser-level errors (thrown by FormatParser implementations)
139    // ============================================================
140    //
141    // These errors occur during lexing/parsing of the input format,
142    // before we even try to map values to Rust types.
143    /// Unexpected character encountered by the parser.
144    ///
145    /// **Level:** Parser (e.g., `JsonParser`)
146    ///
147    /// This happens when the parser encounters a character that doesn't
148    /// fit the format's grammar at the current position.
149    ///
150    /// ```text
151    /// {"name": @invalid}
152    ///          ^
153    ///          unexpected character '@', expected value
154    /// ```
155    UnexpectedChar {
156        /// The character that was found.
157        ch: char,
158        /// What was expected instead (e.g., "value", "digit", "string").
159        expected: &'static str,
160    },
161
162    /// Unexpected end of input.
163    ///
164    /// **Level:** Parser (e.g., `JsonParser`)
165    ///
166    /// The input ended before a complete value could be parsed.
167    ///
168    /// ```text
169    /// {"name": "Alice
170    ///                ^
171    ///                unexpected EOF, expected closing quote
172    /// ```
173    UnexpectedEof {
174        /// What was expected before EOF.
175        expected: &'static str,
176    },
177
178    /// Invalid UTF-8 sequence in input.
179    ///
180    /// **Level:** Parser (e.g., `JsonParser`)
181    ///
182    /// The input contains bytes that don't form valid UTF-8.
183    ///
184    /// ```text
185    /// {"name": "hello\xff world"}
186    ///                 ^^^^
187    ///                 invalid UTF-8 sequence
188    /// ```
189    InvalidUtf8 {
190        /// Up to 16 bytes of context around the invalid sequence.
191        context: [u8; 16],
192        /// Number of valid bytes in context (0-16).
193        context_len: u8,
194    },
195
196    // ============================================================
197    // Deserializer-level errors (thrown by FormatDeserializer)
198    // ============================================================
199    //
200    // These errors occur when mapping parsed tokens to Rust types.
201    // The parser successfully produced tokens, but they don't match
202    // what the deserializer expected for the target type.
203    /// Unexpected token from parser.
204    ///
205    /// **Level:** Deserializer (`FormatDeserializer`)
206    ///
207    /// The parser produced a valid token, but it's not what the deserializer
208    /// expected at this point given the target Rust type.
209    ///
210    /// ```text
211    /// // Deserializing into Vec<i32>
212    /// {"not": "an array"}
213    /// ^
214    /// unexpected token: got object, expected array
215    /// ```
216    ///
217    /// **Not to be confused with:**
218    /// - `UnexpectedChar`: parser-level, about invalid syntax
219    /// - `TypeMismatch`: about shape expectations, not token types
220    UnexpectedToken {
221        /// The token that was found (e.g., "object", "string", "null").
222        got: Cow<'static, str>,
223        /// What was expected instead (e.g., "array", "number").
224        expected: &'static str,
225    },
226
227    /// Type mismatch: expected a shape, got something else from the parser.
228    ///
229    /// **Level:** Deserializer (`FormatDeserializer`)
230    ///
231    /// We know the target Rust type (Shape), but the parser gave us
232    /// something incompatible.
233    ///
234    /// ```text
235    /// // Deserializing into struct User { age: u32 }
236    /// {"age": "not a number"}
237    ///         ^^^^^^^^^^^^^^
238    ///         type mismatch: expected u32, got string
239    /// ```
240    TypeMismatch {
241        /// The expected shape/type we were trying to deserialize into.
242        expected: &'static Shape,
243        /// Description of what we got from the parser.
244        got: Cow<'static, str>,
245    },
246
247    /// Shape mismatch: expected one Rust type, but the code path requires another.
248    ///
249    /// **Level:** Deserializer (`FormatDeserializer`)
250    ///
251    /// This is an internal routing error - the deserializer was asked to
252    /// deserialize into a type that doesn't match what the current code
253    /// path expects. For example, calling enum deserialization on a struct.
254    ///
255    /// ```text
256    /// // Internal error: deserialize_enum called but shape is a struct
257    /// shape mismatch: expected enum, got struct User
258    /// ```
259    ///
260    /// **Not to be confused with:**
261    /// - `TypeMismatch`: about parser output vs expected type
262    /// - `UnexpectedToken`: about token types from parser
263    ShapeMismatch {
264        /// The shape that was expected by this code path.
265        expected: &'static Shape,
266        /// The actual shape that was provided.
267        got: &'static Shape,
268    },
269
270    /// Unknown field in struct.
271    ///
272    /// **Level:** Deserializer (`FormatDeserializer`)
273    ///
274    /// The input contains a field name that doesn't exist in the target struct
275    /// and the struct doesn't allow unknown fields (no `#[facet(deny_unknown_fields)]`
276    /// or similar).
277    ///
278    /// ```text
279    /// // Deserializing into struct User { name: String }
280    /// {"name": "Alice", "age": 30}
281    ///                   ^^^^^
282    ///                   unknown field `age`
283    /// ```
284    UnknownField {
285        /// The unknown field name.
286        field: Cow<'static, str>,
287        /// Optional suggestion for a similar field (typo correction).
288        suggestion: Option<&'static str>,
289    },
290
291    /// Unknown enum variant.
292    ///
293    /// **Level:** Deserializer (`FormatDeserializer`)
294    ///
295    /// The input specifies a variant name that doesn't exist in the target enum.
296    ///
297    /// ```text
298    /// // Deserializing into enum Status { Active, Inactive }
299    /// "Pending"
300    /// ^^^^^^^^^
301    /// unknown variant `Pending` for enum `Status`
302    /// ```
303    UnknownVariant {
304        /// The unknown variant name from the input.
305        variant: Cow<'static, str>,
306
307        /// The enum type.
308        enum_shape: &'static Shape,
309    },
310
311    /// No variant matched for untagged enum.
312    ///
313    /// **Level:** Deserializer (`FormatDeserializer`)
314    ///
315    /// For `#[facet(untagged)]` enums, we try each variant in order.
316    /// This error means none of them matched the input.
317    ///
318    /// ```text
319    /// // Deserializing into #[facet(untagged)] enum Value { Int(i32), Str(String) }
320    /// [1, 2, 3]
321    /// ^^^^^^^^^
322    /// no matching variant for enum `Value` with array input
323    /// ```
324    NoMatchingVariant {
325        /// The enum type.
326        enum_shape: &'static Shape,
327        /// What kind of input was provided (e.g., "array", "object", "string").
328        input_kind: &'static str,
329    },
330
331    /// Missing required field.
332    ///
333    /// **Level:** Deserializer (`FormatDeserializer`)
334    ///
335    /// A struct field without a default value was not provided in the input.
336    ///
337    /// ```text
338    /// // Deserializing into struct User { name: String, email: String }
339    /// {"name": "Alice"}
340    ///                 ^
341    ///                 missing field `email` in type `User`
342    /// ```
343    MissingField {
344        /// The field that is missing.
345        field: &'static str,
346        /// The type that contains the field.
347        container_shape: &'static Shape,
348    },
349
350    /// Duplicate field in input.
351    ///
352    /// **Level:** Deserializer (`FormatDeserializer`)
353    ///
354    /// The same field appears multiple times in the input.
355    ///
356    /// ```text
357    /// {"name": "Alice", "name": "Bob"}
358    ///                   ^^^^^^
359    ///                   duplicate field `name` (first occurrence at offset 1)
360    /// ```
361    DuplicateField {
362        /// The field that appeared more than once.
363        field: Cow<'static, str>,
364        /// Span of the first occurrence (for better diagnostics).
365        first_span: Option<Span>,
366    },
367
368    // ============================================================
369    // Value errors
370    // ============================================================
371    /// Number out of range for target type.
372    ///
373    /// **Level:** Deserializer (`FormatDeserializer`)
374    ///
375    /// The input contains a valid number, but it doesn't fit in the target type.
376    ///
377    /// ```text
378    /// // Deserializing into u8
379    /// 256
380    /// ^^^
381    /// number `256` out of range for u8
382    /// ```
383    NumberOutOfRange {
384        /// The numeric value as a string.
385        value: Cow<'static, str>,
386        /// The target type that couldn't hold the value.
387        target_type: &'static str,
388    },
389
390    /// Invalid value for the target type.
391    ///
392    /// **Level:** Deserializer (`FormatDeserializer`)
393    ///
394    /// The value is syntactically valid but semantically wrong for the target type.
395    /// Used for things like invalid enum discriminants, malformed UUIDs, etc.
396    ///
397    /// ```text
398    /// // Deserializing into Uuid
399    /// "not-a-valid-uuid"
400    /// ^^^^^^^^^^^^^^^^^^
401    /// invalid value: expected UUID format
402    /// ```
403    InvalidValue {
404        /// Description of why the value is invalid.
405        message: Cow<'static, str>,
406    },
407
408    /// Cannot borrow string from input.
409    ///
410    /// **Level:** Deserializer (`FormatDeserializer`)
411    ///
412    /// When deserializing into `&str` or `Cow<str>`, the string in the input
413    /// required processing (e.g., escape sequences) and cannot be borrowed.
414    ///
415    /// ```text
416    /// // Deserializing into &str
417    /// "hello\nworld"
418    /// ^^^^^^^^^^^^^^
419    /// cannot borrow: string contains escape sequences
420    /// ```
421    CannotBorrow {
422        /// Description of why borrowing failed.
423        reason: Cow<'static, str>,
424    },
425
426    // ============================================================
427    // Reflection errors
428    // ============================================================
429    /// Error from the reflection system.
430    ///
431    /// **Level:** Deserializer (via `facet-reflect`)
432    ///
433    /// These errors come from `Partial` operations like field access,
434    /// variant selection, or type building.
435    ///
436    /// Note: The path is stored at the `DeserializeError` level, not here.
437    /// When converting from `ReflectError`, the path is extracted and stored
438    /// in `DeserializeError.path`.
439    Reflect {
440        /// The specific kind of reflection error
441        kind: ReflectErrorKind,
442
443        /// What we were trying to do
444        context: &'static str,
445    },
446
447    // ============================================================
448    // Infrastructure errors
449    // ============================================================
450    /// Feature not implemented.
451    ///
452    /// **Level:** Deserializer or Parser
453    ///
454    /// The requested operation is not yet implemented. This is used for
455    /// known gaps in functionality, not for invalid input.
456    ///
457    /// ```text
458    /// // Trying to deserialize a type that's not yet supported
459    /// unsupported: multi-element tuple variants in flatten not yet supported
460    /// ```
461    Unsupported {
462        /// Description of what is unsupported.
463        message: Cow<'static, str>,
464    },
465
466    /// I/O error during streaming deserialization.
467    ///
468    /// **Level:** Parser
469    ///
470    /// For parsers that read from streams, this wraps I/O errors.
471    Io {
472        /// Description of the I/O error.
473        message: Cow<'static, str>,
474    },
475
476    /// Error from the flatten solver.
477    ///
478    /// **Level:** Deserializer (via `facet-solver`)
479    ///
480    /// When deserializing types with `#[facet(flatten)]`, the solver
481    /// determines which fields go where. This error indicates solver failure.
482    Solver {
483        /// Description of the solver error.
484        message: Cow<'static, str>,
485    },
486
487    /// Validation error.
488    ///
489    /// **Level:** Deserializer (post-deserialization)
490    ///
491    /// After successful deserialization, validation constraints failed.
492    ///
493    /// ```text
494    /// // With #[facet(validate = "validate_age")]
495    /// {"age": -5}
496    ///         ^^
497    ///         validation failed for field `age`: must be non-negative
498    /// ```
499    Validation {
500        /// The field that failed validation.
501        field: &'static str,
502
503        /// The validation error message.
504        message: Cow<'static, str>,
505    },
506
507    /// Internal error indicating a logic bug in facet-format or one of the crates
508    /// that relies on it (facet-json,e tc.)
509    Bug {
510        /// What happened?
511        error: Cow<'static, str>,
512
513        /// What were we doing?
514        context: &'static str,
515    },
516
517    /// Memory allocation failed.
518    ///
519    /// **Level:** Deserializer (internal)
520    ///
521    /// Failed to allocate memory for the partial value being built.
522    /// This is rare but can happen with very large types or low memory.
523    Alloc {
524        /// The shape we tried to allocate.
525        shape: &'static Shape,
526
527        /// What operation was being attempted.
528        operation: &'static str,
529    },
530
531    /// Shape mismatch when materializing a value.
532    ///
533    /// **Level:** Deserializer (internal)
534    ///
535    /// The shape of the built value doesn't match the target type.
536    /// This indicates a bug in the deserializer logic.
537    Materialize {
538        /// The shape that was expected (the target type).
539        expected: &'static Shape,
540
541        /// The shape that was actually found.
542        actual: &'static Shape,
543    },
544
545    /// Raw capture is not supported by the current parser.
546    ///
547    /// **Level:** Deserializer (`FormatDeserializer`)
548    ///
549    /// Types like `RawJson` require capturing the raw input without parsing it.
550    /// This error occurs when attempting to deserialize such a type with a parser
551    /// that doesn't support raw capture (e.g., streaming parsers without buffering).
552    ///
553    /// ```text
554    /// // Deserializing RawJson in streaming mode
555    /// raw capture not supported: type `RawJson` requires raw capture, but the
556    /// parser does not support it (e.g., streaming mode without buffering)
557    /// ```
558    RawCaptureNotSupported {
559        /// The type that requires raw capture.
560        shape: &'static Shape,
561    },
562}
563
564impl fmt::Display for DeserializeError {
565    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
566        write!(f, "{}", self.kind)?;
567        if let Some(ref path) = self.path {
568            write!(f, " at {path:?}")?;
569        }
570        Ok(())
571    }
572}
573
574impl fmt::Display for DeserializeErrorKind {
575    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
576        match self {
577            DeserializeErrorKind::UnexpectedChar { ch, expected } => {
578                write!(f, "unexpected character {ch:?}, expected {expected}")
579            }
580            DeserializeErrorKind::UnexpectedEof { expected } => {
581                write!(f, "unexpected end of input, expected {expected}")
582            }
583            DeserializeErrorKind::UnexpectedToken { got, expected } => {
584                write!(f, "unexpected token: got {got}, expected {expected}")
585            }
586            DeserializeErrorKind::InvalidUtf8 {
587                context,
588                context_len,
589            } => {
590                let len = (*context_len as usize).min(16);
591                if len > 0 {
592                    write!(f, "invalid UTF-8 near: {:?}", &context[..len])
593                } else {
594                    write!(f, "invalid UTF-8")
595                }
596            }
597            DeserializeErrorKind::TypeMismatch { expected, got } => {
598                write!(f, "type mismatch: expected {expected}, got {got}")
599            }
600            DeserializeErrorKind::ShapeMismatch { expected, got } => {
601                write!(f, "shape mismatch: expected {expected}, got {got}")
602            }
603            DeserializeErrorKind::UnknownField { field, suggestion } => {
604                write!(f, "unknown field `{field}`")?;
605                if let Some(s) = suggestion {
606                    write!(f, " (did you mean `{s}`?)")?;
607                }
608                Ok(())
609            }
610            DeserializeErrorKind::UnknownVariant {
611                variant,
612                enum_shape,
613            } => {
614                write!(f, "unknown variant `{variant}` for enum `{enum_shape}`")
615            }
616            DeserializeErrorKind::NoMatchingVariant {
617                enum_shape,
618                input_kind,
619            } => {
620                write!(
621                    f,
622                    "no matching variant found for enum `{enum_shape}` with {input_kind} input"
623                )
624            }
625            DeserializeErrorKind::MissingField {
626                field,
627                container_shape,
628            } => {
629                write!(f, "missing field `{field}` in type `{container_shape}`")
630            }
631            DeserializeErrorKind::DuplicateField { field, .. } => {
632                write!(f, "duplicate field `{field}`")
633            }
634            DeserializeErrorKind::NumberOutOfRange { value, target_type } => {
635                write!(f, "number `{value}` out of range for {target_type}")
636            }
637            DeserializeErrorKind::InvalidValue { message } => {
638                write!(f, "invalid value: {message}")
639            }
640            DeserializeErrorKind::CannotBorrow { reason } => write!(f, "{reason}"),
641            DeserializeErrorKind::Reflect { kind, context } => {
642                if context.is_empty() {
643                    write!(f, "{kind}")
644                } else {
645                    write!(f, "{kind} (while {context})")
646                }
647            }
648            DeserializeErrorKind::Unsupported { message } => write!(f, "unsupported: {message}"),
649            DeserializeErrorKind::Io { message } => write!(f, "I/O error: {message}"),
650            DeserializeErrorKind::Solver { message } => write!(f, "solver error: {message}"),
651            DeserializeErrorKind::Validation { field, message } => {
652                write!(f, "validation failed for field `{field}`: {message}")
653            }
654            DeserializeErrorKind::Bug { error, context } => {
655                write!(f, "internal error: {error} while {context}")
656            }
657            DeserializeErrorKind::Alloc { shape, operation } => {
658                write!(f, "allocation failed for {shape}: {operation}")
659            }
660            DeserializeErrorKind::Materialize { expected, actual } => {
661                write!(
662                    f,
663                    "shape mismatch when materializing: expected {expected}, got {actual}"
664                )
665            }
666            DeserializeErrorKind::RawCaptureNotSupported { shape: type_name } => {
667                write!(
668                    f,
669                    "raw capture not supported: type `{type_name}` requires raw capture, \
670                     but the parser does not support it (e.g., streaming mode without buffering)"
671                )
672            }
673        }
674    }
675}
676
677impl std::error::Error for DeserializeError {}
678
679impl From<ReflectError> for DeserializeError {
680    fn from(e: ReflectError) -> Self {
681        let kind = match e.kind {
682            ReflectErrorKind::UninitializedField { shape, field_name } => {
683                DeserializeErrorKind::MissingField {
684                    field: field_name,
685                    container_shape: shape,
686                }
687            }
688            other => DeserializeErrorKind::Reflect {
689                kind: other,
690                context: "",
691            },
692        };
693        DeserializeError {
694            span: Some(current_span()),
695            path: Some(e.path),
696            kind,
697        }
698    }
699}
700
701impl From<AllocError> for DeserializeError {
702    fn from(e: AllocError) -> Self {
703        DeserializeError {
704            span: None,
705            path: None,
706            kind: DeserializeErrorKind::Alloc {
707                shape: e.shape,
708                operation: e.operation,
709            },
710        }
711    }
712}
713
714impl From<ShapeMismatchError> for DeserializeError {
715    fn from(e: ShapeMismatchError) -> Self {
716        DeserializeError {
717            span: None,
718            path: None,
719            kind: DeserializeErrorKind::Materialize {
720                expected: e.expected,
721                actual: e.actual,
722            },
723        }
724    }
725}
726
727impl DeserializeErrorKind {
728    /// Attach a span to this error kind, producing a full DeserializeError.
729    #[inline]
730    pub const fn with_span(self, span: Span) -> DeserializeError {
731        DeserializeError {
732            span: Some(span),
733            path: None,
734            kind: self,
735        }
736    }
737
738    // Note: there is no "without_span" method because you should always indicate
739    // where an error happened. Hope this helps.
740}
741
742impl DeserializeError {
743    /// Add span information to this error.
744    #[inline]
745    pub fn set_span(mut self, span: Span) -> Self {
746        self.span = Some(span);
747        self
748    }
749
750    /// Add path information to this error.
751    #[inline]
752    pub fn set_path(mut self, path: Path) -> Self {
753        self.path = Some(path);
754        self
755    }
756
757    /// Get the path where the error occurred, if available.
758    #[inline]
759    pub const fn path(&self) -> Option<&Path> {
760        self.path.as_ref()
761    }
762
763    /// Get the span where the error occurred, if available.
764    #[inline]
765    pub const fn span(&self) -> Option<&Span> {
766        self.span.as_ref()
767    }
768
769    /// Add path information to an error (consumes and returns the modified error).
770    #[inline]
771    pub fn with_path(mut self, new_path: Path) -> Self {
772        self.path = Some(new_path);
773        self
774    }
775}
776
777// ============================================================
778// Pretty error rendering with ariadne
779// ============================================================
780
781#[cfg(feature = "ariadne")]
782mod ariadne_impl {
783    use super::*;
784    use ariadne::{Color, Label, Report, ReportKind, Source};
785    use std::io::Write;
786
787    impl DeserializeError {
788        /// Render this error as a pretty diagnostic using ariadne.
789        ///
790        /// # Arguments
791        /// * `filename` - The filename to show in the diagnostic (e.g., "queries.styx")
792        /// * `source` - The source text that was being parsed
793        ///
794        /// # Returns
795        /// A string containing the formatted diagnostic with colors (ANSI codes).
796        pub fn to_pretty(&self, filename: &str, source: &str) -> String {
797            let mut buf = Vec::new();
798            self.write_pretty(&mut buf, filename, source)
799                .expect("writing to Vec<u8> should never fail");
800            String::from_utf8(buf).expect("ariadne output should be valid UTF-8")
801        }
802
803        /// Write this error as a pretty diagnostic to a writer.
804        ///
805        /// # Arguments
806        /// * `writer` - Where to write the diagnostic
807        /// * `filename` - The filename to show in the diagnostic
808        /// * `source` - The source text that was being parsed
809        pub fn write_pretty<W: Write>(
810            &self,
811            writer: &mut W,
812            filename: &str,
813            source: &str,
814        ) -> std::io::Result<()> {
815            let (offset, len) = match self.span {
816                Some(span) => (span.offset as usize, span.len as usize),
817                None => (0, 0),
818            };
819
820            // Clamp to source bounds
821            let offset = offset.min(source.len());
822            let end = (offset + len).min(source.len());
823            let range = offset..end.max(offset + 1).min(source.len());
824
825            let message = self.kind.to_string();
826
827            let mut report =
828                Report::build(ReportKind::Error, (filename, range.clone())).with_message(&message);
829
830            // Add the main label pointing to the error location
831            let label = Label::new((filename, range))
832                .with_message(&message)
833                .with_color(Color::Red);
834            report = report.with_label(label);
835
836            // Add path information as a note if available
837            if let Some(ref path) = self.path {
838                report = report.with_note(format!("at path: {path}"));
839            }
840
841            report
842                .finish()
843                .write((filename, Source::from(source)), writer)
844        }
845
846        /// Print this error as a pretty diagnostic to stderr.
847        ///
848        /// # Arguments
849        /// * `filename` - The filename to show in the diagnostic
850        /// * `source` - The source text that was being parsed
851        pub fn eprint(&self, filename: &str, source: &str) {
852            let _ = self.write_pretty(&mut std::io::stderr(), filename, source);
853        }
854    }
855}