#error #diagnostics #thiserror #line-column #derive-error #derive-debug #snippets

miette

为普通人类(而非编译器黑客)提供的花哨的诊断报告库和协议

61 个版本 (稳定版)

7.2.0 2024年3月7日
6.0.1 2024年2月4日
5.10.0 2023年7月16日
5.6.0 2023年3月14日
0.13.0 2021年8月21日

#2Rust 模式

Download history 155128/week @ 2024-04-27 168684/week @ 2024-05-04 184792/week @ 2024-05-11 159639/week @ 2024-05-18 156769/week @ 2024-05-25 237537/week @ 2024-06-01 226344/week @ 2024-06-08 227845/week @ 2024-06-15 235499/week @ 2024-06-22 212374/week @ 2024-06-29 233804/week @ 2024-07-06 232147/week @ 2024-07-13 231384/week @ 2024-07-20 252107/week @ 2024-07-27 234140/week @ 2024-08-03 184003/week @ 2024-08-10

每月下载量 945,013
1,146 个crate中使用了 (直接使用 448)

Apache-2.0

300KB
5.5K SLoC

miette

你在运行 miette 吗?你像运行软件一样运行她的代码?哦。哦!为程序员准备的错误代码!为千行代码准备的错误代码!

关于

miette 是一个 Rust 的诊断库。它包含一系列特质/协议,允许您钩入其错误报告功能,甚至编写自己的错误报告!它允许您定义可以打印出如下所示的错误类型(或您喜欢的任何格式!)

Hi! miette also includes a screen-reader-oriented diagnostic printer that's enabled in various situations, such as when you use NO_COLOR or CLICOLOR settings, or on CI. This behavior is also fully configurable and customizable. For example, this is what this particular diagnostic will look like when the narrated printer is enabled:
\
Error: Received some bad JSON from the source. Unable to parse.
Caused by: missing field `foo` at line 1 column 1700
\
Begin snippet for https://api.nuget.org/v3/registration5-gz-semver2/json.net/index.json starting
at line 1, column 1659
\
snippet line 1: gs":["json"],"title":"","version":"1.0.0"},"packageContent":"https://api.nuget.o
highlight starting at line 1, column 1699: last parsing location
\
diagnostic help: This is a bug. It might be in ruget, or it might be in the
source you're using, but it's definitely a bug and should be reported.
diagnostic error code: ruget::api::bad_json

注意:您必须启用 "fancy" crate 功能以获取像上面截图那样的花哨报告输出。 您应该在顶层 crate 中这样做,因为花哨功能会引入一些依赖项,这些依赖项可能不适用于库等。

目录

功能

  • 泛型 Diagnostic 协议,兼容(并依赖于)std::error::Error
  • 每个 Diagnostic 都有独特的错误代码。
  • 自定义链接以获取错误代码的更多详细信息。
  • 用于定义诊断元数据的超方便的 derive 宏。
  • anyhow/eyre 类型、ResultReport 以及用于 anyhow!/eyre! 宏的 miette! 宏的替代品。
  • 对任意 SourceCode 的片段数据进行泛型支持,默认支持 String

miette 包还捆绑了一个默认的 ReportHandler,具有以下功能:

  • 使用 ANSI/Unicode 文本进行花哨的图形诊断输出
  • 单行和多行高亮支持
  • 屏幕阅读器/盲文支持,通过 NO_COLOR 和其他启发式方法进行控制。
  • 完全可定制的图形主题(或完全覆盖打印器)。
  • 原因链打印
  • 将诊断代码转换为支持终端中的链接。

安装

$ cargo add miette

如果您想在所有这些屏幕截图中使用花哨的打印器

$ cargo add miette --features fancy

示例

/*
You can derive a `Diagnostic` from any `std::error::Error` type.

`thiserror` is a great way to define them, and plays nicely with `miette`!
*/
use miette::{Diagnostic, 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 {
    // The Source that we're gonna be printing snippets out of.
    // This can be a String if you don't have or care about file names.
    #[source_code]
    src: NamedSource<String>,
    // Snippets and highlights can be included in the diagnostic!
    #[label("This bit here")]
    bad_bit: SourceSpan,
}

/*
Now let's define a function!

Use this `Result` type (or its expanded version) as the return type
throughout your app (but NOT your libraries! Those should always return
concrete types!).
*/
use miette::{NamedSource, Result};
fn this_fails() -> Result<()> {
    // You can use plain strings as a `Source`, or anything that implements
    // the one-method `Source` trait.
    let src = "source\n  text\n    here".to_string();
    let len = src.len();

    Err(MyBad {
        src: NamedSource::new("bad_file.rs", src),
        bad_bit: (9, 4).into(),
    })?;

    Ok(())
}

/*
Now to get everything printed nicely, just return a `Result<()>`
and you're all set!

Note: You can swap out the default reporter for a custom one using
`miette::set_hook()`
*/
fn pretend_this_is_main() -> Result<()> {
    // kaboom~
    this_fails()?;

    Ok(())
}

运行此程序后,您将获得以下输出


Narratable printout:
\
Error: Types mismatched for operation.
Diagnostic severity: error
Begin snippet starting at line 1, column 1
\
snippet line 1: 3 + "5"
label starting at line 1, column 1: int
label starting at line 1, column 1: doesn't support these values.
label starting at line 1, column 1: string
diagnostic help: Change int or string to be the right types and try again.
diagnostic code: nu::parser::unsupported_operation
For more details, see https://docs.rs/nu-parser/0.1.0/nu-parser/enum.ParseError.html#variant.UnsupportedOperation

使用

... 在库中

miette 与库使用完全兼容。不知道或不想使用 miette 功能的消费者可以安全地将其错误类型作为常规 std::error::Error 使用。

我们强烈建议使用类似 thiserror 的工具来为您的库定义独特的错误类型和错误包装器。

虽然 miettethiserror 集成良好,但它不是必需的。如果您不想使用 Diagnostic derive 宏,您可以像处理 std::error::Error 一样直接实现该特性。

// 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),
}

#[derive(Error, Diagnostic, Debug)]
#[error("another error")]
pub struct AnotherError {
   #[label("here")]
   pub at: SourceSpan
}

然后,从所有您的易出错公共 API 返回此错误类型。将任何“外部”错误类型包装在您的错误 enum 中,而不是在库中使用像 Report 这样的东西,是一种最佳实践。

... 在应用程序代码中

应用程序代码通常与库的工作方式略有不同。您不一定需要或关心为来自外部库和工具的错误定义专用错误包装器。

对于这种情况,miette 包含两个工具:ReportIntoDiagnostic。它们协同工作,使将常规 std::error::Error 转换为 Diagnostic 变得非常容易。此外,还有一个可以用来更简洁的 Result 类型别名。

处理非 Diagnostic 类型时,您需要使用 .into_diagnostic() 方法将它们转换为诊断

// my_app/lib/my_internal_file.rs
use miette::{IntoDiagnostic, Result};
use semver::Version;

pub fn some_tool() -> Result<Version> {
    Ok("1.2.x".parse().into_diagnostic()?)
}

miette 还包括类似 anyhow/eyre 风格的 Context/WrapErr 特性,您可以将它们导入以向您的 Diagnostic 添加自定义上下文消息,尽管您仍然需要使用 .into_diagnostic() 来使用它

// my_app/lib/my_internal_file.rs
use miette::{IntoDiagnostic, Result, WrapErr};
use semver::Version;

pub fn some_tool() -> Result<Version> {
    Ok("1.2.x"
        .parse()
        .into_diagnostic()
        .wrap_err("Parsing this tool's semver version failed.")?)
}

要构建自己的简单自定义错误,请使用 [miette!] 宏

// my_app/lib/my_internal_file.rs
use miette::{miette, IntoDiagnostic, Result, WrapErr};
use semver::Version;

pub fn some_tool() -> Result<Version> {
    let version = "1.2.x";
    Ok(version
        .parse()
        .map_err(|_| miette!("Invalid version {}", version))?)
}

还有类似的 [bail!] 和 [ensure!] 宏。

... 在 main()

main() 就像您应用程序内部的其他任何部分一样。使用 Result 作为您的返回值,它将自动以格式化方式打印您的诊断信息。

注意:您必须启用 "fancy" crate 功能以获取类似于此处截图中的花哨报告输出。**您应该只在您的顶级 crate 中这样做,因为花哨功能会引入许多库可能不需要的依赖项。

use miette::{IntoDiagnostic, Result};
use semver::Version;

fn pretend_this_is_main() -> Result<()> {
    let version: Version = "1.2.x".parse().into_diagnostic()?;
    println!("{}", version);
    Ok(())
}

请注意:要获得具有所有漂亮颜色和箭头的花哨诊断渲染,您应该启用 fancy 功能安装 miette

miette = { version = "X.Y.Z", features = ["fancy"] }

另一种显示诊断的方法是使用调试格式化程序打印它们。实际上,这就是从 main 返回诊断所做的事情。要自己这样做,您可以编写以下内容

use miette::{IntoDiagnostic, Result};
use semver::Version;

fn just_a_random_function() {
    let version_result: Result<Version> = "1.2.x".parse().into_diagnostic();
    match version_result {
        Err(e) => println!("{:?}", e),
        Ok(version) => println!("{}", version),
    }
}

... 诊断代码 URL

miette 支持为单个诊断提供 URL。此 URL 将在支持的终端中显示为实际链接,如下所示

 Example showing the graphical report printer for miette
pretty-printing an error code. The code is underlined and followed by text
saying to 'click here'. A hover tooltip shows a full-fledged URL that can be
Ctrl+Clicked to open in a browser.
\
This feature is also available in the narratable printer. It will add a line
after printing the error code showing a plain URL that you can visit.

要使用此功能,您可以将 url() 子参数添加到您的 #[diagnostic] 属性

use miette::Diagnostic;
use thiserror::Error;

#[derive(Error, Diagnostic, Debug)]
#[error("kaboom")]
#[diagnostic(
    code(my_app::my_error),
    // You can do formatting!
    url("https://my_website.com/error_codes#{}", self.code().unwrap())
)]
struct MyErr;

此外,如果您正在开发库,并且您的错误类型是从 crate 的顶级导出的,则可以使用特殊的 url(docsrs) 选项而不是手动构造 URL。这将自动在 docs.rs 上创建一个链接到该诊断的链接,因此人们可以直接转到关于此诊断的(非常高质量和详细的!)文档

use miette::Diagnostic;
use thiserror::Error;

#[derive(Error, Diagnostic, Debug)]
#[diagnostic(
    code(my_app::my_error),
    // Will link users to https://docs.rs/my_crate/0.0.0/my_crate/struct.MyErr.html
    url(docsrs)
)]
#[error("kaboom")]
struct MyErr;

... 代码片段

除了其通用的错误处理和报告功能外,miette 还包括添加错误范围/注释/标签到您输出的功能。当错误与语法相关时,这非常有用,但您甚至可以使用它来打印您自己的源代码部分!

为了实现这一点,miette 定义了自己的轻量级 SourceSpan 类型。这是一个关联 SourceCode 的基本字节偏移量和长度,并且与后者一起,为 miette 提供了所有打印某些片段所需的信息!您还可以使用您自己的 Into<SourceSpan> 类型作为标签范围。

定义此类错误的简单方法是使用 derive(Diagnostic)

use miette::{Diagnostic, SourceSpan};
use thiserror::Error;

#[derive(Diagnostic, Debug, Error)]
#[error("oops")]
#[diagnostic(code(my_lib::random_error))]
pub struct MyErrorType {
    // The `Source` that miette will use.
    #[source_code]
    src: String,

    // This will underline/mark the specific code inside the larger
    // snippet context.
    #[label = "This is the highlight"]
    err_span: SourceSpan,

    // You can add as many labels as you want.
    // They'll be rendered sequentially.
    #[label("This is bad")]
    snip2: (usize, usize), // `(usize, usize)` is `Into<SourceSpan>`!

    // Snippets can be optional, by using Option:
    #[label("some text")]
    snip3: Option<SourceSpan>,

    // with or without label text
    #[label]
    snip4: Option<SourceSpan>,
}

... 帮助文本

miette 为您提供了两种为错误提供帮助文本的方法

第一种是应用于结构体或枚举变体的 #[help()] 格式属性

use miette::Diagnostic;
use thiserror::Error;

#[derive(Debug, Diagnostic, Error)]
#[error("welp")]
#[diagnostic(help("try doing this instead"))]
struct Foo;

另一种是通过程序性地将帮助文本作为字段提供给您诊断

use miette::Diagnostic;
use thiserror::Error;

#[derive(Debug, Diagnostic, Error)]
#[error("welp")]
#[diagnostic()]
struct Foo {
    #[help]
    advice: Option<String>, // Can also just be `String`
}

let err = Foo {
    advice: Some("try doing this instead".to_string()),
};

... 严重级别

miette 提供了一种设置诊断严重级别的方法。

use miette::Diagnostic;
use thiserror::Error;

#[derive(Debug, Diagnostic, Error)]
#[error("welp")]
#[diagnostic(severity(Warning))]
struct Foo;

miette 支持将多个错误收集到一个诊断中,并一起优雅地打印它们。

要这样做,请在您的 Diagnostic 类型中的任何 IntoIter 字段上使用 #[related] 标签

use miette::Diagnostic;
use thiserror::Error;

#[derive(Debug, Error, Diagnostic)]
#[error("oops")]
struct MyError {
    #[related]
    others: Vec<MyError>,
}

... 延迟源代码

有时在错误消息中添加源代码是有意义的。一个选项是使用 with_source_code() 方法来实现

use miette::{Diagnostic, SourceSpan};
use thiserror::Error;

#[derive(Diagnostic, Debug, Error)]
#[error("oops")]
#[diagnostic()]
pub struct MyErrorType {
    // Note: label but no source code
    #[label]
    err_span: SourceSpan,
}

fn do_something() -> miette::Result<()> {
    // This function emits actual error with label
    return Err(MyErrorType {
        err_span: (7..11).into(),
    })?;
}

fn main() -> miette::Result<()> {
    do_something().map_err(|error| {
        // And this code provides the source code for inner error
        error.with_source_code(String::from("source code"))
    })
}

此外,源代码也可以由包装类型提供。这对于与 related 结合使用特别有用,当需要同时发出多个错误时

use miette::{Diagnostic, Report, SourceSpan};
use thiserror::Error;

#[derive(Diagnostic, Debug, Error)]
#[error("oops")]
#[diagnostic()]
pub struct InnerError {
    // Note: label but no source code
    #[label]
    err_span: SourceSpan,
}

#[derive(Diagnostic, Debug, Error)]
#[error("oops: multiple errors")]
#[diagnostic()]
pub struct MultiError {
    // Note source code by no labels
    #[source_code]
    source_code: String,
    // The source code above is used for these errors
    #[related]
    related: Vec<InnerError>,
}

fn do_something() -> Result<(), Vec<InnerError>> {
    Err(vec![
        InnerError {
            err_span: (0..6).into(),
        },
        InnerError {
            err_span: (7..11).into(),
        },
    ])
}

fn main() -> miette::Result<()> {
    do_something().map_err(|err_list| MultiError {
        source_code: "source code".into(),
        related: err_list,
    })?;
    Ok(())
}

...基于诊断的错误源。

当在字段上使用 #[source] 属性时,通常来自 thiserror,并实现了 std::error::Error::source 方法。这在许多情况下都适用,但它是有损的:如果诊断的来源是另一个诊断,那么来源将被简单地视为一个 std::error::Error

尽管这对现有的 reporters 没有影响,因为它们现在不会使用这些信息,但可能需要这些信息的 API 将无法访问这些信息。

如果您认为这些信息对用户来说很重要,您可以在 #[source] 旁边使用 #[diagnostic_source]。请注意,您可能不需要同时使用 两者

use miette::Diagnostic;
use thiserror::Error;

#[derive(Debug, Diagnostic, Error)]
#[error("MyError")]
struct MyError {
    #[source]
    #[diagnostic_source]
    the_cause: OtherError,
}

#[derive(Debug, Diagnostic, Error)]
#[error("OtherError")]
struct OtherError;

... 处理器选项

MietteHandler 是默认处理器,并且非常可定制。在大多数情况下,您可以使用 MietteHandlerOpts 来调整其行为,而不是回退到您自己的自定义处理器。

用法如下

miette::set_hook(Box::new(|_| {
    Box::new(
        miette::MietteHandlerOpts::new()
            .terminal_links(true)
            .unicode(false)
            .context_lines(3)
            .tab_width(4)
            .break_words(true)
            .build(),
    )
}))

有关您可以定制的详细信息,请参阅 MietteHandlerOpts 的文档!

... 动态诊断

如果您...

  • ...不知道所有可能的错误
  • ...如果需要序列化和反序列化错误,则可能希望使用 miette!diagnostic! 宏或直接使用 MietteDiagnostic 来动态创建诊断。

let source = "2 + 2 * 2 = 8".to_string();
let report = miette!(
  labels = vec![
      LabeledSpan::at(12..13, "this should be 6"),
  ],
  help = "'*' has greater precedence than '+'",
  "Wrong answer"
).with_source_code(source);
println!("{:?}", report)

... 语法高亮

miette 可以配置以突出显示源代码片段中的语法。

要使用内置的突出显示功能,必须启用 syntect-highlighter 包特性。当此特性启用时,miette 将自动使用 syntect 包来突出显示你的 Diagnostic#[source_code] 字段。

使用 syntect 进行语法检测是通过检查 SpanContents 特性上的2个方法来处理的。

  • language() - 提供语言的名称作为字符串。例如,"Rust" 将指示 Rust 语法高亮。您可以通过 with_language 方法设置由 NamedSource 产生的 SpanContents 的语言。
  • name() - 如果没有显式设置语言,则假设名称包含文件名或文件路径。高亮器将在名称的末尾检查文件扩展名并尝试从中猜测语法。

如果您想使用自定义高亮器,可以通过调用 with_syntax_highlighting 方法向 MietteHandlerOpts 提供自定义的 Highlighter 特性实现。有关更多详细信息,请参阅 highlighters 模块文档。

... 标签集合

当标签数量未知时,您可以使用一组 SourceSpan(或任何可以转换为 SourceSpan 的类型)。为此,将 collection 参数添加到 label 并使用任何可以迭代的类型作为字段。

#[derive(Debug, Diagnostic, Error)]
#[error("oops!")]
struct MyError {
    #[label("main issue")]
    primary_span: SourceSpan,

    #[label(collection, "related to this")]
    other_spans: Vec<Range<usize>>,
}

let report: miette::Report = MyError {
    primary_span: (6, 9).into(),
    other_spans: vec![19..26, 30..41],
}.into();

println!("{:?}", report.with_source_code("About something or another or yet another ...".to_string()));

如果想要为不同的标签使用不同的文本,则集合也可以是 LabeledSpan。没有文本的标签将使用 label 属性中的文本。

#[derive(Debug, Diagnostic, Error)]
#[error("oops!")]
struct MyError {
    #[label("main issue")]
    primary_span: SourceSpan,

    #[label(collection, "related to this")]
    other_spans: Vec<LabeledSpan>, // LabeledSpan
}

let report: miette::Report = MyError {
    primary_span: (6, 9).into(),
    other_spans: vec![
        LabeledSpan::new(None, 19, 7), // Use default text `related to this`
        LabeledSpan::new(Some("and also this".to_string()), 30, 11), // Use specific text
    ],
}.into();

println!("{:?}", report.with_source_code("About something or another or yet another ...".to_string()));

MSRV

此包需要 rustc 1.70.0 或更高版本。

鸣谢

miette 并非空穴来风。它对各种其他项目和它们的作者给予了巨大的信用。

  • anyhowcolor-eyre:这两个极具影响力的错误处理库推动了应用级错误处理和错误报告的体验。《miette》的 Report 类型是对它们 Report 类型的一个非常粗略的尝试。
  • 用于设置库级错误定义标准的 thiserror,以及作为 miette 的 derive 宏的灵感来源。
  • rustc@estebank 对于他们在编译器诊断方面的一流工作。
  • 为使诊断看起来更美观,ariadne 发挥了推动作用!

许可证

miette 在 Apache 许可证 2.0 下发布给 Rust 社区。

它还包括从 eyrethiserror(同样在 Apache 许可证下)以及从 ariadne(MIT 许可证)中获取的代码。一些代码来自 ariadne

依赖项

~0.6–11MB
~118K SLoC