mirror of https://github.com/zkat/miette.git
Merge branch 'main' into usize
This commit is contained in:
commit
b9da0b6558
|
|
@ -10,14 +10,12 @@ jobs:
|
|||
name: Check fmt & build docs
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
components: rustfmt
|
||||
override: true
|
||||
- name: rustfmt
|
||||
run: cargo fmt --all -- --check
|
||||
- name: docs
|
||||
|
|
@ -32,14 +30,15 @@ jobs:
|
|||
os: [ubuntu-latest, macOS-latest, windows-latest]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: ${{ matrix.rust }}
|
||||
components: clippy
|
||||
override: true
|
||||
- name: Force older version of is-terminal for MSRV builds
|
||||
if: matrix.rust == '1.56.0'
|
||||
run: cargo update -p is-terminal --precise 0.4.7
|
||||
- name: Clippy
|
||||
run: cargo clippy --all -- -D warnings
|
||||
- name: Run tests
|
||||
|
|
@ -54,14 +53,12 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: nightly
|
||||
components: miri,rust-src
|
||||
override: true
|
||||
- name: Run tests with miri
|
||||
env:
|
||||
MIRIFLAGS: -Zmiri-disable-isolation -Zmiri-strict-provenance
|
||||
|
|
@ -75,13 +72,11 @@ jobs:
|
|||
os: [ubuntu-latest, macOS-latest, windows-latest]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: nightly
|
||||
override: true
|
||||
- name: Run minimal version build
|
||||
run: cargo build -Z minimal-versions --all-features
|
||||
|
||||
|
|
|
|||
20
CHANGELOG.md
20
CHANGELOG.md
|
|
@ -1,5 +1,25 @@
|
|||
# `miette` Release Changelog
|
||||
|
||||
<a name="5.10.0"></a>
|
||||
## 5.10.0 (2023-07-16)
|
||||
|
||||
### Features
|
||||
|
||||
* **protocol:** add StdError impl for Box<dyn Diagnostic + Send + Sync> (#273) ([2e3e5c9d](https://github.com/zkat/miette/commit/2e3e5c9d15e234495369e9b47d032644dd5664ad))
|
||||
|
||||
<a name="5.9.0"></a>
|
||||
## 5.9.0 (2023-05-18)
|
||||
|
||||
### Features
|
||||
|
||||
* **serde:** Add `serde` support (#264) ([c25676cb](https://github.com/zkat/miette/commit/c25676cb1f4266c2607836e6359f15b9cbd8637e))
|
||||
* **const:** Constify various functions (#263) ([46adb3bc](https://github.com/zkat/miette/commit/46adb3bc6aa6518d82a4187b34c56e287922136f))
|
||||
* **nested:** Render inner diagnostics (#170) ([aefe3237](https://github.com/zkat/miette/commit/aefe323780bda4e60feb44bb96ee98634ad677ad))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **misc:** Correct some typos (#255) ([675f3411](https://github.com/zkat/miette/commit/675f3411e33d5fae86d4018c3b72f751a4c4bc2f))
|
||||
|
||||
<a name="5.8.0"></a>
|
||||
## 5.8.0 (2023-04-18)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "miette"
|
||||
version = "5.8.0"
|
||||
version = "5.10.0"
|
||||
authors = ["Kat Marchán <kzm@zkat.tech>"]
|
||||
description = "Fancy diagnostic reporting library and protocol for us mere mortals who aren't compiler hackers."
|
||||
categories = ["rust-patterns"]
|
||||
|
|
@ -14,7 +14,7 @@ exclude = ["images/", "tests/", "miette-derive/"]
|
|||
|
||||
[dependencies]
|
||||
thiserror = "1.0.40"
|
||||
miette-derive = { path = "miette-derive", version = "=5.8.0" }
|
||||
miette-derive = { path = "miette-derive", version = "=5.10.0", optional = true }
|
||||
once_cell = "1.8.0"
|
||||
unicode-width = "0.1.9"
|
||||
|
||||
|
|
@ -44,7 +44,8 @@ lazy_static = "1.4"
|
|||
serde_json = "1.0.64"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
default = ["derive"]
|
||||
derive = ["miette-derive"]
|
||||
no-format-args-capture = []
|
||||
fancy-no-backtrace = [
|
||||
"owo-colors",
|
||||
|
|
|
|||
102
README.md
102
README.md
|
|
@ -4,7 +4,7 @@
|
|||
You run miette? You run her code like the software? Oh. Oh! Error code for
|
||||
coder! Error code for One Thousand Lines!
|
||||
|
||||
## About
|
||||
### About
|
||||
|
||||
`miette` is a diagnostic library for Rust. It includes a series of
|
||||
traits/protocols that allow you to hook into its error reporting facilities,
|
||||
|
|
@ -32,7 +32,7 @@ output like in the screenshots above.** You should only do this in your
|
|||
toplevel crate, as the fancy feature pulls in a number of dependencies that
|
||||
libraries and such might not want.
|
||||
|
||||
## Table of Contents <!-- omit in toc -->
|
||||
### Table of Contents <!-- omit in toc -->
|
||||
|
||||
- [About](#about)
|
||||
- [Features](#features)
|
||||
|
|
@ -51,7 +51,7 @@ libraries and such might not want.
|
|||
- [Acknowledgements](#acknowledgements)
|
||||
- [License](#license)
|
||||
|
||||
## Features
|
||||
### Features
|
||||
|
||||
- Generic [`Diagnostic`] protocol, compatible (and dependent on)
|
||||
[`std::error::Error`].
|
||||
|
|
@ -76,7 +76,7 @@ the following features:
|
|||
- Cause chain printing
|
||||
- Turns diagnostic codes into links in [supported terminals](https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda).
|
||||
|
||||
## Installing
|
||||
### Installing
|
||||
|
||||
```sh
|
||||
$ cargo add miette
|
||||
|
|
@ -88,7 +88,7 @@ If you want to use the fancy printer in all these screenshots:
|
|||
$ cargo add miette --features fancy
|
||||
```
|
||||
|
||||
## Example
|
||||
### Example
|
||||
|
||||
```rust
|
||||
/*
|
||||
|
|
@ -96,7 +96,7 @@ You can derive a `Diagnostic` from any `std::error::Error` type.
|
|||
|
||||
`thiserror` is a great way to define them, and plays nicely with `miette`!
|
||||
*/
|
||||
use miette::{Diagnostic, SourceSpan};
|
||||
use miette::{Diagnostic, NamedSource, SourceSpan};
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug, Diagnostic)]
|
||||
|
|
@ -123,7 +123,7 @@ Use this `Result` type (or its expanded version) as the return type
|
|||
throughout your app (but NOT your libraries! Those should always return
|
||||
concrete types!).
|
||||
*/
|
||||
use miette::{NamedSource, Result};
|
||||
use miette::Result;
|
||||
fn this_fails() -> Result<()> {
|
||||
// You can use plain strings as a `Source`, or anything that implements
|
||||
// the one-method `Source` trait.
|
||||
|
|
@ -170,9 +170,9 @@ diagnostic help: Change int or string to be the right types and try again.
|
|||
diagnostic code: nu::parser::unsupported_operation
|
||||
For more details, see https://docs.rs/nu-parser/0.1.0/nu-parser/enum.ParseError.html#variant.UnsupportedOperation">
|
||||
|
||||
## Using
|
||||
### Using
|
||||
|
||||
### ... in libraries
|
||||
#### ... in libraries
|
||||
|
||||
`miette` is _fully compatible_ with library usage. Consumers who don't know
|
||||
about, or don't want, `miette` features can safely use its error types as
|
||||
|
|
@ -187,7 +187,7 @@ the trait directly, just like with `std::error::Error`.
|
|||
|
||||
```rust
|
||||
// lib/error.rs
|
||||
use miette::Diagnostic;
|
||||
use miette::{Diagnostic, SourceSpan};
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Diagnostic, Debug)]
|
||||
|
|
@ -199,6 +199,18 @@ pub enum MyLibError {
|
|||
#[error("Oops it blew up")]
|
||||
#[diagnostic(code(my_lib::bad_code))]
|
||||
BadThingHappened,
|
||||
|
||||
#[error(transparent)]
|
||||
// Use `#[diagnostic(transparent)]` to wrap another [`Diagnostic`]. You won't see labels otherwise
|
||||
#[diagnostic(transparent)]
|
||||
AnotherError(#[from] AnotherError),
|
||||
}
|
||||
|
||||
#[derive(Error, Diagnostic, Debug)]
|
||||
#[error("another error")]
|
||||
pub struct AnotherError {
|
||||
#[label("here")]
|
||||
pub at: SourceSpan
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -206,7 +218,7 @@ Then, return this error type from all your fallible public APIs. It's a best
|
|||
practice to wrap any "external" error types in your error `enum` instead of
|
||||
using something like [`Report`] in a library.
|
||||
|
||||
### ... in application code
|
||||
#### ... in application code
|
||||
|
||||
Application code tends to work a little differently than libraries. You
|
||||
don't always need or care to define dedicated error wrappers for errors
|
||||
|
|
@ -248,8 +260,7 @@ pub fn some_tool() -> Result<Version> {
|
|||
}
|
||||
```
|
||||
|
||||
To construct your own simple adhoc error use the [`miette!`] macro:
|
||||
|
||||
To construct your own simple adhoc error use the [miette!] macro:
|
||||
```rust
|
||||
// my_app/lib/my_internal_file.rs
|
||||
use miette::{miette, IntoDiagnostic, Result, WrapErr};
|
||||
|
|
@ -262,8 +273,9 @@ pub fn some_tool() -> Result<Version> {
|
|||
.map_err(|_| miette!("Invalid version {}", version))?)
|
||||
}
|
||||
```
|
||||
There are also similar [bail!] and [ensure!] macros.
|
||||
|
||||
### ... in `main()`
|
||||
#### ... in `main()`
|
||||
|
||||
`main()` is just like any other part of your application-internal code. Use
|
||||
`Result` as your return value, and it will pretty-print your diagnostics
|
||||
|
|
@ -293,7 +305,24 @@ enabled:
|
|||
miette = { version = "X.Y.Z", features = ["fancy"] }
|
||||
```
|
||||
|
||||
### ... diagnostic code URLs
|
||||
Another way to display a diagnostic is by printing them using the debug formatter.
|
||||
This is, in fact, what returning diagnostics from main ends up doing.
|
||||
To do it yourself, you can write the following:
|
||||
|
||||
```rust
|
||||
use miette::{IntoDiagnostic, Result};
|
||||
use semver::Version;
|
||||
|
||||
fn just_a_random_function() {
|
||||
let version_result: Result<Version> = "1.2.x".parse().into_diagnostic();
|
||||
match version_result {
|
||||
Err(e) => println!("{:?}", e),
|
||||
Ok(version) => println!("{}", version),
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### ... diagnostic code URLs
|
||||
|
||||
`miette` supports providing a URL for individual diagnostics. This URL will
|
||||
be displayed as an actual link in supported terminals, like so:
|
||||
|
|
@ -346,7 +375,7 @@ use thiserror::Error;
|
|||
struct MyErr;
|
||||
```
|
||||
|
||||
### ... snippets
|
||||
#### ... snippets
|
||||
|
||||
Along with its general error handling and reporting features, `miette` also
|
||||
includes facilities for adding error spans/annotations/labels to your
|
||||
|
|
@ -394,7 +423,7 @@ pub struct MyErrorType {
|
|||
}
|
||||
```
|
||||
|
||||
#### ... help text
|
||||
##### ... help text
|
||||
`miette` provides two facilities for supplying help text for your errors:
|
||||
|
||||
The first is the `#[help()]` format attribute that applies to structs or
|
||||
|
|
@ -430,7 +459,7 @@ let err = Foo {
|
|||
};
|
||||
```
|
||||
|
||||
### ... multiple related errors
|
||||
#### ... multiple related errors
|
||||
|
||||
`miette` supports collecting multiple errors into a single diagnostic, and
|
||||
printing them all together nicely.
|
||||
|
|
@ -450,7 +479,7 @@ struct MyError {
|
|||
}
|
||||
```
|
||||
|
||||
### ... delayed source code
|
||||
#### ... delayed source code
|
||||
|
||||
Sometimes it makes sense to add source code to the error message later.
|
||||
One option is to use [`with_source_code()`](Report::with_source_code)
|
||||
|
|
@ -533,7 +562,7 @@ fn main() -> miette::Result<()> {
|
|||
}
|
||||
```
|
||||
|
||||
### ... Diagnostic-based error sources.
|
||||
#### ... Diagnostic-based error sources.
|
||||
|
||||
When one uses the `#[source]` attribute on a field, that usually comes
|
||||
from `thiserror`, and implements a method for
|
||||
|
|
@ -566,7 +595,7 @@ struct MyError {
|
|||
struct OtherError;
|
||||
```
|
||||
|
||||
### ... handler options
|
||||
#### ... handler options
|
||||
|
||||
[`MietteHandler`] is the default handler, and is very customizable. In
|
||||
most cases, you can simply use [`MietteHandlerOpts`] to tweak its behavior
|
||||
|
|
@ -585,12 +614,13 @@ miette::set_hook(Box::new(|_| {
|
|||
.build(),
|
||||
)
|
||||
}))
|
||||
|
||||
```
|
||||
|
||||
See the docs for [`MietteHandlerOpts`] for more details on what you can
|
||||
customize!
|
||||
|
||||
### ... dynamic diagnostics
|
||||
#### ... dynamic diagnostics
|
||||
|
||||
If you...
|
||||
- ...don't know all the possible errors upfront
|
||||
|
|
@ -599,9 +629,10 @@ then you may want to use [`miette!`], [`diagnostic!`] macros or
|
|||
[`MietteDiagnostic`] directly to create diagnostic on the fly.
|
||||
|
||||
```rust
|
||||
|
||||
let source = "2 + 2 * 2 = 8".to_string();
|
||||
let report = miette!(
|
||||
labels = vec[
|
||||
labels = vec![
|
||||
LabeledSpan::at(12..13, "this should be 6"),
|
||||
],
|
||||
help = "'*' has greater precedence than '+'",
|
||||
|
|
@ -610,26 +641,25 @@ let report = miette!(
|
|||
println!("{:?}", report)
|
||||
```
|
||||
|
||||
## Acknowledgements
|
||||
### Acknowledgements
|
||||
|
||||
`miette` was not developed in a void. It owes enormous credit to various
|
||||
other projects and their authors:
|
||||
|
||||
- [`anyhow`](http://crates.io/crates/anyhow) and
|
||||
[`color-eyre`](https://crates.io/crates/color-eyre): these two
|
||||
enormously influential error handling libraries have pushed forward the
|
||||
experience of application-level error handling and error reporting.
|
||||
`miette`'s `Report` type is an attempt at a very very rough version of
|
||||
their `Report` types.
|
||||
- [`thiserror`](https://crates.io/crates/thiserror) for setting the
|
||||
standard for library-level error definitions, and for being the
|
||||
inspiration behind `miette`'s derive macro.
|
||||
- [`anyhow`](http://crates.io/crates/anyhow) and [`color-eyre`](https://crates.io/crates/color-eyre):
|
||||
these two enormously influential error handling libraries have pushed
|
||||
forward the experience of application-level error handling and error
|
||||
reporting. `miette`'s `Report` type is an attempt at a very very rough
|
||||
version of their `Report` types.
|
||||
- [`thiserror`](https://crates.io/crates/thiserror) for setting the standard
|
||||
for library-level error definitions, and for being the inspiration behind
|
||||
`miette`'s derive macro.
|
||||
- `rustc` and [@estebank](https://github.com/estebank) for their
|
||||
state-of-the-art work in compiler diagnostics.
|
||||
- [`ariadne`](https://crates.io/crates/ariadne) for pushing forward how
|
||||
_pretty_ these diagnostics can really look!
|
||||
|
||||
## License
|
||||
### License
|
||||
|
||||
`miette` is released to the Rust community under the [Apache license
|
||||
2.0](./LICENSE).
|
||||
|
|
@ -648,7 +678,7 @@ under the Apache License. Some code is taken from
|
|||
[`MietteHandler`]: https://docs.rs/miette/latest/miette/struct.MietteHandler.html
|
||||
[`MietteDiagnostic`]: https://docs.rs/miette/latest/miette/struct.MietteDiagnostic.html
|
||||
[`Report`]: https://docs.rs/miette/latest/miette/struct.Report.html
|
||||
[`ReportHandler`]: https://docs.rs/miette/latest/miette/struct.ReportHandler.html
|
||||
[`ReportHandler`]: https://docs.rs/miette/latest/miette/trait.ReportHandler.html
|
||||
[`Result`]: https://docs.rs/miette/latest/miette/type.Result.html
|
||||
[`SourceCode`]: https://docs.rs/miette/latest/miette/struct.SourceCode.html
|
||||
[`SourceCode`]: https://docs.rs/miette/latest/miette/trait.SourceCode.html
|
||||
[`SourceSpan`]: https://docs.rs/miette/latest/miette/struct.SourceSpan.html
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
[`MietteHandler`]: https://docs.rs/miette/latest/miette/struct.MietteHandler.html
|
||||
[`MietteDiagnostic`]: https://docs.rs/miette/latest/miette/struct.MietteDiagnostic.html
|
||||
[`Report`]: https://docs.rs/miette/latest/miette/struct.Report.html
|
||||
[`ReportHandler`]: https://docs.rs/miette/latest/miette/struct.ReportHandler.html
|
||||
[`ReportHandler`]: https://docs.rs/miette/latest/miette/trait.ReportHandler.html
|
||||
[`Result`]: https://docs.rs/miette/latest/miette/type.Result.html
|
||||
[`SourceCode`]: https://docs.rs/miette/latest/miette/struct.SourceCode.html
|
||||
[`SourceCode`]: https://docs.rs/miette/latest/miette/trait.SourceCode.html
|
||||
[`SourceSpan`]: https://docs.rs/miette/latest/miette/struct.SourceSpan.html
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "miette-derive"
|
||||
version = "5.8.0"
|
||||
version = "5.10.0"
|
||||
authors = ["Kat Marchán <kzm@zkat.tech>"]
|
||||
edition = "2018"
|
||||
license = "Apache-2.0"
|
||||
|
|
@ -11,6 +11,6 @@ repository = "https://github.com/zkat/miette"
|
|||
proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
proc-macro2 = "1.0"
|
||||
proc-macro2 = "1.0.60"
|
||||
quote = "1.0"
|
||||
syn = "2.0.11"
|
||||
|
|
|
|||
|
|
@ -20,10 +20,12 @@ struct Label {
|
|||
label: Option<Display>,
|
||||
ty: syn::Type,
|
||||
span: syn::Member,
|
||||
primary: bool,
|
||||
}
|
||||
|
||||
struct LabelAttr {
|
||||
label: Option<Display>,
|
||||
primary: bool,
|
||||
}
|
||||
|
||||
impl Parse for LabelAttr {
|
||||
|
|
@ -40,10 +42,22 @@ impl Parse for LabelAttr {
|
|||
}
|
||||
});
|
||||
let la = input.lookahead1();
|
||||
let label = if la.peek(syn::token::Paren) {
|
||||
// #[label("{}", x)]
|
||||
let (primary, label) = if la.peek(syn::token::Paren) {
|
||||
// #[label(primary?, "{}", x)]
|
||||
let content;
|
||||
parenthesized!(content in input);
|
||||
|
||||
let primary = if content.peek(syn::Ident) {
|
||||
let ident: syn::Ident = content.parse()?;
|
||||
if ident != "primary" {
|
||||
return Err(syn::Error::new(input.span(), "Invalid argument to label() attribute. The argument must be a literal string or the keyword `primary`."));
|
||||
}
|
||||
let _ = content.parse::<Token![,]>();
|
||||
true
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
if content.peek(syn::LitStr) {
|
||||
let fmt = content.parse()?;
|
||||
let args = if content.is_empty() {
|
||||
|
|
@ -56,22 +70,27 @@ impl Parse for LabelAttr {
|
|||
args,
|
||||
has_bonus_display: false,
|
||||
};
|
||||
Some(display)
|
||||
(primary, Some(display))
|
||||
} else if !primary {
|
||||
return Err(syn::Error::new(input.span(), "Invalid argument to label() attribute. The argument must be a literal string or the keyword `primary`."));
|
||||
} else {
|
||||
return Err(syn::Error::new(input.span(), "Invalid argument to label() attribute. The first argument must be a literal string."));
|
||||
(primary, None)
|
||||
}
|
||||
} else if la.peek(Token![=]) {
|
||||
// #[label = "blabla"]
|
||||
input.parse::<Token![=]>()?;
|
||||
Some(Display {
|
||||
fmt: input.parse()?,
|
||||
args: TokenStream::new(),
|
||||
has_bonus_display: false,
|
||||
})
|
||||
(
|
||||
false,
|
||||
Some(Display {
|
||||
fmt: input.parse()?,
|
||||
args: TokenStream::new(),
|
||||
has_bonus_display: false,
|
||||
}),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
(false, None)
|
||||
};
|
||||
Ok(LabelAttr { label })
|
||||
Ok(LabelAttr { label, primary })
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -100,12 +119,21 @@ impl Labels {
|
|||
})
|
||||
};
|
||||
use quote::ToTokens;
|
||||
let LabelAttr { label } =
|
||||
let LabelAttr { label, primary } =
|
||||
syn::parse2::<LabelAttr>(attr.meta.to_token_stream())?;
|
||||
|
||||
if primary && labels.iter().any(|l: &Label| l.primary) {
|
||||
return Err(syn::Error::new(
|
||||
field.span(),
|
||||
"Cannot have more than one primary label.",
|
||||
));
|
||||
}
|
||||
|
||||
labels.push(Label {
|
||||
label,
|
||||
span,
|
||||
ty: field.ty.clone(),
|
||||
primary,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -120,13 +148,23 @@ impl Labels {
|
|||
pub(crate) fn gen_struct(&self, fields: &syn::Fields) -> Option<TokenStream> {
|
||||
let (display_pat, display_members) = display_pat_members(fields);
|
||||
let labels = self.0.iter().map(|highlight| {
|
||||
let Label { span, label, ty } = highlight;
|
||||
let Label {
|
||||
span,
|
||||
label,
|
||||
ty,
|
||||
primary,
|
||||
} = highlight;
|
||||
let var = quote! { __miette_internal_var };
|
||||
let ctor = if *primary {
|
||||
quote! { miette::LabeledSpan::new_primary_with_span }
|
||||
} else {
|
||||
quote! { miette::LabeledSpan::new_with_span }
|
||||
};
|
||||
if let Some(display) = label {
|
||||
let (fmt, args) = display.expand_shorthand_cloned(&display_members);
|
||||
quote! {
|
||||
miette::macro_helpers::OptionalWrapper::<#ty>::new().to_option(&self.#span)
|
||||
.map(|#var| miette::LabeledSpan::new_with_span(
|
||||
.map(|#var| #ctor(
|
||||
std::option::Option::Some(format!(#fmt #args)),
|
||||
#var.clone(),
|
||||
))
|
||||
|
|
@ -134,7 +172,7 @@ impl Labels {
|
|||
} else {
|
||||
quote! {
|
||||
miette::macro_helpers::OptionalWrapper::<#ty>::new().to_option(&self.#span)
|
||||
.map(|#var| miette::LabeledSpan::new_with_span(
|
||||
.map(|#var| #ctor(
|
||||
std::option::Option::None,
|
||||
#var.clone(),
|
||||
))
|
||||
|
|
@ -161,7 +199,7 @@ impl Labels {
|
|||
let (display_pat, display_members) = display_pat_members(fields);
|
||||
labels.as_ref().and_then(|labels| {
|
||||
let variant_labels = labels.0.iter().map(|label| {
|
||||
let Label { span, label, ty } = label;
|
||||
let Label { span, label, ty, primary } = label;
|
||||
let field = match &span {
|
||||
syn::Member::Named(ident) => ident.clone(),
|
||||
syn::Member::Unnamed(syn::Index { index, .. }) => {
|
||||
|
|
@ -169,11 +207,16 @@ impl Labels {
|
|||
}
|
||||
};
|
||||
let var = quote! { __miette_internal_var };
|
||||
let ctor = if *primary {
|
||||
quote! { miette::LabeledSpan::new_primary_with_span }
|
||||
} else {
|
||||
quote! { miette::LabeledSpan::new_with_span }
|
||||
};
|
||||
if let Some(display) = label {
|
||||
let (fmt, args) = display.expand_shorthand_cloned(&display_members);
|
||||
quote! {
|
||||
miette::macro_helpers::OptionalWrapper::<#ty>::new().to_option(#field)
|
||||
.map(|#var| miette::LabeledSpan::new_with_span(
|
||||
.map(|#var| #ctor(
|
||||
std::option::Option::Some(format!(#fmt #args)),
|
||||
#var.clone(),
|
||||
))
|
||||
|
|
@ -181,7 +224,7 @@ impl Labels {
|
|||
} else {
|
||||
quote! {
|
||||
miette::macro_helpers::OptionalWrapper::<#ty>::new().to_option(#field)
|
||||
.map(|#var| miette::LabeledSpan::new_with_span(
|
||||
.map(|#var| #ctor(
|
||||
std::option::Option::None,
|
||||
#var.clone(),
|
||||
))
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ use crate::{
|
|||
|
||||
pub struct SourceCode {
|
||||
source_code: syn::Member,
|
||||
is_option: bool,
|
||||
}
|
||||
|
||||
impl SourceCode {
|
||||
|
|
@ -27,6 +28,19 @@ impl SourceCode {
|
|||
for (i, field) in fields.iter().enumerate() {
|
||||
for attr in &field.attrs {
|
||||
if attr.path().is_ident("source_code") {
|
||||
let is_option = if let syn::Type::Path(syn::TypePath {
|
||||
path: syn::Path { segments, .. },
|
||||
..
|
||||
}) = &field.ty
|
||||
{
|
||||
segments
|
||||
.last()
|
||||
.map(|seg| seg.ident == "Option")
|
||||
.unwrap_or(false)
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
let source_code = if let Some(ident) = field.ident.clone() {
|
||||
syn::Member::Named(ident)
|
||||
} else {
|
||||
|
|
@ -35,7 +49,10 @@ impl SourceCode {
|
|||
span: field.span(),
|
||||
})
|
||||
};
|
||||
return Ok(Some(SourceCode { source_code }));
|
||||
return Ok(Some(SourceCode {
|
||||
source_code,
|
||||
is_option,
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -45,11 +62,21 @@ impl SourceCode {
|
|||
pub(crate) fn gen_struct(&self, fields: &syn::Fields) -> Option<TokenStream> {
|
||||
let (display_pat, _display_members) = display_pat_members(fields);
|
||||
let src = &self.source_code;
|
||||
let ret = if self.is_option {
|
||||
quote! {
|
||||
self.#src.as_ref().map(|s| s as _)
|
||||
}
|
||||
} else {
|
||||
quote! {
|
||||
Some(&self.#src)
|
||||
}
|
||||
};
|
||||
|
||||
Some(quote! {
|
||||
#[allow(unused_variables)]
|
||||
fn source_code(&self) -> std::option::Option<&dyn miette::SourceCode> {
|
||||
let Self #display_pat = self;
|
||||
Some(&self.#src)
|
||||
#ret
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -68,10 +95,19 @@ impl SourceCode {
|
|||
}
|
||||
};
|
||||
let variant_name = ident.clone();
|
||||
let ret = if source_code.is_option {
|
||||
quote! {
|
||||
#field.as_ref().map(|s| s as _)
|
||||
}
|
||||
} else {
|
||||
quote! {
|
||||
std::option::Option::Some(#field)
|
||||
}
|
||||
};
|
||||
match &fields {
|
||||
syn::Fields::Unit => None,
|
||||
_ => Some(quote! {
|
||||
Self::#variant_name #display_pat => std::option::Option::Some(#field),
|
||||
Self::#variant_name #display_pat => #ret,
|
||||
}),
|
||||
}
|
||||
})
|
||||
|
|
|
|||
42
src/error.rs
42
src/error.rs
|
|
@ -1,27 +1,51 @@
|
|||
use std::io;
|
||||
use std::{fmt, io};
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::{self as miette, Diagnostic};
|
||||
use crate::Diagnostic;
|
||||
|
||||
/**
|
||||
Error enum for miette. Used by certain operations in the protocol.
|
||||
*/
|
||||
#[derive(Debug, Diagnostic, Error)]
|
||||
#[derive(Debug, Error)]
|
||||
pub enum MietteError {
|
||||
/// Wrapper around [`std::io::Error`]. This is returned when something went
|
||||
/// wrong while reading a [`SourceCode`](crate::SourceCode).
|
||||
#[error(transparent)]
|
||||
#[diagnostic(code(miette::io_error), url(docsrs))]
|
||||
IoError(#[from] io::Error),
|
||||
|
||||
/// Returned when a [`SourceSpan`](crate::SourceSpan) extends beyond the
|
||||
/// bounds of a given [`SourceCode`](crate::SourceCode).
|
||||
#[error("The given offset is outside the bounds of its Source")]
|
||||
#[diagnostic(
|
||||
code(miette::span_out_of_bounds),
|
||||
help("Double-check your spans. Do you have an off-by-one error?"),
|
||||
url(docsrs)
|
||||
)]
|
||||
OutOfBounds,
|
||||
}
|
||||
|
||||
impl Diagnostic for MietteError {
|
||||
fn code<'a>(&'a self) -> Option<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,
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,9 +30,9 @@ impl Report {
|
|||
|
||||
/// Create a new error object from a printable error message.
|
||||
///
|
||||
/// If the argument implements std::error::Error, prefer `Report::new`
|
||||
/// If the argument implements [`std::error::Error`], prefer `Report::new`
|
||||
/// instead which preserves the underlying error's cause chain and
|
||||
/// backtrace. If the argument may or may not implement std::error::Error
|
||||
/// backtrace. If the argument may or may not implement [`std::error::Error`]
|
||||
/// now or in the future, use `miette!(err)` which handles either way
|
||||
/// correctly.
|
||||
///
|
||||
|
|
@ -206,7 +206,7 @@ impl Report {
|
|||
/// Create a new error from an error message to wrap the existing error.
|
||||
///
|
||||
/// For attaching a higher level error message to a `Result` as it is
|
||||
/// propagated, the [crate::WrapErr] extension trait may be more
|
||||
/// propagated, the [`WrapErr`](crate::WrapErr) extension trait may be more
|
||||
/// convenient than this function.
|
||||
///
|
||||
/// The primary reason to use `error.wrap_err(...)` instead of
|
||||
|
|
@ -233,7 +233,7 @@ impl Report {
|
|||
unsafe { Report::construct(error, vtable, handler) }
|
||||
}
|
||||
|
||||
/// Compatibility re-export of wrap_err for interop with `anyhow`
|
||||
/// Compatibility re-export of `wrap_err` for interop with `anyhow`
|
||||
pub fn context<D>(self, msg: D) -> Self
|
||||
where
|
||||
D: Display + Send + Sync + 'static,
|
||||
|
|
|
|||
|
|
@ -222,6 +222,9 @@ macro_rules! ensure {
|
|||
/// ## `anyhow`/`eyre` Users
|
||||
///
|
||||
/// You can just replace `use`s of the `anyhow!`/`eyre!` macros with `miette!`.
|
||||
///
|
||||
/// [`diagnostic!`]: crate::diagnostic!
|
||||
/// [`Report`]: crate::Report
|
||||
#[macro_export]
|
||||
macro_rules! miette {
|
||||
($($key:ident = $value:expr,)* $fmt:literal $($arg:tt)*) => {
|
||||
|
|
@ -282,6 +285,8 @@ macro_rules! miette {
|
|||
)]
|
||||
/// assert_eq!(diag.message, "1 + 2 = 3");
|
||||
/// ```
|
||||
///
|
||||
/// [`MietteDiagnostic`]: crate::MietteDiagnostic
|
||||
#[macro_export]
|
||||
macro_rules! diagnostic {
|
||||
($fmt:literal $($arg:tt)*) => {{
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ where
|
|||
Box::from_raw(self.ptr.as_ptr())
|
||||
}
|
||||
|
||||
pub(crate) fn by_ref<'a>(&self) -> Ref<'a, T> {
|
||||
pub(crate) const fn by_ref<'a>(&self) -> Ref<'a, T> {
|
||||
Ref {
|
||||
ptr: self.ptr,
|
||||
lifetime: PhantomData,
|
||||
|
|
@ -91,7 +91,7 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
pub(crate) fn from_raw(ptr: NonNull<T>) -> Self {
|
||||
pub(crate) const fn from_raw(ptr: NonNull<T>) -> Self {
|
||||
Ref {
|
||||
ptr,
|
||||
lifetime: PhantomData,
|
||||
|
|
@ -112,7 +112,7 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
pub(crate) fn as_ptr(self) -> *const T {
|
||||
pub(crate) const fn as_ptr(self) -> *const T {
|
||||
self.ptr.as_ptr() as *const T
|
||||
}
|
||||
|
||||
|
|
@ -154,7 +154,7 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
pub(crate) fn by_ref(self) -> Ref<'a, T> {
|
||||
pub(crate) const fn by_ref(self) -> Ref<'a, T> {
|
||||
Ref {
|
||||
ptr: self.ptr,
|
||||
lifetime: PhantomData,
|
||||
|
|
|
|||
|
|
@ -55,6 +55,9 @@ pub struct MietteHandlerOpts {
|
|||
pub(crate) context_lines: Option<usize>,
|
||||
pub(crate) tab_width: Option<usize>,
|
||||
pub(crate) with_cause_chain: Option<bool>,
|
||||
pub(crate) break_words: Option<bool>,
|
||||
pub(crate) word_separator: Option<textwrap::WordSeparator>,
|
||||
pub(crate) word_splitter: Option<textwrap::WordSplitter>,
|
||||
}
|
||||
|
||||
impl MietteHandlerOpts {
|
||||
|
|
@ -86,6 +89,27 @@ impl MietteHandlerOpts {
|
|||
self
|
||||
}
|
||||
|
||||
/// If true, long words can be broken when wrapping.
|
||||
///
|
||||
/// If false, long words will not be broken when they exceed the width.
|
||||
///
|
||||
/// Defaults to true.
|
||||
pub fn break_words(mut self, break_words: bool) -> Self {
|
||||
self.break_words = Some(break_words);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the `textwrap::WordSeparator` to use when determining wrap points.
|
||||
pub fn word_separator(mut self, word_separator: textwrap::WordSeparator) -> Self {
|
||||
self.word_separator = Some(word_separator);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the `textwrap::WordSplitter` to use when determining wrap points.
|
||||
pub fn word_splitter(mut self, word_splitter: textwrap::WordSplitter) -> Self {
|
||||
self.word_splitter = Some(word_splitter);
|
||||
self
|
||||
}
|
||||
/// Include the cause chain of the top-level error in the report.
|
||||
pub fn with_cause_chain(mut self) -> Self {
|
||||
self.with_cause_chain = Some(true);
|
||||
|
|
@ -233,6 +257,16 @@ impl MietteHandlerOpts {
|
|||
if let Some(w) = self.tab_width {
|
||||
handler = handler.tab_width(w);
|
||||
}
|
||||
if let Some(b) = self.break_words {
|
||||
handler = handler.with_break_words(b)
|
||||
}
|
||||
if let Some(s) = self.word_separator {
|
||||
handler = handler.with_word_separator(s)
|
||||
}
|
||||
if let Some(s) = self.word_splitter {
|
||||
handler = handler.with_word_splitter(s)
|
||||
}
|
||||
|
||||
MietteHandler {
|
||||
inner: Box::new(handler),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ pub struct DebugReportHandler;
|
|||
impl DebugReportHandler {
|
||||
/// Create a new [`NarratableReportHandler`](crate::NarratableReportHandler)
|
||||
/// There are no customization options.
|
||||
pub fn new() -> Self {
|
||||
pub const fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ use std::fmt::{self, Write};
|
|||
use owo_colors::{OwoColorize, Style};
|
||||
use unicode_width::UnicodeWidthChar;
|
||||
|
||||
use crate::diagnostic_chain::DiagnosticChain;
|
||||
use crate::diagnostic_chain::{DiagnosticChain, ErrorKind};
|
||||
use crate::handlers::theme::*;
|
||||
use crate::protocol::{Diagnostic, Severity};
|
||||
use crate::{LabeledSpan, MietteError, ReportHandler, SourceCode, SourceSpan, SpanContents};
|
||||
|
|
@ -20,7 +20,7 @@ This printer can be customized by using [`new_themed()`](GraphicalReportHandler:
|
|||
|
||||
See [`set_hook()`](crate::set_hook) for more details on customizing your global
|
||||
printer.
|
||||
*/
|
||||
*/
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct GraphicalReportHandler {
|
||||
pub(crate) links: LinkStyle,
|
||||
|
|
@ -30,6 +30,9 @@ pub struct GraphicalReportHandler {
|
|||
pub(crate) context_lines: usize,
|
||||
pub(crate) tab_width: usize,
|
||||
pub(crate) with_cause_chain: bool,
|
||||
pub(crate) break_words: bool,
|
||||
pub(crate) word_separator: Option<textwrap::WordSeparator>,
|
||||
pub(crate) word_splitter: Option<textwrap::WordSplitter>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
|
|
@ -51,6 +54,9 @@ impl GraphicalReportHandler {
|
|||
context_lines: 1,
|
||||
tab_width: 4,
|
||||
with_cause_chain: true,
|
||||
break_words: true,
|
||||
word_separator: None,
|
||||
word_splitter: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -64,6 +70,9 @@ impl GraphicalReportHandler {
|
|||
context_lines: 1,
|
||||
tab_width: 4,
|
||||
with_cause_chain: true,
|
||||
break_words: true,
|
||||
word_separator: None,
|
||||
word_splitter: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -122,6 +131,24 @@ impl GraphicalReportHandler {
|
|||
self
|
||||
}
|
||||
|
||||
/// Enables or disables breaking of words during wrapping.
|
||||
pub fn with_break_words(mut self, break_words: bool) -> Self {
|
||||
self.break_words = break_words;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the word separator to use when wrapping.
|
||||
pub fn with_word_separator(mut self, word_separator: textwrap::WordSeparator) -> Self {
|
||||
self.word_separator = Some(word_separator);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the word splitter to usewhen wrapping.
|
||||
pub fn with_word_splitter(mut self, word_splitter: textwrap::WordSplitter) -> Self {
|
||||
self.word_splitter = Some(word_splitter);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the 'global' footer for this handler.
|
||||
pub fn with_footer(mut self, footer: String) -> Self {
|
||||
self.footer = Some(footer);
|
||||
|
|
@ -151,7 +178,6 @@ impl GraphicalReportHandler {
|
|||
diagnostic: &(dyn Diagnostic),
|
||||
) -> fmt::Result {
|
||||
self.render_header(f, diagnostic)?;
|
||||
writeln!(f)?;
|
||||
self.render_causes(f, diagnostic)?;
|
||||
let src = diagnostic.source_code();
|
||||
self.render_snippets(f, diagnostic, src)?;
|
||||
|
|
@ -160,9 +186,17 @@ impl GraphicalReportHandler {
|
|||
if let Some(footer) = &self.footer {
|
||||
writeln!(f)?;
|
||||
let width = self.termwidth.saturating_sub(4);
|
||||
let opts = textwrap::Options::new(width)
|
||||
let mut opts = textwrap::Options::new(width)
|
||||
.initial_indent(" ")
|
||||
.subsequent_indent(" ");
|
||||
.subsequent_indent(" ")
|
||||
.break_words(self.break_words);
|
||||
if let Some(word_separator) = self.word_separator {
|
||||
opts = opts.word_separator(word_separator);
|
||||
}
|
||||
if let Some(word_splitter) = self.word_splitter.clone() {
|
||||
opts = opts.word_splitter(word_splitter);
|
||||
}
|
||||
|
||||
writeln!(f, "{}", textwrap::fill(footer, opts))?;
|
||||
}
|
||||
Ok(())
|
||||
|
|
@ -190,6 +224,7 @@ impl GraphicalReportHandler {
|
|||
);
|
||||
write!(header, "{}", link)?;
|
||||
writeln!(f, "{}", header)?;
|
||||
writeln!(f)?;
|
||||
} else if let Some(code) = diagnostic.code() {
|
||||
write!(header, "{}", code.style(severity_style),)?;
|
||||
if self.links == LinkStyle::Text && diagnostic.url().is_some() {
|
||||
|
|
@ -197,6 +232,7 @@ impl GraphicalReportHandler {
|
|||
write!(header, " ({})", url.style(self.theme.styles.link))?;
|
||||
}
|
||||
writeln!(f, "{}", header)?;
|
||||
writeln!(f)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -211,9 +247,16 @@ impl GraphicalReportHandler {
|
|||
let initial_indent = format!(" {} ", severity_icon.style(severity_style));
|
||||
let rest_indent = format!(" {} ", self.theme.characters.vbar.style(severity_style));
|
||||
let width = self.termwidth.saturating_sub(2);
|
||||
let opts = textwrap::Options::new(width)
|
||||
let mut opts = textwrap::Options::new(width)
|
||||
.initial_indent(&initial_indent)
|
||||
.subsequent_indent(&rest_indent);
|
||||
.subsequent_indent(&rest_indent)
|
||||
.break_words(self.break_words);
|
||||
if let Some(word_separator) = self.word_separator {
|
||||
opts = opts.word_separator(word_separator);
|
||||
}
|
||||
if let Some(word_splitter) = self.word_splitter.clone() {
|
||||
opts = opts.word_splitter(word_splitter);
|
||||
}
|
||||
|
||||
writeln!(f, "{}", textwrap::fill(&diagnostic.to_string(), opts))?;
|
||||
|
||||
|
|
@ -250,10 +293,33 @@ impl GraphicalReportHandler {
|
|||
)
|
||||
.style(severity_style)
|
||||
.to_string();
|
||||
let opts = textwrap::Options::new(width)
|
||||
let mut opts = textwrap::Options::new(width)
|
||||
.initial_indent(&initial_indent)
|
||||
.subsequent_indent(&rest_indent);
|
||||
writeln!(f, "{}", textwrap::fill(&error.to_string(), opts))?;
|
||||
.subsequent_indent(&rest_indent)
|
||||
.break_words(self.break_words);
|
||||
if let Some(word_separator) = self.word_separator {
|
||||
opts = opts.word_separator(word_separator);
|
||||
}
|
||||
if let Some(word_splitter) = self.word_splitter.clone() {
|
||||
opts = opts.word_splitter(word_splitter);
|
||||
}
|
||||
|
||||
match error {
|
||||
ErrorKind::Diagnostic(diag) => {
|
||||
let mut inner = String::new();
|
||||
|
||||
// Don't print footer for inner errors
|
||||
let mut inner_renderer = self.clone();
|
||||
inner_renderer.footer = None;
|
||||
inner_renderer.with_cause_chain = false;
|
||||
inner_renderer.render_report(&mut inner, diag)?;
|
||||
|
||||
writeln!(f, "{}", textwrap::fill(&inner, opts))?;
|
||||
}
|
||||
ErrorKind::StdError(err) => {
|
||||
writeln!(f, "{}", textwrap::fill(&err.to_string(), opts))?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -264,9 +330,17 @@ impl GraphicalReportHandler {
|
|||
if let Some(help) = diagnostic.help() {
|
||||
let width = self.termwidth.saturating_sub(4);
|
||||
let initial_indent = " help: ".style(self.theme.styles.help).to_string();
|
||||
let opts = textwrap::Options::new(width)
|
||||
let mut opts = textwrap::Options::new(width)
|
||||
.initial_indent(&initial_indent)
|
||||
.subsequent_indent(" ");
|
||||
.subsequent_indent(" ")
|
||||
.break_words(self.break_words);
|
||||
if let Some(word_separator) = self.word_separator {
|
||||
opts = opts.word_separator(word_separator);
|
||||
}
|
||||
if let Some(word_splitter) = self.word_splitter.clone() {
|
||||
opts = opts.word_splitter(word_splitter);
|
||||
}
|
||||
|
||||
writeln!(f, "{}", textwrap::fill(&help.to_string(), opts))?;
|
||||
}
|
||||
Ok(())
|
||||
|
|
@ -287,7 +361,6 @@ impl GraphicalReportHandler {
|
|||
Some(Severity::Advice) => write!(f, "Advice: ")?,
|
||||
};
|
||||
self.render_header(f, rel)?;
|
||||
writeln!(f)?;
|
||||
self.render_causes(f, rel)?;
|
||||
let src = rel.source_code().or(parent_src);
|
||||
self.render_snippets(f, rel, src)?;
|
||||
|
|
@ -367,15 +440,20 @@ impl GraphicalReportHandler {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn render_context<'a>(
|
||||
fn render_context(
|
||||
&self,
|
||||
f: &mut impl fmt::Write,
|
||||
source: &'a dyn SourceCode,
|
||||
source: &dyn SourceCode,
|
||||
context: &LabeledSpan,
|
||||
labels: &[LabeledSpan],
|
||||
) -> fmt::Result {
|
||||
let (contents, lines) = self.get_lines(source, context.inner())?;
|
||||
|
||||
let primary_label = labels
|
||||
.iter()
|
||||
.find(|label| label.primary())
|
||||
.or_else(|| labels.first());
|
||||
|
||||
// sorting is your friend
|
||||
let labels = labels
|
||||
.iter()
|
||||
|
|
@ -416,19 +494,33 @@ impl GraphicalReportHandler {
|
|||
self.theme.characters.hbar,
|
||||
)?;
|
||||
|
||||
if let Some(source_name) = contents.name() {
|
||||
// If there is a primary label, then use its span
|
||||
// as the reference point for line/column information.
|
||||
let primary_contents = match primary_label {
|
||||
Some(label) => source
|
||||
.read_span(label.inner(), 0, 0)
|
||||
.map_err(|_| fmt::Error)?,
|
||||
None => contents,
|
||||
};
|
||||
|
||||
if let Some(source_name) = primary_contents.name() {
|
||||
let source_name = source_name.style(self.theme.styles.link);
|
||||
writeln!(
|
||||
f,
|
||||
"[{}:{}:{}]",
|
||||
source_name,
|
||||
contents.line() + 1,
|
||||
contents.column() + 1
|
||||
primary_contents.line() + 1,
|
||||
primary_contents.column() + 1
|
||||
)?;
|
||||
} else if lines.len() <= 1 {
|
||||
writeln!(f, "{}", self.theme.characters.hbar.to_string().repeat(3))?;
|
||||
} else {
|
||||
writeln!(f, "[{}:{}]", contents.line() + 1, contents.column() + 1)?;
|
||||
writeln!(
|
||||
f,
|
||||
"[{}:{}]",
|
||||
primary_contents.line() + 1,
|
||||
primary_contents.column() + 1
|
||||
)?;
|
||||
}
|
||||
|
||||
// Now it's time for the fun part--actually rendering everything!
|
||||
|
|
@ -453,7 +545,13 @@ impl GraphicalReportHandler {
|
|||
// no line number!
|
||||
self.write_no_linum(f, linum_width)?;
|
||||
// gutter _again_
|
||||
self.render_highlight_gutter(f, max_gutter, line, &labels)?;
|
||||
self.render_highlight_gutter(
|
||||
f,
|
||||
max_gutter,
|
||||
line,
|
||||
&labels,
|
||||
LabelRenderMode::SingleLine,
|
||||
)?;
|
||||
self.render_single_line_highlights(
|
||||
f,
|
||||
line,
|
||||
|
|
@ -465,11 +563,7 @@ impl GraphicalReportHandler {
|
|||
}
|
||||
for hl in multi_line {
|
||||
if hl.label().is_some() && line.span_ends(hl) && !line.span_starts(hl) {
|
||||
// no line number!
|
||||
self.write_no_linum(f, linum_width)?;
|
||||
// gutter _again_
|
||||
self.render_highlight_gutter(f, max_gutter, line, &labels)?;
|
||||
self.render_multi_line_end(f, hl)?;
|
||||
self.render_multi_line_end(f, &labels, max_gutter, linum_width, line, hl)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -483,6 +577,91 @@ impl GraphicalReportHandler {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn render_multi_line_end(
|
||||
&self,
|
||||
f: &mut impl fmt::Write,
|
||||
labels: &[FancySpan],
|
||||
max_gutter: usize,
|
||||
linum_width: usize,
|
||||
line: &Line,
|
||||
label: &FancySpan,
|
||||
) -> fmt::Result {
|
||||
// no line number!
|
||||
self.write_no_linum(f, linum_width)?;
|
||||
|
||||
if let Some(label_parts) = label.label_parts() {
|
||||
// if it has a label, how long is it?
|
||||
let (first, rest) = label_parts
|
||||
.split_first()
|
||||
.expect("cannot crash because rest would have been None, see docs on the `label` field of FancySpan");
|
||||
|
||||
if rest.is_empty() {
|
||||
// gutter _again_
|
||||
self.render_highlight_gutter(
|
||||
f,
|
||||
max_gutter,
|
||||
line,
|
||||
&labels,
|
||||
LabelRenderMode::SingleLine,
|
||||
)?;
|
||||
|
||||
self.render_multi_line_end_single(
|
||||
f,
|
||||
first,
|
||||
label.style,
|
||||
LabelRenderMode::SingleLine,
|
||||
)?;
|
||||
} else {
|
||||
// gutter _again_
|
||||
self.render_highlight_gutter(
|
||||
f,
|
||||
max_gutter,
|
||||
line,
|
||||
&labels,
|
||||
LabelRenderMode::MultiLineFirst,
|
||||
)?;
|
||||
|
||||
self.render_multi_line_end_single(
|
||||
f,
|
||||
first,
|
||||
label.style,
|
||||
LabelRenderMode::MultiLineFirst,
|
||||
)?;
|
||||
for label_line in rest {
|
||||
// no line number!
|
||||
self.write_no_linum(f, linum_width)?;
|
||||
// gutter _again_
|
||||
self.render_highlight_gutter(
|
||||
f,
|
||||
max_gutter,
|
||||
line,
|
||||
&labels,
|
||||
LabelRenderMode::MultiLineRest,
|
||||
)?;
|
||||
self.render_multi_line_end_single(
|
||||
f,
|
||||
label_line,
|
||||
label.style,
|
||||
LabelRenderMode::MultiLineRest,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// gutter _again_
|
||||
self.render_highlight_gutter(
|
||||
f,
|
||||
max_gutter,
|
||||
line,
|
||||
&labels,
|
||||
LabelRenderMode::SingleLine,
|
||||
)?;
|
||||
// has no label
|
||||
writeln!(f, "{}", self.theme.characters.hbar.style(label.style))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn render_line_gutter(
|
||||
&self,
|
||||
f: &mut impl fmt::Write,
|
||||
|
|
@ -551,6 +730,7 @@ impl GraphicalReportHandler {
|
|||
max_gutter: usize,
|
||||
line: &Line,
|
||||
highlights: &[FancySpan],
|
||||
render_mode: LabelRenderMode,
|
||||
) -> fmt::Result {
|
||||
if max_gutter == 0 {
|
||||
return Ok(());
|
||||
|
|
@ -560,15 +740,33 @@ impl GraphicalReportHandler {
|
|||
let applicable = highlights.iter().filter(|hl| line.span_applies(hl));
|
||||
for (i, hl) in applicable.enumerate() {
|
||||
if !line.span_line_only(hl) && line.span_ends(hl) {
|
||||
gutter.push_str(&chars.lbot.style(hl.style).to_string());
|
||||
gutter.push_str(
|
||||
&chars
|
||||
.hbar
|
||||
.to_string()
|
||||
.repeat(max_gutter.saturating_sub(i) + 2)
|
||||
.style(hl.style)
|
||||
.to_string(),
|
||||
);
|
||||
if render_mode == LabelRenderMode::MultiLineRest {
|
||||
// this is to make multiline labels work. We want to make the right amount
|
||||
// of horizontal space for them, but not actually draw the lines
|
||||
for _ in 0..max_gutter.saturating_sub(i) + 2 {
|
||||
gutter.push(' ');
|
||||
}
|
||||
} else {
|
||||
gutter.push_str(&chars.lbot.style(hl.style).to_string());
|
||||
|
||||
gutter.push_str(
|
||||
&chars
|
||||
.hbar
|
||||
.to_string()
|
||||
.repeat(
|
||||
max_gutter.saturating_sub(i)
|
||||
// if we are rendering a multiline label, then leave a bit of space for the
|
||||
// rcross character
|
||||
+ if render_mode == LabelRenderMode::MultiLineFirst {
|
||||
1
|
||||
} else {
|
||||
2
|
||||
},
|
||||
)
|
||||
.style(hl.style)
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
break;
|
||||
} else {
|
||||
gutter.push_str(&chars.vbar.style(hl.style).to_string());
|
||||
|
|
@ -617,11 +815,22 @@ impl GraphicalReportHandler {
|
|||
}
|
||||
|
||||
/// Returns the visual column position of a byte offset on a specific line.
|
||||
fn visual_offset(&self, line: &Line, offset: usize) -> usize {
|
||||
///
|
||||
/// If the offset occurs in the middle of a character, the returned column
|
||||
/// corresponds to that character's first column in `start` is true, or its
|
||||
/// last column if `start` is false.
|
||||
fn visual_offset(&self, line: &Line, offset: usize, start: bool) -> usize {
|
||||
let line_range = line.offset..=(line.offset + line.length);
|
||||
assert!(line_range.contains(&offset));
|
||||
|
||||
let text_index = offset - line.offset;
|
||||
let mut text_index = offset - line.offset;
|
||||
while text_index <= line.text.len() && !line.text.is_char_boundary(text_index) {
|
||||
if start {
|
||||
text_index -= 1;
|
||||
} else {
|
||||
text_index += 1;
|
||||
}
|
||||
}
|
||||
let text = &line.text[..text_index.min(line.text.len())];
|
||||
let text_width = self.line_visual_char_width(text).sum();
|
||||
if text_index > line.text.len() {
|
||||
|
|
@ -644,10 +853,10 @@ impl GraphicalReportHandler {
|
|||
for (c, width) in text.chars().zip(self.line_visual_char_width(text)) {
|
||||
if c == '\t' {
|
||||
for _ in 0..width {
|
||||
f.write_char(' ')?
|
||||
f.write_char(' ')?;
|
||||
}
|
||||
} else {
|
||||
f.write_char(c)?
|
||||
f.write_char(c)?;
|
||||
}
|
||||
}
|
||||
f.write_char('\n')?;
|
||||
|
|
@ -672,32 +881,34 @@ impl GraphicalReportHandler {
|
|||
.map(|hl| {
|
||||
let byte_start = hl.offset();
|
||||
let byte_end = hl.offset() + hl.len();
|
||||
let start = self.visual_offset(line, byte_start).max(highest);
|
||||
let end = self.visual_offset(line, byte_end).max(start + 1);
|
||||
let start = self.visual_offset(line, byte_start, true).max(highest);
|
||||
let end = if hl.len() == 0 {
|
||||
start + 1
|
||||
} else {
|
||||
self.visual_offset(line, byte_end, false).max(start + 1)
|
||||
};
|
||||
|
||||
let vbar_offset = (start + end) / 2;
|
||||
let num_left = vbar_offset - start;
|
||||
let num_right = end - vbar_offset - 1;
|
||||
if start < end {
|
||||
underlines.push_str(
|
||||
&format!(
|
||||
"{:width$}{}{}{}",
|
||||
"",
|
||||
chars.underline.to_string().repeat(num_left),
|
||||
if hl.len() == 0 {
|
||||
chars.uarrow
|
||||
} else if hl.label().is_some() {
|
||||
chars.underbar
|
||||
} else {
|
||||
chars.underline
|
||||
},
|
||||
chars.underline.to_string().repeat(num_right),
|
||||
width = start.saturating_sub(highest),
|
||||
)
|
||||
.style(hl.style)
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
underlines.push_str(
|
||||
&format!(
|
||||
"{:width$}{}{}{}",
|
||||
"",
|
||||
chars.underline.to_string().repeat(num_left),
|
||||
if hl.len() == 0 {
|
||||
chars.uarrow
|
||||
} else if hl.label().is_some() {
|
||||
chars.underbar
|
||||
} else {
|
||||
chars.underline
|
||||
},
|
||||
chars.underline.to_string().repeat(num_right),
|
||||
width = start.saturating_sub(highest),
|
||||
)
|
||||
.style(hl.style)
|
||||
.to_string(),
|
||||
);
|
||||
highest = std::cmp::max(highest, end);
|
||||
|
||||
(hl, vbar_offset)
|
||||
|
|
@ -706,27 +917,40 @@ impl GraphicalReportHandler {
|
|||
writeln!(f, "{}", underlines)?;
|
||||
|
||||
for hl in single_liners.iter().rev() {
|
||||
if let Some(label) = hl.label() {
|
||||
self.write_no_linum(f, linum_width)?;
|
||||
self.render_highlight_gutter(f, max_gutter, line, all_highlights)?;
|
||||
let mut curr_offset = 1usize;
|
||||
for (offset_hl, vbar_offset) in &vbar_offsets {
|
||||
while curr_offset < *vbar_offset + 1 {
|
||||
write!(f, " ")?;
|
||||
curr_offset += 1;
|
||||
}
|
||||
if *offset_hl != hl {
|
||||
write!(f, "{}", chars.vbar.to_string().style(offset_hl.style))?;
|
||||
curr_offset += 1;
|
||||
} else {
|
||||
let lines = format!(
|
||||
"{}{} {}",
|
||||
chars.lbot,
|
||||
chars.hbar.to_string().repeat(2),
|
||||
label,
|
||||
);
|
||||
writeln!(f, "{}", lines.style(hl.style))?;
|
||||
break;
|
||||
if let Some(label) = hl.label_parts() {
|
||||
if label.len() == 1 {
|
||||
self.write_label_text(
|
||||
f,
|
||||
line,
|
||||
linum_width,
|
||||
max_gutter,
|
||||
all_highlights,
|
||||
chars,
|
||||
&vbar_offsets,
|
||||
hl,
|
||||
&label[0],
|
||||
LabelRenderMode::SingleLine,
|
||||
)?;
|
||||
} else {
|
||||
let mut first = true;
|
||||
for label_line in &label {
|
||||
self.write_label_text(
|
||||
f,
|
||||
line,
|
||||
linum_width,
|
||||
max_gutter,
|
||||
all_highlights,
|
||||
chars,
|
||||
&vbar_offsets,
|
||||
hl,
|
||||
label_line,
|
||||
if first {
|
||||
LabelRenderMode::MultiLineFirst
|
||||
} else {
|
||||
LabelRenderMode::MultiLineRest
|
||||
},
|
||||
)?;
|
||||
first = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -734,13 +958,80 @@ impl GraphicalReportHandler {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn render_multi_line_end(&self, f: &mut impl fmt::Write, hl: &FancySpan) -> fmt::Result {
|
||||
writeln!(
|
||||
// I know it's not good practice, but making this a function makes a lot of sense
|
||||
// and making a struct for this does not...
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn write_label_text(
|
||||
&self,
|
||||
f: &mut impl fmt::Write,
|
||||
line: &Line,
|
||||
linum_width: usize,
|
||||
max_gutter: usize,
|
||||
all_highlights: &[FancySpan],
|
||||
chars: &ThemeCharacters,
|
||||
vbar_offsets: &[(&&FancySpan, usize)],
|
||||
hl: &&FancySpan,
|
||||
label: &str,
|
||||
render_mode: LabelRenderMode,
|
||||
) -> fmt::Result {
|
||||
self.write_no_linum(f, linum_width)?;
|
||||
self.render_highlight_gutter(
|
||||
f,
|
||||
"{} {}",
|
||||
self.theme.characters.hbar.style(hl.style),
|
||||
hl.label().unwrap_or_else(|| "".into()),
|
||||
max_gutter,
|
||||
line,
|
||||
all_highlights,
|
||||
LabelRenderMode::SingleLine,
|
||||
)?;
|
||||
let mut curr_offset = 1usize;
|
||||
for (offset_hl, vbar_offset) in vbar_offsets {
|
||||
while curr_offset < *vbar_offset + 1 {
|
||||
write!(f, " ")?;
|
||||
curr_offset += 1;
|
||||
}
|
||||
if *offset_hl != hl {
|
||||
write!(f, "{}", chars.vbar.to_string().style(offset_hl.style))?;
|
||||
curr_offset += 1;
|
||||
} else {
|
||||
let lines = match render_mode {
|
||||
LabelRenderMode::SingleLine => format!(
|
||||
"{}{} {}",
|
||||
chars.lbot,
|
||||
chars.hbar.to_string().repeat(2),
|
||||
label,
|
||||
),
|
||||
LabelRenderMode::MultiLineFirst => {
|
||||
format!("{}{}{} {}", chars.lbot, chars.hbar, chars.rcross, label,)
|
||||
}
|
||||
LabelRenderMode::MultiLineRest => {
|
||||
format!(" {} {}", chars.vbar, label,)
|
||||
}
|
||||
};
|
||||
writeln!(f, "{}", lines.style(hl.style))?;
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn render_multi_line_end_single(
|
||||
&self,
|
||||
f: &mut impl fmt::Write,
|
||||
label: &str,
|
||||
style: Style,
|
||||
render_mode: LabelRenderMode,
|
||||
) -> fmt::Result {
|
||||
match render_mode {
|
||||
LabelRenderMode::SingleLine => {
|
||||
writeln!(f, "{} {}", self.theme.characters.hbar.style(style), label)?;
|
||||
}
|
||||
LabelRenderMode::MultiLineFirst => {
|
||||
writeln!(f, "{} {}", self.theme.characters.rcross.style(style), label)?;
|
||||
}
|
||||
LabelRenderMode::MultiLineRest => {
|
||||
writeln!(f, "{} {}", self.theme.characters.vbar.style(style), label)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
@ -819,6 +1110,16 @@ impl ReportHandler for GraphicalReportHandler {
|
|||
Support types
|
||||
*/
|
||||
|
||||
#[derive(PartialEq, Debug)]
|
||||
enum LabelRenderMode {
|
||||
/// we're rendering a single line label (or not rendering in any special way)
|
||||
SingleLine,
|
||||
/// we're rendering a multiline label
|
||||
MultiLineFirst,
|
||||
/// we're rendering the rest of a multiline label
|
||||
MultiLineRest,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Line {
|
||||
line_number: usize,
|
||||
|
|
@ -836,10 +1137,10 @@ impl Line {
|
|||
let spanlen = if span.len() == 0 { 1 } else { span.len() };
|
||||
// Span starts in this line
|
||||
(span.offset() >= self.offset && span.offset() < self.offset + self.length)
|
||||
// Span passes through this line
|
||||
|| (span.offset() < self.offset && span.offset() + spanlen > self.offset + self.length) //todo
|
||||
// Span ends on this line
|
||||
|| (span.offset() + spanlen > self.offset && span.offset() + spanlen <= self.offset + self.length)
|
||||
// Span passes through this line
|
||||
|| (span.offset() < self.offset && span.offset() + spanlen > self.offset + self.length) //todo
|
||||
// Span ends on this line
|
||||
|| (span.offset() + spanlen > self.offset && span.offset() + spanlen <= self.offset + self.length)
|
||||
}
|
||||
|
||||
// A 'flyby' is a multi-line span that technically covers this line, but
|
||||
|
|
@ -869,7 +1170,10 @@ impl Line {
|
|||
|
||||
#[derive(Debug, Clone)]
|
||||
struct FancySpan {
|
||||
label: Option<String>,
|
||||
/// this is deliberately an option of a vec because I wanted to be very explicit
|
||||
/// that there can also be *no* label. If there is a label, it can have multiple
|
||||
/// lines which is what the vec is for.
|
||||
label: Option<Vec<String>>,
|
||||
span: SourceSpan,
|
||||
style: Style,
|
||||
}
|
||||
|
|
@ -880,9 +1184,17 @@ impl PartialEq for FancySpan {
|
|||
}
|
||||
}
|
||||
|
||||
fn split_label(v: String) -> Vec<String> {
|
||||
v.split('\n').map(|i| i.to_string()).collect()
|
||||
}
|
||||
|
||||
impl FancySpan {
|
||||
fn new(label: Option<String>, span: SourceSpan, style: Style) -> Self {
|
||||
FancySpan { label, span, style }
|
||||
FancySpan {
|
||||
label: label.map(split_label),
|
||||
span,
|
||||
style,
|
||||
}
|
||||
}
|
||||
|
||||
fn style(&self) -> Style {
|
||||
|
|
@ -892,7 +1204,15 @@ impl FancySpan {
|
|||
fn label(&self) -> Option<String> {
|
||||
self.label
|
||||
.as_ref()
|
||||
.map(|l| l.style(self.style()).to_string())
|
||||
.map(|l| l.join("\n").style(self.style()).to_string())
|
||||
}
|
||||
|
||||
fn label_parts(&self) -> Option<Vec<String>> {
|
||||
self.label.as_ref().map(|l| {
|
||||
l.iter()
|
||||
.map(|i| i.style(self.style()).to_string())
|
||||
.collect()
|
||||
})
|
||||
}
|
||||
|
||||
fn offset(&self) -> usize {
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ pub struct JSONReportHandler;
|
|||
impl JSONReportHandler {
|
||||
/// Create a new [`JSONReportHandler`]. There are no customization
|
||||
/// options.
|
||||
pub fn new() -> Self {
|
||||
pub const fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
}
|
||||
|
|
@ -49,7 +49,7 @@ impl fmt::Display for Escape<'_> {
|
|||
}
|
||||
}
|
||||
|
||||
fn escape(input: &'_ str) -> Escape<'_> {
|
||||
const fn escape(input: &'_ str) -> Escape<'_> {
|
||||
Escape(input)
|
||||
}
|
||||
|
||||
|
|
@ -96,7 +96,7 @@ impl JSONReportHandler {
|
|||
}
|
||||
write!(f, r#""{}""#, escape(&error.to_string()))?;
|
||||
}
|
||||
write!(f, "],")?
|
||||
write!(f, "],")?;
|
||||
} else {
|
||||
write!(f, r#""causes": [],"#)?;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ pub struct NarratableReportHandler {
|
|||
impl NarratableReportHandler {
|
||||
/// Create a new [`NarratableReportHandler`]. There are no customization
|
||||
/// options.
|
||||
pub fn new() -> Self {
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
footer: None,
|
||||
context_lines: 1,
|
||||
|
|
@ -31,13 +31,13 @@ impl NarratableReportHandler {
|
|||
|
||||
/// Include the cause chain of the top-level error in the report, if
|
||||
/// available.
|
||||
pub fn with_cause_chain(mut self) -> Self {
|
||||
pub const fn with_cause_chain(mut self) -> Self {
|
||||
self.with_cause_chain = true;
|
||||
self
|
||||
}
|
||||
|
||||
/// Do not include the cause chain of the top-level error in the report.
|
||||
pub fn without_cause_chain(mut self) -> Self {
|
||||
pub const fn without_cause_chain(mut self) -> Self {
|
||||
self.with_cause_chain = false;
|
||||
self
|
||||
}
|
||||
|
|
@ -49,7 +49,7 @@ impl NarratableReportHandler {
|
|||
}
|
||||
|
||||
/// Sets the number of lines of context to show around each error.
|
||||
pub fn with_context_lines(mut self, lines: usize) -> Self {
|
||||
pub const fn with_context_lines(mut self, lines: usize) -> Self {
|
||||
self.context_lines = lines;
|
||||
self
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,9 +55,9 @@ impl GraphicalTheme {
|
|||
|
||||
/// A "basic" graphical theme that skips colors and unicode characters and
|
||||
/// just does monochrome ascii art. If you want a completely non-graphical
|
||||
/// rendering of your `Diagnostic`s, check out
|
||||
/// [crate::NarratableReportHandler], or write your own
|
||||
/// [crate::ReportHandler]!
|
||||
/// rendering of your [`Diagnostic`](crate::Diagnostic)s, check out
|
||||
/// [`NarratableReportHandler`](crate::NarratableReportHandler), or write
|
||||
/// your own [`ReportHandler`](crate::ReportHandler)
|
||||
pub fn none() -> Self {
|
||||
Self {
|
||||
characters: ThemeCharacters::ascii(),
|
||||
|
|
@ -79,7 +79,8 @@ impl Default for GraphicalTheme {
|
|||
}
|
||||
|
||||
/**
|
||||
Styles for various parts of graphical rendering for the [crate::GraphicalReportHandler].
|
||||
Styles for various parts of graphical rendering for the
|
||||
[`GraphicalReportHandler`](crate::GraphicalReportHandler).
|
||||
*/
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ThemeStyles {
|
||||
|
|
@ -159,7 +160,7 @@ impl ThemeStyles {
|
|||
// https://github.com/zesterer/ariadne/blob/e3cb394cb56ecda116a0a1caecd385a49e7f6662/src/draw.rs
|
||||
|
||||
/// Characters to be used when drawing when using
|
||||
/// [crate::GraphicalReportHandler].
|
||||
/// [`GraphicalReportHandler`](crate::GraphicalReportHandler).
|
||||
#[allow(missing_docs)]
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
pub struct ThemeCharacters {
|
||||
|
|
|
|||
33
src/lib.rs
33
src/lib.rs
|
|
@ -186,7 +186,7 @@
|
|||
//!
|
||||
//! ```rust
|
||||
//! // lib/error.rs
|
||||
//! use miette::Diagnostic;
|
||||
//! use miette::{Diagnostic, SourceSpan};
|
||||
//! use thiserror::Error;
|
||||
//!
|
||||
//! #[derive(Error, Diagnostic, Debug)]
|
||||
|
|
@ -198,6 +198,18 @@
|
|||
//! #[error("Oops it blew up")]
|
||||
//! #[diagnostic(code(my_lib::bad_code))]
|
||||
//! BadThingHappened,
|
||||
//!
|
||||
//! #[error(transparent)]
|
||||
//! // Use `#[diagnostic(transparent)]` to wrap another [`Diagnostic`]. You won't see labels otherwise
|
||||
//! #[diagnostic(transparent)]
|
||||
//! AnotherError(#[from] AnotherError),
|
||||
//! }
|
||||
//!
|
||||
//! #[derive(Error, Diagnostic, Debug)]
|
||||
//! #[error("another error")]
|
||||
//! pub struct AnotherError {
|
||||
//! #[label("here")]
|
||||
//! pub at: SourceSpan
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
|
|
@ -292,6 +304,23 @@
|
|||
//! miette = { version = "X.Y.Z", features = ["fancy"] }
|
||||
//! ```
|
||||
//!
|
||||
//! Another way to display a diagnostic is by printing them using the debug formatter.
|
||||
//! This is, in fact, what returning diagnostics from main ends up doing.
|
||||
//! To do it yourself, you can write the following:
|
||||
//!
|
||||
//! ```rust
|
||||
//! use miette::{IntoDiagnostic, Result};
|
||||
//! use semver::Version;
|
||||
//!
|
||||
//! fn just_a_random_function() {
|
||||
//! let version_result: Result<Version> = "1.2.x".parse().into_diagnostic();
|
||||
//! match version_result {
|
||||
//! Err(e) => println!("{:?}", e),
|
||||
//! Ok(version) => println!("{}", version),
|
||||
//! }
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! ### ... diagnostic code URLs
|
||||
//!
|
||||
//! `miette` supports providing a URL for individual diagnostics. This URL will
|
||||
|
|
@ -581,6 +610,7 @@
|
|||
//! .unicode(false)
|
||||
//! .context_lines(3)
|
||||
//! .tab_width(4)
|
||||
//! .break_words(true)
|
||||
//! .build(),
|
||||
//! )
|
||||
//! }))
|
||||
|
|
@ -640,6 +670,7 @@
|
|||
//! and some from [`thiserror`](https://github.com/dtolnay/thiserror), also
|
||||
//! under the Apache License. Some code is taken from
|
||||
//! [`ariadne`](https://github.com/zesterer/ariadne), which is MIT licensed.
|
||||
#[cfg(feature = "derive")]
|
||||
pub use miette_derive::*;
|
||||
|
||||
pub use error::*;
|
||||
|
|
|
|||
|
|
@ -252,7 +252,7 @@ impl MietteDiagnostic {
|
|||
/// ```
|
||||
pub fn and_labels(mut self, labels: impl IntoIterator<Item = LabeledSpan>) -> Self {
|
||||
let mut all_labels = self.labels.unwrap_or_default();
|
||||
all_labels.extend(labels.into_iter());
|
||||
all_labels.extend(labels);
|
||||
self.labels = Some(all_labels);
|
||||
self
|
||||
}
|
||||
|
|
@ -292,14 +292,16 @@ fn test_serialize_miette_diagnostic() {
|
|||
"offset": 0,
|
||||
"length": 0
|
||||
},
|
||||
"label": "label1"
|
||||
"label": "label1",
|
||||
"primary": false
|
||||
},
|
||||
{
|
||||
"span": {
|
||||
"offset": 1,
|
||||
"length": 2
|
||||
},
|
||||
"label": "label2"
|
||||
"label": "label2",
|
||||
"primary": false
|
||||
}
|
||||
]
|
||||
});
|
||||
|
|
@ -350,14 +352,16 @@ fn test_deserialize_miette_diagnostic() {
|
|||
"offset": 0,
|
||||
"length": 0
|
||||
},
|
||||
"label": "label1"
|
||||
"label": "label1",
|
||||
"primary": false
|
||||
},
|
||||
{
|
||||
"span": {
|
||||
"offset": 1,
|
||||
"length": 2
|
||||
},
|
||||
"label": "label2"
|
||||
"label": "label2",
|
||||
"primary": false
|
||||
}
|
||||
]
|
||||
});
|
||||
|
|
|
|||
|
|
@ -69,16 +69,28 @@ pub trait Diagnostic: std::error::Error {
|
|||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for Box<dyn Diagnostic> {
|
||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||
(**self).source()
|
||||
}
|
||||
macro_rules! box_impls {
|
||||
($($box_type:ty),*) => {
|
||||
$(
|
||||
impl std::error::Error for $box_type {
|
||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||
(**self).source()
|
||||
}
|
||||
|
||||
fn cause(&self) -> Option<&dyn std::error::Error> {
|
||||
self.source()
|
||||
fn cause(&self) -> Option<&dyn std::error::Error> {
|
||||
self.source()
|
||||
}
|
||||
}
|
||||
)*
|
||||
}
|
||||
}
|
||||
|
||||
box_impls! {
|
||||
Box<dyn Diagnostic>,
|
||||
Box<dyn Diagnostic + Send>,
|
||||
Box<dyn Diagnostic + Send + Sync>
|
||||
}
|
||||
|
||||
impl<T: Diagnostic + Send + Sync + 'static> From<T>
|
||||
for Box<dyn Diagnostic + Send + Sync + 'static>
|
||||
{
|
||||
|
|
@ -220,7 +232,7 @@ whole thing--meaning you should be able to support `SourceCode`s which are
|
|||
gigabytes or larger in size.
|
||||
*/
|
||||
pub trait SourceCode: Send + Sync {
|
||||
/// Read the bytes for a specific span from this SourceCode, keeping a
|
||||
/// Read the bytes for a specific span from this `SourceCode`, keeping a
|
||||
/// certain number of lines before and after the span as context.
|
||||
fn read_span<'a>(
|
||||
&'a self,
|
||||
|
|
@ -237,14 +249,16 @@ pub struct LabeledSpan {
|
|||
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
||||
label: Option<String>,
|
||||
span: SourceSpan,
|
||||
primary: bool,
|
||||
}
|
||||
|
||||
impl LabeledSpan {
|
||||
/// Makes a new labeled span.
|
||||
pub fn new(label: Option<String>, offset: ByteOffset, len: usize) -> Self {
|
||||
pub const fn new(label: Option<String>, offset: ByteOffset, len: usize) -> Self {
|
||||
Self {
|
||||
label,
|
||||
span: (offset, len).into(),
|
||||
span: SourceSpan::new(SourceOffset(offset), SourceOffset(len)),
|
||||
primary: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -253,6 +267,16 @@ impl LabeledSpan {
|
|||
Self {
|
||||
label,
|
||||
span: span.into(),
|
||||
primary: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Makes a new labeled primary span using an existing span.
|
||||
pub fn new_primary_with_span(label: Option<String>, span: impl Into<SourceSpan>) -> Self {
|
||||
Self {
|
||||
label,
|
||||
span: span.into(),
|
||||
primary: true,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -310,24 +334,29 @@ impl LabeledSpan {
|
|||
}
|
||||
|
||||
/// Returns a reference to the inner [`SourceSpan`].
|
||||
pub fn inner(&self) -> &SourceSpan {
|
||||
pub const fn inner(&self) -> &SourceSpan {
|
||||
&self.span
|
||||
}
|
||||
|
||||
/// Returns the 0-based starting byte offset.
|
||||
pub fn offset(&self) -> usize {
|
||||
pub const fn offset(&self) -> usize {
|
||||
self.span.offset()
|
||||
}
|
||||
|
||||
/// Returns the number of bytes this `LabeledSpan` spans.
|
||||
pub fn len(&self) -> usize {
|
||||
pub const fn len(&self) -> usize {
|
||||
self.span.len()
|
||||
}
|
||||
|
||||
/// True if this `LabeledSpan` is empty.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
pub const fn is_empty(&self) -> bool {
|
||||
self.span.is_empty()
|
||||
}
|
||||
|
||||
/// True if this `LabeledSpan` is a primary span.
|
||||
pub const fn primary(&self) -> bool {
|
||||
self.primary
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
|
|
@ -338,7 +367,8 @@ fn test_serialize_labeled_span() {
|
|||
assert_eq!(
|
||||
json!(LabeledSpan::new(None, 0, 0)),
|
||||
json!({
|
||||
"span": { "offset": 0, "length": 0 }
|
||||
"span": { "offset": 0, "length": 0, },
|
||||
"primary": false,
|
||||
})
|
||||
);
|
||||
|
||||
|
|
@ -346,9 +376,10 @@ fn test_serialize_labeled_span() {
|
|||
json!(LabeledSpan::new(Some("label".to_string()), 0, 0)),
|
||||
json!({
|
||||
"label": "label",
|
||||
"span": { "offset": 0, "length": 0 }
|
||||
"span": { "offset": 0, "length": 0, },
|
||||
"primary": false,
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
|
|
@ -358,23 +389,26 @@ fn test_deserialize_labeled_span() {
|
|||
|
||||
let span: LabeledSpan = serde_json::from_value(json!({
|
||||
"label": null,
|
||||
"span": { "offset": 0, "length": 0 }
|
||||
"span": { "offset": 0, "length": 0, },
|
||||
"primary": false,
|
||||
}))
|
||||
.unwrap();
|
||||
assert_eq!(span, LabeledSpan::new(None, 0, 0));
|
||||
|
||||
let span: LabeledSpan = serde_json::from_value(json!({
|
||||
"span": { "offset": 0, "length": 0 }
|
||||
"span": { "offset": 0, "length": 0, },
|
||||
"primary": false
|
||||
}))
|
||||
.unwrap();
|
||||
assert_eq!(span, LabeledSpan::new(None, 0, 0));
|
||||
|
||||
let span: LabeledSpan = serde_json::from_value(json!({
|
||||
"label": "label",
|
||||
"span": { "offset": 0, "length": 0 }
|
||||
"span": { "offset": 0, "length": 0, },
|
||||
"primary": false
|
||||
}))
|
||||
.unwrap();
|
||||
assert_eq!(span, LabeledSpan::new(Some("label".to_string()), 0, 0))
|
||||
assert_eq!(span, LabeledSpan::new(Some("label".to_string()), 0, 0));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -422,7 +456,7 @@ pub struct MietteSpanContents<'a> {
|
|||
|
||||
impl<'a> MietteSpanContents<'a> {
|
||||
/// Make a new [`MietteSpanContents`] object.
|
||||
pub fn new(
|
||||
pub const fn new(
|
||||
data: &'a [u8],
|
||||
span: SourceSpan,
|
||||
line: usize,
|
||||
|
|
@ -440,7 +474,7 @@ impl<'a> MietteSpanContents<'a> {
|
|||
}
|
||||
|
||||
/// Make a new [`MietteSpanContents`] object, with a name for its 'file'.
|
||||
pub fn new_named(
|
||||
pub const fn new_named(
|
||||
name: String,
|
||||
data: &'a [u8],
|
||||
span: SourceSpan,
|
||||
|
|
@ -492,7 +526,7 @@ pub struct SourceSpan {
|
|||
|
||||
impl SourceSpan {
|
||||
/// Create a new [`SourceSpan`].
|
||||
pub fn new(start: SourceOffset, length: usize) -> Self {
|
||||
pub const fn new(start: SourceOffset, length: usize) -> Self {
|
||||
Self {
|
||||
offset: start,
|
||||
length,
|
||||
|
|
@ -500,18 +534,18 @@ impl SourceSpan {
|
|||
}
|
||||
|
||||
/// The absolute offset, in bytes, from the beginning of a [`SourceCode`].
|
||||
pub fn offset(&self) -> usize {
|
||||
pub const fn offset(&self) -> usize {
|
||||
self.offset.offset()
|
||||
}
|
||||
|
||||
/// Total length of the [`SourceSpan`], in bytes.
|
||||
pub fn len(&self) -> usize {
|
||||
pub const fn len(&self) -> usize {
|
||||
self.length
|
||||
}
|
||||
|
||||
/// Whether this [`SourceSpan`] has a length of zero. It may still be useful
|
||||
/// to point to a specific point.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
pub const fn is_empty(&self) -> bool {
|
||||
self.length == 0
|
||||
}
|
||||
}
|
||||
|
|
@ -563,7 +597,7 @@ fn test_serialize_source_span() {
|
|||
assert_eq!(
|
||||
json!(SourceSpan::from(0)),
|
||||
json!({ "offset": 0, "length": 0})
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
|
|
@ -572,7 +606,7 @@ fn test_deserialize_source_span() {
|
|||
use serde_json::json;
|
||||
|
||||
let span: SourceSpan = serde_json::from_value(json!({ "offset": 0, "length": 0})).unwrap();
|
||||
assert_eq!(span, SourceSpan::from(0))
|
||||
assert_eq!(span, SourceSpan::from(0));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -589,7 +623,7 @@ pub struct SourceOffset(ByteOffset);
|
|||
|
||||
impl SourceOffset {
|
||||
/// Actual byte offset.
|
||||
pub fn offset(&self) -> ByteOffset {
|
||||
pub const fn offset(&self) -> ByteOffset {
|
||||
self.0
|
||||
}
|
||||
|
||||
|
|
@ -674,12 +708,12 @@ fn test_source_offset_from_location() {
|
|||
fn test_serialize_source_offset() {
|
||||
use serde_json::json;
|
||||
|
||||
assert_eq!(json!(SourceOffset::from(0)), 0)
|
||||
assert_eq!(json!(SourceOffset::from(0)), 0);
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
#[test]
|
||||
fn test_deserialize_source_offset() {
|
||||
let offset: SourceOffset = serde_json::from_str("0").unwrap();
|
||||
assert_eq!(offset, SourceOffset::from(0))
|
||||
assert_eq!(offset, SourceOffset::from(0));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -189,7 +189,7 @@ fn fmt_help() {
|
|||
|
||||
assert_eq!(
|
||||
"1 x hello x \"2\"".to_string(),
|
||||
FooStruct("hello".into()).help().unwrap().to_string()
|
||||
FooStruct("hello").help().unwrap().to_string()
|
||||
);
|
||||
|
||||
#[derive(Debug, Diagnostic, Error)]
|
||||
|
|
@ -201,12 +201,7 @@ fn fmt_help() {
|
|||
|
||||
assert_eq!(
|
||||
"1 x hello x \"2\"".to_string(),
|
||||
BarStruct {
|
||||
my_field: "hello".into()
|
||||
}
|
||||
.help()
|
||||
.unwrap()
|
||||
.to_string()
|
||||
BarStruct { my_field: "hello" }.help().unwrap().to_string()
|
||||
);
|
||||
|
||||
#[derive(Debug, Diagnostic, Error)]
|
||||
|
|
@ -224,7 +219,7 @@ fn fmt_help() {
|
|||
|
||||
assert_eq!(
|
||||
"1 x bar x \"2\"".to_string(),
|
||||
FooEnum::X("bar".into()).help().unwrap().to_string()
|
||||
FooEnum::X("bar").help().unwrap().to_string()
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
|
|
@ -250,12 +245,7 @@ fn help_field() {
|
|||
|
||||
assert_eq!(
|
||||
"x".to_string(),
|
||||
Foo {
|
||||
do_this: Some("x".into())
|
||||
}
|
||||
.help()
|
||||
.unwrap()
|
||||
.to_string()
|
||||
Foo { do_this: Some("x") }.help().unwrap().to_string()
|
||||
);
|
||||
|
||||
#[derive(Debug, Diagnostic, Error)]
|
||||
|
|
@ -271,16 +261,11 @@ fn help_field() {
|
|||
|
||||
assert_eq!(
|
||||
"x".to_string(),
|
||||
Bar::A(Some("x".into())).help().unwrap().to_string()
|
||||
Bar::A(Some("x")).help().unwrap().to_string()
|
||||
);
|
||||
assert_eq!(
|
||||
"x".to_string(),
|
||||
Bar::B {
|
||||
do_this: Some("x".into())
|
||||
}
|
||||
.help()
|
||||
.unwrap()
|
||||
.to_string()
|
||||
Bar::B { do_this: Some("x") }.help().unwrap().to_string()
|
||||
);
|
||||
|
||||
#[derive(Debug, Diagnostic, Error)]
|
||||
|
|
@ -288,20 +273,14 @@ fn help_field() {
|
|||
#[diagnostic()]
|
||||
struct Baz<'a>(#[help] Option<&'a str>);
|
||||
|
||||
assert_eq!(
|
||||
"x".to_string(),
|
||||
Baz(Some("x".into())).help().unwrap().to_string()
|
||||
);
|
||||
assert_eq!("x".to_string(), Baz(Some("x")).help().unwrap().to_string());
|
||||
|
||||
#[derive(Debug, Diagnostic, Error)]
|
||||
#[error("welp")]
|
||||
#[diagnostic()]
|
||||
struct Quux<'a>(#[help] &'a str);
|
||||
|
||||
assert_eq!(
|
||||
"x".to_string(),
|
||||
Quux("x".into()).help().unwrap().to_string()
|
||||
);
|
||||
assert_eq!("x".to_string(), Quux("x").help().unwrap().to_string());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -589,7 +568,7 @@ fn test_unit_struct_display() {
|
|||
#[error("unit only")]
|
||||
#[diagnostic(code(foo::bar::overridden), help("hello from unit help"))]
|
||||
struct UnitOnly;
|
||||
assert_eq!(UnitOnly.help().unwrap().to_string(), "hello from unit help")
|
||||
assert_eq!(UnitOnly.help().unwrap().to_string(), "hello from unit help");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -603,5 +582,47 @@ fn test_unit_enum_display() {
|
|||
assert_eq!(
|
||||
Enum::UnitVariant.help().unwrap().to_string(),
|
||||
"hello from unit help"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_optional_source_code() {
|
||||
#[derive(Debug, Diagnostic, Error)]
|
||||
#[error("struct with optional source")]
|
||||
struct Struct {
|
||||
#[source_code]
|
||||
src: Option<String>,
|
||||
}
|
||||
assert!(Struct { src: None }.source_code().is_none());
|
||||
assert!(Struct {
|
||||
src: Some("".to_string())
|
||||
}
|
||||
.source_code()
|
||||
.is_some());
|
||||
|
||||
#[derive(Debug, Diagnostic, Error)]
|
||||
enum Enum {
|
||||
#[error("variant1 with optional source")]
|
||||
Variant1 {
|
||||
#[source_code]
|
||||
src: Option<String>,
|
||||
},
|
||||
#[error("variant2 with optional source")]
|
||||
Variant2 {
|
||||
#[source_code]
|
||||
src: Option<String>,
|
||||
},
|
||||
}
|
||||
assert!(Enum::Variant1 { src: None }.source_code().is_none());
|
||||
assert!(Enum::Variant1 {
|
||||
src: Some("".to_string())
|
||||
}
|
||||
.source_code()
|
||||
.is_some());
|
||||
assert!(Enum::Variant2 { src: None }.source_code().is_none());
|
||||
assert!(Enum::Variant2 {
|
||||
src: Some("".to_string())
|
||||
}
|
||||
.source_code()
|
||||
.is_some());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,6 +34,190 @@ fn fmt_report(diag: Report) -> String {
|
|||
out
|
||||
}
|
||||
|
||||
fn fmt_report_with_settings(
|
||||
diag: Report,
|
||||
with_settings: fn(GraphicalReportHandler) -> GraphicalReportHandler,
|
||||
) -> String {
|
||||
let mut out = String::new();
|
||||
|
||||
let handler = with_settings(GraphicalReportHandler::new_themed(
|
||||
GraphicalTheme::unicode_nocolor(),
|
||||
));
|
||||
|
||||
handler.render_report(&mut out, diag.as_ref()).unwrap();
|
||||
|
||||
println!("Error:\n```\n{}\n```", out);
|
||||
|
||||
out
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn word_wrap_options() -> Result<(), MietteError> {
|
||||
// By default, a long word should not break
|
||||
let out =
|
||||
fmt_report_with_settings(Report::msg("abcdefghijklmnopqrstuvwxyz"), |handler| handler);
|
||||
|
||||
let expected = " × abcdefghijklmnopqrstuvwxyz\n".to_string();
|
||||
assert_eq!(expected, out);
|
||||
|
||||
// A long word can break with a smaller width
|
||||
let out = fmt_report_with_settings(Report::msg("abcdefghijklmnopqrstuvwxyz"), |handler| {
|
||||
handler.with_width(10)
|
||||
});
|
||||
let expected = r#" × abcd
|
||||
│ efgh
|
||||
│ ijkl
|
||||
│ mnop
|
||||
│ qrst
|
||||
│ uvwx
|
||||
│ yz
|
||||
"#
|
||||
.to_string();
|
||||
assert_eq!(expected, out);
|
||||
|
||||
// Unless, word breaking is disabled
|
||||
let out = fmt_report_with_settings(Report::msg("abcdefghijklmnopqrstuvwxyz"), |handler| {
|
||||
handler.with_width(10).with_break_words(false)
|
||||
});
|
||||
let expected = " × abcdefghijklmnopqrstuvwxyz\n".to_string();
|
||||
assert_eq!(expected, out);
|
||||
|
||||
// Breaks should start at the boundary of each word if possible
|
||||
let out = fmt_report_with_settings(
|
||||
Report::msg("12 123 1234 12345 123456 1234567 1234567890"),
|
||||
|handler| handler.with_width(10),
|
||||
);
|
||||
let expected = r#" × 12
|
||||
│ 123
|
||||
│ 1234
|
||||
│ 1234
|
||||
│ 5
|
||||
│ 1234
|
||||
│ 56
|
||||
│ 1234
|
||||
│ 567
|
||||
│ 1234
|
||||
│ 5678
|
||||
│ 90
|
||||
"#
|
||||
.to_string();
|
||||
assert_eq!(expected, out);
|
||||
|
||||
// But long words should not break if word breaking is disabled
|
||||
let out = fmt_report_with_settings(
|
||||
Report::msg("12 123 1234 12345 123456 1234567 1234567890"),
|
||||
|handler| handler.with_width(10).with_break_words(false),
|
||||
);
|
||||
let expected = r#" × 12
|
||||
│ 123
|
||||
│ 1234
|
||||
│ 12345
|
||||
│ 123456
|
||||
│ 1234567
|
||||
│ 1234567890
|
||||
"#
|
||||
.to_string();
|
||||
assert_eq!(expected, out);
|
||||
|
||||
// Unless, of course, there are hyphens
|
||||
let out = fmt_report_with_settings(
|
||||
Report::msg("a-b a-b-c a-b-c-d a-b-c-d-e a-b-c-d-e-f a-b-c-d-e-f-g a-b-c-d-e-f-g-h"),
|
||||
|handler| handler.with_width(10).with_break_words(false),
|
||||
);
|
||||
let expected = r#" × a-b
|
||||
│ a-b-
|
||||
│ c a-
|
||||
│ b-c-
|
||||
│ d a-
|
||||
│ b-c-
|
||||
│ d-e
|
||||
│ a-b-
|
||||
│ c-d-
|
||||
│ e-f
|
||||
│ a-b-
|
||||
│ c-d-
|
||||
│ e-f-
|
||||
│ g a-
|
||||
│ b-c-
|
||||
│ d-e-
|
||||
│ f-g-
|
||||
│ h
|
||||
"#
|
||||
.to_string();
|
||||
assert_eq!(expected, out);
|
||||
|
||||
// Which requires an additional opt-out
|
||||
let out = fmt_report_with_settings(
|
||||
Report::msg("a-b a-b-c a-b-c-d a-b-c-d-e a-b-c-d-e-f a-b-c-d-e-f-g a-b-c-d-e-f-g-h"),
|
||||
|handler| {
|
||||
handler
|
||||
.with_width(10)
|
||||
.with_break_words(false)
|
||||
.with_word_splitter(textwrap::WordSplitter::NoHyphenation)
|
||||
},
|
||||
);
|
||||
let expected = r#" × a-b
|
||||
│ a-b-c
|
||||
│ a-b-c-d
|
||||
│ a-b-c-d-e
|
||||
│ a-b-c-d-e-f
|
||||
│ a-b-c-d-e-f-g
|
||||
│ a-b-c-d-e-f-g-h
|
||||
"#
|
||||
.to_string();
|
||||
assert_eq!(expected, out);
|
||||
|
||||
// Or if there are _other_ unicode word boundaries
|
||||
let out = fmt_report_with_settings(
|
||||
Report::msg("a/b a/b/c a/b/c/d a/b/c/d/e a/b/c/d/e/f a/b/c/d/e/f/g a/b/c/d/e/f/g/h"),
|
||||
|handler| handler.with_width(10).with_break_words(false),
|
||||
);
|
||||
let expected = r#" × a/b
|
||||
│ a/b/
|
||||
│ c a/
|
||||
│ b/c/
|
||||
│ d a/
|
||||
│ b/c/
|
||||
│ d/e
|
||||
│ a/b/
|
||||
│ c/d/
|
||||
│ e/f
|
||||
│ a/b/
|
||||
│ c/d/
|
||||
│ e/f/
|
||||
│ g a/
|
||||
│ b/c/
|
||||
│ d/e/
|
||||
│ f/g/
|
||||
│ h
|
||||
"#
|
||||
.to_string();
|
||||
assert_eq!(expected, out);
|
||||
|
||||
// Such things require you to opt-in to only breaking on ASCII whitespace
|
||||
let out = fmt_report_with_settings(
|
||||
Report::msg("a/b a/b/c a/b/c/d a/b/c/d/e a/b/c/d/e/f a/b/c/d/e/f/g a/b/c/d/e/f/g/h"),
|
||||
|handler| {
|
||||
handler
|
||||
.with_width(10)
|
||||
.with_break_words(false)
|
||||
.with_word_separator(textwrap::WordSeparator::AsciiSpace)
|
||||
},
|
||||
);
|
||||
let expected = r#" × a/b
|
||||
│ a/b/c
|
||||
│ a/b/c/d
|
||||
│ a/b/c/d/e
|
||||
│ a/b/c/d/e/f
|
||||
│ a/b/c/d/e/f/g
|
||||
│ a/b/c/d/e/f/g/h
|
||||
"#
|
||||
.to_string();
|
||||
assert_eq!(expected, out);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_source() -> Result<(), MietteError> {
|
||||
#[derive(Debug, Diagnostic, Error)]
|
||||
|
|
@ -67,6 +251,52 @@ fn empty_source() -> Result<(), MietteError> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_spans_multiline() {
|
||||
#[derive(Error, Debug, Diagnostic)]
|
||||
#[error("oops!")]
|
||||
#[diagnostic(severity(Error))]
|
||||
struct MyBad {
|
||||
#[source_code]
|
||||
src: NamedSource,
|
||||
#[label("big")]
|
||||
big: SourceSpan,
|
||||
#[label("small")]
|
||||
small: SourceSpan,
|
||||
}
|
||||
let err = MyBad {
|
||||
src: NamedSource::new(
|
||||
"issue",
|
||||
"\
|
||||
if true {
|
||||
a
|
||||
} else {
|
||||
b
|
||||
}",
|
||||
),
|
||||
big: (0, 32).into(),
|
||||
small: (14, 1).into(),
|
||||
};
|
||||
let out = fmt_report(err.into());
|
||||
println!("Error: {}", out);
|
||||
|
||||
let expected = r#" × oops!
|
||||
╭─[issue:1:1]
|
||||
1 │ ╭─▶ if true {
|
||||
2 │ │╭▶ a
|
||||
· ││ ┬
|
||||
· ││ ╰── small
|
||||
3 │ │ } else {
|
||||
4 │ │ b
|
||||
5 │ ├─▶ }
|
||||
· ╰──── big
|
||||
╰────
|
||||
"#
|
||||
.to_string();
|
||||
|
||||
assert_eq!(expected, out);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_line_highlight_span_full_line() {
|
||||
#[derive(Error, Debug, Diagnostic)]
|
||||
|
|
@ -85,9 +315,8 @@ fn single_line_highlight_span_full_line() {
|
|||
let out = fmt_report(err.into());
|
||||
println!("Error: {}", out);
|
||||
|
||||
let expected = r#"
|
||||
× oops!
|
||||
╭─[issue:1:1]
|
||||
let expected = r#" × oops!
|
||||
╭─[issue:2:1]
|
||||
1 │ source
|
||||
2 │ text
|
||||
· ──┬─
|
||||
|
|
@ -121,7 +350,7 @@ fn single_line_with_wide_char() -> Result<(), MietteError> {
|
|||
let expected = r#"oops::my::bad
|
||||
|
||||
× oops!
|
||||
╭─[bad_file.rs:1:1]
|
||||
╭─[bad_file.rs:2:7]
|
||||
1 │ source
|
||||
2 │ 👼🏼text
|
||||
· ───┬──
|
||||
|
|
@ -160,7 +389,7 @@ fn single_line_with_two_tabs() -> Result<(), MietteError> {
|
|||
let expected = r#"oops::my::bad
|
||||
|
||||
× oops!
|
||||
╭─[bad_file.rs:1:1]
|
||||
╭─[bad_file.rs:2:3]
|
||||
1 │ source
|
||||
2 │ text
|
||||
· ──┬─
|
||||
|
|
@ -199,7 +428,7 @@ fn single_line_with_tab_in_middle() -> Result<(), MietteError> {
|
|||
let expected = r#"oops::my::bad
|
||||
|
||||
× oops!
|
||||
╭─[bad_file.rs:1:1]
|
||||
╭─[bad_file.rs:2:8]
|
||||
1 │ source
|
||||
2 │ text = text
|
||||
· ──┬─
|
||||
|
|
@ -236,7 +465,7 @@ fn single_line_highlight() -> Result<(), MietteError> {
|
|||
let expected = r#"oops::my::bad
|
||||
|
||||
× oops!
|
||||
╭─[bad_file.rs:1:1]
|
||||
╭─[bad_file.rs:2:3]
|
||||
1 │ source
|
||||
2 │ text
|
||||
· ──┬─
|
||||
|
|
@ -271,7 +500,7 @@ fn external_source() -> Result<(), MietteError> {
|
|||
let expected = r#"oops::my::bad
|
||||
|
||||
× oops!
|
||||
╭─[bad_file.rs:1:1]
|
||||
╭─[bad_file.rs:2:3]
|
||||
1 │ source
|
||||
2 │ text
|
||||
· ──┬─
|
||||
|
|
@ -344,7 +573,7 @@ fn single_line_highlight_offset_end_of_line() -> Result<(), MietteError> {
|
|||
let expected = r#"oops::my::bad
|
||||
|
||||
× oops!
|
||||
╭─[bad_file.rs:1:1]
|
||||
╭─[bad_file.rs:1:7]
|
||||
1 │ source
|
||||
· ▲
|
||||
· ╰── this bit here
|
||||
|
|
@ -380,7 +609,7 @@ fn single_line_highlight_include_end_of_line() -> Result<(), MietteError> {
|
|||
let expected = r#"oops::my::bad
|
||||
|
||||
× oops!
|
||||
╭─[bad_file.rs:1:1]
|
||||
╭─[bad_file.rs:2:3]
|
||||
1 │ source
|
||||
2 │ text
|
||||
· ──┬──
|
||||
|
|
@ -417,7 +646,7 @@ fn single_line_highlight_include_end_of_line_crlf() -> Result<(), MietteError> {
|
|||
let expected = r#"oops::my::bad
|
||||
|
||||
× oops!
|
||||
╭─[bad_file.rs:1:1]
|
||||
╭─[bad_file.rs:2:3]
|
||||
1 │ source
|
||||
2 │ text
|
||||
· ──┬──
|
||||
|
|
@ -454,7 +683,7 @@ fn single_line_highlight_with_empty_span() -> Result<(), MietteError> {
|
|||
let expected = r#"oops::my::bad
|
||||
|
||||
× oops!
|
||||
╭─[bad_file.rs:1:1]
|
||||
╭─[bad_file.rs:2:3]
|
||||
1 │ source
|
||||
2 │ text
|
||||
· ▲
|
||||
|
|
@ -491,7 +720,7 @@ fn single_line_highlight_no_label() -> Result<(), MietteError> {
|
|||
let expected = r#"oops::my::bad
|
||||
|
||||
× oops!
|
||||
╭─[bad_file.rs:1:1]
|
||||
╭─[bad_file.rs:2:3]
|
||||
1 │ source
|
||||
2 │ text
|
||||
· ────
|
||||
|
|
@ -527,7 +756,7 @@ fn single_line_highlight_at_line_start() -> Result<(), MietteError> {
|
|||
let expected = r#"oops::my::bad
|
||||
|
||||
× oops!
|
||||
╭─[bad_file.rs:1:1]
|
||||
╭─[bad_file.rs:2:1]
|
||||
1 │ source
|
||||
2 │ text
|
||||
· ──┬─
|
||||
|
|
@ -542,6 +771,94 @@ fn single_line_highlight_at_line_start() -> Result<(), MietteError> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiline_label() -> Result<(), MietteError> {
|
||||
#[derive(Debug, Diagnostic, Error)]
|
||||
#[error("oops!")]
|
||||
#[diagnostic(code(oops::my::bad), help("try doing it better next time?"))]
|
||||
struct MyBad {
|
||||
#[source_code]
|
||||
src: NamedSource,
|
||||
#[label("this bit here\nand\nthis\ntoo")]
|
||||
highlight: SourceSpan,
|
||||
}
|
||||
|
||||
let src = "source\ntext\n here".to_string();
|
||||
let err = MyBad {
|
||||
src: NamedSource::new("bad_file.rs", src),
|
||||
highlight: (7, 4).into(),
|
||||
};
|
||||
let out = fmt_report(err.into());
|
||||
println!("Error: {}", out);
|
||||
let expected = r#"oops::my::bad
|
||||
|
||||
× oops!
|
||||
╭─[bad_file.rs:2:1]
|
||||
1 │ source
|
||||
2 │ text
|
||||
· ──┬─
|
||||
· ╰─┤ this bit here
|
||||
· │ and
|
||||
· │ this
|
||||
· │ too
|
||||
3 │ here
|
||||
╰────
|
||||
help: try doing it better next time?
|
||||
"#
|
||||
.trim_start()
|
||||
.to_string();
|
||||
assert_eq!(expected, out);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_multi_line_labels() -> Result<(), MietteError> {
|
||||
#[derive(Debug, Diagnostic, Error)]
|
||||
#[error("oops!")]
|
||||
#[diagnostic(code(oops::my::bad), help("try doing it better next time?"))]
|
||||
struct MyBad {
|
||||
#[source_code]
|
||||
src: NamedSource,
|
||||
#[label = "x\ny"]
|
||||
highlight1: SourceSpan,
|
||||
#[label = "z\nw"]
|
||||
highlight2: SourceSpan,
|
||||
#[label = "a\nb"]
|
||||
highlight3: SourceSpan,
|
||||
}
|
||||
|
||||
let src = "source\n text text text text text\n here".to_string();
|
||||
let err = MyBad {
|
||||
src: NamedSource::new("bad_file.rs", src),
|
||||
highlight1: (9, 4).into(),
|
||||
highlight2: (14, 4).into(),
|
||||
highlight3: (24, 4).into(),
|
||||
};
|
||||
let out = fmt_report(err.into());
|
||||
println!("Error: {}", out);
|
||||
let expected = r#"oops::my::bad
|
||||
|
||||
× oops!
|
||||
╭─[bad_file.rs:2:3]
|
||||
1 │ source
|
||||
2 │ text text text text text
|
||||
· ──┬─ ──┬─ ──┬─
|
||||
· │ │ ╰─┤ a
|
||||
· │ │ │ b
|
||||
· │ ╰─┤ z
|
||||
· │ │ w
|
||||
· ╰─┤ x
|
||||
· │ y
|
||||
3 │ here
|
||||
╰────
|
||||
help: try doing it better next time?
|
||||
"#
|
||||
.trim_start()
|
||||
.to_string();
|
||||
assert_eq!(expected, out);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_same_line_highlights() -> Result<(), MietteError> {
|
||||
#[derive(Debug, Diagnostic, Error)]
|
||||
|
|
@ -570,7 +887,7 @@ fn multiple_same_line_highlights() -> Result<(), MietteError> {
|
|||
let expected = r#"oops::my::bad
|
||||
|
||||
× oops!
|
||||
╭─[bad_file.rs:1:1]
|
||||
╭─[bad_file.rs:2:3]
|
||||
1 │ source
|
||||
2 │ text text text text text
|
||||
· ──┬─ ──┬─ ──┬─
|
||||
|
|
@ -617,7 +934,7 @@ fn multiple_same_line_highlights_with_tabs_in_middle() -> Result<(), MietteError
|
|||
let expected = r#"oops::my::bad
|
||||
|
||||
× oops!
|
||||
╭─[bad_file.rs:1:1]
|
||||
╭─[bad_file.rs:2:3]
|
||||
1 │ source
|
||||
2 │ text text text text text
|
||||
· ──┬─ ──┬─ ──┬─
|
||||
|
|
@ -656,7 +973,7 @@ fn multiline_highlight_adjacent() -> Result<(), MietteError> {
|
|||
let expected = r#"oops::my::bad
|
||||
|
||||
× oops!
|
||||
╭─[bad_file.rs:1:1]
|
||||
╭─[bad_file.rs:2:3]
|
||||
1 │ source
|
||||
2 │ ╭─▶ text
|
||||
3 │ ├─▶ here
|
||||
|
|
@ -670,6 +987,43 @@ fn multiline_highlight_adjacent() -> Result<(), MietteError> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiline_highlight_multiline_label() -> Result<(), MietteError> {
|
||||
#[derive(Debug, Diagnostic, Error)]
|
||||
#[error("oops!")]
|
||||
#[diagnostic(code(oops::my::bad), help("try doing it better next time?"))]
|
||||
struct MyBad {
|
||||
#[source_code]
|
||||
src: NamedSource,
|
||||
#[label = "these two lines\nare the problem"]
|
||||
highlight: SourceSpan,
|
||||
}
|
||||
|
||||
let src = "source\n text\n here".to_string();
|
||||
let err = MyBad {
|
||||
src: NamedSource::new("bad_file.rs", src),
|
||||
highlight: (9, 11).into(),
|
||||
};
|
||||
let out = fmt_report(err.into());
|
||||
println!("Error: {}", out);
|
||||
let expected = r#"oops::my::bad
|
||||
|
||||
× oops!
|
||||
╭─[bad_file.rs:2:3]
|
||||
1 │ source
|
||||
2 │ ╭─▶ text
|
||||
3 │ ├─▶ here
|
||||
· ╰──┤ these two lines
|
||||
· │ are the problem
|
||||
╰────
|
||||
help: try doing it better next time?
|
||||
"#
|
||||
.trim_start()
|
||||
.to_string();
|
||||
assert_eq!(expected, out);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiline_highlight_flyby() -> Result<(), MietteError> {
|
||||
#[derive(Debug, Diagnostic, Error)]
|
||||
|
|
@ -970,7 +1324,7 @@ fn related() -> Result<(), MietteError> {
|
|||
let expected = r#"oops::my::bad
|
||||
|
||||
× oops!
|
||||
╭─[bad_file.rs:1:1]
|
||||
╭─[bad_file.rs:2:3]
|
||||
1 │ source
|
||||
2 │ text
|
||||
· ──┬─
|
||||
|
|
@ -1032,7 +1386,7 @@ fn related_source_code_propagation() -> Result<(), MietteError> {
|
|||
let expected = r#"oops::my::bad
|
||||
|
||||
× oops!
|
||||
╭─[bad_file.rs:1:1]
|
||||
╭─[bad_file.rs:2:3]
|
||||
1 │ source
|
||||
2 │ text
|
||||
· ──┬─
|
||||
|
|
@ -1137,7 +1491,7 @@ fn related_severity() -> Result<(), MietteError> {
|
|||
let expected = r#"oops::my::bad
|
||||
|
||||
× oops!
|
||||
╭─[bad_file.rs:1:1]
|
||||
╭─[bad_file.rs:2:3]
|
||||
1 │ source
|
||||
2 │ text
|
||||
· ──┬─
|
||||
|
|
@ -1201,9 +1555,8 @@ fn zero_length_eol_span() {
|
|||
let out = fmt_report(err.into());
|
||||
println!("Error: {}", out);
|
||||
|
||||
let expected = r#"
|
||||
× oops!
|
||||
╭─[issue:1:1]
|
||||
let expected = r#" × oops!
|
||||
╭─[issue:2:1]
|
||||
1 │ this is the first line
|
||||
2 │ this is the second line
|
||||
· ▲
|
||||
|
|
@ -1214,3 +1567,149 @@ fn zero_length_eol_span() {
|
|||
|
||||
assert_eq!(expected, out);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn primary_label() {
|
||||
#[derive(Error, Debug, Diagnostic)]
|
||||
#[error("oops!")]
|
||||
struct MyBad {
|
||||
#[source_code]
|
||||
src: NamedSource,
|
||||
#[label]
|
||||
first_label: SourceSpan,
|
||||
#[label(primary, "nope")]
|
||||
second_label: SourceSpan,
|
||||
}
|
||||
let err = MyBad {
|
||||
src: NamedSource::new("issue", "this is the first line\nthis is the second line"),
|
||||
first_label: (2, 4).into(),
|
||||
second_label: (24, 4).into(),
|
||||
};
|
||||
let out = fmt_report(err.into());
|
||||
println!("Error: {}", out);
|
||||
|
||||
// line 2 should be the primary, not line 1
|
||||
let expected = r#" × oops!
|
||||
╭─[issue:2:2]
|
||||
1 │ this is the first line
|
||||
· ────
|
||||
2 │ this is the second line
|
||||
· ──┬─
|
||||
· ╰── nope
|
||||
╰────
|
||||
"#
|
||||
.to_string();
|
||||
|
||||
assert_eq!(expected, out);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_line_with_wide_char_unaligned_span_start() -> Result<(), MietteError> {
|
||||
#[derive(Debug, Diagnostic, Error)]
|
||||
#[error("oops!")]
|
||||
#[diagnostic(code(oops::my::bad), help("try doing it better next time?"))]
|
||||
struct MyBad {
|
||||
#[source_code]
|
||||
src: NamedSource,
|
||||
#[label("this bit here")]
|
||||
highlight: SourceSpan,
|
||||
}
|
||||
|
||||
let src = "source\n 👼🏼text\n here".to_string();
|
||||
let err = MyBad {
|
||||
src: NamedSource::new("bad_file.rs", src),
|
||||
highlight: (10, 5).into(),
|
||||
};
|
||||
let out = fmt_report(err.into());
|
||||
println!("Error: {}", out);
|
||||
let expected = r#"oops::my::bad
|
||||
|
||||
× oops!
|
||||
╭─[bad_file.rs:2:4]
|
||||
1 │ source
|
||||
2 │ 👼🏼text
|
||||
· ──┬─
|
||||
· ╰── this bit here
|
||||
3 │ here
|
||||
╰────
|
||||
help: try doing it better next time?
|
||||
"#
|
||||
.trim_start()
|
||||
.to_string();
|
||||
assert_eq!(expected, out);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_line_with_wide_char_unaligned_span_end() -> Result<(), MietteError> {
|
||||
#[derive(Debug, Diagnostic, Error)]
|
||||
#[error("oops!")]
|
||||
#[diagnostic(code(oops::my::bad), help("try doing it better next time?"))]
|
||||
struct MyBad {
|
||||
#[source_code]
|
||||
src: NamedSource,
|
||||
#[label("this bit here")]
|
||||
highlight: SourceSpan,
|
||||
}
|
||||
|
||||
let src = "source\n text 👼🏼\n here".to_string();
|
||||
let err = MyBad {
|
||||
src: NamedSource::new("bad_file.rs", src),
|
||||
highlight: (9, 6).into(),
|
||||
};
|
||||
let out = fmt_report(err.into());
|
||||
println!("Error: {}", out);
|
||||
let expected = r#"oops::my::bad
|
||||
|
||||
× oops!
|
||||
╭─[bad_file.rs:2:3]
|
||||
1 │ source
|
||||
2 │ text 👼🏼
|
||||
· ───┬───
|
||||
· ╰── this bit here
|
||||
3 │ here
|
||||
╰────
|
||||
help: try doing it better next time?
|
||||
"#
|
||||
.trim_start()
|
||||
.to_string();
|
||||
assert_eq!(expected, out);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_line_with_wide_char_unaligned_span_empty() -> Result<(), MietteError> {
|
||||
#[derive(Debug, Diagnostic, Error)]
|
||||
#[error("oops!")]
|
||||
#[diagnostic(code(oops::my::bad), help("try doing it better next time?"))]
|
||||
struct MyBad {
|
||||
#[source_code]
|
||||
src: NamedSource,
|
||||
#[label("this bit here")]
|
||||
highlight: SourceSpan,
|
||||
}
|
||||
|
||||
let src = "source\n 👼🏼text\n here".to_string();
|
||||
let err = MyBad {
|
||||
src: NamedSource::new("bad_file.rs", src),
|
||||
highlight: (10, 0).into(),
|
||||
};
|
||||
let out = fmt_report(err.into());
|
||||
println!("Error: {}", out);
|
||||
let expected = r#"oops::my::bad
|
||||
|
||||
× oops!
|
||||
╭─[bad_file.rs:2:4]
|
||||
1 │ source
|
||||
2 │ 👼🏼text
|
||||
· ▲
|
||||
· ╰── this bit here
|
||||
3 │ here
|
||||
╰────
|
||||
help: try doing it better next time?
|
||||
"#
|
||||
.trim_start()
|
||||
.to_string();
|
||||
assert_eq!(expected, out);
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
|
|
@ -1,5 +1,16 @@
|
|||
use miette::Diagnostic;
|
||||
|
||||
#[derive(Debug, miette::Diagnostic, thiserror::Error)]
|
||||
#[error("A complex error happened")]
|
||||
struct SourceError {
|
||||
#[source_code]
|
||||
code: String,
|
||||
#[help]
|
||||
help: String,
|
||||
#[label("here")]
|
||||
label: (usize, usize),
|
||||
}
|
||||
|
||||
#[derive(Debug, miette::Diagnostic, thiserror::Error)]
|
||||
#[error("AnErr")]
|
||||
struct AnErr;
|
||||
|
|
@ -8,7 +19,7 @@ struct AnErr;
|
|||
#[error("TestError")]
|
||||
struct TestStructError {
|
||||
#[diagnostic_source]
|
||||
asdf_inner_foo: AnErr,
|
||||
asdf_inner_foo: SourceError,
|
||||
}
|
||||
|
||||
#[derive(Debug, miette::Diagnostic, thiserror::Error)]
|
||||
|
|
@ -37,7 +48,11 @@ struct TestArcedError(#[diagnostic_source] std::sync::Arc<dyn Diagnostic>);
|
|||
#[test]
|
||||
fn test_diagnostic_source() {
|
||||
let error = TestStructError {
|
||||
asdf_inner_foo: AnErr,
|
||||
asdf_inner_foo: SourceError {
|
||||
code: String::new(),
|
||||
help: String::new(),
|
||||
label: (0, 0),
|
||||
},
|
||||
};
|
||||
assert!(error.diagnostic_source().is_some());
|
||||
|
||||
|
|
@ -59,3 +74,123 @@ fn test_diagnostic_source() {
|
|||
let error = TestArcedError(std::sync::Arc::new(AnErr));
|
||||
assert!(error.diagnostic_source().is_some());
|
||||
}
|
||||
|
||||
#[cfg(feature = "fancy-no-backtrace")]
|
||||
#[test]
|
||||
fn test_diagnostic_source_pass_extra_info() {
|
||||
let diag = TestBoxedError(Box::new(SourceError {
|
||||
code: String::from("Hello\nWorld!"),
|
||||
help: String::from("Have you tried turning it on and off again?"),
|
||||
label: (1, 4),
|
||||
}));
|
||||
let mut out = String::new();
|
||||
miette::GraphicalReportHandler::new_themed(miette::GraphicalTheme::unicode_nocolor())
|
||||
.with_width(80)
|
||||
.with_footer("this is a footer".into())
|
||||
.render_report(&mut out, &diag)
|
||||
.unwrap();
|
||||
println!("Error: {}", out);
|
||||
let expected = r#" × TestError
|
||||
╰─▶ × A complex error happened
|
||||
╭─[1:2]
|
||||
1 │ Hello
|
||||
· ──┬─
|
||||
· ╰── here
|
||||
2 │ World!
|
||||
╰────
|
||||
help: Have you tried turning it on and off again?
|
||||
|
||||
|
||||
this is a footer
|
||||
"#
|
||||
.to_string();
|
||||
assert_eq!(expected, out);
|
||||
}
|
||||
|
||||
#[cfg(feature = "fancy-no-backtrace")]
|
||||
#[test]
|
||||
fn test_diagnostic_source_is_output() {
|
||||
let diag = TestStructError {
|
||||
asdf_inner_foo: SourceError {
|
||||
code: String::from("right here"),
|
||||
help: String::from("That's where the error is!"),
|
||||
label: (6, 4),
|
||||
},
|
||||
};
|
||||
let mut out = String::new();
|
||||
miette::GraphicalReportHandler::new_themed(miette::GraphicalTheme::unicode_nocolor())
|
||||
.with_width(80)
|
||||
.render_report(&mut out, &diag)
|
||||
.unwrap();
|
||||
println!("{}", out);
|
||||
|
||||
let expected = r#" × TestError
|
||||
╰─▶ × A complex error happened
|
||||
╭────
|
||||
1 │ right here
|
||||
· ──┬─
|
||||
· ╰── here
|
||||
╰────
|
||||
help: That's where the error is!
|
||||
|
||||
"#;
|
||||
|
||||
assert_eq!(expected, out);
|
||||
}
|
||||
|
||||
#[derive(Debug, miette::Diagnostic, thiserror::Error)]
|
||||
#[error("A nested error happened")]
|
||||
struct NestedError {
|
||||
#[source_code]
|
||||
code: String,
|
||||
#[label("here")]
|
||||
label: (usize, usize),
|
||||
#[diagnostic_source]
|
||||
the_other_err: Box<dyn Diagnostic>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "fancy-no-backtrace")]
|
||||
#[test]
|
||||
fn test_nested_diagnostic_source_is_output() {
|
||||
let inner_error = TestStructError {
|
||||
asdf_inner_foo: SourceError {
|
||||
code: String::from("This is another error"),
|
||||
help: String::from("You should fix this"),
|
||||
label: (3, 4),
|
||||
},
|
||||
};
|
||||
let diag = NestedError {
|
||||
code: String::from("right here"),
|
||||
label: (6, 4),
|
||||
the_other_err: Box::new(inner_error),
|
||||
};
|
||||
let mut out = String::new();
|
||||
miette::GraphicalReportHandler::new_themed(miette::GraphicalTheme::unicode_nocolor())
|
||||
.with_width(80)
|
||||
.with_footer("Yooo, a footer".to_string())
|
||||
.render_report(&mut out, &diag)
|
||||
.unwrap();
|
||||
println!("{}", out);
|
||||
|
||||
let expected = r#" × A nested error happened
|
||||
├─▶ × TestError
|
||||
│
|
||||
╰─▶ × A complex error happened
|
||||
╭────
|
||||
1 │ This is another error
|
||||
· ──┬─
|
||||
· ╰── here
|
||||
╰────
|
||||
help: You should fix this
|
||||
|
||||
╭────
|
||||
1 │ right here
|
||||
· ──┬─
|
||||
· ╰── here
|
||||
╰────
|
||||
|
||||
Yooo, a footer
|
||||
"#;
|
||||
|
||||
assert_eq!(expected, out);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,7 +52,6 @@ mod json_report_handler {
|
|||
"related": []
|
||||
}"#
|
||||
.lines()
|
||||
.into_iter()
|
||||
.map(|s| s.trim_matches(|c| c == ' ' || c == '\n'))
|
||||
.collect();
|
||||
assert_eq!(expected, out);
|
||||
|
|
@ -98,7 +97,6 @@ mod json_report_handler {
|
|||
"related": []
|
||||
}"#
|
||||
.lines()
|
||||
.into_iter()
|
||||
.map(|s| s.trim_matches(|c| c == ' ' || c == '\n'))
|
||||
.collect();
|
||||
assert_eq!(expected, out);
|
||||
|
|
@ -144,7 +142,6 @@ mod json_report_handler {
|
|||
"related": []
|
||||
}"#
|
||||
.lines()
|
||||
.into_iter()
|
||||
.map(|s| s.trim_matches(|c| c == ' ' || c == '\n'))
|
||||
.collect();
|
||||
assert_eq!(expected, out);
|
||||
|
|
@ -190,7 +187,6 @@ mod json_report_handler {
|
|||
"related": []
|
||||
}"#
|
||||
.lines()
|
||||
.into_iter()
|
||||
.map(|s| s.trim_matches(|c| c == ' ' || c == '\n'))
|
||||
.collect();
|
||||
assert_eq!(expected, out);
|
||||
|
|
@ -235,7 +231,6 @@ mod json_report_handler {
|
|||
"related": []
|
||||
}"#
|
||||
.lines()
|
||||
.into_iter()
|
||||
.map(|s| s.trim_matches(|c| c == ' ' || c == '\n'))
|
||||
.collect();
|
||||
assert_eq!(expected, out);
|
||||
|
|
@ -281,7 +276,6 @@ mod json_report_handler {
|
|||
"related": []
|
||||
}"#
|
||||
.lines()
|
||||
.into_iter()
|
||||
.map(|s| s.trim_matches(|c| c == ' ' || c == '\n'))
|
||||
.collect();
|
||||
assert_eq!(expected, out);
|
||||
|
|
@ -347,7 +341,6 @@ mod json_report_handler {
|
|||
"related": []
|
||||
}"#
|
||||
.lines()
|
||||
.into_iter()
|
||||
.map(|s| s.trim_matches(|c| c == ' ' || c == '\n'))
|
||||
.collect();
|
||||
assert_eq!(expected, out);
|
||||
|
|
@ -393,7 +386,6 @@ mod json_report_handler {
|
|||
"related": []
|
||||
}"#
|
||||
.lines()
|
||||
.into_iter()
|
||||
.map(|s| s.trim_matches(|c| c == ' ' || c == '\n'))
|
||||
.collect();
|
||||
assert_eq!(expected, out);
|
||||
|
|
@ -456,7 +448,6 @@ mod json_report_handler {
|
|||
"related": []
|
||||
}"#
|
||||
.lines()
|
||||
.into_iter()
|
||||
.map(|s| s.trim_matches(|c| c == ' ' || c == '\n'))
|
||||
.collect();
|
||||
assert_eq!(expected, out);
|
||||
|
|
@ -532,7 +523,6 @@ mod json_report_handler {
|
|||
"related": []
|
||||
}"#
|
||||
.lines()
|
||||
.into_iter()
|
||||
.map(|s| s.trim_matches(|c| c == ' ' || c == '\n'))
|
||||
.collect();
|
||||
assert_eq!(expected, out);
|
||||
|
|
@ -588,7 +578,6 @@ mod json_report_handler {
|
|||
"related": []
|
||||
}"#
|
||||
.lines()
|
||||
.into_iter()
|
||||
.map(|s| s.trim_matches(|c| c == ' ' || c == '\n'))
|
||||
.collect();
|
||||
assert_eq!(expected, out);
|
||||
|
|
@ -644,7 +633,6 @@ mod json_report_handler {
|
|||
"related": []
|
||||
}"#
|
||||
.lines()
|
||||
.into_iter()
|
||||
.map(|s| s.trim_matches(|c| c == ' ' || c == '\n'))
|
||||
.collect();
|
||||
assert_eq!(expected, out);
|
||||
|
|
@ -700,7 +688,6 @@ mod json_report_handler {
|
|||
"related": []
|
||||
}"#
|
||||
.lines()
|
||||
.into_iter()
|
||||
.map(|s| s.trim_matches(|c| c == ' ' || c == '\n'))
|
||||
.collect();
|
||||
assert_eq!(expected, out);
|
||||
|
|
@ -728,7 +715,6 @@ mod json_report_handler {
|
|||
"related": []
|
||||
}"#
|
||||
.lines()
|
||||
.into_iter()
|
||||
.map(|s| s.trim_matches(|c| c == ' ' || c == '\n'))
|
||||
.collect();
|
||||
assert_eq!(expected, out);
|
||||
|
|
@ -822,7 +808,6 @@ mod json_report_handler {
|
|||
}]
|
||||
}"#
|
||||
.lines()
|
||||
.into_iter()
|
||||
.map(|s| s.trim_matches(|c| c == ' ' || c == '\n'))
|
||||
.collect();
|
||||
assert_eq!(expected, out);
|
||||
|
|
@ -920,7 +905,6 @@ mod json_report_handler {
|
|||
}]
|
||||
}"#
|
||||
.lines()
|
||||
.into_iter()
|
||||
.map(|s| s.trim_matches(|c| c == ' ' || c == '\n'))
|
||||
.collect();
|
||||
assert_eq!(expected, out);
|
||||
|
|
|
|||
Loading…
Reference in New Issue