miette/
protocol.rs

1/*!
2This module defines the core of the miette protocol: a series of types and
3traits that you can implement to get access to miette's (and related library's)
4full reporting and such features.
5*/
6use std::{
7    fmt::{self, Display},
8    fs,
9    panic::Location,
10};
11
12#[cfg(feature = "serde")]
13use serde::{Deserialize, Serialize};
14
15use crate::MietteError;
16
17/// Adds rich metadata to your Error that can be used by
18/// [`Report`](crate::Report) to print really nice and human-friendly error
19/// messages.
20pub trait Diagnostic: std::error::Error {
21    /// Unique diagnostic code that can be used to look up more information
22    /// about this `Diagnostic`. Ideally also globally unique, and documented
23    /// in the toplevel crate's documentation for easy searching. Rust path
24    /// format (`foo::bar::baz`) is recommended, but more classic codes like
25    /// `E0123` or enums will work just fine.
26    fn code<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
27        None
28    }
29
30    /// Diagnostic severity. This may be used by
31    /// [`ReportHandler`](crate::ReportHandler)s to change the display format
32    /// of this diagnostic.
33    ///
34    /// If `None`, reporters should treat this as [`Severity::Error`].
35    fn severity(&self) -> Option<Severity> {
36        None
37    }
38
39    /// Additional help text related to this `Diagnostic`. Do you have any
40    /// advice for the poor soul who's just run into this issue?
41    fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
42        None
43    }
44
45    /// URL to visit for a more detailed explanation/help about this
46    /// `Diagnostic`.
47    fn url<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
48        None
49    }
50
51    /// Source code to apply this `Diagnostic`'s [`Diagnostic::labels`] to.
52    fn source_code(&self) -> Option<&dyn SourceCode> {
53        None
54    }
55
56    /// Labels to apply to this `Diagnostic`'s [`Diagnostic::source_code`]
57    fn labels(&self) -> Option<Box<dyn Iterator<Item = LabeledSpan> + '_>> {
58        None
59    }
60
61    /// Additional related `Diagnostic`s.
62    fn related<'a>(&'a self) -> Option<Box<dyn Iterator<Item = &'a dyn Diagnostic> + 'a>> {
63        None
64    }
65
66    /// The cause of the error.
67    fn diagnostic_source(&self) -> Option<&dyn Diagnostic> {
68        None
69    }
70}
71
72macro_rules! box_error_impls {
73    ($($box_type:ty),*) => {
74        $(
75            impl std::error::Error for $box_type {
76                fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
77                    (**self).source()
78                }
79
80                fn cause(&self) -> Option<&dyn std::error::Error> {
81                    self.source()
82                }
83            }
84        )*
85    }
86}
87
88box_error_impls! {
89    Box<dyn Diagnostic>,
90    Box<dyn Diagnostic + Send>,
91    Box<dyn Diagnostic + Send + Sync>
92}
93
94macro_rules! box_borrow_impls {
95    ($($box_type:ty),*) => {
96        $(
97            impl std::borrow::Borrow<dyn Diagnostic> for $box_type {
98                fn borrow(&self) -> &(dyn Diagnostic + 'static) {
99                    self.as_ref()
100                }
101            }
102        )*
103    }
104}
105
106box_borrow_impls! {
107    Box<dyn Diagnostic + Send>,
108    Box<dyn Diagnostic + Send + Sync>
109}
110
111impl<T: Diagnostic + Send + Sync + 'static> From<T>
112    for Box<dyn Diagnostic + Send + Sync + 'static>
113{
114    fn from(diag: T) -> Self {
115        Box::new(diag)
116    }
117}
118
119impl<T: Diagnostic + Send + Sync + 'static> From<T> for Box<dyn Diagnostic + Send + 'static> {
120    fn from(diag: T) -> Self {
121        Box::<dyn Diagnostic + Send + Sync>::from(diag)
122    }
123}
124
125impl<T: Diagnostic + Send + Sync + 'static> From<T> for Box<dyn Diagnostic + 'static> {
126    fn from(diag: T) -> Self {
127        Box::<dyn Diagnostic + Send + Sync>::from(diag)
128    }
129}
130
131impl From<&str> for Box<dyn Diagnostic> {
132    fn from(s: &str) -> Self {
133        From::from(String::from(s))
134    }
135}
136
137impl From<&str> for Box<dyn Diagnostic + Send + Sync + '_> {
138    fn from(s: &str) -> Self {
139        From::from(String::from(s))
140    }
141}
142
143impl From<String> for Box<dyn Diagnostic> {
144    fn from(s: String) -> Self {
145        let err1: Box<dyn Diagnostic + Send + Sync> = From::from(s);
146        let err2: Box<dyn Diagnostic> = err1;
147        err2
148    }
149}
150
151impl From<String> for Box<dyn Diagnostic + Send + Sync> {
152    fn from(s: String) -> Self {
153        struct StringError(String);
154
155        impl std::error::Error for StringError {}
156        impl Diagnostic for StringError {}
157
158        impl Display for StringError {
159            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
160                Display::fmt(&self.0, f)
161            }
162        }
163
164        // Purposefully skip printing "StringError(..)"
165        impl fmt::Debug for StringError {
166            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
167                fmt::Debug::fmt(&self.0, f)
168            }
169        }
170
171        Box::new(StringError(s))
172    }
173}
174
175impl From<Box<dyn std::error::Error + Send + Sync>> for Box<dyn Diagnostic + Send + Sync> {
176    fn from(s: Box<dyn std::error::Error + Send + Sync>) -> Self {
177        #[derive(thiserror::Error)]
178        #[error(transparent)]
179        struct BoxedDiagnostic(Box<dyn std::error::Error + Send + Sync>);
180        impl fmt::Debug for BoxedDiagnostic {
181            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
182                fmt::Debug::fmt(&self.0, f)
183            }
184        }
185
186        impl Diagnostic for BoxedDiagnostic {}
187
188        Box::new(BoxedDiagnostic(s))
189    }
190}
191
192/**
193[`Diagnostic`] severity. Intended to be used by
194[`ReportHandler`](crate::ReportHandler)s to change the way different
195[`Diagnostic`]s are displayed. Defaults to [`Severity::Error`].
196*/
197#[derive(Copy, Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
198#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
199#[derive(Default)]
200pub enum Severity {
201    /// Just some help. Here's how you could be doing it better.
202    Advice,
203    /// Warning. Please take note.
204    Warning,
205    /// Critical failure. The program cannot continue.
206    /// This is the default severity, if you don't specify another one.
207    #[default]
208    Error,
209}
210
211#[cfg(feature = "serde")]
212#[test]
213fn test_serialize_severity() {
214    use serde_json::json;
215
216    assert_eq!(json!(Severity::Advice), json!("Advice"));
217    assert_eq!(json!(Severity::Warning), json!("Warning"));
218    assert_eq!(json!(Severity::Error), json!("Error"));
219}
220
221#[cfg(feature = "serde")]
222#[test]
223fn test_deserialize_severity() {
224    use serde_json::json;
225
226    let severity: Severity = serde_json::from_value(json!("Advice")).unwrap();
227    assert_eq!(severity, Severity::Advice);
228
229    let severity: Severity = serde_json::from_value(json!("Warning")).unwrap();
230    assert_eq!(severity, Severity::Warning);
231
232    let severity: Severity = serde_json::from_value(json!("Error")).unwrap();
233    assert_eq!(severity, Severity::Error);
234}
235
236/**
237Represents readable source code of some sort.
238
239This trait is able to support simple `SourceCode` types like [`String`]s, as
240well as more involved types like indexes into centralized `SourceMap`-like
241types, file handles, and even network streams.
242
243If you can read it, you can source it, and it's not necessary to read the
244whole thing--meaning you should be able to support `SourceCode`s which are
245gigabytes or larger in size.
246*/
247pub trait SourceCode: Send + Sync {
248    /// Read the bytes for a specific span from this `SourceCode`, keeping a
249    /// certain number of lines before and after the span as context.
250    fn read_span<'a>(
251        &'a self,
252        span: &SourceSpan,
253        context_lines_before: usize,
254        context_lines_after: usize,
255    ) -> Result<Box<dyn SpanContents<'a> + 'a>, MietteError>;
256}
257
258/// A labeled [`SourceSpan`].
259#[derive(Debug, Clone, PartialEq, Eq)]
260#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
261pub struct LabeledSpan {
262    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
263    label: Option<String>,
264    span: SourceSpan,
265    primary: bool,
266}
267
268impl LabeledSpan {
269    /// Makes a new labeled span.
270    #[must_use]
271    pub const fn new(label: Option<String>, offset: ByteOffset, len: usize) -> Self {
272        Self { label, span: SourceSpan::new(SourceOffset(offset), len), primary: false }
273    }
274
275    /// Makes a new labeled span using an existing span.
276    #[must_use]
277    pub fn new_with_span(label: Option<String>, span: impl Into<SourceSpan>) -> Self {
278        Self { label, span: span.into(), primary: false }
279    }
280
281    /// Makes a new labeled primary span using an existing span.
282    #[must_use]
283    pub fn new_primary_with_span(label: Option<String>, span: impl Into<SourceSpan>) -> Self {
284        Self { label, span: span.into(), primary: true }
285    }
286
287    /// Change the text of the label
288    pub fn set_label(&mut self, label: Option<String>) {
289        self.label = label;
290    }
291
292    /// Makes a new label at specified span
293    ///
294    /// # Examples
295    /// ```
296    /// use miette::LabeledSpan;
297    ///
298    /// let source = "Cpp is the best";
299    /// let label = LabeledSpan::at(0..3, "should be Rust");
300    /// assert_eq!(
301    ///     label,
302    ///     LabeledSpan::new(Some("should be Rust".to_string()), 0, 3)
303    /// )
304    /// ```
305    #[must_use]
306    pub fn at(span: impl Into<SourceSpan>, label: impl Into<String>) -> Self {
307        Self::new_with_span(Some(label.into()), span)
308    }
309
310    /// Makes a new label that points at a specific offset.
311    ///
312    /// # Examples
313    /// ```
314    /// use miette::LabeledSpan;
315    ///
316    /// let source = "(2 + 2";
317    /// let label = LabeledSpan::at_offset(4, "expected a closing parenthesis");
318    /// assert_eq!(
319    ///     label,
320    ///     LabeledSpan::new(Some("expected a closing parenthesis".to_string()), 4, 0)
321    /// )
322    /// ```
323    pub fn at_offset(offset: ByteOffset, label: impl Into<String>) -> Self {
324        Self::new(Some(label.into()), offset, 0)
325    }
326
327    /// Makes a new label without text, that underlines a specific span.
328    ///
329    /// # Examples
330    /// ```
331    /// use miette::LabeledSpan;
332    ///
333    /// let source = "You have an error here";
334    /// let label = LabeledSpan::underline(12..16);
335    /// assert_eq!(label, LabeledSpan::new(None, 12, 4))
336    /// ```
337    #[must_use]
338    pub fn underline(span: impl Into<SourceSpan>) -> Self {
339        Self::new_with_span(None, span)
340    }
341
342    /// Gets the (optional) label string for this `LabeledSpan`.
343    pub fn label(&self) -> Option<&str> {
344        self.label.as_deref()
345    }
346
347    /// Returns a reference to the inner [`SourceSpan`].
348    pub const fn inner(&self) -> &SourceSpan {
349        &self.span
350    }
351
352    /// Returns the 0-based starting byte offset.
353    pub const fn offset(&self) -> usize {
354        self.span.offset()
355    }
356
357    /// Returns the number of bytes this `LabeledSpan` spans.
358    pub const fn len(&self) -> usize {
359        self.span.len()
360    }
361
362    /// True if this `LabeledSpan` is empty.
363    pub const fn is_empty(&self) -> bool {
364        self.span.is_empty()
365    }
366
367    /// True if this `LabeledSpan` is a primary span.
368    pub const fn primary(&self) -> bool {
369        self.primary
370    }
371}
372
373#[cfg(feature = "serde")]
374#[test]
375fn test_serialize_labeled_span() {
376    use serde_json::json;
377
378    assert_eq!(
379        json!(LabeledSpan::new(None, 0, 0)),
380        json!({
381            "span": { "offset": 0, "length": 0, },
382            "primary": false,
383        })
384    );
385
386    assert_eq!(
387        json!(LabeledSpan::new(Some("label".to_string()), 0, 0)),
388        json!({
389            "label": "label",
390            "span": { "offset": 0, "length": 0, },
391            "primary": false,
392        })
393    );
394}
395
396#[cfg(feature = "serde")]
397#[test]
398fn test_deserialize_labeled_span() {
399    use serde_json::json;
400
401    let span: LabeledSpan = serde_json::from_value(json!({
402        "label": null,
403        "span": { "offset": 0, "length": 0, },
404        "primary": false,
405    }))
406    .unwrap();
407    assert_eq!(span, LabeledSpan::new(None, 0, 0));
408
409    let span: LabeledSpan = serde_json::from_value(json!({
410        "span": { "offset": 0, "length": 0, },
411        "primary": false
412    }))
413    .unwrap();
414    assert_eq!(span, LabeledSpan::new(None, 0, 0));
415
416    let span: LabeledSpan = serde_json::from_value(json!({
417        "label": "label",
418        "span": { "offset": 0, "length": 0, },
419        "primary": false
420    }))
421    .unwrap();
422    assert_eq!(span, LabeledSpan::new(Some("label".to_string()), 0, 0));
423}
424
425/**
426Contents of a [`SourceCode`] covered by [`SourceSpan`].
427
428Includes line and column information to optimize highlight calculations.
429*/
430pub trait SpanContents<'a> {
431    /// Reference to the data inside the associated span, in bytes.
432    fn data(&self) -> &'a [u8];
433    /// [`SourceSpan`] representing the span covered by this `SpanContents`.
434    fn span(&self) -> &SourceSpan;
435    /// An optional (file?) name for the container of this `SpanContents`.
436    fn name(&self) -> Option<&str> {
437        None
438    }
439    /// The 0-indexed line in the associated [`SourceCode`] where the data
440    /// begins.
441    fn line(&self) -> usize;
442    /// The 0-indexed column in the associated [`SourceCode`] where the data
443    /// begins, relative to `line`.
444    fn column(&self) -> usize;
445    /// Total number of lines covered by this `SpanContents`.
446    fn line_count(&self) -> usize;
447
448    /// Optional method. The language name for this source code, if any.
449    /// This is used to drive syntax highlighting.
450    ///
451    /// Examples: Rust, TOML, C
452    ///
453    fn language(&self) -> Option<&str> {
454        None
455    }
456}
457
458/**
459Basic implementation of the [`SpanContents`] trait, for convenience.
460*/
461#[derive(Clone, Debug)]
462pub struct MietteSpanContents<'a> {
463    // Data from a [`SourceCode`], in bytes.
464    data: &'a [u8],
465    // span actually covered by this SpanContents.
466    span: SourceSpan,
467    // The 0-indexed line where the associated [`SourceSpan`] _starts_.
468    line: usize,
469    // The 0-indexed column where the associated [`SourceSpan`] _starts_.
470    column: usize,
471    // Number of line in this snippet.
472    line_count: usize,
473    // Optional filename
474    name: Option<String>,
475    // Optional language
476    language: Option<String>,
477}
478
479impl<'a> MietteSpanContents<'a> {
480    /// Make a new [`MietteSpanContents`] object.
481    pub const fn new(
482        data: &'a [u8],
483        span: SourceSpan,
484        line: usize,
485        column: usize,
486        line_count: usize,
487    ) -> MietteSpanContents<'a> {
488        MietteSpanContents { data, span, line, column, line_count, name: None, language: None }
489    }
490
491    /// Make a new [`MietteSpanContents`] object, with a name for its 'file'.
492    pub const fn new_named(
493        name: String,
494        data: &'a [u8],
495        span: SourceSpan,
496        line: usize,
497        column: usize,
498        line_count: usize,
499    ) -> MietteSpanContents<'a> {
500        MietteSpanContents {
501            data,
502            span,
503            line,
504            column,
505            line_count,
506            name: Some(name),
507            language: None,
508        }
509    }
510
511    /// Sets the `language` for syntax highlighting.
512    #[must_use]
513    pub fn with_language(mut self, language: impl Into<String>) -> Self {
514        self.language = Some(language.into());
515        self
516    }
517}
518
519impl<'a> SpanContents<'a> for MietteSpanContents<'a> {
520    fn data(&self) -> &'a [u8] {
521        self.data
522    }
523
524    fn span(&self) -> &SourceSpan {
525        &self.span
526    }
527
528    fn line(&self) -> usize {
529        self.line
530    }
531
532    fn column(&self) -> usize {
533        self.column
534    }
535
536    fn line_count(&self) -> usize {
537        self.line_count
538    }
539
540    fn name(&self) -> Option<&str> {
541        self.name.as_deref()
542    }
543
544    fn language(&self) -> Option<&str> {
545        self.language.as_deref()
546    }
547}
548
549/// Span within a [`SourceCode`]
550#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
551#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
552pub struct SourceSpan {
553    /// The start of the span.
554    offset: SourceOffset,
555    /// The total length of the span
556    length: usize,
557}
558
559impl SourceSpan {
560    /// Create a new [`SourceSpan`].
561    pub const fn new(start: SourceOffset, length: usize) -> Self {
562        Self { offset: start, length }
563    }
564
565    /// The absolute offset, in bytes, from the beginning of a [`SourceCode`].
566    pub const fn offset(&self) -> usize {
567        self.offset.offset()
568    }
569
570    /// Total length of the [`SourceSpan`], in bytes.
571    pub const fn len(&self) -> usize {
572        self.length
573    }
574
575    /// Whether this [`SourceSpan`] has a length of zero. It may still be useful
576    /// to point to a specific point.
577    pub const fn is_empty(&self) -> bool {
578        self.length == 0
579    }
580}
581
582impl From<(ByteOffset, usize)> for SourceSpan {
583    fn from((start, len): (ByteOffset, usize)) -> Self {
584        Self { offset: start.into(), length: len }
585    }
586}
587
588impl From<(SourceOffset, usize)> for SourceSpan {
589    fn from((start, len): (SourceOffset, usize)) -> Self {
590        Self::new(start, len)
591    }
592}
593
594impl From<std::ops::Range<ByteOffset>> for SourceSpan {
595    fn from(range: std::ops::Range<ByteOffset>) -> Self {
596        Self { offset: range.start.into(), length: range.len() }
597    }
598}
599
600impl From<SourceOffset> for SourceSpan {
601    fn from(offset: SourceOffset) -> Self {
602        Self { offset, length: 0 }
603    }
604}
605
606impl From<ByteOffset> for SourceSpan {
607    fn from(offset: ByteOffset) -> Self {
608        Self { offset: offset.into(), length: 0 }
609    }
610}
611
612#[cfg(feature = "serde")]
613#[test]
614fn test_serialize_source_span() {
615    use serde_json::json;
616
617    assert_eq!(json!(SourceSpan::from(0)), json!({ "offset": 0, "length": 0}));
618}
619
620#[cfg(feature = "serde")]
621#[test]
622fn test_deserialize_source_span() {
623    use serde_json::json;
624
625    let span: SourceSpan = serde_json::from_value(json!({ "offset": 0, "length": 0})).unwrap();
626    assert_eq!(span, SourceSpan::from(0));
627}
628
629/**
630"Raw" type for the byte offset from the beginning of a [`SourceCode`].
631*/
632pub type ByteOffset = usize;
633
634/**
635Newtype that represents the [`ByteOffset`] from the beginning of a [`SourceCode`]
636*/
637#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
638#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
639pub struct SourceOffset(ByteOffset);
640
641impl SourceOffset {
642    /// Actual byte offset.
643    pub const fn offset(&self) -> ByteOffset {
644        self.0
645    }
646
647    /// Little utility to help convert 1-based line/column locations into
648    /// miette-compatible Spans
649    ///
650    /// This function is infallible: Giving an out-of-range line/column pair
651    /// will return the offset of the last byte in the source.
652    pub fn from_location(source: impl AsRef<str>, loc_line: usize, loc_col: usize) -> Self {
653        let mut line = 0usize;
654        let mut col = 0usize;
655        let mut offset = 0usize;
656        for char in source.as_ref().chars() {
657            if line + 1 >= loc_line && col + 1 >= loc_col {
658                break;
659            }
660            if char == '\n' {
661                col = 0;
662                line += 1;
663            } else {
664                col += 1;
665            }
666            offset += char.len_utf8();
667        }
668
669        SourceOffset(offset)
670    }
671
672    /// Returns an offset for the _file_ location of wherever this function is
673    /// called. If you want to get _that_ caller's location, mark this
674    /// function's caller with `#[track_caller]` (and so on and so forth).
675    ///
676    /// Returns both the filename that was given and the offset of the caller
677    /// as a [`SourceOffset`].
678    ///
679    /// Keep in mind that this fill only work if the file your Rust source
680    /// file was compiled from is actually available at that location. If
681    /// you're shipping binaries for your application, you'll want to ignore
682    /// the Err case or otherwise report it.
683    #[track_caller]
684    pub fn from_current_location() -> Result<(String, Self), MietteError> {
685        let loc = Location::caller();
686        Ok((
687            loc.file().into(),
688            fs::read_to_string(loc.file())
689                .map(|txt| Self::from_location(txt, loc.line() as usize, loc.column() as usize))?,
690        ))
691    }
692}
693
694impl From<ByteOffset> for SourceOffset {
695    fn from(bytes: ByteOffset) -> Self {
696        SourceOffset(bytes)
697    }
698}
699
700#[test]
701fn test_source_offset_from_location() {
702    let source = "f\n\noo\r\nbar";
703
704    assert_eq!(SourceOffset::from_location(source, 1, 1).offset(), 0);
705    assert_eq!(SourceOffset::from_location(source, 1, 2).offset(), 1);
706    assert_eq!(SourceOffset::from_location(source, 2, 1).offset(), 2);
707    assert_eq!(SourceOffset::from_location(source, 3, 1).offset(), 3);
708    assert_eq!(SourceOffset::from_location(source, 3, 2).offset(), 4);
709    assert_eq!(SourceOffset::from_location(source, 3, 3).offset(), 5);
710    assert_eq!(SourceOffset::from_location(source, 3, 4).offset(), 6);
711    assert_eq!(SourceOffset::from_location(source, 4, 1).offset(), 7);
712    assert_eq!(SourceOffset::from_location(source, 4, 2).offset(), 8);
713    assert_eq!(SourceOffset::from_location(source, 4, 3).offset(), 9);
714    assert_eq!(SourceOffset::from_location(source, 4, 4).offset(), 10);
715
716    // Out-of-range
717    assert_eq!(SourceOffset::from_location(source, 5, 1).offset(), source.len());
718}
719
720#[cfg(feature = "serde")]
721#[test]
722fn test_serialize_source_offset() {
723    use serde_json::json;
724
725    assert_eq!(json!(SourceOffset::from(0)), 0);
726}
727
728#[cfg(feature = "serde")]
729#[test]
730fn test_deserialize_source_offset() {
731    let offset: SourceOffset = serde_json::from_str("0").unwrap();
732    assert_eq!(offset, SourceOffset::from(0));
733}