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}