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::{DiagnosticError, 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        Box::new(DiagnosticError(s))
178    }
179}
180
181/**
182[`Diagnostic`] severity. Intended to be used by
183[`ReportHandler`](crate::ReportHandler)s to change the way different
184[`Diagnostic`]s are displayed. Defaults to [`Severity::Error`].
185*/
186#[derive(Copy, Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
187#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
188#[derive(Default)]
189pub enum Severity {
190    /// Just some help. Here's how you could be doing it better.
191    Advice,
192    /// Warning. Please take note.
193    Warning,
194    /// Critical failure. The program cannot continue.
195    /// This is the default severity, if you don't specify another one.
196    #[default]
197    Error,
198}
199
200#[cfg(feature = "serde")]
201#[test]
202fn test_serialize_severity() {
203    use serde_json::json;
204
205    assert_eq!(json!(Severity::Advice), json!("Advice"));
206    assert_eq!(json!(Severity::Warning), json!("Warning"));
207    assert_eq!(json!(Severity::Error), json!("Error"));
208}
209
210#[cfg(feature = "serde")]
211#[test]
212fn test_deserialize_severity() {
213    use serde_json::json;
214
215    let severity: Severity = serde_json::from_value(json!("Advice")).unwrap();
216    assert_eq!(severity, Severity::Advice);
217
218    let severity: Severity = serde_json::from_value(json!("Warning")).unwrap();
219    assert_eq!(severity, Severity::Warning);
220
221    let severity: Severity = serde_json::from_value(json!("Error")).unwrap();
222    assert_eq!(severity, Severity::Error);
223}
224
225/**
226Represents readable source code of some sort.
227
228This trait is able to support simple `SourceCode` types like [`String`]s, as
229well as more involved types like indexes into centralized `SourceMap`-like
230types, file handles, and even network streams.
231
232If you can read it, you can source it, and it's not necessary to read the
233whole thing--meaning you should be able to support `SourceCode`s which are
234gigabytes or larger in size.
235*/
236pub trait SourceCode: Send + Sync {
237    /// Read the bytes for a specific span from this `SourceCode`, keeping a
238    /// certain number of lines before and after the span as context.
239    fn read_span<'a>(
240        &'a self,
241        span: &SourceSpan,
242        context_lines_before: usize,
243        context_lines_after: usize,
244    ) -> Result<Box<dyn SpanContents<'a> + 'a>, MietteError>;
245}
246
247/// A labeled [`SourceSpan`].
248#[derive(Debug, Clone, PartialEq, Eq)]
249#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
250pub struct LabeledSpan {
251    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
252    label: Option<String>,
253    span: SourceSpan,
254    primary: bool,
255}
256
257impl LabeledSpan {
258    /// Makes a new labeled span.
259    pub const fn new(label: Option<String>, offset: ByteOffset, len: usize) -> Self {
260        Self {
261            label,
262            span: SourceSpan::new(SourceOffset(offset), len),
263            primary: false,
264        }
265    }
266
267    /// Makes a new labeled span using an existing span.
268    pub fn new_with_span(label: Option<String>, span: impl Into<SourceSpan>) -> Self {
269        Self {
270            label,
271            span: span.into(),
272            primary: false,
273        }
274    }
275
276    /// Makes a new labeled primary span using an existing span.
277    pub fn new_primary_with_span(label: Option<String>, span: impl Into<SourceSpan>) -> Self {
278        Self {
279            label,
280            span: span.into(),
281            primary: true,
282        }
283    }
284
285    /// Change the text of the label
286    pub fn set_label(&mut self, label: Option<String>) {
287        self.label = label;
288    }
289
290    /// Makes a new label at specified span
291    ///
292    /// # Examples
293    /// ```
294    /// use miette::LabeledSpan;
295    ///
296    /// let source = "Cpp is the best";
297    /// let label = LabeledSpan::at(0..3, "should be Rust");
298    /// assert_eq!(
299    ///     label,
300    ///     LabeledSpan::new(Some("should be Rust".to_string()), 0, 3)
301    /// )
302    /// ```
303    pub fn at(span: impl Into<SourceSpan>, label: impl Into<String>) -> Self {
304        Self::new_with_span(Some(label.into()), span)
305    }
306
307    /// Makes a new label that points at a specific offset.
308    ///
309    /// # Examples
310    /// ```
311    /// use miette::LabeledSpan;
312    ///
313    /// let source = "(2 + 2";
314    /// let label = LabeledSpan::at_offset(4, "expected a closing parenthesis");
315    /// assert_eq!(
316    ///     label,
317    ///     LabeledSpan::new(Some("expected a closing parenthesis".to_string()), 4, 0)
318    /// )
319    /// ```
320    pub fn at_offset(offset: ByteOffset, label: impl Into<String>) -> Self {
321        Self::new(Some(label.into()), offset, 0)
322    }
323
324    /// Makes a new label without text, that underlines a specific span.
325    ///
326    /// # Examples
327    /// ```
328    /// use miette::LabeledSpan;
329    ///
330    /// let source = "You have an eror here";
331    /// let label = LabeledSpan::underline(12..16);
332    /// assert_eq!(label, LabeledSpan::new(None, 12, 4))
333    /// ```
334    pub fn underline(span: impl Into<SourceSpan>) -> Self {
335        Self::new_with_span(None, span)
336    }
337
338    /// Gets the (optional) label string for this `LabeledSpan`.
339    pub fn label(&self) -> Option<&str> {
340        self.label.as_deref()
341    }
342
343    /// Returns a reference to the inner [`SourceSpan`].
344    pub const fn inner(&self) -> &SourceSpan {
345        &self.span
346    }
347
348    /// Returns the 0-based starting byte offset.
349    pub const fn offset(&self) -> usize {
350        self.span.offset()
351    }
352
353    /// Returns the number of bytes this `LabeledSpan` spans.
354    pub const fn len(&self) -> usize {
355        self.span.len()
356    }
357
358    /// True if this `LabeledSpan` is empty.
359    pub const fn is_empty(&self) -> bool {
360        self.span.is_empty()
361    }
362
363    /// True if this `LabeledSpan` is a primary span.
364    pub const fn primary(&self) -> bool {
365        self.primary
366    }
367}
368
369#[cfg(feature = "serde")]
370#[test]
371fn test_serialize_labeled_span() {
372    use serde_json::json;
373
374    assert_eq!(
375        json!(LabeledSpan::new(None, 0, 0)),
376        json!({
377            "span": { "offset": 0, "length": 0, },
378            "primary": false,
379        })
380    );
381
382    assert_eq!(
383        json!(LabeledSpan::new(Some("label".to_string()), 0, 0)),
384        json!({
385            "label": "label",
386            "span": { "offset": 0, "length": 0, },
387            "primary": false,
388        })
389    );
390}
391
392#[cfg(feature = "serde")]
393#[test]
394fn test_deserialize_labeled_span() {
395    use serde_json::json;
396
397    let span: LabeledSpan = serde_json::from_value(json!({
398        "label": null,
399        "span": { "offset": 0, "length": 0, },
400        "primary": false,
401    }))
402    .unwrap();
403    assert_eq!(span, LabeledSpan::new(None, 0, 0));
404
405    let span: LabeledSpan = serde_json::from_value(json!({
406        "span": { "offset": 0, "length": 0, },
407        "primary": false
408    }))
409    .unwrap();
410    assert_eq!(span, LabeledSpan::new(None, 0, 0));
411
412    let span: LabeledSpan = serde_json::from_value(json!({
413        "label": "label",
414        "span": { "offset": 0, "length": 0, },
415        "primary": false
416    }))
417    .unwrap();
418    assert_eq!(span, LabeledSpan::new(Some("label".to_string()), 0, 0));
419}
420
421/**
422Contents of a [`SourceCode`] covered by [`SourceSpan`].
423
424Includes line and column information to optimize highlight calculations.
425*/
426pub trait SpanContents<'a> {
427    /// Reference to the data inside the associated span, in bytes.
428    fn data(&self) -> &'a [u8];
429    /// [`SourceSpan`] representing the span covered by this `SpanContents`.
430    fn span(&self) -> &SourceSpan;
431    /// An optional (file?) name for the container of this `SpanContents`.
432    fn name(&self) -> Option<&str> {
433        None
434    }
435    /// The 0-indexed line in the associated [`SourceCode`] where the data
436    /// begins.
437    fn line(&self) -> usize;
438    /// The 0-indexed column in the associated [`SourceCode`] where the data
439    /// begins, relative to `line`.
440    fn column(&self) -> usize;
441    /// Total number of lines covered by this `SpanContents`.
442    fn line_count(&self) -> usize;
443
444    /// Optional method. The language name for this source code, if any.
445    /// This is used to drive syntax highlighting.
446    ///
447    /// Examples: Rust, TOML, C
448    ///
449    fn language(&self) -> Option<&str> {
450        None
451    }
452}
453
454/**
455Basic implementation of the [`SpanContents`] trait, for convenience.
456*/
457#[derive(Clone, Debug)]
458pub struct MietteSpanContents<'a> {
459    // Data from a [`SourceCode`], in bytes.
460    data: &'a [u8],
461    // span actually covered by this SpanContents.
462    span: SourceSpan,
463    // The 0-indexed line where the associated [`SourceSpan`] _starts_.
464    line: usize,
465    // The 0-indexed column where the associated [`SourceSpan`] _starts_.
466    column: usize,
467    // Number of line in this snippet.
468    line_count: usize,
469    // Optional filename
470    name: Option<String>,
471    // Optional language
472    language: Option<String>,
473}
474
475impl<'a> MietteSpanContents<'a> {
476    /// Make a new [`MietteSpanContents`] object.
477    pub const fn new(
478        data: &'a [u8],
479        span: SourceSpan,
480        line: usize,
481        column: usize,
482        line_count: usize,
483    ) -> MietteSpanContents<'a> {
484        MietteSpanContents {
485            data,
486            span,
487            line,
488            column,
489            line_count,
490            name: None,
491            language: None,
492        }
493    }
494
495    /// Make a new [`MietteSpanContents`] object, with a name for its 'file'.
496    pub const fn new_named(
497        name: String,
498        data: &'a [u8],
499        span: SourceSpan,
500        line: usize,
501        column: usize,
502        line_count: usize,
503    ) -> MietteSpanContents<'a> {
504        MietteSpanContents {
505            data,
506            span,
507            line,
508            column,
509            line_count,
510            name: Some(name),
511            language: None,
512        }
513    }
514
515    /// Sets the [`language`](SpanContents::language) for syntax highlighting.
516    pub fn with_language(mut self, language: impl Into<String>) -> Self {
517        self.language = Some(language.into());
518        self
519    }
520}
521
522impl<'a> SpanContents<'a> for MietteSpanContents<'a> {
523    fn data(&self) -> &'a [u8] {
524        self.data
525    }
526    fn span(&self) -> &SourceSpan {
527        &self.span
528    }
529    fn line(&self) -> usize {
530        self.line
531    }
532    fn column(&self) -> usize {
533        self.column
534    }
535    fn line_count(&self) -> usize {
536        self.line_count
537    }
538    fn name(&self) -> Option<&str> {
539        self.name.as_deref()
540    }
541    fn language(&self) -> Option<&str> {
542        self.language.as_deref()
543    }
544}
545
546/// Span within a [`SourceCode`]
547#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
548#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
549pub struct SourceSpan {
550    /// The start of the span.
551    offset: SourceOffset,
552    /// The total length of the span
553    length: usize,
554}
555
556impl SourceSpan {
557    /// Create a new [`SourceSpan`].
558    pub const fn new(start: SourceOffset, length: usize) -> Self {
559        Self {
560            offset: start,
561            length,
562        }
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 {
585            offset: start.into(),
586            length: len,
587        }
588    }
589}
590
591impl From<(SourceOffset, usize)> for SourceSpan {
592    fn from((start, len): (SourceOffset, usize)) -> Self {
593        Self::new(start, len)
594    }
595}
596
597impl From<std::ops::Range<ByteOffset>> for SourceSpan {
598    fn from(range: std::ops::Range<ByteOffset>) -> Self {
599        Self {
600            offset: range.start.into(),
601            length: range.len(),
602        }
603    }
604}
605
606impl From<std::ops::RangeInclusive<ByteOffset>> for SourceSpan {
607    /// # Panics
608    ///
609    /// Panics if the total length of the inclusive range would overflow a
610    /// `usize`. This will only occur with the range `0..=usize::MAX`.
611    fn from(range: std::ops::RangeInclusive<ByteOffset>) -> Self {
612        let (start, end) = range.clone().into_inner();
613        Self {
614            offset: start.into(),
615            length: if range.is_empty() {
616                0
617            } else {
618                // will not overflow because `is_empty() == false` guarantees
619                // that `start <= end`
620                (end - start)
621                    .checked_add(1)
622                    .expect("length of inclusive range should fit in a usize")
623            },
624        }
625    }
626}
627
628impl From<SourceOffset> for SourceSpan {
629    fn from(offset: SourceOffset) -> Self {
630        Self { offset, length: 0 }
631    }
632}
633
634impl From<ByteOffset> for SourceSpan {
635    fn from(offset: ByteOffset) -> Self {
636        Self {
637            offset: offset.into(),
638            length: 0,
639        }
640    }
641}
642
643#[cfg(feature = "serde")]
644#[test]
645fn test_serialize_source_span() {
646    use serde_json::json;
647
648    assert_eq!(
649        json!(SourceSpan::from(0)),
650        json!({ "offset": 0, "length": 0})
651    );
652}
653
654#[cfg(feature = "serde")]
655#[test]
656fn test_deserialize_source_span() {
657    use serde_json::json;
658
659    let span: SourceSpan = serde_json::from_value(json!({ "offset": 0, "length": 0})).unwrap();
660    assert_eq!(span, SourceSpan::from(0));
661}
662
663/**
664"Raw" type for the byte offset from the beginning of a [`SourceCode`].
665*/
666pub type ByteOffset = usize;
667
668/**
669Newtype that represents the [`ByteOffset`] from the beginning of a [`SourceCode`]
670*/
671#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
672#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
673pub struct SourceOffset(ByteOffset);
674
675impl SourceOffset {
676    /// Actual byte offset.
677    pub const fn offset(&self) -> ByteOffset {
678        self.0
679    }
680
681    /// Little utility to help convert 1-based line/column locations into
682    /// miette-compatible Spans
683    ///
684    /// This function is infallible: Giving an out-of-range line/column pair
685    /// will return the offset of the last byte in the source.
686    pub fn from_location(source: impl AsRef<str>, loc_line: usize, loc_col: usize) -> Self {
687        let mut line = 0usize;
688        let mut col = 0usize;
689        let mut offset = 0usize;
690        for char in source.as_ref().chars() {
691            if line + 1 >= loc_line && col + 1 >= loc_col {
692                break;
693            }
694            if char == '\n' {
695                col = 0;
696                line += 1;
697            } else {
698                col += 1;
699            }
700            offset += char.len_utf8();
701        }
702
703        SourceOffset(offset)
704    }
705
706    /// Returns an offset for the _file_ location of wherever this function is
707    /// called. If you want to get _that_ caller's location, mark this
708    /// function's caller with `#[track_caller]` (and so on and so forth).
709    ///
710    /// Returns both the filename that was given and the offset of the caller
711    /// as a [`SourceOffset`].
712    ///
713    /// Keep in mind that this fill only work if the file your Rust source
714    /// file was compiled from is actually available at that location. If
715    /// you're shipping binaries for your application, you'll want to ignore
716    /// the Err case or otherwise report it.
717    #[track_caller]
718    pub fn from_current_location() -> Result<(String, Self), MietteError> {
719        let loc = Location::caller();
720        Ok((
721            loc.file().into(),
722            fs::read_to_string(loc.file())
723                .map(|txt| Self::from_location(txt, loc.line() as usize, loc.column() as usize))?,
724        ))
725    }
726}
727
728impl From<ByteOffset> for SourceOffset {
729    fn from(bytes: ByteOffset) -> Self {
730        SourceOffset(bytes)
731    }
732}
733
734#[test]
735fn test_source_offset_from_location() {
736    let source = "f\n\noo\r\nbar";
737
738    assert_eq!(SourceOffset::from_location(source, 1, 1).offset(), 0);
739    assert_eq!(SourceOffset::from_location(source, 1, 2).offset(), 1);
740    assert_eq!(SourceOffset::from_location(source, 2, 1).offset(), 2);
741    assert_eq!(SourceOffset::from_location(source, 3, 1).offset(), 3);
742    assert_eq!(SourceOffset::from_location(source, 3, 2).offset(), 4);
743    assert_eq!(SourceOffset::from_location(source, 3, 3).offset(), 5);
744    assert_eq!(SourceOffset::from_location(source, 3, 4).offset(), 6);
745    assert_eq!(SourceOffset::from_location(source, 4, 1).offset(), 7);
746    assert_eq!(SourceOffset::from_location(source, 4, 2).offset(), 8);
747    assert_eq!(SourceOffset::from_location(source, 4, 3).offset(), 9);
748    assert_eq!(SourceOffset::from_location(source, 4, 4).offset(), 10);
749
750    // Out-of-range
751    assert_eq!(
752        SourceOffset::from_location(source, 5, 1).offset(),
753        source.len()
754    );
755}
756
757#[cfg(feature = "serde")]
758#[test]
759fn test_serialize_source_offset() {
760    use serde_json::json;
761
762    assert_eq!(json!(SourceOffset::from(0)), 0);
763}
764
765#[cfg(feature = "serde")]
766#[test]
767fn test_deserialize_source_offset() {
768    let offset: SourceOffset = serde_json::from_str("0").unwrap();
769    assert_eq!(offset, SourceOffset::from(0));
770}