Repository: zkat/miette
Pull request #401 (2024-08-16 05:23:32)

Inherit source code to causes as well as related errors
At the moment outer source code is supplied to inner related errors but not to causes. This PR changes that.

This allows late-supplied source code to be supplied using concrete types. Comparisons below…

## Concrete types

Firstly, if we wrap our inner error with some well-typed outer wrapper:

```rust
#[derive(thiserror::Error, Debug, miette::Diagnostic)]
#[error("inner-error")]
struct InnerError {
    #[label("inner-label")]
    label: (usize, usize),
}

#[derive(thiserror::Error, Debug, miette::Diagnostic)]
#[error("outer-error")]
struct OuterError {
    #[source_code]
    source_code: String,

    #[diagnostic_source]
    inner: InnerError,
}

fn main() -> miette::Result<(), miette::Report> {
    Err(OuterError {
        source_code: "source code".to_string(),
        inner: InnerError { label: (7, 4) },
    })?
}
```

At the moment we don’t get any source information rendered.

Output:

```
Error: 
  × outer-error
  ╰─▶   × inner-error
```

## Wrapped in Report

On the other hand, if we wrap using `miette::Report`, which loses the underlying type information, we get the source code:

```rs
#[derive(thiserror::Error, Debug, miette::Diagnostic)]
#[error("inner-error")]
struct InnerError {
    #[label("inner-label")]
    label: (usize, usize),
}

fn main() -> miette::Result<(), miette::Report> {
    Err(miette::Report::new(InnerError { label: (7, 4) }).with_source_code("source code"))?
}
```

Output:
```
Error: 
  × inner-error
   ╭────
 1 │ source code
   ·        ──┬─
   ·          ╰── inner-label
   ╰────
```

## Using related

Currently we can achieve something using `#[related]` but that is semantically-inappropriate for some use cases:

```rs
#[derive(thiserror::Error, Debug, miette::Diagnostic)]
#[error("inner-error")]
struct InnerError {
    #[label("inner-label")]
    label: (usize, usize),
}

#[derive(thiserror::Error, Debug, miette::Diagnostic)]
#[error("outer-error")]
struct OuterError {
    #[source_code]
    source_code: String,

    #[related]
    inner: Vec<InnerError>,
}

fn main() -> miette::Result<(), miette::Report> {
    Err(OuterError {
        source_code: "source code".to_string(),
        inner: vec![InnerError { label: (7, 4) }],
    })?
}
```

The rendering is also different since they're rendered as adjacent errors rather than causes:

```
Error: 
  × outer-error

Error:
  × inner-error
   ╭────
 1 │ source code
   ·        ──┬─
   ·          ╰── inner-label
   ╰────
```

## With proposed change

After this PR, the output for the first example (exact same code) should be similar to the last example, except exhibiting cause-nesting:

```
Error: 
  × outer-error
  ╰─▶   × inner-error
         ╭────
       1 │ source code
         ·        ──┬─
         ·          ╰── inner-label
         ╰────
```

Porges: The other solution I had considered was to pull up labels from causes into the parent and merge all of the labels together. I think this would be nice but maybe enabled with a different attribute?

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b179baa..5641367 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,38 @@
 # `miette` Release Changelog
 
+<a name="7.3.0"></a>
+## 7.3.0 (2024-11-26)
+
+### Features
+
+* **SourceSpan:** add impl From<InclusiveRange> (#385) ([73da45b6](https://github.com/zkat/miette/commit/73da45b65c965777a00ba64aa03a247c0e5241ca))
+* **Report:** add `from_err()` method to `Report` (#403) ([93d3bd11](https://github.com/zkat/miette/commit/93d3bd118a072c35aa761f0ec74317166ec08113))
+* **Diagnostic:** Implement `Diagnostic` for `Infallible` (#402) ([f3fb4c1e](https://github.com/zkat/miette/commit/f3fb4c1ecd196ce389cbd71139bb7e3b35474add))
+
+### Performance
+
+* **handlers:** optimize string-buffer reallocations (#387) ([b8dfcda4](https://github.com/zkat/miette/commit/b8dfcda4a8c10a14116ee275250356ac991dc7be))
+
+### Bug Fixes
+
+* **graphical:** fix nested error wrapping (#358) ([3eabbceb](https://github.com/zkat/miette/commit/3eabbcebf113d1d620a6a3f98e8a455414ed3042))
+* **docs:** updated example image (fixes #111) (#270) ([7b42b12c](https://github.com/zkat/miette/commit/7b42b12c5f6316322ce79c59bcb9e99f5d49edb8))
+* **clippy:** Fix clippy lints in docs (#365) ([ea4296da](https://github.com/zkat/miette/commit/ea4296dacec3b0e4762281d9d115c1bd69ecfac3))
+* **docs:** `alt` attribut for `single-line-example.png` (#372) ([b82cc81b](https://github.com/zkat/miette/commit/b82cc81b8ea32a1cf1b4598ed5832bc8e3b0e161))
+* **color:** setting NO_COLOR should not print ansi codes for non-terminals (#381) ([813232ba](https://github.com/zkat/miette/commit/813232ba7957ae09e4fb9d9416d821f4fd9da66d))
+* **clippy:** fix Rust v1.78.0 clippy warnings (#382) ([e1026f75](https://github.com/zkat/miette/commit/e1026f75e0a5d19bbc8e468cb3f5292074543a97))
+* **perf:** mark error constructors cold (#378) ([9bbcf3c6](https://github.com/zkat/miette/commit/9bbcf3c6017fa3455a7db714879816c1cfc511fd))
+* **handlers:** Disable textwrap::smawk feature (#379) ([edfdcb52](https://github.com/zkat/miette/commit/edfdcb525ee30fc54747460ada621f13f0ed1996))
+* **graphical:** Format entire link instead of just name (#389) ([bf5aa374](https://github.com/zkat/miette/commit/bf5aa3742fd664be3c93160b9c28c145b1ed8bc9))
+* **clippy:** fix `clippy::doc_lazy_continuation` lints (#395) ([15beec43](https://github.com/zkat/miette/commit/15beec43303180b811d4c04d1a78775feb9b0905))
+* **graphical:** Handle invalid UTF-8 in source code (#393) ([d6b45585](https://github.com/zkat/miette/commit/d6b4558502e82fa74e030ccb3c8040656590d7eb))
+* **features:** Use `dep:` syntax for dependencies in features. (#394) ([789a04e3](https://github.com/zkat/miette/commit/789a04e30d041179b373b4eb8b340456534a3f0e))
+* **clippy:** Fix `needless_return` lint. (#405) ([5f441d01](https://github.com/zkat/miette/commit/5f441d011560a091fe5d6a6cdb05f09acf622d36))
+
+### Documentation
+
+* **examples:** add serde_json integration example (#407) ([2902a233](https://github.com/zkat/miette/commit/2902a2337c2e36a5d8e0e54b007d6100cca0c9ff))
+
 <a name="7.2.0"></a>
 ## 7.2.0 (2024-03-07)
 
diff --git a/Cargo.toml b/Cargo.toml
index 01b3a71..ee00a05 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "miette"
-version = "7.2.0"
+version = "7.3.0"
 authors = ["Kat Marchán <kzm@zkat.tech>"]
 description = "Fancy diagnostic reporting library and protocol for us mere mortals who aren't compiler hackers."
 categories = ["rust-patterns"]
@@ -14,7 +14,7 @@ exclude = ["images/", "tests/", "miette-derive/"]
 
 [dependencies]
 thiserror = "1.0.56"
-miette-derive = { path = "miette-derive", version = "=7.2.0", optional = true }
+miette-derive = { path = "miette-derive", version = "=7.3.0", optional = true }
 unicode-width = "0.1.11"
 cfg-if = "1.0.0"
 
@@ -24,7 +24,7 @@ supports-hyperlinks = { version = "3.0.0", optional = true }
 supports-color = { version = "3.0.0", optional = true }
 supports-unicode = { version = "3.0.0", optional = true }
 backtrace = { version = "0.3.69", optional = true }
-terminal_size = { version = "0.3.0", optional = true }
+terminal_size = { version = "0.4.0", optional = true }
 backtrace-ext = { version = "0.2.1", optional = true }
 serde = { version = "1.0.196", features = ["derive"], optional = true }
 syntect = { version = "5.1.0", optional = true }
@@ -41,32 +41,37 @@ syn = { version = "2.0.48", features = ["full"] }
 regex = "1.10"
 lazy_static = "1.4"
 
+serde = { version = "1.0.196", features = ["derive"] }
 serde_json = "1.0.113"
 strip-ansi-escapes = "0.2.0"
 
 [features]
 default = ["derive"]
-derive = ["miette-derive"]
+derive = ["dep:miette-derive"]
 no-format-args-capture = []
 fancy-base = [
-    "owo-colors",
-    "textwrap",
+    "dep:owo-colors",
+    "dep:textwrap",
 ]
 fancy-no-syscall = [
     "fancy-base",
 ]
 fancy-no-backtrace = [
     "fancy-base",
-    "terminal_size",
-    "supports-hyperlinks",
-    "supports-color",
-    "supports-unicode",
+    "dep:terminal_size",
+    "dep:supports-hyperlinks",
+    "dep:supports-color",
+    "dep:supports-unicode",
 ]
-fancy = ["fancy-no-backtrace", "backtrace", "backtrace-ext"]
-syntect-highlighter = ["fancy-no-backtrace", "syntect"]
+fancy = ["fancy-no-backtrace", "dep:backtrace", "dep:backtrace-ext"]
+syntect-highlighter = ["fancy-no-backtrace", "dep:syntect"]
 
 [workspace]
 members = ["miette-derive"]
 
 [package.metadata.docs.rs]
 all-features = true
+
+[[example]]
+name = "serde_json"
+required-features = ["fancy"]
diff --git a/README.md b/README.md
index 33022f8..646047c 100644
--- a/README.md
+++ b/README.md
@@ -673,12 +673,12 @@ automatically use the [`syntect`] crate to highlight the `#[source_code]`
 field of your [`Diagnostic`].
 
 Syntax detection with [`syntect`] is handled by checking 2 methods on the [`SpanContents`] trait, in order:
-* [language()](SpanContents::language) - Provides the name of the language
+* [`language()`](SpanContents::language) - Provides the name of the language
   as a string. For example `"Rust"` will indicate Rust syntax highlighting.
   You can set the language of the [`SpanContents`] produced by a
   [`NamedSource`] via the [`with_language`](NamedSource::with_language)
   method.
-* [name()](SpanContents::name) - In the absence of an explicitly set
+* [`name()`](SpanContents::name) - In the absence of an explicitly set
   language, the name is assumed to contain a file name or file path.
   The highlighter will check for a file extension at the end of the name and
   try to guess the syntax from that.
diff --git a/examples/serde_json.rs b/examples/serde_json.rs
new file mode 100644
index 0000000..d57a76a
--- /dev/null
+++ b/examples/serde_json.rs
@@ -0,0 +1,48 @@
+//! This example shows how to integrate miette with serde_json
+//! so the decoding source will be annotated with the decoding error,
+//! providing contextual information about the error.
+
+use miette::{IntoDiagnostic, SourceOffset};
+use serde_json::{self, json};
+
+#[derive(Debug, serde::Deserialize)]
+struct Library {
+    #[allow(unused)]
+    name: String,
+}
+
+#[derive(Debug, thiserror::Error, miette::Diagnostic)]
+#[error("malformed json provided")]
+struct SerdeError {
+    cause: serde_json::Error,
+    #[source_code]
+    input: String,
+    #[label("{cause}")]
+    location: SourceOffset,
+}
+
+impl SerdeError {
+    /// Takes the input and the `serde_json::Error` and returns a SerdeError
+    /// that can be rendered nicely with miette.
+    pub fn from_serde_error(input: impl Into<String>, cause: serde_json::Error) -> Self {
+        let input = input.into();
+        let location = SourceOffset::from_location(&input, cause.line(), cause.column());
+        Self {
+            cause,
+            input,
+            location,
+        }
+    }
+}
+
+fn main() -> miette::Result<()> {
+    let input = serde_json::to_string_pretty(&json!({
+        "name": 123
+    }))
+    .into_diagnostic()?;
+
+    let _library: Library =
+        serde_json::from_str(&input).map_err(|cause| SerdeError::from_serde_error(input, cause))?;
+
+    Ok(())
+}
diff --git a/miette-derive/Cargo.toml b/miette-derive/Cargo.toml
index e8082a0..fb05f89 100644
--- a/miette-derive/Cargo.toml
+++ b/miette-derive/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "miette-derive"
-version = "7.2.0"
+version = "7.3.0"
 authors = ["Kat Marchán <kzm@zkat.tech>"]
 edition = "2018"
 license = "Apache-2.0"
diff --git a/src/diagnostic_impls.rs b/src/diagnostic_impls.rs
new file mode 100644
index 0000000..aebe56b
--- /dev/null
+++ b/src/diagnostic_impls.rs
@@ -0,0 +1,55 @@
+/*!
+Default trait implementations for [`Diagnostic`].
+*/
+
+use std::{convert::Infallible, fmt::Display};
+
+use crate::{Diagnostic, LabeledSpan, Severity, SourceCode};
+
+impl Diagnostic for Infallible {
+    fn code<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
+        match *self {}
+    }
+
+    fn severity(&self) -> Option<Severity> {
+        match *self {}
+    }
+
+    fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
+        match *self {}
+    }
+
+    fn url<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
+        match *self {}
+    }
+
+    fn source_code(&self) -> Option<&dyn SourceCode> {
+        match *self {}
+    }
+
+    fn labels(&self) -> Option<Box<dyn Iterator<Item = LabeledSpan> + '_>> {
+        match *self {}
+    }
+
+    fn related<'a>(&'a self) -> Option<Box<dyn Iterator<Item = &'a dyn Diagnostic> + 'a>> {
+        match *self {}
+    }
+
+    fn diagnostic_source(&self) -> Option<&dyn Diagnostic> {
+        match *self {}
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    use crate::Report;
+
+    /// Test that [`Infallible`] implements [`Diagnostic`] by seeing if a function that's generic over `Diagnostic`
+    /// will accept `Infallible` as a type parameter.
+    #[test]
+    fn infallible() {
+        let _ = Report::new::<Infallible>;
+    }
+}
diff --git a/src/eyreish/error.rs b/src/eyreish/error.rs
index 94e4128..cbb4aee 100644
--- a/src/eyreish/error.rs
+++ b/src/eyreish/error.rs
@@ -425,6 +425,14 @@ impl Report {
         }
         .into()
     }
+
+    /// Construct a [`Report`] directly from an error-like type
+    pub fn from_err<E>(err: E) -> Self
+    where
+        E: std::error::Error + Send + Sync + 'static,
+    {
+        super::DiagnosticError(Box::new(err)).into()
+    }
 }
 
 impl<E> From<E> for Report
diff --git a/src/eyreish/into_diagnostic.rs b/src/eyreish/into_diagnostic.rs
index 6480013..f862ac6 100644
--- a/src/eyreish/into_diagnostic.rs
+++ b/src/eyreish/into_diagnostic.rs
@@ -6,7 +6,7 @@ use crate::{Diagnostic, Report};
 /// Errors. This is intended to be paired with [`IntoDiagnostic`].
 #[derive(Debug, Error)]
 #[error(transparent)]
-struct DiagnosticError(Box<dyn std::error::Error + Send + Sync + 'static>);
+pub(crate) struct DiagnosticError(pub(crate) Box<dyn std::error::Error + Send + Sync + 'static>);
 impl Diagnostic for DiagnosticError {}
 
 /**
diff --git a/src/handler.rs b/src/handler.rs
index efbee86..10d8f65 100644
--- a/src/handler.rs
+++ b/src/handler.rs
@@ -89,7 +89,7 @@ impl MietteHandlerOpts {
     /// Syntax highlighting is disabled by default unless the
     /// `syntect-highlighter` feature is enabled. Call this method
     /// to override the default and use a custom highlighter
-    /// implmentation instead.
+    /// implementation instead.
     ///
     /// Use
     /// [`without_syntax_highlighting()`](MietteHandlerOpts::without_syntax_highlighting())
diff --git a/src/handlers/graphical.rs b/src/handlers/graphical.rs
index 5991890..9e0948c 100644
--- a/src/handlers/graphical.rs
+++ b/src/handlers/graphical.rs
@@ -178,7 +178,8 @@ impl GraphicalReportHandler {
     }
 
     /// Enable syntax highlighting for source code snippets, using the given
-    /// [`Highlighter`]. See the [crate::highlighters] crate for more details.
+    /// [`Highlighter`]. See the [highlighters](crate::highlighters) crate
+    /// for more details.
     pub fn with_syntax_highlighting(
         mut self,
         highlighter: impl Highlighter + Send + Sync + 'static,
@@ -217,9 +218,18 @@ impl GraphicalReportHandler {
         f: &mut impl fmt::Write,
         diagnostic: &(dyn Diagnostic),
     ) -> fmt::Result {
+        self.render_report_inner(f, diagnostic, diagnostic.source_code())
+    }
+
+    fn render_report_inner(
+        &self,
+        f: &mut impl fmt::Write,
+        diagnostic: &(dyn Diagnostic),
+        parent_src: Option<&dyn SourceCode>,
+    ) -> fmt::Result {
+        let src = diagnostic.source_code().or(parent_src);
         self.render_header(f, diagnostic)?;
-        self.render_causes(f, diagnostic)?;
-        let src = diagnostic.source_code();
+        self.render_causes(f, diagnostic, src)?;
         self.render_snippets(f, diagnostic, src)?;
         self.render_footer(f, diagnostic)?;
         self.render_related(f, diagnostic, src)?;
@@ -277,7 +287,14 @@ impl GraphicalReportHandler {
         Ok(())
     }
 
-    fn render_causes(&self, f: &mut impl fmt::Write, diagnostic: &(dyn Diagnostic)) -> fmt::Result {
+    fn render_causes(
+        &self,
+        f: &mut impl fmt::Write,
+        diagnostic: &(dyn Diagnostic),
+        parent_src: Option<&dyn SourceCode>,
+    ) -> fmt::Result {
+        let src = diagnostic.source_code().or(parent_src);
+
         let (severity_style, severity_icon) = match diagnostic.severity() {
             Some(Severity::Error) | None => (self.theme.styles.error, &self.theme.characters.error),
             Some(Severity::Warning) => (self.theme.styles.warning, &self.theme.characters.warning),
@@ -355,7 +372,7 @@ impl GraphicalReportHandler {
                         inner_renderer.with_cause_chain = false;
                         // Since everything from here on is indented, shrink the virtual terminal
                         inner_renderer.termwidth -= rest_indent.width();
-                        inner_renderer.render_report(&mut inner, diag)?;
+                        inner_renderer.render_report_inner(&mut inner, diag, src)?;
 
                         // If there was no header, remove the leading newline
                         let inner = inner.trim_start_matches('\n');
@@ -409,8 +426,8 @@ impl GraphicalReportHandler {
                     Some(Severity::Advice) => write!(f, "Advice: ")?,
                 };
                 inner_renderer.render_header(f, rel)?;
-                inner_renderer.render_causes(f, rel)?;
                 let src = rel.source_code().or(parent_src);
+                inner_renderer.render_causes(f, rel, src)?;
                 inner_renderer.render_snippets(f, rel, src)?;
                 inner_renderer.render_footer(f, rel)?;
                 inner_renderer.render_related(f, rel, src)?;
@@ -799,7 +816,7 @@ impl GraphicalReportHandler {
             return Ok(());
         }
 
-        // keeps track of how many colums wide the gutter is
+        // keeps track of how many columns wide the gutter is
         // important for ansi since simply measuring the size of the final string
         // gives the wrong result when the string contains ansi codes.
         let mut gutter_cols = 0;
diff --git a/src/highlighters/mod.rs b/src/highlighters/mod.rs
index d605c1c..0af1aa2 100644
--- a/src/highlighters/mod.rs
+++ b/src/highlighters/mod.rs
@@ -26,16 +26,16 @@ mod syntect;
 
 /// A syntax highlighter for highlighting miette [`SourceCode`](crate::SourceCode) snippets.
 pub trait Highlighter {
-    ///  Creates a new [HighlighterState] to begin parsing and highlighting
-    /// a [SpanContents].
+    ///  Creates a new [`HighlighterState`] to begin parsing and highlighting
+    /// a [`SpanContents`].
     ///
-    /// The [GraphicalReportHandler](crate::GraphicalReportHandler) will call
-    /// this method at the start of rendering a [SpanContents].
+    /// The [`GraphicalReportHandler`](crate::GraphicalReportHandler) will call
+    /// this method at the start of rendering a [`SpanContents`].
     ///
-    /// The [SpanContents] is provided as input only so that the [Highlighter]
+    /// The [`SpanContents`] is provided as input only so that the [`Highlighter`]
     /// can detect language syntax and make other initialization decisions prior
     /// to highlighting, but it is not intended that the Highlighter begin
-    /// highlighting at this point. The returned [HighlighterState] is
+    /// highlighting at this point. The returned [`HighlighterState`] is
     /// responsible for the actual rendering.
     fn start_highlighter_state<'h>(
         &'h self,
@@ -46,12 +46,12 @@ pub trait Highlighter {
 /// A stateful highlighter that incrementally highlights lines of a particular
 /// source code.
 ///
-/// The [GraphicalReportHandler](crate::GraphicalReportHandler)
+/// The [`GraphicalReportHandler`](crate::GraphicalReportHandler)
 /// will create a highlighter state by calling
-/// [start_highlighter_state](Highlighter::start_highlighter_state) at the
+/// [`start_highlighter_state`](Highlighter::start_highlighter_state) at the
 /// start of rendering, then it will iteratively call
-/// [highlight_line](HighlighterState::highlight_line) to render individual
-/// highlighted lines. This allows [Highlighter] implementations to maintain
+/// [`highlight_line`](HighlighterState::highlight_line) to render individual
+/// highlighted lines. This allows [`Highlighter`] implementations to maintain
 /// mutable parsing and highlighting state.
 pub trait HighlighterState {
     /// Highlight an individual line from the source code by returning a vector of [Styled]
@@ -59,9 +59,9 @@ pub trait HighlighterState {
     fn highlight_line<'s>(&mut self, line: &'s str) -> Vec<Styled<&'s str>>;
 }
 
-/// Arcified trait object for Highlighter. Used internally by [GraphicalReportHandler]
+/// Arcified trait object for Highlighter. Used internally by [`GraphicalReportHandler`]
 ///
-/// Wrapping the trait object in this way allows us to implement Debug and Clone.
+/// Wrapping the trait object in this way allows us to implement `Debug` and `Clone`.
 #[derive(Clone)]
 #[repr(transparent)]
 pub(crate) struct MietteHighlighter(Arc<dyn Highlighter + Send + Sync>);
@@ -92,7 +92,7 @@ impl Default for MietteHighlighter {
     }
     #[cfg(not(feature = "syntect-highlighter"))]
     fn default() -> Self {
-        return MietteHighlighter::nocolor();
+        MietteHighlighter::nocolor()
     }
 }
 
diff --git a/src/highlighters/syntect.rs b/src/highlighters/syntect.rs
index 69ed015..538124c 100644
--- a/src/highlighters/syntect.rs
+++ b/src/highlighters/syntect.rs
@@ -20,7 +20,7 @@ use crate::{
 
 use super::BlankHighlighterState;
 
-/// Highlights miette [SourceCode] with the [syntect](https://docs.rs/syntect/latest/syntect/) highlighting crate.
+/// Highlights miette [`SpanContents`] with the [syntect](https://docs.rs/syntect/latest/syntect/) highlighting crate.
 ///
 /// Currently only 24-bit truecolor output is supported due to syntect themes
 /// representing color as RGBA.
@@ -81,7 +81,7 @@ impl SyntectHighlighter {
         )
     }
 
-    /// Determine syntect SyntaxReference to use for given SourceCode
+    /// Determine syntect [`SyntaxReference`] to use for given [`SpanContents`].
     fn detect_syntax(&self, contents: &dyn SpanContents<'_>) -> Option<&syntect::SyntaxReference> {
         // use language if given
         if let Some(language) = contents.language() {
@@ -105,7 +105,7 @@ impl SyntectHighlighter {
     }
 }
 
-/// Stateful highlighting iterator for [SyntectHighlighter]
+/// Stateful highlighting iterator for [`SyntectHighlighter`].
 #[derive(Debug)]
 pub(crate) struct SyntectHighlighterState<'h> {
     syntax_set: &'h syntect::SyntaxSet,
@@ -133,7 +133,7 @@ impl<'h> HighlighterState for SyntectHighlighterState<'h> {
     }
 }
 
-/// Convert syntect [syntect::Style] into owo_colors [Style] */
+/// Convert syntect [`syntect::Style`] into `owo_colors` [`Style`]
 #[inline]
 fn convert_style(syntect_style: syntect::Style, use_bg_color: bool) -> Style {
     if use_bg_color {
diff --git a/src/lib.rs b/src/lib.rs
index a42f4fa..175c929 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -100,7 +100,7 @@
 //!
 //! `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)]
@@ -127,7 +127,7 @@
 //! 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.
@@ -674,12 +674,12 @@
 //! field of your [`Diagnostic`].
 //!
 //! Syntax detection with [`syntect`] is handled by checking 2 methods on the [`SpanContents`] trait, in order:
-//! * [language()](SpanContents::language) - Provides the name of the language
+//! * [`language()`](SpanContents::language) - Provides the name of the language
 //!   as a string. For example `"Rust"` will indicate Rust syntax highlighting.
 //!   You can set the language of the [`SpanContents`] produced by a
 //!   [`NamedSource`] via the [`with_language`](NamedSource::with_language)
 //!   method.
-//! * [name()](SpanContents::name) - In the absence of an explicitly set
+//! * [`name()`](SpanContents::name) - In the absence of an explicitly set
 //!   language, the name is assumed to contain a file name or file path.
 //!   The highlighter will check for a file extension at the end of the name and
 //!   try to guess the syntax from that.
@@ -788,6 +788,7 @@ pub use protocol::*;
 
 mod chain;
 mod diagnostic_chain;
+mod diagnostic_impls;
 mod error;
 mod eyreish;
 #[cfg(feature = "fancy-base")]
diff --git a/src/protocol.rs b/src/protocol.rs
index d3ee9ca..7f09d4d 100644
--- a/src/protocol.rs
+++ b/src/protocol.rs
@@ -523,7 +523,7 @@ impl<'a> MietteSpanContents<'a> {
         }
     }
 
-    /// Sets the [`language`](SourceCode::language) for syntax highlighting.
+    /// Sets the [`language`](SpanContents::language) for syntax highlighting.
     pub fn with_language(mut self, language: impl Into<String>) -> Self {
         self.language = Some(language.into());
         self


Test patch:
diff --git a/tests/test_diagnostic_source_macro.rs b/tests/test_diagnostic_source_macro.rs
index bbc2179..b21e091 100644
--- a/tests/test_diagnostic_source_macro.rs
+++ b/tests/test_diagnostic_source_macro.rs
@@ -299,3 +299,53 @@ fn test_nested_cause_chains_for_related_errors_are_output() {
 
     assert_eq!(expected, out);
 }
+
+#[cfg(feature = "fancy-no-backtrace")]
+#[derive(Debug, miette::Diagnostic, thiserror::Error)]
+#[error("A case1 error happened")]
+enum NestedEnumError {
+    Case1 {
+        #[source_code]
+        code: String,
+        #[diagnostic_source]
+        the_other_err: Case1Inner,
+    },
+}
+
+#[derive(Debug, miette::Diagnostic, thiserror::Error)]
+#[error("I am the inner error")]
+struct Case1Inner {
+    #[label("inner-label")]
+    label: (usize, usize),
+}
+
+#[cfg(feature = "fancy-no-backtrace")]
+#[test]
+fn source_is_inherited_to_causes() {
+    let diag = NestedEnumError::Case1 {
+        code: String::from("this is the parent source"),
+        the_other_err: Case1Inner { label: (8, 3) },
+    };
+    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 case1 error happened
+  ╰─▶   × I am the inner error
+         ╭────
+       1 │ this is the parent source
+         ·         ─┬─
+         ·          ╰── inner-label
+         ╰────
+      
+
+  Yooo, a footer
+"#;
+
+    assert_eq!(expected, out);
+}