This commit is contained in:
Justus Bastian Flügel 2026-06-01 12:08:34 -07:00 committed by GitHub
commit 6eadab757f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 839 additions and 84 deletions

View File

@ -10,6 +10,7 @@ license = "Apache-2.0"
readme = "README.md"
edition = "2018"
rust-version = "1.82.0"
resolver = "2"
exclude = ["images/", "tests/", "miette-derive/"]
[dependencies]
@ -31,6 +32,8 @@ syntect = { version = "5.1.0", optional = true }
[dev-dependencies]
thiserror = "2.0.11"
semver = "1.0.21"
# (kind of) hacky workaround to enable additional feature flags in tests, requires resolver = "2"
miette = { path = ".", features = ["perfect-derive"] }
# Eyre devdeps
futures = { version = "0.3", default-features = false }
@ -47,6 +50,7 @@ strip-ansi-escapes = "0.2.0"
[features]
default = ["derive"]
derive = ["dep:miette-derive"]
perfect-derive = ["derive","miette-derive?/perfect-derive"]
no-format-args-capture = []
fancy-base = [
"dep:owo-colors",

View File

@ -53,6 +53,7 @@ diagnostic error code: ruget::api::bad_json
- [... syntax highlighting](#-syntax-highlighting)
- [... primary label](#-primary-label)
- [... collection of labels](#-collection-of-labels)
- [... with generic errors](#-with-generic-errors)
- [Acknowledgements](#acknowledgements)
- [License](#license)
@ -782,6 +783,58 @@ let report: miette::Report = MyError {
println!("{:?}", report.with_source_code("About something or another or yet another ...".to_string()));
```
#### ... with generic errors
When tring to build more complex error types, it can often be useful to use generics.
```rust
#[derive(Debug, Diagnostic, Error)]
enum MyError<T> {
#[error(transparent)]
#[diagnostic(transparent)]
Base(T),
#[error("Some other error occured")]
#[diagnostic(help = "See the manual.")]
OtherError
}
```
To enable this pattern, you can enable **the `perfect-derive` feature** on miette.
This will add trait bounds on generics in the `Diagnostic` implementation, depending on how
they are used inside the struct/enum.
This should work for all other attributes as well, like `#[label]` or `#[diagnostic_source]`.
<details>
<summary>
##### ⚠ Warning: (Small) Gotcha with the `#[related]` attribute
</summary>
Because of current lifetime constraints, only generic collection elements but not generic
collections are currently supported, meaning the following works:
```rust
#[derive(Debug, Diagnostic, Error)]
#[error("Some example error")]
struct MyError<T> {
#[related]
related_errors: Vec<T>
}
```
but the following does not:
```rust
#[derive(Debug, Diagnostic, Error)]
#[error("Some example error")]
struct MyError<T> {
// See the difference here?
// Note that the collection is general, and not
// the elements inside the vec. This is **not** supported.
#[related]
related_errors: T // <- here
}
```
</details>
### MSRV

View File

@ -10,6 +10,9 @@ repository = "https://github.com/zkat/miette"
[lib]
proc-macro = true
[features]
perfect-derive = ["syn/extra-traits"]
[dependencies]
proc-macro2 = "1.0.83"
quote = "1.0.35"

View File

@ -56,13 +56,25 @@ impl Code {
let code = &code.as_ref()?.0;
Some(match fields {
syn::Fields::Named(_) => {
quote! { Self::#ident { .. } => std::option::Option::Some(std::boxed::Box::new(#code)), }
quote! {
Self::#ident { .. } => {
std::option::Option::Some(std::boxed::Box::new(#code))
},
}
}
syn::Fields::Unnamed(_) => {
quote! { Self::#ident(..) => std::option::Option::Some(std::boxed::Box::new(#code)), }
quote! {
Self::#ident(..) => {
std::option::Option::Some(std::boxed::Box::new(#code))
},
}
}
syn::Fields::Unit => {
quote! { Self::#ident => std::option::Option::Some(std::boxed::Box::new(#code)), }
quote! {
Self::#ident => {
std::option::Option::Some(std::boxed::Box::new(#code))
},
}
}
})
},

View File

@ -11,6 +11,7 @@ use crate::label::Labels;
use crate::related::Related;
use crate::severity::Severity;
use crate::source_code::SourceCode;
use crate::trait_bounds::TypeParamBoundStore;
use crate::url::Url;
pub enum Diagnostic {
@ -19,11 +20,13 @@ pub enum Diagnostic {
ident: syn::Ident,
fields: syn::Fields,
args: DiagnosticDefArgs,
bound_store: TypeParamBoundStore,
},
Enum {
ident: syn::Ident,
generics: syn::Generics,
variants: Vec<DiagnosticDef>,
bound_store: TypeParamBoundStore,
},
}
@ -71,12 +74,15 @@ pub struct DiagnosticConcreteArgs {
}
impl DiagnosticConcreteArgs {
fn for_fields(fields: &syn::Fields) -> Result<Self, syn::Error> {
let labels = Labels::from_fields(fields)?;
let source_code = SourceCode::from_fields(fields)?;
let related = Related::from_fields(fields)?;
fn for_fields(
fields: &syn::Fields,
bounds_store: &mut TypeParamBoundStore,
) -> Result<Self, syn::Error> {
let labels = Labels::from_fields(fields, bounds_store)?;
let source_code = SourceCode::from_fields(fields, bounds_store)?;
let related = Related::from_fields(fields, bounds_store)?;
let help = Help::from_fields(fields)?;
let diagnostic_source = DiagnosticSource::from_fields(fields)?;
let diagnostic_source = DiagnosticSource::from_fields(fields, bounds_store)?;
Ok(DiagnosticConcreteArgs {
code: None,
help,
@ -156,6 +162,7 @@ impl DiagnosticDefArgs {
_ident: &syn::Ident,
fields: &syn::Fields,
attrs: &[&syn::Attribute],
bounds_store: &mut TypeParamBoundStore,
allow_transparent: bool,
) -> syn::Result<Self> {
let mut errors = Vec::new();
@ -166,7 +173,7 @@ impl DiagnosticDefArgs {
attrs[0].parse_args_with(Punctuated::<DiagnosticArg, Token![,]>::parse_terminated)
{
if matches!(args.first(), Some(DiagnosticArg::Transparent)) {
let forward = Forward::for_transparent_field(fields)?;
let forward = Forward::for_transparent_field(fields, bounds_store)?;
return Ok(Self::Transparent(forward));
}
}
@ -182,7 +189,7 @@ impl DiagnosticDefArgs {
matches!(d, DiagnosticArg::Transparent)
}
let mut concrete = DiagnosticConcreteArgs::for_fields(fields)?;
let mut concrete = DiagnosticConcreteArgs::for_fields(fields, bounds_store)?;
for attr in attrs {
let args =
attr.parse_args_with(Punctuated::<DiagnosticArg, Token![,]>::parse_terminated);
@ -226,10 +233,13 @@ impl Diagnostic {
.collect::<Vec<&syn::Attribute>>();
Ok(match input.data {
syn::Data::Struct(data_struct) => {
let mut bounds_store = TypeParamBoundStore::new(&input.generics);
let args = DiagnosticDefArgs::parse(
&input.ident,
&data_struct.fields,
&input_attrs,
&mut bounds_store,
true,
)?;
@ -238,16 +248,23 @@ impl Diagnostic {
ident: input.ident,
generics: input.generics,
args,
bound_store: bounds_store,
}
}
syn::Data::Enum(syn::DataEnum { variants, .. }) => {
let mut vars = Vec::new();
let mut bound_store = TypeParamBoundStore::new(&input.generics);
for var in variants {
let mut variant_attrs = input_attrs.clone();
variant_attrs
.extend(var.attrs.iter().filter(|x| x.path().is_ident("diagnostic")));
let args =
DiagnosticDefArgs::parse(&var.ident, &var.fields, &variant_attrs, true)?;
let args = DiagnosticDefArgs::parse(
&var.ident,
&var.fields,
&variant_attrs,
&mut bound_store,
true,
)?;
vars.push(DiagnosticDef {
ident: var.ident,
fields: var.fields,
@ -258,6 +275,7 @@ impl Diagnostic {
ident: input.ident,
generics: input.generics,
variants: vars,
bound_store,
}
}
syn::Data::Union(_) => {
@ -276,8 +294,11 @@ impl Diagnostic {
fields,
generics,
args,
bound_store,
} => {
let (impl_generics, ty_generics, where_clause) = &generics.split_for_impl();
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
let where_clause = bound_store.add_to_where_clause(where_clause);
match args {
DiagnosticDefArgs::Transparent(forward) => {
let code_method = forward.gen_struct_method(WhichFn::Code);
@ -291,7 +312,9 @@ impl Diagnostic {
forward.gen_struct_method(WhichFn::DiagnosticSource);
quote! {
impl #impl_generics miette::Diagnostic for #ident #ty_generics #where_clause {
impl #impl_generics miette::Diagnostic
for #ident #ty_generics
#where_clause {
#code_method
#help_method
#url_method
@ -351,7 +374,9 @@ impl Diagnostic {
.and_then(|x| x.gen_struct())
.or_else(|| forward(WhichFn::DiagnosticSource));
quote! {
impl #impl_generics miette::Diagnostic for #ident #ty_generics #where_clause {
impl #impl_generics miette::Diagnostic
for #ident #ty_generics
#where_clause {
#code_body
#help_body
#sev_body
@ -369,8 +394,11 @@ impl Diagnostic {
ident,
generics,
variants,
bound_store,
} => {
let (impl_generics, ty_generics, where_clause) = &generics.split_for_impl();
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
let where_clause = bound_store.add_to_where_clause(where_clause);
let code_body = Code::gen_enum(variants);
let help_body = Help::gen_enum(variants);
let sev_body = Severity::gen_enum(variants);

View File

@ -3,6 +3,7 @@ use quote::quote;
use syn::spanned::Spanned;
use crate::forward::WhichFn;
use crate::trait_bounds::TypeParamBoundStore;
use crate::{
diagnostic::{DiagnosticConcreteArgs, DiagnosticDef},
utils::{display_pat_members, gen_all_variants_with},
@ -11,17 +12,25 @@ use crate::{
pub struct DiagnosticSource(syn::Member);
impl DiagnosticSource {
pub(crate) fn from_fields(fields: &syn::Fields) -> syn::Result<Option<Self>> {
pub(crate) fn from_fields(
fields: &syn::Fields,
bounds_store: &mut TypeParamBoundStore,
) -> syn::Result<Option<Self>> {
match fields {
syn::Fields::Named(named) => Self::from_fields_vec(named.named.iter().collect()),
syn::Fields::Named(named) => {
Self::from_fields_vec(named.named.iter().collect(), bounds_store)
}
syn::Fields::Unnamed(unnamed) => {
Self::from_fields_vec(unnamed.unnamed.iter().collect())
Self::from_fields_vec(unnamed.unnamed.iter().collect(), bounds_store)
}
syn::Fields::Unit => Ok(None),
}
}
fn from_fields_vec(fields: Vec<&syn::Field>) -> syn::Result<Option<Self>> {
fn from_fields_vec(
fields: Vec<&syn::Field>,
bounds_store: &mut TypeParamBoundStore,
) -> syn::Result<Option<Self>> {
for (i, field) in fields.iter().enumerate() {
for attr in &field.attrs {
if attr.path().is_ident("diagnostic_source") {
@ -33,6 +42,12 @@ impl DiagnosticSource {
span: field.span(),
})
};
let ty = &field.ty;
bounds_store.add_where_predicate(
syn::parse_quote!(#ty: ::miette::Diagnostic + 'static),
);
return Ok(Some(DiagnosticSource(diagnostic_source)));
}
}

View File

@ -6,6 +6,8 @@ use syn::{
spanned::Spanned,
};
use crate::trait_bounds::TypeParamBoundStore;
pub enum Forward {
Unnamed(usize),
Named(syn::Ident),
@ -70,10 +72,14 @@ impl WhichFn {
fn severity(&self) -> std::option::Option<miette::Severity>
},
Self::Related => quote! {
fn related(&self) -> std::option::Option<std::boxed::Box<dyn std::iter::Iterator<Item = &dyn miette::Diagnostic> + '_>>
fn related(&self) -> std::option::Option<
std::boxed::Box<dyn std::iter::Iterator<Item = &dyn miette::Diagnostic> + '_>
>
},
Self::Labels => quote! {
fn labels(&self) -> std::option::Option<std::boxed::Box<dyn std::iter::Iterator<Item = miette::LabeledSpan> + '_>>
fn labels(&self) -> std::option::Option<
std::boxed::Box<dyn std::iter::Iterator<Item = miette::LabeledSpan> + '_>
>
},
Self::SourceCode => quote! {
fn source_code(&self) -> std::option::Option<&dyn miette::SourceCode>
@ -90,7 +96,10 @@ impl WhichFn {
}
impl Forward {
pub fn for_transparent_field(fields: &syn::Fields) -> syn::Result<Self> {
pub fn for_transparent_field(
fields: &syn::Fields,
bounds_store: &mut TypeParamBoundStore,
) -> syn::Result<Self> {
let make_err = || {
syn::Error::new(
fields.span(),
@ -108,12 +117,22 @@ impl Forward {
.ident
.clone()
.unwrap_or_else(|| format_ident!("unnamed"));
let ty = &field.ty;
bounds_store
.add_where_predicate(syn::parse_quote! {#ty: ::miette::Diagnostic + 'static});
Ok(Self::Named(field_name))
}
syn::Fields::Unnamed(unnamed) => {
if unnamed.unnamed.iter().len() != 1 {
let mut iter = unnamed.unnamed.iter();
let field = iter.next().ok_or_else(make_err)?;
if iter.next().is_some() {
return Err(make_err());
}
let ty = &field.ty;
bounds_store
.add_where_predicate(syn::parse_quote! {#ty: ::miette::Diagnostic + 'static});
Ok(Self::Unnamed(0))
}
_ => Err(syn::Error::new(

View File

@ -94,7 +94,9 @@ impl Help {
Help::Display(display) => {
let (fmt, args) = display.expand_shorthand_cloned(&display_members);
Some(quote! {
Self::#ident #display_pat => std::option::Option::Some(std::boxed::Box::new(format!(#fmt #args))),
Self::#ident #display_pat => {
std::option::Option::Some(std::boxed::Box::new(format!(#fmt #args)))
},
})
}
Help::Field(member, ty) => {
@ -123,7 +125,9 @@ impl Help {
Help::Display(display) => {
let (fmt, args) = display.expand_shorthand_cloned(&display_members);
Some(quote! {
fn help(&self) -> std::option::Option<std::boxed::Box<dyn std::fmt::Display + '_>> {
fn help(&self) -> std::option::Option<
std::boxed::Box<dyn std::fmt::Display + '_>
> {
#[allow(unused_variables, deprecated)]
let Self #display_pat = self;
std::option::Option::Some(std::boxed::Box::new(format!(#fmt #args)))
@ -133,7 +137,9 @@ impl Help {
Help::Field(member, ty) => {
let var = quote! { __miette_internal_var };
Some(quote! {
fn help(&self) -> std::option::Option<std::boxed::Box<dyn std::fmt::Display + '_>> {
fn help(&self) -> std::option::Option<
std::boxed::Box<dyn std::fmt::Display + '_>
> {
#[allow(unused_variables, deprecated)]
let Self #display_pat = self;
use miette::macro_helpers::ToOption;

View File

@ -4,14 +4,15 @@ use syn::{
parenthesized,
parse::{Parse, ParseStream},
spanned::Spanned,
Token,
Lifetime, Token,
};
use crate::{
diagnostic::{DiagnosticConcreteArgs, DiagnosticDef},
fmt::{self, Display},
forward::WhichFn,
utils::{display_pat_members, gen_all_variants_with},
trait_bounds::TypeParamBoundStore,
utils::{display_pat_members, extract_option, gen_all_variants_with},
};
pub struct Labels(Vec<Label>);
@ -101,22 +102,31 @@ impl Parse for LabelAttr {
} else {
(LabelType::Default, None)
};
Ok(LabelAttr { label, lbl_ty })
}
}
impl Labels {
pub fn from_fields(fields: &syn::Fields) -> syn::Result<Option<Self>> {
pub fn from_fields(
fields: &syn::Fields,
bounds_store: &mut TypeParamBoundStore,
) -> syn::Result<Option<Self>> {
match fields {
syn::Fields::Named(named) => Self::from_fields_vec(named.named.iter().collect()),
syn::Fields::Named(named) => {
Self::from_fields_vec(named.named.iter().collect(), bounds_store)
}
syn::Fields::Unnamed(unnamed) => {
Self::from_fields_vec(unnamed.unnamed.iter().collect())
Self::from_fields_vec(unnamed.unnamed.iter().collect(), bounds_store)
}
syn::Fields::Unit => Ok(None),
}
}
fn from_fields_vec(fields: Vec<&syn::Field>) -> syn::Result<Option<Self>> {
fn from_fields_vec(
fields: Vec<&syn::Field>,
bounds_store: &mut TypeParamBoundStore,
) -> syn::Result<Option<Self>> {
let mut labels = Vec::new();
for (i, field) in fields.iter().enumerate() {
for attr in &field.attrs {
@ -144,6 +154,36 @@ impl Labels {
));
}
match lbl_ty {
LabelType::Default | LabelType::Primary => {
let option_ty = extract_option(&field.ty).unwrap_or(&field.ty);
bounds_store.extend_where_predicates(syn::parse_quote!{
#option_ty: ::std::borrow::ToOwned,
<#option_ty as ::std::borrow::ToOwned>::Owned: ::std::convert::Into<::miette::SourceSpan>
});
}
LabelType::Collection => {
let ty = &field.ty;
let lt: Lifetime = syn::parse_quote!('__miette_internal_lt);
bounds_store.extend_where_predicates(syn::parse_quote!{
for<#lt> &#lt #ty: ::std::iter::IntoIterator,
for<#lt> <&#lt #ty as ::std::iter::IntoIterator>::Item: ::std::ops::Deref,
for<#lt> <
<&#lt #ty as ::std::iter::IntoIterator>::Item
as ::std::ops::Deref
>::Target : ::std::borrow::ToOwned,
for<#lt> <
<
<&#lt #ty as ::std::iter::IntoIterator>::Item
as ::std::ops::Deref
>::Target
as ::std::borrow::ToOwned
>::Owned: ::std::convert::Into<::miette::SourceSpan>
});
}
}
labels.push(Label {
label,
span,
@ -187,10 +227,15 @@ impl Labels {
Some(quote! {
miette::macro_helpers::OptionalWrapper::<#ty>::new().to_option(&self.#span)
.map(|#var| #ctor(
#display,
#var.clone(),
))
.as_ref()
.map(|#var| {
use ::std::borrow::ToOwned;
#ctor(
#display,
(*#var).to_owned(),
)
})
})
});
let collections_chain = self.0.iter().filter_map(|label| {
@ -209,12 +254,14 @@ impl Labels {
} else {
quote! { std::option::Option::None }
};
Some(quote! {
.chain({
let display = #display;
self.#span.iter().map(move |span| {
::std::iter::IntoIterator::into_iter(&self.#span).map(move |span| {
use miette::macro_helpers::{ToLabelSpanWrapper,ToLabeledSpan};
let mut labeled_span = ToLabelSpanWrapper::to_labeled_span(span.clone());
use ::std::borrow::ToOwned;
let mut labeled_span = ToLabelSpanWrapper::to_labeled_span(span.to_owned());
if display.is_some() && labeled_span.label().is_none() {
labeled_span.set_label(display.clone())
}
@ -226,7 +273,9 @@ impl Labels {
Some(quote! {
#[allow(unused_variables)]
fn labels(&self) -> std::option::Option<std::boxed::Box<dyn std::iter::Iterator<Item = miette::LabeledSpan> + '_>> {
fn labels(&self) -> std::option::Option<
std::boxed::Box<dyn std::iter::Iterator<Item = miette::LabeledSpan> + '_>
> {
use miette::macro_helpers::ToOption;
let Self #display_pat = self;
@ -236,7 +285,10 @@ impl Labels {
.into_iter()
#(#collections_chain)*;
std::option::Option::Some(Box::new(labels_iter.filter(Option::is_some).map(Option::unwrap)))
std::option::Option::Some(Box::new(
labels_iter
.filter_map(|x| x)
))
}
})
}
@ -249,7 +301,12 @@ impl Labels {
let (display_pat, display_members) = display_pat_members(fields);
labels.as_ref().and_then(|labels| {
let variant_labels = labels.0.iter().filter_map(|label| {
let Label { span, label, ty, lbl_ty } = label;
let Label {
span,
label,
ty,
lbl_ty,
} = label;
if *lbl_ty == LabelType::Collection {
return None;
}
@ -274,14 +331,24 @@ impl Labels {
Some(quote! {
miette::macro_helpers::OptionalWrapper::<#ty>::new().to_option(#field)
.map(|#var| #ctor(
#display,
#var.clone(),
))
.as_ref()
.map(|#var| {
use ::std::borrow::ToOwned;
#ctor(
#display,
(*#var).to_owned(),
)
})
})
});
let collections_chain = labels.0.iter().filter_map(|label| {
let Label { span, label, ty: _, lbl_ty } = label;
let Label {
span,
label,
ty: _,
lbl_ty,
} = label;
if *lbl_ty != LabelType::Collection {
return None;
}
@ -300,9 +367,12 @@ impl Labels {
Some(quote! {
.chain({
let display = #display;
#field.iter().map(move |span| {
::std::iter::IntoIterator::into_iter(#field).map(move |span| {
use miette::macro_helpers::{ToLabelSpanWrapper,ToLabeledSpan};
let mut labeled_span = ToLabelSpanWrapper::to_labeled_span(span.clone());
use ::std::borrow::ToOwned;
let mut labeled_span = ToLabelSpanWrapper::to_labeled_span(
span.to_owned()
);
if display.is_some() && labeled_span.label().is_none() {
labeled_span.set_label(display.clone());
}
@ -322,7 +392,10 @@ impl Labels {
]
.into_iter()
#(#collections_chain)*;
std::option::Option::Some(std::boxed::Box::new(labels_iter.filter(Option::is_some).map(Option::unwrap)))
std::option::Option::Some(std::boxed::Box::new(
labels_iter
.filter_map(|x| x)
))
}
}),
}

View File

@ -14,6 +14,7 @@ mod label;
mod related;
mod severity;
mod source_code;
mod trait_bounds;
mod url;
mod utils;

View File

@ -5,23 +5,32 @@ use syn::spanned::Spanned;
use crate::{
diagnostic::{DiagnosticConcreteArgs, DiagnosticDef},
forward::WhichFn,
trait_bounds::TypeParamBoundStore,
utils::{display_pat_members, gen_all_variants_with},
};
pub struct Related(syn::Member);
impl Related {
pub(crate) fn from_fields(fields: &syn::Fields) -> syn::Result<Option<Self>> {
pub(crate) fn from_fields(
fields: &syn::Fields,
bounds_store: &mut TypeParamBoundStore,
) -> syn::Result<Option<Self>> {
match fields {
syn::Fields::Named(named) => Self::from_fields_vec(named.named.iter().collect()),
syn::Fields::Named(named) => {
Self::from_fields_vec(named.named.iter().collect(), bounds_store)
}
syn::Fields::Unnamed(unnamed) => {
Self::from_fields_vec(unnamed.unnamed.iter().collect())
Self::from_fields_vec(unnamed.unnamed.iter().collect(), bounds_store)
}
syn::Fields::Unit => Ok(None),
}
}
fn from_fields_vec(fields: Vec<&syn::Field>) -> syn::Result<Option<Self>> {
fn from_fields_vec(
fields: Vec<&syn::Field>,
bounds_store: &mut TypeParamBoundStore,
) -> syn::Result<Option<Self>> {
for (i, field) in fields.iter().enumerate() {
for attr in &field.attrs {
if attr.path().is_ident("related") {
@ -33,6 +42,17 @@ impl Related {
span: field.span(),
})
};
// this is somewhat hacky and only supports concrete types for the #[related] type
// ittself but supports generics for the arguments, i.e. Vec<T> where T is generic.
//
// I think that this is a current limitation of the design of the Diagnostic trait,
// since we'd need bounds on the method and we can't do that (to refer to the lifetime)
//
// Someone smarter than me might be able to figure out a better solution (?)
let ty = &field.ty;
bounds_store.add_where_predicate(syn::parse_quote!(
<#ty as ::std::iter::IntoIterator>::Item: ::miette::Diagnostic + 'static
));
return Ok(Some(Related(related)));
}
}
@ -68,7 +88,9 @@ impl Related {
pub(crate) fn gen_struct(&self) -> Option<TokenStream> {
let rel = &self.0;
Some(quote! {
fn related<'a>(&'a self) -> std::option::Option<std::boxed::Box<dyn std::iter::Iterator<Item = &'a dyn miette::Diagnostic> + 'a>> {
fn related<'a>(&'a self) -> std::option::Option<
std::boxed::Box<dyn std::iter::Iterator<Item = &'a dyn miette::Diagnostic> + 'a>
> {
use ::core::borrow::Borrow;
std::option::Option::Some(std::boxed::Box::new(
self.#rel.iter().map(|x| -> &(dyn miette::Diagnostic) { &*x.borrow() })

View File

@ -71,9 +71,11 @@ impl Severity {
syn::Fields::Unnamed(_) => quote! { (..) },
syn::Fields::Unit => quote! {},
};
Some(
quote! { Self::#ident #fields => std::option::Option::Some(miette::Severity::#severity), },
)
Some(quote! {
Self::#ident #fields => {
std::option::Option::Some(miette::Severity::#severity)
},
})
},
)
}

View File

@ -5,7 +5,8 @@ use syn::spanned::Spanned;
use crate::{
diagnostic::{DiagnosticConcreteArgs, DiagnosticDef},
forward::WhichFn,
utils::{display_pat_members, gen_all_variants_with},
trait_bounds::TypeParamBoundStore,
utils::{display_pat_members, extract_option, gen_all_variants_with},
};
pub struct SourceCode {
@ -14,32 +15,33 @@ pub struct SourceCode {
}
impl SourceCode {
pub fn from_fields(fields: &syn::Fields) -> syn::Result<Option<Self>> {
pub fn from_fields(
fields: &syn::Fields,
bounds_store: &mut TypeParamBoundStore,
) -> syn::Result<Option<Self>> {
match fields {
syn::Fields::Named(named) => Self::from_fields_vec(named.named.iter().collect()),
syn::Fields::Named(named) => {
Self::from_fields_vec(named.named.iter().collect(), bounds_store)
}
syn::Fields::Unnamed(unnamed) => {
Self::from_fields_vec(unnamed.unnamed.iter().collect())
Self::from_fields_vec(unnamed.unnamed.iter().collect(), bounds_store)
}
syn::Fields::Unit => Ok(None),
}
}
fn from_fields_vec(fields: Vec<&syn::Field>) -> syn::Result<Option<Self>> {
fn from_fields_vec(
fields: Vec<&syn::Field>,
bounds_store: &mut TypeParamBoundStore,
) -> syn::Result<Option<Self>> {
for (i, field) in fields.iter().enumerate() {
for attr in &field.attrs {
if attr.path().is_ident("source_code") {
let is_option = if let syn::Type::Path(syn::TypePath {
path: syn::Path { segments, .. },
..
}) = &field.ty
{
segments
.last()
.map(|seg| seg.ident == "Option")
.unwrap_or(false)
} else {
false
};
let is_option = extract_option(&field.ty);
let code_ty = is_option.unwrap_or(&field.ty);
bounds_store
.add_where_predicate(syn::parse_quote!(#code_ty: ::miette::SourceCode));
let source_code = if let Some(ident) = field.ident.clone() {
syn::Member::Named(ident)
@ -51,7 +53,7 @@ impl SourceCode {
};
return Ok(Some(SourceCode {
source_code,
is_option,
is_option: is_option.is_some(),
}));
}
}

View File

@ -0,0 +1,24 @@
#![allow(dead_code)]
use syn::{punctuated::Punctuated, Generics, PredicateType, Token, WhereClause, WherePredicate};
// Mock for when perfect-derive is not enabled,
// this should be completely optimized away and enables
// easily switching on/off the perfect-derive feature without
// needing to modify any other code.
pub struct TypeParamBoundStore;
impl TypeParamBoundStore {
pub fn new(_: &Generics) -> Self {
Self
}
pub fn add_predicate(&mut self, _: PredicateType) {}
pub fn add_where_predicate(&mut self, _: WherePredicate) {}
pub fn extend_where_predicates(&mut self, _: Punctuated<WherePredicate, Token![,]>) {}
pub fn add_to_where_clause(&self, where_clause: Option<&WhereClause>) -> Option<WhereClause> {
where_clause.cloned()
}
}

View File

@ -0,0 +1,9 @@
#[cfg(not(feature = "perfect-derive"))]
mod mock_store;
#[cfg(not(feature = "perfect-derive"))]
pub use mock_store::TypeParamBoundStore;
#[cfg(feature = "perfect-derive")]
mod store;
#[cfg(feature = "perfect-derive")]
pub use store::TypeParamBoundStore;

View File

@ -0,0 +1,247 @@
use std::{
collections::{HashMap, HashSet, VecDeque},
iter::once,
};
use proc_macro2::Span;
use syn::{
punctuated::Punctuated, AngleBracketedGenericArguments, AssocType, BoundLifetimes,
GenericArgument, GenericParam, Generics, ParenthesizedGenericArguments, PathArguments,
PredicateType, ReturnType, Token, Type, TypeArray, TypeGroup, TypeParamBound, TypeParen,
TypePath, TypePtr, TypeReference, TypeSlice, TypeTuple, WhereClause, WherePredicate,
};
// Potential improvement, although idk if this actually ends up
// mattering (if it is a messurable improvement) is to switch this to something like FxHashMap
// like the rustc compiler uses internally, although we should benchmark this and can always do it later
// since it is easy enough to change.
#[cfg(feature = "perfect-derive")]
pub struct TypeParamBoundStore(HashMap<(Option<BoundLifetimes>, Type), HashSet<TypeParamBound>>);
#[cfg(feature = "perfect-derive")]
impl TypeParamBoundStore {
/// Creates a new TraitBoundStore, filling it with some generics which are used to heuristically remove trivial bounds.
///
/// Note that it is essential that all relevant generics are actually passed here, since if they aren't bounds which are required might be heuristically removed.
pub fn new(generics: &Generics) -> Self {
let hash_map = generics
.params
.iter()
.filter_map(|param| match param {
GenericParam::Type(ty) => Some(ty),
_ => None,
})
.map(|param| {
let ident = &param.ident;
Type::Path(syn::parse_quote!(#ident))
})
.map(|ty| ((None, ty), Default::default()))
.collect::<HashMap<_, _>>();
Self(hash_map)
}
/// Checks heuristically if `type` is using any generic type inside it.
///
/// This is guaranteed to never false-negative but might
/// false-positive if checking exhaustively would be expensive or
/// an unexpected case is encountered which this can't handle.
///
/// # Returns
/// Option with a simplified type if determined to be dependant, none otherwise
fn generic_usage_heuristics(&self, mut r#type: Type) -> Option<Type> {
// in theory we could skip all this logic and just allow trivial bounds but that would add redundant trait bounds
// to the derived impl - would be another choice to make. I choose to filter as much as possible so that we don't
// introduce unneccessary bounds.
// this reduces the type down as much as possible to remove unneeded groups.
let original_type = loop {
match r#type {
Type::Paren(TypeParen { elem, .. }) => r#type = *elem,
Type::Group(TypeGroup { elem, .. }) => r#type = *elem,
x => break x,
}
};
let mut depends_on_generic = false;
// max depth to check, after which we'll just add the (maybe redundant) bound anyways.
// this is a tradeoff between filtering speed and compiler speed so I'll keep it
// reasonably low for now, since I assume the compiler is better optimized for more complex
// checks.
let max_depth = 8;
let mut to_check_queue: VecDeque<(&Type, usize)> = VecDeque::new();
to_check_queue.push_back((&original_type, 0));
while !depends_on_generic {
// this needs to be like this cuz if-let-chains aren't supported yet
let Some((elem, current_depth)) = to_check_queue.pop_front() else {
break;
};
// if we exceed the max depth we just assume it depends on the generic and let the compiler check it
if current_depth > max_depth {
depends_on_generic = true;
break;
}
// the map contains types that we know depend on generics so we can just short circuit
//
// this is also the "bottom" check since we add the generics themselves to the map when
// constructing self
if self.0.contains_key(&(None, elem.clone())) {
depends_on_generic = true;
break;
}
// basically go through the type and add all referenced types inside it to the check queue
match elem {
Type::Group(_) => unreachable!("This is unwrapped above"),
Type::Paren(_) => unreachable!("This is unwrapped above"),
// function pointer's can never implement the required trait bounds anyways so we just accept the errors
Type::BareFn(_) => return None,
// impl trait types aren't allowed from struct/enum definitions anyways so we can just ignore them
Type::ImplTrait(_) => return None,
// infered types aren't allowed either
Type::Infer(_) => return None,
// macros are opaque to us and i don't really know how to properly implement this.
// we could in theory I think introduce a type alias and use that instead but honestly
// type macros are such a niche usecase especially in combination with a generic,
// I would say we should just recommend to implement
// the trait manually, as such we just accept the error if any occurs (this still allows using macros when they
// return concrete types which don't depend on any generic or when the generic doesn't affect the
// required trait implementation)
Type::Macro(_) => return None,
// trait objects which depend on a generic inside them seem like very much a hassle to implement so i'll ignore
// them for now, if the need arises we could support that in a future pr maybe?
//
// this again doesn't restrict the usage of trait objects which implement the required traits regardless of the generics.
Type::TraitObject(_) => return None,
// Well never is never and never never.
Type::Never(_) => return None,
Type::Array(TypeArray { elem, .. })
| Type::Ptr(TypePtr { elem, .. })
| Type::Reference(TypeReference { elem, .. })
| Type::Slice(TypeSlice { elem, .. }) => {
to_check_queue.push_back((&**elem, current_depth + 1));
}
Type::Path(TypePath { qself, path }) => {
if let Some(qself) = qself {
to_check_queue.push_back((&qself.ty, current_depth + 1));
}
for segment in &path.segments {
match &segment.arguments {
PathArguments::None => {}
PathArguments::AngleBracketed(AngleBracketedGenericArguments {
args,
..
}) => {
for argument in args {
match argument {
GenericArgument::Type(ty)
| GenericArgument::AssocType(AssocType { ty, .. }) => {
to_check_queue.push_back((ty, current_depth + 1));
}
_ => {}
}
}
}
PathArguments::Parenthesized(ParenthesizedGenericArguments {
inputs,
output,
..
}) => {
for inp in inputs {
to_check_queue.push_back((inp, current_depth + 1));
}
if let ReturnType::Type(_, ty) = output {
to_check_queue.push_back((ty, current_depth + 1));
}
}
}
}
}
Type::Tuple(TypeTuple { elems, .. }) => {
for elem in elems {
to_check_queue.push_back((elem, current_depth + 1));
}
}
// we can't really handle verbatim so we just assume it depends on the generics
Type::Verbatim(_) => depends_on_generic = true,
_ => depends_on_generic = true,
}
}
depends_on_generic.then_some(original_type)
}
pub fn add_predicate(
&mut self,
PredicateType {
lifetimes,
bounded_ty,
colon_token: _,
bounds,
}: PredicateType,
) {
let Some(bounded_ty) = self.generic_usage_heuristics(bounded_ty) else {
return;
};
self.0
.entry((lifetimes, bounded_ty))
.or_default()
.extend(bounds);
}
// Since syn for some reason doesn't implement `Parse` for `PredicateType`
// this method is meant for ease of use with `syn::parse_quote!`.
pub fn add_where_predicate(&mut self, predicate: WherePredicate) {
let WherePredicate::Type(ty) = predicate else {
unimplemented!("Only type predicates are supported");
};
self.add_predicate(ty);
}
pub fn extend_where_predicates(&mut self, predicates: Punctuated<WherePredicate, Token![,]>) {
for predicate in predicates {
self.add_where_predicate(predicate);
}
}
pub fn add_to_where_clause(&self, where_clause: Option<&WhereClause>) -> Option<WhereClause> {
let predicates = self
.0
.iter()
.filter(|(_, bounds)| !bounds.is_empty())
.map(|(a, b)| (a.clone(), b.clone()))
.map(|((lifetimes, bounded_ty), bounds)| {
WherePredicate::Type(PredicateType {
lifetimes,
bounded_ty,
colon_token: Token![:](Span::mixed_site()),
bounds: bounds.into_iter().collect(),
})
})
.peekable();
// de-duplicate elements newly added and within existing where clause
let predicates = predicates
.chain(
where_clause
.into_iter()
.flat_map(|where_clause| where_clause.predicates.clone()),
)
.chain(once(syn::parse_quote!(Self: ::std::error::Error)))
.collect::<HashSet<_>>();
Some(WhereClause {
where_token: Token![where](Span::mixed_site()),
predicates: predicates.into_iter().collect(),
})
}
}

View File

@ -96,7 +96,9 @@ impl Url {
}
};
Some(quote! {
Self::#ident #pat => std::option::Option::Some(std::boxed::Box::new(format!(#fmt #args))),
Self::#ident #pat => {
std::option::Option::Some(std::boxed::Box::new(format!(#fmt #args)))
},
})
},
)

View File

@ -1,6 +1,6 @@
use proc_macro2::TokenStream;
use quote::{format_ident, quote};
use syn::spanned::Spanned;
use syn::{spanned::Spanned, AngleBracketedGenericArguments, GenericArgument, PathArguments, Type};
use crate::{
diagnostic::{DiagnosticConcreteArgs, DiagnosticDef},
@ -104,3 +104,36 @@ impl Display {
(fmt, args)
}
}
/// Tries to extract the type of a (presumed) option type, returning the extracted type if it suceeded.
pub fn extract_option(r#type: &Type) -> Option<&Type> {
let syn::Type::Path(syn::TypePath {
path: syn::Path { segments, .. },
..
}) = r#type
else {
return None;
};
let last_segment = segments.last()?;
if last_segment.ident != "Option" {
return None;
}
let PathArguments::AngleBracketed(AngleBracketedGenericArguments { args, .. }) =
&last_segment.arguments
else {
return None;
};
if args.len() != 1 {
return None;
}
let Some(GenericArgument::Type(ty)) = args.first() else {
return None;
};
Some(ty)
}

View File

@ -1401,13 +1401,17 @@ impl Line {
/// text on this line
fn span_applies(&self, span: &FancySpan) -> bool {
let spanlen = if span.len() == 0 { 1 } else { span.len() };
// Span starts in this line
(span.offset() >= self.offset && span.offset() < self.offset + self.length)
// Span passes through this line
|| (span.offset() < self.offset && span.offset() + spanlen > self.offset + self.length) //todo
// Span ends on this line
|| (span.offset() + spanlen > self.offset && span.offset() + spanlen <= self.offset + self.length)
let span_starts_this_line =
span.offset() >= self.offset && span.offset() < self.offset + self.length;
let span_passes_through_this_line =
span.offset() < self.offset && span.offset() + spanlen > self.offset + self.length;
let span_ends_on_this_line = span.offset() + spanlen > self.offset
&& span.offset() + spanlen <= self.offset + self.length;
span_starts_this_line || span_passes_through_this_line || span_ends_on_this_line
}
/// Returns whether `span` should be visible on this line in the gutter (so this excludes spans

View File

@ -53,6 +53,7 @@
//! - [... syntax highlighting](#-syntax-highlighting)
//! - [... primary label](#-primary-label)
//! - [... collection of labels](#-collection-of-labels)
//! - [... with generic errors](#-with-generic-errors)
//! - [Acknowledgements](#acknowledgements)
//! - [License](#license)
//!
@ -783,6 +784,59 @@
//!
//! println!("{:?}", report.with_source_code("About something or another or yet another ...".to_string()));
//! ```
//! ### ... with generic errors
//!
//! When tring to build more complex error types, it can often be useful to use generics.
//!
//! ```rust,ignore
//! #[derive(Debug, Diagnostic, Error)]
//! enum MyError<T> {
//! #[error(transparent)]
//! #[diagnostic(transparent)]
//! Base(T),
//! #[error("Some other error occured")]
//! #[diagnostic(help = "See the manual.")]
//! OtherError
//! }
//! ```
//! To enable this pattern, you can enable **the `perfect-derive` feature** on miette.
//! This will add trait bounds on generics in the `Diagnostic` implementation, depending on how
//! they are used inside the struct/enum.
//!
//! This should work for all other attributes as well, like `#[label]` or `#[diagnostic_source]`.
//!
//! <details>
//! <summary>
//!
//! #### ⚠ Warning: (Small) Gotcha with the `#[related]` attribute
//!
//! </summary>
//!
//! Because of current lifetime constraints, only generic collection elements but not generic
//! collections are currently supported, meaning the following works:
//!
//! ```rust,ignore
//! #[derive(Debug, Diagnostic, Error)]
//! #[error("Some example error")]
//! struct MyError<T> {
//! #[related]
//! related_errors: Vec<T>
//! }
//! ```
//! but the following does not:
//! ```rust,ignore
//! #[derive(Debug, Diagnostic, Error)]
//! #[error("Some example error")]
//! struct MyError<T> {
//! // See the difference here?
//! // Note that the collection is general, and not
//! // the elements inside the vec. This is **not** supported.
//! #[related]
//! related_errors: T // <- here
//! }
//! ```
//! </details>
//!
//! ## MSRV
//!

View File

@ -217,6 +217,8 @@ fn fmt_help() {
#[diagnostic(code(foo::x), help("{} x {len} x {:?}", 1, "2"))]
Y { len: usize },
// for some reason rust analyzer has a false positive with the self = self in the format!
// here but it compiles and tests just fine. (02/02/2025)
#[diagnostic(code(foo::x), help("{} x {self:?} x {:?}", 1, "2"))]
Z,
}

View File

@ -145,3 +145,143 @@ fn attr_not_required() {
let expectation = LabeledSpan::new(Some("this bit here".into()), 9usize, 4usize);
assert_eq!(err_span, expectation);
}
// Tests for the feature = "perfect-derive".
fn assert_impl_diagnostic<T: Diagnostic>() {}
#[test]
fn transparent_generic() {
#[derive(Debug, Diagnostic, Error)]
enum Combined<T> {
#[error(transparent)]
#[diagnostic(transparent)]
Other(T),
#[error("foo")]
Custom,
}
std::hint::black_box(Combined::<i32>::Other(1));
std::hint::black_box(Combined::<i32>::Custom);
assert_impl_diagnostic::<Combined<miette::MietteDiagnostic>>();
}
#[test]
fn generic_label() {
#[derive(Debug, Diagnostic, Error)]
#[error("foo")]
struct Combined<T> {
#[label]
label: T,
}
assert_impl_diagnostic::<Combined<SourceSpan>>();
assert_impl_diagnostic::<Combined<(usize, usize)>>();
}
#[test]
fn generic_source_code() {
#[derive(Debug, Diagnostic, Error)]
#[error("foo")]
struct Combined<T> {
#[source_code]
label: T,
}
assert_impl_diagnostic::<Combined<String>>();
}
#[test]
fn generic_optional_source_code() {
#[derive(Debug, Diagnostic, Error)]
#[error("foo")]
struct Combined<T> {
#[source_code]
label: Option<T>,
}
assert_impl_diagnostic::<Combined<String>>();
}
#[test]
fn generic_label_primary() {
#[derive(Debug, Diagnostic, Error)]
#[error("foo")]
struct Combined<T> {
#[label(primary)]
label: T,
}
assert_impl_diagnostic::<Combined<SourceSpan>>();
assert_impl_diagnostic::<Combined<(usize, usize)>>();
}
#[test]
fn generic_label_collection() {
#[derive(Debug, Diagnostic, Error)]
#[error("foo")]
struct Combined<T> {
#[label(collection)]
label: Vec<T>,
}
assert_impl_diagnostic::<Combined<SourceSpan>>();
assert_impl_diagnostic::<Combined<(usize, usize)>>();
}
#[test]
fn generic_label_generic_collection() {
#[derive(Debug, Diagnostic, Error)]
#[error("foo")]
struct Combined<T> {
#[label(collection)]
label: T,
}
assert_impl_diagnostic::<Combined<Vec<SourceSpan>>>();
assert_impl_diagnostic::<Combined<Vec<(usize, usize)>>>();
}
#[test]
fn generic_related() {
#[derive(Debug, Diagnostic, Error)]
#[error("foo")]
struct Combined<T> {
#[related]
label: Vec<T>,
}
assert_impl_diagnostic::<Combined<miette::MietteDiagnostic>>();
}
#[test]
fn generic_diagnostic_source() {
#[derive(Debug, Diagnostic, Error)]
enum Combined<T> {
#[error(transparent)]
Other(#[diagnostic_source] T),
#[error("foo")]
Custom,
}
std::hint::black_box(Combined::<i32>::Other(1));
std::hint::black_box(Combined::<i32>::Custom);
assert_impl_diagnostic::<Combined<miette::MietteDiagnostic>>();
}
#[test]
fn generic_not_influencing_default() {
#[derive(Debug, Diagnostic, Error)]
enum Combined<T> {
#[error("bar")]
Other(T),
#[error("foo")]
Custom,
}
std::hint::black_box(Combined::<i32>::Other(1));
std::hint::black_box(Combined::<i32>::Custom);
assert_impl_diagnostic::<Combined<i32>>();
}