Skip to content

Commit

Permalink
feat(collection): add support for collection of labels (#341)
Browse files Browse the repository at this point in the history
Fixes: #315

Allow errors to have a number of labels determined at runtime.
An example of this is when the rust compiler labels all the arms of
a `match` expression when one of them has an incompatible type

To allow customization of the text for each label in a collection, add
support for using LabeledSpan in collections instead of just regular
spans
  • Loading branch information
Nahor authored Feb 16, 2024
1 parent 6f09250 commit 0306024
Show file tree
Hide file tree
Showing 5 changed files with 534 additions and 63 deletions.
52 changes: 52 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ libraries and such might not want.
- [... handler options](#-handler-options)
- [... dynamic diagnostics](#-dynamic-diagnostics)
- [... syntax highlighting](#-syntax-highlighting)
- [... collection of labels](#-collection-of-labels)
- [Acknowledgements](#acknowledgements)
- [License](#license)

Expand Down Expand Up @@ -671,6 +672,57 @@ trait to [`MietteHandlerOpts`] by calling the
[`with_syntax_highlighting`](MietteHandlerOpts::with_syntax_highlighting)
method. See the [`highlighters`] module docs for more details.

#### ... collection of labels

When the number of labels is unknown, you can use a collection of `SourceSpan`
(or any type convertible into `SourceSpan`). For this, add the `collection`
parameter to `label` and use any type than can be iterated over for the field.

```rust
#[derive(Debug, Diagnostic, Error)]
#[error("oops!")]
struct MyError {
#[label("main issue")]
primary_span: SourceSpan,

#[label(collection, "related to this")]
other_spans: Vec<Range<usize>>,
}

let report: miette::Report = MyError {
primary_span: (6, 9).into(),
other_spans: vec![19..26, 30..41],
}.into();

println!("{:?}", report.with_source_code("About something or another or yet another ...".to_string()));
```

A collection can also be of `LabeledSpan` if you want to have different text
for different labels. Labels with no text will use the one from the `label`
attribute

```rust
#[derive(Debug, Diagnostic, Error)]
#[error("oops!")]
struct MyError {
#[label("main issue")]
primary_span: SourceSpan,

#[label(collection, "related to this")]
other_spans: Vec<LabeledSpan>, // LabeledSpan
}

let report: miette::Report = MyError {
primary_span: (6, 9).into(),
other_spans: vec![
LabeledSpan::new(None, 19, 7), // Use default text `related to this`
LabeledSpan::new(Some("and also this".to_string()), 30, 11), // Use specific text
],
}.into();

println!("{:?}", report.with_source_code("About something or another or yet another ...".to_string()));
```

### MSRV

This crate requires rustc 1.70.0 or later.
Expand Down
211 changes: 148 additions & 63 deletions miette-derive/src/label.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,23 @@ use crate::{

pub struct Labels(Vec<Label>);

#[derive(PartialEq, Eq)]
enum LabelType {
Default,
Primary,
Collection,
}

struct Label {
label: Option<Display>,
ty: syn::Type,
span: syn::Member,
primary: bool,
lbl_ty: LabelType,
}

struct LabelAttr {
label: Option<Display>,
primary: bool,
lbl_ty: LabelType,
}

impl Parse for LabelAttr {
Expand All @@ -42,20 +49,24 @@ impl Parse for LabelAttr {
}
});
let la = input.lookahead1();
let (primary, label) = if la.peek(syn::token::Paren) {
let (lbl_ty, label) = if la.peek(syn::token::Paren) {
// #[label(primary?, "{}", x)]
let content;
parenthesized!(content in input);

let primary = if content.peek(syn::Ident) {
let ident: syn::Ident = content.parse()?;
if ident != "primary" {
return Err(syn::Error::new(input.span(), "Invalid argument to label() attribute. The argument must be a literal string or the keyword `primary`."));
let attr = match content.parse::<Option<syn::Ident>>()? {
Some(ident) if ident == "primary" => {
let _ = content.parse::<Token![,]>();
LabelType::Primary
}
let _ = content.parse::<Token![,]>();
true
} else {
false
Some(ident) if ident == "collection" => {
let _ = content.parse::<Token![,]>();
LabelType::Collection
}
Some(_) => {
return Err(syn::Error::new(input.span(), "Invalid argument to label() attribute. The argument must be a literal string or either the keyword `primary` or `collection`."));
}
_ => LabelType::Default,
};

if content.peek(syn::LitStr) {
Expand All @@ -70,27 +81,27 @@ impl Parse for LabelAttr {
args,
has_bonus_display: false,
};
(primary, Some(display))
} else if !primary {
return Err(syn::Error::new(input.span(), "Invalid argument to label() attribute. The argument must be a literal string or the keyword `primary`."));
(attr, Some(display))
} else if !content.is_empty() {
return Err(syn::Error::new(input.span(), "Invalid argument to label() attribute. The argument must be a literal string or either the keyword `primary` or `collection`."));
} else {
(primary, None)
(attr, None)
}
} else if la.peek(Token![=]) {
// #[label = "blabla"]
input.parse::<Token![=]>()?;
(
false,
LabelType::Default,
Some(Display {
fmt: input.parse()?,
args: TokenStream::new(),
has_bonus_display: false,
}),
)
} else {
(false, None)
(LabelType::Default, None)
};
Ok(LabelAttr { label, primary })
Ok(LabelAttr { label, lbl_ty })
}
}

Expand Down Expand Up @@ -119,10 +130,14 @@ impl Labels {
})
};
use quote::ToTokens;
let LabelAttr { label, primary } =
let LabelAttr { label, lbl_ty } =
syn::parse2::<LabelAttr>(attr.meta.to_token_stream())?;

if primary && labels.iter().any(|l: &Label| l.primary) {
if lbl_ty == LabelType::Primary
&& labels
.iter()
.any(|l: &Label| l.lbl_ty == LabelType::Primary)
{
return Err(syn::Error::new(
field.span(),
"Cannot have more than one primary label.",
Expand All @@ -133,7 +148,7 @@ impl Labels {
label,
span,
ty: field.ty.clone(),
primary,
lbl_ty,
});
}
}
Expand All @@ -147,46 +162,82 @@ impl Labels {

pub(crate) fn gen_struct(&self, fields: &syn::Fields) -> Option<TokenStream> {
let (display_pat, display_members) = display_pat_members(fields);
let labels = self.0.iter().map(|highlight| {
let labels_gen_var = quote! { labels };
let labels = self.0.iter().filter_map(|highlight| {
let Label {
span,
label,
ty,
primary,
lbl_ty,
} = highlight;
if *lbl_ty == LabelType::Collection {
return None;
}
let var = quote! { __miette_internal_var };
let ctor = if *primary {
let display = if let Some(display) = label {
let (fmt, args) = display.expand_shorthand_cloned(&display_members);
quote! { std::option::Option::Some(format!(#fmt #args)) }
} else {
quote! { std::option::Option::None }
};
let ctor = if *lbl_ty == LabelType::Primary {
quote! { miette::LabeledSpan::new_primary_with_span }
} else {
quote! { miette::LabeledSpan::new_with_span }
};
if let Some(display) = label {

Some(quote! {
miette::macro_helpers::OptionalWrapper::<#ty>::new().to_option(&self.#span)
.map(|#var| #ctor(
#display,
#var.clone(),
))
})
});
let collections = self.0.iter().filter_map(|label| {
let Label {
span,
label,
ty,
lbl_ty,
} = label;
if *lbl_ty != LabelType::Collection {
return None;
}
let display = if let Some(display) = label {
let (fmt, args) = display.expand_shorthand_cloned(&display_members);
quote! {
miette::macro_helpers::OptionalWrapper::<#ty>::new().to_option(&self.#span)
.map(|#var| #ctor(
std::option::Option::Some(format!(#fmt #args)),
#var.clone(),
))
}
quote! { std::option::Option::Some(format!(#fmt #args)) }
} else {
quote! {
miette::macro_helpers::OptionalWrapper::<#ty>::new().to_option(&self.#span)
.map(|#var| #ctor(
std::option::Option::None,
#var.clone(),
))
}
}
quote! { std::option::Option::None }
};
Some(quote! {
let display = #display;
#labels_gen_var.extend(self.#span.iter().map(|label| {
miette::macro_helpers::OptionalWrapper::<#ty>::new().to_option(label)
.map(|span| {
use miette::macro_helpers::{ToLabelSpanWrapper,ToLabeledSpan};
let mut labeled_span = ToLabelSpanWrapper::to_labeled_span(span.clone());
if #display.is_some() && labeled_span.label().is_none() {
labeled_span.set_label(#display)
}
labeled_span
})
}));
})
});

Some(quote! {
#[allow(unused_variables)]
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;
std::option::Option::Some(Box::new(vec![

let mut #labels_gen_var = vec![
#(#labels),*
].into_iter().filter(Option::is_some).map(Option::unwrap)))
];
#(#collections)*

std::option::Option::Some(Box::new(#labels_gen_var.into_iter().filter(Option::is_some).map(Option::unwrap)))
}
})
}
Expand All @@ -198,48 +249,82 @@ impl Labels {
|ident, fields, DiagnosticConcreteArgs { labels, .. }| {
let (display_pat, display_members) = display_pat_members(fields);
labels.as_ref().and_then(|labels| {
let variant_labels = labels.0.iter().map(|label| {
let Label { span, label, ty, primary } = label;
let labels_gen_var = quote! { labels };
let variant_labels = labels.0.iter().filter_map(|label| {
let Label { span, label, ty, lbl_ty } = label;
if *lbl_ty == LabelType::Collection {
return None;
}
let field = match &span {
syn::Member::Named(ident) => ident.clone(),
syn::Member::Unnamed(syn::Index { index, .. }) => {
format_ident!("_{}", index)
}
};
let var = quote! { __miette_internal_var };
let ctor = if *primary {
let display = if let Some(display) = label {
let (fmt, args) = display.expand_shorthand_cloned(&display_members);
quote! { std::option::Option::Some(format!(#fmt #args)) }
} else {
quote! { std::option::Option::None }
};
let ctor = if *lbl_ty == LabelType::Primary {
quote! { miette::LabeledSpan::new_primary_with_span }
} else {
quote! { miette::LabeledSpan::new_with_span }
};
if let Some(display) = label {
let (fmt, args) = display.expand_shorthand_cloned(&display_members);
quote! {
miette::macro_helpers::OptionalWrapper::<#ty>::new().to_option(#field)
.map(|#var| #ctor(
std::option::Option::Some(format!(#fmt #args)),
#var.clone(),
))

Some(quote! {
miette::macro_helpers::OptionalWrapper::<#ty>::new().to_option(#field)
.map(|#var| #ctor(
#display,
#var.clone(),
))
})
});
let collections = labels.0.iter().filter_map(|label| {
let Label { span, label, ty, lbl_ty } = label;
if *lbl_ty != LabelType::Collection {
return None;
}
let field = match &span {
syn::Member::Named(ident) => ident.clone(),
syn::Member::Unnamed(syn::Index { index, .. }) => {
format_ident!("_{}", index)
}
};
let display = if let Some(display) = label {
let (fmt, args) = display.expand_shorthand_cloned(&display_members);
quote! { std::option::Option::Some(format!(#fmt #args)) }
} else {
quote! {
miette::macro_helpers::OptionalWrapper::<#ty>::new().to_option(#field)
.map(|#var| #ctor(
std::option::Option::None,
#var.clone(),
))
}
}
quote! { std::option::Option::None }
};
Some(quote! {
let display = #display;
#labels_gen_var.extend(#field.iter().map(|label| {
miette::macro_helpers::OptionalWrapper::<#ty>::new().to_option(label)
.map(|span| {
use miette::macro_helpers::{ToLabelSpanWrapper,ToLabeledSpan};
let mut labeled_span = ToLabelSpanWrapper::to_labeled_span(span.clone());
if #display.is_some() && labeled_span.label().is_none() {
labeled_span.set_label(#display)
}
labeled_span
})
}));
})
});
let variant_name = ident.clone();
match &fields {
syn::Fields::Unit => None,
_ => Some(quote! {
Self::#variant_name #display_pat => {
use miette::macro_helpers::ToOption;
std::option::Option::Some(std::boxed::Box::new(vec![
let mut #labels_gen_var = vec![
#(#variant_labels),*
].into_iter().filter(Option::is_some).map(Option::unwrap)))
];
#(#collections)*
std::option::Option::Some(std::boxed::Box::new(#labels_gen_var.into_iter().filter(Option::is_some).map(Option::unwrap)))
}
}),
}
Expand Down
Loading

0 comments on commit 0306024

Please sign in to comment.