From 46adb3bc6aa6518d82a4187b34c56e287922136f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?kleines=20Filmr=C3=B6llchen?= <28656157+kleinesfilmroellchen@users.noreply.github.com> Date: Thu, 18 May 2023 00:42:42 +0200 Subject: [PATCH 01/26] feat(const): Constify various functions (#263) This is primarily aimed at making `SourceSpan` and `SourceOffset` usable in const contexts. Constifiable functions were found with the `clippy::missing_const_for_fn` lint, though it reported at least two false positives. --- src/eyreish/ptr.rs | 8 ++++---- src/handlers/debug.rs | 2 +- src/handlers/json.rs | 4 ++-- src/handlers/narratable.rs | 8 ++++---- src/protocol.rs | 26 +++++++++++++------------- 5 files changed, 24 insertions(+), 24 deletions(-) diff --git a/src/eyreish/ptr.rs b/src/eyreish/ptr.rs index c8d63d2..fa954d1 100644 --- a/src/eyreish/ptr.rs +++ b/src/eyreish/ptr.rs @@ -43,7 +43,7 @@ where Box::from_raw(self.ptr.as_ptr()) } - pub(crate) fn by_ref<'a>(&self) -> Ref<'a, T> { + pub(crate) const fn by_ref<'a>(&self) -> Ref<'a, T> { Ref { ptr: self.ptr, lifetime: PhantomData, @@ -91,7 +91,7 @@ where } } - pub(crate) fn from_raw(ptr: NonNull) -> Self { + pub(crate) const fn from_raw(ptr: NonNull) -> Self { Ref { ptr, lifetime: PhantomData, @@ -112,7 +112,7 @@ where } } - pub(crate) fn as_ptr(self) -> *const T { + pub(crate) const fn as_ptr(self) -> *const T { self.ptr.as_ptr() as *const T } @@ -154,7 +154,7 @@ where } } - pub(crate) fn by_ref(self) -> Ref<'a, T> { + pub(crate) const fn by_ref(self) -> Ref<'a, T> { Ref { ptr: self.ptr, lifetime: PhantomData, diff --git a/src/handlers/debug.rs b/src/handlers/debug.rs index a9460d1..50450a4 100644 --- a/src/handlers/debug.rs +++ b/src/handlers/debug.rs @@ -13,7 +13,7 @@ pub struct DebugReportHandler; impl DebugReportHandler { /// Create a new [`NarratableReportHandler`](crate::NarratableReportHandler) /// There are no customization options. - pub fn new() -> Self { + pub const fn new() -> Self { Self } } diff --git a/src/handlers/json.rs b/src/handlers/json.rs index ec214ca..29e21a0 100644 --- a/src/handlers/json.rs +++ b/src/handlers/json.rs @@ -13,7 +13,7 @@ pub struct JSONReportHandler; impl JSONReportHandler { /// Create a new [`JSONReportHandler`]. There are no customization /// options. - pub fn new() -> Self { + pub const fn new() -> Self { Self } } @@ -49,7 +49,7 @@ impl fmt::Display for Escape<'_> { } } -fn escape(input: &'_ str) -> Escape<'_> { +const fn escape(input: &'_ str) -> Escape<'_> { Escape(input) } diff --git a/src/handlers/narratable.rs b/src/handlers/narratable.rs index 5af4b97..c809124 100644 --- a/src/handlers/narratable.rs +++ b/src/handlers/narratable.rs @@ -21,7 +21,7 @@ pub struct NarratableReportHandler { impl NarratableReportHandler { /// Create a new [`NarratableReportHandler`]. There are no customization /// options. - pub fn new() -> Self { + pub const fn new() -> Self { Self { footer: None, context_lines: 1, @@ -31,13 +31,13 @@ impl NarratableReportHandler { /// Include the cause chain of the top-level error in the report, if /// available. - pub fn with_cause_chain(mut self) -> Self { + pub const fn with_cause_chain(mut self) -> Self { self.with_cause_chain = true; self } /// Do not include the cause chain of the top-level error in the report. - pub fn without_cause_chain(mut self) -> Self { + pub const fn without_cause_chain(mut self) -> Self { self.with_cause_chain = false; self } @@ -49,7 +49,7 @@ impl NarratableReportHandler { } /// Sets the number of lines of context to show around each error. - pub fn with_context_lines(mut self, lines: usize) -> Self { + pub const fn with_context_lines(mut self, lines: usize) -> Self { self.context_lines = lines; self } diff --git a/src/protocol.rs b/src/protocol.rs index 611b827..7531db1 100644 --- a/src/protocol.rs +++ b/src/protocol.rs @@ -241,10 +241,10 @@ pub struct LabeledSpan { impl LabeledSpan { /// Makes a new labeled span. - pub fn new(label: Option, offset: ByteOffset, len: usize) -> Self { + pub const fn new(label: Option, offset: ByteOffset, len: usize) -> Self { Self { label, - span: (offset, len).into(), + span: SourceSpan::new(SourceOffset(offset), SourceOffset(len)), } } @@ -310,22 +310,22 @@ 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() } } @@ -422,7 +422,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 +440,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 +492,7 @@ pub struct SourceSpan { impl SourceSpan { /// Create a new [`SourceSpan`]. - pub fn new(start: SourceOffset, length: SourceOffset) -> Self { + pub const fn new(start: SourceOffset, length: SourceOffset) -> Self { Self { offset: start, length: length.offset(), @@ -500,18 +500,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 } } @@ -589,7 +589,7 @@ pub struct SourceOffset(ByteOffset); impl SourceOffset { /// Actual byte offset. - pub fn offset(&self) -> ByteOffset { + pub const fn offset(&self) -> ByteOffset { self.0 } From aefe323780bda4e60feb44bb96ee98634ad677ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcel=20M=C3=BCller?= Date: Thu, 18 May 2023 22:41:17 +0000 Subject: [PATCH 02/26] feat(nested): Render inner diagnostics (#170) --- src/handlers/graphical.rs | 23 ++++- tests/graphical.rs | 6 +- tests/test_diagnostic_source_macro.rs | 136 +++++++++++++++++++++++++- 3 files changed, 155 insertions(+), 10 deletions(-) diff --git a/src/handlers/graphical.rs b/src/handlers/graphical.rs index 1ab09fa..b5dd754 100644 --- a/src/handlers/graphical.rs +++ b/src/handlers/graphical.rs @@ -3,7 +3,7 @@ use std::fmt::{self, Write}; use owo_colors::{OwoColorize, Style}; use unicode_width::UnicodeWidthChar; -use crate::diagnostic_chain::DiagnosticChain; +use crate::diagnostic_chain::{DiagnosticChain, ErrorKind}; use crate::handlers::theme::*; use crate::protocol::{Diagnostic, Severity}; use crate::{LabeledSpan, MietteError, ReportHandler, SourceCode, SourceSpan, SpanContents}; @@ -151,7 +151,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)?; @@ -190,6 +189,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 +197,7 @@ impl GraphicalReportHandler { write!(header, " ({})", url.style(self.theme.styles.link))?; } writeln!(f, "{}", header)?; + writeln!(f)?; } Ok(()) } @@ -253,7 +254,22 @@ impl GraphicalReportHandler { let opts = textwrap::Options::new(width) .initial_indent(&initial_indent) .subsequent_indent(&rest_indent); - writeln!(f, "{}", textwrap::fill(&error.to_string(), opts))?; + 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))?; + } + } } } @@ -287,7 +303,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)?; diff --git a/tests/graphical.rs b/tests/graphical.rs index f929884..0c69470 100644 --- a/tests/graphical.rs +++ b/tests/graphical.rs @@ -85,8 +85,7 @@ fn single_line_highlight_span_full_line() { let out = fmt_report(err.into()); println!("Error: {}", out); - let expected = r#" - × oops! + let expected = r#" × oops! ╭─[issue:1:1] 1 │ source 2 │ text @@ -1201,8 +1200,7 @@ fn zero_length_eol_span() { let out = fmt_report(err.into()); println!("Error: {}", out); - let expected = r#" - × oops! + let expected = r#" × oops! ╭─[issue:1:1] 1 │ this is the first line 2 │ this is the second line diff --git a/tests/test_diagnostic_source_macro.rs b/tests/test_diagnostic_source_macro.rs index 8af2e88..536aedf 100644 --- a/tests/test_diagnostic_source_macro.rs +++ b/tests/test_diagnostic_source_macro.rs @@ -1,5 +1,16 @@ use miette::Diagnostic; +#[derive(Debug, miette::Diagnostic, thiserror::Error)] +#[error("A complex error happened")] +struct SourceError { + #[source_code] + code: String, + #[help] + help: String, + #[label("here")] + label: (usize, usize), +} + #[derive(Debug, miette::Diagnostic, thiserror::Error)] #[error("AnErr")] struct AnErr; @@ -8,7 +19,7 @@ struct AnErr; #[error("TestError")] struct TestStructError { #[diagnostic_source] - asdf_inner_foo: AnErr, + asdf_inner_foo: SourceError, } #[derive(Debug, miette::Diagnostic, thiserror::Error)] @@ -37,7 +48,11 @@ struct TestArcedError(#[diagnostic_source] std::sync::Arc); #[test] fn test_diagnostic_source() { let error = TestStructError { - asdf_inner_foo: AnErr, + asdf_inner_foo: SourceError { + code: String::new(), + help: String::new(), + label: (0, 0), + }, }; assert!(error.diagnostic_source().is_some()); @@ -59,3 +74,120 @@ fn test_diagnostic_source() { let error = TestArcedError(std::sync::Arc::new(AnErr)); assert!(error.diagnostic_source().is_some()); } + +#[test] +fn test_diagnostic_source_pass_extra_info() { + let diag = TestBoxedError(Box::new(SourceError { + code: String::from("Hello\nWorld!"), + help: format!("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:1] + 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); +} + +#[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, +} + +#[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); +} From b1d74e351dc65cbadb85431423da7531ea5150ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kat=20March=C3=A1n?= Date: Thu, 18 May 2023 15:57:33 -0700 Subject: [PATCH 03/26] docs: update changelog --- CHANGELOG.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e3542f2..0b842de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # `miette` Release Changelog + +## 5.9.0 (2023-05-18) + +### Features + +* **serde:** Add `serde` support (#264) ([c25676cb](https://github.com/zkat/miette/commit/c25676cb1f4266c2607836e6359f15b9cbd8637e)) +* **const:** Constify various functions (#263) ([46adb3bc](https://github.com/zkat/miette/commit/46adb3bc6aa6518d82a4187b34c56e287922136f)) +* **nested:** Render inner diagnostics (#170) ([aefe3237](https://github.com/zkat/miette/commit/aefe323780bda4e60feb44bb96ee98634ad677ad)) + +### Bug Fixes + +* **misc:** Correct some typos (#255) ([675f3411](https://github.com/zkat/miette/commit/675f3411e33d5fae86d4018c3b72f751a4c4bc2f)) + ## 5.8.0 (2023-04-18) From 91e5f5b7e347b921921bac7135c56c600abb1fab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kat=20March=C3=A1n?= Date: Thu, 18 May 2023 15:58:11 -0700 Subject: [PATCH 04/26] chore: Release --- Cargo.toml | 4 ++-- miette-derive/Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 96b9c9f..160556f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "miette" -version = "5.8.0" +version = "5.9.0" authors = ["Kat Marchán "] description = "Fancy diagnostic reporting library and protocol for us mere mortals who aren't compiler hackers." categories = ["rust-patterns"] @@ -14,7 +14,7 @@ exclude = ["images/", "tests/", "miette-derive/"] [dependencies] thiserror = "1.0.40" -miette-derive = { path = "miette-derive", version = "=5.8.0" } +miette-derive = { path = "miette-derive", version = "=5.9.0" } once_cell = "1.8.0" unicode-width = "0.1.9" diff --git a/miette-derive/Cargo.toml b/miette-derive/Cargo.toml index ee79d66..461a328 100644 --- a/miette-derive/Cargo.toml +++ b/miette-derive/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "miette-derive" -version = "5.8.0" +version = "5.9.0" authors = ["Kat Marchán "] edition = "2018" license = "Apache-2.0" From 01b60a7df8d0373c205ef7be564e28d210e809b3 Mon Sep 17 00:00:00 2001 From: Khang Nguyen Duy <40488299+iceghost@users.noreply.github.com> Date: Sun, 25 Jun 2023 12:15:28 +0700 Subject: [PATCH 05/26] docs: fix broken links (#275) the links point to structs, whereas it should be traits --- README.md | 4 ++-- README.tpl | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index ff25b3a..61fcf50 100644 --- a/README.md +++ b/README.md @@ -648,7 +648,7 @@ under the Apache License. Some code is taken from [`MietteHandler`]: https://docs.rs/miette/latest/miette/struct.MietteHandler.html [`MietteDiagnostic`]: https://docs.rs/miette/latest/miette/struct.MietteDiagnostic.html [`Report`]: https://docs.rs/miette/latest/miette/struct.Report.html -[`ReportHandler`]: https://docs.rs/miette/latest/miette/struct.ReportHandler.html +[`ReportHandler`]: https://docs.rs/miette/latest/miette/trait.ReportHandler.html [`Result`]: https://docs.rs/miette/latest/miette/type.Result.html -[`SourceCode`]: https://docs.rs/miette/latest/miette/struct.SourceCode.html +[`SourceCode`]: https://docs.rs/miette/latest/miette/trait.SourceCode.html [`SourceSpan`]: https://docs.rs/miette/latest/miette/struct.SourceSpan.html diff --git a/README.tpl b/README.tpl index d598eb8..f1126f1 100644 --- a/README.tpl +++ b/README.tpl @@ -12,7 +12,7 @@ [`MietteHandler`]: https://docs.rs/miette/latest/miette/struct.MietteHandler.html [`MietteDiagnostic`]: https://docs.rs/miette/latest/miette/struct.MietteDiagnostic.html [`Report`]: https://docs.rs/miette/latest/miette/struct.Report.html -[`ReportHandler`]: https://docs.rs/miette/latest/miette/struct.ReportHandler.html +[`ReportHandler`]: https://docs.rs/miette/latest/miette/trait.ReportHandler.html [`Result`]: https://docs.rs/miette/latest/miette/type.Result.html -[`SourceCode`]: https://docs.rs/miette/latest/miette/struct.SourceCode.html +[`SourceCode`]: https://docs.rs/miette/latest/miette/trait.SourceCode.html [`SourceSpan`]: https://docs.rs/miette/latest/miette/struct.SourceSpan.html From 89806755a4d89e2dc15bfd1d94f6b758df205571 Mon Sep 17 00:00:00 2001 From: 0x009922 Date: Thu, 29 Jun 2023 08:42:12 +0700 Subject: [PATCH 06/26] docs: a little cleaning in README (#274) --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index 61fcf50..8375876 100644 --- a/README.md +++ b/README.md @@ -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, Result, SourceSpan}; use thiserror::Error; #[derive(Error, Debug, Diagnostic)] @@ -123,12 +123,10 @@ 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}; fn this_fails() -> Result<()> { // You can use plain strings as a `Source`, or anything that implements // the one-method `Source` trait. let src = "source\n text\n here".to_string(); - let len = src.len(); Err(MyBad { src: NamedSource::new("bad_file.rs", src), From 2e3e5c9d15e234495369e9b47d032644dd5664ad Mon Sep 17 00:00:00 2001 From: Bennett Hardwick Date: Thu, 29 Jun 2023 11:58:35 +1000 Subject: [PATCH 07/26] feat(protocol): add StdError impl for Box (#273) --- src/protocol.rs | 24 ++++++++--- tests/test_derive_source_chain.rs | 69 +++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+), 6 deletions(-) create mode 100644 tests/test_derive_source_chain.rs diff --git a/src/protocol.rs b/src/protocol.rs index 7531db1..f516984 100644 --- a/src/protocol.rs +++ b/src/protocol.rs @@ -69,16 +69,28 @@ pub trait Diagnostic: std::error::Error { } } -impl std::error::Error for Box { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - (**self).source() - } +macro_rules! box_impls { + ($($box_type:ty),*) => { + $( + impl std::error::Error for $box_type { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + (**self).source() + } - fn cause(&self) -> Option<&dyn std::error::Error> { - self.source() + fn cause(&self) -> Option<&dyn std::error::Error> { + self.source() + } + } + )* } } +box_impls! { + Box, + Box, + Box +} + impl From for Box { diff --git a/tests/test_derive_source_chain.rs b/tests/test_derive_source_chain.rs new file mode 100644 index 0000000..fb7ddb2 --- /dev/null +++ b/tests/test_derive_source_chain.rs @@ -0,0 +1,69 @@ +use std::sync::Arc; + +use miette::{miette, Diagnostic}; +use thiserror::Error; + +#[test] +fn test_source() { + #[derive(Debug, Diagnostic, Error)] + #[error("Bar")] + struct Bar; + + #[derive(Debug, Diagnostic, Error)] + #[error("Foo")] + struct Foo { + #[source] + bar: Bar, + } + + let e = miette!(Foo { bar: Bar }); + let mut chain = e.chain(); + + assert_eq!("Foo", chain.next().unwrap().to_string()); + assert_eq!("Bar", chain.next().unwrap().to_string()); + assert!(chain.next().is_none()); +} + +#[test] +fn test_source_boxed() { + #[derive(Debug, Diagnostic, Error)] + #[error("Bar")] + struct Bar; + + #[derive(Debug, Diagnostic, Error)] + #[error("Foo")] + struct Foo { + #[source] + bar: Box, + } + + let error = miette!(Foo { bar: Box::new(Bar) }); + + let mut chain = error.chain(); + + assert_eq!("Foo", chain.next().unwrap().to_string()); + assert_eq!("Bar", chain.next().unwrap().to_string()); + assert!(chain.next().is_none()); +} + +#[test] +fn test_source_arc() { + #[derive(Debug, Diagnostic, Error)] + #[error("Bar")] + struct Bar; + + #[derive(Debug, Diagnostic, Error)] + #[error("Foo")] + struct Foo { + #[source] + bar: Arc, + } + + let error = miette!(Foo { bar: Arc::new(Bar) }); + + let mut chain = error.chain(); + + assert_eq!("Foo", chain.next().unwrap().to_string()); + assert_eq!("Bar", chain.next().unwrap().to_string()); + assert!(chain.next().is_none()); +} From c3d4db2618c06743c6db615a372a347e0bd4ed1a Mon Sep 17 00:00:00 2001 From: Bruce Mitchener Date: Sat, 8 Jul 2023 07:16:53 +0700 Subject: [PATCH 08/26] docs: Fix broken links from macro docs. (#278) --- src/eyreish/macros.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/eyreish/macros.rs b/src/eyreish/macros.rs index 938ac32..e13309f 100644 --- a/src/eyreish/macros.rs +++ b/src/eyreish/macros.rs @@ -222,6 +222,9 @@ macro_rules! ensure { /// ## `anyhow`/`eyre` Users /// /// You can just replace `use`s of the `anyhow!`/`eyre!` macros with `miette!`. +/// +/// [`diagnostic!`]: crate::diagnostic! +/// [`Report`]: crate::Report #[macro_export] macro_rules! miette { ($($key:ident = $value:expr,)* $fmt:literal $($arg:tt)*) => { @@ -282,6 +285,8 @@ macro_rules! miette { )] /// assert_eq!(diag.message, "1 + 2 = 3"); /// ``` +/// +/// [`MietteDiagnostic`]: crate::MietteDiagnostic #[macro_export] macro_rules! diagnostic { ($fmt:literal $($arg:tt)*) => {{ From bfacb6329f0063b5dc5a8c0b4344743cf3fa183a Mon Sep 17 00:00:00 2001 From: Gavrilikhin Daniil Date: Sun, 16 Jul 2023 04:46:54 +0300 Subject: [PATCH 09/26] doc: document `#[diagnostic(transparent)]` (#266) --- README.md | 75 ++++++++++++++++++++++++++++++++---------------------- src/lib.rs | 14 +++++++++- 2 files changed, 57 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index 8375876..a60ef11 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ You run miette? You run her code like the software? Oh. Oh! Error code for coder! Error code for One Thousand Lines! -## About +### About `miette` is a diagnostic library for Rust. It includes a series of traits/protocols that allow you to hook into its error reporting facilities, @@ -32,7 +32,7 @@ output like in the screenshots above.** You should only do this in your toplevel crate, as the fancy feature pulls in a number of dependencies that libraries and such might not want. -## Table of Contents +### Table of Contents - [About](#about) - [Features](#features) @@ -51,7 +51,7 @@ libraries and such might not want. - [Acknowledgements](#acknowledgements) - [License](#license) -## Features +### Features - Generic [`Diagnostic`] protocol, compatible (and dependent on) [`std::error::Error`]. @@ -76,7 +76,7 @@ the following features: - Cause chain printing - Turns diagnostic codes into links in [supported terminals](https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda). -## Installing +### Installing ```sh $ cargo add miette @@ -88,7 +88,7 @@ If you want to use the fancy printer in all these screenshots: $ cargo add miette --features fancy ``` -## Example +### Example ```rust /* @@ -168,9 +168,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 @@ -185,7 +185,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)] @@ -197,6 +197,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 } ``` @@ -204,7 +216,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 @@ -246,8 +258,7 @@ pub fn some_tool() -> Result { } ``` -To construct your own simple adhoc error use the [`miette!`] macro: - +To construct your own simple adhoc error use the [miette!] macro: ```rust // my_app/lib/my_internal_file.rs use miette::{miette, IntoDiagnostic, Result, WrapErr}; @@ -260,8 +271,9 @@ pub fn some_tool() -> Result { .map_err(|_| miette!("Invalid version {}", version))?) } ``` +There are also similar [bail!] and [ensure!] macros. -### ... in `main()` +#### ... in `main()` `main()` is just like any other part of your application-internal code. Use `Result` as your return value, and it will pretty-print your diagnostics @@ -291,7 +303,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: @@ -344,7 +356,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 @@ -392,7 +404,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 @@ -428,7 +440,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. @@ -448,7 +460,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) @@ -531,7 +543,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 @@ -564,7 +576,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 @@ -583,12 +595,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 @@ -597,6 +610,7 @@ 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[ @@ -608,26 +622,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). diff --git a/src/lib.rs b/src/lib.rs index cc113ce..20589ef 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -186,7 +186,7 @@ //! //! ```rust //! // lib/error.rs -//! use miette::Diagnostic; +//! use miette::{Diagnostic, SourceSpan}; //! use thiserror::Error; //! //! #[derive(Error, Diagnostic, Debug)] @@ -198,6 +198,18 @@ //! #[error("Oops it blew up")] //! #[diagnostic(code(my_lib::bad_code))] //! BadThingHappened, +//! +//! #[error(transparent)] +//! // Use `#[diagnostic(transparent)]` to wrap another [`Diagnostic`]. You won't see labels otherwise +//! #[diagnostic(transparent)] +//! AnotherError(#[from] AnotherError), +//! } +//! +//! #[derive(Error, Diagnostic, Debug)] +//! #[error("another error")] +//! pub struct AnotherError { +//! #[label("here")] +//! pub at: SourceSpan //! } //! ``` //! From 10c3b7f809ec7f8885c4f63fc9e36d3e419c339e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kat=20March=C3=A1n?= Date: Sat, 15 Jul 2023 18:48:00 -0700 Subject: [PATCH 10/26] docs: update readme again --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index a60ef11..7aabe99 100644 --- a/README.md +++ b/README.md @@ -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, NamedSource, Result, SourceSpan}; +use miette::{Diagnostic, SourceSpan}; use thiserror::Error; #[derive(Error, Debug, Diagnostic)] @@ -123,10 +123,12 @@ 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}; fn this_fails() -> Result<()> { // You can use plain strings as a `Source`, or anything that implements // the one-method `Source` trait. let src = "source\n text\n here".to_string(); + let len = src.len(); Err(MyBad { src: NamedSource::new("bad_file.rs", src), From f8d13825015317828dc268071b8f9bd50579e4ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kat=20March=C3=A1n?= Date: Sat, 15 Jul 2023 18:48:59 -0700 Subject: [PATCH 11/26] docs: update changelog --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b842de..955c27d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # `miette` Release Changelog + +## 5.10.0 (2023-07-16) + +### Features + +* **protocol:** add StdError impl for Box (#273) ([2e3e5c9d](https://github.com/zkat/miette/commit/2e3e5c9d15e234495369e9b47d032644dd5664ad)) + ## 5.9.0 (2023-05-18) From f4d056e1ffeb9a0bf36e2a6501365bd7e00db22d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kat=20March=C3=A1n?= Date: Sat, 15 Jul 2023 18:49:14 -0700 Subject: [PATCH 12/26] chore: Release --- Cargo.toml | 4 ++-- miette-derive/Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 160556f..dd6e584 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "miette" -version = "5.9.0" +version = "5.10.0" authors = ["Kat Marchán "] description = "Fancy diagnostic reporting library and protocol for us mere mortals who aren't compiler hackers." categories = ["rust-patterns"] @@ -14,7 +14,7 @@ exclude = ["images/", "tests/", "miette-derive/"] [dependencies] thiserror = "1.0.40" -miette-derive = { path = "miette-derive", version = "=5.9.0" } +miette-derive = { path = "miette-derive", version = "=5.10.0" } once_cell = "1.8.0" unicode-width = "0.1.9" diff --git a/miette-derive/Cargo.toml b/miette-derive/Cargo.toml index 461a328..becdc17 100644 --- a/miette-derive/Cargo.toml +++ b/miette-derive/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "miette-derive" -version = "5.9.0" +version = "5.10.0" authors = ["Kat Marchán "] edition = "2018" license = "Apache-2.0" From a9c2bae9dc095e530c5ff4cccb6a08b05084afc8 Mon Sep 17 00:00:00 2001 From: Bruce Mitchener Date: Mon, 18 Sep 2023 23:12:56 +0700 Subject: [PATCH 13/26] docs: Improve linking. (#289) --- src/eyreish/error.rs | 8 ++++---- src/handlers/theme.rs | 11 ++++++----- src/protocol.rs | 2 +- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/eyreish/error.rs b/src/eyreish/error.rs index 6b0dc34..677f368 100644 --- a/src/eyreish/error.rs +++ b/src/eyreish/error.rs @@ -30,9 +30,9 @@ impl Report { /// Create a new error object from a printable error message. /// - /// If the argument implements std::error::Error, prefer `Report::new` + /// If the argument implements [`std::error::Error`], prefer `Report::new` /// instead which preserves the underlying error's cause chain and - /// backtrace. If the argument may or may not implement std::error::Error + /// backtrace. If the argument may or may not implement [`std::error::Error`] /// now or in the future, use `miette!(err)` which handles either way /// correctly. /// @@ -206,7 +206,7 @@ impl Report { /// Create a new error from an error message to wrap the existing error. /// /// For attaching a higher level error message to a `Result` as it is - /// propagated, the [crate::WrapErr] extension trait may be more + /// propagated, the [`WrapErr`](crate::WrapErr) extension trait may be more /// convenient than this function. /// /// The primary reason to use `error.wrap_err(...)` instead of @@ -233,7 +233,7 @@ impl Report { unsafe { Report::construct(error, vtable, handler) } } - /// Compatibility re-export of wrap_err for interop with `anyhow` + /// Compatibility re-export of `wrap_err` for interop with `anyhow` pub fn context(self, msg: D) -> Self where D: Display + Send + Sync + 'static, diff --git a/src/handlers/theme.rs b/src/handlers/theme.rs index 1f5236a..aa6649e 100644 --- a/src/handlers/theme.rs +++ b/src/handlers/theme.rs @@ -55,9 +55,9 @@ impl GraphicalTheme { /// A "basic" graphical theme that skips colors and unicode characters and /// just does monochrome ascii art. If you want a completely non-graphical - /// rendering of your `Diagnostic`s, check out - /// [crate::NarratableReportHandler], or write your own - /// [crate::ReportHandler]! + /// rendering of your [`Diagnostic`](crate::Diagnostic)s, check out + /// [`NarratableReportHandler`](crate::NarratableReportHandler), or write + /// your own [`ReportHandler`](crate::ReportHandler) pub fn none() -> Self { Self { characters: ThemeCharacters::ascii(), @@ -79,7 +79,8 @@ impl Default for GraphicalTheme { } /** -Styles for various parts of graphical rendering for the [crate::GraphicalReportHandler]. +Styles for various parts of graphical rendering for the +[`GraphicalReportHandler`](crate::GraphicalReportHandler). */ #[derive(Debug, Clone)] pub struct ThemeStyles { @@ -159,7 +160,7 @@ impl ThemeStyles { // https://github.com/zesterer/ariadne/blob/e3cb394cb56ecda116a0a1caecd385a49e7f6662/src/draw.rs /// Characters to be used when drawing when using -/// [crate::GraphicalReportHandler]. +/// [`GraphicalReportHandler`](crate::GraphicalReportHandler). #[allow(missing_docs)] #[derive(Debug, Clone, Eq, PartialEq)] pub struct ThemeCharacters { diff --git a/src/protocol.rs b/src/protocol.rs index f516984..36c3539 100644 --- a/src/protocol.rs +++ b/src/protocol.rs @@ -232,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, From cc81382a6070dd226a20e4a39518d88e957ac0e1 Mon Sep 17 00:00:00 2001 From: Bruce Mitchener Date: Thu, 21 Sep 2023 00:37:40 +0700 Subject: [PATCH 14/26] fix(misc): Improve ci and fix clippy (#290) * ci: Update actions, replace actions-rs. * `actions/checkout` is updated from `v1` to the current `v4`. * `actions-rs/toolchain` is replaced by `dtolnay/rust-toolchain` as the `actions-rs` actions haven't been maintained in a long time. * clippy: Remove unnecessary call to `into_iter`. The parameter takes `IntoIterator`, so we don't have to call `into_iter` at the call site. * clippy: Remove explicit lifetime that can be elided. * clippy: tests: Fix useless conversion warnings. * clippy: tests: Remove call to `format!`. * Fix minimal-versions build. Due to changes in the nightly compiler, using a recent nightly requires proc-macro2 1.0.60 or later: https://github.com/dtolnay/proc-macro2/issues/356 * ci: Use is-terminal 0.4.7 for MSRV builds. is-terminal 0.4.8 updated its MSRV to 1.63, so we can't use it with our MSRV of 1.56. Force usage of the older version which has an older MSRV. --- .github/workflows/ci.yml | 27 ++++++++----------- miette-derive/Cargo.toml | 2 +- src/handlers/graphical.rs | 4 +-- src/miette_diagnostic.rs | 2 +- tests/derive.rs | 37 ++++++--------------------- tests/test_diagnostic_source_macro.rs | 2 +- tests/test_json.rs | 16 ------------ 7 files changed, 24 insertions(+), 66 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4fafac3..912bfcc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,14 +10,12 @@ jobs: name: Check fmt & build docs runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v4 - name: Install Rust - uses: actions-rs/toolchain@v1 + uses: dtolnay/rust-toolchain@master with: - profile: minimal toolchain: stable components: rustfmt - override: true - name: rustfmt run: cargo fmt --all -- --check - name: docs @@ -32,14 +30,15 @@ jobs: os: [ubuntu-latest, macOS-latest, windows-latest] steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v4 - name: Install Rust - uses: actions-rs/toolchain@v1 + uses: dtolnay/rust-toolchain@master with: - profile: minimal toolchain: ${{ matrix.rust }} components: clippy - override: true + - name: Force older version of is-terminal for MSRV builds + if: matrix.rust == '1.56.0' + run: cargo update -p is-terminal --precise 0.4.7 - name: Clippy run: cargo clippy --all -- -D warnings - name: Run tests @@ -54,14 +53,12 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v4 - name: Install Rust - uses: actions-rs/toolchain@v1 + uses: dtolnay/rust-toolchain@master with: - profile: minimal toolchain: nightly components: miri,rust-src - override: true - name: Run tests with miri env: MIRIFLAGS: -Zmiri-disable-isolation -Zmiri-strict-provenance @@ -75,13 +72,11 @@ jobs: os: [ubuntu-latest, macOS-latest, windows-latest] steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v4 - name: Install Rust - uses: actions-rs/toolchain@v1 + uses: dtolnay/rust-toolchain@master with: - profile: minimal toolchain: nightly - override: true - name: Run minimal version build run: cargo build -Z minimal-versions --all-features diff --git a/miette-derive/Cargo.toml b/miette-derive/Cargo.toml index becdc17..b5a7032 100644 --- a/miette-derive/Cargo.toml +++ b/miette-derive/Cargo.toml @@ -11,6 +11,6 @@ repository = "https://github.com/zkat/miette" proc-macro = true [dependencies] -proc-macro2 = "1.0" +proc-macro2 = "1.0.60" quote = "1.0" syn = "2.0.11" diff --git a/src/handlers/graphical.rs b/src/handlers/graphical.rs index b5dd754..e61f595 100644 --- a/src/handlers/graphical.rs +++ b/src/handlers/graphical.rs @@ -382,10 +382,10 @@ 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 { diff --git a/src/miette_diagnostic.rs b/src/miette_diagnostic.rs index 67b75d0..dc0468e 100644 --- a/src/miette_diagnostic.rs +++ b/src/miette_diagnostic.rs @@ -252,7 +252,7 @@ impl MietteDiagnostic { /// ``` pub fn and_labels(mut self, labels: impl IntoIterator) -> Self { let mut all_labels = self.labels.unwrap_or_default(); - all_labels.extend(labels.into_iter()); + all_labels.extend(labels); self.labels = Some(all_labels); self } diff --git a/tests/derive.rs b/tests/derive.rs index 7faae42..dbaf7cb 100644 --- a/tests/derive.rs +++ b/tests/derive.rs @@ -189,7 +189,7 @@ fn fmt_help() { assert_eq!( "1 x hello x \"2\"".to_string(), - FooStruct("hello".into()).help().unwrap().to_string() + FooStruct("hello").help().unwrap().to_string() ); #[derive(Debug, Diagnostic, Error)] @@ -201,12 +201,7 @@ fn fmt_help() { assert_eq!( "1 x hello x \"2\"".to_string(), - BarStruct { - my_field: "hello".into() - } - .help() - .unwrap() - .to_string() + BarStruct { my_field: "hello" }.help().unwrap().to_string() ); #[derive(Debug, Diagnostic, Error)] @@ -224,7 +219,7 @@ fn fmt_help() { assert_eq!( "1 x bar x \"2\"".to_string(), - FooEnum::X("bar".into()).help().unwrap().to_string() + FooEnum::X("bar").help().unwrap().to_string() ); assert_eq!( @@ -250,12 +245,7 @@ fn help_field() { assert_eq!( "x".to_string(), - Foo { - do_this: Some("x".into()) - } - .help() - .unwrap() - .to_string() + Foo { do_this: Some("x") }.help().unwrap().to_string() ); #[derive(Debug, Diagnostic, Error)] @@ -271,16 +261,11 @@ fn help_field() { assert_eq!( "x".to_string(), - Bar::A(Some("x".into())).help().unwrap().to_string() + Bar::A(Some("x")).help().unwrap().to_string() ); assert_eq!( "x".to_string(), - Bar::B { - do_this: Some("x".into()) - } - .help() - .unwrap() - .to_string() + Bar::B { do_this: Some("x") }.help().unwrap().to_string() ); #[derive(Debug, Diagnostic, Error)] @@ -288,20 +273,14 @@ fn help_field() { #[diagnostic()] struct Baz<'a>(#[help] Option<&'a str>); - assert_eq!( - "x".to_string(), - Baz(Some("x".into())).help().unwrap().to_string() - ); + assert_eq!("x".to_string(), Baz(Some("x")).help().unwrap().to_string()); #[derive(Debug, Diagnostic, Error)] #[error("welp")] #[diagnostic()] struct Quux<'a>(#[help] &'a str); - assert_eq!( - "x".to_string(), - Quux("x".into()).help().unwrap().to_string() - ); + assert_eq!("x".to_string(), Quux("x").help().unwrap().to_string()); } #[test] diff --git a/tests/test_diagnostic_source_macro.rs b/tests/test_diagnostic_source_macro.rs index 536aedf..df30b2e 100644 --- a/tests/test_diagnostic_source_macro.rs +++ b/tests/test_diagnostic_source_macro.rs @@ -79,7 +79,7 @@ fn test_diagnostic_source() { fn test_diagnostic_source_pass_extra_info() { let diag = TestBoxedError(Box::new(SourceError { code: String::from("Hello\nWorld!"), - help: format!("Have you tried turning it on and off again?"), + help: String::from("Have you tried turning it on and off again?"), label: (1, 4), })); let mut out = String::new(); diff --git a/tests/test_json.rs b/tests/test_json.rs index 5bd14cb..ae482b8 100644 --- a/tests/test_json.rs +++ b/tests/test_json.rs @@ -52,7 +52,6 @@ mod json_report_handler { "related": [] }"# .lines() - .into_iter() .map(|s| s.trim_matches(|c| c == ' ' || c == '\n')) .collect(); assert_eq!(expected, out); @@ -98,7 +97,6 @@ mod json_report_handler { "related": [] }"# .lines() - .into_iter() .map(|s| s.trim_matches(|c| c == ' ' || c == '\n')) .collect(); assert_eq!(expected, out); @@ -144,7 +142,6 @@ mod json_report_handler { "related": [] }"# .lines() - .into_iter() .map(|s| s.trim_matches(|c| c == ' ' || c == '\n')) .collect(); assert_eq!(expected, out); @@ -190,7 +187,6 @@ mod json_report_handler { "related": [] }"# .lines() - .into_iter() .map(|s| s.trim_matches(|c| c == ' ' || c == '\n')) .collect(); assert_eq!(expected, out); @@ -235,7 +231,6 @@ mod json_report_handler { "related": [] }"# .lines() - .into_iter() .map(|s| s.trim_matches(|c| c == ' ' || c == '\n')) .collect(); assert_eq!(expected, out); @@ -281,7 +276,6 @@ mod json_report_handler { "related": [] }"# .lines() - .into_iter() .map(|s| s.trim_matches(|c| c == ' ' || c == '\n')) .collect(); assert_eq!(expected, out); @@ -347,7 +341,6 @@ mod json_report_handler { "related": [] }"# .lines() - .into_iter() .map(|s| s.trim_matches(|c| c == ' ' || c == '\n')) .collect(); assert_eq!(expected, out); @@ -393,7 +386,6 @@ mod json_report_handler { "related": [] }"# .lines() - .into_iter() .map(|s| s.trim_matches(|c| c == ' ' || c == '\n')) .collect(); assert_eq!(expected, out); @@ -456,7 +448,6 @@ mod json_report_handler { "related": [] }"# .lines() - .into_iter() .map(|s| s.trim_matches(|c| c == ' ' || c == '\n')) .collect(); assert_eq!(expected, out); @@ -532,7 +523,6 @@ mod json_report_handler { "related": [] }"# .lines() - .into_iter() .map(|s| s.trim_matches(|c| c == ' ' || c == '\n')) .collect(); assert_eq!(expected, out); @@ -588,7 +578,6 @@ mod json_report_handler { "related": [] }"# .lines() - .into_iter() .map(|s| s.trim_matches(|c| c == ' ' || c == '\n')) .collect(); assert_eq!(expected, out); @@ -644,7 +633,6 @@ mod json_report_handler { "related": [] }"# .lines() - .into_iter() .map(|s| s.trim_matches(|c| c == ' ' || c == '\n')) .collect(); assert_eq!(expected, out); @@ -700,7 +688,6 @@ mod json_report_handler { "related": [] }"# .lines() - .into_iter() .map(|s| s.trim_matches(|c| c == ' ' || c == '\n')) .collect(); assert_eq!(expected, out); @@ -728,7 +715,6 @@ mod json_report_handler { "related": [] }"# .lines() - .into_iter() .map(|s| s.trim_matches(|c| c == ' ' || c == '\n')) .collect(); assert_eq!(expected, out); @@ -822,7 +808,6 @@ mod json_report_handler { }] }"# .lines() - .into_iter() .map(|s| s.trim_matches(|c| c == ' ' || c == '\n')) .collect(); assert_eq!(expected, out); @@ -920,7 +905,6 @@ mod json_report_handler { }] }"# .lines() - .into_iter() .map(|s| s.trim_matches(|c| c == ' ' || c == '\n')) .collect(); assert_eq!(expected, out); From db0b7e403a5ae52ae360991b6508490d8c579886 Mon Sep 17 00:00:00 2001 From: Will Crichton Date: Wed, 20 Sep 2023 15:36:33 -0700 Subject: [PATCH 15/26] feat(labels): Add support for primary label in specifying line/col information (#291) --- miette-derive/src/label.rs | 79 +++++++++++++++++++++------ src/handlers/graphical.rs | 27 +++++++-- src/miette_diagnostic.rs | 12 ++-- src/protocol.rs | 32 +++++++++-- tests/graphical.rs | 73 ++++++++++++++++++------- tests/test_diagnostic_source_macro.rs | 2 +- 6 files changed, 174 insertions(+), 51 deletions(-) diff --git a/miette-derive/src/label.rs b/miette-derive/src/label.rs index e0bc70a..dd5ec69 100644 --- a/miette-derive/src/label.rs +++ b/miette-derive/src/label.rs @@ -20,10 +20,12 @@ struct Label { label: Option, ty: syn::Type, span: syn::Member, + primary: bool, } struct LabelAttr { label: Option, + primary: bool, } impl Parse for LabelAttr { @@ -40,10 +42,22 @@ impl Parse for LabelAttr { } }); let la = input.lookahead1(); - let label = if la.peek(syn::token::Paren) { - // #[label("{}", x)] + let (primary, label) = if la.peek(syn::token::Paren) { + // #[label(primary?, "{}", x)] let content; parenthesized!(content in input); + + let primary = if content.peek(syn::Ident) { + let ident: syn::Ident = content.parse()?; + if ident != "primary" { + return Err(syn::Error::new(input.span(), "Invalid argument to label() attribute. The argument must be a literal string or the keyword `primary`.")); + } + let _ = content.parse::(); + true + } else { + false + }; + if content.peek(syn::LitStr) { let fmt = content.parse()?; let args = if content.is_empty() { @@ -56,22 +70,27 @@ impl Parse for LabelAttr { args, has_bonus_display: false, }; - Some(display) + (primary, Some(display)) + } else if !primary { + return Err(syn::Error::new(input.span(), "Invalid argument to label() attribute. The argument must be a literal string or the keyword `primary`.")); } else { - return Err(syn::Error::new(input.span(), "Invalid argument to label() attribute. The first argument must be a literal string.")); + (primary, None) } } else if la.peek(Token![=]) { // #[label = "blabla"] input.parse::()?; - Some(Display { - fmt: input.parse()?, - args: TokenStream::new(), - has_bonus_display: false, - }) + ( + false, + Some(Display { + fmt: input.parse()?, + args: TokenStream::new(), + has_bonus_display: false, + }), + ) } else { - None + (false, None) }; - Ok(LabelAttr { label }) + Ok(LabelAttr { label, primary }) } } @@ -100,12 +119,21 @@ impl Labels { }) }; use quote::ToTokens; - let LabelAttr { label } = + let LabelAttr { label, primary } = syn::parse2::(attr.meta.to_token_stream())?; + + if primary && labels.iter().any(|l: &Label| l.primary) { + return Err(syn::Error::new( + field.span(), + "Cannot have more than one primary label.", + )); + } + labels.push(Label { label, span, ty: field.ty.clone(), + primary, }); } } @@ -120,13 +148,23 @@ impl Labels { pub(crate) fn gen_struct(&self, fields: &syn::Fields) -> Option { let (display_pat, display_members) = display_pat_members(fields); let labels = self.0.iter().map(|highlight| { - let Label { span, label, ty } = highlight; + let Label { + span, + label, + ty, + primary, + } = highlight; let var = quote! { __miette_internal_var }; + let ctor = if *primary { + quote! { miette::LabeledSpan::new_primary_with_span } + } else { + quote! { miette::LabeledSpan::new_with_span } + }; if let Some(display) = label { let (fmt, args) = display.expand_shorthand_cloned(&display_members); quote! { miette::macro_helpers::OptionalWrapper::<#ty>::new().to_option(&self.#span) - .map(|#var| miette::LabeledSpan::new_with_span( + .map(|#var| #ctor( std::option::Option::Some(format!(#fmt #args)), #var.clone(), )) @@ -134,7 +172,7 @@ impl Labels { } else { quote! { miette::macro_helpers::OptionalWrapper::<#ty>::new().to_option(&self.#span) - .map(|#var| miette::LabeledSpan::new_with_span( + .map(|#var| #ctor( std::option::Option::None, #var.clone(), )) @@ -161,7 +199,7 @@ impl Labels { let (display_pat, display_members) = display_pat_members(fields); labels.as_ref().and_then(|labels| { let variant_labels = labels.0.iter().map(|label| { - let Label { span, label, ty } = label; + let Label { span, label, ty, primary } = label; let field = match &span { syn::Member::Named(ident) => ident.clone(), syn::Member::Unnamed(syn::Index { index, .. }) => { @@ -169,11 +207,16 @@ impl Labels { } }; let var = quote! { __miette_internal_var }; + let ctor = if *primary { + quote! { miette::LabeledSpan::new_primary_with_span } + } else { + quote! { miette::LabeledSpan::new_with_span } + }; if let Some(display) = label { let (fmt, args) = display.expand_shorthand_cloned(&display_members); quote! { miette::macro_helpers::OptionalWrapper::<#ty>::new().to_option(#field) - .map(|#var| miette::LabeledSpan::new_with_span( + .map(|#var| #ctor( std::option::Option::Some(format!(#fmt #args)), #var.clone(), )) @@ -181,7 +224,7 @@ impl Labels { } else { quote! { miette::macro_helpers::OptionalWrapper::<#ty>::new().to_option(#field) - .map(|#var| miette::LabeledSpan::new_with_span( + .map(|#var| #ctor( std::option::Option::None, #var.clone(), )) diff --git a/src/handlers/graphical.rs b/src/handlers/graphical.rs index e61f595..a7399fd 100644 --- a/src/handlers/graphical.rs +++ b/src/handlers/graphical.rs @@ -391,6 +391,11 @@ impl GraphicalReportHandler { ) -> 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() @@ -431,19 +436,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! diff --git a/src/miette_diagnostic.rs b/src/miette_diagnostic.rs index dc0468e..9863e88 100644 --- a/src/miette_diagnostic.rs +++ b/src/miette_diagnostic.rs @@ -292,14 +292,16 @@ fn test_serialize_miette_diagnostic() { "offset": 0, "length": 0 }, - "label": "label1" + "label": "label1", + "primary": false }, { "span": { "offset": 1, "length": 2 }, - "label": "label2" + "label": "label2", + "primary": false } ] }); @@ -350,14 +352,16 @@ fn test_deserialize_miette_diagnostic() { "offset": 0, "length": 0 }, - "label": "label1" + "label": "label1", + "primary": false }, { "span": { "offset": 1, "length": 2 }, - "label": "label2" + "label": "label2", + "primary": false } ] }); diff --git a/src/protocol.rs b/src/protocol.rs index 36c3539..be313b1 100644 --- a/src/protocol.rs +++ b/src/protocol.rs @@ -249,6 +249,7 @@ pub struct LabeledSpan { #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] label: Option, span: SourceSpan, + primary: bool, } impl LabeledSpan { @@ -257,6 +258,7 @@ impl LabeledSpan { Self { label, span: SourceSpan::new(SourceOffset(offset), SourceOffset(len)), + primary: false, } } @@ -265,6 +267,16 @@ impl LabeledSpan { Self { label, span: span.into(), + primary: false, + } + } + + /// Makes a new labeled primary span using an existing span. + pub fn new_primary_with_span(label: Option, span: impl Into) -> Self { + Self { + label, + span: span.into(), + primary: true, } } @@ -340,6 +352,11 @@ impl LabeledSpan { 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")] @@ -350,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, }) ); @@ -358,7 +376,8 @@ 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, }) ) } @@ -370,20 +389,23 @@ 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)) diff --git a/tests/graphical.rs b/tests/graphical.rs index 0c69470..887e454 100644 --- a/tests/graphical.rs +++ b/tests/graphical.rs @@ -86,7 +86,7 @@ fn single_line_highlight_span_full_line() { println!("Error: {}", out); let expected = r#" × oops! - ╭─[issue:1:1] + ╭─[issue:2:1] 1 │ source 2 │ text · ──┬─ @@ -120,7 +120,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 · ───┬── @@ -159,7 +159,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 · ──┬─ @@ -198,7 +198,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 · ──┬─ @@ -235,7 +235,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 · ──┬─ @@ -270,7 +270,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 · ──┬─ @@ -343,7 +343,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 @@ -379,7 +379,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 · ──┬── @@ -416,7 +416,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 · ──┬── @@ -453,7 +453,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 · ▲ @@ -490,7 +490,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 · ──── @@ -526,7 +526,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 · ──┬─ @@ -569,7 +569,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 · ──┬─ ──┬─ ──┬─ @@ -616,7 +616,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 · ──┬─ ──┬─ ──┬─ @@ -655,7 +655,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 @@ -969,7 +969,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 · ──┬─ @@ -1031,7 +1031,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 · ──┬─ @@ -1136,7 +1136,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,7 +1201,7 @@ fn zero_length_eol_span() { println!("Error: {}", out); let expected = r#" × oops! - ╭─[issue:1:1] + ╭─[issue:2:1] 1 │ this is the first line 2 │ this is the second line · ▲ @@ -1212,3 +1212,38 @@ 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); +} diff --git a/tests/test_diagnostic_source_macro.rs b/tests/test_diagnostic_source_macro.rs index df30b2e..0ca396a 100644 --- a/tests/test_diagnostic_source_macro.rs +++ b/tests/test_diagnostic_source_macro.rs @@ -91,7 +91,7 @@ fn test_diagnostic_source_pass_extra_info() { println!("Error: {}", out); let expected = r#" × TestError ╰─▶ × A complex error happened - ╭─[1:1] + ╭─[1:2] 1 │ Hello · ──┬─ · ╰── here From 1f448e47751d0f914134b0e9138fdb1a5a95d55c Mon Sep 17 00:00:00 2001 From: Bruce Mitchener Date: Tue, 26 Sep 2023 00:31:44 +0700 Subject: [PATCH 16/26] fix(tests): Fix `cargo test` with default features. (#294) A couple of tests require `fancy-no-backtrace`, so mark them accordingly. --- tests/test_diagnostic_source_macro.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/test_diagnostic_source_macro.rs b/tests/test_diagnostic_source_macro.rs index 0ca396a..334ca34 100644 --- a/tests/test_diagnostic_source_macro.rs +++ b/tests/test_diagnostic_source_macro.rs @@ -75,6 +75,7 @@ fn test_diagnostic_source() { 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 { @@ -106,6 +107,7 @@ fn test_diagnostic_source_pass_extra_info() { assert_eq!(expected, out); } +#[cfg(feature = "fancy-no-backtrace")] #[test] fn test_diagnostic_source_is_output() { let diag = TestStructError { @@ -147,6 +149,7 @@ struct NestedError { the_other_err: Box, } +#[cfg(feature = "fancy-no-backtrace")] #[test] fn test_nested_diagnostic_source_is_output() { let inner_error = TestStructError { From 06b348230aaf153b8b050322f05e5d185351d2d1 Mon Sep 17 00:00:00 2001 From: Bruce Mitchener Date: Tue, 26 Sep 2023 00:33:07 +0700 Subject: [PATCH 17/26] fix(clippy): Add missing semicolons where nothing is returned. (#293) --- src/handlers/graphical.rs | 4 ++-- src/handlers/json.rs | 2 +- src/protocol.rs | 12 ++++++------ tests/derive.rs | 4 ++-- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/handlers/graphical.rs b/src/handlers/graphical.rs index a7399fd..770944d 100644 --- a/src/handlers/graphical.rs +++ b/src/handlers/graphical.rs @@ -678,10 +678,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')?; diff --git a/src/handlers/json.rs b/src/handlers/json.rs index 29e21a0..0b4a405 100644 --- a/src/handlers/json.rs +++ b/src/handlers/json.rs @@ -96,7 +96,7 @@ impl JSONReportHandler { } write!(f, r#""{}""#, escape(&error.to_string()))?; } - write!(f, "],")? + write!(f, "],")?; } else { write!(f, r#""causes": [],"#)?; } diff --git a/src/protocol.rs b/src/protocol.rs index be313b1..4a6012c 100644 --- a/src/protocol.rs +++ b/src/protocol.rs @@ -379,7 +379,7 @@ fn test_serialize_labeled_span() { "span": { "offset": 0, "length": 0, }, "primary": false, }) - ) + ); } #[cfg(feature = "serde")] @@ -408,7 +408,7 @@ fn test_deserialize_labeled_span() { "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)); } /** @@ -597,7 +597,7 @@ fn test_serialize_source_span() { assert_eq!( json!(SourceSpan::from(0)), json!({ "offset": 0, "length": 0}) - ) + ); } #[cfg(feature = "serde")] @@ -606,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)); } /** @@ -708,12 +708,12 @@ fn test_source_offset_from_location() { fn test_serialize_source_offset() { use serde_json::json; - assert_eq!(json!(SourceOffset::from(0)), 0) + assert_eq!(json!(SourceOffset::from(0)), 0); } #[cfg(feature = "serde")] #[test] fn test_deserialize_source_offset() { let offset: SourceOffset = serde_json::from_str("0").unwrap(); - assert_eq!(offset, SourceOffset::from(0)) + assert_eq!(offset, SourceOffset::from(0)); } diff --git a/tests/derive.rs b/tests/derive.rs index dbaf7cb..f4739a7 100644 --- a/tests/derive.rs +++ b/tests/derive.rs @@ -568,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] @@ -582,5 +582,5 @@ fn test_unit_enum_display() { assert_eq!( Enum::UnitVariant.help().unwrap().to_string(), "hello from unit help" - ) + ); } From ba313282a8e6950fa7c19b9b66d45deb83aaa1bd Mon Sep 17 00:00:00 2001 From: Thomas Versteeg Date: Fri, 29 Sep 2023 13:56:09 +0000 Subject: [PATCH 18/26] docs: typo in README.md (#295) Dynamic diagnostics example contains a macro invocation without a '!' symbol. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7aabe99..7d73ba3 100644 --- a/README.md +++ b/README.md @@ -615,7 +615,7 @@ then you may want to use [`miette!`], [`diagnostic!`] macros or 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 '+'", From 88d00e0e20bf95e03b8f81dcd5adf38c917e190e Mon Sep 17 00:00:00 2001 From: Gavrilikhin Daniil Date: Sat, 21 Oct 2023 11:50:15 +0800 Subject: [PATCH 19/26] feat(derive): Allow optional sources in derive (#301) Fixes: https://github.com/zkat/miette/issues/217 --- miette-derive/src/source_code.rs | 42 +++++++++++++++++++++++++++++--- tests/derive.rs | 42 ++++++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+), 3 deletions(-) diff --git a/miette-derive/src/source_code.rs b/miette-derive/src/source_code.rs index 62f28e7..e1b85ab 100644 --- a/miette-derive/src/source_code.rs +++ b/miette-derive/src/source_code.rs @@ -10,6 +10,7 @@ use crate::{ pub struct SourceCode { source_code: syn::Member, + is_option: bool, } impl SourceCode { @@ -27,6 +28,19 @@ impl SourceCode { for (i, field) in fields.iter().enumerate() { for attr in &field.attrs { if attr.path().is_ident("source_code") { + let is_option = if let syn::Type::Path(syn::TypePath { + path: syn::Path { segments, .. }, + .. + }) = &field.ty + { + segments + .last() + .map(|seg| seg.ident == "Option") + .unwrap_or(false) + } else { + false + }; + let source_code = if let Some(ident) = field.ident.clone() { syn::Member::Named(ident) } else { @@ -35,7 +49,10 @@ impl SourceCode { span: field.span(), }) }; - return Ok(Some(SourceCode { source_code })); + return Ok(Some(SourceCode { + source_code, + is_option, + })); } } } @@ -45,11 +62,21 @@ impl SourceCode { pub(crate) fn gen_struct(&self, fields: &syn::Fields) -> Option { let (display_pat, _display_members) = display_pat_members(fields); let src = &self.source_code; + let ret = if self.is_option { + quote! { + self.#src.as_ref().map(|s| s as _) + } + } else { + quote! { + Some(&self.#src) + } + }; + Some(quote! { #[allow(unused_variables)] fn source_code(&self) -> std::option::Option<&dyn miette::SourceCode> { let Self #display_pat = self; - Some(&self.#src) + #ret } }) } @@ -68,10 +95,19 @@ impl SourceCode { } }; let variant_name = ident.clone(); + let ret = if source_code.is_option { + quote! { + #field.as_ref().map(|s| s as _) + } + } else { + quote! { + std::option::Option::Some(#field) + } + }; match &fields { syn::Fields::Unit => None, _ => Some(quote! { - Self::#variant_name #display_pat => std::option::Option::Some(#field), + Self::#variant_name #display_pat => #ret, }), } }) diff --git a/tests/derive.rs b/tests/derive.rs index f4739a7..d1da1bb 100644 --- a/tests/derive.rs +++ b/tests/derive.rs @@ -584,3 +584,45 @@ fn test_unit_enum_display() { "hello from unit help" ); } + +#[test] +fn test_optional_source_code() { + #[derive(Debug, Diagnostic, Error)] + #[error("struct with optional source")] + struct Struct { + #[source_code] + src: Option, + } + assert!(Struct { src: None }.source_code().is_none()); + assert!(Struct { + src: Some("".to_string()) + } + .source_code() + .is_some()); + + #[derive(Debug, Diagnostic, Error)] + enum Enum { + #[error("variant1 with optional source")] + Variant1 { + #[source_code] + src: Option, + }, + #[error("variant2 with optional source")] + Variant2 { + #[source_code] + src: Option, + }, + } + assert!(Enum::Variant1 { src: None }.source_code().is_none()); + assert!(Enum::Variant1 { + src: Some("".to_string()) + } + .source_code() + .is_some()); + assert!(Enum::Variant2 { src: None }.source_code().is_none()); + assert!(Enum::Variant2 { + src: Some("".to_string()) + } + .source_code() + .is_some()); +} From d37ada876a5831d3f47622274e334c9a24aa5d2b Mon Sep 17 00:00:00 2001 From: Andre Meyering Date: Mon, 23 Oct 2023 19:09:00 +0200 Subject: [PATCH 20/26] docs(README): Move import of `NamedResult` to where it is used (#309) An example in README.md makes use of `miette::NamedResult` . It is imported in the second "example-step", but already used in the first one. Beginners, that copy the example step-by-step to understand it, will run into compile errors, as `NamedResult` was not imported. --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 7d73ba3..a29b428 100644 --- a/README.md +++ b/README.md @@ -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. From a8b4ae012aa0cf03b53a18f013c2b3f76c5040e7 Mon Sep 17 00:00:00 2001 From: Matt Date: Thu, 2 Nov 2023 12:33:55 -0400 Subject: [PATCH 21/26] fix(graphical): Extend error text span to whole code points (#312) Fixes: https://github.com/zkat/miette/issues/223 This fixes a panic when an error starts inside a Unicode code point. The range is extended to start (or end) at the beginning (or end) of the character inside which the byte offset is located. --- src/handlers/graphical.rs | 19 +++++++--- tests/graphical.rs | 74 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 89 insertions(+), 4 deletions(-) diff --git a/src/handlers/graphical.rs b/src/handlers/graphical.rs index 770944d..44d92b7 100644 --- a/src/handlers/graphical.rs +++ b/src/handlers/graphical.rs @@ -651,11 +651,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() { @@ -706,8 +717,8 @@ 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 = self.visual_offset(line, byte_end, false).max(start + 1); let vbar_offset = (start + end) / 2; let num_left = vbar_offset - start; diff --git a/tests/graphical.rs b/tests/graphical.rs index 887e454..31db4dc 100644 --- a/tests/graphical.rs +++ b/tests/graphical.rs @@ -1247,3 +1247,77 @@ fn primary_label() { 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(()) +} From 3d6f903df0e7c9d0eb9a1fdbbf0028bab5496429 Mon Sep 17 00:00:00 2001 From: Benjamin Lee Date: Thu, 9 Nov 2023 13:21:32 -0800 Subject: [PATCH 22/26] fix(formatting): Fix formatting bug when an empty span is not aligned to a char boundary (#314) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previous output looked like this: ---- single_line_with_wide_char_unaligned_span_empty stdout ---- Error: 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? Note that the .max(start + 1) term is still necessary in the nonempty branch, since it's possible to have a nonempty span covering zero-width text. * remove uncessary if statement start > end in all cases. --- src/handlers/graphical.rs | 44 ++++++++++++++++++++------------------- tests/graphical.rs | 37 ++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 21 deletions(-) diff --git a/src/handlers/graphical.rs b/src/handlers/graphical.rs index 44d92b7..8cec88b 100644 --- a/src/handlers/graphical.rs +++ b/src/handlers/graphical.rs @@ -718,31 +718,33 @@ impl GraphicalReportHandler { let byte_start = hl.offset(); let byte_end = hl.offset() + hl.len(); let start = self.visual_offset(line, byte_start, true).max(highest); - let end = self.visual_offset(line, byte_end, false).max(start + 1); + let end = if hl.len() == 0 { + start + 1 + } else { + self.visual_offset(line, byte_end, false).max(start + 1) + }; let vbar_offset = (start + end) / 2; let 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) diff --git a/tests/graphical.rs b/tests/graphical.rs index 31db4dc..d24879a 100644 --- a/tests/graphical.rs +++ b/tests/graphical.rs @@ -1321,3 +1321,40 @@ fn single_line_with_wide_char_unaligned_span_end() -> Result<(), MietteError> { 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(()) +} From c7ba5b7e52e05991cecd3ca925c710bbe49850b9 Mon Sep 17 00:00:00 2001 From: ManicMarrc <122530518+ManicMarrc@users.noreply.github.com> Date: Fri, 10 Nov 2023 04:22:47 +0700 Subject: [PATCH 23/26] feat(derive): Make `miette-derive` be able to be turned off (#304) --- Cargo.toml | 5 +++-- src/error.rs | 42 +++++++++++++++++++++++++++++++++--------- src/lib.rs | 1 + 3 files changed, 37 insertions(+), 11 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index dd6e584..f8c2cb5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,7 @@ exclude = ["images/", "tests/", "miette-derive/"] [dependencies] thiserror = "1.0.40" -miette-derive = { path = "miette-derive", version = "=5.10.0" } +miette-derive = { path = "miette-derive", version = "=5.10.0", optional = true } once_cell = "1.8.0" unicode-width = "0.1.9" @@ -44,7 +44,8 @@ lazy_static = "1.4" serde_json = "1.0.64" [features] -default = [] +default = ["derive"] +derive = ["miette-derive"] no-format-args-capture = [] fancy-no-backtrace = [ "owo-colors", diff --git a/src/error.rs b/src/error.rs index 56041ca..4e57a78 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,27 +1,51 @@ -use std::io; +use std::{fmt, io}; use thiserror::Error; -use crate::{self as miette, Diagnostic}; +use crate::Diagnostic; /** Error enum for miette. Used by certain operations in the protocol. */ -#[derive(Debug, Diagnostic, Error)] +#[derive(Debug, Error)] pub enum MietteError { /// Wrapper around [`std::io::Error`]. This is returned when something went /// wrong while reading a [`SourceCode`](crate::SourceCode). #[error(transparent)] - #[diagnostic(code(miette::io_error), url(docsrs))] IoError(#[from] io::Error), /// Returned when a [`SourceSpan`](crate::SourceSpan) extends beyond the /// bounds of a given [`SourceCode`](crate::SourceCode). #[error("The given offset is outside the bounds of its Source")] - #[diagnostic( - code(miette::span_out_of_bounds), - help("Double-check your spans. Do you have an off-by-one error?"), - url(docsrs) - )] OutOfBounds, } + +impl Diagnostic for MietteError { + fn code<'a>(&'a self) -> Option> { + match self { + MietteError::IoError(_) => Some(Box::new("miette::io_error")), + MietteError::OutOfBounds => Some(Box::new("miette::span_out_of_bounds")), + } + } + + fn help<'a>(&'a self) -> Option> { + match self { + MietteError::IoError(_) => None, + MietteError::OutOfBounds => Some(Box::new( + "Double-check your spans. Do you have an off-by-one error?", + )), + } + } + + fn url<'a>(&'a self) -> Option> { + let crate_version = env!("CARGO_PKG_VERSION"); + let variant = match self { + MietteError::IoError(_) => "#variant.IoError", + MietteError::OutOfBounds => "#variant.OutOfBounds", + }; + Some(Box::new(format!( + "https://docs.rs/miette/{}/miette/enum.MietteError.html{}", + crate_version, variant, + ))) + } +} diff --git a/src/lib.rs b/src/lib.rs index 20589ef..3cb021b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -652,6 +652,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::*; From fd77257cee0f5d03aa7dccb4ba8cbaa40c1a88c6 Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Wed, 15 Nov 2023 12:34:24 -0600 Subject: [PATCH 24/26] feat(graphical): Expose additional `textwrap` options (#321) --- src/handler.rs | 34 +++++++ src/handlers/graphical.rs | 74 +++++++++++++-- src/lib.rs | 1 + tests/graphical.rs | 184 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 285 insertions(+), 8 deletions(-) diff --git a/src/handler.rs b/src/handler.rs index e983a55..e32f3ef 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -55,6 +55,9 @@ pub struct MietteHandlerOpts { pub(crate) context_lines: Option, pub(crate) tab_width: Option, pub(crate) with_cause_chain: Option, + pub(crate) break_words: Option, + pub(crate) word_separator: Option, + pub(crate) word_splitter: Option, } impl MietteHandlerOpts { @@ -86,6 +89,27 @@ impl MietteHandlerOpts { self } + /// If true, long words can be broken when wrapping. + /// + /// If false, long words will not be broken when they exceed the width. + /// + /// Defaults to true. + pub fn break_words(mut self, break_words: bool) -> Self { + self.break_words = Some(break_words); + self + } + + /// Sets the `textwrap::WordSeparator` to use when determining wrap points. + pub fn word_separator(mut self, word_separator: textwrap::WordSeparator) -> Self { + self.word_separator = Some(word_separator); + self + } + + /// Sets the `textwrap::WordSplitter` to use when determining wrap points. + pub fn word_splitter(mut self, word_splitter: textwrap::WordSplitter) -> Self { + self.word_splitter = Some(word_splitter); + self + } /// Include the cause chain of the top-level error in the report. pub fn with_cause_chain(mut self) -> Self { self.with_cause_chain = Some(true); @@ -233,6 +257,16 @@ impl MietteHandlerOpts { if let Some(w) = self.tab_width { handler = handler.tab_width(w); } + if let Some(b) = self.break_words { + handler = handler.with_break_words(b) + } + if let Some(s) = self.word_separator { + handler = handler.with_word_separator(s) + } + if let Some(s) = self.word_splitter { + handler = handler.with_word_splitter(s) + } + MietteHandler { inner: Box::new(handler), } diff --git a/src/handlers/graphical.rs b/src/handlers/graphical.rs index 8cec88b..35e9c79 100644 --- a/src/handlers/graphical.rs +++ b/src/handlers/graphical.rs @@ -30,6 +30,9 @@ pub struct GraphicalReportHandler { pub(crate) context_lines: usize, pub(crate) tab_width: usize, pub(crate) with_cause_chain: bool, + pub(crate) break_words: bool, + pub(crate) word_separator: Option, + pub(crate) word_splitter: Option, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -51,6 +54,9 @@ impl GraphicalReportHandler { context_lines: 1, tab_width: 4, with_cause_chain: true, + break_words: true, + word_separator: None, + word_splitter: None, } } @@ -64,6 +70,9 @@ impl GraphicalReportHandler { context_lines: 1, tab_width: 4, with_cause_chain: true, + break_words: true, + word_separator: None, + word_splitter: None, } } @@ -122,6 +131,24 @@ impl GraphicalReportHandler { self } + /// Enables or disables breaking of words during wrapping. + pub fn with_break_words(mut self, break_words: bool) -> Self { + self.break_words = break_words; + self + } + + /// Sets the word separator to use when wrapping. + pub fn with_word_separator(mut self, word_separator: textwrap::WordSeparator) -> Self { + self.word_separator = Some(word_separator); + self + } + + /// Sets the word splitter to usewhen wrapping. + pub fn with_word_splitter(mut self, word_splitter: textwrap::WordSplitter) -> Self { + self.word_splitter = Some(word_splitter); + self + } + /// Sets the 'global' footer for this handler. pub fn with_footer(mut self, footer: String) -> Self { self.footer = Some(footer); @@ -159,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(()) @@ -212,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))?; @@ -251,9 +293,17 @@ 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); + .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(); @@ -280,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(()) diff --git a/src/lib.rs b/src/lib.rs index 3cb021b..08de732 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -593,6 +593,7 @@ //! .unicode(false) //! .context_lines(3) //! .tab_width(4) +//! .break_words(true) //! .build(), //! ) //! })) diff --git a/tests/graphical.rs b/tests/graphical.rs index d24879a..536efcd 100644 --- a/tests/graphical.rs +++ b/tests/graphical.rs @@ -34,6 +34,190 @@ fn fmt_report(diag: Report) -> String { out } +fn fmt_report_with_settings( + diag: Report, + with_settings: fn(GraphicalReportHandler) -> GraphicalReportHandler, +) -> String { + let mut out = String::new(); + + let handler = with_settings(GraphicalReportHandler::new_themed( + GraphicalTheme::unicode_nocolor(), + )); + + handler.render_report(&mut out, diag.as_ref()).unwrap(); + + println!("Error:\n```\n{}\n```", out); + + out +} + +#[test] +fn word_wrap_options() -> Result<(), MietteError> { + // By default, a long word should not break + let out = + fmt_report_with_settings(Report::msg("abcdefghijklmnopqrstuvwxyz"), |handler| handler); + + let expected = " × abcdefghijklmnopqrstuvwxyz\n".to_string(); + assert_eq!(expected, out); + + // A long word can break with a smaller width + let out = fmt_report_with_settings(Report::msg("abcdefghijklmnopqrstuvwxyz"), |handler| { + handler.with_width(10) + }); + let expected = r#" × abcd + │ efgh + │ ijkl + │ mnop + │ qrst + │ uvwx + │ yz +"# + .to_string(); + assert_eq!(expected, out); + + // Unless, word breaking is disabled + let out = fmt_report_with_settings(Report::msg("abcdefghijklmnopqrstuvwxyz"), |handler| { + handler.with_width(10).with_break_words(false) + }); + let expected = " × abcdefghijklmnopqrstuvwxyz\n".to_string(); + assert_eq!(expected, out); + + // Breaks should start at the boundary of each word if possible + let out = fmt_report_with_settings( + Report::msg("12 123 1234 12345 123456 1234567 1234567890"), + |handler| handler.with_width(10), + ); + let expected = r#" × 12 + │ 123 + │ 1234 + │ 1234 + │ 5 + │ 1234 + │ 56 + │ 1234 + │ 567 + │ 1234 + │ 5678 + │ 90 +"# + .to_string(); + assert_eq!(expected, out); + + // But long words should not break if word breaking is disabled + let out = fmt_report_with_settings( + Report::msg("12 123 1234 12345 123456 1234567 1234567890"), + |handler| handler.with_width(10).with_break_words(false), + ); + let expected = r#" × 12 + │ 123 + │ 1234 + │ 12345 + │ 123456 + │ 1234567 + │ 1234567890 +"# + .to_string(); + assert_eq!(expected, out); + + // Unless, of course, there are hyphens + let out = fmt_report_with_settings( + Report::msg("a-b a-b-c a-b-c-d a-b-c-d-e a-b-c-d-e-f a-b-c-d-e-f-g a-b-c-d-e-f-g-h"), + |handler| handler.with_width(10).with_break_words(false), + ); + let expected = r#" × a-b + │ a-b- + │ c a- + │ b-c- + │ d a- + │ b-c- + │ d-e + │ a-b- + │ c-d- + │ e-f + │ a-b- + │ c-d- + │ e-f- + │ g a- + │ b-c- + │ d-e- + │ f-g- + │ h +"# + .to_string(); + assert_eq!(expected, out); + + // Which requires an additional opt-out + let out = fmt_report_with_settings( + Report::msg("a-b a-b-c a-b-c-d a-b-c-d-e a-b-c-d-e-f a-b-c-d-e-f-g a-b-c-d-e-f-g-h"), + |handler| { + handler + .with_width(10) + .with_break_words(false) + .with_word_splitter(textwrap::WordSplitter::NoHyphenation) + }, + ); + let expected = r#" × a-b + │ a-b-c + │ a-b-c-d + │ a-b-c-d-e + │ a-b-c-d-e-f + │ a-b-c-d-e-f-g + │ a-b-c-d-e-f-g-h +"# + .to_string(); + assert_eq!(expected, out); + + // Or if there are _other_ unicode word boundaries + let out = fmt_report_with_settings( + Report::msg("a/b a/b/c a/b/c/d a/b/c/d/e a/b/c/d/e/f a/b/c/d/e/f/g a/b/c/d/e/f/g/h"), + |handler| handler.with_width(10).with_break_words(false), + ); + let expected = r#" × a/b + │ a/b/ + │ c a/ + │ b/c/ + │ d a/ + │ b/c/ + │ d/e + │ a/b/ + │ c/d/ + │ e/f + │ a/b/ + │ c/d/ + │ e/f/ + │ g a/ + │ b/c/ + │ d/e/ + │ f/g/ + │ h +"# + .to_string(); + assert_eq!(expected, out); + + // Such things require you to opt-in to only breaking on ASCII whitespace + let out = fmt_report_with_settings( + Report::msg("a/b a/b/c a/b/c/d a/b/c/d/e a/b/c/d/e/f a/b/c/d/e/f/g a/b/c/d/e/f/g/h"), + |handler| { + handler + .with_width(10) + .with_break_words(false) + .with_word_separator(textwrap::WordSeparator::AsciiSpace) + }, + ); + let expected = r#" × a/b + │ a/b/c + │ a/b/c/d + │ a/b/c/d/e + │ a/b/c/d/e/f + │ a/b/c/d/e/f/g + │ a/b/c/d/e/f/g/h +"# + .to_string(); + assert_eq!(expected, out); + + Ok(()) +} + #[test] fn empty_source() -> Result<(), MietteError> { #[derive(Debug, Diagnostic, Error)] From 251d6d59292397458328ef57fb7957faedafd019 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonathan=20D=C3=B6nszelmann?= Date: Wed, 15 Nov 2023 19:35:46 +0100 Subject: [PATCH 25/26] fix(docs): add example to README and docs fixing #96 (#319) --- README.md | 17 +++++++++++++++++ src/lib.rs | 17 +++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/README.md b/README.md index a29b428..5f79c52 100644 --- a/README.md +++ b/README.md @@ -305,6 +305,23 @@ enabled: miette = { version = "X.Y.Z", features = ["fancy"] } ``` +Another way to display a diagnostic is by printing them using the debug formatter. +This is, in fact, what returning diagnostics from main ends up doing. +To do it yourself, you can write the following: + +```rust +use miette::{IntoDiagnostic, Result}; +use semver::Version; + +fn just_a_random_function() { + let version_result: Result = "1.2.x".parse().into_diagnostic(); + match version_result { + Err(e) => println!("{:?}", e), + Ok(version) => println!("{}", version), + } +} +``` + #### ... diagnostic code URLs `miette` supports providing a URL for individual diagnostics. This URL will diff --git a/src/lib.rs b/src/lib.rs index 08de732..d80efa7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -304,6 +304,23 @@ //! miette = { version = "X.Y.Z", features = ["fancy"] } //! ``` //! +//! Another way to display a diagnostic is by printing them using the debug formatter. +//! This is, in fact, what returning diagnostics from main ends up doing. +//! To do it yourself, you can write the following: +//! +//! ```rust +//! use miette::{IntoDiagnostic, Result}; +//! use semver::Version; +//! +//! fn just_a_random_function() { +//! let version_result: Result = "1.2.x".parse().into_diagnostic(); +//! match version_result { +//! Err(e) => println!("{:?}", e), +//! Ok(version) => println!("{}", version), +//! } +//! } +//! ``` +//! //! ### ... diagnostic code URLs //! //! `miette` supports providing a URL for individual diagnostics. This URL will From 865d67c8dda119ddd03ac43be22f4fa272a9f433 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonathan=20D=C3=B6nszelmann?= Date: Wed, 15 Nov 2023 19:39:29 +0100 Subject: [PATCH 26/26] feat(graphical): support rendering labels that contain newlines (#318) Fixes: https://github.com/zkat/miette/issues/85 --- src/handlers/graphical.rs | 313 ++++++++++++++++++++++++++++++++------ tests/graphical.rs | 171 +++++++++++++++++++++ 2 files changed, 435 insertions(+), 49 deletions(-) diff --git a/src/handlers/graphical.rs b/src/handlers/graphical.rs index 35e9c79..baadfb3 100644 --- a/src/handlers/graphical.rs +++ b/src/handlers/graphical.rs @@ -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, @@ -545,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, @@ -557,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)?; } } } @@ -575,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, @@ -643,6 +730,7 @@ impl GraphicalReportHandler { max_gutter: usize, line: &Line, highlights: &[FancySpan], + render_mode: LabelRenderMode, ) -> fmt::Result { if max_gutter == 0 { return Ok(()); @@ -652,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()); @@ -811,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; } } } @@ -839,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(()) } @@ -924,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, @@ -941,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 @@ -974,7 +1170,10 @@ impl Line { #[derive(Debug, Clone)] struct FancySpan { - label: Option, + /// this is deliberately an option of a vec because I wanted to be very explicit + /// that there can also be *no* label. If there is a label, it can have multiple + /// lines which is what the vec is for. + label: Option>, span: SourceSpan, style: Style, } @@ -985,9 +1184,17 @@ impl PartialEq for FancySpan { } } +fn split_label(v: String) -> Vec { + v.split('\n').map(|i| i.to_string()).collect() +} + impl FancySpan { fn new(label: Option, span: SourceSpan, style: Style) -> Self { - FancySpan { label, span, style } + FancySpan { + label: label.map(split_label), + span, + style, + } } fn style(&self) -> Style { @@ -997,7 +1204,15 @@ impl FancySpan { fn label(&self) -> Option { self.label .as_ref() - .map(|l| l.style(self.style()).to_string()) + .map(|l| l.join("\n").style(self.style()).to_string()) + } + + fn label_parts(&self) -> Option> { + self.label.as_ref().map(|l| { + l.iter() + .map(|i| i.style(self.style()).to_string()) + .collect() + }) } fn offset(&self) -> usize { diff --git a/tests/graphical.rs b/tests/graphical.rs index 536efcd..f59f849 100644 --- a/tests/graphical.rs +++ b/tests/graphical.rs @@ -251,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)] @@ -725,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)] @@ -853,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)]