mirror of https://github.com/zkat/miette.git
feat(links): added URL linking support and automatic docs.rs link generation
Fixes: https://github.com/zkat/miette/issues/17
This commit is contained in:
parent
3546dcec98
commit
7e76e2dea4
52
README.md
52
README.md
|
|
@ -34,6 +34,7 @@ diagnostic error code: ruget::api::bad_json
|
|||
- [... in libraries](#-in-libraries)
|
||||
- [... in application code](#-in-application-code)
|
||||
- [... in `main()`](#-in-main)
|
||||
- [... diagnostic code URLs](#-diagnostic-code-urls)
|
||||
- [... snippets](#-snippets)
|
||||
- [Acknowledgements](#acknowledgements)
|
||||
- [License](#license)
|
||||
|
|
@ -42,6 +43,7 @@ diagnostic error code: ruget::api::bad_json
|
|||
|
||||
- Generic [Diagnostic] protocol, compatible (and dependent on) `std::error::Error`.
|
||||
- Unique error codes on every [Diagnostic].
|
||||
- Custom links to get more details on error codes.
|
||||
- Super handy derive macro for defining diagnostic metadata.
|
||||
- Lightweight [`anyhow`](https://docs.rs/anyhow)/[`eyre`](https://docs.rs/eyre)-style error wrapper type, [DiagnosticReport],
|
||||
which can be returned from `main`.
|
||||
|
|
@ -54,6 +56,7 @@ The `miette` crate also comes bundles with a default [DiagnosticReportPrinter] w
|
|||
- Screen reader/braille support, gated on [`NO_COLOR`](http://no-color.org/), and other heuristics.
|
||||
- Fully customizable graphical theming (or overriding the printers entirely).
|
||||
- Cause chain printing
|
||||
- Turns diagnostic codes into links in [supported terminals](https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda).
|
||||
|
||||
## Installing
|
||||
|
||||
|
|
@ -219,6 +222,55 @@ fn pretend_this_is_main() -> DiagnosticResult<()> {
|
|||
}
|
||||
```
|
||||
|
||||
### ... 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:
|
||||
|
||||
<img
|
||||
src="https://raw.githubusercontent.com/zkat/miette/main/images/code_linking.png"
|
||||
alt=" Example showing the graphical report printer for miette pretty-printing
|
||||
an error code. The code is underlined and followed by text saying to 'click
|
||||
here'. A hover tooltip shows a full-fledged URL that can be Ctrl+Clicked to
|
||||
open in a browser.
|
||||
\
|
||||
This feature is also available in the narratable printer. It will add a line after printing the error code showing a plain URL that you can visit.
|
||||
">
|
||||
|
||||
To use this, you can add a `url()` sub-param to your `#[diagnostic]` attribute:
|
||||
|
||||
```rust
|
||||
use miette::Diagnostic;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Diagnostic, Debug)]
|
||||
#[diagnostic(
|
||||
code(my_app::my_error),
|
||||
// You can do formatting!
|
||||
url("https://my_website.com/error_codes#{}", self.code())
|
||||
)]
|
||||
struct MyErr;
|
||||
```
|
||||
|
||||
Additionally, if you're developing a library and your error type is exported
|
||||
from your crate's top level, you can use a special `url(docsrs)` option
|
||||
instead of manually constructing the URL. This will automatically create a
|
||||
link to this diagnostic on `docs.rs`, so folks can just go straight to
|
||||
your (very high quality and detailed!) documentation on this diagnostic:
|
||||
|
||||
```rust
|
||||
use miette::Diagnostic;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Diagnostic, Debug)]
|
||||
#[diagnostic(
|
||||
code(my_app::my_error),
|
||||
// Will link users to https://docs.rs/my_crate/0.0.0/my_crate/struct.MyErr.html
|
||||
url(docsrs)
|
||||
)]
|
||||
struct MyErr;
|
||||
```
|
||||
|
||||
### ... snippets
|
||||
|
||||
Along with its general error handling and reporting features, `miette` also
|
||||
|
|
|
|||
Binary file not shown.
|
After Width: | Height: | Size: 365 KiB |
|
|
@ -7,6 +7,7 @@ use crate::diagnostic_arg::DiagnosticArg;
|
|||
use crate::help::Help;
|
||||
use crate::severity::Severity;
|
||||
use crate::snippets::Snippets;
|
||||
use crate::url::Url;
|
||||
|
||||
pub enum Diagnostic {
|
||||
Struct {
|
||||
|
|
@ -17,6 +18,7 @@ pub enum Diagnostic {
|
|||
severity: Option<Severity>,
|
||||
help: Option<Help>,
|
||||
snippets: Option<Snippets>,
|
||||
url: Option<Url>,
|
||||
},
|
||||
Enum {
|
||||
ident: syn::Ident,
|
||||
|
|
@ -32,6 +34,7 @@ pub struct DiagnosticVariant {
|
|||
pub severity: Option<Severity>,
|
||||
pub help: Option<Help>,
|
||||
pub snippets: Option<Snippets>,
|
||||
pub url: Option<Url>,
|
||||
}
|
||||
|
||||
impl Diagnostic {
|
||||
|
|
@ -45,12 +48,16 @@ impl Diagnostic {
|
|||
let mut code = None;
|
||||
let mut severity = None;
|
||||
let mut help = None;
|
||||
let mut url = None;
|
||||
for arg in args {
|
||||
match arg {
|
||||
DiagnosticArg::Code(new_code) => {
|
||||
// TODO: error on multiple?
|
||||
code = Some(new_code);
|
||||
}
|
||||
DiagnosticArg::Url(u) => {
|
||||
url = Some(u);
|
||||
}
|
||||
DiagnosticArg::Severity(sev) => {
|
||||
severity = Some(sev);
|
||||
}
|
||||
|
|
@ -69,6 +76,7 @@ impl Diagnostic {
|
|||
help,
|
||||
severity,
|
||||
snippets,
|
||||
url,
|
||||
}
|
||||
} else {
|
||||
// Also handle when there's multiple `#[diagnostic]` attrs?
|
||||
|
|
@ -88,6 +96,7 @@ impl Diagnostic {
|
|||
let mut code = None;
|
||||
let mut severity = None;
|
||||
let mut help = None;
|
||||
let mut url = None;
|
||||
for arg in args {
|
||||
match arg {
|
||||
DiagnosticArg::Code(new_code) => {
|
||||
|
|
@ -100,6 +109,9 @@ impl Diagnostic {
|
|||
DiagnosticArg::Help(hl) => {
|
||||
help = Some(hl);
|
||||
}
|
||||
DiagnosticArg::Url(u) => {
|
||||
url = Some(u);
|
||||
}
|
||||
}
|
||||
}
|
||||
let snippets = Snippets::from_fields(&var.fields)?;
|
||||
|
|
@ -113,6 +125,7 @@ impl Diagnostic {
|
|||
help,
|
||||
severity,
|
||||
snippets,
|
||||
url,
|
||||
});
|
||||
} else {
|
||||
// Also handle when there's multiple `#[diagnostic]` attrs?
|
||||
|
|
@ -147,12 +160,14 @@ impl Diagnostic {
|
|||
severity,
|
||||
help,
|
||||
snippets,
|
||||
url,
|
||||
} => {
|
||||
let (impl_generics, ty_generics, where_clause) = &generics.split_for_impl();
|
||||
let code_body = code.gen_struct();
|
||||
let help_body = help.as_ref().and_then(|x| x.gen_struct(fields));
|
||||
let sev_body = severity.as_ref().and_then(|x| x.gen_struct());
|
||||
let snip_body = snippets.as_ref().and_then(|x| x.gen_struct());
|
||||
let url_body = url.as_ref().and_then(|x| x.gen_struct(ident, fields));
|
||||
|
||||
quote! {
|
||||
impl #impl_generics miette::Diagnostic for #ident #ty_generics #where_clause {
|
||||
|
|
@ -160,6 +175,7 @@ impl Diagnostic {
|
|||
#help_body
|
||||
#sev_body
|
||||
#snip_body
|
||||
#url_body
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -173,13 +189,14 @@ impl Diagnostic {
|
|||
let help_body = Help::gen_enum(variants);
|
||||
let sev_body = Severity::gen_enum(variants);
|
||||
let snip_body = Snippets::gen_enum(variants);
|
||||
|
||||
let url_body = Url::gen_enum(ident, variants);
|
||||
quote! {
|
||||
impl #impl_generics miette::Diagnostic for #ident #ty_generics #where_clause {
|
||||
#code_body
|
||||
#help_body
|
||||
#sev_body
|
||||
#snip_body
|
||||
#url_body
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,11 +3,13 @@ use syn::parse::{Parse, ParseStream};
|
|||
use crate::code::Code;
|
||||
use crate::help::Help;
|
||||
use crate::severity::Severity;
|
||||
use crate::url::Url;
|
||||
|
||||
pub enum DiagnosticArg {
|
||||
Code(Code),
|
||||
Severity(Severity),
|
||||
Help(Help),
|
||||
Url(Url),
|
||||
}
|
||||
|
||||
impl Parse for DiagnosticArg {
|
||||
|
|
@ -19,6 +21,8 @@ impl Parse for DiagnosticArg {
|
|||
Ok(DiagnosticArg::Severity(input.parse()?))
|
||||
} else if ident == "help" {
|
||||
Ok(DiagnosticArg::Help(input.parse()?))
|
||||
} else if ident == "url" {
|
||||
Ok(DiagnosticArg::Url(input.parse()?))
|
||||
} else {
|
||||
Err(syn::Error::new(
|
||||
ident.span(),
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ mod fmt;
|
|||
mod help;
|
||||
mod severity;
|
||||
mod snippets;
|
||||
mod url;
|
||||
mod utils;
|
||||
|
||||
#[proc_macro_derive(Diagnostic, attributes(diagnostic, snippet, highlight))]
|
||||
|
|
|
|||
|
|
@ -0,0 +1,204 @@
|
|||
use std::collections::HashSet;
|
||||
|
||||
use proc_macro2::TokenStream;
|
||||
use quote::{format_ident, quote};
|
||||
use syn::{
|
||||
parenthesized,
|
||||
parse::{Parse, ParseStream},
|
||||
spanned::Spanned,
|
||||
Fields, Token,
|
||||
};
|
||||
|
||||
use crate::diagnostic::DiagnosticVariant;
|
||||
use crate::fmt::{self, Display};
|
||||
|
||||
pub enum Url {
|
||||
Display(Display),
|
||||
DocsRs,
|
||||
}
|
||||
|
||||
impl Parse for Url {
|
||||
fn parse(input: ParseStream) -> syn::Result<Self> {
|
||||
let ident = input.parse::<syn::Ident>()?;
|
||||
if ident == "url" {
|
||||
let la = input.lookahead1();
|
||||
if la.peek(syn::token::Paren) {
|
||||
let content;
|
||||
parenthesized!(content in input);
|
||||
if content.peek(syn::LitStr) {
|
||||
let fmt = content.parse()?;
|
||||
let args = if content.is_empty() {
|
||||
TokenStream::new()
|
||||
} else {
|
||||
content.parse::<Token![,]>()?;
|
||||
fmt::parse_token_expr(&content, false)?
|
||||
};
|
||||
let display = Display {
|
||||
fmt,
|
||||
args,
|
||||
has_bonus_display: false,
|
||||
};
|
||||
Ok(Url::Display(display))
|
||||
} else {
|
||||
let option = content.parse::<syn::Ident>()?;
|
||||
if option == "docsrs" {
|
||||
Ok(Url::DocsRs)
|
||||
} else {
|
||||
Err(syn::Error::new(option.span(), "Invalid argument to url() sub-attribute. It must be either a string or a plain `docsrs` identifier"))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
input.parse::<Token![=]>()?;
|
||||
Ok(Url::Display(Display {
|
||||
fmt: input.parse()?,
|
||||
args: TokenStream::new(),
|
||||
has_bonus_display: false,
|
||||
}))
|
||||
}
|
||||
} else {
|
||||
Err(syn::Error::new(ident.span(), "not a url"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Url {
|
||||
pub(crate) fn gen_enum(
|
||||
enum_name: &syn::Ident,
|
||||
variants: &[DiagnosticVariant],
|
||||
) -> Option<TokenStream> {
|
||||
let url_pairs = variants
|
||||
.iter()
|
||||
.filter(|v| v.url.is_some())
|
||||
.map(
|
||||
|DiagnosticVariant {
|
||||
ref ident,
|
||||
ref url,
|
||||
ref fields,
|
||||
..
|
||||
}| {
|
||||
let member_idents = fields.iter().enumerate().map(|(i, field)| {
|
||||
field
|
||||
.ident
|
||||
.as_ref()
|
||||
.cloned()
|
||||
.unwrap_or_else(|| format_ident!("_{}", i))
|
||||
});
|
||||
let members: HashSet<syn::Member> = fields.iter().enumerate().map(|(i, field)| {
|
||||
if let Some(ident) = field.ident.as_ref().cloned() {
|
||||
syn::Member::Named(ident)
|
||||
} else {
|
||||
syn::Member::Unnamed(syn::Index { index: i as u32, span: field.span() })
|
||||
}
|
||||
}).collect();
|
||||
let (fmt, args) = match url.as_ref().expect("MIETTE BUG: we already checked for `Some`") {
|
||||
Url::Display(display) => {
|
||||
let mut display = display.clone();
|
||||
display.expand_shorthand(&members);
|
||||
let Display { fmt, args, .. } = display;
|
||||
(fmt.value(), args)
|
||||
}
|
||||
Url::DocsRs => {
|
||||
let fmt = "https://docs.rs/{crate_name}/{crate_version}/{crate_name}/{item_path}".into();
|
||||
let item_path = format!("enum.{}.html#variant.{}", enum_name, ident);
|
||||
let args = quote! {
|
||||
crate_name=env!("CARGO_PKG_NAME"),
|
||||
crate_version=env!("CARGO_PKG_VERSION"),
|
||||
item_path=#item_path
|
||||
};
|
||||
(fmt, args)
|
||||
}
|
||||
};
|
||||
match fields {
|
||||
syn::Fields::Named(_) => {
|
||||
quote! { Self::#ident{ #(#member_idents),* } => std::option::Option::Some(std::boxed::Box::new(format!(#fmt, #args))), }
|
||||
}
|
||||
syn::Fields::Unnamed(_) => {
|
||||
quote! { Self::#ident( #(#member_idents),* ) => std::option::Option::Some(std::boxed::Box::new(format!(#fmt, #args))), }
|
||||
}
|
||||
syn::Fields::Unit =>
|
||||
quote! { Self::#ident => std::option::Option::Some(std::boxed::Box::new(format!(#fmt, #args))), },
|
||||
}
|
||||
},
|
||||
)
|
||||
.collect::<Vec<_>>();
|
||||
if url_pairs.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(quote! {
|
||||
fn url<'a>(&'a self) -> std::option::Option<std::boxed::Box<dyn std::fmt::Display + 'a>> {
|
||||
#[allow(unused_variables, deprecated)]
|
||||
match self {
|
||||
#(#url_pairs)*
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn gen_struct(
|
||||
&self,
|
||||
struct_name: &syn::Ident,
|
||||
fields: &Fields,
|
||||
) -> Option<TokenStream> {
|
||||
let members: HashSet<syn::Member> = fields
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, field)| {
|
||||
if let Some(ident) = field.ident.as_ref().cloned() {
|
||||
syn::Member::Named(ident)
|
||||
} else {
|
||||
syn::Member::Unnamed(syn::Index {
|
||||
index: i as u32,
|
||||
span: field.span(),
|
||||
})
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
let (fmt, args) = match self {
|
||||
Url::Display(display) => {
|
||||
let mut display = display.clone();
|
||||
display.expand_shorthand(&members);
|
||||
let Display { fmt, args, .. } = display;
|
||||
(fmt.value(), args)
|
||||
}
|
||||
Url::DocsRs => {
|
||||
let fmt =
|
||||
"https://docs.rs/{crate_name}/{crate_version}/{crate_name}/{item_path}".into();
|
||||
let item_path = format!("struct.{}.html", struct_name);
|
||||
let args = quote! {
|
||||
crate_name=env!("CARGO_PKG_NAME"),
|
||||
crate_version=env!("CARGO_PKG_VERSION"),
|
||||
item_path=#item_path
|
||||
};
|
||||
(fmt, args)
|
||||
}
|
||||
};
|
||||
let members = members.iter();
|
||||
let fields_pat = match fields {
|
||||
Fields::Named(_) => quote! {
|
||||
let Self { #(#members),* } = self;
|
||||
},
|
||||
Fields::Unnamed(_) => {
|
||||
let vars = members.map(|member| {
|
||||
if let syn::Member::Unnamed(member) = member {
|
||||
format_ident!("_{}", member)
|
||||
} else {
|
||||
unreachable!()
|
||||
}
|
||||
});
|
||||
quote! {
|
||||
let Self(#(#vars),*) = self;
|
||||
}
|
||||
}
|
||||
Fields::Unit => quote! {},
|
||||
};
|
||||
Some(quote! {
|
||||
fn url<'a>(&'a self) -> std::option::Option<std::boxed::Box<dyn std::fmt::Display + 'a>> {
|
||||
#[allow(unused_variables, deprecated)]
|
||||
#fields_pat
|
||||
std::option::Option::Some(std::boxed::Box::new(format!(#fmt, #args)))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -25,6 +25,7 @@ miette::set_printer(GraphicalReportPrinter::new_themed(GraphicalTheme::unicode_n
|
|||
*/
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct GraphicalReportPrinter {
|
||||
pub(crate) linkify_code: bool,
|
||||
pub(crate) theme: GraphicalTheme,
|
||||
}
|
||||
|
||||
|
|
@ -33,13 +34,23 @@ impl GraphicalReportPrinter {
|
|||
/// [GraphicalTheme]. This will use both unicode characters and colors.
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
linkify_code: true,
|
||||
theme: GraphicalTheme::default(),
|
||||
}
|
||||
}
|
||||
|
||||
///Create a new [GraphicalReportPrinter] with a given [GraphicalTheme].
|
||||
pub fn new_themed(theme: GraphicalTheme) -> Self {
|
||||
Self { theme }
|
||||
Self {
|
||||
linkify_code: true,
|
||||
theme,
|
||||
}
|
||||
}
|
||||
|
||||
/// Disables error code linkification using [Diagnostic::url].
|
||||
pub fn without_code_linking(mut self) -> Self {
|
||||
self.linkify_code = false;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -79,14 +90,16 @@ impl GraphicalReportPrinter {
|
|||
Some(Severity::Warning) => (self.theme.styles.warning, self.theme.characters.warning),
|
||||
Some(Severity::Advice) => (self.theme.styles.advice, self.theme.characters.point_right),
|
||||
};
|
||||
let code = diagnostic.code();
|
||||
writeln!(
|
||||
f,
|
||||
"{}[{}]{}",
|
||||
self.theme.characters.hbar.to_string().repeat(4),
|
||||
code.style(self.theme.styles.code),
|
||||
self.theme.characters.hbar.to_string().repeat(20),
|
||||
)?;
|
||||
write!(f, "{}", self.theme.characters.hbar.to_string().repeat(4))?;
|
||||
if self.linkify_code && diagnostic.url().is_some() {
|
||||
let url = diagnostic.url().unwrap(); // safe
|
||||
let code = format!("{} (click for details)", diagnostic.code());
|
||||
let link = format!("\u{1b}]8;;{}\u{1b}\\{}\u{1b}]8;;\u{1b}\\", url, code);
|
||||
write!(f, "[{}]", link.style(self.theme.styles.code))?;
|
||||
} else {
|
||||
write!(f, "[{}]", diagnostic.code().style(self.theme.styles.code))?;
|
||||
}
|
||||
writeln!(f, "{}", self.theme.characters.hbar.to_string().repeat(20),)?;
|
||||
writeln!(f)?;
|
||||
writeln!(
|
||||
f,
|
||||
|
|
|
|||
|
|
@ -50,9 +50,7 @@ fn get_default_printer() -> Box<dyn DiagnosticReportPrinter + Send + Sync + 'sta
|
|||
atty::is(Stream::Stdout) && atty::is(Stream::Stderr) && !ci_info::is_ci()
|
||||
};
|
||||
if fancy {
|
||||
Box::new(GraphicalReportPrinter {
|
||||
theme: GraphicalTheme::default(),
|
||||
})
|
||||
Box::new(GraphicalReportPrinter::new())
|
||||
} else {
|
||||
Box::new(NarratableReportPrinter)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -76,6 +76,9 @@ impl NarratableReportPrinter {
|
|||
writeln!(f, "diagnostic help: {}", help)?;
|
||||
}
|
||||
writeln!(f, "diagnostic error code: {}", diagnostic.code())?;
|
||||
if let Some(url) = diagnostic.url() {
|
||||
writeln!(f, "For more details, see {}", url)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ impl GraphicalTheme {
|
|||
pub fn unicode() -> Self {
|
||||
Self {
|
||||
characters: ThemeCharacters::unicode(),
|
||||
styles: ThemeStyles::ansi(),
|
||||
styles: ThemeStyles::rgb(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -38,6 +38,11 @@ pub trait Diagnostic: std::error::Error {
|
|||
None
|
||||
}
|
||||
|
||||
/// URL to visit for a more details explanation/help about this Diagnostic.
|
||||
fn url<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Additional contextual snippets. This is typically used for adding
|
||||
/// marked-up source file output the way compilers often do.
|
||||
fn snippets(&self) -> Option<Box<dyn Iterator<Item = DiagnosticSnippet<'_>> + '_>> {
|
||||
|
|
|
|||
|
|
@ -293,3 +293,32 @@ fn test_snippet_enum() {
|
|||
),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn url_basic() {
|
||||
#[derive(Debug, Diagnostic, Error)]
|
||||
#[error("welp")]
|
||||
#[diagnostic(code(foo::bar::baz), url("https://example.com/foo/bar"))]
|
||||
struct Foo {}
|
||||
|
||||
assert_eq!(
|
||||
"https://example.com/foo/bar".to_string(),
|
||||
Foo {}.url().unwrap().to_string()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn url_docsrs() {
|
||||
#[derive(Debug, Diagnostic, Error)]
|
||||
#[error("welp")]
|
||||
#[diagnostic(code(foo::bar::baz), url(docsrs))]
|
||||
struct Foo {}
|
||||
|
||||
assert_eq!(
|
||||
format!(
|
||||
"https://docs.rs/miette/{}/miette/struct.Foo.html",
|
||||
env!("CARGO_PKG_VERSION")
|
||||
),
|
||||
Foo {}.url().unwrap().to_string()
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -407,3 +407,45 @@ fn multiple_multiline_highlights_overlapping_offsets() -> Result<(), MietteError
|
|||
assert_eq!("Error [oops::my::bad]: oops!\n\n[bad_file.rs] This is the part that broke:\n\n 1 │ source\n 2 │ text\n · ──┬─\n · ╰── this bit here\n 3 │ here\n\n﹦ try doing it better next time?\n".to_string(), out);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn url_links() -> Result<(), MietteError> {
|
||||
#[derive(Debug, Diagnostic, Error)]
|
||||
#[error("oops!")]
|
||||
#[diagnostic(
|
||||
code(oops::my::bad),
|
||||
help("try doing it better next time?"),
|
||||
url("https://example.com")
|
||||
)]
|
||||
struct MyBad;
|
||||
let err = MyBad;
|
||||
let out = fmt_report(err.into());
|
||||
println!("{}", out);
|
||||
assert!(out.contains("https://example.com"));
|
||||
assert!(out.contains("click for details"));
|
||||
assert!(out.contains("oops::my::bad"));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn disable_url_links() -> Result<(), MietteError> {
|
||||
#[derive(Debug, Diagnostic, Error)]
|
||||
#[error("oops!")]
|
||||
#[diagnostic(
|
||||
code(oops::my::bad),
|
||||
help("try doing it better next time?"),
|
||||
url("https://example.com")
|
||||
)]
|
||||
struct MyBad;
|
||||
let err = MyBad;
|
||||
let mut out = String::new();
|
||||
GraphicalReportPrinter::new_themed(GraphicalTheme::unicode_nocolor())
|
||||
.without_code_linking()
|
||||
.render_report(&mut out, &err)
|
||||
.unwrap();
|
||||
println!("{}", out);
|
||||
assert!(!out.contains("https://example.com"));
|
||||
assert!(!out.contains("click for details"));
|
||||
assert!(out.contains("oops::my::bad"));
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue