Merge branch 'main' into usize

This commit is contained in:
Kat Marchán 2023-11-15 10:43:37 -08:00 committed by GitHub
commit b9da0b6558
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 1598 additions and 312 deletions

View File

@ -10,14 +10,12 @@ jobs:
name: Check fmt & build docs
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: actions/checkout@v4
- name: Install Rust
uses: actions-rs/toolchain@v1
uses: dtolnay/rust-toolchain@master
with:
profile: minimal
toolchain: stable
components: rustfmt
override: true
- name: rustfmt
run: cargo fmt --all -- --check
- name: docs
@ -32,14 +30,15 @@ jobs:
os: [ubuntu-latest, macOS-latest, windows-latest]
steps:
- uses: actions/checkout@v1
- uses: actions/checkout@v4
- name: Install Rust
uses: actions-rs/toolchain@v1
uses: dtolnay/rust-toolchain@master
with:
profile: minimal
toolchain: ${{ matrix.rust }}
components: clippy
override: true
- name: Force older version of is-terminal for MSRV builds
if: matrix.rust == '1.56.0'
run: cargo update -p is-terminal --precise 0.4.7
- name: Clippy
run: cargo clippy --all -- -D warnings
- name: Run tests
@ -54,14 +53,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: actions/checkout@v4
- name: Install Rust
uses: actions-rs/toolchain@v1
uses: dtolnay/rust-toolchain@master
with:
profile: minimal
toolchain: nightly
components: miri,rust-src
override: true
- name: Run tests with miri
env:
MIRIFLAGS: -Zmiri-disable-isolation -Zmiri-strict-provenance
@ -75,13 +72,11 @@ jobs:
os: [ubuntu-latest, macOS-latest, windows-latest]
steps:
- uses: actions/checkout@v1
- uses: actions/checkout@v4
- name: Install Rust
uses: actions-rs/toolchain@v1
uses: dtolnay/rust-toolchain@master
with:
profile: minimal
toolchain: nightly
override: true
- name: Run minimal version build
run: cargo build -Z minimal-versions --all-features

View File

@ -1,5 +1,25 @@
# `miette` Release Changelog
<a name="5.10.0"></a>
## 5.10.0 (2023-07-16)
### Features
* **protocol:** add StdError impl for Box<dyn Diagnostic + Send + Sync> (#273) ([2e3e5c9d](https://github.com/zkat/miette/commit/2e3e5c9d15e234495369e9b47d032644dd5664ad))
<a name="5.9.0"></a>
## 5.9.0 (2023-05-18)
### Features
* **serde:** Add `serde` support (#264) ([c25676cb](https://github.com/zkat/miette/commit/c25676cb1f4266c2607836e6359f15b9cbd8637e))
* **const:** Constify various functions (#263) ([46adb3bc](https://github.com/zkat/miette/commit/46adb3bc6aa6518d82a4187b34c56e287922136f))
* **nested:** Render inner diagnostics (#170) ([aefe3237](https://github.com/zkat/miette/commit/aefe323780bda4e60feb44bb96ee98634ad677ad))
### Bug Fixes
* **misc:** Correct some typos (#255) ([675f3411](https://github.com/zkat/miette/commit/675f3411e33d5fae86d4018c3b72f751a4c4bc2f))
<a name="5.8.0"></a>
## 5.8.0 (2023-04-18)

View File

@ -1,6 +1,6 @@
[package]
name = "miette"
version = "5.8.0"
version = "5.10.0"
authors = ["Kat Marchán <kzm@zkat.tech>"]
description = "Fancy diagnostic reporting library and protocol for us mere mortals who aren't compiler hackers."
categories = ["rust-patterns"]
@ -14,7 +14,7 @@ exclude = ["images/", "tests/", "miette-derive/"]
[dependencies]
thiserror = "1.0.40"
miette-derive = { path = "miette-derive", version = "=5.8.0" }
miette-derive = { path = "miette-derive", version = "=5.10.0", optional = true }
once_cell = "1.8.0"
unicode-width = "0.1.9"
@ -44,7 +44,8 @@ lazy_static = "1.4"
serde_json = "1.0.64"
[features]
default = []
default = ["derive"]
derive = ["miette-derive"]
no-format-args-capture = []
fancy-no-backtrace = [
"owo-colors",

102
README.md
View File

@ -4,7 +4,7 @@
You run miette? You run her code like the software? Oh. Oh! Error code for
coder! Error code for One Thousand Lines!
## About
### About
`miette` is a diagnostic library for Rust. It includes a series of
traits/protocols that allow you to hook into its error reporting facilities,
@ -32,7 +32,7 @@ output like in the screenshots above.** You should only do this in your
toplevel crate, as the fancy feature pulls in a number of dependencies that
libraries and such might not want.
## Table of Contents <!-- omit in toc -->
### Table of Contents <!-- omit in toc -->
- [About](#about)
- [Features](#features)
@ -51,7 +51,7 @@ libraries and such might not want.
- [Acknowledgements](#acknowledgements)
- [License](#license)
## Features
### Features
- Generic [`Diagnostic`] protocol, compatible (and dependent on)
[`std::error::Error`].
@ -76,7 +76,7 @@ the following features:
- Cause chain printing
- Turns diagnostic codes into links in [supported terminals](https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda).
## Installing
### Installing
```sh
$ cargo add miette
@ -88,7 +88,7 @@ If you want to use the fancy printer in all these screenshots:
$ cargo add miette --features fancy
```
## Example
### Example
```rust
/*
@ -96,7 +96,7 @@ 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 miette::{Diagnostic, NamedSource, SourceSpan};
use thiserror::Error;
#[derive(Error, Debug, Diagnostic)]
@ -123,7 +123,7 @@ 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};
use miette::Result;
fn this_fails() -> Result<()> {
// You can use plain strings as a `Source`, or anything that implements
// the one-method `Source` trait.
@ -170,9 +170,9 @@ 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">
## Using
### Using
### ... in libraries
#### ... in libraries
`miette` is _fully compatible_ with library usage. Consumers who don't know
about, or don't want, `miette` features can safely use its error types as
@ -187,7 +187,7 @@ the trait directly, just like with `std::error::Error`.
```rust
// lib/error.rs
use miette::Diagnostic;
use miette::{Diagnostic, SourceSpan};
use thiserror::Error;
#[derive(Error, Diagnostic, Debug)]
@ -199,6 +199,18 @@ pub enum MyLibError {
#[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
}
```
@ -206,7 +218,7 @@ Then, return this error type from all your fallible public APIs. It's a best
practice to wrap any "external" error types in your error `enum` instead of
using something like [`Report`] in a library.
### ... in application code
#### ... in application code
Application code tends to work a little differently than libraries. You
don't always need or care to define dedicated error wrappers for errors
@ -248,8 +260,7 @@ pub fn some_tool() -> Result<Version> {
}
```
To construct your own simple adhoc error use the [`miette!`] macro:
To construct your own simple adhoc error use the [miette!] macro:
```rust
// my_app/lib/my_internal_file.rs
use miette::{miette, IntoDiagnostic, Result, WrapErr};
@ -262,8 +273,9 @@ pub fn some_tool() -> Result<Version> {
.map_err(|_| miette!("Invalid version {}", version))?)
}
```
There are also similar [bail!] and [ensure!] macros.
### ... in `main()`
#### ... in `main()`
`main()` is just like any other part of your application-internal code. Use
`Result` as your return value, and it will pretty-print your diagnostics
@ -293,7 +305,24 @@ enabled:
miette = { version = "X.Y.Z", features = ["fancy"] }
```
### ... diagnostic code URLs
Another way to display a diagnostic is by printing them using the debug formatter.
This is, in fact, what returning diagnostics from main ends up doing.
To do it yourself, you can write the following:
```rust
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),
}
}
```
#### ... diagnostic code URLs
`miette` supports providing a URL for individual diagnostics. This URL will
be displayed as an actual link in supported terminals, like so:
@ -346,7 +375,7 @@ use thiserror::Error;
struct MyErr;
```
### ... snippets
#### ... snippets
Along with its general error handling and reporting features, `miette` also
includes facilities for adding error spans/annotations/labels to your
@ -394,7 +423,7 @@ pub struct MyErrorType {
}
```
#### ... help text
##### ... help text
`miette` provides two facilities for supplying help text for your errors:
The first is the `#[help()]` format attribute that applies to structs or
@ -430,7 +459,7 @@ let err = Foo {
};
```
### ... multiple related errors
#### ... multiple related errors
`miette` supports collecting multiple errors into a single diagnostic, and
printing them all together nicely.
@ -450,7 +479,7 @@ struct MyError {
}
```
### ... delayed source code
#### ... delayed source code
Sometimes it makes sense to add source code to the error message later.
One option is to use [`with_source_code()`](Report::with_source_code)
@ -533,7 +562,7 @@ fn main() -> miette::Result<()> {
}
```
### ... Diagnostic-based error sources.
#### ... Diagnostic-based error sources.
When one uses the `#[source]` attribute on a field, that usually comes
from `thiserror`, and implements a method for
@ -566,7 +595,7 @@ struct MyError {
struct OtherError;
```
### ... handler options
#### ... handler options
[`MietteHandler`] is the default handler, and is very customizable. In
most cases, you can simply use [`MietteHandlerOpts`] to tweak its behavior
@ -585,12 +614,13 @@ miette::set_hook(Box::new(|_| {
.build(),
)
}))
```
See the docs for [`MietteHandlerOpts`] for more details on what you can
customize!
### ... dynamic diagnostics
#### ... dynamic diagnostics
If you...
- ...don't know all the possible errors upfront
@ -599,9 +629,10 @@ then you may want to use [`miette!`], [`diagnostic!`] macros or
[`MietteDiagnostic`] directly to create diagnostic on the fly.
```rust
let source = "2 + 2 * 2 = 8".to_string();
let report = miette!(
labels = vec[
labels = vec![
LabeledSpan::at(12..13, "this should be 6"),
],
help = "'*' has greater precedence than '+'",
@ -610,26 +641,25 @@ let report = miette!(
println!("{:?}", report)
```
## Acknowledgements
### Acknowledgements
`miette` was not developed in a void. It owes enormous credit to various
other projects and their authors:
- [`anyhow`](http://crates.io/crates/anyhow) and
[`color-eyre`](https://crates.io/crates/color-eyre): these two
enormously influential error handling libraries have pushed forward the
experience of application-level error handling and error reporting.
`miette`'s `Report` type is an attempt at a very very rough version of
their `Report` types.
- [`thiserror`](https://crates.io/crates/thiserror) for setting the
standard for library-level error definitions, and for being the
inspiration behind `miette`'s derive macro.
- [`anyhow`](http://crates.io/crates/anyhow) and [`color-eyre`](https://crates.io/crates/color-eyre):
these two enormously influential error handling libraries have pushed
forward the experience of application-level error handling and error
reporting. `miette`'s `Report` type is an attempt at a very very rough
version of their `Report` types.
- [`thiserror`](https://crates.io/crates/thiserror) for setting the standard
for library-level error definitions, and for being the inspiration behind
`miette`'s derive macro.
- `rustc` and [@estebank](https://github.com/estebank) for their
state-of-the-art work in compiler diagnostics.
- [`ariadne`](https://crates.io/crates/ariadne) for pushing forward how
_pretty_ these diagnostics can really look!
## License
### License
`miette` is released to the Rust community under the [Apache license
2.0](./LICENSE).
@ -648,7 +678,7 @@ under the Apache License. Some code is taken from
[`MietteHandler`]: https://docs.rs/miette/latest/miette/struct.MietteHandler.html
[`MietteDiagnostic`]: https://docs.rs/miette/latest/miette/struct.MietteDiagnostic.html
[`Report`]: https://docs.rs/miette/latest/miette/struct.Report.html
[`ReportHandler`]: https://docs.rs/miette/latest/miette/struct.ReportHandler.html
[`ReportHandler`]: https://docs.rs/miette/latest/miette/trait.ReportHandler.html
[`Result`]: https://docs.rs/miette/latest/miette/type.Result.html
[`SourceCode`]: https://docs.rs/miette/latest/miette/struct.SourceCode.html
[`SourceCode`]: https://docs.rs/miette/latest/miette/trait.SourceCode.html
[`SourceSpan`]: https://docs.rs/miette/latest/miette/struct.SourceSpan.html

View File

@ -12,7 +12,7 @@
[`MietteHandler`]: https://docs.rs/miette/latest/miette/struct.MietteHandler.html
[`MietteDiagnostic`]: https://docs.rs/miette/latest/miette/struct.MietteDiagnostic.html
[`Report`]: https://docs.rs/miette/latest/miette/struct.Report.html
[`ReportHandler`]: https://docs.rs/miette/latest/miette/struct.ReportHandler.html
[`ReportHandler`]: https://docs.rs/miette/latest/miette/trait.ReportHandler.html
[`Result`]: https://docs.rs/miette/latest/miette/type.Result.html
[`SourceCode`]: https://docs.rs/miette/latest/miette/struct.SourceCode.html
[`SourceCode`]: https://docs.rs/miette/latest/miette/trait.SourceCode.html
[`SourceSpan`]: https://docs.rs/miette/latest/miette/struct.SourceSpan.html

View File

@ -1,6 +1,6 @@
[package]
name = "miette-derive"
version = "5.8.0"
version = "5.10.0"
authors = ["Kat Marchán <kzm@zkat.tech>"]
edition = "2018"
license = "Apache-2.0"
@ -11,6 +11,6 @@ repository = "https://github.com/zkat/miette"
proc-macro = true
[dependencies]
proc-macro2 = "1.0"
proc-macro2 = "1.0.60"
quote = "1.0"
syn = "2.0.11"

View File

@ -20,10 +20,12 @@ struct Label {
label: Option<Display>,
ty: syn::Type,
span: syn::Member,
primary: bool,
}
struct LabelAttr {
label: Option<Display>,
primary: bool,
}
impl Parse for LabelAttr {
@ -40,10 +42,22 @@ impl Parse for LabelAttr {
}
});
let la = input.lookahead1();
let label = if la.peek(syn::token::Paren) {
// #[label("{}", x)]
let (primary, label) = if la.peek(syn::token::Paren) {
// #[label(primary?, "{}", x)]
let content;
parenthesized!(content in input);
let primary = if content.peek(syn::Ident) {
let ident: syn::Ident = content.parse()?;
if ident != "primary" {
return Err(syn::Error::new(input.span(), "Invalid argument to label() attribute. The argument must be a literal string or the keyword `primary`."));
}
let _ = content.parse::<Token![,]>();
true
} else {
false
};
if content.peek(syn::LitStr) {
let fmt = content.parse()?;
let args = if content.is_empty() {
@ -56,22 +70,27 @@ impl Parse for LabelAttr {
args,
has_bonus_display: false,
};
Some(display)
(primary, Some(display))
} else if !primary {
return Err(syn::Error::new(input.span(), "Invalid argument to label() attribute. The argument must be a literal string or the keyword `primary`."));
} else {
return Err(syn::Error::new(input.span(), "Invalid argument to label() attribute. The first argument must be a literal string."));
(primary, None)
}
} else if la.peek(Token![=]) {
// #[label = "blabla"]
input.parse::<Token![=]>()?;
Some(Display {
fmt: input.parse()?,
args: TokenStream::new(),
has_bonus_display: false,
})
(
false,
Some(Display {
fmt: input.parse()?,
args: TokenStream::new(),
has_bonus_display: false,
}),
)
} else {
None
(false, None)
};
Ok(LabelAttr { label })
Ok(LabelAttr { label, primary })
}
}
@ -100,12 +119,21 @@ impl Labels {
})
};
use quote::ToTokens;
let LabelAttr { label } =
let LabelAttr { label, primary } =
syn::parse2::<LabelAttr>(attr.meta.to_token_stream())?;
if primary && labels.iter().any(|l: &Label| l.primary) {
return Err(syn::Error::new(
field.span(),
"Cannot have more than one primary label.",
));
}
labels.push(Label {
label,
span,
ty: field.ty.clone(),
primary,
});
}
}
@ -120,13 +148,23 @@ impl Labels {
pub(crate) fn gen_struct(&self, fields: &syn::Fields) -> Option<TokenStream> {
let (display_pat, display_members) = display_pat_members(fields);
let labels = self.0.iter().map(|highlight| {
let Label { span, label, ty } = highlight;
let Label {
span,
label,
ty,
primary,
} = highlight;
let var = quote! { __miette_internal_var };
let ctor = if *primary {
quote! { miette::LabeledSpan::new_primary_with_span }
} else {
quote! { miette::LabeledSpan::new_with_span }
};
if let Some(display) = label {
let (fmt, args) = display.expand_shorthand_cloned(&display_members);
quote! {
miette::macro_helpers::OptionalWrapper::<#ty>::new().to_option(&self.#span)
.map(|#var| miette::LabeledSpan::new_with_span(
.map(|#var| #ctor(
std::option::Option::Some(format!(#fmt #args)),
#var.clone(),
))
@ -134,7 +172,7 @@ impl Labels {
} else {
quote! {
miette::macro_helpers::OptionalWrapper::<#ty>::new().to_option(&self.#span)
.map(|#var| miette::LabeledSpan::new_with_span(
.map(|#var| #ctor(
std::option::Option::None,
#var.clone(),
))
@ -161,7 +199,7 @@ impl Labels {
let (display_pat, display_members) = display_pat_members(fields);
labels.as_ref().and_then(|labels| {
let variant_labels = labels.0.iter().map(|label| {
let Label { span, label, ty } = label;
let Label { span, label, ty, primary } = label;
let field = match &span {
syn::Member::Named(ident) => ident.clone(),
syn::Member::Unnamed(syn::Index { index, .. }) => {
@ -169,11 +207,16 @@ impl Labels {
}
};
let var = quote! { __miette_internal_var };
let ctor = if *primary {
quote! { miette::LabeledSpan::new_primary_with_span }
} else {
quote! { miette::LabeledSpan::new_with_span }
};
if let Some(display) = label {
let (fmt, args) = display.expand_shorthand_cloned(&display_members);
quote! {
miette::macro_helpers::OptionalWrapper::<#ty>::new().to_option(#field)
.map(|#var| miette::LabeledSpan::new_with_span(
.map(|#var| #ctor(
std::option::Option::Some(format!(#fmt #args)),
#var.clone(),
))
@ -181,7 +224,7 @@ impl Labels {
} else {
quote! {
miette::macro_helpers::OptionalWrapper::<#ty>::new().to_option(#field)
.map(|#var| miette::LabeledSpan::new_with_span(
.map(|#var| #ctor(
std::option::Option::None,
#var.clone(),
))

View File

@ -10,6 +10,7 @@ use crate::{
pub struct SourceCode {
source_code: syn::Member,
is_option: bool,
}
impl SourceCode {
@ -27,6 +28,19 @@ impl SourceCode {
for (i, field) in fields.iter().enumerate() {
for attr in &field.attrs {
if attr.path().is_ident("source_code") {
let is_option = if let syn::Type::Path(syn::TypePath {
path: syn::Path { segments, .. },
..
}) = &field.ty
{
segments
.last()
.map(|seg| seg.ident == "Option")
.unwrap_or(false)
} else {
false
};
let source_code = if let Some(ident) = field.ident.clone() {
syn::Member::Named(ident)
} else {
@ -35,7 +49,10 @@ impl SourceCode {
span: field.span(),
})
};
return Ok(Some(SourceCode { source_code }));
return Ok(Some(SourceCode {
source_code,
is_option,
}));
}
}
}
@ -45,11 +62,21 @@ impl SourceCode {
pub(crate) fn gen_struct(&self, fields: &syn::Fields) -> Option<TokenStream> {
let (display_pat, _display_members) = display_pat_members(fields);
let src = &self.source_code;
let ret = if self.is_option {
quote! {
self.#src.as_ref().map(|s| s as _)
}
} else {
quote! {
Some(&self.#src)
}
};
Some(quote! {
#[allow(unused_variables)]
fn source_code(&self) -> std::option::Option<&dyn miette::SourceCode> {
let Self #display_pat = self;
Some(&self.#src)
#ret
}
})
}
@ -68,10 +95,19 @@ impl SourceCode {
}
};
let variant_name = ident.clone();
let ret = if source_code.is_option {
quote! {
#field.as_ref().map(|s| s as _)
}
} else {
quote! {
std::option::Option::Some(#field)
}
};
match &fields {
syn::Fields::Unit => None,
_ => Some(quote! {
Self::#variant_name #display_pat => std::option::Option::Some(#field),
Self::#variant_name #display_pat => #ret,
}),
}
})

View File

@ -1,27 +1,51 @@
use std::io;
use std::{fmt, io};
use thiserror::Error;
use crate::{self as miette, Diagnostic};
use crate::Diagnostic;
/**
Error enum for miette. Used by certain operations in the protocol.
*/
#[derive(Debug, Diagnostic, Error)]
#[derive(Debug, Error)]
pub enum MietteError {
/// Wrapper around [`std::io::Error`]. This is returned when something went
/// wrong while reading a [`SourceCode`](crate::SourceCode).
#[error(transparent)]
#[diagnostic(code(miette::io_error), url(docsrs))]
IoError(#[from] io::Error),
/// Returned when a [`SourceSpan`](crate::SourceSpan) extends beyond the
/// bounds of a given [`SourceCode`](crate::SourceCode).
#[error("The given offset is outside the bounds of its Source")]
#[diagnostic(
code(miette::span_out_of_bounds),
help("Double-check your spans. Do you have an off-by-one error?"),
url(docsrs)
)]
OutOfBounds,
}
impl Diagnostic for MietteError {
fn code<'a>(&'a self) -> Option<Box<dyn fmt::Display + 'a>> {
match self {
MietteError::IoError(_) => Some(Box::new("miette::io_error")),
MietteError::OutOfBounds => Some(Box::new("miette::span_out_of_bounds")),
}
}
fn help<'a>(&'a self) -> Option<Box<dyn fmt::Display + 'a>> {
match self {
MietteError::IoError(_) => None,
MietteError::OutOfBounds => Some(Box::new(
"Double-check your spans. Do you have an off-by-one error?",
)),
}
}
fn url<'a>(&'a self) -> Option<Box<dyn fmt::Display + 'a>> {
let crate_version = env!("CARGO_PKG_VERSION");
let variant = match self {
MietteError::IoError(_) => "#variant.IoError",
MietteError::OutOfBounds => "#variant.OutOfBounds",
};
Some(Box::new(format!(
"https://docs.rs/miette/{}/miette/enum.MietteError.html{}",
crate_version, variant,
)))
}
}

View File

@ -30,9 +30,9 @@ impl Report {
/// Create a new error object from a printable error message.
///
/// If the argument implements std::error::Error, prefer `Report::new`
/// If the argument implements [`std::error::Error`], prefer `Report::new`
/// instead which preserves the underlying error's cause chain and
/// backtrace. If the argument may or may not implement std::error::Error
/// backtrace. If the argument may or may not implement [`std::error::Error`]
/// now or in the future, use `miette!(err)` which handles either way
/// correctly.
///
@ -206,7 +206,7 @@ impl Report {
/// Create a new error from an error message to wrap the existing error.
///
/// For attaching a higher level error message to a `Result` as it is
/// propagated, the [crate::WrapErr] extension trait may be more
/// propagated, the [`WrapErr`](crate::WrapErr) extension trait may be more
/// convenient than this function.
///
/// The primary reason to use `error.wrap_err(...)` instead of
@ -233,7 +233,7 @@ impl Report {
unsafe { Report::construct(error, vtable, handler) }
}
/// Compatibility re-export of wrap_err for interop with `anyhow`
/// Compatibility re-export of `wrap_err` for interop with `anyhow`
pub fn context<D>(self, msg: D) -> Self
where
D: Display + Send + Sync + 'static,

View File

@ -222,6 +222,9 @@ macro_rules! ensure {
/// ## `anyhow`/`eyre` Users
///
/// You can just replace `use`s of the `anyhow!`/`eyre!` macros with `miette!`.
///
/// [`diagnostic!`]: crate::diagnostic!
/// [`Report`]: crate::Report
#[macro_export]
macro_rules! miette {
($($key:ident = $value:expr,)* $fmt:literal $($arg:tt)*) => {
@ -282,6 +285,8 @@ macro_rules! miette {
)]
/// assert_eq!(diag.message, "1 + 2 = 3");
/// ```
///
/// [`MietteDiagnostic`]: crate::MietteDiagnostic
#[macro_export]
macro_rules! diagnostic {
($fmt:literal $($arg:tt)*) => {{

View File

@ -43,7 +43,7 @@ where
Box::from_raw(self.ptr.as_ptr())
}
pub(crate) fn by_ref<'a>(&self) -> Ref<'a, T> {
pub(crate) const fn by_ref<'a>(&self) -> Ref<'a, T> {
Ref {
ptr: self.ptr,
lifetime: PhantomData,
@ -91,7 +91,7 @@ where
}
}
pub(crate) fn from_raw(ptr: NonNull<T>) -> Self {
pub(crate) const fn from_raw(ptr: NonNull<T>) -> Self {
Ref {
ptr,
lifetime: PhantomData,
@ -112,7 +112,7 @@ where
}
}
pub(crate) fn as_ptr(self) -> *const T {
pub(crate) const fn as_ptr(self) -> *const T {
self.ptr.as_ptr() as *const T
}
@ -154,7 +154,7 @@ where
}
}
pub(crate) fn by_ref(self) -> Ref<'a, T> {
pub(crate) const fn by_ref(self) -> Ref<'a, T> {
Ref {
ptr: self.ptr,
lifetime: PhantomData,

View File

@ -55,6 +55,9 @@ pub struct MietteHandlerOpts {
pub(crate) context_lines: Option<usize>,
pub(crate) tab_width: Option<usize>,
pub(crate) with_cause_chain: Option<bool>,
pub(crate) break_words: Option<bool>,
pub(crate) word_separator: Option<textwrap::WordSeparator>,
pub(crate) word_splitter: Option<textwrap::WordSplitter>,
}
impl MietteHandlerOpts {
@ -86,6 +89,27 @@ impl MietteHandlerOpts {
self
}
/// If true, long words can be broken when wrapping.
///
/// If false, long words will not be broken when they exceed the width.
///
/// Defaults to true.
pub fn break_words(mut self, break_words: bool) -> Self {
self.break_words = Some(break_words);
self
}
/// Sets the `textwrap::WordSeparator` to use when determining wrap points.
pub fn word_separator(mut self, word_separator: textwrap::WordSeparator) -> Self {
self.word_separator = Some(word_separator);
self
}
/// Sets the `textwrap::WordSplitter` to use when determining wrap points.
pub fn word_splitter(mut self, word_splitter: textwrap::WordSplitter) -> Self {
self.word_splitter = Some(word_splitter);
self
}
/// Include the cause chain of the top-level error in the report.
pub fn with_cause_chain(mut self) -> Self {
self.with_cause_chain = Some(true);
@ -233,6 +257,16 @@ impl MietteHandlerOpts {
if let Some(w) = self.tab_width {
handler = handler.tab_width(w);
}
if let Some(b) = self.break_words {
handler = handler.with_break_words(b)
}
if let Some(s) = self.word_separator {
handler = handler.with_word_separator(s)
}
if let Some(s) = self.word_splitter {
handler = handler.with_word_splitter(s)
}
MietteHandler {
inner: Box::new(handler),
}

View File

@ -13,7 +13,7 @@ pub struct DebugReportHandler;
impl DebugReportHandler {
/// Create a new [`NarratableReportHandler`](crate::NarratableReportHandler)
/// There are no customization options.
pub fn new() -> Self {
pub const fn new() -> Self {
Self
}
}

View File

@ -3,7 +3,7 @@ use std::fmt::{self, Write};
use owo_colors::{OwoColorize, Style};
use unicode_width::UnicodeWidthChar;
use crate::diagnostic_chain::DiagnosticChain;
use crate::diagnostic_chain::{DiagnosticChain, ErrorKind};
use crate::handlers::theme::*;
use crate::protocol::{Diagnostic, Severity};
use crate::{LabeledSpan, MietteError, ReportHandler, SourceCode, SourceSpan, SpanContents};
@ -20,7 +20,7 @@ This printer can be customized by using [`new_themed()`](GraphicalReportHandler:
See [`set_hook()`](crate::set_hook) for more details on customizing your global
printer.
*/
*/
#[derive(Debug, Clone)]
pub struct GraphicalReportHandler {
pub(crate) links: LinkStyle,
@ -30,6 +30,9 @@ pub struct GraphicalReportHandler {
pub(crate) context_lines: usize,
pub(crate) tab_width: usize,
pub(crate) with_cause_chain: bool,
pub(crate) break_words: bool,
pub(crate) word_separator: Option<textwrap::WordSeparator>,
pub(crate) word_splitter: Option<textwrap::WordSplitter>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@ -51,6 +54,9 @@ impl GraphicalReportHandler {
context_lines: 1,
tab_width: 4,
with_cause_chain: true,
break_words: true,
word_separator: None,
word_splitter: None,
}
}
@ -64,6 +70,9 @@ impl GraphicalReportHandler {
context_lines: 1,
tab_width: 4,
with_cause_chain: true,
break_words: true,
word_separator: None,
word_splitter: None,
}
}
@ -122,6 +131,24 @@ impl GraphicalReportHandler {
self
}
/// Enables or disables breaking of words during wrapping.
pub fn with_break_words(mut self, break_words: bool) -> Self {
self.break_words = break_words;
self
}
/// Sets the word separator to use when wrapping.
pub fn with_word_separator(mut self, word_separator: textwrap::WordSeparator) -> Self {
self.word_separator = Some(word_separator);
self
}
/// Sets the word splitter to usewhen wrapping.
pub fn with_word_splitter(mut self, word_splitter: textwrap::WordSplitter) -> Self {
self.word_splitter = Some(word_splitter);
self
}
/// Sets the 'global' footer for this handler.
pub fn with_footer(mut self, footer: String) -> Self {
self.footer = Some(footer);
@ -151,7 +178,6 @@ impl GraphicalReportHandler {
diagnostic: &(dyn Diagnostic),
) -> fmt::Result {
self.render_header(f, diagnostic)?;
writeln!(f)?;
self.render_causes(f, diagnostic)?;
let src = diagnostic.source_code();
self.render_snippets(f, diagnostic, src)?;
@ -160,9 +186,17 @@ impl GraphicalReportHandler {
if let Some(footer) = &self.footer {
writeln!(f)?;
let width = self.termwidth.saturating_sub(4);
let opts = textwrap::Options::new(width)
let mut opts = textwrap::Options::new(width)
.initial_indent(" ")
.subsequent_indent(" ");
.subsequent_indent(" ")
.break_words(self.break_words);
if let Some(word_separator) = self.word_separator {
opts = opts.word_separator(word_separator);
}
if let Some(word_splitter) = self.word_splitter.clone() {
opts = opts.word_splitter(word_splitter);
}
writeln!(f, "{}", textwrap::fill(footer, opts))?;
}
Ok(())
@ -190,6 +224,7 @@ impl GraphicalReportHandler {
);
write!(header, "{}", link)?;
writeln!(f, "{}", header)?;
writeln!(f)?;
} else if let Some(code) = diagnostic.code() {
write!(header, "{}", code.style(severity_style),)?;
if self.links == LinkStyle::Text && diagnostic.url().is_some() {
@ -197,6 +232,7 @@ impl GraphicalReportHandler {
write!(header, " ({})", url.style(self.theme.styles.link))?;
}
writeln!(f, "{}", header)?;
writeln!(f)?;
}
Ok(())
}
@ -211,9 +247,16 @@ impl GraphicalReportHandler {
let initial_indent = format!(" {} ", severity_icon.style(severity_style));
let rest_indent = format!(" {} ", self.theme.characters.vbar.style(severity_style));
let width = self.termwidth.saturating_sub(2);
let opts = textwrap::Options::new(width)
let mut opts = textwrap::Options::new(width)
.initial_indent(&initial_indent)
.subsequent_indent(&rest_indent);
.subsequent_indent(&rest_indent)
.break_words(self.break_words);
if let Some(word_separator) = self.word_separator {
opts = opts.word_separator(word_separator);
}
if let Some(word_splitter) = self.word_splitter.clone() {
opts = opts.word_splitter(word_splitter);
}
writeln!(f, "{}", textwrap::fill(&diagnostic.to_string(), opts))?;
@ -250,10 +293,33 @@ impl GraphicalReportHandler {
)
.style(severity_style)
.to_string();
let opts = textwrap::Options::new(width)
let mut opts = textwrap::Options::new(width)
.initial_indent(&initial_indent)
.subsequent_indent(&rest_indent);
writeln!(f, "{}", textwrap::fill(&error.to_string(), opts))?;
.subsequent_indent(&rest_indent)
.break_words(self.break_words);
if let Some(word_separator) = self.word_separator {
opts = opts.word_separator(word_separator);
}
if let Some(word_splitter) = self.word_splitter.clone() {
opts = opts.word_splitter(word_splitter);
}
match error {
ErrorKind::Diagnostic(diag) => {
let mut inner = String::new();
// Don't print footer for inner errors
let mut inner_renderer = self.clone();
inner_renderer.footer = None;
inner_renderer.with_cause_chain = false;
inner_renderer.render_report(&mut inner, diag)?;
writeln!(f, "{}", textwrap::fill(&inner, opts))?;
}
ErrorKind::StdError(err) => {
writeln!(f, "{}", textwrap::fill(&err.to_string(), opts))?;
}
}
}
}
@ -264,9 +330,17 @@ impl GraphicalReportHandler {
if let Some(help) = diagnostic.help() {
let width = self.termwidth.saturating_sub(4);
let initial_indent = " help: ".style(self.theme.styles.help).to_string();
let opts = textwrap::Options::new(width)
let mut opts = textwrap::Options::new(width)
.initial_indent(&initial_indent)
.subsequent_indent(" ");
.subsequent_indent(" ")
.break_words(self.break_words);
if let Some(word_separator) = self.word_separator {
opts = opts.word_separator(word_separator);
}
if let Some(word_splitter) = self.word_splitter.clone() {
opts = opts.word_splitter(word_splitter);
}
writeln!(f, "{}", textwrap::fill(&help.to_string(), opts))?;
}
Ok(())
@ -287,7 +361,6 @@ impl GraphicalReportHandler {
Some(Severity::Advice) => write!(f, "Advice: ")?,
};
self.render_header(f, rel)?;
writeln!(f)?;
self.render_causes(f, rel)?;
let src = rel.source_code().or(parent_src);
self.render_snippets(f, rel, src)?;
@ -367,15 +440,20 @@ impl GraphicalReportHandler {
Ok(())
}
fn render_context<'a>(
fn render_context(
&self,
f: &mut impl fmt::Write,
source: &'a dyn SourceCode,
source: &dyn SourceCode,
context: &LabeledSpan,
labels: &[LabeledSpan],
) -> fmt::Result {
let (contents, lines) = self.get_lines(source, context.inner())?;
let primary_label = labels
.iter()
.find(|label| label.primary())
.or_else(|| labels.first());
// sorting is your friend
let labels = labels
.iter()
@ -416,19 +494,33 @@ impl GraphicalReportHandler {
self.theme.characters.hbar,
)?;
if let Some(source_name) = contents.name() {
// If there is a primary label, then use its span
// as the reference point for line/column information.
let primary_contents = match primary_label {
Some(label) => source
.read_span(label.inner(), 0, 0)
.map_err(|_| fmt::Error)?,
None => contents,
};
if let Some(source_name) = primary_contents.name() {
let source_name = source_name.style(self.theme.styles.link);
writeln!(
f,
"[{}:{}:{}]",
source_name,
contents.line() + 1,
contents.column() + 1
primary_contents.line() + 1,
primary_contents.column() + 1
)?;
} else if lines.len() <= 1 {
writeln!(f, "{}", self.theme.characters.hbar.to_string().repeat(3))?;
} else {
writeln!(f, "[{}:{}]", contents.line() + 1, contents.column() + 1)?;
writeln!(
f,
"[{}:{}]",
primary_contents.line() + 1,
primary_contents.column() + 1
)?;
}
// Now it's time for the fun part--actually rendering everything!
@ -453,7 +545,13 @@ impl GraphicalReportHandler {
// no line number!
self.write_no_linum(f, linum_width)?;
// gutter _again_
self.render_highlight_gutter(f, max_gutter, line, &labels)?;
self.render_highlight_gutter(
f,
max_gutter,
line,
&labels,
LabelRenderMode::SingleLine,
)?;
self.render_single_line_highlights(
f,
line,
@ -465,11 +563,7 @@ impl GraphicalReportHandler {
}
for hl in multi_line {
if hl.label().is_some() && line.span_ends(hl) && !line.span_starts(hl) {
// no line number!
self.write_no_linum(f, linum_width)?;
// gutter _again_
self.render_highlight_gutter(f, max_gutter, line, &labels)?;
self.render_multi_line_end(f, hl)?;
self.render_multi_line_end(f, &labels, max_gutter, linum_width, line, hl)?;
}
}
}
@ -483,6 +577,91 @@ impl GraphicalReportHandler {
Ok(())
}
fn render_multi_line_end(
&self,
f: &mut impl fmt::Write,
labels: &[FancySpan],
max_gutter: usize,
linum_width: usize,
line: &Line,
label: &FancySpan,
) -> fmt::Result {
// no line number!
self.write_no_linum(f, linum_width)?;
if let Some(label_parts) = label.label_parts() {
// if it has a label, how long is it?
let (first, rest) = label_parts
.split_first()
.expect("cannot crash because rest would have been None, see docs on the `label` field of FancySpan");
if rest.is_empty() {
// gutter _again_
self.render_highlight_gutter(
f,
max_gutter,
line,
&labels,
LabelRenderMode::SingleLine,
)?;
self.render_multi_line_end_single(
f,
first,
label.style,
LabelRenderMode::SingleLine,
)?;
} else {
// gutter _again_
self.render_highlight_gutter(
f,
max_gutter,
line,
&labels,
LabelRenderMode::MultiLineFirst,
)?;
self.render_multi_line_end_single(
f,
first,
label.style,
LabelRenderMode::MultiLineFirst,
)?;
for label_line in rest {
// no line number!
self.write_no_linum(f, linum_width)?;
// gutter _again_
self.render_highlight_gutter(
f,
max_gutter,
line,
&labels,
LabelRenderMode::MultiLineRest,
)?;
self.render_multi_line_end_single(
f,
label_line,
label.style,
LabelRenderMode::MultiLineRest,
)?;
}
}
} else {
// gutter _again_
self.render_highlight_gutter(
f,
max_gutter,
line,
&labels,
LabelRenderMode::SingleLine,
)?;
// has no label
writeln!(f, "{}", self.theme.characters.hbar.style(label.style))?;
}
Ok(())
}
fn render_line_gutter(
&self,
f: &mut impl fmt::Write,
@ -551,6 +730,7 @@ impl GraphicalReportHandler {
max_gutter: usize,
line: &Line,
highlights: &[FancySpan],
render_mode: LabelRenderMode,
) -> fmt::Result {
if max_gutter == 0 {
return Ok(());
@ -560,15 +740,33 @@ impl GraphicalReportHandler {
let applicable = highlights.iter().filter(|hl| line.span_applies(hl));
for (i, hl) in applicable.enumerate() {
if !line.span_line_only(hl) && line.span_ends(hl) {
gutter.push_str(&chars.lbot.style(hl.style).to_string());
gutter.push_str(
&chars
.hbar
.to_string()
.repeat(max_gutter.saturating_sub(i) + 2)
.style(hl.style)
.to_string(),
);
if render_mode == LabelRenderMode::MultiLineRest {
// this is to make multiline labels work. We want to make the right amount
// of horizontal space for them, but not actually draw the lines
for _ in 0..max_gutter.saturating_sub(i) + 2 {
gutter.push(' ');
}
} else {
gutter.push_str(&chars.lbot.style(hl.style).to_string());
gutter.push_str(
&chars
.hbar
.to_string()
.repeat(
max_gutter.saturating_sub(i)
// if we are rendering a multiline label, then leave a bit of space for the
// rcross character
+ if render_mode == LabelRenderMode::MultiLineFirst {
1
} else {
2
},
)
.style(hl.style)
.to_string(),
);
}
break;
} else {
gutter.push_str(&chars.vbar.style(hl.style).to_string());
@ -617,11 +815,22 @@ impl GraphicalReportHandler {
}
/// Returns the visual column position of a byte offset on a specific line.
fn visual_offset(&self, line: &Line, offset: usize) -> usize {
///
/// If the offset occurs in the middle of a character, the returned column
/// corresponds to that character's first column in `start` is true, or its
/// last column if `start` is false.
fn visual_offset(&self, line: &Line, offset: usize, start: bool) -> usize {
let line_range = line.offset..=(line.offset + line.length);
assert!(line_range.contains(&offset));
let text_index = offset - line.offset;
let mut text_index = offset - line.offset;
while text_index <= line.text.len() && !line.text.is_char_boundary(text_index) {
if start {
text_index -= 1;
} else {
text_index += 1;
}
}
let text = &line.text[..text_index.min(line.text.len())];
let text_width = self.line_visual_char_width(text).sum();
if text_index > line.text.len() {
@ -644,10 +853,10 @@ impl GraphicalReportHandler {
for (c, width) in text.chars().zip(self.line_visual_char_width(text)) {
if c == '\t' {
for _ in 0..width {
f.write_char(' ')?
f.write_char(' ')?;
}
} else {
f.write_char(c)?
f.write_char(c)?;
}
}
f.write_char('\n')?;
@ -672,32 +881,34 @@ impl GraphicalReportHandler {
.map(|hl| {
let byte_start = hl.offset();
let byte_end = hl.offset() + hl.len();
let start = self.visual_offset(line, byte_start).max(highest);
let end = self.visual_offset(line, byte_end).max(start + 1);
let start = self.visual_offset(line, byte_start, true).max(highest);
let end = if hl.len() == 0 {
start + 1
} else {
self.visual_offset(line, byte_end, false).max(start + 1)
};
let vbar_offset = (start + end) / 2;
let num_left = vbar_offset - start;
let num_right = end - vbar_offset - 1;
if start < end {
underlines.push_str(
&format!(
"{:width$}{}{}{}",
"",
chars.underline.to_string().repeat(num_left),
if hl.len() == 0 {
chars.uarrow
} else if hl.label().is_some() {
chars.underbar
} else {
chars.underline
},
chars.underline.to_string().repeat(num_right),
width = start.saturating_sub(highest),
)
.style(hl.style)
.to_string(),
);
}
underlines.push_str(
&format!(
"{:width$}{}{}{}",
"",
chars.underline.to_string().repeat(num_left),
if hl.len() == 0 {
chars.uarrow
} else if hl.label().is_some() {
chars.underbar
} else {
chars.underline
},
chars.underline.to_string().repeat(num_right),
width = start.saturating_sub(highest),
)
.style(hl.style)
.to_string(),
);
highest = std::cmp::max(highest, end);
(hl, vbar_offset)
@ -706,27 +917,40 @@ impl GraphicalReportHandler {
writeln!(f, "{}", underlines)?;
for hl in single_liners.iter().rev() {
if let Some(label) = hl.label() {
self.write_no_linum(f, linum_width)?;
self.render_highlight_gutter(f, max_gutter, line, all_highlights)?;
let mut curr_offset = 1usize;
for (offset_hl, vbar_offset) in &vbar_offsets {
while curr_offset < *vbar_offset + 1 {
write!(f, " ")?;
curr_offset += 1;
}
if *offset_hl != hl {
write!(f, "{}", chars.vbar.to_string().style(offset_hl.style))?;
curr_offset += 1;
} else {
let lines = format!(
"{}{} {}",
chars.lbot,
chars.hbar.to_string().repeat(2),
label,
);
writeln!(f, "{}", lines.style(hl.style))?;
break;
if let Some(label) = hl.label_parts() {
if label.len() == 1 {
self.write_label_text(
f,
line,
linum_width,
max_gutter,
all_highlights,
chars,
&vbar_offsets,
hl,
&label[0],
LabelRenderMode::SingleLine,
)?;
} else {
let mut first = true;
for label_line in &label {
self.write_label_text(
f,
line,
linum_width,
max_gutter,
all_highlights,
chars,
&vbar_offsets,
hl,
label_line,
if first {
LabelRenderMode::MultiLineFirst
} else {
LabelRenderMode::MultiLineRest
},
)?;
first = false;
}
}
}
@ -734,13 +958,80 @@ impl GraphicalReportHandler {
Ok(())
}
fn render_multi_line_end(&self, f: &mut impl fmt::Write, hl: &FancySpan) -> fmt::Result {
writeln!(
// I know it's not good practice, but making this a function makes a lot of sense
// and making a struct for this does not...
#[allow(clippy::too_many_arguments)]
fn write_label_text(
&self,
f: &mut impl fmt::Write,
line: &Line,
linum_width: usize,
max_gutter: usize,
all_highlights: &[FancySpan],
chars: &ThemeCharacters,
vbar_offsets: &[(&&FancySpan, usize)],
hl: &&FancySpan,
label: &str,
render_mode: LabelRenderMode,
) -> fmt::Result {
self.write_no_linum(f, linum_width)?;
self.render_highlight_gutter(
f,
"{} {}",
self.theme.characters.hbar.style(hl.style),
hl.label().unwrap_or_else(|| "".into()),
max_gutter,
line,
all_highlights,
LabelRenderMode::SingleLine,
)?;
let mut curr_offset = 1usize;
for (offset_hl, vbar_offset) in vbar_offsets {
while curr_offset < *vbar_offset + 1 {
write!(f, " ")?;
curr_offset += 1;
}
if *offset_hl != hl {
write!(f, "{}", chars.vbar.to_string().style(offset_hl.style))?;
curr_offset += 1;
} else {
let lines = match render_mode {
LabelRenderMode::SingleLine => format!(
"{}{} {}",
chars.lbot,
chars.hbar.to_string().repeat(2),
label,
),
LabelRenderMode::MultiLineFirst => {
format!("{}{}{} {}", chars.lbot, chars.hbar, chars.rcross, label,)
}
LabelRenderMode::MultiLineRest => {
format!(" {} {}", chars.vbar, label,)
}
};
writeln!(f, "{}", lines.style(hl.style))?;
break;
}
}
Ok(())
}
fn render_multi_line_end_single(
&self,
f: &mut impl fmt::Write,
label: &str,
style: Style,
render_mode: LabelRenderMode,
) -> fmt::Result {
match render_mode {
LabelRenderMode::SingleLine => {
writeln!(f, "{} {}", self.theme.characters.hbar.style(style), label)?;
}
LabelRenderMode::MultiLineFirst => {
writeln!(f, "{} {}", self.theme.characters.rcross.style(style), label)?;
}
LabelRenderMode::MultiLineRest => {
writeln!(f, "{} {}", self.theme.characters.vbar.style(style), label)?;
}
}
Ok(())
}
@ -819,6 +1110,16 @@ impl ReportHandler for GraphicalReportHandler {
Support types
*/
#[derive(PartialEq, Debug)]
enum LabelRenderMode {
/// we're rendering a single line label (or not rendering in any special way)
SingleLine,
/// we're rendering a multiline label
MultiLineFirst,
/// we're rendering the rest of a multiline label
MultiLineRest,
}
#[derive(Debug)]
struct Line {
line_number: usize,
@ -836,10 +1137,10 @@ impl Line {
let spanlen = if span.len() == 0 { 1 } else { span.len() };
// Span starts in this line
(span.offset() >= self.offset && span.offset() < self.offset + self.length)
// Span passes through this line
|| (span.offset() < self.offset && span.offset() + spanlen > self.offset + self.length) //todo
// Span ends on this line
|| (span.offset() + spanlen > self.offset && span.offset() + spanlen <= self.offset + self.length)
// Span passes through this line
|| (span.offset() < self.offset && span.offset() + spanlen > self.offset + self.length) //todo
// Span ends on this line
|| (span.offset() + spanlen > self.offset && span.offset() + spanlen <= self.offset + self.length)
}
// A 'flyby' is a multi-line span that technically covers this line, but
@ -869,7 +1170,10 @@ impl Line {
#[derive(Debug, Clone)]
struct FancySpan {
label: Option<String>,
/// this is deliberately an option of a vec because I wanted to be very explicit
/// that there can also be *no* label. If there is a label, it can have multiple
/// lines which is what the vec is for.
label: Option<Vec<String>>,
span: SourceSpan,
style: Style,
}
@ -880,9 +1184,17 @@ impl PartialEq for FancySpan {
}
}
fn split_label(v: String) -> Vec<String> {
v.split('\n').map(|i| i.to_string()).collect()
}
impl FancySpan {
fn new(label: Option<String>, span: SourceSpan, style: Style) -> Self {
FancySpan { label, span, style }
FancySpan {
label: label.map(split_label),
span,
style,
}
}
fn style(&self) -> Style {
@ -892,7 +1204,15 @@ impl FancySpan {
fn label(&self) -> Option<String> {
self.label
.as_ref()
.map(|l| l.style(self.style()).to_string())
.map(|l| l.join("\n").style(self.style()).to_string())
}
fn label_parts(&self) -> Option<Vec<String>> {
self.label.as_ref().map(|l| {
l.iter()
.map(|i| i.style(self.style()).to_string())
.collect()
})
}
fn offset(&self) -> usize {

View File

@ -13,7 +13,7 @@ pub struct JSONReportHandler;
impl JSONReportHandler {
/// Create a new [`JSONReportHandler`]. There are no customization
/// options.
pub fn new() -> Self {
pub const fn new() -> Self {
Self
}
}
@ -49,7 +49,7 @@ impl fmt::Display for Escape<'_> {
}
}
fn escape(input: &'_ str) -> Escape<'_> {
const fn escape(input: &'_ str) -> Escape<'_> {
Escape(input)
}
@ -96,7 +96,7 @@ impl JSONReportHandler {
}
write!(f, r#""{}""#, escape(&error.to_string()))?;
}
write!(f, "],")?
write!(f, "],")?;
} else {
write!(f, r#""causes": [],"#)?;
}

View File

@ -21,7 +21,7 @@ pub struct NarratableReportHandler {
impl NarratableReportHandler {
/// Create a new [`NarratableReportHandler`]. There are no customization
/// options.
pub fn new() -> Self {
pub const fn new() -> Self {
Self {
footer: None,
context_lines: 1,
@ -31,13 +31,13 @@ impl NarratableReportHandler {
/// Include the cause chain of the top-level error in the report, if
/// available.
pub fn with_cause_chain(mut self) -> Self {
pub const fn with_cause_chain(mut self) -> Self {
self.with_cause_chain = true;
self
}
/// Do not include the cause chain of the top-level error in the report.
pub fn without_cause_chain(mut self) -> Self {
pub const fn without_cause_chain(mut self) -> Self {
self.with_cause_chain = false;
self
}
@ -49,7 +49,7 @@ impl NarratableReportHandler {
}
/// Sets the number of lines of context to show around each error.
pub fn with_context_lines(mut self, lines: usize) -> Self {
pub const fn with_context_lines(mut self, lines: usize) -> Self {
self.context_lines = lines;
self
}

View File

@ -55,9 +55,9 @@ impl GraphicalTheme {
/// A "basic" graphical theme that skips colors and unicode characters and
/// just does monochrome ascii art. If you want a completely non-graphical
/// rendering of your `Diagnostic`s, check out
/// [crate::NarratableReportHandler], or write your own
/// [crate::ReportHandler]!
/// rendering of your [`Diagnostic`](crate::Diagnostic)s, check out
/// [`NarratableReportHandler`](crate::NarratableReportHandler), or write
/// your own [`ReportHandler`](crate::ReportHandler)
pub fn none() -> Self {
Self {
characters: ThemeCharacters::ascii(),
@ -79,7 +79,8 @@ impl Default for GraphicalTheme {
}
/**
Styles for various parts of graphical rendering for the [crate::GraphicalReportHandler].
Styles for various parts of graphical rendering for the
[`GraphicalReportHandler`](crate::GraphicalReportHandler).
*/
#[derive(Debug, Clone)]
pub struct ThemeStyles {
@ -159,7 +160,7 @@ impl ThemeStyles {
// https://github.com/zesterer/ariadne/blob/e3cb394cb56ecda116a0a1caecd385a49e7f6662/src/draw.rs
/// Characters to be used when drawing when using
/// [crate::GraphicalReportHandler].
/// [`GraphicalReportHandler`](crate::GraphicalReportHandler).
#[allow(missing_docs)]
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct ThemeCharacters {

View File

@ -186,7 +186,7 @@
//!
//! ```rust
//! // lib/error.rs
//! use miette::Diagnostic;
//! use miette::{Diagnostic, SourceSpan};
//! use thiserror::Error;
//!
//! #[derive(Error, Diagnostic, Debug)]
@ -198,6 +198,18 @@
//! #[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
//! }
//! ```
//!
@ -292,6 +304,23 @@
//! miette = { version = "X.Y.Z", features = ["fancy"] }
//! ```
//!
//! Another way to display a diagnostic is by printing them using the debug formatter.
//! This is, in fact, what returning diagnostics from main ends up doing.
//! To do it yourself, you can write the following:
//!
//! ```rust
//! 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),
//! }
//! }
//! ```
//!
//! ### ... diagnostic code URLs
//!
//! `miette` supports providing a URL for individual diagnostics. This URL will
@ -581,6 +610,7 @@
//! .unicode(false)
//! .context_lines(3)
//! .tab_width(4)
//! .break_words(true)
//! .build(),
//! )
//! }))
@ -640,6 +670,7 @@
//! and some from [`thiserror`](https://github.com/dtolnay/thiserror), also
//! under the Apache License. Some code is taken from
//! [`ariadne`](https://github.com/zesterer/ariadne), which is MIT licensed.
#[cfg(feature = "derive")]
pub use miette_derive::*;
pub use error::*;

View File

@ -252,7 +252,7 @@ impl MietteDiagnostic {
/// ```
pub fn and_labels(mut self, labels: impl IntoIterator<Item = LabeledSpan>) -> Self {
let mut all_labels = self.labels.unwrap_or_default();
all_labels.extend(labels.into_iter());
all_labels.extend(labels);
self.labels = Some(all_labels);
self
}
@ -292,14 +292,16 @@ fn test_serialize_miette_diagnostic() {
"offset": 0,
"length": 0
},
"label": "label1"
"label": "label1",
"primary": false
},
{
"span": {
"offset": 1,
"length": 2
},
"label": "label2"
"label": "label2",
"primary": false
}
]
});
@ -350,14 +352,16 @@ fn test_deserialize_miette_diagnostic() {
"offset": 0,
"length": 0
},
"label": "label1"
"label": "label1",
"primary": false
},
{
"span": {
"offset": 1,
"length": 2
},
"label": "label2"
"label": "label2",
"primary": false
}
]
});

View File

@ -69,16 +69,28 @@ pub trait Diagnostic: std::error::Error {
}
}
impl std::error::Error for Box<dyn Diagnostic> {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
(**self).source()
}
macro_rules! box_impls {
($($box_type:ty),*) => {
$(
impl std::error::Error for $box_type {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
(**self).source()
}
fn cause(&self) -> Option<&dyn std::error::Error> {
self.source()
fn cause(&self) -> Option<&dyn std::error::Error> {
self.source()
}
}
)*
}
}
box_impls! {
Box<dyn Diagnostic>,
Box<dyn Diagnostic + Send>,
Box<dyn Diagnostic + Send + Sync>
}
impl<T: Diagnostic + Send + Sync + 'static> From<T>
for Box<dyn Diagnostic + Send + Sync + 'static>
{
@ -220,7 +232,7 @@ whole thing--meaning you should be able to support `SourceCode`s which are
gigabytes or larger in size.
*/
pub trait SourceCode: Send + Sync {
/// Read the bytes for a specific span from this SourceCode, keeping a
/// Read the bytes for a specific span from this `SourceCode`, keeping a
/// certain number of lines before and after the span as context.
fn read_span<'a>(
&'a self,
@ -237,14 +249,16 @@ pub struct LabeledSpan {
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
label: Option<String>,
span: SourceSpan,
primary: bool,
}
impl LabeledSpan {
/// Makes a new labeled span.
pub fn new(label: Option<String>, offset: ByteOffset, len: usize) -> Self {
pub const fn new(label: Option<String>, offset: ByteOffset, len: usize) -> Self {
Self {
label,
span: (offset, len).into(),
span: SourceSpan::new(SourceOffset(offset), SourceOffset(len)),
primary: false,
}
}
@ -253,6 +267,16 @@ impl LabeledSpan {
Self {
label,
span: span.into(),
primary: false,
}
}
/// Makes a new labeled primary span using an existing span.
pub fn new_primary_with_span(label: Option<String>, span: impl Into<SourceSpan>) -> Self {
Self {
label,
span: span.into(),
primary: true,
}
}
@ -310,24 +334,29 @@ impl LabeledSpan {
}
/// Returns a reference to the inner [`SourceSpan`].
pub fn inner(&self) -> &SourceSpan {
pub const fn inner(&self) -> &SourceSpan {
&self.span
}
/// Returns the 0-based starting byte offset.
pub fn offset(&self) -> usize {
pub const fn offset(&self) -> usize {
self.span.offset()
}
/// Returns the number of bytes this `LabeledSpan` spans.
pub fn len(&self) -> usize {
pub const fn len(&self) -> usize {
self.span.len()
}
/// True if this `LabeledSpan` is empty.
pub fn is_empty(&self) -> bool {
pub const fn is_empty(&self) -> bool {
self.span.is_empty()
}
/// True if this `LabeledSpan` is a primary span.
pub const fn primary(&self) -> bool {
self.primary
}
}
#[cfg(feature = "serde")]
@ -338,7 +367,8 @@ fn test_serialize_labeled_span() {
assert_eq!(
json!(LabeledSpan::new(None, 0, 0)),
json!({
"span": { "offset": 0, "length": 0 }
"span": { "offset": 0, "length": 0, },
"primary": false,
})
);
@ -346,9 +376,10 @@ fn test_serialize_labeled_span() {
json!(LabeledSpan::new(Some("label".to_string()), 0, 0)),
json!({
"label": "label",
"span": { "offset": 0, "length": 0 }
"span": { "offset": 0, "length": 0, },
"primary": false,
})
)
);
}
#[cfg(feature = "serde")]
@ -358,23 +389,26 @@ fn test_deserialize_labeled_span() {
let span: LabeledSpan = serde_json::from_value(json!({
"label": null,
"span": { "offset": 0, "length": 0 }
"span": { "offset": 0, "length": 0, },
"primary": false,
}))
.unwrap();
assert_eq!(span, LabeledSpan::new(None, 0, 0));
let span: LabeledSpan = serde_json::from_value(json!({
"span": { "offset": 0, "length": 0 }
"span": { "offset": 0, "length": 0, },
"primary": false
}))
.unwrap();
assert_eq!(span, LabeledSpan::new(None, 0, 0));
let span: LabeledSpan = serde_json::from_value(json!({
"label": "label",
"span": { "offset": 0, "length": 0 }
"span": { "offset": 0, "length": 0, },
"primary": false
}))
.unwrap();
assert_eq!(span, LabeledSpan::new(Some("label".to_string()), 0, 0))
assert_eq!(span, LabeledSpan::new(Some("label".to_string()), 0, 0));
}
/**
@ -422,7 +456,7 @@ pub struct MietteSpanContents<'a> {
impl<'a> MietteSpanContents<'a> {
/// Make a new [`MietteSpanContents`] object.
pub fn new(
pub const fn new(
data: &'a [u8],
span: SourceSpan,
line: usize,
@ -440,7 +474,7 @@ impl<'a> MietteSpanContents<'a> {
}
/// Make a new [`MietteSpanContents`] object, with a name for its 'file'.
pub fn new_named(
pub const fn new_named(
name: String,
data: &'a [u8],
span: SourceSpan,
@ -492,7 +526,7 @@ pub struct SourceSpan {
impl SourceSpan {
/// Create a new [`SourceSpan`].
pub fn new(start: SourceOffset, length: usize) -> Self {
pub const fn new(start: SourceOffset, length: usize) -> Self {
Self {
offset: start,
length,
@ -500,18 +534,18 @@ impl SourceSpan {
}
/// The absolute offset, in bytes, from the beginning of a [`SourceCode`].
pub fn offset(&self) -> usize {
pub const fn offset(&self) -> usize {
self.offset.offset()
}
/// Total length of the [`SourceSpan`], in bytes.
pub fn len(&self) -> usize {
pub const fn len(&self) -> usize {
self.length
}
/// Whether this [`SourceSpan`] has a length of zero. It may still be useful
/// to point to a specific point.
pub fn is_empty(&self) -> bool {
pub const fn is_empty(&self) -> bool {
self.length == 0
}
}
@ -563,7 +597,7 @@ fn test_serialize_source_span() {
assert_eq!(
json!(SourceSpan::from(0)),
json!({ "offset": 0, "length": 0})
)
);
}
#[cfg(feature = "serde")]
@ -572,7 +606,7 @@ fn test_deserialize_source_span() {
use serde_json::json;
let span: SourceSpan = serde_json::from_value(json!({ "offset": 0, "length": 0})).unwrap();
assert_eq!(span, SourceSpan::from(0))
assert_eq!(span, SourceSpan::from(0));
}
/**
@ -589,7 +623,7 @@ pub struct SourceOffset(ByteOffset);
impl SourceOffset {
/// Actual byte offset.
pub fn offset(&self) -> ByteOffset {
pub const fn offset(&self) -> ByteOffset {
self.0
}
@ -674,12 +708,12 @@ fn test_source_offset_from_location() {
fn test_serialize_source_offset() {
use serde_json::json;
assert_eq!(json!(SourceOffset::from(0)), 0)
assert_eq!(json!(SourceOffset::from(0)), 0);
}
#[cfg(feature = "serde")]
#[test]
fn test_deserialize_source_offset() {
let offset: SourceOffset = serde_json::from_str("0").unwrap();
assert_eq!(offset, SourceOffset::from(0))
assert_eq!(offset, SourceOffset::from(0));
}

View File

@ -189,7 +189,7 @@ fn fmt_help() {
assert_eq!(
"1 x hello x \"2\"".to_string(),
FooStruct("hello".into()).help().unwrap().to_string()
FooStruct("hello").help().unwrap().to_string()
);
#[derive(Debug, Diagnostic, Error)]
@ -201,12 +201,7 @@ fn fmt_help() {
assert_eq!(
"1 x hello x \"2\"".to_string(),
BarStruct {
my_field: "hello".into()
}
.help()
.unwrap()
.to_string()
BarStruct { my_field: "hello" }.help().unwrap().to_string()
);
#[derive(Debug, Diagnostic, Error)]
@ -224,7 +219,7 @@ fn fmt_help() {
assert_eq!(
"1 x bar x \"2\"".to_string(),
FooEnum::X("bar".into()).help().unwrap().to_string()
FooEnum::X("bar").help().unwrap().to_string()
);
assert_eq!(
@ -250,12 +245,7 @@ fn help_field() {
assert_eq!(
"x".to_string(),
Foo {
do_this: Some("x".into())
}
.help()
.unwrap()
.to_string()
Foo { do_this: Some("x") }.help().unwrap().to_string()
);
#[derive(Debug, Diagnostic, Error)]
@ -271,16 +261,11 @@ fn help_field() {
assert_eq!(
"x".to_string(),
Bar::A(Some("x".into())).help().unwrap().to_string()
Bar::A(Some("x")).help().unwrap().to_string()
);
assert_eq!(
"x".to_string(),
Bar::B {
do_this: Some("x".into())
}
.help()
.unwrap()
.to_string()
Bar::B { do_this: Some("x") }.help().unwrap().to_string()
);
#[derive(Debug, Diagnostic, Error)]
@ -288,20 +273,14 @@ fn help_field() {
#[diagnostic()]
struct Baz<'a>(#[help] Option<&'a str>);
assert_eq!(
"x".to_string(),
Baz(Some("x".into())).help().unwrap().to_string()
);
assert_eq!("x".to_string(), Baz(Some("x")).help().unwrap().to_string());
#[derive(Debug, Diagnostic, Error)]
#[error("welp")]
#[diagnostic()]
struct Quux<'a>(#[help] &'a str);
assert_eq!(
"x".to_string(),
Quux("x".into()).help().unwrap().to_string()
);
assert_eq!("x".to_string(), Quux("x").help().unwrap().to_string());
}
#[test]
@ -589,7 +568,7 @@ fn test_unit_struct_display() {
#[error("unit only")]
#[diagnostic(code(foo::bar::overridden), help("hello from unit help"))]
struct UnitOnly;
assert_eq!(UnitOnly.help().unwrap().to_string(), "hello from unit help")
assert_eq!(UnitOnly.help().unwrap().to_string(), "hello from unit help");
}
#[test]
@ -603,5 +582,47 @@ fn test_unit_enum_display() {
assert_eq!(
Enum::UnitVariant.help().unwrap().to_string(),
"hello from unit help"
)
);
}
#[test]
fn test_optional_source_code() {
#[derive(Debug, Diagnostic, Error)]
#[error("struct with optional source")]
struct Struct {
#[source_code]
src: Option<String>,
}
assert!(Struct { src: None }.source_code().is_none());
assert!(Struct {
src: Some("".to_string())
}
.source_code()
.is_some());
#[derive(Debug, Diagnostic, Error)]
enum Enum {
#[error("variant1 with optional source")]
Variant1 {
#[source_code]
src: Option<String>,
},
#[error("variant2 with optional source")]
Variant2 {
#[source_code]
src: Option<String>,
},
}
assert!(Enum::Variant1 { src: None }.source_code().is_none());
assert!(Enum::Variant1 {
src: Some("".to_string())
}
.source_code()
.is_some());
assert!(Enum::Variant2 { src: None }.source_code().is_none());
assert!(Enum::Variant2 {
src: Some("".to_string())
}
.source_code()
.is_some());
}

View File

@ -34,6 +34,190 @@ fn fmt_report(diag: Report) -> String {
out
}
fn fmt_report_with_settings(
diag: Report,
with_settings: fn(GraphicalReportHandler) -> GraphicalReportHandler,
) -> String {
let mut out = String::new();
let handler = with_settings(GraphicalReportHandler::new_themed(
GraphicalTheme::unicode_nocolor(),
));
handler.render_report(&mut out, diag.as_ref()).unwrap();
println!("Error:\n```\n{}\n```", out);
out
}
#[test]
fn word_wrap_options() -> Result<(), MietteError> {
// By default, a long word should not break
let out =
fmt_report_with_settings(Report::msg("abcdefghijklmnopqrstuvwxyz"), |handler| handler);
let expected = " × abcdefghijklmnopqrstuvwxyz\n".to_string();
assert_eq!(expected, out);
// A long word can break with a smaller width
let out = fmt_report_with_settings(Report::msg("abcdefghijklmnopqrstuvwxyz"), |handler| {
handler.with_width(10)
});
let expected = r#" × abcd
efgh
ijkl
mnop
qrst
uvwx
yz
"#
.to_string();
assert_eq!(expected, out);
// Unless, word breaking is disabled
let out = fmt_report_with_settings(Report::msg("abcdefghijklmnopqrstuvwxyz"), |handler| {
handler.with_width(10).with_break_words(false)
});
let expected = " × abcdefghijklmnopqrstuvwxyz\n".to_string();
assert_eq!(expected, out);
// Breaks should start at the boundary of each word if possible
let out = fmt_report_with_settings(
Report::msg("12 123 1234 12345 123456 1234567 1234567890"),
|handler| handler.with_width(10),
);
let expected = r#" × 12
123
1234
1234
5
1234
56
1234
567
1234
5678
90
"#
.to_string();
assert_eq!(expected, out);
// But long words should not break if word breaking is disabled
let out = fmt_report_with_settings(
Report::msg("12 123 1234 12345 123456 1234567 1234567890"),
|handler| handler.with_width(10).with_break_words(false),
);
let expected = r#" × 12
123
1234
12345
123456
1234567
1234567890
"#
.to_string();
assert_eq!(expected, out);
// Unless, of course, there are hyphens
let out = fmt_report_with_settings(
Report::msg("a-b a-b-c a-b-c-d a-b-c-d-e a-b-c-d-e-f a-b-c-d-e-f-g a-b-c-d-e-f-g-h"),
|handler| handler.with_width(10).with_break_words(false),
);
let expected = r#" × a-b
a-b-
c a-
b-c-
d a-
b-c-
d-e
a-b-
c-d-
e-f
a-b-
c-d-
e-f-
g a-
b-c-
d-e-
f-g-
h
"#
.to_string();
assert_eq!(expected, out);
// Which requires an additional opt-out
let out = fmt_report_with_settings(
Report::msg("a-b a-b-c a-b-c-d a-b-c-d-e a-b-c-d-e-f a-b-c-d-e-f-g a-b-c-d-e-f-g-h"),
|handler| {
handler
.with_width(10)
.with_break_words(false)
.with_word_splitter(textwrap::WordSplitter::NoHyphenation)
},
);
let expected = r#" × a-b
a-b-c
a-b-c-d
a-b-c-d-e
a-b-c-d-e-f
a-b-c-d-e-f-g
a-b-c-d-e-f-g-h
"#
.to_string();
assert_eq!(expected, out);
// Or if there are _other_ unicode word boundaries
let out = fmt_report_with_settings(
Report::msg("a/b a/b/c a/b/c/d a/b/c/d/e a/b/c/d/e/f a/b/c/d/e/f/g a/b/c/d/e/f/g/h"),
|handler| handler.with_width(10).with_break_words(false),
);
let expected = r#" × a/b
a/b/
c a/
b/c/
d a/
b/c/
d/e
a/b/
c/d/
e/f
a/b/
c/d/
e/f/
g a/
b/c/
d/e/
f/g/
h
"#
.to_string();
assert_eq!(expected, out);
// Such things require you to opt-in to only breaking on ASCII whitespace
let out = fmt_report_with_settings(
Report::msg("a/b a/b/c a/b/c/d a/b/c/d/e a/b/c/d/e/f a/b/c/d/e/f/g a/b/c/d/e/f/g/h"),
|handler| {
handler
.with_width(10)
.with_break_words(false)
.with_word_separator(textwrap::WordSeparator::AsciiSpace)
},
);
let expected = r#" × a/b
a/b/c
a/b/c/d
a/b/c/d/e
a/b/c/d/e/f
a/b/c/d/e/f/g
a/b/c/d/e/f/g/h
"#
.to_string();
assert_eq!(expected, out);
Ok(())
}
#[test]
fn empty_source() -> Result<(), MietteError> {
#[derive(Debug, Diagnostic, Error)]
@ -67,6 +251,52 @@ fn empty_source() -> Result<(), MietteError> {
Ok(())
}
#[test]
fn multiple_spans_multiline() {
#[derive(Error, Debug, Diagnostic)]
#[error("oops!")]
#[diagnostic(severity(Error))]
struct MyBad {
#[source_code]
src: NamedSource,
#[label("big")]
big: SourceSpan,
#[label("small")]
small: SourceSpan,
}
let err = MyBad {
src: NamedSource::new(
"issue",
"\
if true {
a
} else {
b
}",
),
big: (0, 32).into(),
small: (14, 1).into(),
};
let out = fmt_report(err.into());
println!("Error: {}", out);
let expected = r#" × oops!
[issue:1:1]
1 if true {
2 a
·
· small
3 } else {
4 b
5 }
· big
"#
.to_string();
assert_eq!(expected, out);
}
#[test]
fn single_line_highlight_span_full_line() {
#[derive(Error, Debug, Diagnostic)]
@ -85,9 +315,8 @@ fn single_line_highlight_span_full_line() {
let out = fmt_report(err.into());
println!("Error: {}", out);
let expected = r#"
× oops!
[issue:1:1]
let expected = r#" × oops!
[issue:2:1]
1 source
2 text
·
@ -121,7 +350,7 @@ fn single_line_with_wide_char() -> Result<(), MietteError> {
let expected = r#"oops::my::bad
× oops!
[bad_file.rs:1:1]
[bad_file.rs:2:7]
1 source
2 👼🏼text
·
@ -160,7 +389,7 @@ fn single_line_with_two_tabs() -> Result<(), MietteError> {
let expected = r#"oops::my::bad
× oops!
[bad_file.rs:1:1]
[bad_file.rs:2:3]
1 source
2 text
·
@ -199,7 +428,7 @@ fn single_line_with_tab_in_middle() -> Result<(), MietteError> {
let expected = r#"oops::my::bad
× oops!
[bad_file.rs:1:1]
[bad_file.rs:2:8]
1 source
2 text = text
·
@ -236,7 +465,7 @@ fn single_line_highlight() -> Result<(), MietteError> {
let expected = r#"oops::my::bad
× oops!
[bad_file.rs:1:1]
[bad_file.rs:2:3]
1 source
2 text
·
@ -271,7 +500,7 @@ fn external_source() -> Result<(), MietteError> {
let expected = r#"oops::my::bad
× oops!
[bad_file.rs:1:1]
[bad_file.rs:2:3]
1 source
2 text
·
@ -344,7 +573,7 @@ fn single_line_highlight_offset_end_of_line() -> Result<(), MietteError> {
let expected = r#"oops::my::bad
× oops!
[bad_file.rs:1:1]
[bad_file.rs:1:7]
1 source
·
· this bit here
@ -380,7 +609,7 @@ fn single_line_highlight_include_end_of_line() -> Result<(), MietteError> {
let expected = r#"oops::my::bad
× oops!
[bad_file.rs:1:1]
[bad_file.rs:2:3]
1 source
2 text
·
@ -417,7 +646,7 @@ fn single_line_highlight_include_end_of_line_crlf() -> Result<(), MietteError> {
let expected = r#"oops::my::bad
× oops!
[bad_file.rs:1:1]
[bad_file.rs:2:3]
1 source
2 text
·
@ -454,7 +683,7 @@ fn single_line_highlight_with_empty_span() -> Result<(), MietteError> {
let expected = r#"oops::my::bad
× oops!
[bad_file.rs:1:1]
[bad_file.rs:2:3]
1 source
2 text
·
@ -491,7 +720,7 @@ fn single_line_highlight_no_label() -> Result<(), MietteError> {
let expected = r#"oops::my::bad
× oops!
[bad_file.rs:1:1]
[bad_file.rs:2:3]
1 source
2 text
·
@ -527,7 +756,7 @@ fn single_line_highlight_at_line_start() -> Result<(), MietteError> {
let expected = r#"oops::my::bad
× oops!
[bad_file.rs:1:1]
[bad_file.rs:2:1]
1 source
2 text
·
@ -542,6 +771,94 @@ fn single_line_highlight_at_line_start() -> Result<(), MietteError> {
Ok(())
}
#[test]
fn multiline_label() -> Result<(), MietteError> {
#[derive(Debug, Diagnostic, Error)]
#[error("oops!")]
#[diagnostic(code(oops::my::bad), help("try doing it better next time?"))]
struct MyBad {
#[source_code]
src: NamedSource,
#[label("this bit here\nand\nthis\ntoo")]
highlight: SourceSpan,
}
let src = "source\ntext\n here".to_string();
let err = MyBad {
src: NamedSource::new("bad_file.rs", src),
highlight: (7, 4).into(),
};
let out = fmt_report(err.into());
println!("Error: {}", out);
let expected = r#"oops::my::bad
× oops!
[bad_file.rs:2:1]
1 source
2 text
·
· this bit here
· and
· this
· too
3 here
help: try doing it better next time?
"#
.trim_start()
.to_string();
assert_eq!(expected, out);
Ok(())
}
#[test]
fn multiple_multi_line_labels() -> Result<(), MietteError> {
#[derive(Debug, Diagnostic, Error)]
#[error("oops!")]
#[diagnostic(code(oops::my::bad), help("try doing it better next time?"))]
struct MyBad {
#[source_code]
src: NamedSource,
#[label = "x\ny"]
highlight1: SourceSpan,
#[label = "z\nw"]
highlight2: SourceSpan,
#[label = "a\nb"]
highlight3: SourceSpan,
}
let src = "source\n text text text text text\n here".to_string();
let err = MyBad {
src: NamedSource::new("bad_file.rs", src),
highlight1: (9, 4).into(),
highlight2: (14, 4).into(),
highlight3: (24, 4).into(),
};
let out = fmt_report(err.into());
println!("Error: {}", out);
let expected = r#"oops::my::bad
× oops!
[bad_file.rs:2:3]
1 source
2 text text text text text
·
· a
· b
· z
· w
· x
· y
3 here
help: try doing it better next time?
"#
.trim_start()
.to_string();
assert_eq!(expected, out);
Ok(())
}
#[test]
fn multiple_same_line_highlights() -> Result<(), MietteError> {
#[derive(Debug, Diagnostic, Error)]
@ -570,7 +887,7 @@ fn multiple_same_line_highlights() -> Result<(), MietteError> {
let expected = r#"oops::my::bad
× oops!
[bad_file.rs:1:1]
[bad_file.rs:2:3]
1 source
2 text text text text text
·
@ -617,7 +934,7 @@ fn multiple_same_line_highlights_with_tabs_in_middle() -> Result<(), MietteError
let expected = r#"oops::my::bad
× oops!
[bad_file.rs:1:1]
[bad_file.rs:2:3]
1 source
2 text text text text text
·
@ -656,7 +973,7 @@ fn multiline_highlight_adjacent() -> Result<(), MietteError> {
let expected = r#"oops::my::bad
× oops!
[bad_file.rs:1:1]
[bad_file.rs:2:3]
1 source
2 text
3 here
@ -670,6 +987,43 @@ fn multiline_highlight_adjacent() -> Result<(), MietteError> {
Ok(())
}
#[test]
fn multiline_highlight_multiline_label() -> Result<(), MietteError> {
#[derive(Debug, Diagnostic, Error)]
#[error("oops!")]
#[diagnostic(code(oops::my::bad), help("try doing it better next time?"))]
struct MyBad {
#[source_code]
src: NamedSource,
#[label = "these two lines\nare the problem"]
highlight: SourceSpan,
}
let src = "source\n text\n here".to_string();
let err = MyBad {
src: NamedSource::new("bad_file.rs", src),
highlight: (9, 11).into(),
};
let out = fmt_report(err.into());
println!("Error: {}", out);
let expected = r#"oops::my::bad
× oops!
[bad_file.rs:2:3]
1 source
2 text
3 here
· these two lines
· are the problem
help: try doing it better next time?
"#
.trim_start()
.to_string();
assert_eq!(expected, out);
Ok(())
}
#[test]
fn multiline_highlight_flyby() -> Result<(), MietteError> {
#[derive(Debug, Diagnostic, Error)]
@ -970,7 +1324,7 @@ fn related() -> Result<(), MietteError> {
let expected = r#"oops::my::bad
× oops!
[bad_file.rs:1:1]
[bad_file.rs:2:3]
1 source
2 text
·
@ -1032,7 +1386,7 @@ fn related_source_code_propagation() -> Result<(), MietteError> {
let expected = r#"oops::my::bad
× oops!
[bad_file.rs:1:1]
[bad_file.rs:2:3]
1 source
2 text
·
@ -1137,7 +1491,7 @@ fn related_severity() -> Result<(), MietteError> {
let expected = r#"oops::my::bad
× oops!
[bad_file.rs:1:1]
[bad_file.rs:2:3]
1 source
2 text
·
@ -1201,9 +1555,8 @@ fn zero_length_eol_span() {
let out = fmt_report(err.into());
println!("Error: {}", out);
let expected = r#"
× oops!
[issue:1:1]
let expected = r#" × oops!
[issue:2:1]
1 this is the first line
2 this is the second line
·
@ -1214,3 +1567,149 @@ fn zero_length_eol_span() {
assert_eq!(expected, out);
}
#[test]
fn primary_label() {
#[derive(Error, Debug, Diagnostic)]
#[error("oops!")]
struct MyBad {
#[source_code]
src: NamedSource,
#[label]
first_label: SourceSpan,
#[label(primary, "nope")]
second_label: SourceSpan,
}
let err = MyBad {
src: NamedSource::new("issue", "this is the first line\nthis is the second line"),
first_label: (2, 4).into(),
second_label: (24, 4).into(),
};
let out = fmt_report(err.into());
println!("Error: {}", out);
// line 2 should be the primary, not line 1
let expected = r#" × oops!
[issue:2:2]
1 this is the first line
·
2 this is the second line
·
· nope
"#
.to_string();
assert_eq!(expected, out);
}
#[test]
fn single_line_with_wide_char_unaligned_span_start() -> Result<(), MietteError> {
#[derive(Debug, Diagnostic, Error)]
#[error("oops!")]
#[diagnostic(code(oops::my::bad), help("try doing it better next time?"))]
struct MyBad {
#[source_code]
src: NamedSource,
#[label("this bit here")]
highlight: SourceSpan,
}
let src = "source\n 👼🏼text\n here".to_string();
let err = MyBad {
src: NamedSource::new("bad_file.rs", src),
highlight: (10, 5).into(),
};
let out = fmt_report(err.into());
println!("Error: {}", out);
let expected = r#"oops::my::bad
× oops!
[bad_file.rs:2:4]
1 source
2 👼🏼text
·
· this bit here
3 here
help: try doing it better next time?
"#
.trim_start()
.to_string();
assert_eq!(expected, out);
Ok(())
}
#[test]
fn single_line_with_wide_char_unaligned_span_end() -> Result<(), MietteError> {
#[derive(Debug, Diagnostic, Error)]
#[error("oops!")]
#[diagnostic(code(oops::my::bad), help("try doing it better next time?"))]
struct MyBad {
#[source_code]
src: NamedSource,
#[label("this bit here")]
highlight: SourceSpan,
}
let src = "source\n text 👼🏼\n here".to_string();
let err = MyBad {
src: NamedSource::new("bad_file.rs", src),
highlight: (9, 6).into(),
};
let out = fmt_report(err.into());
println!("Error: {}", out);
let expected = r#"oops::my::bad
× oops!
[bad_file.rs:2:3]
1 source
2 text 👼🏼
·
· this bit here
3 here
help: try doing it better next time?
"#
.trim_start()
.to_string();
assert_eq!(expected, out);
Ok(())
}
#[test]
fn single_line_with_wide_char_unaligned_span_empty() -> Result<(), MietteError> {
#[derive(Debug, Diagnostic, Error)]
#[error("oops!")]
#[diagnostic(code(oops::my::bad), help("try doing it better next time?"))]
struct MyBad {
#[source_code]
src: NamedSource,
#[label("this bit here")]
highlight: SourceSpan,
}
let src = "source\n 👼🏼text\n here".to_string();
let err = MyBad {
src: NamedSource::new("bad_file.rs", src),
highlight: (10, 0).into(),
};
let out = fmt_report(err.into());
println!("Error: {}", out);
let expected = r#"oops::my::bad
× oops!
[bad_file.rs:2:4]
1 source
2 👼🏼text
·
· this bit here
3 here
help: try doing it better next time?
"#
.trim_start()
.to_string();
assert_eq!(expected, out);
Ok(())
}

View File

@ -0,0 +1,69 @@
use std::sync::Arc;
use miette::{miette, Diagnostic};
use thiserror::Error;
#[test]
fn test_source() {
#[derive(Debug, Diagnostic, Error)]
#[error("Bar")]
struct Bar;
#[derive(Debug, Diagnostic, Error)]
#[error("Foo")]
struct Foo {
#[source]
bar: Bar,
}
let e = miette!(Foo { bar: Bar });
let mut chain = e.chain();
assert_eq!("Foo", chain.next().unwrap().to_string());
assert_eq!("Bar", chain.next().unwrap().to_string());
assert!(chain.next().is_none());
}
#[test]
fn test_source_boxed() {
#[derive(Debug, Diagnostic, Error)]
#[error("Bar")]
struct Bar;
#[derive(Debug, Diagnostic, Error)]
#[error("Foo")]
struct Foo {
#[source]
bar: Box<dyn Diagnostic + Send + Sync>,
}
let error = miette!(Foo { bar: Box::new(Bar) });
let mut chain = error.chain();
assert_eq!("Foo", chain.next().unwrap().to_string());
assert_eq!("Bar", chain.next().unwrap().to_string());
assert!(chain.next().is_none());
}
#[test]
fn test_source_arc() {
#[derive(Debug, Diagnostic, Error)]
#[error("Bar")]
struct Bar;
#[derive(Debug, Diagnostic, Error)]
#[error("Foo")]
struct Foo {
#[source]
bar: Arc<dyn Diagnostic + Send + Sync>,
}
let error = miette!(Foo { bar: Arc::new(Bar) });
let mut chain = error.chain();
assert_eq!("Foo", chain.next().unwrap().to_string());
assert_eq!("Bar", chain.next().unwrap().to_string());
assert!(chain.next().is_none());
}

View File

@ -1,5 +1,16 @@
use miette::Diagnostic;
#[derive(Debug, miette::Diagnostic, thiserror::Error)]
#[error("A complex error happened")]
struct SourceError {
#[source_code]
code: String,
#[help]
help: String,
#[label("here")]
label: (usize, usize),
}
#[derive(Debug, miette::Diagnostic, thiserror::Error)]
#[error("AnErr")]
struct AnErr;
@ -8,7 +19,7 @@ struct AnErr;
#[error("TestError")]
struct TestStructError {
#[diagnostic_source]
asdf_inner_foo: AnErr,
asdf_inner_foo: SourceError,
}
#[derive(Debug, miette::Diagnostic, thiserror::Error)]
@ -37,7 +48,11 @@ struct TestArcedError(#[diagnostic_source] std::sync::Arc<dyn Diagnostic>);
#[test]
fn test_diagnostic_source() {
let error = TestStructError {
asdf_inner_foo: AnErr,
asdf_inner_foo: SourceError {
code: String::new(),
help: String::new(),
label: (0, 0),
},
};
assert!(error.diagnostic_source().is_some());
@ -59,3 +74,123 @@ fn test_diagnostic_source() {
let error = TestArcedError(std::sync::Arc::new(AnErr));
assert!(error.diagnostic_source().is_some());
}
#[cfg(feature = "fancy-no-backtrace")]
#[test]
fn test_diagnostic_source_pass_extra_info() {
let diag = TestBoxedError(Box::new(SourceError {
code: String::from("Hello\nWorld!"),
help: String::from("Have you tried turning it on and off again?"),
label: (1, 4),
}));
let mut out = String::new();
miette::GraphicalReportHandler::new_themed(miette::GraphicalTheme::unicode_nocolor())
.with_width(80)
.with_footer("this is a footer".into())
.render_report(&mut out, &diag)
.unwrap();
println!("Error: {}", out);
let expected = r#" × TestError
× A complex error happened
[1:2]
1 Hello
·
· here
2 World!
help: Have you tried turning it on and off again?
this is a footer
"#
.to_string();
assert_eq!(expected, out);
}
#[cfg(feature = "fancy-no-backtrace")]
#[test]
fn test_diagnostic_source_is_output() {
let diag = TestStructError {
asdf_inner_foo: SourceError {
code: String::from("right here"),
help: String::from("That's where the error is!"),
label: (6, 4),
},
};
let mut out = String::new();
miette::GraphicalReportHandler::new_themed(miette::GraphicalTheme::unicode_nocolor())
.with_width(80)
.render_report(&mut out, &diag)
.unwrap();
println!("{}", out);
let expected = r#" × TestError
× A complex error happened
1 right here
·
· here
help: That's where the error is!
"#;
assert_eq!(expected, out);
}
#[derive(Debug, miette::Diagnostic, thiserror::Error)]
#[error("A nested error happened")]
struct NestedError {
#[source_code]
code: String,
#[label("here")]
label: (usize, usize),
#[diagnostic_source]
the_other_err: Box<dyn Diagnostic>,
}
#[cfg(feature = "fancy-no-backtrace")]
#[test]
fn test_nested_diagnostic_source_is_output() {
let inner_error = TestStructError {
asdf_inner_foo: SourceError {
code: String::from("This is another error"),
help: String::from("You should fix this"),
label: (3, 4),
},
};
let diag = NestedError {
code: String::from("right here"),
label: (6, 4),
the_other_err: Box::new(inner_error),
};
let mut out = String::new();
miette::GraphicalReportHandler::new_themed(miette::GraphicalTheme::unicode_nocolor())
.with_width(80)
.with_footer("Yooo, a footer".to_string())
.render_report(&mut out, &diag)
.unwrap();
println!("{}", out);
let expected = r#" × A nested error happened
× TestError
× A complex error happened
1 This is another error
·
· here
help: You should fix this
1 right here
·
· here
Yooo, a footer
"#;
assert_eq!(expected, out);
}

View File

@ -52,7 +52,6 @@ mod json_report_handler {
"related": []
}"#
.lines()
.into_iter()
.map(|s| s.trim_matches(|c| c == ' ' || c == '\n'))
.collect();
assert_eq!(expected, out);
@ -98,7 +97,6 @@ mod json_report_handler {
"related": []
}"#
.lines()
.into_iter()
.map(|s| s.trim_matches(|c| c == ' ' || c == '\n'))
.collect();
assert_eq!(expected, out);
@ -144,7 +142,6 @@ mod json_report_handler {
"related": []
}"#
.lines()
.into_iter()
.map(|s| s.trim_matches(|c| c == ' ' || c == '\n'))
.collect();
assert_eq!(expected, out);
@ -190,7 +187,6 @@ mod json_report_handler {
"related": []
}"#
.lines()
.into_iter()
.map(|s| s.trim_matches(|c| c == ' ' || c == '\n'))
.collect();
assert_eq!(expected, out);
@ -235,7 +231,6 @@ mod json_report_handler {
"related": []
}"#
.lines()
.into_iter()
.map(|s| s.trim_matches(|c| c == ' ' || c == '\n'))
.collect();
assert_eq!(expected, out);
@ -281,7 +276,6 @@ mod json_report_handler {
"related": []
}"#
.lines()
.into_iter()
.map(|s| s.trim_matches(|c| c == ' ' || c == '\n'))
.collect();
assert_eq!(expected, out);
@ -347,7 +341,6 @@ mod json_report_handler {
"related": []
}"#
.lines()
.into_iter()
.map(|s| s.trim_matches(|c| c == ' ' || c == '\n'))
.collect();
assert_eq!(expected, out);
@ -393,7 +386,6 @@ mod json_report_handler {
"related": []
}"#
.lines()
.into_iter()
.map(|s| s.trim_matches(|c| c == ' ' || c == '\n'))
.collect();
assert_eq!(expected, out);
@ -456,7 +448,6 @@ mod json_report_handler {
"related": []
}"#
.lines()
.into_iter()
.map(|s| s.trim_matches(|c| c == ' ' || c == '\n'))
.collect();
assert_eq!(expected, out);
@ -532,7 +523,6 @@ mod json_report_handler {
"related": []
}"#
.lines()
.into_iter()
.map(|s| s.trim_matches(|c| c == ' ' || c == '\n'))
.collect();
assert_eq!(expected, out);
@ -588,7 +578,6 @@ mod json_report_handler {
"related": []
}"#
.lines()
.into_iter()
.map(|s| s.trim_matches(|c| c == ' ' || c == '\n'))
.collect();
assert_eq!(expected, out);
@ -644,7 +633,6 @@ mod json_report_handler {
"related": []
}"#
.lines()
.into_iter()
.map(|s| s.trim_matches(|c| c == ' ' || c == '\n'))
.collect();
assert_eq!(expected, out);
@ -700,7 +688,6 @@ mod json_report_handler {
"related": []
}"#
.lines()
.into_iter()
.map(|s| s.trim_matches(|c| c == ' ' || c == '\n'))
.collect();
assert_eq!(expected, out);
@ -728,7 +715,6 @@ mod json_report_handler {
"related": []
}"#
.lines()
.into_iter()
.map(|s| s.trim_matches(|c| c == ' ' || c == '\n'))
.collect();
assert_eq!(expected, out);
@ -822,7 +808,6 @@ mod json_report_handler {
}]
}"#
.lines()
.into_iter()
.map(|s| s.trim_matches(|c| c == ' ' || c == '\n'))
.collect();
assert_eq!(expected, out);
@ -920,7 +905,6 @@ mod json_report_handler {
}]
}"#
.lines()
.into_iter()
.map(|s| s.trim_matches(|c| c == ' ' || c == '\n'))
.collect();
assert_eq!(expected, out);