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 name: Check fmt & build docs
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v1 - uses: actions/checkout@v4
- name: Install Rust - name: Install Rust
uses: actions-rs/toolchain@v1 uses: dtolnay/rust-toolchain@master
with: with:
profile: minimal
toolchain: stable toolchain: stable
components: rustfmt components: rustfmt
override: true
- name: rustfmt - name: rustfmt
run: cargo fmt --all -- --check run: cargo fmt --all -- --check
- name: docs - name: docs
@ -32,14 +30,15 @@ jobs:
os: [ubuntu-latest, macOS-latest, windows-latest] os: [ubuntu-latest, macOS-latest, windows-latest]
steps: steps:
- uses: actions/checkout@v1 - uses: actions/checkout@v4
- name: Install Rust - name: Install Rust
uses: actions-rs/toolchain@v1 uses: dtolnay/rust-toolchain@master
with: with:
profile: minimal
toolchain: ${{ matrix.rust }} toolchain: ${{ matrix.rust }}
components: clippy 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 - name: Clippy
run: cargo clippy --all -- -D warnings run: cargo clippy --all -- -D warnings
- name: Run tests - name: Run tests
@ -54,14 +53,12 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v1 - uses: actions/checkout@v4
- name: Install Rust - name: Install Rust
uses: actions-rs/toolchain@v1 uses: dtolnay/rust-toolchain@master
with: with:
profile: minimal
toolchain: nightly toolchain: nightly
components: miri,rust-src components: miri,rust-src
override: true
- name: Run tests with miri - name: Run tests with miri
env: env:
MIRIFLAGS: -Zmiri-disable-isolation -Zmiri-strict-provenance MIRIFLAGS: -Zmiri-disable-isolation -Zmiri-strict-provenance
@ -75,13 +72,11 @@ jobs:
os: [ubuntu-latest, macOS-latest, windows-latest] os: [ubuntu-latest, macOS-latest, windows-latest]
steps: steps:
- uses: actions/checkout@v1 - uses: actions/checkout@v4
- name: Install Rust - name: Install Rust
uses: actions-rs/toolchain@v1 uses: dtolnay/rust-toolchain@master
with: with:
profile: minimal
toolchain: nightly toolchain: nightly
override: true
- name: Run minimal version build - name: Run minimal version build
run: cargo build -Z minimal-versions --all-features run: cargo build -Z minimal-versions --all-features

View File

@ -1,5 +1,25 @@
# `miette` Release Changelog # `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> <a name="5.8.0"></a>
## 5.8.0 (2023-04-18) ## 5.8.0 (2023-04-18)

View File

@ -1,6 +1,6 @@
[package] [package]
name = "miette" name = "miette"
version = "5.8.0" version = "5.10.0"
authors = ["Kat Marchán <kzm@zkat.tech>"] authors = ["Kat Marchán <kzm@zkat.tech>"]
description = "Fancy diagnostic reporting library and protocol for us mere mortals who aren't compiler hackers." description = "Fancy diagnostic reporting library and protocol for us mere mortals who aren't compiler hackers."
categories = ["rust-patterns"] categories = ["rust-patterns"]
@ -14,7 +14,7 @@ exclude = ["images/", "tests/", "miette-derive/"]
[dependencies] [dependencies]
thiserror = "1.0.40" 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" once_cell = "1.8.0"
unicode-width = "0.1.9" unicode-width = "0.1.9"
@ -44,7 +44,8 @@ lazy_static = "1.4"
serde_json = "1.0.64" serde_json = "1.0.64"
[features] [features]
default = [] default = ["derive"]
derive = ["miette-derive"]
no-format-args-capture = [] no-format-args-capture = []
fancy-no-backtrace = [ fancy-no-backtrace = [
"owo-colors", "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 You run miette? You run her code like the software? Oh. Oh! Error code for
coder! Error code for One Thousand Lines! coder! Error code for One Thousand Lines!
## About ### About
`miette` is a diagnostic library for Rust. It includes a series of `miette` is a diagnostic library for Rust. It includes a series of
traits/protocols that allow you to hook into its error reporting facilities, 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 toplevel crate, as the fancy feature pulls in a number of dependencies that
libraries and such might not want. libraries and such might not want.
## Table of Contents <!-- omit in toc --> ### Table of Contents <!-- omit in toc -->
- [About](#about) - [About](#about)
- [Features](#features) - [Features](#features)
@ -51,7 +51,7 @@ libraries and such might not want.
- [Acknowledgements](#acknowledgements) - [Acknowledgements](#acknowledgements)
- [License](#license) - [License](#license)
## Features ### Features
- Generic [`Diagnostic`] protocol, compatible (and dependent on) - Generic [`Diagnostic`] protocol, compatible (and dependent on)
[`std::error::Error`]. [`std::error::Error`].
@ -76,7 +76,7 @@ the following features:
- Cause chain printing - Cause chain printing
- Turns diagnostic codes into links in [supported terminals](https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda). - Turns diagnostic codes into links in [supported terminals](https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda).
## Installing ### Installing
```sh ```sh
$ cargo add miette $ cargo add miette
@ -88,7 +88,7 @@ If you want to use the fancy printer in all these screenshots:
$ cargo add miette --features fancy $ cargo add miette --features fancy
``` ```
## Example ### Example
```rust ```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`! `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; use thiserror::Error;
#[derive(Error, Debug, Diagnostic)] #[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 throughout your app (but NOT your libraries! Those should always return
concrete types!). concrete types!).
*/ */
use miette::{NamedSource, Result}; use miette::Result;
fn this_fails() -> Result<()> { fn this_fails() -> Result<()> {
// You can use plain strings as a `Source`, or anything that implements // You can use plain strings as a `Source`, or anything that implements
// the one-method `Source` trait. // 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 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"> 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 `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 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 ```rust
// lib/error.rs // lib/error.rs
use miette::Diagnostic; use miette::{Diagnostic, SourceSpan};
use thiserror::Error; use thiserror::Error;
#[derive(Error, Diagnostic, Debug)] #[derive(Error, Diagnostic, Debug)]
@ -199,6 +199,18 @@ pub enum MyLibError {
#[error("Oops it blew up")] #[error("Oops it blew up")]
#[diagnostic(code(my_lib::bad_code))] #[diagnostic(code(my_lib::bad_code))]
BadThingHappened, 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 practice to wrap any "external" error types in your error `enum` instead of
using something like [`Report`] in a library. using something like [`Report`] in a library.
### ... in application code #### ... in application code
Application code tends to work a little differently than libraries. You Application code tends to work a little differently than libraries. You
don't always need or care to define dedicated error wrappers for errors 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 ```rust
// my_app/lib/my_internal_file.rs // my_app/lib/my_internal_file.rs
use miette::{miette, IntoDiagnostic, Result, WrapErr}; use miette::{miette, IntoDiagnostic, Result, WrapErr};
@ -262,8 +273,9 @@ pub fn some_tool() -> Result<Version> {
.map_err(|_| miette!("Invalid version {}", 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 `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 `Result` as your return value, and it will pretty-print your diagnostics
@ -293,7 +305,24 @@ enabled:
miette = { version = "X.Y.Z", features = ["fancy"] } 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 `miette` supports providing a URL for individual diagnostics. This URL will
be displayed as an actual link in supported terminals, like so: be displayed as an actual link in supported terminals, like so:
@ -346,7 +375,7 @@ use thiserror::Error;
struct MyErr; struct MyErr;
``` ```
### ... snippets #### ... snippets
Along with its general error handling and reporting features, `miette` also Along with its general error handling and reporting features, `miette` also
includes facilities for adding error spans/annotations/labels to your 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: `miette` provides two facilities for supplying help text for your errors:
The first is the `#[help()]` format attribute that applies to structs or 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 `miette` supports collecting multiple errors into a single diagnostic, and
printing them all together nicely. 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. 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) 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 When one uses the `#[source]` attribute on a field, that usually comes
from `thiserror`, and implements a method for from `thiserror`, and implements a method for
@ -566,7 +595,7 @@ struct MyError {
struct OtherError; struct OtherError;
``` ```
### ... handler options #### ... handler options
[`MietteHandler`] is the default handler, and is very customizable. In [`MietteHandler`] is the default handler, and is very customizable. In
most cases, you can simply use [`MietteHandlerOpts`] to tweak its behavior most cases, you can simply use [`MietteHandlerOpts`] to tweak its behavior
@ -585,12 +614,13 @@ miette::set_hook(Box::new(|_| {
.build(), .build(),
) )
})) }))
``` ```
See the docs for [`MietteHandlerOpts`] for more details on what you can See the docs for [`MietteHandlerOpts`] for more details on what you can
customize! customize!
### ... dynamic diagnostics #### ... dynamic diagnostics
If you... If you...
- ...don't know all the possible errors upfront - ...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. [`MietteDiagnostic`] directly to create diagnostic on the fly.
```rust ```rust
let source = "2 + 2 * 2 = 8".to_string(); let source = "2 + 2 * 2 = 8".to_string();
let report = miette!( let report = miette!(
labels = vec[ labels = vec![
LabeledSpan::at(12..13, "this should be 6"), LabeledSpan::at(12..13, "this should be 6"),
], ],
help = "'*' has greater precedence than '+'", help = "'*' has greater precedence than '+'",
@ -610,26 +641,25 @@ let report = miette!(
println!("{:?}", report) println!("{:?}", report)
``` ```
## Acknowledgements ### Acknowledgements
`miette` was not developed in a void. It owes enormous credit to various `miette` was not developed in a void. It owes enormous credit to various
other projects and their authors: other projects and their authors:
- [`anyhow`](http://crates.io/crates/anyhow) and - [`anyhow`](http://crates.io/crates/anyhow) and [`color-eyre`](https://crates.io/crates/color-eyre):
[`color-eyre`](https://crates.io/crates/color-eyre): these two these two enormously influential error handling libraries have pushed
enormously influential error handling libraries have pushed forward the forward the experience of application-level error handling and error
experience of application-level error handling and error reporting. reporting. `miette`'s `Report` type is an attempt at a very very rough
`miette`'s `Report` type is an attempt at a very very rough version of version of their `Report` types.
their `Report` types. - [`thiserror`](https://crates.io/crates/thiserror) for setting the standard
- [`thiserror`](https://crates.io/crates/thiserror) for setting the for library-level error definitions, and for being the inspiration behind
standard for library-level error definitions, and for being the `miette`'s derive macro.
inspiration behind `miette`'s derive macro.
- `rustc` and [@estebank](https://github.com/estebank) for their - `rustc` and [@estebank](https://github.com/estebank) for their
state-of-the-art work in compiler diagnostics. state-of-the-art work in compiler diagnostics.
- [`ariadne`](https://crates.io/crates/ariadne) for pushing forward how - [`ariadne`](https://crates.io/crates/ariadne) for pushing forward how
_pretty_ these diagnostics can really look! _pretty_ these diagnostics can really look!
## License ### License
`miette` is released to the Rust community under the [Apache license `miette` is released to the Rust community under the [Apache license
2.0](./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 [`MietteHandler`]: https://docs.rs/miette/latest/miette/struct.MietteHandler.html
[`MietteDiagnostic`]: https://docs.rs/miette/latest/miette/struct.MietteDiagnostic.html [`MietteDiagnostic`]: https://docs.rs/miette/latest/miette/struct.MietteDiagnostic.html
[`Report`]: https://docs.rs/miette/latest/miette/struct.Report.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 [`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 [`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 [`MietteHandler`]: https://docs.rs/miette/latest/miette/struct.MietteHandler.html
[`MietteDiagnostic`]: https://docs.rs/miette/latest/miette/struct.MietteDiagnostic.html [`MietteDiagnostic`]: https://docs.rs/miette/latest/miette/struct.MietteDiagnostic.html
[`Report`]: https://docs.rs/miette/latest/miette/struct.Report.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 [`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 [`SourceSpan`]: https://docs.rs/miette/latest/miette/struct.SourceSpan.html

View File

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

View File

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

View File

@ -10,6 +10,7 @@ use crate::{
pub struct SourceCode { pub struct SourceCode {
source_code: syn::Member, source_code: syn::Member,
is_option: bool,
} }
impl SourceCode { impl SourceCode {
@ -27,6 +28,19 @@ impl SourceCode {
for (i, field) in fields.iter().enumerate() { for (i, field) in fields.iter().enumerate() {
for attr in &field.attrs { for attr in &field.attrs {
if attr.path().is_ident("source_code") { 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() { let source_code = if let Some(ident) = field.ident.clone() {
syn::Member::Named(ident) syn::Member::Named(ident)
} else { } else {
@ -35,7 +49,10 @@ impl SourceCode {
span: field.span(), 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> { pub(crate) fn gen_struct(&self, fields: &syn::Fields) -> Option<TokenStream> {
let (display_pat, _display_members) = display_pat_members(fields); let (display_pat, _display_members) = display_pat_members(fields);
let src = &self.source_code; 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! { Some(quote! {
#[allow(unused_variables)] #[allow(unused_variables)]
fn source_code(&self) -> std::option::Option<&dyn miette::SourceCode> { fn source_code(&self) -> std::option::Option<&dyn miette::SourceCode> {
let Self #display_pat = self; let Self #display_pat = self;
Some(&self.#src) #ret
} }
}) })
} }
@ -68,10 +95,19 @@ impl SourceCode {
} }
}; };
let variant_name = ident.clone(); 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 { match &fields {
syn::Fields::Unit => None, syn::Fields::Unit => None,
_ => Some(quote! { _ => 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 thiserror::Error;
use crate::{self as miette, Diagnostic}; use crate::Diagnostic;
/** /**
Error enum for miette. Used by certain operations in the protocol. Error enum for miette. Used by certain operations in the protocol.
*/ */
#[derive(Debug, Diagnostic, Error)] #[derive(Debug, Error)]
pub enum MietteError { pub enum MietteError {
/// Wrapper around [`std::io::Error`]. This is returned when something went /// Wrapper around [`std::io::Error`]. This is returned when something went
/// wrong while reading a [`SourceCode`](crate::SourceCode). /// wrong while reading a [`SourceCode`](crate::SourceCode).
#[error(transparent)] #[error(transparent)]
#[diagnostic(code(miette::io_error), url(docsrs))]
IoError(#[from] io::Error), IoError(#[from] io::Error),
/// Returned when a [`SourceSpan`](crate::SourceSpan) extends beyond the /// Returned when a [`SourceSpan`](crate::SourceSpan) extends beyond the
/// bounds of a given [`SourceCode`](crate::SourceCode). /// bounds of a given [`SourceCode`](crate::SourceCode).
#[error("The given offset is outside the bounds of its Source")] #[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, 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. /// 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 /// 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 /// now or in the future, use `miette!(err)` which handles either way
/// correctly. /// correctly.
/// ///
@ -206,7 +206,7 @@ impl Report {
/// Create a new error from an error message to wrap the existing error. /// 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 /// 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. /// convenient than this function.
/// ///
/// The primary reason to use `error.wrap_err(...)` instead of /// The primary reason to use `error.wrap_err(...)` instead of
@ -233,7 +233,7 @@ impl Report {
unsafe { Report::construct(error, vtable, handler) } 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 pub fn context<D>(self, msg: D) -> Self
where where
D: Display + Send + Sync + 'static, D: Display + Send + Sync + 'static,

View File

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

View File

@ -43,7 +43,7 @@ where
Box::from_raw(self.ptr.as_ptr()) 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 { Ref {
ptr: self.ptr, ptr: self.ptr,
lifetime: PhantomData, 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 { Ref {
ptr, ptr,
lifetime: PhantomData, 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 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 { Ref {
ptr: self.ptr, ptr: self.ptr,
lifetime: PhantomData, lifetime: PhantomData,

View File

@ -55,6 +55,9 @@ pub struct MietteHandlerOpts {
pub(crate) context_lines: Option<usize>, pub(crate) context_lines: Option<usize>,
pub(crate) tab_width: Option<usize>, pub(crate) tab_width: Option<usize>,
pub(crate) with_cause_chain: Option<bool>, 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 { impl MietteHandlerOpts {
@ -86,6 +89,27 @@ impl MietteHandlerOpts {
self 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. /// Include the cause chain of the top-level error in the report.
pub fn with_cause_chain(mut self) -> Self { pub fn with_cause_chain(mut self) -> Self {
self.with_cause_chain = Some(true); self.with_cause_chain = Some(true);
@ -233,6 +257,16 @@ impl MietteHandlerOpts {
if let Some(w) = self.tab_width { if let Some(w) = self.tab_width {
handler = handler.tab_width(w); 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 { MietteHandler {
inner: Box::new(handler), inner: Box::new(handler),
} }

View File

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

View File

@ -3,7 +3,7 @@ use std::fmt::{self, Write};
use owo_colors::{OwoColorize, Style}; use owo_colors::{OwoColorize, Style};
use unicode_width::UnicodeWidthChar; use unicode_width::UnicodeWidthChar;
use crate::diagnostic_chain::DiagnosticChain; use crate::diagnostic_chain::{DiagnosticChain, ErrorKind};
use crate::handlers::theme::*; use crate::handlers::theme::*;
use crate::protocol::{Diagnostic, Severity}; use crate::protocol::{Diagnostic, Severity};
use crate::{LabeledSpan, MietteError, ReportHandler, SourceCode, SourceSpan, SpanContents}; 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 See [`set_hook()`](crate::set_hook) for more details on customizing your global
printer. printer.
*/ */
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct GraphicalReportHandler { pub struct GraphicalReportHandler {
pub(crate) links: LinkStyle, pub(crate) links: LinkStyle,
@ -30,6 +30,9 @@ pub struct GraphicalReportHandler {
pub(crate) context_lines: usize, pub(crate) context_lines: usize,
pub(crate) tab_width: usize, pub(crate) tab_width: usize,
pub(crate) with_cause_chain: bool, 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)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
@ -51,6 +54,9 @@ impl GraphicalReportHandler {
context_lines: 1, context_lines: 1,
tab_width: 4, tab_width: 4,
with_cause_chain: true, with_cause_chain: true,
break_words: true,
word_separator: None,
word_splitter: None,
} }
} }
@ -64,6 +70,9 @@ impl GraphicalReportHandler {
context_lines: 1, context_lines: 1,
tab_width: 4, tab_width: 4,
with_cause_chain: true, with_cause_chain: true,
break_words: true,
word_separator: None,
word_splitter: None,
} }
} }
@ -122,6 +131,24 @@ impl GraphicalReportHandler {
self 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. /// Sets the 'global' footer for this handler.
pub fn with_footer(mut self, footer: String) -> Self { pub fn with_footer(mut self, footer: String) -> Self {
self.footer = Some(footer); self.footer = Some(footer);
@ -151,7 +178,6 @@ impl GraphicalReportHandler {
diagnostic: &(dyn Diagnostic), diagnostic: &(dyn Diagnostic),
) -> fmt::Result { ) -> fmt::Result {
self.render_header(f, diagnostic)?; self.render_header(f, diagnostic)?;
writeln!(f)?;
self.render_causes(f, diagnostic)?; self.render_causes(f, diagnostic)?;
let src = diagnostic.source_code(); let src = diagnostic.source_code();
self.render_snippets(f, diagnostic, src)?; self.render_snippets(f, diagnostic, src)?;
@ -160,9 +186,17 @@ impl GraphicalReportHandler {
if let Some(footer) = &self.footer { if let Some(footer) = &self.footer {
writeln!(f)?; writeln!(f)?;
let width = self.termwidth.saturating_sub(4); let width = self.termwidth.saturating_sub(4);
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(footer, opts))?; writeln!(f, "{}", textwrap::fill(footer, opts))?;
} }
Ok(()) Ok(())
@ -190,6 +224,7 @@ impl GraphicalReportHandler {
); );
write!(header, "{}", link)?; write!(header, "{}", link)?;
writeln!(f, "{}", header)?; writeln!(f, "{}", header)?;
writeln!(f)?;
} else if let Some(code) = diagnostic.code() { } else if let Some(code) = diagnostic.code() {
write!(header, "{}", code.style(severity_style),)?; write!(header, "{}", code.style(severity_style),)?;
if self.links == LinkStyle::Text && diagnostic.url().is_some() { if self.links == LinkStyle::Text && diagnostic.url().is_some() {
@ -197,6 +232,7 @@ impl GraphicalReportHandler {
write!(header, " ({})", url.style(self.theme.styles.link))?; write!(header, " ({})", url.style(self.theme.styles.link))?;
} }
writeln!(f, "{}", header)?; writeln!(f, "{}", header)?;
writeln!(f)?;
} }
Ok(()) Ok(())
} }
@ -211,9 +247,16 @@ impl GraphicalReportHandler {
let initial_indent = format!(" {} ", severity_icon.style(severity_style)); let initial_indent = format!(" {} ", severity_icon.style(severity_style));
let rest_indent = format!(" {} ", self.theme.characters.vbar.style(severity_style)); let rest_indent = format!(" {} ", self.theme.characters.vbar.style(severity_style));
let width = self.termwidth.saturating_sub(2); let width = self.termwidth.saturating_sub(2);
let opts = textwrap::Options::new(width) let mut opts = textwrap::Options::new(width)
.initial_indent(&initial_indent) .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))?; writeln!(f, "{}", textwrap::fill(&diagnostic.to_string(), opts))?;
@ -250,10 +293,33 @@ impl GraphicalReportHandler {
) )
.style(severity_style) .style(severity_style)
.to_string(); .to_string();
let opts = textwrap::Options::new(width) let mut opts = textwrap::Options::new(width)
.initial_indent(&initial_indent) .initial_indent(&initial_indent)
.subsequent_indent(&rest_indent); .subsequent_indent(&rest_indent)
writeln!(f, "{}", textwrap::fill(&error.to_string(), opts))?; .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() { if let Some(help) = diagnostic.help() {
let width = self.termwidth.saturating_sub(4); let width = self.termwidth.saturating_sub(4);
let initial_indent = " help: ".style(self.theme.styles.help).to_string(); 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) .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))?; writeln!(f, "{}", textwrap::fill(&help.to_string(), opts))?;
} }
Ok(()) Ok(())
@ -287,7 +361,6 @@ impl GraphicalReportHandler {
Some(Severity::Advice) => write!(f, "Advice: ")?, Some(Severity::Advice) => write!(f, "Advice: ")?,
}; };
self.render_header(f, rel)?; self.render_header(f, rel)?;
writeln!(f)?;
self.render_causes(f, rel)?; self.render_causes(f, rel)?;
let src = rel.source_code().or(parent_src); let src = rel.source_code().or(parent_src);
self.render_snippets(f, rel, src)?; self.render_snippets(f, rel, src)?;
@ -367,15 +440,20 @@ impl GraphicalReportHandler {
Ok(()) Ok(())
} }
fn render_context<'a>( fn render_context(
&self, &self,
f: &mut impl fmt::Write, f: &mut impl fmt::Write,
source: &'a dyn SourceCode, source: &dyn SourceCode,
context: &LabeledSpan, context: &LabeledSpan,
labels: &[LabeledSpan], labels: &[LabeledSpan],
) -> fmt::Result { ) -> fmt::Result {
let (contents, lines) = self.get_lines(source, context.inner())?; 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 // sorting is your friend
let labels = labels let labels = labels
.iter() .iter()
@ -416,19 +494,33 @@ impl GraphicalReportHandler {
self.theme.characters.hbar, 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); let source_name = source_name.style(self.theme.styles.link);
writeln!( writeln!(
f, f,
"[{}:{}:{}]", "[{}:{}:{}]",
source_name, source_name,
contents.line() + 1, primary_contents.line() + 1,
contents.column() + 1 primary_contents.column() + 1
)?; )?;
} else if lines.len() <= 1 { } else if lines.len() <= 1 {
writeln!(f, "{}", self.theme.characters.hbar.to_string().repeat(3))?; writeln!(f, "{}", self.theme.characters.hbar.to_string().repeat(3))?;
} else { } 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! // Now it's time for the fun part--actually rendering everything!
@ -453,7 +545,13 @@ impl GraphicalReportHandler {
// no line number! // no line number!
self.write_no_linum(f, linum_width)?; self.write_no_linum(f, linum_width)?;
// gutter _again_ // 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( self.render_single_line_highlights(
f, f,
line, line,
@ -465,11 +563,7 @@ impl GraphicalReportHandler {
} }
for hl in multi_line { for hl in multi_line {
if hl.label().is_some() && line.span_ends(hl) && !line.span_starts(hl) { if hl.label().is_some() && line.span_ends(hl) && !line.span_starts(hl) {
// no line number! self.render_multi_line_end(f, &labels, max_gutter, linum_width, line, hl)?;
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)?;
} }
} }
} }
@ -483,6 +577,91 @@ impl GraphicalReportHandler {
Ok(()) 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( fn render_line_gutter(
&self, &self,
f: &mut impl fmt::Write, f: &mut impl fmt::Write,
@ -551,6 +730,7 @@ impl GraphicalReportHandler {
max_gutter: usize, max_gutter: usize,
line: &Line, line: &Line,
highlights: &[FancySpan], highlights: &[FancySpan],
render_mode: LabelRenderMode,
) -> fmt::Result { ) -> fmt::Result {
if max_gutter == 0 { if max_gutter == 0 {
return Ok(()); return Ok(());
@ -560,15 +740,33 @@ impl GraphicalReportHandler {
let applicable = highlights.iter().filter(|hl| line.span_applies(hl)); let applicable = highlights.iter().filter(|hl| line.span_applies(hl));
for (i, hl) in applicable.enumerate() { for (i, hl) in applicable.enumerate() {
if !line.span_line_only(hl) && line.span_ends(hl) { if !line.span_line_only(hl) && line.span_ends(hl) {
gutter.push_str(&chars.lbot.style(hl.style).to_string()); if render_mode == LabelRenderMode::MultiLineRest {
gutter.push_str( // this is to make multiline labels work. We want to make the right amount
&chars // of horizontal space for them, but not actually draw the lines
.hbar for _ in 0..max_gutter.saturating_sub(i) + 2 {
.to_string() gutter.push(' ');
.repeat(max_gutter.saturating_sub(i) + 2) }
.style(hl.style) } else {
.to_string(), 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; break;
} else { } else {
gutter.push_str(&chars.vbar.style(hl.style).to_string()); 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. /// 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); let line_range = line.offset..=(line.offset + line.length);
assert!(line_range.contains(&offset)); 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 = &line.text[..text_index.min(line.text.len())];
let text_width = self.line_visual_char_width(text).sum(); let text_width = self.line_visual_char_width(text).sum();
if text_index > line.text.len() { 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)) { for (c, width) in text.chars().zip(self.line_visual_char_width(text)) {
if c == '\t' { if c == '\t' {
for _ in 0..width { for _ in 0..width {
f.write_char(' ')? f.write_char(' ')?;
} }
} else { } else {
f.write_char(c)? f.write_char(c)?;
} }
} }
f.write_char('\n')?; f.write_char('\n')?;
@ -672,32 +881,34 @@ impl GraphicalReportHandler {
.map(|hl| { .map(|hl| {
let byte_start = hl.offset(); let byte_start = hl.offset();
let byte_end = hl.offset() + hl.len(); let byte_end = hl.offset() + hl.len();
let start = self.visual_offset(line, byte_start).max(highest); let start = self.visual_offset(line, byte_start, true).max(highest);
let end = self.visual_offset(line, byte_end).max(start + 1); 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 vbar_offset = (start + end) / 2;
let num_left = vbar_offset - start; let num_left = vbar_offset - start;
let num_right = end - vbar_offset - 1; let num_right = end - vbar_offset - 1;
if start < end { underlines.push_str(
underlines.push_str( &format!(
&format!( "{:width$}{}{}{}",
"{:width$}{}{}{}", "",
"", chars.underline.to_string().repeat(num_left),
chars.underline.to_string().repeat(num_left), if hl.len() == 0 {
if hl.len() == 0 { chars.uarrow
chars.uarrow } else if hl.label().is_some() {
} else if hl.label().is_some() { chars.underbar
chars.underbar } else {
} else { chars.underline
chars.underline },
}, chars.underline.to_string().repeat(num_right),
chars.underline.to_string().repeat(num_right), width = start.saturating_sub(highest),
width = start.saturating_sub(highest), )
) .style(hl.style)
.style(hl.style) .to_string(),
.to_string(), );
);
}
highest = std::cmp::max(highest, end); highest = std::cmp::max(highest, end);
(hl, vbar_offset) (hl, vbar_offset)
@ -706,27 +917,40 @@ impl GraphicalReportHandler {
writeln!(f, "{}", underlines)?; writeln!(f, "{}", underlines)?;
for hl in single_liners.iter().rev() { for hl in single_liners.iter().rev() {
if let Some(label) = hl.label() { if let Some(label) = hl.label_parts() {
self.write_no_linum(f, linum_width)?; if label.len() == 1 {
self.render_highlight_gutter(f, max_gutter, line, all_highlights)?; self.write_label_text(
let mut curr_offset = 1usize; f,
for (offset_hl, vbar_offset) in &vbar_offsets { line,
while curr_offset < *vbar_offset + 1 { linum_width,
write!(f, " ")?; max_gutter,
curr_offset += 1; all_highlights,
} chars,
if *offset_hl != hl { &vbar_offsets,
write!(f, "{}", chars.vbar.to_string().style(offset_hl.style))?; hl,
curr_offset += 1; &label[0],
} else { LabelRenderMode::SingleLine,
let lines = format!( )?;
"{}{} {}", } else {
chars.lbot, let mut first = true;
chars.hbar.to_string().repeat(2), for label_line in &label {
label, self.write_label_text(
); f,
writeln!(f, "{}", lines.style(hl.style))?; line,
break; 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(()) Ok(())
} }
fn render_multi_line_end(&self, f: &mut impl fmt::Write, hl: &FancySpan) -> fmt::Result { // I know it's not good practice, but making this a function makes a lot of sense
writeln!( // 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, f,
"{} {}", max_gutter,
self.theme.characters.hbar.style(hl.style), line,
hl.label().unwrap_or_else(|| "".into()), 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(()) Ok(())
} }
@ -819,6 +1110,16 @@ impl ReportHandler for GraphicalReportHandler {
Support types 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)] #[derive(Debug)]
struct Line { struct Line {
line_number: usize, line_number: usize,
@ -836,10 +1137,10 @@ impl Line {
let spanlen = if span.len() == 0 { 1 } else { span.len() }; let spanlen = if span.len() == 0 { 1 } else { span.len() };
// Span starts in this line // Span starts in this line
(span.offset() >= self.offset && span.offset() < self.offset + self.length) (span.offset() >= self.offset && span.offset() < self.offset + self.length)
// Span passes through this line // Span passes through this line
|| (span.offset() < self.offset && span.offset() + spanlen > self.offset + self.length) //todo || (span.offset() < self.offset && span.offset() + spanlen > self.offset + self.length) //todo
// Span ends on this line // Span ends on this line
|| (span.offset() + spanlen > self.offset && span.offset() + spanlen <= self.offset + self.length) || (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 // A 'flyby' is a multi-line span that technically covers this line, but
@ -869,7 +1170,10 @@ impl Line {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
struct FancySpan { 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, span: SourceSpan,
style: Style, 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 { impl FancySpan {
fn new(label: Option<String>, span: SourceSpan, style: Style) -> Self { 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 { fn style(&self) -> Style {
@ -892,7 +1204,15 @@ impl FancySpan {
fn label(&self) -> Option<String> { fn label(&self) -> Option<String> {
self.label self.label
.as_ref() .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 { fn offset(&self) -> usize {

View File

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

View File

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

View File

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

View File

@ -186,7 +186,7 @@
//! //!
//! ```rust //! ```rust
//! // lib/error.rs //! // lib/error.rs
//! use miette::Diagnostic; //! use miette::{Diagnostic, SourceSpan};
//! use thiserror::Error; //! use thiserror::Error;
//! //!
//! #[derive(Error, Diagnostic, Debug)] //! #[derive(Error, Diagnostic, Debug)]
@ -198,6 +198,18 @@
//! #[error("Oops it blew up")] //! #[error("Oops it blew up")]
//! #[diagnostic(code(my_lib::bad_code))] //! #[diagnostic(code(my_lib::bad_code))]
//! BadThingHappened, //! 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"] } //! 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 //! ### ... diagnostic code URLs
//! //!
//! `miette` supports providing a URL for individual diagnostics. This URL will //! `miette` supports providing a URL for individual diagnostics. This URL will
@ -581,6 +610,7 @@
//! .unicode(false) //! .unicode(false)
//! .context_lines(3) //! .context_lines(3)
//! .tab_width(4) //! .tab_width(4)
//! .break_words(true)
//! .build(), //! .build(),
//! ) //! )
//! })) //! }))
@ -640,6 +670,7 @@
//! and some from [`thiserror`](https://github.com/dtolnay/thiserror), also //! and some from [`thiserror`](https://github.com/dtolnay/thiserror), also
//! under the Apache License. Some code is taken from //! under the Apache License. Some code is taken from
//! [`ariadne`](https://github.com/zesterer/ariadne), which is MIT licensed. //! [`ariadne`](https://github.com/zesterer/ariadne), which is MIT licensed.
#[cfg(feature = "derive")]
pub use miette_derive::*; pub use miette_derive::*;
pub use error::*; pub use error::*;

View File

@ -252,7 +252,7 @@ impl MietteDiagnostic {
/// ``` /// ```
pub fn and_labels(mut self, labels: impl IntoIterator<Item = LabeledSpan>) -> Self { pub fn and_labels(mut self, labels: impl IntoIterator<Item = LabeledSpan>) -> Self {
let mut all_labels = self.labels.unwrap_or_default(); 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.labels = Some(all_labels);
self self
} }
@ -292,14 +292,16 @@ fn test_serialize_miette_diagnostic() {
"offset": 0, "offset": 0,
"length": 0 "length": 0
}, },
"label": "label1" "label": "label1",
"primary": false
}, },
{ {
"span": { "span": {
"offset": 1, "offset": 1,
"length": 2 "length": 2
}, },
"label": "label2" "label": "label2",
"primary": false
} }
] ]
}); });
@ -350,14 +352,16 @@ fn test_deserialize_miette_diagnostic() {
"offset": 0, "offset": 0,
"length": 0 "length": 0
}, },
"label": "label1" "label": "label1",
"primary": false
}, },
{ {
"span": { "span": {
"offset": 1, "offset": 1,
"length": 2 "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> { macro_rules! box_impls {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { ($($box_type:ty),*) => {
(**self).source() $(
} 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> { fn cause(&self) -> Option<&dyn std::error::Error> {
self.source() self.source()
}
}
)*
} }
} }
box_impls! {
Box<dyn Diagnostic>,
Box<dyn Diagnostic + Send>,
Box<dyn Diagnostic + Send + Sync>
}
impl<T: Diagnostic + Send + Sync + 'static> From<T> impl<T: Diagnostic + Send + Sync + 'static> From<T>
for Box<dyn Diagnostic + Send + Sync + 'static> 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. gigabytes or larger in size.
*/ */
pub trait SourceCode: Send + Sync { 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. /// certain number of lines before and after the span as context.
fn read_span<'a>( fn read_span<'a>(
&'a self, &'a self,
@ -237,14 +249,16 @@ pub struct LabeledSpan {
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
label: Option<String>, label: Option<String>,
span: SourceSpan, span: SourceSpan,
primary: bool,
} }
impl LabeledSpan { impl LabeledSpan {
/// Makes a new labeled span. /// 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 { Self {
label, label,
span: (offset, len).into(), span: SourceSpan::new(SourceOffset(offset), SourceOffset(len)),
primary: false,
} }
} }
@ -253,6 +267,16 @@ impl LabeledSpan {
Self { Self {
label, label,
span: span.into(), 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`]. /// Returns a reference to the inner [`SourceSpan`].
pub fn inner(&self) -> &SourceSpan { pub const fn inner(&self) -> &SourceSpan {
&self.span &self.span
} }
/// Returns the 0-based starting byte offset. /// Returns the 0-based starting byte offset.
pub fn offset(&self) -> usize { pub const fn offset(&self) -> usize {
self.span.offset() self.span.offset()
} }
/// Returns the number of bytes this `LabeledSpan` spans. /// Returns the number of bytes this `LabeledSpan` spans.
pub fn len(&self) -> usize { pub const fn len(&self) -> usize {
self.span.len() self.span.len()
} }
/// True if this `LabeledSpan` is empty. /// True if this `LabeledSpan` is empty.
pub fn is_empty(&self) -> bool { pub const fn is_empty(&self) -> bool {
self.span.is_empty() self.span.is_empty()
} }
/// True if this `LabeledSpan` is a primary span.
pub const fn primary(&self) -> bool {
self.primary
}
} }
#[cfg(feature = "serde")] #[cfg(feature = "serde")]
@ -338,7 +367,8 @@ fn test_serialize_labeled_span() {
assert_eq!( assert_eq!(
json!(LabeledSpan::new(None, 0, 0)), json!(LabeledSpan::new(None, 0, 0)),
json!({ 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!(LabeledSpan::new(Some("label".to_string()), 0, 0)),
json!({ json!({
"label": "label", "label": "label",
"span": { "offset": 0, "length": 0 } "span": { "offset": 0, "length": 0, },
"primary": false,
}) })
) );
} }
#[cfg(feature = "serde")] #[cfg(feature = "serde")]
@ -358,23 +389,26 @@ fn test_deserialize_labeled_span() {
let span: LabeledSpan = serde_json::from_value(json!({ let span: LabeledSpan = serde_json::from_value(json!({
"label": null, "label": null,
"span": { "offset": 0, "length": 0 } "span": { "offset": 0, "length": 0, },
"primary": false,
})) }))
.unwrap(); .unwrap();
assert_eq!(span, LabeledSpan::new(None, 0, 0)); assert_eq!(span, LabeledSpan::new(None, 0, 0));
let span: LabeledSpan = serde_json::from_value(json!({ let span: LabeledSpan = serde_json::from_value(json!({
"span": { "offset": 0, "length": 0 } "span": { "offset": 0, "length": 0, },
"primary": false
})) }))
.unwrap(); .unwrap();
assert_eq!(span, LabeledSpan::new(None, 0, 0)); assert_eq!(span, LabeledSpan::new(None, 0, 0));
let span: LabeledSpan = serde_json::from_value(json!({ let span: LabeledSpan = serde_json::from_value(json!({
"label": "label", "label": "label",
"span": { "offset": 0, "length": 0 } "span": { "offset": 0, "length": 0, },
"primary": false
})) }))
.unwrap(); .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> { impl<'a> MietteSpanContents<'a> {
/// Make a new [`MietteSpanContents`] object. /// Make a new [`MietteSpanContents`] object.
pub fn new( pub const fn new(
data: &'a [u8], data: &'a [u8],
span: SourceSpan, span: SourceSpan,
line: usize, line: usize,
@ -440,7 +474,7 @@ impl<'a> MietteSpanContents<'a> {
} }
/// Make a new [`MietteSpanContents`] object, with a name for its 'file'. /// Make a new [`MietteSpanContents`] object, with a name for its 'file'.
pub fn new_named( pub const fn new_named(
name: String, name: String,
data: &'a [u8], data: &'a [u8],
span: SourceSpan, span: SourceSpan,
@ -492,7 +526,7 @@ pub struct SourceSpan {
impl SourceSpan { impl SourceSpan {
/// Create a new [`SourceSpan`]. /// Create a new [`SourceSpan`].
pub fn new(start: SourceOffset, length: usize) -> Self { pub const fn new(start: SourceOffset, length: usize) -> Self {
Self { Self {
offset: start, offset: start,
length, length,
@ -500,18 +534,18 @@ impl SourceSpan {
} }
/// The absolute offset, in bytes, from the beginning of a [`SourceCode`]. /// 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() self.offset.offset()
} }
/// Total length of the [`SourceSpan`], in bytes. /// Total length of the [`SourceSpan`], in bytes.
pub fn len(&self) -> usize { pub const fn len(&self) -> usize {
self.length self.length
} }
/// Whether this [`SourceSpan`] has a length of zero. It may still be useful /// Whether this [`SourceSpan`] has a length of zero. It may still be useful
/// to point to a specific point. /// to point to a specific point.
pub fn is_empty(&self) -> bool { pub const fn is_empty(&self) -> bool {
self.length == 0 self.length == 0
} }
} }
@ -563,7 +597,7 @@ fn test_serialize_source_span() {
assert_eq!( assert_eq!(
json!(SourceSpan::from(0)), json!(SourceSpan::from(0)),
json!({ "offset": 0, "length": 0}) json!({ "offset": 0, "length": 0})
) );
} }
#[cfg(feature = "serde")] #[cfg(feature = "serde")]
@ -572,7 +606,7 @@ fn test_deserialize_source_span() {
use serde_json::json; use serde_json::json;
let span: SourceSpan = serde_json::from_value(json!({ "offset": 0, "length": 0})).unwrap(); 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 { impl SourceOffset {
/// Actual byte offset. /// Actual byte offset.
pub fn offset(&self) -> ByteOffset { pub const fn offset(&self) -> ByteOffset {
self.0 self.0
} }
@ -674,12 +708,12 @@ fn test_source_offset_from_location() {
fn test_serialize_source_offset() { fn test_serialize_source_offset() {
use serde_json::json; use serde_json::json;
assert_eq!(json!(SourceOffset::from(0)), 0) assert_eq!(json!(SourceOffset::from(0)), 0);
} }
#[cfg(feature = "serde")] #[cfg(feature = "serde")]
#[test] #[test]
fn test_deserialize_source_offset() { fn test_deserialize_source_offset() {
let offset: SourceOffset = serde_json::from_str("0").unwrap(); 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!( assert_eq!(
"1 x hello x \"2\"".to_string(), "1 x hello x \"2\"".to_string(),
FooStruct("hello".into()).help().unwrap().to_string() FooStruct("hello").help().unwrap().to_string()
); );
#[derive(Debug, Diagnostic, Error)] #[derive(Debug, Diagnostic, Error)]
@ -201,12 +201,7 @@ fn fmt_help() {
assert_eq!( assert_eq!(
"1 x hello x \"2\"".to_string(), "1 x hello x \"2\"".to_string(),
BarStruct { BarStruct { my_field: "hello" }.help().unwrap().to_string()
my_field: "hello".into()
}
.help()
.unwrap()
.to_string()
); );
#[derive(Debug, Diagnostic, Error)] #[derive(Debug, Diagnostic, Error)]
@ -224,7 +219,7 @@ fn fmt_help() {
assert_eq!( assert_eq!(
"1 x bar x \"2\"".to_string(), "1 x bar x \"2\"".to_string(),
FooEnum::X("bar".into()).help().unwrap().to_string() FooEnum::X("bar").help().unwrap().to_string()
); );
assert_eq!( assert_eq!(
@ -250,12 +245,7 @@ fn help_field() {
assert_eq!( assert_eq!(
"x".to_string(), "x".to_string(),
Foo { Foo { do_this: Some("x") }.help().unwrap().to_string()
do_this: Some("x".into())
}
.help()
.unwrap()
.to_string()
); );
#[derive(Debug, Diagnostic, Error)] #[derive(Debug, Diagnostic, Error)]
@ -271,16 +261,11 @@ fn help_field() {
assert_eq!( assert_eq!(
"x".to_string(), "x".to_string(),
Bar::A(Some("x".into())).help().unwrap().to_string() Bar::A(Some("x")).help().unwrap().to_string()
); );
assert_eq!( assert_eq!(
"x".to_string(), "x".to_string(),
Bar::B { Bar::B { do_this: Some("x") }.help().unwrap().to_string()
do_this: Some("x".into())
}
.help()
.unwrap()
.to_string()
); );
#[derive(Debug, Diagnostic, Error)] #[derive(Debug, Diagnostic, Error)]
@ -288,20 +273,14 @@ fn help_field() {
#[diagnostic()] #[diagnostic()]
struct Baz<'a>(#[help] Option<&'a str>); struct Baz<'a>(#[help] Option<&'a str>);
assert_eq!( assert_eq!("x".to_string(), Baz(Some("x")).help().unwrap().to_string());
"x".to_string(),
Baz(Some("x".into())).help().unwrap().to_string()
);
#[derive(Debug, Diagnostic, Error)] #[derive(Debug, Diagnostic, Error)]
#[error("welp")] #[error("welp")]
#[diagnostic()] #[diagnostic()]
struct Quux<'a>(#[help] &'a str); struct Quux<'a>(#[help] &'a str);
assert_eq!( assert_eq!("x".to_string(), Quux("x").help().unwrap().to_string());
"x".to_string(),
Quux("x".into()).help().unwrap().to_string()
);
} }
#[test] #[test]
@ -589,7 +568,7 @@ fn test_unit_struct_display() {
#[error("unit only")] #[error("unit only")]
#[diagnostic(code(foo::bar::overridden), help("hello from unit help"))] #[diagnostic(code(foo::bar::overridden), help("hello from unit help"))]
struct UnitOnly; 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] #[test]
@ -603,5 +582,47 @@ fn test_unit_enum_display() {
assert_eq!( assert_eq!(
Enum::UnitVariant.help().unwrap().to_string(), Enum::UnitVariant.help().unwrap().to_string(),
"hello from unit help" "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 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] #[test]
fn empty_source() -> Result<(), MietteError> { fn empty_source() -> Result<(), MietteError> {
#[derive(Debug, Diagnostic, Error)] #[derive(Debug, Diagnostic, Error)]
@ -67,6 +251,52 @@ fn empty_source() -> Result<(), MietteError> {
Ok(()) 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] #[test]
fn single_line_highlight_span_full_line() { fn single_line_highlight_span_full_line() {
#[derive(Error, Debug, Diagnostic)] #[derive(Error, Debug, Diagnostic)]
@ -85,9 +315,8 @@ fn single_line_highlight_span_full_line() {
let out = fmt_report(err.into()); let out = fmt_report(err.into());
println!("Error: {}", out); println!("Error: {}", out);
let expected = r#" let expected = r#" × oops!
× oops! [issue:2:1]
[issue:1:1]
1 source 1 source
2 text 2 text
· ·
@ -121,7 +350,7 @@ fn single_line_with_wide_char() -> Result<(), MietteError> {
let expected = r#"oops::my::bad let expected = r#"oops::my::bad
× oops! × oops!
[bad_file.rs:1:1] [bad_file.rs:2:7]
1 source 1 source
2 👼🏼text 2 👼🏼text
· ·
@ -160,7 +389,7 @@ fn single_line_with_two_tabs() -> Result<(), MietteError> {
let expected = r#"oops::my::bad let expected = r#"oops::my::bad
× oops! × oops!
[bad_file.rs:1:1] [bad_file.rs:2:3]
1 source 1 source
2 text 2 text
· ·
@ -199,7 +428,7 @@ fn single_line_with_tab_in_middle() -> Result<(), MietteError> {
let expected = r#"oops::my::bad let expected = r#"oops::my::bad
× oops! × oops!
[bad_file.rs:1:1] [bad_file.rs:2:8]
1 source 1 source
2 text = text 2 text = text
· ·
@ -236,7 +465,7 @@ fn single_line_highlight() -> Result<(), MietteError> {
let expected = r#"oops::my::bad let expected = r#"oops::my::bad
× oops! × oops!
[bad_file.rs:1:1] [bad_file.rs:2:3]
1 source 1 source
2 text 2 text
· ·
@ -271,7 +500,7 @@ fn external_source() -> Result<(), MietteError> {
let expected = r#"oops::my::bad let expected = r#"oops::my::bad
× oops! × oops!
[bad_file.rs:1:1] [bad_file.rs:2:3]
1 source 1 source
2 text 2 text
· ·
@ -344,7 +573,7 @@ fn single_line_highlight_offset_end_of_line() -> Result<(), MietteError> {
let expected = r#"oops::my::bad let expected = r#"oops::my::bad
× oops! × oops!
[bad_file.rs:1:1] [bad_file.rs:1:7]
1 source 1 source
· ·
· this bit here · this bit here
@ -380,7 +609,7 @@ fn single_line_highlight_include_end_of_line() -> Result<(), MietteError> {
let expected = r#"oops::my::bad let expected = r#"oops::my::bad
× oops! × oops!
[bad_file.rs:1:1] [bad_file.rs:2:3]
1 source 1 source
2 text 2 text
· ·
@ -417,7 +646,7 @@ fn single_line_highlight_include_end_of_line_crlf() -> Result<(), MietteError> {
let expected = r#"oops::my::bad let expected = r#"oops::my::bad
× oops! × oops!
[bad_file.rs:1:1] [bad_file.rs:2:3]
1 source 1 source
2 text 2 text
· ·
@ -454,7 +683,7 @@ fn single_line_highlight_with_empty_span() -> Result<(), MietteError> {
let expected = r#"oops::my::bad let expected = r#"oops::my::bad
× oops! × oops!
[bad_file.rs:1:1] [bad_file.rs:2:3]
1 source 1 source
2 text 2 text
· ·
@ -491,7 +720,7 @@ fn single_line_highlight_no_label() -> Result<(), MietteError> {
let expected = r#"oops::my::bad let expected = r#"oops::my::bad
× oops! × oops!
[bad_file.rs:1:1] [bad_file.rs:2:3]
1 source 1 source
2 text 2 text
· ·
@ -527,7 +756,7 @@ fn single_line_highlight_at_line_start() -> Result<(), MietteError> {
let expected = r#"oops::my::bad let expected = r#"oops::my::bad
× oops! × oops!
[bad_file.rs:1:1] [bad_file.rs:2:1]
1 source 1 source
2 text 2 text
· ·
@ -542,6 +771,94 @@ fn single_line_highlight_at_line_start() -> Result<(), MietteError> {
Ok(()) 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] #[test]
fn multiple_same_line_highlights() -> Result<(), MietteError> { fn multiple_same_line_highlights() -> Result<(), MietteError> {
#[derive(Debug, Diagnostic, Error)] #[derive(Debug, Diagnostic, Error)]
@ -570,7 +887,7 @@ fn multiple_same_line_highlights() -> Result<(), MietteError> {
let expected = r#"oops::my::bad let expected = r#"oops::my::bad
× oops! × oops!
[bad_file.rs:1:1] [bad_file.rs:2:3]
1 source 1 source
2 text text text text text 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 let expected = r#"oops::my::bad
× oops! × oops!
[bad_file.rs:1:1] [bad_file.rs:2:3]
1 source 1 source
2 text text text text text 2 text text text text text
· ·
@ -656,7 +973,7 @@ fn multiline_highlight_adjacent() -> Result<(), MietteError> {
let expected = r#"oops::my::bad let expected = r#"oops::my::bad
× oops! × oops!
[bad_file.rs:1:1] [bad_file.rs:2:3]
1 source 1 source
2 text 2 text
3 here 3 here
@ -670,6 +987,43 @@ fn multiline_highlight_adjacent() -> Result<(), MietteError> {
Ok(()) 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] #[test]
fn multiline_highlight_flyby() -> Result<(), MietteError> { fn multiline_highlight_flyby() -> Result<(), MietteError> {
#[derive(Debug, Diagnostic, Error)] #[derive(Debug, Diagnostic, Error)]
@ -970,7 +1324,7 @@ fn related() -> Result<(), MietteError> {
let expected = r#"oops::my::bad let expected = r#"oops::my::bad
× oops! × oops!
[bad_file.rs:1:1] [bad_file.rs:2:3]
1 source 1 source
2 text 2 text
· ·
@ -1032,7 +1386,7 @@ fn related_source_code_propagation() -> Result<(), MietteError> {
let expected = r#"oops::my::bad let expected = r#"oops::my::bad
× oops! × oops!
[bad_file.rs:1:1] [bad_file.rs:2:3]
1 source 1 source
2 text 2 text
· ·
@ -1137,7 +1491,7 @@ fn related_severity() -> Result<(), MietteError> {
let expected = r#"oops::my::bad let expected = r#"oops::my::bad
× oops! × oops!
[bad_file.rs:1:1] [bad_file.rs:2:3]
1 source 1 source
2 text 2 text
· ·
@ -1201,9 +1555,8 @@ fn zero_length_eol_span() {
let out = fmt_report(err.into()); let out = fmt_report(err.into());
println!("Error: {}", out); println!("Error: {}", out);
let expected = r#" let expected = r#" × oops!
× oops! [issue:2:1]
[issue:1:1]
1 this is the first line 1 this is the first line
2 this is the second line 2 this is the second line
· ·
@ -1214,3 +1567,149 @@ fn zero_length_eol_span() {
assert_eq!(expected, out); 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; 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)] #[derive(Debug, miette::Diagnostic, thiserror::Error)]
#[error("AnErr")] #[error("AnErr")]
struct AnErr; struct AnErr;
@ -8,7 +19,7 @@ struct AnErr;
#[error("TestError")] #[error("TestError")]
struct TestStructError { struct TestStructError {
#[diagnostic_source] #[diagnostic_source]
asdf_inner_foo: AnErr, asdf_inner_foo: SourceError,
} }
#[derive(Debug, miette::Diagnostic, thiserror::Error)] #[derive(Debug, miette::Diagnostic, thiserror::Error)]
@ -37,7 +48,11 @@ struct TestArcedError(#[diagnostic_source] std::sync::Arc<dyn Diagnostic>);
#[test] #[test]
fn test_diagnostic_source() { fn test_diagnostic_source() {
let error = TestStructError { 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()); assert!(error.diagnostic_source().is_some());
@ -59,3 +74,123 @@ fn test_diagnostic_source() {
let error = TestArcedError(std::sync::Arc::new(AnErr)); let error = TestArcedError(std::sync::Arc::new(AnErr));
assert!(error.diagnostic_source().is_some()); 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": [] "related": []
}"# }"#
.lines() .lines()
.into_iter()
.map(|s| s.trim_matches(|c| c == ' ' || c == '\n')) .map(|s| s.trim_matches(|c| c == ' ' || c == '\n'))
.collect(); .collect();
assert_eq!(expected, out); assert_eq!(expected, out);
@ -98,7 +97,6 @@ mod json_report_handler {
"related": [] "related": []
}"# }"#
.lines() .lines()
.into_iter()
.map(|s| s.trim_matches(|c| c == ' ' || c == '\n')) .map(|s| s.trim_matches(|c| c == ' ' || c == '\n'))
.collect(); .collect();
assert_eq!(expected, out); assert_eq!(expected, out);
@ -144,7 +142,6 @@ mod json_report_handler {
"related": [] "related": []
}"# }"#
.lines() .lines()
.into_iter()
.map(|s| s.trim_matches(|c| c == ' ' || c == '\n')) .map(|s| s.trim_matches(|c| c == ' ' || c == '\n'))
.collect(); .collect();
assert_eq!(expected, out); assert_eq!(expected, out);
@ -190,7 +187,6 @@ mod json_report_handler {
"related": [] "related": []
}"# }"#
.lines() .lines()
.into_iter()
.map(|s| s.trim_matches(|c| c == ' ' || c == '\n')) .map(|s| s.trim_matches(|c| c == ' ' || c == '\n'))
.collect(); .collect();
assert_eq!(expected, out); assert_eq!(expected, out);
@ -235,7 +231,6 @@ mod json_report_handler {
"related": [] "related": []
}"# }"#
.lines() .lines()
.into_iter()
.map(|s| s.trim_matches(|c| c == ' ' || c == '\n')) .map(|s| s.trim_matches(|c| c == ' ' || c == '\n'))
.collect(); .collect();
assert_eq!(expected, out); assert_eq!(expected, out);
@ -281,7 +276,6 @@ mod json_report_handler {
"related": [] "related": []
}"# }"#
.lines() .lines()
.into_iter()
.map(|s| s.trim_matches(|c| c == ' ' || c == '\n')) .map(|s| s.trim_matches(|c| c == ' ' || c == '\n'))
.collect(); .collect();
assert_eq!(expected, out); assert_eq!(expected, out);
@ -347,7 +341,6 @@ mod json_report_handler {
"related": [] "related": []
}"# }"#
.lines() .lines()
.into_iter()
.map(|s| s.trim_matches(|c| c == ' ' || c == '\n')) .map(|s| s.trim_matches(|c| c == ' ' || c == '\n'))
.collect(); .collect();
assert_eq!(expected, out); assert_eq!(expected, out);
@ -393,7 +386,6 @@ mod json_report_handler {
"related": [] "related": []
}"# }"#
.lines() .lines()
.into_iter()
.map(|s| s.trim_matches(|c| c == ' ' || c == '\n')) .map(|s| s.trim_matches(|c| c == ' ' || c == '\n'))
.collect(); .collect();
assert_eq!(expected, out); assert_eq!(expected, out);
@ -456,7 +448,6 @@ mod json_report_handler {
"related": [] "related": []
}"# }"#
.lines() .lines()
.into_iter()
.map(|s| s.trim_matches(|c| c == ' ' || c == '\n')) .map(|s| s.trim_matches(|c| c == ' ' || c == '\n'))
.collect(); .collect();
assert_eq!(expected, out); assert_eq!(expected, out);
@ -532,7 +523,6 @@ mod json_report_handler {
"related": [] "related": []
}"# }"#
.lines() .lines()
.into_iter()
.map(|s| s.trim_matches(|c| c == ' ' || c == '\n')) .map(|s| s.trim_matches(|c| c == ' ' || c == '\n'))
.collect(); .collect();
assert_eq!(expected, out); assert_eq!(expected, out);
@ -588,7 +578,6 @@ mod json_report_handler {
"related": [] "related": []
}"# }"#
.lines() .lines()
.into_iter()
.map(|s| s.trim_matches(|c| c == ' ' || c == '\n')) .map(|s| s.trim_matches(|c| c == ' ' || c == '\n'))
.collect(); .collect();
assert_eq!(expected, out); assert_eq!(expected, out);
@ -644,7 +633,6 @@ mod json_report_handler {
"related": [] "related": []
}"# }"#
.lines() .lines()
.into_iter()
.map(|s| s.trim_matches(|c| c == ' ' || c == '\n')) .map(|s| s.trim_matches(|c| c == ' ' || c == '\n'))
.collect(); .collect();
assert_eq!(expected, out); assert_eq!(expected, out);
@ -700,7 +688,6 @@ mod json_report_handler {
"related": [] "related": []
}"# }"#
.lines() .lines()
.into_iter()
.map(|s| s.trim_matches(|c| c == ' ' || c == '\n')) .map(|s| s.trim_matches(|c| c == ' ' || c == '\n'))
.collect(); .collect();
assert_eq!(expected, out); assert_eq!(expected, out);
@ -728,7 +715,6 @@ mod json_report_handler {
"related": [] "related": []
}"# }"#
.lines() .lines()
.into_iter()
.map(|s| s.trim_matches(|c| c == ' ' || c == '\n')) .map(|s| s.trim_matches(|c| c == ' ' || c == '\n'))
.collect(); .collect();
assert_eq!(expected, out); assert_eq!(expected, out);
@ -822,7 +808,6 @@ mod json_report_handler {
}] }]
}"# }"#
.lines() .lines()
.into_iter()
.map(|s| s.trim_matches(|c| c == ' ' || c == '\n')) .map(|s| s.trim_matches(|c| c == ' ' || c == '\n'))
.collect(); .collect();
assert_eq!(expected, out); assert_eq!(expected, out);
@ -920,7 +905,6 @@ mod json_report_handler {
}] }]
}"# }"#
.lines() .lines()
.into_iter()
.map(|s| s.trim_matches(|c| c == ' ' || c == '\n')) .map(|s| s.trim_matches(|c| c == ' ' || c == '\n'))
.collect(); .collect();
assert_eq!(expected, out); assert_eq!(expected, out);