miette

repository·main·Indexed 25 days ago

https://github.com/zkat/miette

A diagnostic reporting library and protocol for Rust designed to provide user-friendly error reports. It supports source code snippets, syntax highlighting via the syntect-highlighter feature, error codes with documentation links, and customizable graphical output. The library provides the `Diagnostic` trait and a derive macro to attach metadata like severity levels, help text, and labels to error types.

Tokens
12.7K
Snippets
25
Records
60
Agent score
82%

What's inside miette

  1. How multiple related errors work

    main

    miette allows you to group multiple errors into a single diagnostic report. By adding the #[related] attribute to a field that implements IntoIter (such as a Vec), miette will print all related errors together in a single, organized output.

    #[derive(Debug, Error, Diagnostic)]
    #[error("oops")]
    struct MyError {
        #[related]
        others: Vec<MyError>,
    }
  2. Add snippets and labels to diagnostics

    main

    To provide visual context for errors, use the #[source_code] attribute to attach a SourceCode (like a String or NamedSource) to your diagnostic. You can then use #[label] to highlight specific parts of that source using a SourceSpan.

    Key features:

    • Primary Labels: Use #[label(primary, "...")] to designate the main issue (only one allowed per diagnostic).
    • Collections of Labels: Use #[label(collection, "...")] on a field containing an iterator (like Vec<SourceSpan> or Vec<LabeledSpan>) to render multiple highlights.
    • Custom Text: Labels can have specific text, or use the default text provided in the attribute.
    use miette::{Diagnostic, SourceSpan};
    use thiserror::Error;
    
    #[derive(Diagnostic, Debug, Error)]
    #[error("oops!")]
    struct MyError {
        #[source_code]
        src: String,
    
        #[label(primary, "main issue")]
        primary_span: SourceSpan,
    
        #[label("This is the highlight")]
        err_span: SourceSpan,
    
        #[label("This is bad")]
        snip2: (usize, usize), // (usize, usize) implements Into<SourceSpan>
    
        #[label(collection, "related to this")]
        other_spans: Vec<SourceSpan>,
    }
  3. Handle errors in application code

    main

    In application code, you can use miette::Result for a more terse return type. To convert standard errors into diagnostics, use the .into_diagnostic() method. You can also add ad-hoc context to diagnostics using the WrapErr trait (similar to anyhow). For quick, ad-hoc error creation, use the miette! macro, or the bail! and ensure! macros.

    // my_app/lib/my_internal_file.rs
    use miette::{IntoDiagnostic, Result, WrapErr};
    use semver::Version;
    
    pub fn some_tool() -> Result<Version> {
        "1.2.x"
            .parse()
            .into_diagnostic()
            .wrap_err("Parsing this tool's semver version failed.")
    }
    
    // Using the miette! macro for ad-hoc errors
    pub fn some_tool_adhoc() -> Result<Version> {
        let version = "1.2.x";
        version
            .parse()
            .map_err(|_| miette!("Invalid version {}", version))
    }
  4. Define custom diagnostics in libraries

    main

    When writing a library, it is best practice to define concrete error types using thiserror and implement the miette::Diagnostic trait. This ensures compatibility with std::error::Error for consumers who do not use miette. You can use #[diagnostic] attributes to add metadata like error codes, URLs, and help text. Use #[diagnostic(transparent)] to wrap another diagnostic without losing its labels, or #[diagnostic(forward(field_name))] to forward the diagnostic to a specific field.

    // lib/error.rs
    use miette::{Diagnostic, SourceSpan};
    use thiserror::Error;
    
    #[derive(Error, Diagnostic, Debug)]
    pub enum MyLibError {
        #[error(transparent)]
        #[diagnostic(code(my_lib::io_error))]
        IoError(#[from] std::io::Error),
    
        #[error("Oops it blew up")]
        #[diagnostic(code(my_lib::bad_code))]
        BadThingHappened,
    
        #[error(transparent)]
        // Use `#[diagnostic(transparent)]` to wrap another `Diagnostic`. You won't see labels otherwise
        #[diagnostic(transparent)]
        AnotherError(#[from] AnotherError),
    
        /// Forward the diagnostic to a particular field.
        #[error("other error")]
        #[diagnostic(forward(the_actual_diagnostic))]
        EvenMoreData {
            unrelated_field_1: String,
            unrelated_field_2: usize,
    
            #[source]
            the_actual_diagnostic: AnotherError,
        }
    }
    
    #[derive(Error, Diagnostic, Debug)]
    #[error("another error")]
    pub struct AnotherError {
       #[label("here")]
       pub at: SourceSpan
    }
  5. Install miette

    main

    To add miette to your project, use cargo add. To enable the "fancy" feature, which provides high-quality graphical diagnostic output (colors, Unicode, etc.), you must explicitly enable it. Note that the fancy feature should only be enabled in your top-level crate to avoid pulling in unnecessary dependencies for libraries.

    $ cargo add miette
    
    # To enable fancy report output:
    $ cargo add miette --features fancy
  6. Customize the graphical report theme with GraphicalTheme

    main

    The GraphicalTheme struct is used by GraphicalReportHandler to define how Diagnostic reports are visually rendered. A theme is composed of ThemeCharacters (the symbols used for drawing boxes and arrows) and ThemeStyles (the owo_colors::Style applied to different diagnostic elements).

    You can use predefined themes or construct your own by specifying custom characters and styles.

  7. Add source code snippets and labels to diagnostics

    main

    To show users exactly where an error occurred, include a SourceSpan and a #[source_code] field in your diagnostic struct.

    • SourceSpan: A lightweight type representing a byte offset and length. It can be created from ranges like (0..5).into().
    • #[label]: Highlights a specific part of the source code. You can use primary to indicate the main issue, or collection to provide multiple spans.
    • NamedSource: Used to associate a name (like a filename) with the source string.
    use miette::{Diagnostic, SourceSpan, NamedSource};
    use thiserror::Error;
    
    #[derive(Diagnostic, Debug, Error)]
    #[error("oops")]
    #[diagnostic(code(my_lib::random_error))]
    pub struct MyErrorType {
        #[source_code]
        src: NamedSource<String>,
    
        #[label = "This is the highlight"]
        err_span: SourceSpan,
    
        #[label("This is bad")]
        snip2: (usize, usize), // (start, end) works via Into<SourceSpan>
    }
  8. How to implement a custom syntax highlighter

    main

    To provide custom syntax highlighting for miette diagnostics, you must implement two traits: Highlighter and HighlighterState.

    1. Highlighter: This is the entry point. You implement start_highlighter_state which takes a SpanContents (to detect language or context) and returns a boxed HighlighterState.
    2. HighlighterState: This is a stateful object used during the rendering process. You implement highlight_line, which takes a string slice and returns a Vec<Styled<&str>> containing the text with ANSI escape sequences applied via owo-colors.

    The GraphicalReportHandler uses these to incrementally render source code snippets in diagnostics.

  9. Define custom diagnostics with `#[derive(Diagnostic)]`

    main

    You can define rich, diagnostic-capable error types by combining thiserror with miette's Diagnostic derive macro. This allows you to attach error codes, URLs, help text, and source code snippets to your errors.

    Key attributes for #[diagnostic]:

    • code(CODE): A unique error code.
    • url(URL_TYPE): A link to more info. Use docsrs to automatically link to your crate's documentation on docs.rs.
    • help(TEXT): Provides guidance to the user.
    • severity(LEVEL): Sets the error severity (e.g., Warning).
    • source_code: Marks a field as the source for snippets.
    • label(TEXT): Marks a field as a snippet highlight.
    • transparent: Used to wrap another Diagnostic without showing extra labels.
    • forward(FIELD): Forwards the diagnostic to a specific field.
    use miette::{Diagnostic, NamedSource, SourceSpan};
    use thiserror::Error;
    
    #[derive(Error, Debug, Diagnostic)]
    #[error("oops!")]
    #[diagnostic(
        code(oops::my::bad),
        url(docsrs),
        help("try doing it better next time?")
    )]
    struct MyBad {
        #[source_code]
        src: NamedSource<String>,
        #[label("This bit here")]
        bad_bit: SourceSpan,
    }
  10. How syntax highlighting interacts with color settings

    main

    Syntax highlighting behavior depends on several factors:

    1. Feature Flag: Syntax highlighting is disabled by default unless the syntect-highlighter feature is enabled.
    2. Color Configuration: If MietteHandlerOpts::color(false) is called, syntax highlighting is always disabled. If the terminal does not support color and color is not explicitly enabled, highlighting is disabled.
    3. Precedence:
      • If you provide a custom highlighter via with_syntax_highlighting, it takes precedence over rgb_colors (meaning highlighting will be enabled even if rgb_colors is set to Never).
      • However, the color(bool) setting still takes precedence over the highlighter configuration. If color(false) is set, no highlighting will occur.
  11. How Line and FancySpan relationships are calculated

    main

    The GraphicalReportHandler uses internal logic to determine how FancySpan objects interact with specific lines of text to build the visual report.

    Line Visibility

    For a given Line, the following checks are performed:

    • span_applies: Returns true if the span is visible on this line (either in the gutter or under the text).
    • span_applies_gutter: Returns true if the span should be visible in the gutter. This excludes spans that start and end entirely within the same line.
    • span_flyby: Returns true if the span is a 'flyby'—a multi-line span that covers this line but does not begin or end within it.

    Span Boundaries

    • span_starts: Returns true if the line contains the beginning of the span.
    • span_ends: Returns true if the line contains the end of the span.
    impl Line {
        fn span_line_only(&self, span: &FancySpan) -> bool {
            span.offset() >= self.offset && span.offset() + span.len() <= self.offset + self.length
        }
    
        fn span_applies(&self, span: &FancySpan) -> bool {
            let spanlen = if span.len() == 0 { 1 } else { span.len() };
            (span.offset() >= self.offset && span.offset() < self.offset + self.length)
                || (span.offset() < self.offset && span.offset() + spanlen > self.offset + self.length)
                || (span.offset() + spanlen > self.offset && span.offset() + spanlen <= self.offset + self.length)
        }
    
        fn span_applies_gutter(&self, span: &FancySpan) -> bool {
            let spanlen = if span.len() == 0 { 1 } else { span.len() };
            self.span_applies(span)
                && !(
                    (span.offset() >= self.offset && span.offset() < self.offset + self.length)
                        && (span.offset() + spanlen > self.offset
                            && span.offset() + spanlen <= self.offset + self.length)
                )
        }
    
        fn span_flyby(&self, span: &FancySpan) -> bool {
            span.offset() < self.offset
                && span.offset() + span.len() > self.offset + self.length
        }
    
        fn span_starts(&self, span: &FancySpan) -> bool {
            span.offset() >= self.offset
        }
    
        fn span_ends(&self, span: &FancySpan) -> bool {
            span.offset() + span.len() >= self.offset
                && span.offset() + span.len() <= self.offset + self.length
        }
    }
  12. Enable syntax highlighting in snippets

    main

    To enable automatic syntax highlighting for #[source_code] fields, enable the syntect-highlighter crate feature. miette will use the syntect crate to detect language based on:

    1. The language() method of the SpanContents trait.
    2. The name() method (guessing from file extensions if a name is provided via NamedSource).