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