#诊断 #错误 #thiserror #miette #derive-debug #derive-error #line-column

no-std miden-miette

为非编译器黑客的我们设计的花哨诊断报告库和协议

2个稳定版本

7.1.1 2024年8月7日

#251Rust模式

Download history 261/week @ 2024-08-04 577/week @ 2024-08-11

838 每月下载量
5 个crate(2直接)中使用

Apache-2.0

305KB
5.5K SLoC

这是对 miette 的修改版本,支持 no-std

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(())
}

... 基于 diagnostics 的错误源。

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

虽然这对现有的 reporters 没有影响,因为它们现在不使用这些信息,但可能希望使用这些信息的 API 将无法访问这些信息。

如果您认为这些信息对用户来说很重要,您可以使用 #[diagnostic_source]#[source] 一起使用。请注意,您可能不需要使用 both

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:这两个极具影响力的错误处理库推动了应用级错误处理和错误报告的体验。 mietteReport 类型是它们 Report 类型的非常粗略版本尝试。
  • thiserror 用于定义库级别的错误标准,并为 miette 的 derive 宏提供灵感。
  • rustc@estebank 为其在编译器诊断方面的前沿工作。
  • ariadne 推进了这些诊断看起来有多“漂亮”!

许可协议

miette 在 Rust 社区中以 Apache license 2.0 许可证发布。

它还包括从 eyre 和一些来自 thiserror(也处于 Apache License 许可下)的代码。一些代码来自 ariadne,该代码采用 MIT 许可。

依赖项

~1–15MB
~145K SLoC