Repository: zkat/miette
Pull request #347 (2024-02-21 20:46:12)

fix(invalid span): skip the snippet when read_span fails
Fixes: https://github.com/zkat/miette/issues/219

When a snippet couldn't be read (typically because the span didn't fit within the source code), it and the rest of the diagnostic were silently dropped, which was confusing to the developer.
Now, in place of the snippet, print an error message with the name of the failed label and the error it triggered, then proceed with the rest of the diagnostic (footer, related, ...)


This is what it looks like:
![image](https://github.com/zkat/miette/assets/1198364/61c5ce14-ba05-43dc-abc3-0ed1a6ed2d0b)


Nahor: The CI failure should be addressed with #348

Patch:
diff --git a/src/handlers/graphical.rs b/src/handlers/graphical.rs
index 3f74227..a4474f9 100644
--- a/src/handlers/graphical.rs
+++ b/src/handlers/graphical.rs
@@ -436,9 +436,25 @@ impl GraphicalReportHandler {
 
         let mut contexts = Vec::with_capacity(labels.len());
         for right in labels.iter().cloned() {
-            let right_conts = source
-                .read_span(right.inner(), self.context_lines, self.context_lines)
-                .map_err(|_| fmt::Error)?;
+            let right_conts =
+                match source.read_span(right.inner(), self.context_lines, self.context_lines) {
+                    Ok(cont) => cont,
+                    Err(err) => {
+                        writeln!(
+                            f,
+                            "  [{} `{}` (offset: {}, length: {}): {:?}]",
+                            "Failed to read contents for label".style(self.theme.styles.error),
+                            right
+                                .label()
+                                .unwrap_or("<none>")
+                                .style(self.theme.styles.link),
+                            right.offset().style(self.theme.styles.link),
+                            right.len().style(self.theme.styles.link),
+                            err.style(self.theme.styles.warning)
+                        )?;
+                        return Ok(());
+                    }
+                };
 
             if contexts.is_empty() {
                 contexts.push((right, right_conts));


Test patch:
diff --git a/tests/graphical.rs b/tests/graphical.rs
index b5fa4d8..4763117 100644
--- a/tests/graphical.rs
+++ b/tests/graphical.rs
@@ -1970,3 +1970,229 @@ fn non_adjacent_highlight() -> Result<(), MietteError> {
     assert_eq!(expected, &out);
     Ok(())
 }
+
+#[test]
+fn invalid_span_bad_offset() -> Result<(), MietteError> {
+    #[derive(Debug, Diagnostic, Error)]
+    #[error("oops!")]
+    #[diagnostic(code(oops::my::bad), help("help info"))]
+    struct MyBad {
+        #[source_code]
+        src: NamedSource<String>,
+        #[label = "1st"]
+        highlight1: SourceSpan,
+    }
+
+    let src = "blabla blibli".to_string();
+    let err = MyBad {
+        src: NamedSource::new("bad_file.rs", src),
+        highlight1: (50, 6).into(),
+    };
+    let out = fmt_report(err.into());
+    println!("Error: {}", out);
+    let expected = "oops::my::bad
+
+  × oops!
+  [Failed to read contents for label `1st` (offset: 50, length: 6): OutOfBounds]
+  help: help info
+";
+    assert_eq!(expected, &out);
+    Ok(())
+}
+
+#[test]
+fn invalid_span_bad_length() -> Result<(), MietteError> {
+    #[derive(Debug, Diagnostic, Error)]
+    #[error("oops!")]
+    #[diagnostic(code(oops::my::bad), help("help info"))]
+    struct MyBad {
+        #[source_code]
+        src: NamedSource<String>,
+        #[label = "1st"]
+        highlight1: SourceSpan,
+    }
+
+    let src = "blabla blibli".to_string();
+    let err = MyBad {
+        src: NamedSource::new("bad_file.rs", src),
+        highlight1: (0, 50).into(),
+    };
+    let out = fmt_report(err.into());
+    println!("Error: {}", out);
+    let expected = "oops::my::bad
+
+  × oops!
+  [Failed to read contents for label `1st` (offset: 0, length: 50): OutOfBounds]
+  help: help info
+";
+    assert_eq!(expected, &out);
+    Ok(())
+}
+
+#[test]
+fn invalid_span_no_label() -> Result<(), MietteError> {
+    #[derive(Debug, Diagnostic, Error)]
+    #[error("oops!")]
+    #[diagnostic(code(oops::my::bad), help("help info"))]
+    struct MyBad {
+        #[source_code]
+        src: NamedSource<String>,
+        #[label]
+        highlight1: SourceSpan,
+    }
+
+    let src = "blabla blibli".to_string();
+    let err = MyBad {
+        src: NamedSource::new("bad_file.rs", src),
+        highlight1: (50, 6).into(),
+    };
+    let out = fmt_report(err.into());
+    println!("Error: {}", out);
+    let expected = "oops::my::bad
+
+  × oops!
+  [Failed to read contents for label `<none>` (offset: 50, length: 6): OutOfBounds]
+  help: help info
+";
+    assert_eq!(expected, &out);
+    Ok(())
+}
+
+#[test]
+fn invalid_span_2nd_label() -> Result<(), MietteError> {
+    #[derive(Debug, Diagnostic, Error)]
+    #[error("oops!")]
+    #[diagnostic(code(oops::my::bad), help("help info"))]
+    struct MyBad {
+        #[source_code]
+        src: NamedSource<String>,
+        #[label("1st")]
+        highlight1: SourceSpan,
+        #[label("2nd")]
+        highlight2: SourceSpan,
+    }
+
+    let src = "blabla blibli".to_string();
+    let err = MyBad {
+        src: NamedSource::new("bad_file.rs", src),
+        highlight1: (0, 6).into(),
+        highlight2: (50, 6).into(),
+    };
+    let out = fmt_report(err.into());
+    println!("Error: {}", out);
+    let expected = "oops::my::bad
+
+  × oops!
+  [Failed to read contents for label `2nd` (offset: 50, length: 6): OutOfBounds]
+  help: help info
+";
+    assert_eq!(expected, &out);
+    Ok(())
+}
+
+#[test]
+fn invalid_span_inner() -> Result<(), MietteError> {
+    #[derive(Debug, Diagnostic, Error)]
+    #[error("oops inside!")]
+    #[diagnostic(code(oops::my::inner), help("help info"))]
+    struct MyInner {
+        #[source_code]
+        src: NamedSource<String>,
+        #[label("inner label")]
+        inner_label: SourceSpan,
+    }
+
+    #[derive(Debug, Diagnostic, Error)]
+    #[error("oops outside!")]
+    #[diagnostic(code(oops::my::outer), help("help info"))]
+    struct MyBad {
+        #[source_code]
+        src: NamedSource<String>,
+        #[label("outer label")]
+        outer_label: SourceSpan,
+        #[source]
+        inner: MyInner,
+    }
+
+    let src_outer = "outer source".to_string();
+    let src_inner = "inner source".to_string();
+    let err = MyBad {
+        src: NamedSource::new("bad_file.rs", src_outer),
+        outer_label: (0, 6).into(),
+        inner: MyInner {
+            src: NamedSource::new("bad_file2.rs", src_inner),
+            inner_label: (60, 6).into(),
+        },
+    };
+    let out = fmt_report(err.into());
+    println!("Error: {}", out);
+    let expected = "oops::my::outer
+
+  × oops outside!
+  ╰─▶ oops inside!
+   ╭─[bad_file.rs:1:1]
+ 1 │ outer source
+   · ───┬──
+   ·    ╰── outer label
+   ╰────
+  help: help info
+";
+    assert_eq!(expected, &out);
+    Ok(())
+}
+
+#[test]
+fn invalid_span_related() -> Result<(), MietteError> {
+    #[derive(Debug, Diagnostic, Error)]
+    #[error("oops inside!")]
+    #[diagnostic(code(oops::my::inner), help("help info"))]
+    struct MyRelated {
+        #[source_code]
+        src: NamedSource<String>,
+        #[label("inner label")]
+        inner_label: SourceSpan,
+    }
+
+    #[derive(Debug, Diagnostic, Error)]
+    #[error("oops outside!")]
+    #[diagnostic(code(oops::my::outer), help("help info"))]
+    struct MyBad {
+        #[source_code]
+        src: NamedSource<String>,
+        #[label("outer label")]
+        outer_label: SourceSpan,
+        #[related]
+        inner: Vec<MyRelated>,
+    }
+
+    let src_outer = "outer source".to_string();
+    let src_inner = "related source".to_string();
+    let err = MyBad {
+        src: NamedSource::new("bad_file.rs", src_outer),
+        outer_label: (0, 6).into(),
+        inner: vec![MyRelated {
+            src: NamedSource::new("bad_file2.rs", src_inner),
+            inner_label: (60, 6).into(),
+        }],
+    };
+    let out = fmt_report(err.into());
+    println!("Error: {}", out);
+    let expected = "oops::my::outer
+
+  × oops outside!
+   ╭─[bad_file.rs:1:1]
+ 1 │ outer source
+   · ───┬──
+   ·    ╰── outer label
+   ╰────
+  help: help info
+
+Error: oops::my::inner
+
+  × oops inside!
+  [Failed to read contents for label `inner label` (offset: 60, length: 6): OutOfBounds]
+  help: help info
+";
+    assert_eq!(expected, &out);
+    Ok(())
+}