Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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 }
Expand All @@ -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",
Expand Down
53 changes: 53 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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

Expand Down
3 changes: 3 additions & 0 deletions miette-derive/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
18 changes: 15 additions & 3 deletions miette-derive/src/code.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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))
},
}
}
})
},
Expand Down
54 changes: 41 additions & 13 deletions miette-derive/src/diagnostic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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,
},
}

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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();
Expand All @@ -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));
}
}
Expand All @@ -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);
Expand Down Expand Up @@ -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,
)?;

Expand All @@ -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,
Expand All @@ -258,6 +275,7 @@ impl Diagnostic {
ident: input.ident,
generics: input.generics,
variants: vars,
bound_store,
}
}
syn::Data::Union(_) => {
Expand All @@ -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);
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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);
Expand Down
23 changes: 19 additions & 4 deletions miette-derive/src/diagnostic_source.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand All @@ -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") {
Expand All @@ -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)));
}
}
Expand Down
Loading