Add dynamic diagnostic (#262)

This commit is contained in:
Gavrilikhin Daniil 2023-05-14 04:59:43 +08:00 committed by GitHub
parent 675f3411e3
commit 024145dbdd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 585 additions and 73 deletions

View File

@ -43,7 +43,11 @@ jobs:
- name: Clippy - name: Clippy
run: cargo clippy --all -- -D warnings run: cargo clippy --all -- -D warnings
- name: Run tests - name: Run tests
if: matrix.rust == 'stable'
run: cargo test --all --verbose --features fancy run: cargo test --all --verbose --features fancy
- name: Run tests
if: matrix.rust == '1.56.0'
run: cargo test --all --verbose --features fancy no-format-args-capture
miri: miri:
name: Miri name: Miri

View File

@ -23,7 +23,7 @@
## Introduction ## Introduction
Thank you so much for your interest in contributing!. All types of contributions are encouraged and valued. See the [table of contents](#toc) for different ways to help and details about how this project handles them!📝 Thank you so much for your interest in contributing! All types of contributions are encouraged and valued. See the [table of contents](#toc) for different ways to help and details about how this project handles them!📝
Please make sure to read the relevant section before making your contribution! It will make it a lot easier for us maintainers to make the most of it and smooth out the experience for all involved. 💚 Please make sure to read the relevant section before making your contribution! It will make it a lot easier for us maintainers to make the most of it and smooth out the experience for all involved. 💚

View File

@ -42,7 +42,16 @@ lazy_static = "1.4"
[features] [features]
default = [] default = []
fancy-no-backtrace = ["owo-colors", "is-terminal", "textwrap", "terminal_size", "supports-hyperlinks", "supports-color", "supports-unicode"] no-format-args-capture = []
fancy-no-backtrace = [
"owo-colors",
"is-terminal",
"textwrap",
"terminal_size",
"supports-hyperlinks",
"supports-color",
"supports-unicode",
]
fancy = ["fancy-no-backtrace", "backtrace", "backtrace-ext"] fancy = ["fancy-no-backtrace", "backtrace", "backtrace-ext"]
[workspace] [workspace]

View File

@ -4,7 +4,7 @@
You run miette? You run her code like the software? Oh. Oh! Error code for You run miette? You run her code like the software? Oh. Oh! Error code for
coder! Error code for One Thousand Lines! coder! Error code for One Thousand Lines!
### About ## About
`miette` is a diagnostic library for Rust. It includes a series of `miette` is a diagnostic library for Rust. It includes a series of
traits/protocols that allow you to hook into its error reporting facilities, traits/protocols that allow you to hook into its error reporting facilities,
@ -32,7 +32,7 @@ output like in the screenshots above.** You should only do this in your
toplevel crate, as the fancy feature pulls in a number of dependencies that toplevel crate, as the fancy feature pulls in a number of dependencies that
libraries and such might not want. libraries and such might not want.
### Table of Contents <!-- omit in toc --> ## Table of Contents <!-- omit in toc -->
- [About](#about) - [About](#about)
- [Features](#features) - [Features](#features)
@ -47,10 +47,11 @@ libraries and such might not want.
- [... multiple related errors](#-multiple-related-errors) - [... multiple related errors](#-multiple-related-errors)
- [... delayed source code](#-delayed-source-code) - [... delayed source code](#-delayed-source-code)
- [... handler options](#-handler-options) - [... handler options](#-handler-options)
- [... dynamic diagnostics](#-dynamic-diagnostics)
- [Acknowledgements](#acknowledgements) - [Acknowledgements](#acknowledgements)
- [License](#license) - [License](#license)
### Features ## Features
- Generic [`Diagnostic`] protocol, compatible (and dependent on) - Generic [`Diagnostic`] protocol, compatible (and dependent on)
[`std::error::Error`]. [`std::error::Error`].
@ -75,7 +76,7 @@ the following features:
- Cause chain printing - Cause chain printing
- Turns diagnostic codes into links in [supported terminals](https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda). - Turns diagnostic codes into links in [supported terminals](https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda).
### Installing ## Installing
```sh ```sh
$ cargo add miette $ cargo add miette
@ -87,7 +88,7 @@ If you want to use the fancy printer in all these screenshots:
$ cargo add miette --features fancy $ cargo add miette --features fancy
``` ```
### Example ## Example
```rust ```rust
/* /*
@ -169,9 +170,9 @@ diagnostic help: Change int or string to be the right types and try again.
diagnostic code: nu::parser::unsupported_operation diagnostic code: nu::parser::unsupported_operation
For more details, see https://docs.rs/nu-parser/0.1.0/nu-parser/enum.ParseError.html#variant.UnsupportedOperation"> For more details, see https://docs.rs/nu-parser/0.1.0/nu-parser/enum.ParseError.html#variant.UnsupportedOperation">
### Using ## Using
#### ... in libraries ### ... in libraries
`miette` is _fully compatible_ with library usage. Consumers who don't know `miette` is _fully compatible_ with library usage. Consumers who don't know
about, or don't want, `miette` features can safely use its error types as about, or don't want, `miette` features can safely use its error types as
@ -205,7 +206,7 @@ Then, return this error type from all your fallible public APIs. It's a best
practice to wrap any "external" error types in your error `enum` instead of practice to wrap any "external" error types in your error `enum` instead of
using something like [`Report`] in a library. using something like [`Report`] in a library.
#### ... in application code ### ... in application code
Application code tends to work a little differently than libraries. You Application code tends to work a little differently than libraries. You
don't always need or care to define dedicated error wrappers for errors don't always need or care to define dedicated error wrappers for errors
@ -247,11 +248,11 @@ pub fn some_tool() -> Result<Version> {
} }
``` ```
To construct your own simple adhoc error use the `miette::miette!` macro: To construct your own simple adhoc error use the [`miette!`] macro:
```rust ```rust
// my_app/lib/my_internal_file.rs // my_app/lib/my_internal_file.rs
use miette::{IntoDiagnostic, Result, WrapErr, miette}; use miette::{miette, IntoDiagnostic, Result, WrapErr};
use semver::Version; use semver::Version;
pub fn some_tool() -> Result<Version> { pub fn some_tool() -> Result<Version> {
@ -262,9 +263,7 @@ pub fn some_tool() -> Result<Version> {
} }
``` ```
There are also similar `miette::bail!` and `miette::ensure!` macros. ### ... in `main()`
#### ... in `main()`
`main()` is just like any other part of your application-internal code. Use `main()` is just like any other part of your application-internal code. Use
`Result` as your return value, and it will pretty-print your diagnostics `Result` as your return value, and it will pretty-print your diagnostics
@ -294,7 +293,7 @@ enabled:
miette = { version = "X.Y.Z", features = ["fancy"] } miette = { version = "X.Y.Z", features = ["fancy"] }
``` ```
#### ... diagnostic code URLs ### ... diagnostic code URLs
`miette` supports providing a URL for individual diagnostics. This URL will `miette` supports providing a URL for individual diagnostics. This URL will
be displayed as an actual link in supported terminals, like so: be displayed as an actual link in supported terminals, like so:
@ -347,7 +346,7 @@ use thiserror::Error;
struct MyErr; struct MyErr;
``` ```
#### ... snippets ### ... snippets
Along with its general error handling and reporting features, `miette` also Along with its general error handling and reporting features, `miette` also
includes facilities for adding error spans/annotations/labels to your includes facilities for adding error spans/annotations/labels to your
@ -395,8 +394,7 @@ pub struct MyErrorType {
} }
``` ```
##### ... help text #### ... help text
`miette` provides two facilities for supplying help text for your errors: `miette` provides two facilities for supplying help text for your errors:
The first is the `#[help()]` format attribute that applies to structs or The first is the `#[help()]` format attribute that applies to structs or
@ -432,7 +430,7 @@ let err = Foo {
}; };
``` ```
#### ... multiple related errors ### ... multiple related errors
`miette` supports collecting multiple errors into a single diagnostic, and `miette` supports collecting multiple errors into a single diagnostic, and
printing them all together nicely. printing them all together nicely.
@ -452,7 +450,7 @@ struct MyError {
} }
``` ```
#### ... delayed source code ### ... delayed source code
Sometimes it makes sense to add source code to the error message later. Sometimes it makes sense to add source code to the error message later.
One option is to use [`with_source_code()`](Report::with_source_code) One option is to use [`with_source_code()`](Report::with_source_code)
@ -535,7 +533,7 @@ fn main() -> miette::Result<()> {
} }
``` ```
#### ... Diagnostic-based error sources. ### ... Diagnostic-based error sources.
When one uses the `#[source]` attribute on a field, that usually comes When one uses the `#[source]` attribute on a field, that usually comes
from `thiserror`, and implements a method for from `thiserror`, and implements a method for
@ -568,7 +566,7 @@ struct MyError {
struct OtherError; struct OtherError;
``` ```
#### ... handler options ### ... handler options
[`MietteHandler`] is the default handler, and is very customizable. In [`MietteHandler`] is the default handler, and is very customizable. In
most cases, you can simply use [`MietteHandlerOpts`] to tweak its behavior most cases, you can simply use [`MietteHandlerOpts`] to tweak its behavior
@ -587,13 +585,32 @@ miette::set_hook(Box::new(|_| {
.build(), .build(),
) )
})) }))
``` ```
See the docs for [`MietteHandlerOpts`] for more details on what you can See the docs for [`MietteHandlerOpts`] for more details on what you can
customize! customize!
### Acknowledgements ### ... dynamic diagnostics
If you...
- ...don't know all the possible errors upfront
- ...need to serialize/deserialize errors
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[
LabeledSpan::at(12..13, "this should be 6"),
],
help = "'*' has greater precedence than '+'",
"Wrong answer"
).with_source_code(source);
println!("{:?}", report)
```
## Acknowledgements
`miette` was not developed in a void. It owes enormous credit to various `miette` was not developed in a void. It owes enormous credit to various
other projects and their authors: other projects and their authors:
@ -612,7 +629,7 @@ other projects and their authors:
- [`ariadne`](https://crates.io/crates/ariadne) for pushing forward how - [`ariadne`](https://crates.io/crates/ariadne) for pushing forward how
_pretty_ these diagnostics can really look! _pretty_ these diagnostics can really look!
### License ## License
`miette` is released to the Rust community under the [Apache license `miette` is released to the Rust community under the [Apache license
2.0](./LICENSE). 2.0](./LICENSE).
@ -623,11 +640,13 @@ under the Apache License. Some code is taken from
[`ariadne`](https://github.com/zesterer/ariadne), which is MIT licensed. [`ariadne`](https://github.com/zesterer/ariadne), which is MIT licensed.
[`miette!`]: https://docs.rs/miette/latest/miette/macro.miette.html [`miette!`]: https://docs.rs/miette/latest/miette/macro.miette.html
[`diagnostic!`]: https://docs.rs/miette/latest/miette/macro.diagnostic.html
[`std::error::Error`]: https://doc.rust-lang.org/nightly/std/error/trait.Error.html [`std::error::Error`]: https://doc.rust-lang.org/nightly/std/error/trait.Error.html
[`Diagnostic`]: https://docs.rs/miette/latest/miette/trait.Diagnostic.html [`Diagnostic`]: https://docs.rs/miette/latest/miette/trait.Diagnostic.html
[`IntoDiagnostic`]: https://docs.rs/miette/latest/miette/trait.IntoDiagnostic.html [`IntoDiagnostic`]: https://docs.rs/miette/latest/miette/trait.IntoDiagnostic.html
[`MietteHandlerOpts`]: https://docs.rs/miette/latest/miette/struct.MietteHandlerOpts.html [`MietteHandlerOpts`]: https://docs.rs/miette/latest/miette/struct.MietteHandlerOpts.html
[`MietteHandler`]: https://docs.rs/miette/latest/miette/struct.MietteHandler.html [`MietteHandler`]: https://docs.rs/miette/latest/miette/struct.MietteHandler.html
[`MietteDiagnostic`]: https://docs.rs/miette/latest/miette/struct.MietteDiagnostic.html
[`Report`]: https://docs.rs/miette/latest/miette/struct.Report.html [`Report`]: https://docs.rs/miette/latest/miette/struct.Report.html
[`ReportHandler`]: https://docs.rs/miette/latest/miette/struct.ReportHandler.html [`ReportHandler`]: https://docs.rs/miette/latest/miette/struct.ReportHandler.html
[`Result`]: https://docs.rs/miette/latest/miette/type.Result.html [`Result`]: https://docs.rs/miette/latest/miette/type.Result.html

View File

@ -4,11 +4,13 @@
{{readme}} {{readme}}
[`miette!`]: https://docs.rs/miette/latest/miette/macro.miette.html [`miette!`]: https://docs.rs/miette/latest/miette/macro.miette.html
[`diagnostic!`]: https://docs.rs/miette/latest/miette/macro.diagnostic.html
[`std::error::Error`]: https://doc.rust-lang.org/nightly/std/error/trait.Error.html [`std::error::Error`]: https://doc.rust-lang.org/nightly/std/error/trait.Error.html
[`Diagnostic`]: https://docs.rs/miette/latest/miette/struct.Diagnostic.html [`Diagnostic`]: https://docs.rs/miette/latest/miette/trait.Diagnostic.html
[`IntoDiagnostic`]: https://docs.rs/miette/latest/miette/trait.IntoDiagnostic.html [`IntoDiagnostic`]: https://docs.rs/miette/latest/miette/trait.IntoDiagnostic.html
[`MietteHandlerOpts`]: https://docs.rs/miette/latest/miette/struct.MietteHandlerOpts.html [`MietteHandlerOpts`]: https://docs.rs/miette/latest/miette/struct.MietteHandlerOpts.html
[`MietteHandler`]: https://docs.rs/miette/latest/miette/struct.MietteHandler.html [`MietteHandler`]: https://docs.rs/miette/latest/miette/struct.MietteHandler.html
[`MietteDiagnostic`]: https://docs.rs/miette/latest/miette/struct.MietteDiagnostic.html
[`Report`]: https://docs.rs/miette/latest/miette/struct.Report.html [`Report`]: https://docs.rs/miette/latest/miette/struct.Report.html
[`ReportHandler`]: https://docs.rs/miette/latest/miette/struct.ReportHandler.html [`ReportHandler`]: https://docs.rs/miette/latest/miette/struct.ReportHandler.html
[`Result`]: https://docs.rs/miette/latest/miette/type.Result.html [`Result`]: https://docs.rs/miette/latest/miette/type.Result.html

View File

@ -16,7 +16,14 @@
/// # let resource = 0; /// # let resource = 0;
/// # /// #
/// if !has_permission(user, resource) { /// if !has_permission(user, resource) {
/// bail!("permission denied for accessing {}", resource); #[cfg_attr(
not(feature = "no-format-args-capture"),
doc = r#" bail!("permission denied for accessing {resource}");"#
)]
#[cfg_attr(
feature = "no-format-args-capture",
doc = r#" bail!("permission denied for accessing {}", resource);"#
)]
/// } /// }
/// # Ok(()) /// # Ok(())
/// # } /// # }
@ -48,17 +55,37 @@
/// # Ok(()) /// # Ok(())
/// # } /// # }
/// ``` /// ```
///
/// ```
/// use miette::{bail, Result, Severity};
///
/// fn divide(x: f64, y: f64) -> Result<f64> {
/// if y.abs() < 1e-3 {
/// bail!(
/// severity = Severity::Warning,
#[cfg_attr(
not(feature = "no-format-args-capture"),
doc = r#" "dividing by value ({y}) close to 0""#
)]
#[cfg_attr(
feature = "no-format-args-capture",
doc = r#" "dividing by value ({}) close to 0", y"#
)]
/// );
/// }
/// Ok(x / y)
/// }
/// ```
#[macro_export] #[macro_export]
macro_rules! bail { macro_rules! bail {
($msg:literal $(,)?) => { ($($key:ident = $value:expr,)* $fmt:literal $($arg:tt)*) => {
return $crate::private::Err($crate::miette!($msg)); return $crate::private::Err(
$crate::miette!($($key = $value,)* $fmt $($arg)*)
);
}; };
($err:expr $(,)?) => { ($err:expr $(,)?) => {
return $crate::private::Err($crate::miette!($err)); return $crate::private::Err($crate::miette!($err));
}; };
($fmt:expr, $($arg:tt)*) => {
return $crate::private::Err($crate::miette!($fmt, $($arg)*));
};
} }
/// Return early with an error if a condition is not satisfied. /// Return early with an error if a condition is not satisfied.
@ -105,11 +132,33 @@ macro_rules! bail {
/// # Ok(()) /// # Ok(())
/// # } /// # }
/// ``` /// ```
///
/// ```
/// use miette::{ensure, Result, Severity};
///
/// fn divide(x: f64, y: f64) -> Result<f64> {
/// ensure!(
/// y.abs() >= 1e-3,
/// severity = Severity::Warning,
#[cfg_attr(
not(feature = "no-format-args-capture"),
doc = r#" "dividing by value ({y}) close to 0""#
)]
#[cfg_attr(
feature = "no-format-args-capture",
doc = r#" "dividing by value ({}) close to 0", y"#
)]
/// );
/// Ok(x / y)
/// }
/// ```
#[macro_export] #[macro_export]
macro_rules! ensure { macro_rules! ensure {
($cond:expr, $msg:literal $(,)?) => { ($cond:expr, $($key:ident = $value:expr,)* $fmt:literal $($arg:tt)*) => {
if !$cond { if !$cond {
return $crate::private::Err($crate::miette!($msg)); return $crate::private::Err(
$crate::miette!($($key = $value,)* $fmt $($arg)*)
);
} }
}; };
($cond:expr, $err:expr $(,)?) => { ($cond:expr, $err:expr $(,)?) => {
@ -117,34 +166,57 @@ macro_rules! ensure {
return $crate::private::Err($crate::miette!($err)); return $crate::private::Err($crate::miette!($err));
} }
}; };
($cond:expr, $fmt:expr, $($arg:tt)*) => {
if !$cond {
return $crate::private::Err($crate::miette!($fmt, $($arg)*));
}
};
} }
/// Construct an ad-hoc error from a string. /// Construct an ad-hoc [`Report`].
/// ///
/// This evaluates to an `Error`. It can take either just a string, or a format /// # Examples
/// string with arguments. It also can take any custom type which implements
/// `Debug` and `Display`.
///
/// # Example
/// ///
/// With string literal and interpolation:
/// ``` /// ```
/// # type V = (); /// # use miette::miette;
/// # /// let x = 1;
/// use miette::{miette, Result}; /// let y = 2;
#[cfg_attr(
not(feature = "no-format-args-capture"),
doc = r#"let report = miette!("{x} + {} = {z}", y, z = x + y);"#
)]
#[cfg_attr(
feature = "no-format-args-capture",
doc = r#"let report = miette!("{} + {} = {z}", x, y, z = x + y);"#
)]
/// ///
/// fn lookup(key: &str) -> Result<V> { /// assert_eq!(report.to_string().as_str(), "1 + 2 = 3");
/// if key.len() != 16 {
/// return Err(miette!("key length must be 16 characters, got {:?}", key));
/// }
/// ///
/// // ... /// let z = x + y;
/// # Ok(()) #[cfg_attr(
/// } not(feature = "no-format-args-capture"),
doc = r#"let report = miette!("{x} + {y} = {z}");"#
)]
#[cfg_attr(
feature = "no-format-args-capture",
doc = r#"let report = miette!("{} + {} = {}", x, y, z);"#
)]
/// assert_eq!(report.to_string().as_str(), "1 + 2 = 3");
/// ```
///
/// With [`diagnostic!`]-like arguments:
/// ```
/// use miette::{miette, LabeledSpan, Severity};
///
/// let source = "(2 + 2".to_string();
/// let report = miette!(
/// // Those fields are optional
/// severity = Severity::Error,
/// code = "expected::rparen",
/// help = "always close your parens",
/// labels = vec![LabeledSpan::at_offset(6, "here")],
/// url = "https://example.com",
/// // Rest of the arguments are passed to `format!`
/// // to form diagnostic message
/// "expected closing ')'"
/// )
/// .with_source_code(source);
/// ``` /// ```
/// ///
/// ## `anyhow`/`eyre` Users /// ## `anyhow`/`eyre` Users
@ -152,17 +224,69 @@ macro_rules! ensure {
/// You can just replace `use`s of the `anyhow!`/`eyre!` macros with `miette!`. /// You can just replace `use`s of the `anyhow!`/`eyre!` macros with `miette!`.
#[macro_export] #[macro_export]
macro_rules! miette { macro_rules! miette {
($msg:literal $(,)?) => { ($($key:ident = $value:expr,)* $fmt:literal $($arg:tt)*) => {
// Handle $:literal as a special case to make cargo-expanded code more $crate::Report::from(
// concise in the common case. $crate::diagnostic!($($key = $value,)* $fmt $($arg)*)
$crate::private::new_adhoc($msg) )
}; };
($err:expr $(,)?) => ({ ($err:expr $(,)?) => ({
use $crate::private::kind::*; use $crate::private::kind::*;
let error = $err; let error = $err;
(&error).miette_kind().new(error) (&error).miette_kind().new(error)
}); });
($fmt:expr, $($arg:tt)*) => { }
$crate::private::new_adhoc(format!($fmt, $($arg)*))
}; /// Construct a [`MietteDiagnostic`] in more user-friendly way.
///
/// # Examples
/// ```
/// use miette::{diagnostic, LabeledSpan, Severity};
///
/// let source = "(2 + 2".to_string();
/// let diag = diagnostic!(
/// // Those fields are optional
/// severity = Severity::Error,
/// code = "expected::rparen",
/// help = "always close your parens",
/// labels = vec![LabeledSpan::at_offset(6, "here")],
/// url = "https://example.com",
/// // Rest of the arguments are passed to `format!`
/// // to form diagnostic message
/// "expected closing ')'",
/// );
/// ```
/// Diagnostic without any fields:
/// ```
/// # use miette::diagnostic;
/// let x = 1;
/// let y = 2;
///
#[cfg_attr(
not(feature = "no-format-args-capture"),
doc = r#" let diag = diagnostic!("{x} + {} = {z}", y, z = x + y);"#
)]
#[cfg_attr(
feature = "no-format-args-capture",
doc = r#" let diag = diagnostic!("{} + {} = {z}", x, y, z = x + y);"#
)]
/// assert_eq!(diag.message, "1 + 2 = 3");
///
/// let z = x + y;
#[cfg_attr(
not(feature = "no-format-args-capture"),
doc = r#"let diag = diagnostic!("{x} + {y} = {z}");"#
)]
#[cfg_attr(
feature = "no-format-args-capture",
doc = r#"let diag = diagnostic!("{} + {} = {}", x, y, z);"#
)]
/// assert_eq!(diag.message, "1 + 2 = 3");
/// ```
#[macro_export]
macro_rules! diagnostic {
($($key:ident = $value:expr,)* $fmt:literal $($arg:tt)*) => {{
let mut diag = $crate::MietteDiagnostic::new(format!($fmt $($arg)*));
$(diag.$key = Some($value.into());)*
diag
}};
} }

View File

@ -46,6 +46,7 @@
//! - [... multiple related errors](#-multiple-related-errors) //! - [... multiple related errors](#-multiple-related-errors)
//! - [... delayed source code](#-delayed-source-code) //! - [... delayed source code](#-delayed-source-code)
//! - [... handler options](#-handler-options) //! - [... handler options](#-handler-options)
//! - [... dynamic diagnostics](#-dynamic-diagnostics)
//! - [Acknowledgements](#acknowledgements) //! - [Acknowledgements](#acknowledgements)
//! - [License](#license) //! - [License](#license)
//! //!
@ -249,7 +250,7 @@
//! To construct your own simple adhoc error use the [miette!] macro: //! To construct your own simple adhoc error use the [miette!] macro:
//! ```rust //! ```rust
//! // my_app/lib/my_internal_file.rs //! // my_app/lib/my_internal_file.rs
//! use miette::{IntoDiagnostic, Result, WrapErr, miette}; //! use miette::{miette, IntoDiagnostic, Result, WrapErr};
//! use semver::Version; //! use semver::Version;
//! //!
//! pub fn some_tool() -> Result<Version> { //! pub fn some_tool() -> Result<Version> {
@ -590,6 +591,28 @@
//! See the docs for [`MietteHandlerOpts`] for more details on what you can //! See the docs for [`MietteHandlerOpts`] for more details on what you can
//! customize! //! customize!
//! //!
//! ### ... dynamic diagnostics
//!
//! If you...
//! - ...don't know all the possible errors upfront
//! - ...need to serialize/deserialize errors
//! then you may want to use [`miette!`], [`diagnostic!`] macros or
//! [`MietteDiagnostic`] directly to create diagnostic on the fly.
//!
//! ```rust,ignore
//! # use miette::{miette, LabeledSpan, Report};
//!
//! let source = "2 + 2 * 2 = 8".to_string();
//! let report = miette!(
//! labels = vec[
//! LabeledSpan::at(12..13, "this should be 6"),
//! ],
//! help = "'*' has greater precedence than '+'",
//! "Wrong answer"
//! ).with_source_code(source);
//! println!("{:?}", report)
//! ```
//!
//! ## Acknowledgements //! ## Acknowledgements
//! //!
//! `miette` was not developed in a void. It owes enormous credit to various //! `miette` was not developed in a void. It owes enormous credit to various
@ -624,6 +647,7 @@ pub use eyreish::*;
#[cfg(feature = "fancy-no-backtrace")] #[cfg(feature = "fancy-no-backtrace")]
pub use handler::*; pub use handler::*;
pub use handlers::*; pub use handlers::*;
pub use miette_diagnostic::*;
pub use named_source::*; pub use named_source::*;
#[cfg(feature = "fancy")] #[cfg(feature = "fancy")]
pub use panic::*; pub use panic::*;
@ -638,6 +662,7 @@ mod handler;
mod handlers; mod handlers;
#[doc(hidden)] #[doc(hidden)]
pub mod macro_helpers; pub mod macro_helpers;
mod miette_diagnostic;
mod named_source; mod named_source;
#[cfg(feature = "fancy")] #[cfg(feature = "fancy")]
mod panic; mod panic;

250
src/miette_diagnostic.rs Normal file
View File

@ -0,0 +1,250 @@
use std::{
error::Error,
fmt::{Debug, Display},
};
use crate::{Diagnostic, LabeledSpan, Severity};
/// Diagnostic that can be created at runtime.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MietteDiagnostic {
/// Displayed diagnostic message
pub message: String,
/// Unique diagnostic code to look up more information
/// about this Diagnostic. Ideally also globally unique, and documented
/// in the toplevel crate's documentation for easy searching.
/// Rust path format (`foo::bar::baz`) is recommended, but more classic
/// codes like `E0123` will work just fine
pub code: Option<String>,
/// [`Diagnostic`] severity. Intended to be used by
/// [`ReportHandler`](crate::ReportHandler)s to change the way different
/// [`Diagnostic`]s are displayed. Defaults to [`Severity::Error`]
pub severity: Option<Severity>,
/// Additional help text related to this Diagnostic
pub help: Option<String>,
/// URL to visit for a more detailed explanation/help about this
/// [`Diagnostic`].
pub url: Option<String>,
/// Labels to apply to this `Diagnostic`'s [`Diagnostic::source_code`]
pub labels: Option<Vec<LabeledSpan>>,
}
impl Display for MietteDiagnostic {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", &self.message)
}
}
impl Error for MietteDiagnostic {}
impl Diagnostic for MietteDiagnostic {
fn code<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
self.code
.as_ref()
.map(Box::new)
.map(|c| c as Box<dyn Display>)
}
fn severity(&self) -> Option<Severity> {
self.severity
}
fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
self.help
.as_ref()
.map(Box::new)
.map(|c| c as Box<dyn Display>)
}
fn url<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
self.url
.as_ref()
.map(Box::new)
.map(|c| c as Box<dyn Display>)
}
fn labels(&self) -> Option<Box<dyn Iterator<Item = LabeledSpan> + '_>> {
self.labels
.as_ref()
.map(|ls| ls.iter().cloned())
.map(Box::new)
.map(|b| b as Box<dyn Iterator<Item = LabeledSpan>>)
}
}
impl MietteDiagnostic {
/// Create a new dynamic diagnostic with the given message.
///
/// # Examples
/// ```
/// use miette::{Diagnostic, MietteDiagnostic, Severity};
///
/// let diag = MietteDiagnostic::new("Oops, something went wrong!");
/// assert_eq!(diag.to_string(), "Oops, something went wrong!");
/// assert_eq!(diag.message, "Oops, something went wrong!");
/// ```
pub fn new(message: impl Into<String>) -> Self {
Self {
message: message.into(),
labels: None,
severity: None,
code: None,
help: None,
url: None,
}
}
/// Return new diagnostic with the given code.
///
/// # Examples
/// ```
/// use miette::{Diagnostic, MietteDiagnostic};
///
/// let diag = MietteDiagnostic::new("Oops, something went wrong!").with_code("foo::bar::baz");
/// assert_eq!(diag.message, "Oops, something went wrong!");
/// assert_eq!(diag.code, Some("foo::bar::baz".to_string()));
/// ```
pub fn with_code(mut self, code: impl Into<String>) -> Self {
self.code = Some(code.into());
self
}
/// Return new diagnostic with the given severity.
///
/// # Examples
/// ```
/// use miette::{Diagnostic, MietteDiagnostic, Severity};
///
/// let diag = MietteDiagnostic::new("I warn you to stop!").with_severity(Severity::Warning);
/// assert_eq!(diag.message, "I warn you to stop!");
/// assert_eq!(diag.severity, Some(Severity::Warning));
/// ```
pub fn with_severity(mut self, severity: Severity) -> Self {
self.severity = Some(severity);
self
}
/// Return new diagnostic with the given help message.
///
/// # Examples
/// ```
/// use miette::{Diagnostic, MietteDiagnostic};
///
/// let diag = MietteDiagnostic::new("PC is not working").with_help("Try to reboot it again");
/// assert_eq!(diag.message, "PC is not working");
/// assert_eq!(diag.help, Some("Try to reboot it again".to_string()));
/// ```
pub fn with_help(mut self, help: impl Into<String>) -> Self {
self.help = Some(help.into());
self
}
/// Return new diagnostic with the given URL.
///
/// # Examples
/// ```
/// use miette::{Diagnostic, MietteDiagnostic};
///
/// let diag = MietteDiagnostic::new("PC is not working")
/// .with_url("https://letmegooglethat.com/?q=Why+my+pc+doesn%27t+work");
/// assert_eq!(diag.message, "PC is not working");
/// assert_eq!(
/// diag.url,
/// Some("https://letmegooglethat.com/?q=Why+my+pc+doesn%27t+work".to_string())
/// );
/// ```
pub fn with_url(mut self, url: impl Into<String>) -> Self {
self.url = Some(url.into());
self
}
/// Return new diagnostic with the given label.
///
/// Discards previous labels
///
/// # Examples
/// ```
/// use miette::{Diagnostic, LabeledSpan, MietteDiagnostic};
///
/// let source = "cpp is the best language";
///
/// let label = LabeledSpan::at(0..3, "This should be Rust");
/// let diag = MietteDiagnostic::new("Wrong best language").with_label(label.clone());
/// assert_eq!(diag.message, "Wrong best language");
/// assert_eq!(diag.labels, Some(vec![label]));
/// ```
pub fn with_label(mut self, label: impl Into<LabeledSpan>) -> Self {
self.labels = Some(vec![label.into()]);
self
}
/// Return new diagnostic with the given labels.
///
/// Discards previous labels
///
/// # Examples
/// ```
/// use miette::{Diagnostic, LabeledSpan, MietteDiagnostic};
///
/// let source = "helo wrld";
///
/// let labels = vec![
/// LabeledSpan::at_offset(3, "add 'l'"),
/// LabeledSpan::at_offset(6, "add 'r'"),
/// ];
/// let diag = MietteDiagnostic::new("Typos in 'hello world'").with_labels(labels.clone());
/// assert_eq!(diag.message, "Typos in 'hello world'");
/// assert_eq!(diag.labels, Some(labels));
/// ```
pub fn with_labels(mut self, labels: impl IntoIterator<Item = LabeledSpan>) -> Self {
self.labels = Some(labels.into_iter().collect());
self
}
/// Return new diagnostic with new label added to the existing ones.
///
/// # Examples
/// ```
/// use miette::{Diagnostic, LabeledSpan, MietteDiagnostic};
///
/// let source = "helo wrld";
///
/// let label1 = LabeledSpan::at_offset(3, "add 'l'");
/// let label2 = LabeledSpan::at_offset(6, "add 'r'");
/// let diag = MietteDiagnostic::new("Typos in 'hello world'")
/// .and_label(label1.clone())
/// .and_label(label2.clone());
/// assert_eq!(diag.message, "Typos in 'hello world'");
/// assert_eq!(diag.labels, Some(vec![label1, label2]));
/// ```
pub fn and_label(mut self, label: impl Into<LabeledSpan>) -> Self {
let mut labels = self.labels.unwrap_or_default();
labels.push(label.into());
self.labels = Some(labels);
self
}
/// Return new diagnostic with new labels added to the existing ones.
///
/// # Examples
/// ```
/// use miette::{Diagnostic, LabeledSpan, MietteDiagnostic};
///
/// let source = "helo wrld";
///
/// let label1 = LabeledSpan::at_offset(3, "add 'l'");
/// let label2 = LabeledSpan::at_offset(6, "add 'r'");
/// let label3 = LabeledSpan::at_offset(9, "add '!'");
/// let diag = MietteDiagnostic::new("Typos in 'hello world!'")
/// .and_label(label1.clone())
/// .and_labels([label2.clone(), label3.clone()]);
/// assert_eq!(diag.message, "Typos in 'hello world!'");
/// assert_eq!(diag.labels, Some(vec![label1, label2, label3]));
/// ```
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());
self.labels = Some(all_labels);
self
}
}

View File

@ -160,7 +160,7 @@ impl From<Box<dyn std::error::Error + Send + Sync>> for Box<dyn Diagnostic + Sen
/** /**
[`Diagnostic`] severity. Intended to be used by [`Diagnostic`] severity. Intended to be used by
[`ReportHandler`](crate::ReportHandler)s to change the way different [`ReportHandler`](crate::ReportHandler)s to change the way different
[`Diagnostic`]s are displayed. [`Diagnostic`]s are displayed. Defaults to [`Severity::Error`].
*/ */
#[derive(Copy, Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] #[derive(Copy, Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
pub enum Severity { pub enum Severity {
@ -169,9 +169,16 @@ pub enum Severity {
/// Warning. Please take note. /// Warning. Please take note.
Warning, Warning,
/// Critical failure. The program cannot continue. /// Critical failure. The program cannot continue.
/// This is the default severity, if you don't specify another one.
Error, Error,
} }
impl Default for Severity {
fn default() -> Self {
Severity::Error
}
}
/** /**
Represents readable source code of some sort. Represents readable source code of some sort.
@ -218,6 +225,54 @@ impl LabeledSpan {
} }
} }
/// Makes a new label at specified span
///
/// # Examples
/// ```
/// use miette::LabeledSpan;
///
/// let source = "Cpp is the best";
/// let label = LabeledSpan::at(0..3, "should be Rust");
/// assert_eq!(
/// label,
/// LabeledSpan::new(Some("should be Rust".to_string()), 0, 3)
/// )
/// ```
pub fn at(span: impl Into<SourceSpan>, label: impl Into<String>) -> Self {
Self::new_with_span(Some(label.into()), span)
}
/// Makes a new label that points at a specific offset.
///
/// # Examples
/// ```
/// use miette::LabeledSpan;
///
/// let source = "(2 + 2";
/// let label = LabeledSpan::at_offset(4, "expected a closing parenthesis");
/// assert_eq!(
/// label,
/// LabeledSpan::new(Some("expected a closing parenthesis".to_string()), 4, 0)
/// )
/// ```
pub fn at_offset(offset: ByteOffset, label: impl Into<String>) -> Self {
Self::new(Some(label.into()), offset, 0)
}
/// Makes a new label without text, that underlines a specific span.
///
/// # Examples
/// ```
/// use miette::LabeledSpan;
///
/// let source = "You have an eror here";
/// let label = LabeledSpan::underline(12..16);
/// assert_eq!(label, LabeledSpan::new(None, 12, 4))
/// ```
pub fn underline(span: impl Into<SourceSpan>) -> Self {
Self::new_with_span(None, span)
}
/// Gets the (optional) label string for this `LabeledSpan`. /// Gets the (optional) label string for this `LabeledSpan`.
pub fn label(&self) -> Option<&str> { pub fn label(&self) -> Option<&str> {
self.label.as_deref() self.label.as_deref()

View File

@ -1,7 +1,7 @@
use miette::{miette, Report}; use miette::{miette, Report};
fn error() -> Report { fn error() -> Report {
miette!(0).wrap_err(1).wrap_err(2).wrap_err(3) miette!("0").wrap_err(1).wrap_err(2).wrap_err(3)
} }
#[test] #[test]

View File

@ -3,7 +3,7 @@ mod drop;
use self::common::*; use self::common::*;
use self::drop::{DetectDrop, Flag}; use self::drop::{DetectDrop, Flag};
use miette::{Diagnostic, Report}; use miette::{Diagnostic, MietteDiagnostic, Report};
use std::error::Error as StdError; use std::error::Error as StdError;
use std::fmt::{self, Display}; use std::fmt::{self, Display};
use std::io; use std::io;
@ -12,11 +12,19 @@ use std::io;
fn test_downcast() { fn test_downcast() {
assert_eq!( assert_eq!(
"oh no!", "oh no!",
bail_literal().unwrap_err().downcast::<&str>().unwrap(), bail_literal()
.unwrap_err()
.downcast::<MietteDiagnostic>()
.unwrap()
.message,
); );
assert_eq!( assert_eq!(
"oh no!", "oh no!",
bail_fmt().unwrap_err().downcast::<String>().unwrap(), bail_fmt()
.unwrap_err()
.downcast::<MietteDiagnostic>()
.unwrap()
.message,
); );
assert_eq!( assert_eq!(
"oh no!", "oh no!",
@ -32,11 +40,19 @@ fn test_downcast() {
fn test_downcast_ref() { fn test_downcast_ref() {
assert_eq!( assert_eq!(
"oh no!", "oh no!",
*bail_literal().unwrap_err().downcast_ref::<&str>().unwrap(), bail_literal()
.unwrap_err()
.downcast_ref::<MietteDiagnostic>()
.unwrap()
.message,
); );
assert_eq!( assert_eq!(
"oh no!", "oh no!",
bail_fmt().unwrap_err().downcast_ref::<String>().unwrap(), bail_fmt()
.unwrap_err()
.downcast_ref::<MietteDiagnostic>()
.unwrap()
.message,
); );
assert_eq!( assert_eq!(
"oh no!", "oh no!",
@ -52,11 +68,19 @@ fn test_downcast_ref() {
fn test_downcast_mut() { fn test_downcast_mut() {
assert_eq!( assert_eq!(
"oh no!", "oh no!",
*bail_literal().unwrap_err().downcast_mut::<&str>().unwrap(), bail_literal()
.unwrap_err()
.downcast_mut::<MietteDiagnostic>()
.unwrap()
.message,
); );
assert_eq!( assert_eq!(
"oh no!", "oh no!",
bail_fmt().unwrap_err().downcast_mut::<String>().unwrap(), bail_fmt()
.unwrap_err()
.downcast_mut::<MietteDiagnostic>()
.unwrap()
.message,
); );
assert_eq!( assert_eq!(
"oh no!", "oh no!",