diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 4fafac3..912bfcc 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -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
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e3542f2..955c27d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,25 @@
# `miette` Release Changelog
+
+## 5.10.0 (2023-07-16)
+
+### Features
+
+* **protocol:** add StdError impl for Box (#273) ([2e3e5c9d](https://github.com/zkat/miette/commit/2e3e5c9d15e234495369e9b47d032644dd5664ad))
+
+
+## 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))
+
## 5.8.0 (2023-04-18)
diff --git a/Cargo.toml b/Cargo.toml
index 96b9c9f..f8c2cb5 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "miette"
-version = "5.8.0"
+version = "5.10.0"
authors = ["Kat Marchán "]
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",
diff --git a/README.md b/README.md
index ff25b3a..5f79c52 100644
--- a/README.md
+++ b/README.md
@@ -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
+### Table of Contents
- [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 {
}
```
-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 {
.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 = "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
diff --git a/README.tpl b/README.tpl
index d598eb8..f1126f1 100644
--- a/README.tpl
+++ b/README.tpl
@@ -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
diff --git a/miette-derive/Cargo.toml b/miette-derive/Cargo.toml
index ee79d66..b5a7032 100644
--- a/miette-derive/Cargo.toml
+++ b/miette-derive/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "miette-derive"
-version = "5.8.0"
+version = "5.10.0"
authors = ["Kat Marchán "]
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"
diff --git a/miette-derive/src/label.rs b/miette-derive/src/label.rs
index e0bc70a..dd5ec69 100644
--- a/miette-derive/src/label.rs
+++ b/miette-derive/src/label.rs
@@ -20,10 +20,12 @@ struct Label {
label: Option,
ty: syn::Type,
span: syn::Member,
+ primary: bool,
}
struct LabelAttr {
label: Option,
+ 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::();
+ 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::()?;
- 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::(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 {
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(),
))
diff --git a/miette-derive/src/source_code.rs b/miette-derive/src/source_code.rs
index 62f28e7..e1b85ab 100644
--- a/miette-derive/src/source_code.rs
+++ b/miette-derive/src/source_code.rs
@@ -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 {
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,
}),
}
})
diff --git a/src/error.rs b/src/error.rs
index 56041ca..4e57a78 100644
--- a/src/error.rs
+++ b/src/error.rs
@@ -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> {
+ 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> {
+ 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> {
+ 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,
+ )))
+ }
+}
diff --git a/src/eyreish/error.rs b/src/eyreish/error.rs
index 6b0dc34..677f368 100644
--- a/src/eyreish/error.rs
+++ b/src/eyreish/error.rs
@@ -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(self, msg: D) -> Self
where
D: Display + Send + Sync + 'static,
diff --git a/src/eyreish/macros.rs b/src/eyreish/macros.rs
index 938ac32..e13309f 100644
--- a/src/eyreish/macros.rs
+++ b/src/eyreish/macros.rs
@@ -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)*) => {{
diff --git a/src/eyreish/ptr.rs b/src/eyreish/ptr.rs
index c8d63d2..fa954d1 100644
--- a/src/eyreish/ptr.rs
+++ b/src/eyreish/ptr.rs
@@ -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) -> Self {
+ pub(crate) const fn from_raw(ptr: NonNull) -> 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,
diff --git a/src/handler.rs b/src/handler.rs
index e983a55..e32f3ef 100644
--- a/src/handler.rs
+++ b/src/handler.rs
@@ -55,6 +55,9 @@ pub struct MietteHandlerOpts {
pub(crate) context_lines: Option,
pub(crate) tab_width: Option,
pub(crate) with_cause_chain: Option,
+ pub(crate) break_words: Option,
+ pub(crate) word_separator: Option,
+ pub(crate) word_splitter: Option,
}
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),
}
diff --git a/src/handlers/debug.rs b/src/handlers/debug.rs
index a9460d1..50450a4 100644
--- a/src/handlers/debug.rs
+++ b/src/handlers/debug.rs
@@ -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
}
}
diff --git a/src/handlers/graphical.rs b/src/handlers/graphical.rs
index 1ab09fa..baadfb3 100644
--- a/src/handlers/graphical.rs
+++ b/src/handlers/graphical.rs
@@ -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,
+ pub(crate) word_splitter: Option,
}
#[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,
+ /// 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>,
span: SourceSpan,
style: Style,
}
@@ -880,9 +1184,17 @@ impl PartialEq for FancySpan {
}
}
+fn split_label(v: String) -> Vec {
+ v.split('\n').map(|i| i.to_string()).collect()
+}
+
impl FancySpan {
fn new(label: Option, 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 {
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> {
+ self.label.as_ref().map(|l| {
+ l.iter()
+ .map(|i| i.style(self.style()).to_string())
+ .collect()
+ })
}
fn offset(&self) -> usize {
diff --git a/src/handlers/json.rs b/src/handlers/json.rs
index ec214ca..0b4a405 100644
--- a/src/handlers/json.rs
+++ b/src/handlers/json.rs
@@ -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": [],"#)?;
}
diff --git a/src/handlers/narratable.rs b/src/handlers/narratable.rs
index 5af4b97..c809124 100644
--- a/src/handlers/narratable.rs
+++ b/src/handlers/narratable.rs
@@ -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
}
diff --git a/src/handlers/theme.rs b/src/handlers/theme.rs
index 1f5236a..aa6649e 100644
--- a/src/handlers/theme.rs
+++ b/src/handlers/theme.rs
@@ -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 {
diff --git a/src/lib.rs b/src/lib.rs
index cc113ce..d80efa7 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -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 = "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::*;
diff --git a/src/miette_diagnostic.rs b/src/miette_diagnostic.rs
index 67b75d0..9863e88 100644
--- a/src/miette_diagnostic.rs
+++ b/src/miette_diagnostic.rs
@@ -252,7 +252,7 @@ impl MietteDiagnostic {
/// ```
pub fn and_labels(mut self, labels: impl IntoIterator- ) -> 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
}
]
});
diff --git a/src/protocol.rs b/src/protocol.rs
index b0177c6..b403494 100644
--- a/src/protocol.rs
+++ b/src/protocol.rs
@@ -69,16 +69,28 @@ pub trait Diagnostic: std::error::Error {
}
}
-impl std::error::Error for Box {
- 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,
+ Box,
+ Box
+}
+
impl From
for Box
{
@@ -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,
span: SourceSpan,
+ primary: bool,
}
impl LabeledSpan {
/// Makes a new labeled span.
- pub fn new(label: Option, offset: ByteOffset, len: usize) -> Self {
+ pub const fn new(label: Option, 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, span: impl Into) -> 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));
}
diff --git a/tests/derive.rs b/tests/derive.rs
index 5715823..ac29eee 100644
--- a/tests/derive.rs
+++ b/tests/derive.rs
@@ -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,
+ }
+ 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,
+ },
+ #[error("variant2 with optional source")]
+ Variant2 {
+ #[source_code]
+ src: Option,
+ },
+ }
+ 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());
}
diff --git a/tests/graphical.rs b/tests/graphical.rs
index f929884..f59f849 100644
--- a/tests/graphical.rs
+++ b/tests/graphical.rs
@@ -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(())
+}
diff --git a/tests/test_derive_source_chain.rs b/tests/test_derive_source_chain.rs
new file mode 100644
index 0000000..fb7ddb2
--- /dev/null
+++ b/tests/test_derive_source_chain.rs
@@ -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,
+ }
+
+ 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,
+ }
+
+ 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());
+}
diff --git a/tests/test_diagnostic_source_macro.rs b/tests/test_diagnostic_source_macro.rs
index 8af2e88..334ca34 100644
--- a/tests/test_diagnostic_source_macro.rs
+++ b/tests/test_diagnostic_source_macro.rs
@@ -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);
#[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,
+}
+
+#[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);
+}
diff --git a/tests/test_json.rs b/tests/test_json.rs
index 5bd14cb..ae482b8 100644
--- a/tests/test_json.rs
+++ b/tests/test_json.rs
@@ -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);