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}