Repository: zkat/miette
Pull request #394 (2024-08-06 02:16:57)

Use `dep:` syntax for dependencies in features.
This prevents creating implicit features for them which can be confusing (and duplicates features).

waywardmonkeys: This drastically improves what is shown when using `cargo add`.

Before:

```
❯ cargo init && cargo add miette
    Creating binary (application) package
note: see more `Cargo.toml` keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
    Updating crates.io index
      Adding miette v7.2.0 to dependencies
             Features:
             + derive
             + miette-derive
             - backtrace
             - backtrace-ext
             - fancy
             - fancy-base
             - fancy-no-backtrace
             - fancy-no-syscall
             - no-format-args-capture
             - owo-colors
             - serde
             - supports-color
             - supports-hyperlinks
             - supports-unicode
             - syntect
             - syntect-highlighter
             - terminal_size
             - textwrap
    Updating crates.io index
     Locking 11 packages to latest compatible versions
```

After:

```
❯ cargo init && cargo add miette --path ../miette
    Creating binary (application) package
note: see more `Cargo.toml` keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
      Adding miette (local) to dependencies
             Features:
             + derive
             - fancy
             - fancy-base
             - fancy-no-backtrace
             - fancy-no-syscall
             - no-format-args-capture
             - serde
             - syntect-highlighter
    Updating crates.io index
     Locking 11 packages to latest compatible versions
```


Patch:
diff --git a/Cargo.toml b/Cargo.toml
index 01b3a71..7678079 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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 }
@@ -46,24 +46,24 @@ 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"]
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/handlers/graphical.rs b/src/handlers/graphical.rs
index 87fb354..5991890 100644
--- a/src/handlers/graphical.rs
+++ b/src/handlers/graphical.rs
@@ -1186,7 +1186,7 @@ impl GraphicalReportHandler {
         let context_data = source
             .read_span(context_span, self.context_lines, self.context_lines)
             .map_err(|_| fmt::Error)?;
-        let context = std::str::from_utf8(context_data.data()).expect("Bad utf8 detected");
+        let context = String::from_utf8_lossy(context_data.data());
         let mut line = context_data.line();
         let mut column = context_data.column();
         let mut offset = context_data.span().offset();


Test patch:
diff --git a/tests/graphical.rs b/tests/graphical.rs
index 7221a70..3176812 100644
--- a/tests/graphical.rs
+++ b/tests/graphical.rs
@@ -2349,3 +2349,164 @@ Error: oops::my::inner
     assert_eq!(expected, &out);
     Ok(())
 }
+
+#[test]
+fn after_unicode_width() -> Result<(), MietteError> {
+    #[derive(Debug, Diagnostic, Error)]
+    #[error("pointing")]
+    #[diagnostic(code(pointing_at))]
+    struct E {
+        #[label("something of interest")]
+        src: SourceSpan,
+    }
+
+    let unicode_source = "höööt!";
+
+    // make err pointing at the t
+    let (t_index, _) = unicode_source
+        .bytes()
+        .enumerate()
+        .find(|&(_, x)| x == b't')
+        .unwrap();
+
+    let err = E {
+        src: SourceSpan::from((t_index, 1)),
+    };
+
+    let result = fmt_report(Report::new(err).with_source_code(String::from(unicode_source)));
+
+    let expected = "pointing_at
+
+  × pointing
+   ╭────
+ 1 │ höööt!
+   ·     ┬
+   ·     ╰── something of interest
+   ╰────
+";
+
+    assert_eq!(expected, result);
+
+    Ok(())
+}
+
+#[test]
+fn at_unicode_width() -> Result<(), MietteError> {
+    #[derive(Debug, Diagnostic, Error)]
+    #[error("pointing")]
+    #[diagnostic(code(pointing_at))]
+    struct E {
+        #[label("something of interest")]
+        src: SourceSpan,
+    }
+
+    let unicode_source = "höööt!";
+
+    // make err pointing at an ö
+    let err = E {
+        src: SourceSpan::from((1, "ö".len())),
+    };
+
+    // we want to make sure the pointer is one char wide, not 2
+    assert!(err.src.len() == 2);
+
+    let result = fmt_report(Report::new(err).with_source_code(String::from(unicode_source)));
+
+    let expected = "pointing_at
+
+  × pointing
+   ╭────
+ 1 │ höööt!
+   ·  ┬
+   ·  ╰── something of interest
+   ╰────
+";
+
+    assert_eq!(expected, result);
+
+    Ok(())
+}
+
+#[test]
+fn at_invalid_unicode() -> Result<(), MietteError> {
+    #[derive(Debug, Diagnostic, Error)]
+    #[error("decoding error")]
+    #[diagnostic(code(decode_err))]
+    struct E {
+        #[label("invalid data here")]
+        src: SourceSpan,
+    }
+
+    // 3 bytes here are mapped to 1 char when replaced by replacement character
+    // this tests that the line pointing from the label is the correct length when rendered
+    // - it should be 1 char wide not 3 chars
+    let invalid_source: &[u8] = b"malformed h\xf0\x93\x8aXYZ";
+
+    // invalid utf8 lint only available from 1.72
+    #[allow(unknown_lints, invalid_from_utf8)]
+    let utf8_err = std::str::from_utf8(invalid_source).unwrap_err();
+
+    // make err pointing at the invalid part
+    let err = E {
+        src: SourceSpan::from((utf8_err.valid_up_to(), utf8_err.error_len().unwrap_or(1))),
+    };
+
+    // check that we're testing the thing we think we're testing
+    assert_eq!(err.src.len(), 3);
+
+    let result = fmt_report(Report::new(err).with_source_code(Vec::from(invalid_source)));
+
+    let expected = "decode_err
+
+  × decoding error
+   ╭────
+ 1 │ malformed h�XYZ
+   ·            ┬
+   ·            ╰── invalid data here
+   ╰────
+";
+
+    assert_eq!(expected, result);
+
+    Ok(())
+}
+
+#[test]
+fn after_invalid_unicode() -> Result<(), MietteError> {
+    #[derive(Debug, Diagnostic, Error)]
+    #[error("decoding error")]
+    #[diagnostic(code(decode_err))]
+    struct E {
+        #[label("valid data here")]
+        src: SourceSpan,
+    }
+
+    let invalid_source: &[u8] = b"malformed h\xf0\x93\x8aXYZ";
+
+    let (x_index, _) = invalid_source
+        .iter()
+        .enumerate()
+        .find(|&(_, &x)| x == b'X')
+        .unwrap();
+
+    // make err pointing at the X
+    let err = E {
+        src: SourceSpan::from((x_index, 1)),
+    };
+
+    let result = fmt_report(Report::new(err).with_source_code(invalid_source));
+
+    let expected = "decode_err
+
+  × decoding error
+   ╭────
+ 1 │ malformed h�XYZ
+   ·             ┬
+   ·             ╰── valid data here
+   ╰────
+";
+
+    assert_eq!(expected, result);
+
+    Ok(())
+}