diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 710eb1c8..4fafac33 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -43,7 +43,11 @@ jobs: - name: Clippy run: cargo clippy --all -- -D warnings - name: Run tests + if: matrix.rust == 'stable' 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: name: Miri diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b38d0292..87215d69 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -23,7 +23,7 @@ ## 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. 💚 diff --git a/Cargo.toml b/Cargo.toml index 7615d6b7..61d357af 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,7 +42,16 @@ lazy_static = "1.4" [features] 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"] [workspace] diff --git a/README.md b/README.md index c12734d0..ff25b3ab 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ You run miette? You run her code like the software? Oh. Oh! Error code for coder! Error code for One Thousand Lines! -### About +## About `miette` is a diagnostic library for Rust. It includes a series of traits/protocols that allow you to hook into its error reporting facilities, @@ -32,7 +32,7 @@ output like in the screenshots above.** You should only do this in your toplevel crate, as the fancy feature pulls in a number of dependencies that libraries and such might not want. -### Table of Contents +## Table of Contents - [About](#about) - [Features](#features) @@ -47,10 +47,11 @@ libraries and such might not want. - [... multiple related errors](#-multiple-related-errors) - [... delayed source code](#-delayed-source-code) - [... handler options](#-handler-options) + - [... dynamic diagnostics](#-dynamic-diagnostics) - [Acknowledgements](#acknowledgements) - [License](#license) -### Features +## Features - Generic [`Diagnostic`] protocol, compatible (and dependent on) [`std::error::Error`]. @@ -75,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 @@ -87,7 +88,7 @@ If you want to use the fancy printer in all these screenshots: $ cargo add miette --features fancy ``` -### Example +## Example ```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 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 @@ -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 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 @@ -247,11 +248,11 @@ pub fn some_tool() -> Result { } ``` -To construct your own simple adhoc error use the `miette::miette!` macro: +To construct your own simple adhoc error use the [`miette!`] macro: ```rust // my_app/lib/my_internal_file.rs -use miette::{IntoDiagnostic, Result, WrapErr, miette}; +use miette::{miette, IntoDiagnostic, Result, WrapErr}; use semver::Version; pub fn some_tool() -> Result { @@ -262,9 +263,7 @@ pub fn some_tool() -> Result { } ``` -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 `Result` as your return value, and it will pretty-print your diagnostics @@ -294,7 +293,7 @@ enabled: miette = { version = "X.Y.Z", features = ["fancy"] } ``` -#### ... diagnostic code URLs +### ... 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: @@ -347,7 +346,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 @@ -395,8 +394,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 @@ -432,7 +430,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. @@ -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. 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 from `thiserror`, and implements a method for @@ -568,7 +566,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 @@ -587,13 +585,32 @@ miette::set_hook(Box::new(|_| { .build(), ) })) - ``` See the docs for [`MietteHandlerOpts`] for more details on what you can 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 other projects and their authors: @@ -612,7 +629,7 @@ other projects and their authors: - [`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). @@ -623,11 +640,13 @@ under the Apache License. Some code is taken from [`ariadne`](https://github.com/zesterer/ariadne), which is MIT licensed. [`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 [`Diagnostic`]: https://docs.rs/miette/latest/miette/trait.Diagnostic.html [`IntoDiagnostic`]: https://docs.rs/miette/latest/miette/trait.IntoDiagnostic.html [`MietteHandlerOpts`]: https://docs.rs/miette/latest/miette/struct.MietteHandlerOpts.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 [`ReportHandler`]: https://docs.rs/miette/latest/miette/struct.ReportHandler.html [`Result`]: https://docs.rs/miette/latest/miette/type.Result.html diff --git a/README.tpl b/README.tpl index a7913449..d598eb8b 100644 --- a/README.tpl +++ b/README.tpl @@ -4,11 +4,13 @@ {{readme}} [`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 -[`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 [`MietteHandlerOpts`]: https://docs.rs/miette/latest/miette/struct.MietteHandlerOpts.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 [`ReportHandler`]: https://docs.rs/miette/latest/miette/struct.ReportHandler.html [`Result`]: https://docs.rs/miette/latest/miette/type.Result.html diff --git a/src/eyreish/macros.rs b/src/eyreish/macros.rs index 0bc5520f..50605e71 100644 --- a/src/eyreish/macros.rs +++ b/src/eyreish/macros.rs @@ -16,7 +16,14 @@ /// # let resource = 0; /// # /// 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(()) /// # } @@ -48,17 +55,37 @@ /// # Ok(()) /// # } /// ``` +/// +/// ``` +/// use miette::{bail, Result, Severity}; +/// +/// fn divide(x: f64, y: f64) -> Result { +/// 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_rules! bail { - ($msg:literal $(,)?) => { - return $crate::private::Err($crate::miette!($msg)); + ($($key:ident = $value:expr,)* $fmt:literal $($arg:tt)*) => { + return $crate::private::Err( + $crate::miette!($($key = $value,)* $fmt $($arg)*) + ); }; ($err:expr $(,)?) => { 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. @@ -105,11 +132,33 @@ macro_rules! bail { /// # Ok(()) /// # } /// ``` +/// +/// ``` +/// use miette::{ensure, Result, Severity}; +/// +/// fn divide(x: f64, y: f64) -> Result { +/// 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_rules! ensure { - ($cond:expr, $msg:literal $(,)?) => { + ($cond:expr, $($key:ident = $value:expr,)* $fmt:literal $($arg:tt)*) => { if !$cond { - return $crate::private::Err($crate::miette!($msg)); + return $crate::private::Err( + $crate::miette!($($key = $value,)* $fmt $($arg)*) + ); } }; ($cond:expr, $err:expr $(,)?) => { @@ -117,34 +166,57 @@ macro_rules! ensure { 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 -/// string with arguments. It also can take any custom type which implements -/// `Debug` and `Display`. +/// # Examples /// -/// # Example +/// With string literal and interpolation: +/// ``` +/// # use miette::miette; +/// let x = 1; +/// 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);"# +)] /// +/// assert_eq!(report.to_string().as_str(), "1 + 2 = 3"); +/// +/// let z = x + y; +#[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"); /// ``` -/// # type V = (); -/// # -/// use miette::{miette, Result}; /// -/// fn lookup(key: &str) -> Result { -/// if key.len() != 16 { -/// return Err(miette!("key length must be 16 characters, got {:?}", key)); -/// } +/// With [`diagnostic!`]-like arguments: +/// ``` +/// use miette::{miette, LabeledSpan, Severity}; /// -/// // ... -/// # Ok(()) -/// } +/// 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 @@ -152,17 +224,69 @@ macro_rules! ensure { /// You can just replace `use`s of the `anyhow!`/`eyre!` macros with `miette!`. #[macro_export] macro_rules! miette { - ($msg:literal $(,)?) => { - // Handle $:literal as a special case to make cargo-expanded code more - // concise in the common case. - $crate::private::new_adhoc($msg) + ($($key:ident = $value:expr,)* $fmt:literal $($arg:tt)*) => { + $crate::Report::from( + $crate::diagnostic!($($key = $value,)* $fmt $($arg)*) + ) }; ($err:expr $(,)?) => ({ use $crate::private::kind::*; let error = $err; (&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 + }}; } diff --git a/src/lib.rs b/src/lib.rs index b18239a5..cc113ce3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -46,6 +46,7 @@ //! - [... multiple related errors](#-multiple-related-errors) //! - [... delayed source code](#-delayed-source-code) //! - [... handler options](#-handler-options) +//! - [... dynamic diagnostics](#-dynamic-diagnostics) //! - [Acknowledgements](#acknowledgements) //! - [License](#license) //! @@ -249,7 +250,7 @@ //! To construct your own simple adhoc error use the [miette!] macro: //! ```rust //! // my_app/lib/my_internal_file.rs -//! use miette::{IntoDiagnostic, Result, WrapErr, miette}; +//! use miette::{miette, IntoDiagnostic, Result, WrapErr}; //! use semver::Version; //! //! pub fn some_tool() -> Result { @@ -590,6 +591,28 @@ //! See the docs for [`MietteHandlerOpts`] for more details on what you can //! 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 //! //! `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")] pub use handler::*; pub use handlers::*; +pub use miette_diagnostic::*; pub use named_source::*; #[cfg(feature = "fancy")] pub use panic::*; @@ -638,6 +662,7 @@ mod handler; mod handlers; #[doc(hidden)] pub mod macro_helpers; +mod miette_diagnostic; mod named_source; #[cfg(feature = "fancy")] mod panic; diff --git a/src/miette_diagnostic.rs b/src/miette_diagnostic.rs new file mode 100644 index 00000000..29a0c80d --- /dev/null +++ b/src/miette_diagnostic.rs @@ -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, + /// [`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, + /// Additional help text related to this Diagnostic + pub help: Option, + /// URL to visit for a more detailed explanation/help about this + /// [`Diagnostic`]. + pub url: Option, + /// Labels to apply to this `Diagnostic`'s [`Diagnostic::source_code`] + pub labels: Option>, +} + +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> { + self.code + .as_ref() + .map(Box::new) + .map(|c| c as Box) + } + + fn severity(&self) -> Option { + self.severity + } + + fn help<'a>(&'a self) -> Option> { + self.help + .as_ref() + .map(Box::new) + .map(|c| c as Box) + } + + fn url<'a>(&'a self) -> Option> { + self.url + .as_ref() + .map(Box::new) + .map(|c| c as Box) + } + + fn labels(&self) -> Option + '_>> { + self.labels + .as_ref() + .map(|ls| ls.iter().cloned()) + .map(Box::new) + .map(|b| b as Box>) + } +} + +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) -> 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) -> 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) -> 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) -> 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) -> 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) -> 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) -> 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) -> Self { + let mut all_labels = self.labels.unwrap_or_default(); + all_labels.extend(labels.into_iter()); + self.labels = Some(all_labels); + self + } +} diff --git a/src/protocol.rs b/src/protocol.rs index 53312497..0f606f65 100644 --- a/src/protocol.rs +++ b/src/protocol.rs @@ -160,7 +160,7 @@ impl From> for Box Self { + Severity::Error + } +} + /** 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, label: impl Into) -> 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) -> 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) -> Self { + Self::new_with_span(None, span) + } + /// Gets the (optional) label string for this `LabeledSpan`. pub fn label(&self) -> Option<&str> { self.label.as_deref() diff --git a/tests/test_chain.rs b/tests/test_chain.rs index 3fdd4cdb..29d3927b 100644 --- a/tests/test_chain.rs +++ b/tests/test_chain.rs @@ -1,7 +1,7 @@ use miette::{miette, 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] diff --git a/tests/test_downcast.rs b/tests/test_downcast.rs index 2823177e..cf5ab2ee 100644 --- a/tests/test_downcast.rs +++ b/tests/test_downcast.rs @@ -3,7 +3,7 @@ mod drop; use self::common::*; use self::drop::{DetectDrop, Flag}; -use miette::{Diagnostic, Report}; +use miette::{Diagnostic, MietteDiagnostic, Report}; use std::error::Error as StdError; use std::fmt::{self, Display}; use std::io; @@ -12,11 +12,19 @@ use std::io; fn test_downcast() { assert_eq!( "oh no!", - bail_literal().unwrap_err().downcast::<&str>().unwrap(), + bail_literal() + .unwrap_err() + .downcast::() + .unwrap() + .message, ); assert_eq!( "oh no!", - bail_fmt().unwrap_err().downcast::().unwrap(), + bail_fmt() + .unwrap_err() + .downcast::() + .unwrap() + .message, ); assert_eq!( "oh no!", @@ -32,11 +40,19 @@ fn test_downcast() { fn test_downcast_ref() { assert_eq!( "oh no!", - *bail_literal().unwrap_err().downcast_ref::<&str>().unwrap(), + bail_literal() + .unwrap_err() + .downcast_ref::() + .unwrap() + .message, ); assert_eq!( "oh no!", - bail_fmt().unwrap_err().downcast_ref::().unwrap(), + bail_fmt() + .unwrap_err() + .downcast_ref::() + .unwrap() + .message, ); assert_eq!( "oh no!", @@ -52,11 +68,19 @@ fn test_downcast_ref() { fn test_downcast_mut() { assert_eq!( "oh no!", - *bail_literal().unwrap_err().downcast_mut::<&str>().unwrap(), + bail_literal() + .unwrap_err() + .downcast_mut::() + .unwrap() + .message, ); assert_eq!( "oh no!", - bail_fmt().unwrap_err().downcast_mut::().unwrap(), + bail_fmt() + .unwrap_err() + .downcast_mut::() + .unwrap() + .message, ); assert_eq!( "oh no!",