Repository: zkat/miette (rust)

Issue:
`.source_code()` and `.with_source_code()` methods for `Report`
This will make it possible to delay providing `source_code` until the app is ready to provide it, which is a common pattern in compilers.

Pull request:
More ergonomic source code handling
Fixes #64 and #99

@zkat, I'm not sure what you mean by `source_code()` and `with_source_code()` code in #64. Since the report object is already an error it doesn't make much sense to put a lambda there. And since `source_code()` might be confused with `Diagnostic::source_code` I think `with_` works better.

The internal `WithSourceCode` wrapper is designed in a way that would allow adding `with_source_code` on error or even `WrapErr`. But I'm not sure these are needed, so did not include in the PR.


Patch:
diff --git a/README.md b/README.md
index 9dbe265..5515bbd 100644
--- a/README.md
+++ b/README.md
@@ -42,6 +42,7 @@ and such might not want.
   - [... diagnostic code URLs](#-diagnostic-code-urls)
   - [... snippets](#-snippets)
   - [... multiple related errors](#-multiple-related-errors)
+  - [... delayed source code](#-delayed-source-code)
   - [... handler options](#-handler-options)
 - [Acknowledgements](#acknowledgements)
 - [License](#license)
@@ -375,6 +376,86 @@ struct MyError {
 }
 ```
 
+### ... 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) method for
+that:
+
+```rust,no_run
+use miette::{Diagnostic, SourceSpan};
+use thiserror::Error;
+
+#[derive(Diagnostic, Debug, Error)]
+#[error("oops")]
+#[diagnostic()]
+pub struct MyErrorType {
+    // Note: label but no source code
+    #[label]
+    err_span: SourceSpan,
+}
+
+fn do_something() -> miette::Result<()> {
+    // This function emits actual error with label
+    return Err(MyErrorType { err_span: (7..11).into() })?;
+}
+
+fn main() -> miette::Result<()> {
+    do_something().map_err(|error| {
+        // And this code provides the source code for inner error
+        error.with_source_code(String::from("source code"))
+    })
+}
+
+```
+
+Also source code can be provided by a wrapper type. This is especially useful
+in combination with `related`, when multiple errors should be emitted at the
+same time:
+
+```rust,no_run
+use miette::{Report, Diagnostic, SourceSpan};
+use thiserror::Error;
+
+#[derive(Diagnostic, Debug, Error)]
+#[error("oops")]
+#[diagnostic()]
+pub struct InnerError {
+    // Note: label but no source code
+    #[label]
+    err_span: SourceSpan,
+}
+
+#[derive(Diagnostic, Debug, Error)]
+#[error("oops: multiple errors")]
+#[diagnostic()]
+pub struct MultiError {
+    // Note source code by no labels
+    #[source_code]
+    source_code: String,
+    // The source code above is used for these errors
+    #[related]
+    related: Vec<InnerError>,
+}
+
+fn do_something() -> Result<(), Vec<InnerError>> {
+    Err(vec![
+        InnerError { err_span: (0..6).into() },
+        InnerError { err_span: (7..11).into() },
+    ])
+}
+
+fn main() -> miette::Result<()> {
+    do_something().map_err(|err_list| {
+        MultiError {
+            source_code: "source code".into(),
+            related: err_list,
+        }
+    })?;
+    Ok(())
+}
+```
+
 ### ... handler options
 
 [MietteHandler] is the default handler, and is very customizable. In most
diff --git a/src/eyreish/error.rs b/src/eyreish/error.rs
index b078e5f..0f45dc9 100644
--- a/src/eyreish/error.rs
+++ b/src/eyreish/error.rs
@@ -7,7 +7,8 @@ use std::error::Error as StdError;
 use super::Report;
 use super::ReportHandler;
 use crate::chain::Chain;
-use crate::Diagnostic;
+use crate::eyreish::wrapper::WithSourceCode;
+use crate::{Diagnostic, SourceCode};
 use core::ops::{Deref, DerefMut};
 
 impl Report {
@@ -386,6 +387,15 @@ impl Report {
     pub fn handler_mut(&mut self) -> &mut dyn ReportHandler {
         self.inner.handler.as_mut().unwrap().as_mut()
     }
+
+    /// Provide source code for this error
+    pub fn with_source_code(self, source_code: impl SourceCode + Send + Sync + 'static) -> Report {
+        WithSourceCode {
+            source_code,
+            error: self,
+        }
+        .into()
+    }
 }
 
 impl<E> From<E> for Report
diff --git a/src/eyreish/wrapper.rs b/src/eyreish/wrapper.rs
index f3605e6..a5e67db 100644
--- a/src/eyreish/wrapper.rs
+++ b/src/eyreish/wrapper.rs
@@ -2,7 +2,7 @@ use core::fmt::{self, Debug, Display};
 
 use std::error::Error as StdError;
 
-use crate::{Diagnostic, LabeledSpan};
+use crate::{Diagnostic, LabeledSpan, Report, SourceCode};
 
 use crate as miette;
 
@@ -117,3 +117,92 @@ impl Display for BoxedError {
 }
 
 impl StdError for BoxedError {}
+
+pub(crate) struct WithSourceCode<E, C> {
+    pub(crate) error: E,
+    pub(crate) source_code: C,
+}
+
+impl<E: Diagnostic, C: SourceCode> Diagnostic for WithSourceCode<E, C> {
+    fn code<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
+        self.error.code()
+    }
+
+    fn severity(&self) -> Option<miette::Severity> {
+        self.error.severity()
+    }
+
+    fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
+        self.error.help()
+    }
+
+    fn url<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
+        self.error.url()
+    }
+
+    fn labels<'a>(&'a self) -> Option<Box<dyn Iterator<Item = LabeledSpan> + 'a>> {
+        self.error.labels()
+    }
+
+    fn source_code(&self) -> Option<&dyn miette::SourceCode> {
+        Some(&self.source_code)
+    }
+
+    fn related<'a>(&'a self) -> Option<Box<dyn Iterator<Item = &'a dyn Diagnostic> + 'a>> {
+        self.error.related()
+    }
+}
+
+impl<C: SourceCode> Diagnostic for WithSourceCode<Report, C> {
+    fn code<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
+        self.error.code()
+    }
+
+    fn severity(&self) -> Option<miette::Severity> {
+        self.error.severity()
+    }
+
+    fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
+        self.error.help()
+    }
+
+    fn url<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
+        self.error.url()
+    }
+
+    fn labels<'a>(&'a self) -> Option<Box<dyn Iterator<Item = LabeledSpan> + 'a>> {
+        self.error.labels()
+    }
+
+    fn source_code(&self) -> Option<&dyn miette::SourceCode> {
+        Some(&self.source_code)
+    }
+
+    fn related<'a>(&'a self) -> Option<Box<dyn Iterator<Item = &'a dyn Diagnostic> + 'a>> {
+        self.error.related()
+    }
+}
+
+impl<E: Debug, C> Debug for WithSourceCode<E, C> {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        Debug::fmt(&self.error, f)
+    }
+}
+
+impl<E: Display, C> Display for WithSourceCode<E, C> {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        Display::fmt(&self.error, f)
+    }
+}
+
+impl<E: StdError, C> StdError for WithSourceCode<E, C> {
+    fn source(&self) -> Option<&(dyn StdError + 'static)> {
+        self.error.source()
+    }
+}
+
+impl<C> StdError for WithSourceCode<Report, C> {
+    fn source(&self) -> Option<&(dyn StdError + 'static)> {
+        self.error.source()
+    }
+}
diff --git a/src/handlers/graphical.rs b/src/handlers/graphical.rs
index 98d5e54..ea293e5 100644
--- a/src/handlers/graphical.rs
+++ b/src/handlers/graphical.rs
@@ -110,9 +110,10 @@ impl GraphicalReportHandler {
         self.render_header(f, diagnostic)?;
         writeln!(f)?;
         self.render_causes(f, diagnostic)?;
-        self.render_snippets(f, diagnostic)?;
+        let src = diagnostic.source_code();
+        self.render_snippets(f, diagnostic, src)?;
         self.render_footer(f, diagnostic)?;
-        self.render_related(f, diagnostic)?;
+        self.render_related(f, diagnostic, src)?;
         if let Some(footer) = &self.footer {
             writeln!(f)?;
             let width = self.termwidth.saturating_sub(4);
@@ -226,6 +227,7 @@ impl GraphicalReportHandler {
         &self,
         f: &mut impl fmt::Write,
         diagnostic: &(dyn Diagnostic),
+        parent_src: Option<&dyn SourceCode>,
     ) -> fmt::Result {
         if let Some(related) = diagnostic.related() {
             writeln!(f)?;
@@ -234,9 +236,10 @@ impl GraphicalReportHandler {
                 self.render_header(f, rel)?;
                 writeln!(f)?;
                 self.render_causes(f, rel)?;
-                self.render_snippets(f, rel)?;
+                let src = rel.source_code().or(parent_src);
+                self.render_snippets(f, rel, src)?;
                 self.render_footer(f, rel)?;
-                self.render_related(f, rel)?;
+                self.render_related(f, rel, src)?;
             }
         }
         Ok(())
@@ -246,8 +249,9 @@ impl GraphicalReportHandler {
         &self,
         f: &mut impl fmt::Write,
         diagnostic: &(dyn Diagnostic),
+        opt_source: Option<&dyn SourceCode>,
     ) -> fmt::Result {
-        if let Some(source) = diagnostic.source_code() {
+        if let Some(source) = opt_source {
             if let Some(labels) = diagnostic.labels() {
                 let mut labels = labels.collect::<Vec<_>>();
                 labels.sort_unstable_by_key(|l| l.inner().offset());
diff --git a/src/handlers/narratable.rs b/src/handlers/narratable.rs
index 31e75cf..c1dd601 100644
--- a/src/handlers/narratable.rs
+++ b/src/handlers/narratable.rs
@@ -58,9 +58,10 @@ impl NarratableReportHandler {
     ) -> fmt::Result {
         self.render_header(f, diagnostic)?;
         self.render_causes(f, diagnostic)?;
-        self.render_snippets(f, diagnostic)?;
+        let src = diagnostic.source_code();
+        self.render_snippets(f, diagnostic, src)?;
         self.render_footer(f, diagnostic)?;
-        self.render_related(f, diagnostic)?;
+        self.render_related(f, diagnostic, src)?;
         if let Some(footer) = &self.footer {
             writeln!(f, "{}", footer)?;
         }
@@ -105,6 +106,7 @@ impl NarratableReportHandler {
         &self,
         f: &mut impl fmt::Write,
         diagnostic: &(dyn Diagnostic),
+        parent_src: Option<&dyn SourceCode>,
     ) -> fmt::Result {
         if let Some(related) = diagnostic.related() {
             writeln!(f)?;
@@ -113,9 +115,10 @@ impl NarratableReportHandler {
                 self.render_header(f, rel)?;
                 writeln!(f)?;
                 self.render_causes(f, rel)?;
-                self.render_snippets(f, rel)?;
+                let src = rel.source_code().or(parent_src);
+                self.render_snippets(f, rel, src)?;
                 self.render_footer(f, rel)?;
-                self.render_related(f, rel)?;
+                self.render_related(f, rel, src)?;
             }
         }
         Ok(())
@@ -125,8 +128,9 @@ impl NarratableReportHandler {
         &self,
         f: &mut impl fmt::Write,
         diagnostic: &(dyn Diagnostic),
+        source_code: Option<&dyn SourceCode>,
     ) -> fmt::Result {
-        if let Some(source) = diagnostic.source_code() {
+        if let Some(source) = source_code {
             if let Some(labels) = diagnostic.labels() {
                 let mut labels = labels.collect::<Vec<_>>();
                 labels.sort_unstable_by_key(|l| l.inner().offset());


Test patch:
diff --git a/tests/graphical.rs b/tests/graphical.rs
index 4f81ef8..1c5a6ba 100644
--- a/tests/graphical.rs
+++ b/tests/graphical.rs
@@ -186,6 +186,41 @@ fn single_line_highlight() -> Result<(), MietteError> {
     Ok(())
 }
 
+#[test]
+fn external_source() -> Result<(), MietteError> {
+    #[derive(Debug, Diagnostic, Error)]
+    #[error("oops!")]
+    #[diagnostic(code(oops::my::bad), help("try doing it better next time?"))]
+    struct MyBad {
+        #[label("this bit here")]
+        highlight: SourceSpan,
+    }
+
+    let src = "source\n  text\n    here".to_string();
+    let err = Report::from(MyBad {
+        highlight: (9, 4).into(),
+    })
+    .with_source_code(NamedSource::new("bad_file.rs", src));
+    let out = fmt_report(err);
+    println!("Error: {}", out);
+    let expected = r#"oops::my::bad
+
+  × oops!
+   ╭─[bad_file.rs:1:1]
+ 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_highlight_offset_zero() -> Result<(), MietteError> {
     #[derive(Debug, Diagnostic, Error)]
@@ -785,3 +820,63 @@ Error: oops::my::bad
     assert_eq!(expected, out);
     Ok(())
 }
+
+#[test]
+fn related_source_code_propagation() -> 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,
+        #[related]
+        related: Vec<InnerError>,
+    }
+
+    #[derive(Debug, Diagnostic, Error)]
+    #[error("oops!")]
+    #[diagnostic(code(oops::my::bad))]
+    struct InnerError {
+        #[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.clone()),
+        highlight: (9, 4).into(),
+        related: vec![InnerError {
+            highlight: (0, 6).into(),
+        }],
+    };
+    let out = fmt_report(err.into());
+    println!("Error: {}", out);
+    let expected = r#"oops::my::bad
+
+  × oops!
+   ╭─[bad_file.rs:1:1]
+ 1 │ source
+ 2 │   text
+   ·   ──┬─
+   ·     ╰── this bit here
+ 3 │     here
+   ╰────
+  help: try doing it better next time?
+
+Error: oops::my::bad
+
+  × oops!
+   ╭─[bad_file.rs:1:1]
+ 1 │ source
+   · ───┬──
+   ·    ╰── this bit here
+ 2 │   text
+   ╰────
+"#
+    .trim_start()
+    .to_string();
+    assert_eq!(expected, out);
+    Ok(())
+}
diff --git a/tests/narrated.rs b/tests/narrated.rs
index 5d2eff1..93d9695 100644
--- a/tests/narrated.rs
+++ b/tests/narrated.rs
@@ -605,3 +605,62 @@ diagnostic code: oops::my::bad
     assert_eq!(expected, out);
     Ok(())
 }
+
+#[test]
+fn related_source_code_propagation() -> 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,
+        #[related]
+        related: Vec<InnerError>,
+    }
+
+    #[derive(Debug, Diagnostic, Error)]
+    #[error("oops!")]
+    #[diagnostic(code(oops::my::bad))]
+    struct InnerError {
+        #[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.clone()),
+        highlight: (9, 4).into(),
+        related: vec![InnerError {
+            highlight: (0, 6).into(),
+        }],
+    };
+    let out = fmt_report(err.into());
+    println!("Error: {}", out);
+    let expected = r#"oops!
+    Diagnostic severity: error
+Begin snippet for bad_file.rs starting at line 1, column 1
+
+snippet line 1: source
+snippet line 2:   text
+    label at line 2, columns 3 to 6: this bit here
+snippet line 3:     here
+diagnostic help: try doing it better next time?
+diagnostic code: oops::my::bad
+
+Error: oops!
+    Diagnostic severity: error
+
+Begin snippet for bad_file.rs starting at line 1, column 1
+
+snippet line 1: source
+    label at line 1, columns 1 to 6: this bit here
+snippet line 2:   text
+diagnostic code: oops::my::bad
+"#
+    .trim_start()
+    .to_string();
+    assert_eq!(expected, out);
+    Ok(())
+}